Fixing label in Safari

Published 17:46 on 30 August, 2007

Safari, the excellent WebKit-based browser from Apple, has long neglected to include functionality that activates a form element (such as an input, a textarea, a select etc.) when its relevant label is clicked.

Although this behaviour has been fixed in the beta of version 3, any users still browsing with version 2 or below still suffer this needless blight on web accessibility.

I’ve recently reworked the JavaScript used in Steve Marshall’s tasty channelled search solution, which requires the label to be clickable, as it hides the corresponding radio input. For this reason, I’ve written a quick bit of JavaScript to activate this behaviour.

The Requirements

As a Yahoo! developer, I’ve written this implementation using the YUI JavaScript library. There’s really no reason why you couldn’t re-jig it to work with any other JS library – including one you’ve written yourself.

The YUI objects we will require are: the YAHOO Global Object and the Event Utility. To include these files (in their minified form), insert the following lines of code in the head of your html document:

<script type="text/javascript" src="http://yui.yahooapis.com/2.3.0/build/yahoo/yahoo-min.js" ></script>
<script type="text/javascript" src="http://yui.yahooapis.com/2.3.0/build/event/event-min.js" ></script>

Alternatively, if you wish to include more of the YUI objects, you can use one of the following files:

<script type="text/javascript" src="http://yui.yahooapis.com/2.3.0/build/yahoo-dom-event/yahoo-dom-event.js"></script>

or

<script type="text/javascript" src="http://yui.yahooapis.com/2.3.0/build/utilities/utilities.js"></script>

The yahoo-dom-event.js file contains only the YAHOO Global Object, the DOM collection, and the Event Utility.

The utilities.js file includes the YAHOO Global Object, the DOM collection, the Event Utility, the Element utility, the Connection Manager, the Drag & Drog utility, and the Animation utility.

Personally, I tend to include the utilities.js file so that I have as much of the YUI functionality included as I may need. In the essence of avoiding script bloat though, I would recommend including only what you need.

The Method

We want to activate a form element when its corresponding label is clicked, so we need to bind an onclick event to any label elements within our page. To do this, we can either loop through all label elements in the DOM, and attach a separate event to each of them; or we can attach a single onclick event to the body of our page and use “event delegation” to discover our target element (the label) – a nice side effect of “event bubbling”.

If we click a label element in our page, the onclick event doesn’t occur on just that element alone; it bubbles up the DOM tree through each of the label element’s parents. This means an onclick event will fire on the form element, any container elements (div elements, for example), and finally the body element because, in effect, we’ve clicked all these elements as well.

Event delegation is the process of discovering the element at the bottom of the event bubbling tree – the “target” element. This is fairly straight forward and requires only a few lines of code, whether you’re using a JavaScript library or not.

Using event delegation will require less code, less memory, and will ultimately run much faster. This is because we do not need to discover all label elements in the page in one go. If we used the other method, our script would become slower and slower the more label elements were included in the page.

Once we have our target element, we can make sure it’s a label, obtain the form element it’s related to, and finally activate that element.

The Code

Firstly, we need to create our namespace. In this case, I’ve decided to create an extra layer entitled “browserfix” because we’re essentially fixing an oversight in the browser. Creating the extra layer means that I could possibly bind other browser fixes to the same namespace (although, if we were trying to improve IE, that namespace could get pretty big).

To create our namespace, we do the following:

NEF = window.NEF || {};
NEF.browserfix = window.NEF.browserfix || {};

This either reuses existing objects, or creates new ones using the object literal.

Next we bind a method to our browserfix object to encapsulate our code:

NEF.browserfix.Label = function() {
  …
}

The first thing our method needs to do is bind an event to the body element of our page. To do this, we use the addListener method of the YUI Event Utility:

YAHOO.util.Event.addListener(document.body, "click", function(e) {
  …
});

The addListener method expects three parameters by default; the element we wish to attach our event listener to (document.body), the event we’re listening for (“click” – we remove the “on” because only certain specific browsers require it and the addListener method will handle that for us), and the function we want to bind to that event (in this case, an anonymous – or “lambda” – function). We can also pass an arbitrary object to that function (should it require parameters), or scope the execution of our listener to a specific object. In this case, we’re not doing either of those.

