<select> Something New, Part 2

By Aaron Gustafson

When we last left our faux-<select>, we had a highly usable select replacement, but it wasn’t quite as accessible as it should be. In order to make the faux-<select> truly accessible, users need to be able to use it with more than just a mouse. Keyboard controls are the most widely supported mouse alternative, so this article will address supporting them within the framework of our faux-<select>.

Setting the stage

In order to best demonstrate this technique in action, we should start with a more complete form example. Our goal is to support keyboard control for our faux-<select>, which translates to the following specific goals:

  1. support tab-ing to/from the faux-<select> (using tabindex)
  2. allow users to “navigate” the faux-<select> using only the keyboard

It is important to note that, as of the writing of this article, Safari ignores tabindex on a <select> and, rather than looking for some hack-ish way to force Safari to do what it should be doing already, we will operate under the assumption that the developers will fix this issue shortly. It is also important to note that there are many differences in the ways browsers implement keyboard “navigation” within a <select>. Every one I’ve tested supports the up and down arrow keys for moving forward and backward in the list, but support varies for the following:

I am not going to bother chronicling the supported keystrokes for each browser, as that is beyond the scope of this article, but you can rest assured that this script will be able to tie into whatever keystrokes are normally supported in a user’s browser of choice.

A little tidying up

After finishing Part 1, I revisited some of the code to further optimize it. The major change was dumping

function selectReplacement(obj) {
  …
  // iterate through them to look for the selected one
  for (var i=0; i<opts.length; i++) {
    // create a variable to store the selected option
    var selectedOpt;
    // check for the selected option (defaults to the first option)
    if (opts[i].selected) { // this option is selected
      // store the option's position in the array
      selectedOpt = i;
      // leave the loop so we don't lose the value
      break;
    } else { // no selected option
             // the first one should show
      selectedOpt = 0;
    }
  }
  …
}

in favor of

function selectReplacement(obj) {
  …
  // grab the selected option or default to 0
  var selectedOpt = (!obj.selectedIndex) ? 0 : obj.selectedIndex;
  …
}

The change does not affect behavior in the least; it does, however, save some time and CPU cycles. For those unfamiliar with ternary operators, this line sets our variable selectedOpt equal to obj’s selectedIndex if it has one, or to 0 if it doesn’t.

The plan

Alright, before we begin the first new line of code, we need to construct a logical, goal-oriented plan for this project.

The first task is to allow focus to be brought to the faux-<select>. Now, since we want to have the faux-<select> respond to keystrokes while focused, it makes the most sense to tie its behavior to that of our replaced <select>. Therefore, we need some way to provide feedback to the user when they are “focused” on the faux-<select>. We can leave the tabindex assigned to the replaced <select> and trigger events with onfocus and onblur to change the visual appearance of the faux-<select>. This way, we are providing visual feedback to people who use keyboard navigation for forms while still allowing vision-impaired users to have the same experience they normally would from a <select> (label reading, etc.).

The second part of this project involves updating the faux-<select>’s displayed value (providing visual feedback) as a user “navigates” it with his/her keyboard. The best way to provide this feedback is to hook into the onchange event for the replaced <select>, so that it triggers the faux-<select> to update each time a user moves up or down in the list. There are some hurdles to overcome in performing this (due to browser differences) and those will be discussed further on in this article.

Taking care of tabindex

As our plan calls for the onfocus and onblur events of the replaced <select> to trigger a visual cue that the faux-<select> is active, we need to bring the replaced <select> back into the tab order. In the previous article, we hid the replaced <select> by styling it as display: none, which causes it to be skipped in the tab order. To put it back in the flow, we need it to be visible, but still hidden from view. There are numerous ways to accomplish this. For this article’s purposes, we’ll change the select.replaced rule to the following:

select.replaced {
  width: 1px;
  position: absolute;
  left: -999em;
}

which removes it from sight, but keeps it tab-able.

Now that we can tab to the replaced <select>, we can go about adding the behavior necessary to offer the user a visual cue that the faux-<select> is active. We can do this in our selectReplacement function by having the select add a class to the faux-<select> <ul> when the replaced select (obj) is focused, and resetting the class on the <ul> when the <select> is blurred:

function selectReplacement(obj) {
  …
  // add the selectFocused class to the faux-<select> 
  // when the real <select> is focused
  obj.onfocus = function() {
    ul.className += ' selectFocused';
  };
  // reset the class on the faux-<select> 
  // when the real <select> is blurred
  obj.onblur = function() {
    ul.className = 'selectReplacement';
  };
  …
}

In this particular example, we’ll make the visual cue a change of background for the faux-<select>:

ul.selectFocused {
  background-image: url(top-focus.jpg);
}

And there you have it, a faux-<select> which mimics focus when its associated <select> is tab-ed to.

