La Cascade

Rechercher

Créer des systèmes de menu accessibles

par Heydon Pickering, 1er mars 2018, accessibilite, html, javascript, article original paru le 23 novembre 2017 dans Smashing Magazine

Il existe de nombreux types de menus différents sur le Web. Créer des expériences inclusives est une question d'utilisation des bons modèles de menu aux bons endroits, avec le balisage et le comportement adéquats.


Note de l'éditeur : Cet article a été initialement publié dans l'ouvrage Inclusive Components.

La classification est difficile. Prenez les crabes, par exemple. Les bernard-l'hermite, les crabes en porcelaine et les limules ne sont pas — taxonomiquement parlant — de vrais crabes. Mais cela ne nous empêche pas d'utiliser le suffixe "crabe". La situation devient plus confuse lorsque, au fil du temps et grâce à un processus appelé carcinisation, les faux crabes évoluent pour ressembler davantage aux vrais crabes. C'est le cas du crabe royal, qui aurait été autrefois un bernard-l'hermite. Imaginez la taille de leur carapace !

En design, nous faisons souvent la même erreur de donner le même nom à des choses différentes. Elles semblent similaires, mais les apparences peuvent être trompeuses. Cela peut avoir un effet malheureux sur la clarté de votre bibliothèque de composants. En termes d'inclusion, cela peut également vous conduire à réaffecter un composant inapproprié sur le plan sémantique et comportemental. Les utilisateurs s'attendront à une chose et en obtiendront une autre.

