La navigation directionnelle (à l'aide des touches fléchées ou des pavés directionnels) permet aux utilisateurs de naviguer dans les éléments interactifs en fonction de leur disposition visuelle en 2D, qu'ils utilisent un clavier ou un autre périphérique d'entrée. Il est très utile pour les interfaces utilisateur spatiales (comme les rails multimédias, les panneaux de tableau de bord ou les catalogues imbriqués), mais il doit être implémenté avec soin pour éviter de perturber les comportements natifs du navigateur, comme le défilement de la fenêtre d'affichage.
Ce guide explique comment créer une navigation directionnelle 2D performante et accessible dans des widgets composites.
Principes architecturaux de base
Lorsque vous implémentez une sélection spatiale personnalisée à l'aide des touches fléchées, respectez les règles de base suivantes :
- Limitez l'utilisation aux widgets composites : ne remplacez pas les touches fléchées dans le flux de texte normal d'un document ou dans les dispositions en liste verticales simples où la navigation séquentielle par défaut avec la touche Tabulation est attendue. Consultez les consignes de codage pour l'accessibilité pour obtenir des conseils généraux sur le flux de documents.
- Conserver le défilement natif des limites : s'il n'y a pas d'élément sélectionnable dans la direction de la pression, ne pas empêcher le comportement par défaut du navigateur. Laissez l'événement se propager pour que le navigateur puisse faire défiler la fenêtre d'affichage de manière native. Le remplacement des touches fléchées nuit à l'accessibilité (WCAG 1.4.10).
- Assurez la visibilité et l'alignement de la sélection : lorsque la sélection change, assurez-vous que le nouvel élément sélectionné est visible (par exemple, en utilisant
scrollIntoView({ block: 'nearest', inline: 'nearest' })) afin qu'il reste entièrement visible. - Coordonner avec le système de mise en page : déterminez les positions des candidats au ciblage à partir des géométries de délimitation actives (
getBoundingClientRect()) tout en gérant chaque pression de touche. Évitez de supposer que l'ordre du DOM ou une structure de grille fixe représente la présentation visuelle. Consultez Mises en page CSS et responsive design pour obtenir des recommandations sur les mises en page modernes.
Gestion accessible du focus
Le conteneur spatial doit exposer un seul taquet de tabulation à la navigation séquentielle par tabulation. Dans le widget, gérez le focus clavier à l'aide du modèle d'index de tabulation itinérant :
- L'élément actif possède
tabindex="0". - Tous les éléments inactifs ont
tabindex="-1". - Lors de la navigation, déplacez le focus de manière programmatique en déplaçant l'attribut
tabindex="0"et en appelantfocus().
Sélection visuelle des candidats
Dans les mises en page asymétriques, encapsulées ou en grille, le mappage basé sur l'index échoue. À la place, dérivez les candidats de la géométrie visuelle en direct pour la pression de touche actuelle :
- Candidates : utilisez les descendants sélectionnables du conteneur de widget qui transmettent
checkVisibility(). Les éléments masqués peuvent avoir un rectangle de taille nulle à(0, 0)et ne doivent pas devenir accidentellement des ciblesupouleft. - Géométrie : lisez
getBoundingClientRect()pour l'élément sélectionné et chaque candidat lors de la gestion de l'appui sur la touche. Ne mettez pas en cache ces rectangles de manière globale : la mise en page peut changer pour de nombreuses raisons, et un petit nombre de lectures géométriques est peu coûteux lors de la gestion d'un événement de touche au rythme de l'utilisateur. - Filtre de direction : un candidat n'est dans la direction de la pression que lorsque son bord proche se trouve au niveau ou au-delà du bord éloigné de l'élément actuel sur cet axe, ce qui permet une petite tolérance pour l'arrondi des sous-pixels. Par exemple, un candidat pour
rightdoit avoircandidate.left >= current.right - tolerance. Ne comparez pas les centres pour déterminer si un candidat se trouve dans la direction. - Candidats alignés en premier : parmi les candidats qui passent le filtre de direction, privilégiez ceux dont l'étendue sur l'axe perpendiculaire chevauche l'étendue de l'élément actuel. Choisissez l'écart le plus petit sur l'axe de navigation, en départageant les égalités avec le décalage central perpendiculaire le plus petit. Cela permet de maintenir la navigation dans la ligne ou la colonne visuelle adjacente, même lorsqu'un élément est exceptionnellement large.
- Score de remplacement : si aucun candidat ne chevauche l'axe perpendiculaire, attribuez le score
gap + w * perpendicularCentreOffsetaux candidats restants et choisissez le minimum. Utilisez une pondérationw >= 2comme point de départ, puis ajustez-la pour la mise en page. Une pondération plus élevée favorise les candidats qui sont plus étroitement alignés. - Aucun candidat : ne renvoie rien. Le gestionnaire d'événements ne doit pas appeler
preventDefault(), ce qui permet au navigateur de faire défiler la page de manière native.
L'implémentation exacte peut varier, mais conservez le test de direction basé sur les bords et appliquez la préférence de chevauchement avant le remplacement pondéré. C'est le cœur de l'algorithme. Évitez de présenter un cache ou une pondération fixe comme universellement correct.
Intégration des touches et coopération du défilement
Votre routeur d'événements doit empêcher le défilement standard de la fenêtre d'affichage uniquement lorsqu'une transition de sélection spatiale valide se produit :
- N'interceptez pas l'événement lorsqu'une touche de modification est enfoncée, lorsque
e.defaultPreventedest déjà défini sur "true" ou lorsque la cible d'événement la plus interne utilise elle-même les touches fléchées. Utiliseze.composedPath()[0]plutôt quee.target, care.targetpeut être reciblé vers un hôte fantôme. Une vérification pratique de la possibilité de modification/du contrôle esttarget.matches(':read-write, select, input[type="range"], [role="slider"]'). Les racines fantômes fermées ne peuvent pas être inspectées. - Limitez l'écouteur au conteneur du widget afin que plusieurs widgets spatiaux puissent coexister sur une page.
- N'agir que lorsque l'élément sélectionné est lui-même un candidat. Si les éléments contiennent leurs propres commandes sélectionnables, faites-en également des candidats ou laissez-les tels quels. Ne résolvez pas un parent avec
closest(). - Lorsqu'un candidat est trouvé, appelez
preventDefault(), mettez à jour letabindexitinérant, appelezfocus()et utilisezscrollIntoView({ block: 'nearest', inline: 'nearest' }). - Si aucun candidat n'est trouvé, renvoyez sans appeler
preventDefault()pour que le défilement natif se poursuive.
Évitez d'ajouter des raccourcis à un seul caractère tels que h/j/k/l ou W/A/S/D pour la navigation spatiale. Les touches fléchées ne sont pas des touches de caractères. Conserver le défilement natif lorsqu'aucun candidat n'existe évite de bloquer inutilement le mouvement de la fenêtre d'affichage.