A few weeks back I set myself the goal of digging back into the current ARIA implementations and specifications to keep some of my coding and development skills sharp. My general approach to such things is to setup little programming exercises for myself that I then have to complete. Classically this is like “make a TODO list.” In this case I went for: “build out an HTML element tree that has some reasonable semblance of native operating system tree functionality.” Thus started the journey to building out a well-structured, decomped, ARIA enabled HTML tree.
Before I go any further, if you want to ignore all the description on this post and just get straight to working tree code just check out the ARIA Tree Example in the SSB University Codepen. As someone that routinely skips past prose and goes for code I can appreciate that.
As with the vast majority of code written I started with some other peoples’ implementations, studied them, monkeyed around with them and then rewrote them to work the way I wanted them to work. To that end there are a few invaluable resources I would recommend you check out if you are working to implement an accessible tree control:
- WAI-ARIA Authoring Practices 1.1 – Tree View – The definitive document on how to implement tree views and a variety of other ARIA controls. Read this first, it basically has the entire specification for how a tree should work. Great work by the team that put this together.
- (Not so) Simple ARIA Tree Views and Screen Readers – A great overview of how screen readers and ARIA trees should work and are generally put together. This was the first long form post I read on this.
- Example Treeview – An example implementation of a treeview control from the OpenAjax Alliance. OpenAjax alliance has a bunch of great examples that are worth checking out.
You should note that in my current role as CEO that don’t actually let me production write code anymore. I would imagine the developers would probably think the desire of the CEO to write code as “cute”. So take the code I produce with a big grain of salt – this is principally for education purposes – to rework into production code you will want to lock a few aspect of it down.
In setting out to build a tree I wanted to ensure that it demonstrated the following accessibility functions:
- Accessible Names
- A label (accessible name) for the entire tree set via aria-labelledby
- A label (accessible name) for each node set via the text node for the element
- A role of tree (role=tree) for the overall tree container
- A role of tree item (role=treeitem) for each focusable node set on the LI item versus the span child (open to discussion)
- Set role=”group” for groups of child nodes (set on the UL container)
- A expanded / collapsed state for each non-leaf node (aria-expanded=”true” or aria-expanded=”false”)
- A hidden / not hidden state for each non-leaf node when collapsed or expanded (aria-hidden=”true” or aria-hidden=”false”)
- Information about where this element is in the list of elements (aria-setsize and aria-posinset) defined
- Reasonable mouse control for each node and expand and collapse
- A single tab stop for the entire tree
- Default keyboard focus going to the root node of the tree on receipt of tab focus
- Ability to focus on each node (tabindex=-1) but only the current focused node in the tab order (tabindex=0) at the default location of tab ordering in the page.
- Cache keyboard focus on the current node – basically you get this for free with the above
- Basic ability to handle keyboard navigation throughout the tree and expand collapse. This turned out to be the most work.
Tools of the Trade
A few key things you are going to need to get a good build, test cycle going:
- Web Browser – Also your call, pretty much all of them have good ARIA support these days. Baseline browser for ARIA I used is Firefox but pretty much all of them work. Bonus points if you are adventurous enough to use Safari on an iOS device emulator.
- Screen Reader – NVDA or JAWS. Most of you will probably use NVDA because it is free but if you are using production code note you really should test in multiple commercially used ATs and browser combos.
- Inspect – For the win. Inspect is a UI monitoring tool that is part of the Microsoft Windows Development SDK. It lets you see all the data exported by a UI element via the relevant accessibility APIs. This lets you determine – definitively – that your code is exporting the right accessibility data. This is critical for debugging accessibility implementations.
From a keyboard control perspective providing for full keyboard control of a tree is a pretty significant project. Take a minute and check out all the stuff you would need to implement per the ARIA Authoring Guide for Tree View to mimic a fully function operating system tree control. For my part I really wanted to focus on a minimal but still usable implementation and manage absolutely as little keyboard focus as possible. In the same vein that would punt as much of this as possible to the browser. To that end I kept my keyboard focus management implementation very simple – it doesn’t have all the bells and whistles a full implementation might have.
Cross Browser Keyboard Support
Support for keyboard bindings in browser and reaction to keyboard commands seem to vary widely. Full disclosure: I have done very limited cross browser testing for this code. It will probably, kind-of work in IE and Chrome but principal testing was done in Firefox. I did this because, as noted in a variety of places here, I am not trying to produce production code – just learn a little bit more about how things work in current browsers.
There are easily demonstrable variances in how different browsers are handling keyboard events and where and when the events are consumed in the propagation of those events up the object tree. The biggest lesson learned is that Chrome and IE seem to only support keydown and keyup events – which is correct per the specification. They don’t, however, provide the (admittedly legacy) keypress event type. So code like this:
spanElement.addEventListener("keypress", this, false);
will only work in Firefox. Instead you want to use something like this:
spanElement.addEventListener("keyup", this, false);
which will work reasonably well across all the various different browsers. It wouldn’t, though, handle the repeat case for holding down a key or key chords, etc.
For readability on my handleKeyPress function I initially was switching on e.key which is a string value of the pressed key. When I built out the actual code, though, I updated this to switch on e.keyCode. You will see some console output associated with that inline which will tell you what key is getting pressed.
In addition, I simply didn’t have the time to implement all the various different native code implementation items for a tree so I focused on the few items I cared about:
- Up Arrow and Down Arrow – Move focus between nodes. I didn’t respect visual v. hidden nodes with the down arrow and the default behavior will show you the next node if you press down.
- Left Arrow – Collapse the current node
- Right Arrow – Expand the current node
- End – Jump to the last node
- Home – Jump to the first node
- * – Expand all nodes
If you really want to go for bonus points you can see all the keyboard behaviors to support here. I also provided a pretty basic model for handling key events which you can expand by adding in more switch statements.
Finally you will note I am adding event handler listeners at the level of specific node elements and I am consuming those events (event.stopPropagation()) at those levels as well. I am a big fan of keeping your events limited to a specific scope and not trying to interpret keyboard controls at the level of the entire document. That noted, probably a better method is to really handle this all at the level of the tree and add the event handlers to the root list element. This would likely be a better implementation as the events would be listened for and consumed globally across the tree as the tree is responsible for managing focus. So tree level keyboard event handling would – potentially – be more appropriate, atomic point in the document tree. It would also be more difficult to write, result in a lot of messaging between objects and require a bunch of code re-write so I will leave that as an exercise to the reader.
Visual Focus Indicator
I found some decidedly odd behavior with the visual focus indicator in Firefox when playing with the implementation. Basically while the keyboard and programmatic focus would move – provable via monitoring with Inspect – the visual focus indicator wouldn’t update. Looking online I could not find any other people that were reporting similar issues so I chalked it up to being a weird corner case in focus management in the browser. That noted, it still needed a fix.
The quick and easy hack I found for it was to use either the outline or border CSS attribute and then update it when the element receives focus. You can basically use either of these and it will give you more controllable behavior.
this.spanElement.style.outline = "1px dashed black";
this.spanElement.style.border = "thick solid #0000FF";
The one thing to be aware of is if you do override the visual focus indication you also need to handle the case of tabbing into and out of the tree. You can do that by registering for the focus and blur events and then updating the focus rectangle accordingly. I did this by decomposing out the concepts of set and clear visual focus (setVisualFocus, clearVisualFocus) from the actually setting and clearing of focus on specific nodes.
A couple of random items came up during the creation of the keyboard aspects of this that warrant notes. The first was that you have to set tabindex on the node that has the ARIA role treeitem. Initially I had this set on the LI element but it needs to be set on the SPAN element which receives keyboard focus. Per Jon Avila and Bryan Garaventa on the team this is a pretty common mistake.
One other thing that turned out to be amazingly annoying is that the same key controls that control movement in the tree (up, down, left, right, home, end) also control scrolling and navigation in the document. I initially completely missed it but Jon Avila pointed this out when he was doing some testing on the page. The idea is when you press Up you not only move up a node but you also are causing the browser to scroll up. That’s not the ideal situation where we would want scrolling to follow the current focused element. The issue seemed odd since I was already calling
event.stopPropogation() on the relevant events. Two key things fixed this for me. First, associate your events with the
keydown event handler and not the
keyup event handler. It seems that scrolling is triggered in most browsers by key down. (This makes sense since if you hold a key down – see key repeat – you get continuous scrolling.) So do this:
spanElement.addEventListener("keydown", this, false);
don’t do this:
spanElement.addEventListener("keyup", this, false);
For your core keyboard event handling. Second, make sure that you call
event.preventDefault() to prevent the browser from taking its default action. I know, theoretically
event.stopPropogation() should have knocked it out. But I saw completely mixed cross browser behavior with just
stopPropogation() and I got nice, consistent behavior when I added a redundant call to
preventDefault(). So, yeah, do that.
The key thing that I found with ARIA is that all the ARIA attributes need to go on the same node in the tree. Credit where due Bryan Garaventa’s easy explanation: “Basically, all of the supporting attributes that relate to a Treeitem node, must be on the same element that has focus when focus is used to move between each child of that widget.” In the case of my implementation that meant that pretty much everything had to go on the span element since that was the UI workhorse. Setting aria-expanded on the parent LI, or the child UL, for an example, didn’t have the desired behavior. So something like this was bad:
<li class="expandedList" > <span style="outline: 0px none;" aria-selected="false" tabindex="-1" role="treeitem">Jungle Animals</span> <ul style="display: block;" role="group" aria-hidden="false" aria-expanded="true" > <li> <span style="outline: 0px none;" aria-selected="false" tabindex="-1" role="treeitem">Lions</span></li> …
Whereas this actually worked just fine:
<li class="expandedList"> <span aria-hidden="false" aria-expanded="true" style="outline: 0px none;" aria- selected="false" tabindex="-1" role="treeitem">Jungle Animals</span> <ul style="display: block;" role="group"> <li> <span style="outline: 0px none;" aria-selected="false" tabindex="-1" role="treeitem">Lions</span></li> …
So knock yourself – load up those span elements.
Other Implementation Notes
Not surprisingly HTML doesn’t really have a perfect model for implementing a multi-level tree. Basically you have to model one element – in my implementation a span – that actually handles the user interactions and then wrap this in a list item (LI) element that models the semantic structure of the tree. Further (significantly) complicating things is that the LI elements can have list children which are sub-nodes. The list children are modelled as UL elements in my tree.
I wanted to code to be stateful to the best extent possible. So when you moved focus away from the tree and came back your focus resumed at an obvious place. This is one of those seemingly odd UI tenants that, when not implemented, is glaringly obvious in functional testing. Still worth noting as a requirement.
I am a little old school – probably not in the good way – in that I don’t use all of the modern debugging tools available to me. As such you will see the really old fashion method of debugging via output to the console. On the plus side this gives you a bunch of easy items you can uncomment if you want to see the internal state of any of the items in line.
Outside of those items I am sure that there are dozens of other items that are completely wrong in this implementation so feel free to point those out and we can fix them up as time permits.