Using ARIA and jQuery to build an accessible tree – part 1
On jQuery.com’s tutorial page there is an article that outlines how to use jQuery to build a tree from unordered lists called “Turn Nested Lists Into a Collapsible Tree With jQuery.” This seems like a nice trick to have in the toolkit however the article did not take accessibility into account. Thus the tree does not work well for screen reader assistive technology. Using WAI-ARIA it is possible to improve the tree and make a more usable experience for users who are using an ARIA aware browser and assistive technology. This series seeks to demonstrate how to make an ARIA-enabled, interactive tree using the “Turn Nested Lists Into a Collapsible Tree With jQuery” article as a jumping off point.
As a reminder we will be marking up the unordered nested lists that follow this layout:
<li>item 1
<ul>
<li>item 1.1</li>
<li>item 1.2</li>
<li>item 1.3</li>
</ul>
</li>
<li>item 2
<ul>
<li>item 2.1
<ul>
<li>item 2.1.1</li>
<li>item 2.2.2</li>
</ul>
</li>
</ul>
</li>
<li>item 3</li>
</ul>
We need to add the ARIA markup that describes a tree and its states to assistive technology. For a description of how this markup works see my posting “ARIA Tree Markup.”
In addition, keyboard event handling is needed for screen reader and keyboard users.
Once again, from the “Turn Nested Lists Into a Collapsible Tree With jQuery” article we will start with the following code in the $(document).ready() function:
$(document).ready(function() {
// Find list items representing folders and
// style them accordingly. Also, turn them
// into links that can expand/collapse the
// tree leaf.
$(’li > ul’).each(function(i) {
// Find this list’s parent list item.
var parent_li = $(this).parent(’li’);
// Style the list item as folder.
parent_li.addClass(’folder’);
// Temporarily remove the list from the
// parent list item, wrap the remaining
// text in an anchor, then reattach it.
var sub_ul = $(this).remove();
parent_li.wrapInner(’<a/>’).find(’a').click(function() {
// Make the anchor toggle the leaf display.
sub_ul.toggle();
}); // close the add a click event handler
parent_li.append(sub_ul);
}); // closes the loop working on each branch
// Hide all lists except the outermost.
$(’ul ul’).hide();
});
Define the tree element
In this example the root of the tree is an unordered list. We can use jQuery to programmatically set the role of the list to be a tree. We can add this code to the end of the above function since it is not dependent on any earlier work. In fact we could add it to the beginning as well but let’s not complicate things.
var first_ul = $(”ul”).filter(”:first”);
first_ul.attr(”role”, “tree”);
The filter of “:first” causes jQuery to return the first unordered list in the document. If we had several lists which were being defined as separate trees, or if otherwise there were multiple lists and only one is to be a tree then a more elegant selector would need to be used. For example the selector could look for unordered lists that have a class of tree. Refining this is left as an exercise for the reader to do as is needed in any specific application. Back to adding ARIA…
The second line adds a “role” attribute using jQuery’s attr() function and sets its value to “tree.”
Defining tree items
Each list item will be given an ARIA role of treeitem. The following jQuery will loop through the DOM and give each list item a new “role” attribute and set it to “treeitem.” We can place the code at the end of the above function.
$(’ul li’).attr(”role”, “treeitem”);
Add state
A tree item has two possible states: expanded and collapsed. To tell which state should be conveyed to assistive technology ARIA defines the “aria-expanded” attribute. We need to add “aria-expanded” attributes to each tree item that has children.
In this example we note that by default only the root tree is being displayed and all children are being hidden (or collapsed). In addition, the example is already determining which tree items have children, so we just need to append to the end of the logic that is working with the branches.
The branches that need to have the “aria-expanded” attribute are represented in the parent_li variable. Add the attribute by placing this line just before the sub-list is appended back to the parent list item:
parent_li.attr(”aria-expanded”, “false”);
Naming the branches
Next a name for each branch should be defined to keep the browser from returning too long of a name to assistive technology. For discussion on why a branch with children needs to be specifically given a label, see the discussion on “aria-labelledby” in “ARIA Tree Markup.”
We will use the same technique of wrapping the text that should be used to label the branch in a span element. Currently the code is already determining the item’s text and wrapping it in an anchor tag so we can borrow on this work and wrap the text in a span before it gets wrapped with the anchor tag.
parent_li.wrapInner(”<span id=’a11yLabel”+treeitem_label_i + “‘/>”);
The line can be placed after the removal of the sub-list from the parent list item:
var sub_ul = $(this).remove();
The variable treeitem_label_i is just an index counter we increment by one with each iteration of the loop to create different ID’s for each item. We will have ID’s of “a11yLabel1″, “a11yLabel2″, etc.
Then set the “aria-labelledby” attribute on the parent list item to point to the span we just created:
parent_li.attr(”aria-labelledby”, “a11yLabel”+treeitem_label_i);
The tree itself should also be given a label so that it accurately conveys itself when the user tabs to it. If an element elsewhere on the page is the best label then use that. Otherwise we can use the first item in the tree. This will ensure that screen readers will have something to say when focus lands on the tree.
The variable first_ul created earlier points to the list that is defined as a tree that we need to label. Using the first span we just created we can set the label for the tree to the span by:
first_ul.attr(”aria-labelledby”, “a11yLabel1″);
Tabindex
The tree needs to receive focus when tabbed too. In order to do this a tabindex = 0 is given to the root list. In addition, to keep tab from going to the links in the tree tabindex = -1 is given to them. This way focus can be programmatically set to the elements, but tab will bypass them, giving the user a nice quick way to continue to other parts of the page. (The tree itself will be navigated by using the arrow keys once it has focus.)
Set the tree tabindex = 0 with this line:
first_ul.attr(”tabindex”, “0″);
And give the other list items and links a tabindex = -1:
$(’ul li’).attr(”tabindex”,”-1″);
$(’ul li a’).attr(”tabindex”, “-1″);
Putting it together
At this point we have the code we need to build ARIA into the default tree that is being created from the nested lists. When we combine the steps outlined to this point we end up with the following JavaScript.
// code
// the ready event runs after page is loaded
$(document).ready(function() {
// Find list items representing folders and
// style them accordingly. Also, turn them
// into links that can expand/collapse the
// tree leaf.
var treeitem_label_i = 0; // index for ARIA labels
$(’li > ul’).each(function(i) {
treeitem_label_i++;
// Find this list’s parent list item.
var parent_li = $(this).parent(’li’);
// Style the list item as folder.
parent_li.addClass(’folder’);
// Temporarily remove the list from the
// parent list item, wrap the remaining
// text in an span and a anchor, then reattach it.
var sub_ul = $(this).remove();
parent_li.wrapInner(”<span id=’a11yLabel”+treeitem_label_i + “‘/>”); // use this for ARIA label
parent_li.attr(”aria-labelledby”, “a11yLabel”+treeitem_label_i);
parent_li.wrapInner(’<a/>’).find(’a').click(function() {
// Make the anchor toggle the leaf display.
sub_ul.toggle();
}); // close the add a click event handler
parent_li.attr(”aria-expanded”, “false”); // adding state via ARIA
parent_li.append(sub_ul);
}); // closes the loop working on each branch
// Hide all lists except the outermost.
$(’ul ul’).hide();
// ARIA code
// set the outer list to be the tree
var first_ul = $(”ul”).filter(”:first”);
first_ul.attr(”role”, “tree”);
// name the tree using the first item in the tree (for some trees other text on page may be more suitable)
first_ul.attr(”aria-labelledby”, “a11yLabel1″);
// set tabindex
first_ul.attr(”tabindex”, “0″);
$(’ul li’).attr(”tabindex”,”-1″);
$(’ul li a’).attr(”tabindex”, “-1″);
// define treeitems
$(’ul li’).attr(”role”, “treeitem”);
});
In part two event handling will be discussed and added to the tree so that nodes can be expanded and collapsed. After all, what fun is a static tree? (Astute readers will note there already is some event handling provided but at this point it is mouse oriented. It needs to be device independent, or in JavaScript’s case handle both mouse and keyboard events.) In addition, when the state of a node changes, the ARIA needs to be updated to reflect the change. This also needs to be added to event handler logic.
References
- Turn Nested Lists Into a Collapsible Tree With jQuery
- An Introduction to jQuery - Part 1: The Client Side
- Working with jQuery, Part 2: Building tomorrow’s Web applications today
- ARIA Tree Markup