Accessible Tabs in the new Yahoo Homepage – recreated with YUI3 and WAI-ARIA

Ian Pouncey and a dog - photo by Marco van Hylckama Vlieg One of the things you might not be aware of when it comes to Yahoo's new shiny front page is that a lot of accessibility work went into it.

As always this is nothing that comes from high up but is the work of dedicated developers on the ground. one of these developers is Ian Pouncey and we are very happy to have him here as a guest blogger to tell us about the tricks he used to make the tabs inside the new Yahoo homepage available to all users - regardless of technical set up or physical ability. Take it away, Ian.

WAI-ARIA enabled tabs with YUI3

This is a follow on from the feature I wrote for the issue 195 of .net magazine as part of an article on the new Yahoo! front page. Don't worry if you don't have this magazine, I'll cover everything you need here.

I was intending to provide you with exactly the code you need, however some clever folks on the YUI team have beaten me to it! so instead I will be adding my own thoughts to their Accessible TabView implementation.

First of all you shouldn't try to write this kind of interactive functionality without a good framework. In this case our framework is the shiny and new YUI3, but if YUI is not your cup of tea I can highly recommend Dirk Ginader's blog post on jQuery Accessible Tabs.

Before we dive in to the code go and take a look at the example implementation. Try using it with only a keyboard - you will find that once one of the tabs has focus you can navigate to the other tabs using the left and right arrow keys and activate the selected tab by pressing return. This exactly mimics the interaction with a mouse or other pointer device and is the way tabs in operating systems work.

Six steps

The article in the .net magazine breaks down the creation of accessible tabs into 6 steps. Here's how they relate to the YUI code.

Step 1: Content and structure

Tabs are represented by a list of <a> elements, each one a 'tab', whose href attribute is set to the id of a <div> 'tab panel' which contains the content for that tab. Without JavaScript the tabs will function as in-page links. Add CSS for basic styling.

For the YUI version the code looks like:

<div id="tabview-1">
<ul>
<li class="yui-tab yui-tab-selected"><a href="#top-stories"><em>Top Stories</em></a></li>
<li class="yui-tab"><a href="#world-news"><em>World</em></a></li>
<li class="yui-tab"><a href="#entertainment-news"><em>Entertainment</em></a></li>
<li class="yui-tab"><a href="#sports-news"><em>Sports</em></a></li>
<li class="yui-tab"><a href="#technology-news"><em>Technology</em></a></li>
</ul>
<div>
<div class="yui-tabpanel yui-tabpanel-selected" id="top-stories">
<!-- Tab Panel Content Here  -->
</div>
<div class="yui-tabpanel" id="world-news">
<!-- Tab Panel Content Here  -->
</div>
<div class="yui-tabpanel" id="entertainment-news">
<!-- Tab Panel Content Here  -->
</div>
<div class="yui-tabpanel" id="sports-news">
<!-- Tab Panel Content Here  -->
</div>
<div class="yui-tabpanel" id="technology-news">
<!-- Tab Panel Content Here  -->
</div>
</div>
</div>

Quite straight forward, so let's move on.

Step 2: Adding JavaScript

Using JavaScript add a class of 'js' to the tabs container and a class of 'enabled' to the initially selected tab and tab panel. Use the 'js' class to hook styles to highlight the enabled tab and hide all but the enabled tab panel.

YUI does things slightly differently. First it dynamically loads any JavaScript and CSS assets it needs when you instantiate the code, and secondly it uses a specific class which applies only to the tab view, rather than a generic 'js' class.

tabView.addClass("yui-tabview");

It also uses a classes of 'yui-tab-selected' and 'yui-tabpanel-selected' rather than 'enabled'. Other than that, the logic is the same. The YUI solution simply provides you with more hooks to style and access the tabs.

Step 3. Add Javascript interaction

With JavaScript remove the 'enabled' class from the current tab and tab panel when a user activates the link with either keyboard or mouse, and add it to the selected tab and tab panel. Set tabindex of the tab panel to '0' and call focus() on it.

The code to do this in YUI is:

