Construire des systèmes de menus accessibles

Publié: 2022-03-10
Résumé rapide ↬ Il existe de nombreux types de menus différents sur le Web. Créer des expériences inclusives consiste à utiliser les bons modèles de menu aux bons endroits, avec le bon balisage et le bon comportement.

Note de l'éditeur : Cet article a été initialement publié sur Inclusive Components. Si vous souhaitez en savoir plus sur des articles similaires sur les composants inclusifs, suivez @inclusicomps sur Twitter ou abonnez-vous au flux RSS. En prenant en charge inclusive-components.design sur Patreon, vous pouvez contribuer à en faire la base de données la plus complète de composants d'interface robustes disponibles.

Le classement est difficile. Prenez les crabes, par exemple. Les bernard-l'ermite, les crabes porcelaine et les limules ne sont pas - taxonomiquement parlant - de vrais crabes. Mais cela ne nous empêche pas d'utiliser le suffixe "crabe". Cela devient plus déroutant 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 des crabes royaux, qui auraient été des bernard-l'ermite dans le passé. Imaginez la taille de leurs coquilles !

En design, nous commettons souvent la même erreur en donnant le même nom à différentes choses. Ils semblent similaires, mais les apparences peuvent être trompeuses. Cela peut avoir un effet fâcheux sur la clarté de votre bibliothèque de composants. En termes d'inclusion, cela peut également vous amener à réutiliser un composant sémantiquement et comportementalement inapproprié. Les utilisateurs s'attendront à une chose et en obtiendront une autre.