Le terme "dropdown" nomme un exemple classique. Beaucoup de choses "tombent" (drop down) dans les interfaces, notamment l'ensemble des <options> d'un élément <select>, et la liste de liens révélée par JavaScript qui constitue un sous-menu de navigation. Même nom, mais des choses bien différentes. (Certaines personnes les appellent "pulldowns", bien sûr, mais n'en parlons pas).

Les dropdowns composés d'un ensemble d'options sont souvent appelés "menus", et je veux en parler ici. Nous allons concevoir un vrai menu, mais il y aura beaucoup à dire en cours de route sur les menus pas vraiment vrais.

Commençons par un quiz. La boîte de liens qui pend de la barre de navigation dans l'illustration est-elle un menu ?

Une barre de navigation avec un lien vers une boutique, sous lequel se trouve un ensemble de trois autres liens vers des costumes de chiens, des gaufriers et des orbes magiques respectivement.
Une barre de navigation avec un lien vers une boutique, sous lequel se trouve un ensemble de trois autres liens vers des costumes de chiens, des gaufriers et des orbes magiques respectivement.

La réponse est non, ce n'est pas un vrai menu.

Une convention de longue date veut que les schémas de navigation soient composés de listes de liens. Une convention presque aussi ancienne veut que la sous-navigation soit fournie sous forme de listes de liens imbriqués. Si je supprimais le CSS du composant illustré ci-dessus, je devrais voir quelque chose comme ci-dessous, mais coloré en bleu et en Times New Roman.

  • Home
  • About
  • Shop
    • Dog costumes
    • Waffle irons
    • Magical orbs
  • Contact

Sémantiquement parlant, les listes de liens imbriqués sont correctes dans ce contexte. Les systèmes de navigation sont en réalité des tables des matières et c'est ainsi que les tables des matières sont structurées. La seule chose qui nous fait vraiment penser à un "menu" est le style des listes imbriquées et la façon dont elles sont révélées au survol ou au focus.

C'est là que certains font fausse route et commencent à ajouter la sémantique WAI-ARIA : aria-haspopup="true", role="menu", role="menuitem" etc. Ces éléments ont leur place, comme nous allons le voir, mais pas ici. En voici les deux raisons :

  1. Les menus ARIA ne sont pas destinés à la navigation mais au comportement de l'application. Imaginez le système de menu d'une application de bureau.
  2. Le lien de niveau supérieur doit être utilisable comme un lien, ce qui signifie qu'il ne se comporte pas comme un bouton de menu.

Concernant le point (2) : Lorsque l'on parcourt une région de navigation comportant des sous-menus, on s'attend à ce que chaque sous-menu apparaisse au survol ou au focus du lien de "niveau supérieur" ("Shop" dans l'illustration). Cela permet à la fois de révéler le sous-menu et de placer ses propres liens dans l'ordre de focalisation. Avec un peu d'aide de JavaScript capturant les événements de focalisation et de floutage pour maintenir l'apparence des sous-menus pendant qu'ils sont nécessaires, une personne utilisant le clavier devrait être capable de parcourir chaque lien de chaque niveau, tour à tour.

Les boutons de menu qui prennent la propriété aria-haspopup="true" ne se comportent pas ainsi. Ils sont activés au clic et n'ont d'autre but que de révéler un menu secret.

À gauche : un bouton de menu intitulé 'menu' avec une icône de flèche pointant vers le bas et l'état aria-expanded = false. A droite : Le même bouton de menu mais avec le menu ouvert. Ce bouton est dans l'état aria-expanded = true.
À gauche : un bouton de menu intitulé 'menu' avec une icône de flèche pointant vers le bas et l'état aria-expanded = false. A droite : Le même bouton de menu mais avec le menu ouvert. Ce bouton est dans l'état aria-expanded = true.

Comme illustré, le fait que ce menu soit ouvert ou fermé doit être communiqué avec aria-expanded. Vous ne devez modifier cet état qu'au moment du clic, et non au moment du focus. Les utilisateurs ne s'attendent généralement pas à un changement d'état explicite lors d'un simple événement de focus. Dans notre système de navigation, l'état ne change pas vraiment ; c'est juste une astuce de style. D'un point de vue comportemental, nous pouvons avancer par tabulation dans la navigation comme si cette astuce d'affichage et de masquage ne se produisait pas.

Le problème des sous-menus de navigation

Les sous-menus de navigation (ou "dropdowns" pour certains) fonctionnent bien avec une souris ou un clavier, mais ils ne sont pas très performants au toucher. Lorsque vous appuyez pour la première fois sur le lien supérieur "Boutique" dans notre exemple, vous lui demandez à la fois d'ouvrir le sous-menu et de suivre le lien.

Il y a deux résolutions possibles ici :

  1. Empêcher le comportement par défaut des liens de haut niveau (e.preventDefault()) et écrire dans la sémantique et le comportement complets des menus WAI-ARIA.
  2. S'assurer que chaque page de destination de haut niveau possède une table des matières, comme alternative au sous-menu.

(1) n'est pas satisfaisant car, comme je l'ai noté précédemment, ces types de sémantique et de comportements ne sont pas attendus dans ce contexte, où les liens sont les contrôles du sujet. De plus, les utilisateurs ne pourraient plus naviguer vers une page de niveau supérieur, si elle existe.

Note : quels appareils sont des appareils tactiles ?

Il est tentant de penser "ce n'est pas une super solution, mais je ne l'ajouterai que pour les interfaces tactiles". Le problème est le suivant : comment détecter si un appareil a un écran tactile ?

Vous ne devriez certainement pas assimiler "petit écran" à "activé par le toucher". Ayant travaillé dans le même bureau que des personnes fabriquant des écrans tactiles pour des musées, je peux vous assurer que certains des plus grands écrans qui existent sont des écrans tactiles. Les ordinateurs portables à double clavier et à entrée tactile sont également de plus en plus nombreux.

De même, beaucoup de petits appareils, mais pas tous, sont des appareils tactiles. Dans le domaine de la conception inclusive, vous ne pouvez pas vous permettre de faire des suppositions.

La résolution (2) est plus inclusive et plus robuste dans la mesure où elle fournit un "repli" pour les utilisateurs de toutes les entrées. Mais les guillemets autour du terme de repli sont délibérés car je pense que les tables de contenu dans les pages sont un moyen supérieur de fournir la navigation.

L'équipe primée des Services numériques du gouvernement semble être d'accord. Vous les avez peut-être aussi vus sur Wikipédia.

Les tables de contenu de Gov.uk sont minimales avec des tirets comme styles de liste. Wikipedia propose une boîte grise bordée d'éléments numérotés. Les deux sont étiquetés contenus.
Les tables de contenu de Gov.uk sont minimales avec des tirets comme styles de liste. Wikipedia propose une boîte grise bordée d'éléments numérotés. Les deux sont des contenus étiquetés.

Tables des matières

Les tables des matières constituent une navigation pour des pages ou des sections de pages connexes et doivent être sémantiquement similaires aux régions de navigation du site principal, en utilisant un élément <nav>, une liste et un mécanisme d'étiquetage de groupe.

<nav aria-labelledby="sections-heading">
  <h2 id="sections-heading">Produits</h2>
  <ul>
    <li><a href="/produits/costumes-de-chien">Costumes-de-chien</a></li>
    <li><a href="/produits/fers à gaufrer">Fers à gaufrer</a></li>
    <li><a href="/produits/magical-orbs">Magical orbs</a></li>
  </ul>
</nav>
<!-- chaque section, dans l'ordre, ici -->

Notes

  • Dans cet exemple, nous imaginons que chaque section est sa propre page, comme elle l'aurait été dans le sous-menu déroulant.
  • Il est important que chacune de ces pages "Boutique" ait la même structure, avec cette table des matières "Produits" présente au même endroit. La cohérence favorise la compréhension.
  • La liste regroupe les articles et les énumère en sortie de technologie d'assistance, par exemple via la voix synthétique d'un lecteur d'écran.
  • Le <nav> est étiqueté de manière récursive par le titre à l'aide de aria-labelledby. Cela signifie que "navigation produits" sera annoncé dans la plupart des lecteurs d'écran lorsqu'on entre dans la région par la tabulation. Cela signifie également que la "navigation des produits" sera détaillée dans les interfaces d'éléments des lecteurs d'écran, à partir desquels les utilisateurs peuvent naviguer directement vers les régions.

Tout sur une seule page

Si vous pouvez faire tenir toutes les sections sur une seule page sans qu'elle devienne trop longue et difficile à faire défiler, c'est encore mieux. Il suffit de créer un lien vers l'identifiant de chaque section. Par exemple, href="#waffle-irons" devrait pointer vers id="waffle-irons".

<nav aria-labelledby="sections-heading">
    <h2 id="sections-heading">Produits</h2>
    <ul>
        <li><a href="#dog-costumes">Costumes pour chiens</a></li>
        <li><a href=#waffle-irons">Fers à gaufres</a></li>
        <li><a href="#magical-orbs">Les orbes magiques</a></li>
    </ul>
</nav>

<!-- section costumes de chiens ici -->
<section id="#waffle-irons" tabindex="-1">
<h2>Fers à gaufres</h2>
</section>
<!-- section orbes magiques ici -->

(Remarque : certains navigateurs sont peu doués pour envoyer réellement le focus sur les fragments de page liés. Placer tabindex="-1" sur le fragment cible résout ce problème).

Lorsqu'un site a beaucoup de contenu, une architecture d'information soigneusement construite, exprimée par l'utilisation libérale de "menus" de tables des matières est infiniment préférable à un système de liste déroulante précaire et peu maniable. Non seulement il est plus facile de le rendre réactif et nécessite moins de code, mais il rend les choses plus claires : là où les listes déroulantes dissimulent la structure, les tables des matières la mettent à nu.

Certains sites, dont le site gov.uk du Government Digital Service, comprennent des pages d'index (ou de "sujets") qui ne sont que des tables des matières. C'est un concept tellement puissant que le populaire générateur de sites statiques Hugo génère de telles pages par défaut.

Diagramme de style arbre généalogique avec la page d'accueil du sujet en haut et deux ramifications de pages individuelles. Chacune des ramifications de la page individuelle possède plusieurs ramifications de section de page.
Diagramme de type arbre généalogique avec la page d'accueil du sujet en haut et deux ramifications de pages individuelles. Chacune des ramifications de la page individuelle possède plusieurs ramifications de section de page.

L'architecture de l'information est une partie importante de l'inclusion. Un site mal organisé peut être aussi techniquement conforme que vous le souhaitez, il aliènera quand même de nombreux utilisateurs — en particulier ceux souffrant de troubles cognitifs, ou ceux qui sont pressés par le temps.

Boutons du menu de navigation

Tant que nous sommes sur le sujet des faux menus de navigation, il serait négligent de ma part de ne pas parler des boutons de menu de navigation. Vous les avez presque certainement vus désignés par une icône "hamburger" ou "navicon" à trois lignes.

Même avec une architecture d'information épurée et un seul niveau de liens de navigation, l'espace sur les petits écrans est précieux. En cachant la navigation derrière un bouton, il reste plus de place pour le contenu principal dans la fenêtre d'affichage.

Un bouton de navigation est ce qui se rapproche le plus d'un véritable bouton de menu que nous avons étudié jusqu'à présent. Puisqu'il a pour but de faire basculer la disponibilité d'un menu en cas de clic, il doit :

  1. S'identifier comme un bouton, et non un lien ;
  2. Identifier l'état déployé ou replié de son menu correspondant (qui, en termes stricts, est juste une liste de liens).

Amélioration progressive

Mais ne nous emballons pas. Nous devons tenir compte de l'amélioration progressive et examiner comment cela fonctionnerait sans JavaScript.

Dans un document HTML non amélioré, on ne peut pas faire grand-chose à faire avec des boutons (à l'exception des boutons de soumission, mais cela n'a rien à voir avec ce que nous voulons réaliser ici). Au lieu de cela, peut-être devrions-nous commencer par un simple lien qui nous amène à la navigation ?

<a href="#navigation">navigation</a>
<!-- un peu de contenu ici peut-être -->

<nav id="navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/shop">Shop</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Il n'y a pas grand intérêt à avoir le lien à moins qu'il n'y ait beaucoup de contenu entre le lien et la navigation. Puisque la navigation du site devrait presque toujours apparaître près du sommet de l'ordre des sources, ce n'est pas nécessaire. Donc, en réalité, un menu de navigation en l'absence de JavaScript devrait juste être... une navigation.

<nav id="navigation">
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/shop">Shop</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Vous améliorez ceci en ajoutant le bouton, dans son état initial, et en masquant la navigation (à l'aide de l'attribut hidden) :

<nav id="navigation">
  <button aria-expanded="false">Menu</button>
  <ul hidden>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/shop">Shop</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Certains anciens navigateurs — vous savez lesquels — ne prennent pas en charge le mode caché, alors pensez à mettre ce qui suit dans votre CSS. Cela résout le problème car display : none cache le menu aux technologies d'assistance et supprime les liens de l'ordre de focus.

[hidden] {
  display: none;
}

Tout faire pour prendre en charge les anciens logiciels est, bien sûr, un acte de conception inclusive quand certains navigateurs ne peuvent ou ne veulent pas faire de mise à niveau.

Placement

Là où beaucoup de gens se trompent, c'est en plaçant le bouton en dehors de la région. Cela signifie que les utilisateurs de lecteurs d'écran qui se déplacent vers le <nav> en utilisant un raccourci le trouveraient vide, ce qui n'est pas très utile. Avec la liste cachée aux lecteurs d'écran, ils rencontreraient simplement ceci :

<nav id="navigation"></nav>

Voici comment nous pourrions faire basculer l'état :

var navButton = document.querySelector('nav button')
navButton.addEventListener('click', function () {
  let expanded = this.getAttribute('aria-expanded') === 'true' || false
  this.setAttribute('aria-expanded', !expanded)
  let menu = this.nextElementSibling
  menu.hidden = !menu.hidden
})

Aria-controls

Comme je l'ai écrit dans Aria-controls Is Poop, l'attribut aria-controls, destiné à aider les utilisateurs de lecteurs d'écran à naviguer d'un élément "contrôlant" à un élément contrôlé, n'est pris en charge que par le lecteur d'écran JAWS. Vous ne pouvez donc tout simplement pas vous y fier.

En l'absence d'une bonne méthode pour diriger les utilisateurs entre les éléments, vous devez plutôt vous assurer que l'un des éléments suivants est vrai :

  1. Le premier lien de la liste étendue est le suivant dans l'ordre de focalisation après le bouton (comme dans l'exemple de code précédent).
  2. Le premier lien est mis en évidence de manière programmatique lors de la révélation de la liste.

Dans ce cas, je recommande (1). C'est beaucoup plus simple, car vous n'avez pas à vous soucier de ramener le focus sur le bouton et sur quel(s) événement(s) le faire. De plus, il n'y a actuellement rien en place pour avertir les utilisateurs que leur focus va être déplacé vers un autre endroit. Dans les vrais menus dont nous parlerons bientôt, c'est le travail de aria-haspopup="true".

L'utilisation des contrôles aria ne fait pas vraiment de mal, sauf qu'elle rend la lecture dans les lecteurs d'écran plus verbeuse. Cependant, certains utilisateurs de JAWS peuvent s'y attendre. Voici comment il serait appliqué, en utilisant l'id de la liste comme chiffre :

<nav id="navigation">
  <button aria-expanded="false" aria-controls="menu-list">Menu</button>
  <ul id="menu-list" hidden>
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/shop">Shop</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

Les rôles menu et menuitem

Un vrai menu (au sens WAI-ARIA) doit s'identifier comme tel en utilisant le rôle menu (pour le conteneur) et, généralement, les enfants menuitem (d'autres rôles enfants peuvent s'appliquer). Ces rôles parent et enfant fonctionnent ensemble pour fournir des informations aux technologies d'assistance. Voici comment une liste pourrait être augmentée pour avoir une sémantique de menu :

<ul role="menu">
  <li role="menuitem">Item 1</li>
  <li role="menuitem">Item 2</li>
  <li role="menuitem">Item 3</li>
</ul>

Puisque notre menu de navigation commence à se comporter un peu comme un "vrai" menu, ces éléments ne devraient-ils pas être présents ?

La réponse courte est : non. La réponse longue est : non, car nos éléments de liste contiennent des liens et les éléments menuitem ne sont pas destinés à avoir des descendants interactifs. C'est-à-dire que ce sont eux les contrôles du menu.

Nous pourrions, bien sûr, supprimer la sémantique de liste des <li>s en utilisant role="presentation" ou role="none" (qui sont équivalents) et placer le rôle menuitem sur chaque lien. Toutefois, cela supprimerait le rôle de lien implicite. En d'autres termes, l'exemple à suivre serait annoncé comme "Accueil, élément de menu", et non "Accueil, lien" ou "Accueil, élément de menu, lien". Les rôles ARIA remplacent simplement les rôles HTML.

<!-- sera lu comme "Home, menu item" -->
<li role="présentation">
  <a href="/" role="menuitem">Home</a>
</li>

Nous voulons que l'utilisateur sache qu'il utilise un lien et qu'il puisse s'attendre à un comportement de lien, donc ce n'est pas bon. Comme je l'ai dit, les vrais menus sont destinés au comportement des applications (pilotées par JavaScript).

Ce qui nous reste est une sorte de composant hybride, qui n'est pas tout à fait un vrai menu mais qui indique au moins aux utilisateurs si la liste de liens est ouverte, grâce à l'état aria-expanded. C'est un pattern tout à fait satisfaisant pour les menus de navigation.

Note : L'élément <select> ?

Si vous êtes impliqué dans le design responsif depuis le début, vous vous souvenez peut-être d'un modèle selon lequel la navigation était condensée dans un élément <select> pour les fenêtres d'affichage étroites.

téléphone portable avec élément de sélection montrant 'home' sélectionné en haut de la fenêtre.
téléphone portable avec élément de sélection montrant "home" sélectionné en haut de la fenêtre.

Comme pour les boutons de basculement à base de cases à cocher dont nous avons parlé ailleurs, l'utilisation d'un élément natif qui se comporte à peu près comme prévu sans script supplémentaire est un bon choix pour l'efficacité et — surtout sur mobile — les performances. Et les éléments <select> sont d'une certaine manière des menus, avec une sémantique similaire à celle du menu déclenché par un bouton que nous allons bientôt construire.

Toutefois, tout comme pour le bouton de basculement de la case à cocher (checkbox toggle button), nous utilisons un élément associé à la saisie d'une entrée, et non pas simplement au fait de faire un choix. Cela risque de créer une certaine confusion chez de nombreux utilisateurs — d'autant plus que ce modèle utilise JavaScript pour que l'<option> sélectionnée se comporte comme un lien. Le changement de contexte inattendu qui en découle est considéré comme un échec selon le critère 3.2.2 On Input (Level A) des WCAG.

Les vrais menus

Maintenant que nous avons parlé des faux menus et des quasi-menus, le moment est venu de créer un vrai menu, tel qu'ouvert et fermé par un vrai bouton de menu. À partir de maintenant, je ferai référence au bouton et au menu ensemble comme étant simplement un "bouton de menu".

Mais à quels égards notre bouton de menu sera-t-il vrai ? Eh bien, il s'agira d'un composant de menu destiné à choisir des options dans l'application concernée, qui implémente toute la sémantique attendue et les comportements correspondants à considérer comme conventionnels pour un tel outil.

Comme nous l'avons déjà mentionné, ces conventions proviennent de la conception d'applications de bureau. L'attribution ARIA et la gestion du focus régie par JavaScript sont nécessaires pour les imiter pleinement. L'objectif d'ARIA est en partie d'aider les développeurs Web à créer des expériences Web riches sans rompre avec les conventions d'utilisation forgées dans le monde natif.

Dans cet exemple, nous allons imaginer que notre application est une sorte de jeu ou de quiz. Notre bouton de menu permettra à l'utilisateur de choisir un niveau de difficulté. Avec toute la sémantique en place, le menu ressemble à ceci :

<button aria-haspopup="true" aria-expanded="false">
  Difficulty <span aria-hidden="true">&#x25be;</span>
</button>
<div role="menu">
  <button role="menuitem">Easy</button>
  <button role="menuitem">Medium</button>
  <button role="menuitem">Incredibly Hard</button>
</div>

Notes

  • La propriété aria-haspopup indique simplement que le bouton sécrète un menu. Elle avertit que, lorsque le bouton est pressé, l'utilisateur sera déplacé vers le menu "popup" (nous couvrirons le comportement du focus sous peu). Sa valeur ne change pas — elle reste true à tout moment.
  • Le <span> à l'intérieur du bouton contient le point unicode d'un petit triangle noir pointant vers le bas. Cette convention indique visuellement ce que l'aria-haspopup fait non visuellement — qu'en appuyant sur le bouton, quelque chose sera révélé en dessous. L'attribut aria-hidden="true" empêche les lecteurs d'écran d'annoncer "triangle pointant vers le bas" ou autre. Grâce à aria-haspopup, elle n'est pas nécessaire dans le contexte non visuel.
  • La propriété aria-haspopup est complétée par aria-expanded. Celle-ci indique à l'utilisateur l'état actuel du menu, ouvert (développé) ou fermé (replié), en basculant entre les valeurs true et false.
  • Le menu lui-même prend le rôle (bien nommé) de menu. Il prend des descendants avec le rôle menuitem. Il n'est pas nécessaire qu'ils soient des enfants directs de l'élément menu, mais ils le sont dans ce cas, pour des raisons de simplicité.

Comportement du clavier et du focus

Lorsqu'il s'agit de rendre les contrôles interactifs accessibles au clavier, la meilleure chose que vous puissiez faire est d'utiliser les bons éléments. Comme nous utilisons ici des éléments <button>, nous pouvons être sûrs que les événements de clic se déclencheront lors de la frappe des touches Entrée et Espace, comme spécifié dans l'interface HTMLButtonElement. Cela signifie également que nous pouvons désactiver les éléments de menu à l'aide de la propriété disabled associée au bouton.

Mais l'interaction clavier des boutons de menu ne s'arrête pas là. Voici un résumé de tous les comportements de focus et de clavier que nous allons mettre en œuvre, sur la base des pratiques de création WAI-ARIA 1.1 :

Entrée, Espace ou sur le bouton de menuOuvre le menu
sur un élément de menuDéplace le focus vers l'élément de menu suivant, ou le premier élément de menu si vous êtes sur le dernier
sur un élément de menuDéplace le focus sur l'élément de menu précédent, ou le dernier élément de menu si vous êtes sur le premier
sur le bouton de menuFerme le menu s'il est ouvert
Esc sur un élément de menuFerme le menu et met le focus sur le bouton de menu

L'avantage de déplacer le focus entre les éléments de menu à l'aide des touches fléchées est que la tabulation est préservée pour sortir du menu. Dans la pratique, cela signifie que les utilisateurs ne doivent pas passer par chaque élément de menu pour quitter le menu — une énorme amélioration pour la convivialité, surtout lorsqu'il y a beaucoup d'éléments de menu.

L'application de tabindex="-1" rend les éléments de menu non focusables par Tab mais préserve la possibilité de focaliser les éléments de manière programmatique, lors de la capture des frappes sur les touches fléchées.

<button aria-haspopup="true" aria-expanded="false">
  Difficulty <span aria-hidden="true">&#x25be;</span>
</button>
<div role="menu">
  <button role="menuitem" tabindex="-1">Easy</button>
  <button role="menuitem" tabindex="-1">Medium</button>
  <button role="menuitem" tabindex="-1">Incredibly Hard</button>
</div>

La méthode "open"

Dans le cadre d'une bonne conception d'API, nous pouvons construire des méthodes pour gérer les différents événements.

Par exemple, la méthode open doit faire passer la valeur aria-expanded à true, changer la propriété hidden du menu à false, et focaliser le premier menuitem du menu qui n'est pas désactivé :

MenuButton.prototype.open = function () {
    this.button.setAttribute('aria-expanded', true) ;
    this.menu.hidden = false ;
    this.menu.querySelector(':not(\[disabled]))').focus() ;
    retourner ceci ;
}

Nous pouvons exécuter cette méthode lorsque l'utilisateur appuie sur la touche flèche vers le bas sur une instance de bouton de menu focalisée :

this.button.addEventListener(
  'keydown',
  function (e) {
    if (e.keyCode === 40) {
      this.open()
    }
  }.bind(this)
)

En outre, un développeur utilisant ce script pourra désormais ouvrir le menu de manière programmatique :

exampleMenuButton = new MenuButton(document.querySelector('\[aria-haspopup]])) ;
exampleMenuButton.open() ;

Note : Le hack des cases à cocher

Dans la mesure du possible, il est préférable de ne pas utiliser JavaScript, sauf si vous en avez besoin. L'implication d'une troisième technologie au-dessus de HTML et CSS est nécessairement une augmentation de la complexité et de la fragilité du système. Cependant, les composants ne peuvent pas tous être construits de manière satisfaisante sans JavaScript dans le mix.

Dans le cas des boutons de menu, l'enthousiasme pour les faire "fonctionner sans JavaScript" a conduit à ce que l'on appelle le "checkbox hack". Il s'agit d'utiliser l'état coché (ou non coché) d'une case à cocher cachée pour basculer la visibilité d'un élément de menu à l'aide de CSS.

/* menu fermé */
[type='checkbox'] + [role='menu'] {
  display: none;
}

/* menu ouvert */
[type='checkbox']:checked + [role='menu'] {
  display: block;
}

Pour les utilisateurs de lecteurs d'écran, le rôle de la case à cocher et l'état coché n'ont aucun sens dans ce contexte. Ce problème peut être partiellement résolu en ajoutant role="bouton" à la case à cocher.

<input type="checkbox" role="button" aria-haspopup="true" id="toggle" />

Malheureusement, cela supprime la communication implicite de l'état coché, nous privant ainsi d'un retour d'état sans JavaScript (même s'il aurait été pauvre en tant que "coché" dans ce contexte).

Mais il est possible de parodier l'aria-expanded. Il suffit de fournir à notre étiquette deux <span> comme ci-dessous.

<input type="checkbox" role="button" aria-haspopup="true" id="toggle" class="vh">
<label for="toggle" data-opens-menu> Difficulty <span class="vh expanded-text">expanded&lt;/span>
    <span class="vh collapsed-text">collapsed</span>
    <span aria-hidden="true">&#x25be;</span>
</label>

Ces deux éléments sont masqués visuellement à l'aide de la classe visually-hidden, mais — selon l'état dans lequel nous nous trouvons — un seul est également masqué aux lecteurs d'écran. C'est-à-dire qu'un seul a display : none, et ceci est déterminé par l'état coché existant (mais non communiqué) :

/* classe pour masquer visuellement les spans */
.vh {
  position: absolute !important ;
  clip: rect(1px, 1px, 1px, 1px);
  padding: 0 !important ;
  border: 0 !important ;
  height: 1px !important ;
  width: 1px !important ;
  overflow: hidden;
}

/* révéler le libellé d'état correct aux lecteurs d'écran en fonction de l'état */

[type='checkbox']:checked + label .expanded-text {
  display: inline;
}

[type='checkbox']:checked + label .collapsed-text {
  display: none;
}

[type='checkbox']:not(:checked) + label .expanded-text {
  display: none;
}

[type='checkbox']:not(:checked) + label .collapsed-text {
  display: inline;
}

C'est très astucieux, mais notre bouton de menu est toujours incomplet, car les comportements de focus attendus dont nous avons parlé ne peuvent pas être mis en œuvre sans JavaScript.

Ces comportements sont conventionnels et attendus, ce qui rend le bouton plus utilisable. Cependant, si vous avez vraiment besoin d'implémenter un bouton de menu sans JavaScript, c'est à peu près ce que vous pouvez faire de mieux. Étant donné que le bouton de menu de navigation réduit dont j'ai parlé précédemment offre un contenu de menu qui ne dépend pas lui-même de JavaScript (c'est-à-dire des liens), cette approche peut être une option appropriée.

Pour le plaisir, voici un codePen implémentant un bouton de menu de navigation sans JavaScript.

(Note : Seule la barre d'espace ouvre le menu).

L'événement "choisir"

L'exécution de certaines méthodes doit émettre des événements afin que nous puissions mettre en place des écouteurs (listeners). Par exemple, nous pouvons émettre un événement choose lorsqu'un utilisateur clique sur un élément de menu. Nous pouvons le configurer en utilisant CustomEvent, qui nous permet de passer un argument à la propriété detail de l'événement. Dans ce cas, l'argument (choice) serait le nœud DOM de l'élément de menu choisi.

MenuButton.prototype.choose = function (choice) {
  // Define the 'choose' event
  var chooseEvent = new CustomEvent('choose', {
    detail: {
      choice: choice,
    },
  })
  // Dispatch the event
  this.button.dispatchEvent(chooseEvent)
  return this
}

Il y a toutes sortes de choses que nous pouvons faire avec ce mécanisme. Peut-être avons-nous une région active configurée avec un id de menuFeedback :

<div role="alert" id="menuFeedback"></div>

Maintenant, nous pouvons configurer un écouteur et remplir la région en direct avec les informations sécrétées dans l'événement :

exampleMenuButton.addEventListener('choose', function (e) {
  // Get the node's text content (label)
  var choiceLabel = e.details.choice.textContent

  // Get the live region node
  var liveRegion = document.getElementById('menuFeedback')

  // Populate the live region
  liveRegion.textContent = 'Your difficulty level is ${choiceLabel}'
})
Lorsqu'un utilisateur choisit une option, le menu se ferme et le focus est renvoyé sur le bouton de menu. Il est important que les utilisateurs soient ramenés à l'élément déclencheur après la fermeture du menu.
Lorsqu'un utilisateur choisit une option, le menu se ferme et le focus est renvoyé sur le bouton de menu. Il est important que les utilisateurs soient ramenés à l'élément déclencheur après la fermeture du menu.

Lorsqu'un élément de menu est sélectionné, l'utilisateur du lecteur d'écran entend "Vous avez choisi [étiquette de l'élément de menu]". Une région en direct (définie ici avec l'attribution role="alert") annonce son contenu dans les lecteurs d'écran chaque fois que ce contenu change. La région en direct n'est pas obligatoire, mais c'est un exemple de ce qui pourrait se passer dans l'interface en réponse au choix de l'utilisateur dans le menu.

Choix persistants

Tous les éléments de menu ne servent pas à choisir des paramètres persistants. Beaucoup d'entre eux agissent comme des boutons standard qui déclenchent quelque chose dans l'interface lorsqu'on appuie. Cependant, dans le cas de notre bouton de menu de difficulté, nous aimerions indiquer quel est le paramètre de difficulté actuel — celui choisi en dernier.

L'attribut aria-checked="true" fonctionne pour les éléments qui, au lieu de menuitem, prennent le rôle de menuitemradio. Le balisage amélioré, avec le deuxième élément coché (défini) ressemble à ceci :

<button aria-haspopup="true" aria-expanded="false">
  Difficulty
  <span aria-hidden="true">&#x25be;</span>
</button>
<div role="menu">
  <button role="menuitemradio" tabindex="-1">Easy</button>
  <button role="menuitemradio" aria-checked="true" tabindex="-1">Medium</button>
  <button role="menuitemradio" tabindex="-1">Incredibly Hard</button>
</div>

Les menus natifs de nombreuses plateformes indiquent les éléments choisis à l'aide de coches. Nous pouvons le faire sans problème en utilisant un peu de CSS supplémentaire :

[role='menuitem'] [aria-checked='true']::before {
  content: '\2713\0020';
}

Lorsque vous parcourez le menu avec un lecteur d'écran en cours d'exécution, le fait de mettre le focus sur cet élément coché entraînera une annonce du type "case à cocher, élément de menu moyen, coché".

Le comportement à l'ouverture d'un menu avec un menuitemradio coché diffère légèrement. Au lieu de focaliser le premier élément (activé) du menu, c'est l'élément coché qui est focalisé.

Le bouton de menu démarre avec le menu non ouvert. À l'ouverture, le deuxième paramètre de difficulté (moyen) est mis en évidence. Il est préfixé d'une coche en fonction de la présence de l'attribut aria-checked.
Le bouton de menu démarre avec le menu non ouvert. À l'ouverture, le deuxième paramètre de difficulté (moyen) est mis en évidence. Il est préfixé d'une coche en fonction de la présence de l'attribut aria-checked.

Quel est l'avantage de ce comportement ? L'utilisateur (tout utilisateur) se voit rappeler l'option qu'il a précédemment sélectionnée. Dans les menus comportant de nombreuses options incrémentielles (par exemple, un ensemble de niveaux de zoom), les personnes opérant par clavier sont placées dans la position optimale pour effectuer leur réglage.

Utilisation du bouton de menu avec un lecteur d'écran

Dans cette vidéo, je vais vous montrer comment utiliser le bouton de menu avec le lecteur d'écran Voiceover et Chrome. L'exemple utilise des éléments avec menuitemradio, aria-checked et le comportement focus discuté. On peut s'attendre à des expériences similaires dans toute la gamme des logiciels de lecture d'écran populaires.

Bouton de menu inclusif sur Github

Kitty Giraudel et moi avons travaillé ensemble sur la création d'un composant de bouton de menu avec les fonctionnalités API que j'ai décrites, et plus encore. Vous devez remercier Hugo pour bon nombre de ces fonctionnalités, car elles sont basées sur le travail qu'il a effectué sur a11y-dialog — un dialogue modal accessible. Il est disponible sur Github et NPM :

npm i inclusive-menu-button --save

En outre, Kitty a créé une version React pour votre délectation.

Liste de contrôle

  • N'utilisez pas la sémantique de menu ARIA dans les systèmes de menu de navigation.
  • Sur les sites à riche contenu, ne cachez pas la structure dans des menus de navigation imbriqués et alimentés par des menus déroulants.
  • Utilisez aria-expanded pour indiquer l'état ouvert/fermé d'un menu de navigation activé par un bouton.
  • Assurez-vous que ledit menu de navigation est le prochain dans l'ordre de focus après le bouton qui l'ouvre/le ferme.
  • Ne sacrifiez jamais la convivialité à la recherche de solutions sans JavaScript. C'est de la vanité.
Voir la liste des articles de Heydon Pickering traduits dans La Cascade.
article original paru le 23 novembre 2017 dans Smashing Magazine
Traduit avec l'aimable autorisation de Smashing Magazine et de Heydon Pickering.
Copyright Smashing Magazine © 2017