if (selectedTabAnchor) {
// remove classes from tab and tab panel
selectedTabAnchor.get("parentNode").removeClass("yui-tab-selected");
Y.one(("#" + panelMap[selectedTabAnchor.get("id")])).removeClass("yui-tabpanel-selected");
}

// assign reference to the tab that has been activated to selectedTabAnchor
selectedTabAnchor = this;

// add class to activated tab
selectedTabAnchor.get("parentNode").addClass("yui-tab-selected");

// get the tab panel based on the active tab (cached in panelMap)
selectedPanel = Y.one(("#" + panelMap[sID]));

// add class to activated tab panel
selectedPanel.addClass("yui-tabpanel-selected");

The YUI version doesn't automatically set focus when you activate a tab, but it has a few other tricks up its sleeve as we will see later. Setting and removing the tabindex of an element makes our tabs keyboard accessible to all JavaScript supporting browsers. With newer browsers however we can tap into the richness of ARIA to build accessible interfaces

Step 4. ARIA support and Step 5. More ARIA

Steps 4 and 5 are combined in this implementation.

Using JavaScript add a role attribute with a value of 'tablist' to the list containing the tabs, a role of 'presentation' to each of its list elements, a role of 'tab' to each of the tabs and a role of 'tabpanel' to each of the tab panels.

Again using JavaScript add an 'aria-labelledby' attribute to each of the tab panels. The value for each one should be the id attribute value of the associated tabs parent <li> element. Next add an 'aria-selected' attribute with value of 'true' to the currently enabled tab.

This is easy with YUI:

// loop through each tab
tabView.all(".yui-tab > a").each(function (anchor) {
// set role attributes on tab
anchor.set("role", "tab");
anchor.get("parentNode").set("role", "presentation");

// get panel based on the tab href attribute value
// (which is stored as sPanelID)
panel = Y.one(("#" + sPanelID));
// set role and aria-labelledby attributes on tab panel
panel.setAttrs({
role: "tabpanel",
"aria-labelledby": anchor.get("id")
});

});

The aria-selected attribute is not set at all. The nice thing about the YUI team is that they are always happy to receive suggestions for improvements, and will incorporate good ideas in to the code. Maybe in the future this will be added.

Step 6. ARIA interaction JavaScript

Write support for navigation of tabs using left and right arrow keys. Set the tabindex of the enabled tab to '0' and the other tabs to '-1'. The user will then be able to navigate the tabs as a single control instead of a list of links.

Keyboard navigation like this is something that can be tricky to write cross browser without a framework. I'm not going to try and explain the magic of the Y.Plugin.NodeFocusManager - it will make my brain hurt! The important thing to see is that the code is nice and compact. Really, if you are doing this without a framework you are doing it wrong.

tabView.plug(Y.Plugin.NodeFocusManager, {
descendants: ".yui-tab > a",
keys: { next: "down:39", // Right arrow
previous: "down:37" },  // Left arrow
focusClass: {
className: "yui-tab-focus",
fn: function (node) {
return node.get("parentNode");
}
},
circular: true
});

Further enhancements

In addition to the ARIA enhanced interactions in the six steps TabView gives you extra 'Accessibility Sugar'.

One of these sweeteners is the addition of instructional text for screen reader users:

tabHeading.set("innerHTML", (sInstructionalText + " <em>Press the enter key to load the content of each tab.</em>"));

Where possible you should show such text to all users - screen readers users are not the only ones who need to use a keyboard - but if you can't fit it in to your design this is better than nothing.

It also adds an additional mechanism for navigating the tab content in the form of previous and next buttons:

if (listitem.previous()) {
sHTML += '<button type="button" class="yui-tabview-prevbtn">Previous Tab Panel</button>';
}

if (listitem.next()) {
sHTML += '<button type="button" class="yui-tabview-nextbtn">Next Tab Panel</button>';
}

In conclusion I have to say that I am very impressed with what the YUI team have come up with. There may be a few things missing, but you could add these yourself with far less effort than would be required to write this from scratch.

Accessibility for complex interactions can be complicated, but with the right tools it becomes a whole lot easier.

Ian Pouncey
Web Developer, Yahoo! London