Docs/Keyboard navigable JS widgets

From CodeTalks

Contents

Key-navigable custom JavaScript widgets

The problem: making JavaScript widgets support the keyboard

Web applications often use JavaScript to mimic desktop widgets like menus, tree views, rich text fields, and tab panels. Web developers are constantly innovating, and future applications will contain complex, interactive elements such as spreadsheets, calendars, organizational charts, and beyond. This document describes how to make these widgets accessible with the keyboard...

Here's a real example: many JavaScript tree views don't act like regular tree views with respect to keyboard access. A common mistake is to put each tree item in the tab order (often accomplished by making each menu item an <a> element). In fact, the tree should be in the tab order once, and arrow key navigation should be supported to move from tree item to tree item. This also true for other "grouped navigation" widgets such as menus, grids (spreadsheets), and tab panels.

For a list of typical widgets and the keyboard support that is normally expected for them, please see the DHTML Style Guide.

Solution 1: tabindex

research papers Originally introduced as part of HTML4, the tabindex attribute gives authors the means to define the order in which elements will receive focus when navigated by the user via the keyboard. The detailed behavior has now changed and is covered in the HTML5 draft specs. All major browsers now implement the changes!

The following table describes tabindex behavior in modern browsers:

tabindex attribute Focusable with mouse or JavaScript via element.focus() Tab navigable
not present Follows the default behavior of element (yes for form controls, links, etc.). Follows default behavior of element.
Negative (e.g. tabindex="-1") Yes No, author must focus it with focus() as a result of arrow or other key presses.
Zero (e.g. tabindex="0") Yes In tab order relative to element's position in document.
Positive (e.g. tabindex="33") Yes Tabindex value manually changes where this element is positioned in the tab order. These elements will be positioned in the tab order before elements that have tabindex="0" or that are naturally tabbable.

Simple controls

To make simple tab navigable widgets, the solution is to use tabindex="0" on the <div> or <span> representing it. Here's an example of a span-based checkbox using this technique (although the [:before]</code> rule for the checkbox image doesn't work in IE yet).

Grouping controls

For grouping widgets -- such as menus, tab panels, grids, or tree views -- the parent element should have tabindex="0", and each decendent choice/tab/cell/row should have tabindex="-1". An onkeydown event that watches for arrow keys can then use element.focus() to set the focus on the appropriate decendent widget and style it so that it appears focused. Here's an example of a WAI-ARIA tree view using this technique.

Authoring tips

Use onfocus to track the current focus

The events onfocus and onblur can now be used with every element. There is no standard DOM interface to get the current document focus, so if you want to track that you'll have to keep track of it in a JavaScript variable.

Don't assume that all focus changes will come via key and mouse events, because assistive technologies such as screen readers can set the focus to any focusable element, and that needs to be handled elegantly by the JavaScript widget.

Dynamically change focusability using the tabIndex property

You may want to do this if a custom control becomes disabled or enabled. Disabled controls should not be in the tab order. However, you can typically arrow to them if they're part of grouped navigation widget.

Use setTimeout with element.focus() to set focus

Do not use createEvent(), initEvent() and dispatchEvent to send focus to an element, because DOM focus events are considered informational only -- generated by the system after something is focused, but not actually used to set focus. The timeout is necessary in both IE and Firefox (not sure about others), to prevent scripts from doing strange unexpected things as the user clicks on buttons and other controls. The actual code to focus an element will look something like this:

window.setTimeout(function () { focusItem.focus(); },0);  // focusItem must be in scope

Don't use :focus or attribute selectors to style the focus (if you care about IE 7 and earlier)

You will not be able to use :focus or attribute selectors to style the focus if you want the focus to appear in IE (fixed in IE 8 standards mode). Set the style in an onfocus event handler. For example, in a <div> menu item's focus handler add this.style.backgroundColor = "gray";.

Always draw the focus for tabindex="-1" items and elements that receive focus programatically

IE will not automatically draw the focus outline for items that programatically receive focus. Choose between changing the background color via something like this.style.backgroundColor = "gray"; or add a dotted border via this.style.border = "1px dotted invert". In the dotted border case you will need to make sure those elements have an invisible 1px border to start with, so that the element doesn't grow when the border style is applied (borders take up space, and IE doesn't implement CSS outlines).

Use onkeydown to trap key events, not onkeypress

IE will not fire keypress events for non-alphanumeric keys.

Prevent used key events from performing browser functions

If a key such as an arrow key is used, prevent the browser from using the key to do something (such as scrolling) by using code like the following:

<span tabindex="-1" onkeydown="return handleKeyDown();">

If handleKeyDown() returns false, the event will be consumed, preventing the browser from performing any action based on the keystroke.

Use key event handlers to enable activation of the element

For every mouse event handler, a keyboard event handler is required. For example, if you have an onclick="doSomething()" you may also need onkeydown="return event.keyCode != 13 || doSomething();" in order to allow the Enter key to activate that element.

Use try/catch to avoid JavaScript errors

This system is not currently supported by shipping versions of Safari, but is now going in. Because some browsers older don't support new capabilities like the tabIndex property on all elements, use try/catch where appropriate.

Don't rely on consistent behavior for key repeat, at this point

Unfortunately onkeydown may or may not repeat depending on what browser and OS you're running on.

Solution 2: the aria-activedescendant solution

Why?

One disadvantage of Solution 1 above is that you have to put every element you want to focus in the tab order, and add a focus handler to each. You can't have one handler on the container, because focus doesn't bubble. Note, however that key events bubble.

How?

Via the aria-activedescendant attribute. This can be much simpler, because just the container widget (a list, tree or grid for example) needs to be added to the tab order (by adding the attribute: tabindex="0"). Then, point to the currently focused descendant with the aria-activedescendant="id_of_descendant]" attribute. The keydown handler on the container widget should change which descendant is pointed to, and ensure that the current item is styled (e.g. with a border or background color) to show it is the current one. See the source code of this " ARIA listbox example for a direct illustration of how this works.

Tips

want to spend a vacation with your family? treat their lifetime health like you've never done before, make then experience what is a real vacation place, then you might want to try this site.


scrollIntoView

Note that the use of this pattern requires the DHTML author to ensure that the current focused widget is scrolled into view. You should be able to use the element.scrollIntoView() function, but we recommend confirming this works for you in your target browsers using the quirksmode test.

Issues