Listbox and keyboard navigation

Associated themes :
  • Component

Listbox and keyboard navigation

Introduction

For this article, we start from the example of the listbox with checkboxes. The objective is to improve keyboard navigation in order to get close to the W3C / WAI guidelines.

ARIA patterns

The W3C maintains online specifications that describe how ARIA components should behave: WAI-ARIA Authoring Practices 1.1.

The specifications for the listbox tell us that:

  • The list must be focusable (tabindex="0").
  • The up / down arrows must be used to choose the selected item.
  • We must be able to select an item by quickly typing the first few characters (e.g. when pressing the “p” and “h” keys, the selection must move to “Photos”).
  • Shift + F10 must allow to open the focused item context menu if the item has one.
  • In case of a multiple-selection list, the Ctrl + a shortcut should select all items.
  • The Home and End keys must be able to respectively select the first and the last item of the list.

Example

Implementation

The functionality to automatically select the item by typing the first letters is not easy to implement. The following example uses XPath.

The first thing to do is listen to the keyboard once the focus is on the list. Depending on the key, it performs an action (selecting an item, moving the selected item…).


…
// Init
const listbox = document.querySelector("[role=listbox]");

// On keydown
listbox.onkeydown = function(e) {          
	var currentItem = this.querySelector("[aria-selected=true]"); 
  switch (e.keyCode) {
      case 9: // TAB
          break;
      case 36: // home
          …
          e.preventDefault();
          break;
      case 35: // end
          …
          e.preventDefault();
          break;
      case 38:  // Up arrow
          …
          e.preventDefault();
          break;
      …

Other keys that are not used to perform an action on the list, i.e. letters and digits, are saved to create the search string. When the user does not type anything for a few milliseconds (500 in our example), we look for a list item that begins with the typed string and select it.


…
case 65: // Ctrl + A
  if (e.ctrlKey) {
      …
  }
default:  // Search item starts with
  // Cancel current timer
  clearTimeout(timer);

  // Create search string
  searchString += e.key;
  var self = this;

  // Set a timer to search item after 500ms
  timer = setTimeout(function(){
      // Search item
      var xpath = "li/span[starts-with(translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), '" + searchString + "')]";
      var matchingElement = document.evaluate(xpath, self, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

      // Reset search string
      searchString = "";

      // If an item is found…
      if (matchingElement) {                            
	   currentItem.setAttribute("aria-selected", "false");
	   matchingElement.parentElement.setAttribute("aria-selected", "true");
	   matchingElement.parentElement.focus();
	   matchingElement.parentElement.classList.add("active");
      }     
  }, 500);

  e.preventDefault();

To search the item in the list, we use the following XPath query /li/span[starts-with(text(), "the string to search")]. In addition, in order to fix the character case issue, we use the translate function.

Finally, for compatibility issues with Internet Explorer, we use the Google XPath polyfill, you just need to include it and install it at page load using: wgxpath.install();.