Now we’ve bound a function to the event we need to populate it with some functionality. Incidentally, the e parameter that is passed to our function is an identifier of type “Event”. This is a special object that contains contextual information about the event. You can find out more about this in the W3C docs for the EventListener interface.

The first thing we need to do is work out which element we originally clicked; which is surprisingly straightforward. Using the getTarget method of the YUI Event Utility, we can get that element in a single line – in fact, without the YUI, this code is still surprisingly simple; the YUI just sorts out all those nasty cross-browser differences for us:

YAHOO.util.Event.addListener(document.body, "click", function(e) {
  var target = YAHOO.util.Event.getTarget(e);
  …
});

The getTarget method returns the element at the bottom of our event bubbling tree and requires only a single parameter; that special “Event” object I was talking about earlier. It needs this to identify which event we’re tracking. In effect, we’re passing the event from function to function.

If you’re interested in the code behind getTarget, I’d recommend delving into the YUI Event Utility code in the API documentation.

Now that we’ve obtained our clicked element, we need to check to see whether it’s a label; and if it isn’t, we return harmlessly – allowing default behaviour and other events to continue regardless. To do this, we’ll do a comparison on the tagName property of the element, which we’ll convert to uppercase to combat case sensitivity:

if (target.tagName && target.tagName.toUpperCase() !== "LABEL") {
  return;
}

To force a best practice of using explicitly defined label elements, we make the assumption that the htmlFor property will exist. If it doesn’t, the JavaScript will throw an error – which is a good thing. Obviously, we could check for the existence of the htmlFor property, or even write a DOM walking loop to handle implicitly defined label elements; but, to be honest, that would be slow and cumbersome, and would allow for a situation we don’t really want. This assumption allows us to return a new target element; that which is referred to in the for attribute of the opening label tag in our HTML:

target = document.getElementById(target.htmlFor);

Finally, we need to activate our new target element. For most elements, this will involve simply firing the focus method, but if our element is an input of type “radio” or “checkbox”, we’ll need to fire the click method:

if (target) {
  if (target.type) {
    switch (target.type) {
      case "radio":
      case "checkbox":
        target.click();
        break;
      default:
        target.focus();
        break;
    }
  } else {
    target.focus();
  }
}

First we check for the existence of the target element – if it doesn’t exist, we don’t want to attempt doing anything. Next we check for the existence of a type property – this will tell us whether we’re dealing with an input element or not. If we are dealing with an input element, we perform a switch statement on the type property to ascertain what sort of input we’re dealing with. If we’re dealing with a “radio” or a “checkbox”, we utilise the fall-through behaviour of the switch statement to fire the click method. Also, because we’re using fall-through, we make sure we supply the switch statement with a default clause that fires the focus method. Finally, if our target doesn’t have the type property, we also fire the focus method.

Lastly, we run our function when the DOM is in a usable state, using the YUI onDOMReady method:

YAHOO.util.Event.onDOMReady(NEF.browserfix.Label);

Rather than waiting for a window.onload event (which will have to wait for images to load etc.), this will trigger our function as soon as the DOM is loaded thus making sure the behaviour is available as soon as possible.

So here’s all that code in full:

NEF = window.NEF || {};
NEF.browserfix = window.NEF.browserfix || {};
NEF.browserfix.Label = function() {
  YAHOO.util.Event.addListener(document.body, "click", function(e) {
    var target = YAHOO.util.Event.getTarget(e);
    if (target.tagName && target.tagName.toUpperCase() !== "LABEL") {
      return;
    }
    target = document.getElementById(target.htmlFor);
    if (target) {
      if (target.type) {
        switch (target.type) {
          case "radio":
          case "checkbox":
            target.click();
            break;
          default:
            target.focus();
            break;
        }
      } else {
        target.focus();
      }
    }
  });
};

YAHOO.util.Event.onDOMReady(NEF.browserfix.Label);

And for anyone browsing with Safari, here’s a handy demo of NEF.browserfix.Label in action