Other Entries

Log

Add Label Click Support to Safari

Safari’s lack of full support for the <label> element has always bothered me. You can’t click the label and focus on the associated form control, which is not only extremely convenient, but a great improvement to accessibility. It also seems like a simple thing to implement in the browser.

I’ve been playing around with LiveSearch, which dynamically adds events to form controls, and I realized that it would be trivial to dynamically add click support to labels. So here it is:


if (navigator.userAgent.indexOf("Safari") > 0) 
  {
  var labels = document.getElementsByTagName("label");
  for (i = 0; i < labels.length; i++)
    {
    labels[i].addEventListener("click", addLabelFocus, false);
    }
  }
function addLabelFocus()
  {
  var item = document.getElementById(this.getAttribute("for"));
  item.focus();
  if (item.getAttribute("type") == "checkbox")
    {
    if (!item["checked"])
      {
      item["checked"] = true;
      }
    else
      {
      item["checked"] = false;
      }
    }
  else if (item.getAttribute("type") == "radio")
    {
    var allRadios = document.getElementsByTagName("input");
    var radios = new Array();
    for (i = 0; i < allRadios.length; i++)
      {
      if (allRadios[i].getAttribute("name") == item.getAttribute("name"))
        {
        radios.push(allRadios[i]);
        }
      }
    for (i = 0; i < radios.length; i++)
      {
      if (radios[i]["checked"] && 
      radios[i].getAttribute("id") != item.getAttribute("id"))
        {
        radios[i]["checked"] = false;
        }
      }
    item["checked"] = true;
    }
  }

Add the first part to an onload block or do what I like to do: put it in some <script> tags just before you close your <body> tag. That way it gets executed after all the html loads, but you don’t have to wait for the rest of the elements on the page, like images, to load.

See it in action.

Let’s take a look at the code in detail:

if (navigator.userAgent.indexOf("Safari") > 0)
We’re looking for “Safari” in the user agent. If we don’t find Safari, forget it.

var labels = document.getElementsByTagName("label");
Grab all the label elements on the page and stick them in an array called “labels”.

for (i = 0; i < labels.length; i++)
{
labels[i].addEventListener("click", addLabelFocus, false);
}

Loop through all the items in the labels array and add an event listener. addEventListener takes three arguments: the event for which to listen, the name of the function to execute when the event is detected, and whether to use bubbling or capturing for that event (false for bubbling, true for capturing).

Event names differ between Safari, Gecko and Explorer. Since we’re only targeting Safari, we only have to use the event names that Safari recognizes. If we were targeting all browsers, we would need to branch at this point and use different event names. We want labels to listen for “click”.

We then pass our function name and tell the event to use bubbling, which basically means that the label element itself should respond to the click event. Along with the function, we implicitly pass a reference to the element to which the listener is being added. What this means is that we can use this to refer to the clicked element in our function.

function addLabelFocus()
Our function is what actually places focus on the form control. It also contains logic to manipulate the value of radio buttons and checkboxes.

var item = document.getElementById(this.getAttribute("for"));
item.focus();

The only slightly tricky thing here is getting the id of the form control on which to focus. We do that by looking at the “for” attribute of the label element, a reference to which was passed along with the function (this). We then call the getAttribute() method on it. Once we have the value of the “for” attribute, we pass it to document.getElementById and call the focus() method. Our form control has focus.

Next we look at the type attribute of the form control referenced by item and check the control if appropriate. Checkboxes are simpler, so they’re up first:

if (item.getAttribute("type") == "checkbox")
  {
  if (!item["checked"])
    {
    item["checked"] = true;
    }
  else
    {
    item["checked"] = false;
    }
  }

First, see if the type attribute is “checkbox”. If so, we’ll check the value of its member “checked”, which is a boolean (true/false). This is different from the attribute “checked”, which isn’t set when clicking or tabbing to the checkbox and pressing the <space> key. We then check to see if it’s false, if it is, set it to true. If not, set it to false.

else if (item.getAttribute("type") == "radio")
  {
  var allInputs = document.getElementsByTagName("input");
  var radios = new Array();
  for (i = 0; i < allInputs.length; i++)
    {
    if (allRadios[i].getAttribute("name") == item.getAttribute("name"))
      {
      radios.push(allRadios[i]);
      }
    }
  for (i = 0; i < radios.length; i++)
    {
    if (radios[i]["checked"] && 
    radios[i].getAttribute("id") != item.getAttribute("id"))
      {
      radios[i]["checked"] = false;
      }
    }
  item["checked"] = true;
  }