Updating the faux-<select>

In the Part 1, our data flow was in one direction, from the faux-<select> to the replaced one; now, we are going in the other direction. As mentioned in our plan, we want to hook into the onchange event for the replaced <select> so we can grab the current value and update the faux-<select> to mirror it. This can be accomplished by adding this short event function to selectReplacement():

function selectReplacement(obj) {
  …
  // when the user changes the item within the <select>, 
  // update the faux-<select> accordingly
  obj.onchange = function() {
    // get the selectedIndex and...
    var idx = this.selectedIndex;
    // pass it to the selectMe function
    selectMe(ul.childNodes[idx]);
  };
  …
}

At this point things get a little hairy.

Aside: the <select> and onchange

I prefer to do much of my initial development testing in Firefox, primarily because I find the Web Developer Toolbar, ColorZilla, JavaScript Console and DOM Inspector to be indispensable tools. For the most part, Firefox (and Mozilla browsers in general) has great support for web standards and accessibility, but I did come across one oddity that threw me for a loop. It seems Mozilla has the only family of modern browsers (in my experience, at least) which does not register the onchange event when the keyboard is used to change the value of a <select>. These browsers only register an onchange event when focus is taken away from the <select>. This follows the guidelines in the HTML 4.01 spec:

The onchange event occurs when a control loses the input focus and its value has been modified since gaining focus. This attribute applies to the following elements: INPUT, SELECT, and TEXTAREA.

but it does not follow the accessibility recommendations set forth in UAAG 1.0 Test Suite for HTML 4.01 in Checkpoint 1.2, Provision 1:

Checkpoint 1.2 Activate event handlers (Priority 1)
Provision 1: Allow the user to activate, through keyboard input alone, all input device event handlers that are explicitly associated with the element designated by the content focus. Using the keyboard or an assistive technology that emulates the keyboard, select a value from the menu to trigger the onChange event.

Checkpoint 9.5, Provision 1 also touches on a related, though contradictory, premise:

Checkpoint 9.5 No events on focus change (Priority 2)
Provision 1: Allow configuration so that moving the content focus to or from an enabled element does not automatically activate any explicitly associated event handlers of any event type.

What’s a web developer to do? Well, it is my opinion that in the case of a conflict between the spec and accessibility, assuming a good case can be made for the accessibility side, accessibility should win.

That said, we need a way to make Mozilla browsers behave like the other browsers do, so we can tie into the onchange event of the original <select>. At first, I created a function to run when the onkeydown event took place on the <select>. It checked for certain keys (up, down, left, right, home, end, etc.) and then triggered the faux-<select> to change the displayed list item accordingly:

function selectReplacement(obj) {
  ...
  // when a key is pressed, get its keyCode and perform
  // the required actions to update the faux-<select>
  obj.onkeydown = function(event) {
    // grab the selectedIndex
    var idx = this.selectedIndex;
    // decide what do do based on the keyCode
    switch (event.keyCode) {
      // move to previous item
      case 37: // left
      case 38: // up
        if (this.prev >= 0) {
          selectMe(ul.childNodes[idx-1]);
        }
        break;
      // move to next item
      case 39: // right
      case 40: // down
        if (this.next < ul.childNodes.length) {
          selectMe(ul.childNodes[idx+1]);
        }
        break;
      // go to the beginning of the list
      case 33: // page up
      case 36: // home
        selectMe(ul.firstChild);
        break;
      // go to the end of the list
      case 34: // page down
      case 35: // end
        selectMe(ul.lastChild);
        break;
    } 
  };
  …
}

This worked fine in every browser but Opera, which for some reason kept jumping to every other item, up and down the list. Obviously, this won’t do, so I began looking for another way to make Mozilla behave the way we need it to (and, in my opinion, feel it should) and, after banging my head against the wall for nearly a day, I came up with this simple way of equalizing the event models:

function selectReplacement(obj) {
  …
  // equalize the event models of onkeypress and onchange
  // for the replaced <select>
  obj.onkeypress = obj.onchange;
  …
}

It’s my experience that the simplest solutions often aren’t the most obvious and forthcoming ones, at least not in my brain.

Conclusion

All told, making the faux-<select> more accessible is not all that taxing (apart from working around a few browser deficiencies/idiosyncrasies). It just goes to show that accessibility can be a quick win if you make smart decisions.

As mentioned in Part 1, we’re not stopping here: Part 3 will explore a faux-<select> with organizational <optgroup>s and Part 4 will examine a faux multiple-<select>s. We received an email asking for a Part 5, to address the creation of a combo box (a <select> which switches to an input when you select a value like “other”), and we’ve decided to grant that request, so there is even more to come in this series. I better get cracking.

Get the Source

You can download the files that go with this article in a single compressed archive: replaceSelect2_files.zip