Tous les articles

Tous les articles

Amener le frontend au niveau WCAG 2.2 AA

Quatre contrôles qui possèdent chacun une classe de défauts d'accessibilité, et ce qu'ils ont trouvé en route vers le WCAG 2.2 AA.

L'accessibilité de l'application est tenue par quatre contrôles, et chacun possède une classe de défauts que les trois autres ne voient pas. Ce découpage est le principal résultat du travail, parce que celui qui corrige un défaut doit savoir quel contrôle l'attrapera s'il revient. Le reste, ce sont les défauts que ces contrôles ont trouvés une fois en place.

Visible n'est pas utilisable#

Le travail de fond était arrivé deux jours plus tôt, avec le changement d'identité, et partait d'indicateurs de focus presque tous invisibles. Tailwind compile un anneau intérieur en une propriété personnalisée qui préfixe chaque ombre d'anneau, y compris celle du décalage : tout contrôle qui utilisait son anneau comme bord au repos dessinait donc son anneau de focus à l'intérieur de lui-même, derrière son propre fond. La couleur de l'anneau était en outre l'accent à 45 % d'opacité, environ 1,6:1 sur la carte claire, bien en dessous des 3:1 exigés pour un indicateur de focus. Le focus est passé sur outline, dont l'écart est réellement transparent, dans une couleur pleine par thème. Les cadres de champ s'allumaient aussi au clic de souris, parce que :focus-visible s'applique à tout élément focalisé qui accepte la saisie : un petit module retient désormais si la dernière interaction venait du clavier ou d'un pointeur.

Visible ne voulait toujours pas dire utilisable. Le menu déroulant n'avait aucune gestion du clavier : ni flèches, ni Échap, ni recherche à la frappe, et un déclencheur qui était un div avec un gestionnaire de clic. J'ai passé les menus et les listes de sélection sur Headless UI, sur lequel la fenêtre modale reposait déjà, et scindé ce qui était un seul composant en deux. Un menu est une liste de commandes, et chaque choix le ferme ; un popover est un panneau d'état que l'utilisateur ajuste encore et encore, et il ne s'approprie pas les flèches. Le calendrier s'ouvrait au clavier puis le retenait prisonnier, parce que sa touche Échap était liée à un panneau téléporté en fin de document, qui ne voyait jamais la touche. Les en-têtes et les lignes de tableau ne réagissaient qu'au clic : impossible de trier une colonne ou d'ouvrir un enregistrement sans souris.

Deux défauts ont pris bien plus de temps que les autres. Chaque élément de menu de l'application était mort au clavier : étaler un objet d'attributs sur un élément dans une boucle pousse le compilateur Vue à préparer sa ref pour un tableau, ce qui traversait la fusion des props de Headless UI jusqu'à la ref de l'élément de menu. Appuyer sur Entrée ne cliquait donc sur rien et fermait le menu. Trois tentatives pour le comprendre en lisant le code ont échoué ; compiler le composant et comparer deux reproductions l'a trouvé. L'autre était la restauration du focus. Headless UI rend le focus au déclencheur quand une boîte de dialogue ou un menu se ferme, mais pas sous les transitions de l'application, qui démontent la boîte de dialogue avant que sa restauration ne s'exécute : le focus atterrissait sur le corps de la page. Un petit composable le restaure maintenant, en réessayant à chaque image jusqu'à ce que le verrou de focus de la boîte de dialogue lâche prise.

Quatre contrôles, une classe de défauts chacun#

Le lint des templates tourne dans l'éditeur et à chaque commit, avec toutes les règles du plugin d'accessibilité en erreur. Il juge chaque noeud de template isolément et ne peut donc pas voir les attributs ARIA que Headless UI pose à l'exécution.

