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.