Navigation spatiale (focus sur le pavé directionnel)

Fonctionnalités Web : scrollIntoView()

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 :

  1. 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.
  2. 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).
  3. 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.
  4. 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 appelant focus().

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 cibles up ou left.
  • 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 right doit avoir candidate.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 * perpendicularCentreOffset aux candidats restants et choisissez le minimum. Utilisez une pondération w >= 2 comme 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.defaultPrevented est 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. Utilisez e.composedPath()[0] plutôt que e.target, car e.target peut être reciblé vers un hôte fantôme. Une vérification pratique de la possibilité de modification/du contrôle est target.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 le tabindex itinérant, appelez focus() et utilisez scrollIntoView({ 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.