Les listbox et la navigation au clavier

Thématiques associées :
  • Composant

Introduction

Pour cet article, nous repartons de l’exemple sur les listbox avec cases à cocher. L’objectif est d’améliorer la navigation au clavier afin de se rapprocher des spécifications design patterns du W3C/WAI.

Les patterns ARIA

Le W3C maintient en ligne des spécifications qui décrivent comment doivent se comporter les composants ARIA : WAI-ARIA Authoring Practices 1.1.

La spécification concernant les listbox nous apprend que :

  • La liste doit être focusable (tabindex="0").
  • Les flèches haut/bas doivent pouvoir servir à choisir l’élément sélectionné.
  • On doit pouvoir sélectionner un élément en tapant rapidement ses premiers caractères (ex. : si on appuie sur les touches p et h, la sélection doit se déplacer sur "Photos").
  • Les touches Shift + F10 doivent permettre d’ouvrir le menu contextuel de l’élément focusé si un menu contextuel est disponible.
  • Dans le cas d’une liste à sélection multiple, le raccourci Ctrl+a doit permettre de sélectionner tous les éléments.
  • Les touches Début et Fin doivent permettre de sélectionner les éléments depuis le début ou la fin de la liste.

Exemple

Implémentation

La fonctionnalité permettant de sélectionner automatiquement l’élément dont l’utilisateur tape les premières lettres n’est pas forcément simple à implémenter. Voici un exemple qui utilise XPath.

La première chose à faire est d’écouter le clavier lorsque le focus est positionné sur la liste. En fonction de la touche pressée, on effectue une action (déplacement de l’élément sélectionné, sélection d’un élément…).


…
// 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;
      …

Pour les autres touches qui ne servent pas à effectuer une action sur la liste, c’est à dire les lettres et les chiffres, on les mémorise pour former une chaîne de caractère. Lorsque l’utilisateur ne tape pas de touche pendant quelques millisecondes (500 dans notre exemple), on recherche si un élément de liste commence par la chaîne tapée et on sélectionne cet élément de liste.


…
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();

Pour rechercher l’élément dans la liste, nous utilisons la requête XPath suivante : /li/span[starts-with(text(), "la chaine à rechercher")]. De plus, pour s’affranchir de la casse des caractères, nous utilisons la fonction translate.

Enfin pour la compatibilité avec Internet Explorer, nous utilisons le polyfill Xpath de Google qu’il suffit d’inclure dans la page et de charger à l’aide de la ligne de code wgxpath.install(); au chargement de la page.