Radio buttons are a little more complicated. First thing we need to do is collect all the <input> elements in an array. Then we loop through the array and check to see if any have their name attribute set to the same name as our item. If so, they go into another array, “radios”. We now have an array containing only the radio buttons in the same set as item. This approach may be slow if the page has lots of form elements on it. You could also step up through the DOM to a parent element and grab a smaller set of inputs from there. <fieldset> seems like a great option here.

The whole reason going through this extra work is that we can’t assume that pressing one radio button with javascript will unpress all the others. So the next step is to loop through the radios array. We’re looking for checked members that aren’t item. We determine this by comparing each member’s id attribute to item’s id. Each time we find one, it gets unchecked (this should really only happen once, since only one radio button in a given set should be checked at any one time). When we’re done unchecking, we check the right one.

That’s it. I’ve tested it on all the form controls and it seems to exactly match Firefox’s behavior.

Some caveats:

Feel free to use it, modify it, whatever. Just don’t blame me if it doesn’t work…

12/19/04 10:18PM Accessibility and Usability Design Geekiness Macintosh

Comments

Dave Campbell:

Wow, what a great and useful script! This has always been one of my pet peeves with Safari and now I’ve got a fix for it… Thanks!

01/28/05 1:55PM

Dave Campbell:

This script is great and fixes one of my peeves with Safari. Thank you!

01/28/05 1:57PM

Gavin Montague:

Very useful! One thing I might suggest though is that it’s legal to have a label reference a child element rather e.g.


Your name:

I’m just writing off the top of my head here but…


}
function addLabelFocus()
{
var item = document.getElementById(this.getAttribute("for"));
if(!item)
{
kids = this.childNodes;
for(i=0; i<kids.length; i++)
{
if(kids[i].tagName == 'INPUT' || kids[i].tagName == 'SELECT' || kids[i].tagName == 'TEXTAREA')
{
item= kids[i];
break;
}
}
}

Should be a quick way of enabling that functionality.

03/02/05 7:51AM

Dan Dean:

Great script — I haven’t used it yet but do plan on trying it out. Is there any way to tie this into OBJECT detection instead of BROWSER detection? I’m assuming here that Safari will eventually support clickable labels and it would be great to bypass this script when that future version comes out.

07/23/05 3:18AM

Chris Cassell:

Dan—not that I’m aware of, and even if there were, I would hesitate to rely on its being dropped in a future version of Safari. It seems to me that browser detection is ok here, because other browsers support label clickability, while Safari doesn’t, although I suppose that the same issue affects KHTML too…

If anyone knows of an object supported only by KHTML/Safari, then the script could be modified to detect that object. If a new version of Safari is ever released that supports clickable labels, hopefully a new object will also be supported, too, otherwise it’s the same deal: browser detection.

07/23/05 9:46PM

Kevin Clark:

What you’re missing is the triggering of the checkbox/radio button’s onclick event:

if (item.getAttribute(“type”) == “checkbox”) {
if (!item[“checked”]) item[“checked”] = true;
else item[“checked”] = false;

if (item.onclick) item.onclick.call(item);
} else …

08/22/05 4:31PM

Chris Cassell:

Kevin—I’m not entirely sure what you mean, but I assume that you mean that checkboxes and radio buttons are tricky, and if so, you’re right, but the script works by changing the checked property rather than the checked attribute. That way, it actually changes the value, not just the display. It took a little bit of trial and error and inspecting of all the members of each checkbox/radio button set to figure that one out. This may be something that’s peculiar to Safari—I don’t remember checking other browsers or not—but in any case, we only need to target Safari.

08/26/05 9:05PM

Steve Brown:

@Chris re Kevin-
What he’s saying is that your script doesn’t trigger the onclick events for a checkbox/radio button that gets toggled by your script. For example:

<input type=”checkbox” onclick=”alert(‘I got changed’)” id=”check” /><label for=”check”>Try me</label>

When you click “Try me,” the browser should toggle the checkbox then do the alert. The script you wrote only toggles the checkbox but doesn’t trigger the other event. I found that this is also true for onchange events. The fix is simple: after toggle the check/radio, do the following:

if (item[‘onclick’]) {
item[‘onclick’].call(item);
}

Repeat for onchange events. Since this entry is the first hit when googling “safari label tag,” I thought I’d let you knwo even though this entry is old. ;-)

BTW, great script. Thanks for making Safari slightly more useful to the rest of the world.

01/24/07 1:02PM

Steve:

Wow! Thank you so much! I’ve never googled something, found the code, pasted it in, and been on my way with such efficiency. That works perfectly! Truly inspiring. You just saved me an hour.

08/07/07 11:31PM

Add a Comment

Have something to say about what I wrote here? Let’s hear it!

The Rules

Personal Information




Remember Information


Comment Preview