Une spec de politique tient les invariants qui traversent plusieurs fichiers, dont les régressions ne font rien échouer ailleurs. Un glyphe de police d'icônes est du texte sur un point de code à usage privé, que plusieurs lecteurs d'écran lisent comme du bruit, et sur 71 appels d'icône, 3 seulement pensaient à aria-hidden. Le composant d'icône se masque désormais par défaut, et la spec interdit un élément d'icône brut qui le contournerait. Elle fige aussi la liste des fichiers autorisés à ouvrir une région live, parce qu'une région live réclame le canal vocal de l'utilisateur et qu'en ajouter une doit être une décision relue.

Un balayage axe tourne dans un vrai navigateur, au niveau WCAG 2.2 AA, sur neuf classes de surfaces : les pages hors connexion, une liste, un formulaire, une boîte de dialogue ouverte sur du contenu, la déclaration d'accessibilité, et un catalogue qui affiche chaque composant de base dans chaque état. C'est le seul contrôle qui voit l'arbre rendu, y compris tout ce qu'ajoute Headless UI. Son builder cache un piège d'ordre bon à connaître : options() remplace l'objet dans lequel withTags() écrit, si bien que des règles posées dans le mauvais ordre disparaissent sans bruit.

Des parcours au clavier jugent ce qu'aucun scanner ne peut juger : où le focus arrive, où il revient et si un contrôle peut réellement être actionné. Ils couvrent le lien d'évitement, la barre latérale, les boîtes de dialogue, les menus, les listes de sélection, l'arbre de regroupement, les onglets de formulaire et les segments de date.

Ce que les contrôles ont trouvé#

Élargir le balayage a fait remonter des défauts que la version étroite n'affichait jamais. L'accordéon masquait un contenu replié avec aria-hidden alors que ses liens restaient accessibles à la tabulation : un lecteur d'écran atteignait des arrêts qu'il ne pouvait pas énoncer. inert retire un sous-arbre de l'ordre de tabulation et de l'arbre d'accessibilité en une fois. La barre d'onglets déclarait une liste d'onglets puis laissait chaque onglet comme arrêt séparé, sans flèches et sans lien vers son panneau, ce qui se lit plus mal que pas d'ARIA du tout, parce que le rôle promet un contrat clavier que le composant rompt. Elle utilise désormais un tabindex itinérant.

Un champ invalide changeait de couleur, et rien d'autre. Quatre composants annonçaient leur erreur, et quinze affichaient un message vers lequel aucun contrôle ne pointait. Chaque composant marque désormais son vrai contrôle comme invalide et le relie à son message, et les deux moitiés sont vérifiées, parce qu'un message sans identifiant s'affiche très bien et cesse simplement d'être atteignable. Le cadre de la liste de sélection était un bouton rendu comme un div, si bien que la bibliothèque posait aria-expanded sur un élément dont le rôle ne l'accepte pas : la seule violation axe critique connue dans l'application. Le cadre est maintenant un simple conteneur, le champ de saisie porte le rôle combobox, et le chevron est le vrai bouton, retiré de l'ordre de tabulation.

Chaque page connectée a reçu un lien d'évitement comme premier arrêt de tabulation. Sa cible est la véritable zone de défilement, qui avait aussi besoin d'un tabindex et d'un rôle de région, parce qu'une zone défilante que le clavier ne peut pas focaliser est une zone que le clavier ne peut pas faire défiler.

Le dire publiquement#

L'European Accessibility Act attend d'un service qu'il déclare sa conformité, nomme ses limites connues et offre un moyen de signaler un obstacle. La déclaration est publique et accessible sans connexion, pour qu'une personne bloquée puisse toujours la lire, et elle revendique une conformité partielle tant que le produit n'est pas encore sorti. Elle vit aujourd'hui avec les autres documents légaux sur le site public.

Le site public est venu plus tard et demandait une pièce de plus. Suivre un lien y déclenchait une navigation côté client qui ne déplaçait pas le focus et n'annonçait rien : un utilisateur de lecteur d'écran n'entendait rien du tout, et la tabulation suivante repartait d'une page qui n'existait plus. Depuis septembre, le site comme l'application annoncent chaque changement de page et placent le focus sur le contenu de la nouvelle page, là où pointe aussi le lien d'évitement.