Creating accessible JavaScript menu systems – some basic steps that get forgotten
Tuesday, June 23rd, 2009 at 11:11 pmOK, so you want to write yet another collapsing menu/section/widget thingamabob. This is really easy and in the following few examples I want to point out some ideas that make it much easier for you.
The following is what we’re going to build – it doesn’t have any bells and whistles like smooth animation, random unicorns, swoosh noises or whatever else you might want to come up with, but it is a good solid base to work from.
Click the image to go to the live example.
The Markup
The first trick is to use easy to understand and highly styleable markup for your widget. You see a lot of widgets that use endless DIVs, SPANs, IDs on every element and classes on every sub-element. If you build a simple solution (and not a catch-all reusable widget that is part of a framework) none of that is needed. In this case the markup is the following:
In other words: a nested list with headings for the different sections. This makes sense without styling or scripting which is still and important point considering mobile devices or environments that have JavaScript turned off (and yes, they are not mythical but do exist).
Assistive access does not mean lack of JavaScript
The first mistake that people do is to think that this is all you need to be accessible – as users with assistive technology have no JavaScript, right? Wrong. Assistive technology is not a browser replacement but in most cases hooks into browser functionality and offers easier access. In other words, what the browser gets the assistive technology gets and you have to make sure it still makes sense without a mouse (to name just one example).
Thinking too complex
The next extra step people take is starting to loop through the menu construct to hide the elements with JavaScript. This is made much easier with clever libraries that don’t use the DOM to do that but piggy-back on the CSS selector engine, but for good old IE6 this is still not an option. And you don’t need to. In order to hide the nested lists in the above example all you need to do is to apply a class to the main list and leave the rest to CSS:
This means that in order to show any of the nested lists, all you need to to is to add a class called “open” to the parent list item. The CSS to undo the hiding is the following:
You can add the classes on the backend when you render the page – which makes a lot more sense to indicate a current open section of the page than any JavaScript analyzing of window.location
or other shenanigans. See the first stage demo. You can also define a “current” class which means you can style it differently and the script will simply quietly skip that section and not collapse it at all.
That is the beauty in leaving all the showing and hiding to the CSS. Of course you can’t animate in CSS (unless you use webkit), but you can make your animation precede the change in classes.
Hiding and showing
So the way to show and hide things is simply to add and remove classes. We need to use event handling to listen for click events to do that. Click events are the only safe ones as they fire with keyboard and mouse. The problem is that a header can be made clickable, but is not keyboard accessible. To work around this, let’s use DOM scripting to inject a button element into the headers. You can use CSS to make it look not like a button:
The second stage demo shows how this works and here’s the code.
(function(){
// get the menu and make sure it exists
var m = document.getElementById(‘menu’);
if(m){
// add a class to allow the CSS magic to happen
m.className = ‘js’;
// loop over all headers
var headers = m.getElementsByTagName(‘h3’);
for(var i=0;headers[i];i++){
// get the content of the current header
var content = headers[i].innerHTML;
// create a button, delete the content of the header
// replace it with the button and set the content to
// the cached header content
var a = document.createElement(‘button’);
headers[i].innerHTML = ‘’;
headers[i].appendChild(a);
a.innerHTML = content;
}
// apply a single click event to the menu
m.addEventListener(‘click’,function(e){
// find the event target and chec that it was a button
var t = e.target;
if(t.nodeName.toLowerCase()===’button’){
// get the LI the button is in
var mom = t.parentNode.parentNode;
// check if its class name is not content and
// remove the open class if it exists, else
// add an open class
if(mom.className!==’current’){
if(mom.className = 'open'){
mom.className = '';
} else {
mom.className = 'open';
}
}
// don't do the normal things buttons do
e.preventDefault();
}
},true);
}
})()
This code also takes advantage of event delegation – there is so no need to apply and event handler to each heading – even if that is really easy to do with a library and an each()
or batch()
command. Event handling is a great concept and the tricks you find when you bother to read the docs are staggering.
Hiding is not removing
This is where most menu scripts stop. Great stuff, it expands and collapses and everybody is happy. Unless you are an unhappy chap and you need to use a keyboard to access it – or an older mobile device. Try it out yourself – use the second example by tabbing from link to link. You’ll find that you have to tab through the invisible links to reach the next section to open or hide. That surely can’t be the idea, right?
The trick to work around is to change the tabindex
property of an element. A value of 1 will remove it from the tabbing order and setting it back to 0 will make them available again. So, to make keyboard access easier, let’s remove the tab order upfront and reset or remove it when we show and hide the menus. This is terribly annoying as we have to loop through the links. I hate loops, but browsers are not our friends.
(function(){
// get the menu and make sure it exists
var m = document.getElementById(‘menu’);
if(m){
// add a class to allow the CSS magic to happen
m.className = ‘js’;
// loop over all headers
var headers = m.getElementsByTagName(‘h3’);
for(var i=0;headers[i];i++){
// get the content of the current header
var content = headers[i].innerHTML;
// create a button, delete the content of the header
// replace it with the button and set the content to
// the cached header content
var a = document.createElement(‘button’);
headers[i].innerHTML = ‘’;
headers[i].appendChild(a);
a.innerHTML = content;
}
// loop over all nested lists
// and set the taborder of the nested links to -1 to
// remove them from the tab order
var uls = m.getElementsByTagName(‘ul’);
for(var i=0;uls[i];i++){
if(uls[i].parentNode.className!‘open’ &&
uls[i].parentNode.className!==’current’){
var as = uls[i].getElementsByTagName(‘a’);
tabOrder(as,-1);
}
}
// apply a single click event to the menu
m.addEventListener(‘click’,function(e){
// find the event target and chec that it was a button
var t = e.target;
if(t.nodeName.toLowerCase()===’button’){
// get the LI the button is in
var mom = t.parentNode.parentNode;
// check if its class name is not content and
// remove the open class if it exists, else
// add an open class. Also, remove or add the
// links to the tab order
if(mom.className!==’current’){
if(mom.className = 'open'){
mom.className = '';
tabOrder(as,-1);
} else {
mom.className = 'open';
tabOrder(as,-1);
}
}
// don't do the normal things buttons do
e.preventDefault();
}
},true);
}
// remove from or add elements to the tab order
function tabOrder(elms,index){
for(var i=0;elms[i];i++){
elms[i].tabIndex = index;
}
}
})()
The third demo does exactly that – use your keyboard and marvel at being able to tab from parent element to parent element when sub-sections are collapsed.
Fixing it for Internet Explorer 6 and 7bad old browsers
Now, this is all good and fine but of course we need to fix the code to work around the quirks of browsers that don’t understand the W3C event model or consider an element with a name
attribute the same as one with and id
. We could fork and fix in the code we have here, but frankly I am tired of this. People give us libraries to work around browser differences and to make our lives easier. This is why we can use YUI for example to make this work reliably cross-browser:
Good start
This is just a start to make this work. A real menu should also have support for all kind of keys that a menu like this in a real app gives us and notify screen readers of all the happenings. For this, we need ARIA. Right now, I’d be happy to get some comments here :)
Tags: accessibility, css, event delegation, javascript, keyboard access, menus, webdevtrick