Le terme "liste déroulante" désigne un exemple classique. Beaucoup de choses "déroulent" dans les interfaces, y compris l'ensemble de <option> s d'un élément <select> et la liste de liens révélés par JavaScript qui constituent un sous-menu de navigation. Même nom; des choses bien différentes. (Certaines personnes appellent ces "pulldowns", bien sûr, mais n'entrons pas dans les détails.)

Les listes déroulantes qui constituent un ensemble d'options sont souvent appelées "menus", et je veux en parler ici. Nous concevrons un vrai menu, mais il y a beaucoup à dire sur les menus pas vraiment vrais en cours de route.

Plus après saut! Continuez à lire ci-dessous ↓

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 comprend un lien vers la boutique, sous lequel se trouve un ensemble de trois autres liens vers des costumes de chien, des gaufriers et des orbes magiques respectivement.
Une barre de navigation comprend un lien vers la boutique, sous lequel se trouve un ensemble de trois autres liens vers des costumes de chien, des gaufriers et des orbes magiques respectivement. ( Grand aperçu )

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

C'est une convention de longue date que les schémas de navigation sont composés de listes de liens. Une convention presque aussi ancienne stipule que la sous-navigation doit être fournie sous forme de listes imbriquées de liens. Si je devais supprimer le CSS pour le composant illustré ci-dessus, je devrais voir quelque chose comme ce qui suit, sauf en bleu et en Times New Roman.

Sémantiquement parlant, les listes imbriquées de liens sont correctes dans ce contexte. Les systèmes de navigation sont vraiment 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 "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 se trompent et commencent à ajouter la sémantique WAI-ARIA : aria-haspopup="true" , role="menu" , role="menuitem" etc. Il y a une place pour ceux-ci, comme nous le verrons, mais pas ici . Voici deux raisons :

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

Concernant (2): Lors de la traversée d'une région de navigation avec des sous-menus, on s'attendrait à ce que chaque sous-menu apparaisse lors du survol ou de la mise au point du lien "de niveau supérieur" ("Boutique" dans l'illustration). Cela révèle à la fois le sous-menu et place ses propres liens dans l'ordre de mise au point. Avec un peu d'aide de JavaScript capturant les événements de mise au point et de flou pour conserver l'apparence des sous-menus en cas de besoin, quelqu'un utilisant le clavier devrait pouvoir parcourir chaque lien de chaque niveau, à tour de rôle.

Les boutons de menu qui prennent la aria-haspopup="true" ne se comportent pas comme ça. Ils s'activent au clic et n'ont d'autre but que de dévoiler un menu secret.

remises ebook bieten wir
À gauche : un bouton de menu intitulé « menu » avec une icône de flèche pointant vers le bas et l'état aria-expanded = false. À droite : le même bouton de menu, mais avec le menu ouvert. Ce bouton est dans l'état aria-expanded = true. ( Grand aperçu )

Comme illustré, si ce menu est ouvert ou fermé doit être communiqué avec aria-expanded . Vous ne devez modifier cet état qu'au clic, pas au focus. Les utilisateurs ne s'attendent généralement pas à un changement d'état explicite sur 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. Sur le plan comportemental, nous pouvons naviguer dans la navigation comme si aucune supercherie d'affichage/masquage ne se produisait.

Le problème avec les sous-menus de navigation

Les sous-menus de navigation (ou "menus déroulants" pour certains) fonctionnent bien avec une souris ou un clavier, mais ils ne sont pas si chauds quand il s'agit de toucher. Lorsque vous appuyez sur le lien "Boutique" de niveau supérieur dans notre exemple pour la première fois, vous lui dites d'ouvrir le sous-menu et de suivre le lien.

Il y a deux résolutions possibles ici :

  1. Empêche le comportement par défaut des liens de niveau supérieur ( e.preventDefault() ) et du script dans la sémantique et le comportement complets du menu WAI-ARIA.
  2. Assurez-vous que chaque page de destination de niveau supérieur dispose d'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 pouvaient plus accéder à une page de niveau supérieur, si elle existe.

Sidenote : Quels appareils sont des appareils tactiles ?

Il est tentant de se dire « ce n'est pas une bonne solution, mais je ne l'ajouterai que pour les interfaces tactiles ». Le problème est le suivant : comment détecter si un appareil est doté d'un écran tactile ?

Vous ne devriez certainement pas assimiler "petit écran" à "activation tactile". Ayant travaillé dans le même bureau que des personnes fabriquant des écrans tactiles pour les musées, je peux vous assurer que certains des plus grands écrans du marché sont des écrans tactiles. Les ordinateurs portables à double clavier et à saisie tactile deviennent également de plus en plus prolifiques.

De la même manière, de nombreux appareils plus petits, mais pas tous, sont des appareils tactiles. Dans la conception inclusive, vous ne pouvez pas vous permettre de faire des suppositions.

La résolution (2) est plus inclusive et plus robuste en ce sens qu'elle fournit une "repli" pour les utilisateurs de toutes les entrées. Mais les citations effrayantes autour du terme de repli ici sont assez délibérées, car je pense en fait que les tables des matières dans la page sont un moyen supérieur de fournir une navigation.

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

Les tables de contenu Gov.uk sont minimales avec des traits d'union comme styles de liste. Wikipédia fournit une boîte grise bordée d'éléments numérotés. Les deux sont des contenus étiquetés.
Les tables de contenu Gov.uk sont minimales avec des traits d'union comme styles de liste. Wikipédia fournit 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 permettent de naviguer vers des pages ou des sections de page connexes et doivent être sémantiquement similaires aux régions de navigation principales du site, en utilisant un élément <nav> , une liste et un mécanisme d'étiquetage de groupe.

 <nav aria-labelledby="sections-heading"> <h2>Products</h2> <ul> <li><a href="/products/dog-costumes">Dog costumes</a></li> <li><a href="/products/waffle-irons">Waffle irons</a></li> <li><a href="/products/magical-orbs">Magical orbs</a></li> </ul> </nav> <!-- each section, in order, here -->

Remarques

  • Dans cet exemple, nous imaginons que chaque section est sa propre page, comme cela aurait été le cas 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 éléments et les énumère dans la sortie de la technologie d'assistance, telle que la voix synthétique d'un lecteur d'écran.
  • Le <nav> est récursivement étiqueté par le titre en utilisant aria-labelledby . Cela signifie que la "navigation des produits" sera annoncée dans la plupart des lecteurs d'écran lors de l'entrée dans la région par Tab . Cela signifie également que la "navigation des produits" sera détaillée dans les interfaces des éléments de lecteur d'écran, à partir desquelles 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 que cela devienne trop long et fastidieux à faire défiler, c'est encore mieux. Il suffit de créer un lien vers l'identifiant de hachage de chaque section. Par exemple, href="#waffle-irons" doit pointer vers .

 <nav aria-labelledby="sections-heading"> <h2>Products</h2> <ul> <li><a href="#dog-costumes">Dog costumes</a></li> <li><a href="#waffle-irons">Waffle irons</a></li> <li><a href="#magical-orbs">Magical orbs</a></li> </ul> </nav> <!-- dog costumes section here --> <section tabindex="-1"> <h2>Waffle Irons</h2> </section> <!-- magical orbs section here -->

( Remarque : certains navigateurs ne parviennent pas à envoyer le focus aux 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 rendre réactif et nécessite moins de code pour le faire, mais cela rend les choses plus claires : là où les systèmes déroulants masquent la structure, les tables de contenu la mettent à nu.

Certains sites, y compris gov.uk du Government Digital Service, incluent des pages d'index (ou « sujets ») qui ne sont que des tables des matières. C'est un concept si puissant que le célèbre 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 de destination du sujet en haut avec deux ramifications de page individuelles. Chacune des ramifications de page individuelles a plusieurs ramifications de section de page
Diagramme de style arbre généalogique avec la page de destination du sujet en haut avec deux ramifications de page individuelles. Chacune des ramifications de page individuelles a plusieurs ramifications de section de page ( Grand aperçu )

L'architecture de l'information est une grande partie de l'inclusion. Un site mal organisé peut être aussi techniquement conforme que vous le souhaitez, mais aliénera toujours de nombreux utilisateurs, en particulier ceux qui ont des troubles cognitifs ou ceux qui sont pressés par le temps.

Boutons du menu de navigation

Pendant que nous parlons de faux menus liés à la navigation, ce serait négligent de ma part de ne pas parler des boutons du menu de navigation. Vous les avez presque certainement vus indiqués par une icône "hamburger" ou "navicon" à trois lignes.

Même avec une architecture d'information simplifiée et un seul niveau de liens de navigation, l'espace sur les petits écrans est limité. Masquer la navigation derrière un bouton signifie qu'il y a plus de place pour le contenu principal dans la fenêtre d'affichage.

Un bouton de navigation est la chose la plus proche que nous ayons étudiée jusqu'à présent d'un véritable bouton de menu. Puisqu'il a pour but de basculer la disponibilité d'un menu au clic, il devrait :

  1. S'identifier comme un bouton, pas comme un lien ;
  2. Identifiez l'état développé ou réduit de son menu correspondant (qui, au sens strict, n'est qu'une liste de liens).

Amélioration progressive

Mais ne nous précipitons pas. Nous devons être attentifs à l'amélioration progressive et réfléchir à la manière dont cela fonctionnerait sans JavaScript.

Dans un document HTML non amélioré, vous ne pouvez pas faire grand-chose avec les boutons (à l'exception des boutons de soumission, mais ce n'est même pas étroitement lié à 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> <!-- some content here perhaps --> <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/content">Content</a></li> </ul> </nav>

Il n'y a pas beaucoup d'intérêt à avoir le lien à moins qu'il y ait beaucoup de contenu entre le lien et la navigation. Étant donné que la navigation sur le site doit presque toujours apparaître en haut de la commande source, cela n'est pas nécessaire. Donc, vraiment, un menu de navigation en l'absence de JavaScript devrait juste être… une navigation.

 <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/content">Content</a></li> </ul> </nav>

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

 <nav> <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 navigateurs plus anciens — vous savez lesquels — ne prennent pas en charge hidden , alors n'oubliez pas de mettre ce qui suit dans votre CSS. Cela résout le problème car display: none a le même effet de masquer le menu des technologies d'assistance et de supprimer les liens de l'ordre de mise au point.

 [hidden] { display: none; }

Faire de son mieux pour prendre en charge les logiciels plus anciens est, bien sûr, un acte de conception inclusive. Certains ne peuvent pas ou ne veulent pas mettre à niveau.

Placement

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

 <nav> </nav>

Voici comment nous pourrions 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-contrôles

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 de contrôle à un élément contrôlé, n'est pris en charge que dans le lecteur d'écran JAWS. Vous ne pouvez donc tout simplement pas vous y fier.

Sans une bonne méthode pour diriger les utilisateurs entre les éléments, vous devez plutôt vous assurer que l'une des conditions suivantes est vraie :

  1. Le premier lien de la liste développée est le suivant dans l'ordre du focus après le bouton (comme dans l'exemple de code précédent).
  2. Le premier lien est axé par programme sur la révélation de la liste.

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

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

 <nav> <button aria-expanded="false" aria-controls="menu-list">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>

Les rôles de menu et menuitem

Un vrai menu (au sens WAI-ARIA) doit s'identifier en tant que 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 de parent et d'enfant fonctionnent ensemble pour fournir des informations aux technologies d'assistance. Voici comment une liste peut ê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>

Étant donné que notre menu de navigation commence à se comporter un peu comme un "vrai" menu, ceux-ci 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. Autrement dit, ce sont les commandes d'un menu.

Nous pourrions, bien sûr, supprimer la sémantique de liste des <li> en utilisant role="presentation" ou role="none" (qui sont équivalents) et placer le rôle menuitem sur chaque lien. Cependant, 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.

 <!-- will be read as "Home, menu item" --> <li role="presentation"> <a href="/" role="menuitem">Home</a> </li>

Nous voulons que l'utilisateur sache qu'il utilise un lien et qu'il peut 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 de l'application (pilotée 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 des liens est ouverte, grâce à l'état aria-expanded . C'est un modèle parfaitement satisfaisant pour les menus de navigation.

Sidenote : L'élément <select>

Si vous avez été impliqué dans la conception réactive depuis le début, vous vous souvenez peut-être d'un modèle dans lequel la navigation était condensée dans un élément <select> pour les fenêtres étroites.

combiné avec élément de sélection affichant "home" sélectionné en haut de la fenêtre
Combiné avec élément de sélection affichant « home » sélectionné en haut de la fenêtre.

Comme pour les boutons à bascule basés sur des cases à cocher dont nous avons discuté, l'utilisation d'un élément natif qui se comporte un peu comme prévu sans script supplémentaire est un bon choix pour l'efficacité et, en particulier sur les mobiles, les performances. Et les éléments <select> sont des sortes de menus, avec une sémantique similaire au menu déclenché par bouton que nous allons bientôt construire.

Cependant, tout comme avec le bouton bascule de la case à cocher, nous utilisons un élément associé à la saisie d'une entrée, pas simplement à un choix. Cela est susceptible de semer la 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 que cela provoque est considéré comme un échec selon le critère 3.2.2 On Input (Niveau A) des WCAG.

De vrais menus

Maintenant que nous avons eu la discussion sur les faux menus et les quasi-menus, le moment est venu de créer un vrai menu, comme ouvert et fermé par un vrai bouton de menu. À partir de maintenant, je ferai référence au bouton et au menu ensemble comme simplement un "bouton de menu".

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

Comme mentionné précédemment, ces conventions proviennent de la conception d'applications de bureau. L'attribution ARIA et la gestion des focus régie par JavaScript sont nécessaires pour les imiter pleinement. Une partie de l'objectif d'ARIA est d'aider les développeurs Web à créer des expériences Web riches sans rompre avec les conventions d'utilisabilité 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>

Remarques

  • La propriété aria-haspopup indique simplement que le bouton sécrète un menu. Il agit comme un avertissement que, lorsqu'il est pressé, l'utilisateur sera déplacé vers le menu "popup" (nous couvrirons le comportement de mise au point sous peu). Sa valeur ne change pas — elle reste aussi true à tout moment.
  • Le <span> à l'intérieur du bouton contient le point Unicode pour un petit triangle noir pointant vers le bas. Cette convention indique visuellement ce que aria-haspopup fait de manière non visuelle - qu'appuyer sur le bouton révélera quelque chose en dessous. L'attribution aria-hidden="true" empêche les lecteurs d'écran d'annoncer "triangle pointant vers le bas" ou similaire. Grâce à aria-haspopup , ce n'est pas nécessaire dans le contexte non visuel.
  • La propriété aria-haspopup est complétée par aria-expanded . Cela indique à l'utilisateur si le menu est actuellement dans un état ouvert (développé) ou fermé (réduit) en basculant entre les valeurs true et false .
  • Le menu lui-même prend le rôle de menu (bien nommé). Il prend des descendants avec le rôle menuitem . Ils n'ont pas besoin d'être des enfants directs de l'élément de menu , mais ils le sont dans ce cas — pour plus de simplicité.

Comportement du clavier et de la mise au point

Lorsqu'il s'agit de rendre les commandes interactives accessibles au clavier, la meilleure chose à faire est d'utiliser les bons éléments. Étant donné que nous utilisons ici des éléments <button> , nous pouvons être assurés que les événements de clic se déclencheront sur les frappes 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.

Il y a cependant beaucoup plus d'interactions avec le clavier des boutons de menu. Voici un résumé de tous les comportements de focus et de clavier que nous allons implémenter, basés sur WAI-ARIA Authoring Practices 1.1 :

Entrez , Espace ou sur le bouton de menu Ouvre le menu
sur un élément de menu Dé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 menu Déplace le focus sur l'élément de menu précédent, ou sur le dernier élément de menu si vous êtes sur le premier
sur le bouton de menu Ferme le menu s'il est ouvert
Échap sur un élément de menu Ferme le menu et concentre 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 Tab est conservé pour sortir du menu. En pratique, cela signifie que les utilisateurs n'ont pas à parcourir chaque élément de menu pour quitter le menu - une amélioration considérable pour la convivialité, en particulier lorsqu'il existe de nombreux éléments de menu.

L'application de tabindex="-1" rend les éléments de menu non focalisables par Tab mais préserve la possibilité de focaliser les éléments par programme, lors de la capture de touches 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 ouverte

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

Par exemple, la méthode open doit basculer la valeur aria-expanded sur "true", changer la propriété hidden du menu sur false et mettre l'accent sur 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(); return this; }

Nous pouvons exécuter cette méthode lorsque l'utilisateur appuie sur la touche bas sur une instance de bouton de menu ciblé :

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

De plus, un développeur utilisant ce script pourra désormais ouvrir le menu par programmation :

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

Sidenote : le hack de la case à cocher

Autant que possible, il est préférable de ne pas utiliser JavaScript à moins que vous n'en ayez besoin. Impliquer une troisième technologie en plus du HTML et du CSS est nécessairement une augmentation de la complexité et de la fragilité systémiques. Cependant, tous les composants ne peuvent pas être construits de manière satisfaisante sans JavaScript dans le mélange.

Dans le cas des boutons de menu, un enthousiasme pour les faire « fonctionner sans JavaScript » a conduit à ce qu'on appelle le hack de la case à cocher. C'est là que l'état coché (ou non coché) d'une case à cocher masquée est utilisé pour basculer la visibilité d'un élément de menu à l'aide de CSS.

 /* menu closed */ [type="checkbox"] + [role="menu"] { display: none; } /* menu open */ [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. Cela peut être en partie surmonté en ajoutant role="button" à la case à cocher.

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

Malheureusement, cela supprime la communication implicite de l'état vérifié, nous privant d'un retour d'état sans JavaScript (bien qu'il aurait été aussi «vérifié» dans ce contexte).

Mais il est possible d'usurper aria-expanded . Nous avons juste besoin de fournir notre étiquette avec deux portées comme ci-dessous.

 <input type="checkbox" role="button" aria-haspopup="true" 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>

Celles-ci sont toutes deux visuellement masquées à l'aide de la classe visually-hidden , mais, selon l'état dans lequel nous nous trouvons, une seule est également masquée pour les lecteurs d'écran. C'est-à-dire qu'un seul a display: none , et cela est déterminé par l'état coché existant (mais non communiqué):

 /* class to hide spans visually */ .vh { position: absolute !important; clip: rect(1px, 1px, 1px, 1px); padding: 0 !important; border: 0 !important; height: 1px !important; width: 1px !important; overflow: hidden; } /* reveal the correct state wording to screen readers based on state */ [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 intelligent et tout, mais notre bouton de menu est encore incomplet puisque les comportements de focus attendus dont nous avons discuté ne peuvent tout simplement pas être implémentés sans JavaScript.

Ces comportements sont conventionnels et attendus, rendant le bouton plus utilisable. Cependant, si vous avez vraiment besoin d'implémenter un bouton de menu sans JavaScript, c'est à peu près aussi proche que possible. Considérant que le bouton de menu de navigation réduit que j'ai couvert précédemment offre un contenu de menu qui ne dépend pas de JavaScript lui-même (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.

Voir l'exemple de bouton de menu de navigation du stylet non JS par Heydon (@heydon) sur CodePen.

( Remarque : seul l'espace ouvre le menu.)

L'événement "choisir"

L'exécution de certaines méthodes devrait émettre des événements afin que nous puissions configurer des écouteurs. Par exemple, nous pouvons émettre un événement choose lorsqu'un utilisateur clique sur un élément de menu. Nous pouvons configurer cela en utilisant CustomEvent , ce qui nous permet de passer un argument à la propriété detail de l'événement. Dans ce cas, l'argument ("choix") 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 en direct configurée avec un id de menuFeedback :

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

Nous pouvons maintenant configurer un écouteur et remplir la région en direct avec les informations secrètes à l'intérieur de 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 revient au bouton de menu. Il est important que les utilisateurs reviennent à l'élément déclencheur après la fermeture du menu.
Lorsqu'un utilisateur choisit une option, le menu se ferme et le focus revient au bouton de menu. Il est important que les utilisateurs reviennent à l'élément déclencheur après la fermeture du menu. ( Grand aperçu )

Lorsqu'un élément de menu est sélectionné, l'utilisateur du lecteur d'écran entendra : « 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 à l'utilisateur faisant un choix de menu.

Choix persistants

Tous les éléments de menu ne permettent pas de choisir des paramètres persistants. Beaucoup agissent simplement comme des boutons standard qui font que quelque chose se produit dans l'interface lorsqu'ils sont pressés. 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 menuitemradio . Le balisage amélioré, avec le deuxième élément coché ( set ) 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 sur de nombreuses plates-formes 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, la mise au point de cet élément coché déclenchera une annonce telle que "coche, é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é) dans le menu, l'élément coché est focalisé à la place.

Le bouton de menu démarre avec le menu non ouvert. À l'ouverture, le deuxième paramètre de difficulté (moyen) est ciblé. Il est précédé d'une coche basée sur 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 ciblé. Il est précédé d'une coche basée sur la présence de l'attribut aria-checked. ( Grand aperçu )

Quel est l'avantage de ce comportement ? L'utilisateur (n'importe quel utilisateur) est rappelé de son option précédemment sélectionnée. Dans les menus comportant de nombreuses options incrémentales (par exemple, un ensemble de niveaux de zoom), les personnes opérant au clavier sont placées dans la position optimale pour effectuer leur réglage.

Utilisation du bouton Menu avec un lecteur d'écran

Dans cette vidéo, je vais vous montrer à quoi ressemble l'utilisation du bouton de menu avec le lecteur d'écran Voiceover et Chrome. L'exemple utilise des éléments avec menuitemradio , aria-checked et le comportement de focus discuté. Des expériences similaires peuvent être attendues dans toute la gamme des logiciels de lecture d'écran populaires.

Bouton de menu inclus sur Github

Kitty Giraudel et moi avons travaillé ensemble sur la création d'un composant de bouton de menu avec les fonctionnalités de l'API que j'ai décrites, et plus encore. Vous devez remercier Hugo pour bon nombre de ces fonctionnalités, car elles étaient basées sur le travail qu'ils ont effectué sur a11y-dialog - une boîte de dialogue modale accessible. Il est disponible sur Github et NPM.

 npm i inclusive-menu-button --save

De plus, Kitty a créé une version React pour votre plaisir.

Liste de contrôle

  • N'utilisez pas la sémantique de menu ARIA dans les systèmes de menus de navigation.
  • Sur les sites à fort contenu, ne masquez pas la structure dans les menus de navigation imbriqués et 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 suivant dans l'ordre de mise au point après le bouton qui l'ouvre/le ferme.
  • Ne sacrifiez jamais la convivialité dans la recherche de solutions sans JavaScript. C'est de la vanité.