Fixing label in Safari
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…