Concevoir un composant d’auto-complétion accessible
Attention ! Cet article a été écrit en 2021. Son contenu a peut-être besoin d’une mise à jour. Complétez votre veille avec des articles plus récents, par exemple en consultant les nouveautés de notre blog accessibilité numérique, ou en lançant une recherche pour trouver des articles similaires, mais à jour.
Aujourd’hui, nous partageons avec joie la traduction en français de l’article « Building an accessible autocomplete control » écrit d’Adam Silver et publié le 2 février 2020.
Nous remercions l’auteur pour son aimable autorisation.
L’extrait qui suit est tiré de mon livre, Form Design Patterns.
Vous pourrez le retrouver (en anglais) au milieu du chapitre 3, A Flight Booking Form, où je présente différentes manières de saisir un pays de destination dans un formulaire de réservation de vols.
Malheureusement, les contrôles HTML natifs des formulaires ne sont pas assez robustes pour ce type d’interaction.
Il est donc nécessaire d’envisager de concevoir un système d’auto-complétion depuis le début.
En guise d’avant-propos, je vous annonce dès maintenant que, contrairement à ce qu’on pourrait imaginer, l’auto-complétion est un des composants d’interface les plus complexes que j’aie jamais eu à réaliser.

L’auto-complétion affiche des suggestions qui correspondent à une chaîne de caractères saisis en temps réel.
Elle permet de sélectionner une suggestion pour terminer sa saisie rapidement avec une valeur exacte, ou alors de continuer à saisir des caractères pour affiner les options proposées.
Le code de base
Pour que l’auto-complétion fonctionne sans JavaScript, il faut commencer avec un élément de formulaire natif, fourni gratuitement par les navigateurs.
Comme je l’explique plus tôt dans mon livre, opter pour des boutons radio infligerait trop de possibilités de réponse aux utilisateurs et utilisatrices ; utiliser un moteur de recherche prend du temps et ne garantit pas d’obtenir un résultat ; et la solution de la datalist est trop peu fiable. Il ne reste alors plus que la liste déroulante.
Le code enrichi
Lorsque JavaScript est disponible, le constructeur Autocomplete()
que nous allons définir enrichira le HTML de base. On obtiendra alors :
Masquer la liste déroulante sans empêcher l’envoi de sa valeur
Pour masquer la liste déroulante sans empêcher l’envoi de sa valeur au serveur, il faut ajouter :
visually-hidden
pour rendre la liste déroulante invisible aux personnes voyantes ;aria-hidden="true"
pour la rendre invisible pour les personnes qui utilisent un lecteur d’écran ;tabindex="-1"
pour empêcher les personnes naviguant au clavier de positionner le focus dessus.
Si vous utilisez les 3 attributs ensemble, il est généralement recommandé d’utiliser la propriété display: none
, qui génère le même résultat, mais avec un code plus propre.
Par contre, cette méthode empêcherait l’envoi au serveur de la valeur de la liste déroulante. Et c’est un point important parce que même si les utilisateurs et les utilisatrices n’auront pas à interagir directement avec la liste déroulante, la valeur correspondante doit quand même être envoyée pour être traitée.
Réattribuer l’étiquette
On transfert l’attribut id
de la liste déroulante à la zone de saisie, parce que l’étiquette doit lui être associée, d’une part pour qu’elle soit restituée par les lecteurs d’écran, et d’autre part pour étendre sa zone de clic. Dans mon livre, je traite ce point dans le premier chapitre, A Registration Form, sur le formulaire d’inscription.
L’attribut name de la zone de texte n’est pas nécessaire puisque sa valeur n’est pas envoyée au serveur. Il remplit purement une fonction d’interaction et sert de proxy pour définir la valeur de la liste déroulante en arrière-plan.
À propos des attributs de la zone de saisie
La propriété role="combobox"
garantit que l’élément sera bien restitué comme étant une combo box. Une combo box est un champ de saisie accompagné d’une liste de suggestions prédéfinies.
La propriété aria-autocomplete="list"
permet d’indiquer qu’une liste d’options va s’afficher. La propriété aria-expanded
permet d’indiquer si le menu est réduit ou développé selon que la valeur est définie à true
ou false
.
L’attribut autocomplete="off"
empêche les navigateurs d’afficher leurs propres suggestions et donc d’interférer avec les suggestions proposées par notre composant.
Enfin, l’attribut autocapitalize="none"
empêche les navigateurs d’ajouter automatiquement une majuscule à la première lettre de la saisie. Le chapitre 4 A Login Form, sur le formulaire de connexion, explore cette question plus en profondeur.
On utilise CSS pour superposer l’icône SVG sur la zone de saisie. L’attribut focusable="false"
empêche que les éléments SVG ne prennent le focus par défaut dans Internet Explorer.
À propos des attributs du menu
La propriété role="list"
est utilisée pour afficher le menu sous forme d’une liste puisqu’il contiendra une liste d’options. À chaque option est associée une propriété role="option"
.
La propriété aria-selected="true"
permet d’indiquer quelle option au sein de la liste est sélectionnée ou non, selon que la valeur est définie sur true ou false.
L’attribut tabindex="-1"
signifie que le focus peut être programmé pour se positionner sur l’option lorsque certaines touches de clavier sont activées. On se penchera sur ces interactions au clavier un peu plus tard.
L’attribut data-option-value
enregistre la valeur de la liste déroulante. Lorsqu’on clique sur une option suggérée par l’auto-complétion, la valeur de la zone de saisie est actualisée. Les deux valeurs sont ainsi synchronisées. Cette action fait le pont entre l’interface (ce que la personne voit) et la valeur de la liste déroulante (ce qu’elle ne voit pas).
Utiliser une zone live
pour informer les personnes utilisant un lecteur d’écran que des suggestions sont disponibles
Les personnes voyant l’écran verront les suggestions s’afficher dans le menu à mesure qu’elles saisiront des caractères, mais les personnes se servant d’un lecteur d’écran ne détermineront pas forcément les suggestions présentes dans le menu si elles ne quittent pas la zone de saisie pour explorer ce menu.
Afin de proposer à ces personnes une expérience identique (premier principe de conception inclusive), on va utiliser une zone live
, que j’étudie plus en détail dans le chapitre A Checkout Form.
À mesure que le menu est généré, la zone live
se remplira d’autant de résultats disponibles. Par exemple : « 13 résultats disponibles ». Avec ces informations à disposition, les utilisateurs et utilisatrices de lecteurs d’écran peuvent décider de poursuivre leur saisie pour affiner les résultats ou alors sélectionner l’une des suggestions à partir du menu.
Étant donné que ces informations ne sont utiles que pour les personnes qui utilisent un lecteur d’écran, on les masque en utilisant visually-hidden
, une fois de plus.
Gérer la saisie de texte
Lorsque quelqu’un saisit des caractères dans la zone de texte, il faudra écouter, en JavaScript, l’activation de certaines touches.
L’objet this.keys
est un ensemble de nombres qui correspondent à des touches spécifiques en fonction de leur nom. Ça permet d’éviter les nombres magiques, rendant le code plus facile à comprendre.
L’instruction switch
exclut les touches Echap, Flèche Haut, Flèche Gauche, Flèche Droite, Espace, Entrée, Tabulation, et Shift. Sans ça, le cas par défaut s’exécuterait, affichant à tort le menu.Au lieu de préciser ces touches qui ne sont pas concernées, on aurait pu préciser les touches qui, à l’inverse, sont concernées. Mais ça nécessiterait de préciser une longue liste de touches, au risque d’en oublier.Les principales instructions qui nous intéressent sont les deux dernières, à savoir le cas où la personne appuie sur la touche Flèche Bas, et le cas par défaut mentionné juste avant, c’est-à-dire tout le reste (une lettre, un nombre, un symbole, etc.). Dans ce cas-là, la fonction onTextBoxType()
sera appelée.
La méthode getOptions()
(étudiée plus loin) filtre les options en fonction de la saisie.
Prévoir une seule tabulation pour les contrôles composites
L’auto-complétion est un contrôle composite ce qui signifie qu’il est constitué d’éléments interactifs et d’éléments pouvant recevoir le focus. Par exemple : on saisit des caractères dans la zone de texte puis on se déplace vers le menu pour sélectionner une option.
Les contrôles composites ne doivent susciter qu’une seule tabulation, conformément à la spécification WAI-ARIA Authoring Practices 1.1 qui indique :
Il existe une pratique de navigation au clavier, admise par toutes les plateformes, selon laquelle les touches Tabulation et Maj+Tabulation déplacent le focus d’un composant d’interface à un autre tandis que d’autres touches, principalement les flèches de direction, déplacent le focus à l’intérieur des composants d’interface qui possèdent plusieurs éléments pouvant recevoir le focus. On appelle ce parcours du focus à la pression de la touche Tabulation « séquence de tabulation » ou « boucle de tabulation » (tab ring, en anglais). (Traduction libre)
Un ensemble de boutons radio est aussi un contrôle composite.
Une fois que le premier bouton radio a reçu le focus, il est possible d’utiliser les flèches de direction pour se déplacer entre chaque option. Appuyer sur la touche Tabulation déplacera le focus sur l’élément tabulable le plus proche dans la séquence de tabulation.
Retournons maintenant à l’auto-complétion.
La zone de saisie reçoit naturellement le focus quand on appuie sur la touche Tabulation. Une fois le focus positionné dessus, on pourra alors appuyer sur les touches de direction pour parcourir le menu. On regardera ça en détail un peu plus loin.
Si on appuie sur la touche Tabulation dès lors que le focus est positionné sur la zone de saisie ou sur une option du menu, le menu devrait disparaître de façon à ne pas masquer le contenu en dessous lorsque le menu ne sert pas. On verra un peu plus tard comment y arriver.
Utiliser ARIA activedescendant est incompatible avec l’auto-complétion
Souvent, les systèmes d’auto-complétion utilisent la propriété aria-activedescendant
comme solution alternative pour s’assurer qu’il n’y ait qu’une seule tabulation.
Cet attribut permet de conserver le focus sur le conteneur du composant en permanence, et de restituer l’élément qui est activé à ce moment-là.
Mais ça ne fonctionne pas avec l’auto-complétion parce que la zone de saisie est sœur du menu et non le parent.
Masquer le menu à la perte du focus (blur
) ne fonctionne pas
L’événement onblur
est déclenché lorsqu’on quitte un élément qui jusqu’alors avait le focus. Dans le cas de l’auto-complétion, on pourrait écouter cet événement sur la zone de saisie.
L’avantage de l’événement onblur
est qu’il sera déclenché dès qu’on quitte la zone de saisie au moyen de la touche Tabulation et en cliquant ou en touchant une zone en dehors de l’élément.
Mais malheureusement, déplacer le focus sur le menu programmatiquement déclenche la perte du focus, masquant le menu. Résultat, le menu devient inaccessible aux personnes naviguant exclusivement au clavier.Une solution consiste à utiliser setTimeout()
, qui nous permet d’associer un délai à l’événement. Ce délai nous laisse le temps d’annuler l’événement en utilisant clearTimeout()
dans le cas où l’utilisateur ou l’utilisatrice déciderait de déplacer le focus sur le menu avant que ce délai ne se soit écoulé.Cette solution empêche le menu de disparaître, le rendant donc accessible de nouveau.
Mais cette méthode n’est pas satisfaisante, car l’événement blur présente des problèmes sous iOS 10. L’événement blur est déclenché de façon erronée sur la zone de saisie dès lors que l’on masque le clavier virtuel. Le menu devient donc complètement inaccessible.
La véritable solution est la suivante.
Masquer le menu en écoutant l’utilisation de la touche tabulation
Au lieu de masquer le menu en utilisant l’événement blur, on peut utiliser l’événement keydown
(touche enfoncée) pour détecter l’utilisation de la touche Tabulation.
Mais contrairement à l’événement blur
, la méthode keydown
(touche enfoncée) ne prend pas en compte les cas où un élément perd le focus parce qu’on a cliqué en dehors de celui-ci.
On corrigera ça en écoutant l’événement click
au sein du document et en veillant à masquer le menu uniquement lorsqu’on clique en dehors de celui-ci.
$(document).on(’click’, $.proxy(function(e) if(!this.container[0].contains(e.target)) // masquer le menu , this)) ;
Appuyer sur la touche Flèche Bas pour déplacer le focus sur le menu
Lorsque la zone de saisie reçoit le focus, appuyer sur la touche Flèche Bas déclenche la fonction onTextBoxDownPressed()
.
Si on appuie sur la touche Flèche Bas sans avoir saisi de contenu dans la zone de texte, le menu affichera toutes les options et le focus se positionnera sur la première option.Il en va de même si on saisit une correspondance exacte. Mais ce cas est rare dans la pratique. Généralement, on sélectionne une option parmi celles proposées, pour aller plus vite. La condition else
remplira le menu avec les options qui correspondent (s’il y en a), et le focus se positionnera sur la première d’entre elles. Ces deux scénarios se terminent avec un appel de la fonction highlightOption()
, que nous allons examiner plus loin.
Faire défiler les suggestions du menu
Le menu peut contenir des centaines d’options. Pour s’assurer que les éléments du menu soient visibles, on utilisera les styles suivants :
La propriété max-height
permet au menu de s’agrandir jusqu’à ce qu’il atteigne une hauteur maximum. Dès lors que le contenu du menu dépasse cette hauteur, on pourra faire défiler les propositions grâce à la propriété overflow-y: scroll
.La dernière propriété (non standard) active le défilement momentum (momentum scrolling) sur iOS, ce qui permet à l’auto-complétion d’avoir un défilement qui fonctionne de la même façon partout.
Sélectionner une option
On utilisera la méthode de la délégation d’événement pour détecter un clic sur une option. C’est une méthode plus efficace que celle qui consisterait à ajouter un événement click
à chaque option.
Le gestionnaire d’événement récupère l’option (avec e.currentTarget
) et la transmet à selectOption()
.
La méthode
selectOption()
prend l’option devant être sélectionnée et extrait la valeur de son attribut data-option-value
. Cette valeur est alors transmise vers setValue()
, et s’affiche dans la zone de saisie ainsi que dans la liste déroulante masquée. Enfin, le menu est masqué et la zone de saisie prend le focus.Le même fonctionnement est enclenché dès lors qu’on sélectionne une option à l’aide des touches Espace ou Entrée.
Interagir avec le menu à l’aide d’un clavier
Une fois que le focus arrive à l’intérieur du menu, on peut permettre aux utilisateurs et utilisatrices de parcourir le menu au clavier grâce à l’écoute de l’événement keydown
(touche enfoncée).
Touche | Action |
---|---|
Flèche Haut | Si le focus est positionné sur la première option, cette touche positionne alors le focus sur la zone de saisie. Sinon, le focus se positionne sur l’option précédente. |
Flèche Bas | Positionne le focus sur l’option suivante du menu. Si le focus est sur la dernière option du menu, il ne se passe rien. |
Tabulation | Masque le menu. |
Entrée ou Espace | Sélectionne l’option déjà en surbrillance et déplace le focus sur la zone de saisie. |
Echap | Masque le menu et positionne le focus sur la zone de saisie. |
Toutes les autres touches | Positionnent le focus sur la zone de saisie (pour permettre de continuer à taper). |
Mettre en surbrillance les options recevant le focus
Lorsque l’utilisateur ou l’utilisatrice déplace le focus sur une option à l’aide de la touche Flèche Haut ou Flèche Bas, la fonction highlightOption()
est appelée.
Cette méthode effectue plusieurs tâches.
Premièrement, elle vérifie si une option avait déjà été sélectionnée. Le cas échéant, sa propriété aria-selected
est alors définie à false
, ce qui permet de s’assurer que cet état est bien restitué aux personnes utilisant un lecteur d’écran.
Deuxièmement, la propriété aria-selected
de la nouvelle option sélectionnée passe à true.
Étant donné que la hauteur du menu est fixe, il se peut que la nouvelle option ne s’affiche pas dans la zone visible du menu. On vérifiera si c’est le cas ou non en utilisant isElementVisible()
.
Si l’option n’est pas visible, on ajustera le niveau de défilement du menu avec scrollTop()
pour s’assurer que l’option est bien visible.
Ensuite, on mémorise l’option sélectionnée pour pouvoir la réutiliser lorsque la fonction sera de nouveau appelée pour une option différente. Enfin, l’option reçoit le focus pour garantir la restitution de sa valeur par les lecteurs d’écran.
Pour informer les personnes voyant l’écran, on peut utiliser le même sélecteur d’attribut CSS [aria-selected=true]
de la manière suivante :
Associer état et style est une bonne initiative ; ça permet d’assurer que les changements d’état sont restitués de manière interopérable. La forme devrait découler de la fonction, assurant ainsi directement leur synchronisation.
Filtrer les options
Un filtre performant passe outre les erreurs typographiques mineures ou l’utilisation d’une casse hétérogène.Petit rappel : n’oubliez pas que les données qui génèrent les suggestions se trouvent à l’intérieur des éléments option
.
Comme je l’ai mentionné plus haut, la fonction getOptions()
est appelée lorsqu’il faut remplir le menu avec des suggestions pertinentes.
Cette méthode prend la valeur saisie par l’utilisateur ou l’utilisatrice comme paramètre. Elle parcourt ensuite chaque option
avant de comparer les caractères saisis avec le contenu de l’option (c’est-à-dire le texte indiqué à l’intérieur de la balise).
C’est indexOf()
qui vérifie si la saisie contient une occurrence de la valeur renseignée. Dans la pratique, ça permet de saisir des noms de pays incomplets et quand même se voir proposer des suggestions pertinentes.La valeur est extraite et transposée en minuscules, ce qui veut dire que les options s’afficheront même si, par exemple, le verrouillage de la majuscule a été activé. Les utilisateurs et utilisatrices ne devraient pas avoir à résoudre ces problèmes s’il est possible d’y remédier automatiquement.Chaque saisie correspondant à une option existante est ajoutée à un tableau de correspondances, qui sera utilisé à l’appel de la fonction pour remplir le menu. Chaque option avec une correspondance est ajoutée à la matrice des correspondances, qui sera utilisée par la fonction d’appel pour remplir le menu.
Admettre les endonymes et les incohérences typographiques
Un endonyme est un nom qu’utilisent les personnes originaires d’une région ou d’un lieu donné pour désigner cette région ou ce lieu (ou la langue ou le peuple associés).Par exemple, « Allemagne » en allemand se dit « Deutschland ». En laissant les utilisateurs et utilisatrices saisir un endonyme, on respecte le cinquième principe de conception inclusive (« Donner le choix »).Pour ce faire, il faut d’abord répertorier ces endonymes quelque part. On peut les référencer dans un attribut de données au niveau de l’élément option
.
On peut maintenant modifier la fonction filter
pour vérifier la valeur alternative :
Si vous voulez, vous pouvez aussi utiliser cet attribut pour référencer les erreurs typographiques les plus courantes.
Démonstration
Maintenant que vous savez tout, voici une démonstration.
Traduction : Eleanor Hac.
1 commentaire
Les commentaires sont désormais fermés, mais vous pouvez toujours nous contacter pour réagir à cet article !
Bonjour et merci pour cette traduction.
D'abord, j'aime bien l’idée de la zone Live pour annoncer la disponibilité des résultats et leurs mise à jour, mais il y a un aspect dans cette approche que je ne comprend pas et vous saurez peut-être m'éclairer.
On est presque sur une implémentation classique du Design Pattern Combobox WAI-Aria, seulement, je ne comprend pas l’utilité du champ Select caché, c’est juste une histoire de soumission de formulaire ? Peut-être que le champ texte ne peut pas le gérer ici parce qu’on surcharge son role natif avec le role Combobox mais c’est aussi pour ça que l’on fait habituellement porter le role Combobox par un container parent à l’Input (et frère de la listbox).