Navegación espacial (enfoque del teclado direccional)

Funciones web: scrollIntoView()

La navegación con enfoque direccional (con las teclas de flecha o los pads direccionales) permite que los usuarios que solo usan el teclado o que usan entradas alternativas naveguen por los elementos interactivos según su diseño visual en 2D. Es muy beneficioso para las interfaces de usuario espaciales (como los rieles de contenido multimedia, los paneles de control o los catálogos anidados), pero se debe implementar con cuidado para evitar que se interrumpan los comportamientos nativos del navegador, como el desplazamiento del viewport.

En esta guía, se explica cómo compilar una navegación direccional en 2D accesible y de alto rendimiento dentro de widgets compuestos.


Principios básicos de arquitectura

Cuando implementes el enfoque espacial personalizado con las teclas de flecha, cumple con estas reglas fundamentales:

  1. Limita el uso de widgets compuestos: No anules las teclas de flecha en el flujo de texto normal del documento ni en los diseños de listas verticales simples en los que se espera la navegación secuencial predeterminada con la tecla Tab. Consulta los Lineamientos de programación de accesibilidad para obtener orientación general sobre el flujo de documentos.
  2. Conservar el desplazamiento nativo de los límites: Si no hay ningún elemento enfocable en la dirección presionada, no impidas el comportamiento predeterminado del navegador. Permite que el evento se propague para que el navegador pueda desplazar el viewport de forma nativa. Anular las teclas de flecha de forma incondicional interrumpe la accesibilidad (WCAG 1.4.10).
  3. Garantiza la visibilidad y la alineación del enfoque: Cuando cambie el enfoque, asegúrate de que el elemento que se enfocó recientemente se desplace a la vista (p.ej., con scrollIntoView({ block: 'nearest', inline: 'nearest' })) para que permanezca completamente visible.
  4. Coordinar con el sistema de diseño: Determina las posiciones de los candidatos de enfoque a partir de las geometrías de límite activas (getBoundingClientRect()) mientras se controla cada pulsación de tecla. Evita suponer que el orden del DOM o una estructura de cuadrícula fija representan el diseño visual. Consulta Diseños de CSS y diseño responsivo para obtener recomendaciones de diseño modernas.

Administración de focos accesible

El contenedor espacial debe exponer una sola tabulación para la navegación secuencial con la tecla Tab. Dentro del widget, administra el enfoque del teclado con el patrón de índice de tabulación móvil:

  • El elemento activo tiene tabindex="0".
  • Todos los elementos inactivos tienen tabindex="-1".
  • Cuando navegues, mueve el foco de manera programática cambiando el atributo tabindex="0" y llamando a focus().

Selección visual de candidatos

En los diseños asimétricos, ajustados o de cuadrícula, la asignación basada en índices falla. En su lugar, deriva los candidatos de la geometría visual en vivo para la presión de tecla actual:

  • Candidatos: Usa los descendientes enfocables del contenedor del widget que pasan checkVisibility(). Los elementos ocultos pueden tener un rectángulo de tamaño cero en (0, 0) y no deben convertirse en objetivos accidentales de up o left.
  • Geometría: Lee getBoundingClientRect() para el elemento enfocado y cada candidato mientras se controla la presión de la tecla. No almacenes en caché estos rectángulos de forma global: el diseño puede cambiar por muchos motivos, y una pequeña cantidad de lecturas de geometría es económica cuando se controla un evento clave al ritmo del usuario.
  • Filtro de dirección: Un candidato se encuentra en la dirección presionada solo cuando su borde cercano está en el borde lejano del elemento actual en ese eje o más allá de él, lo que permite una pequeña tolerancia para el redondeo de subpíxeles. Por ejemplo, un candidato para right debe tener candidate.left >= current.right - tolerance. No compares los centros para determinar si un candidato está en la dirección.
  • Primero los candidatos alineados: Entre los candidatos que pasan el filtro de dirección, se prefieren aquellos cuya extensión en el eje perpendicular se superpone con la extensión del elemento actual. Elige la brecha más pequeña en el eje de navegación y desempata con el desplazamiento central perpendicular más pequeño. Esto mantiene la navegación en la fila o columna visual adyacente, incluso cuando un elemento es inusualmente ancho.
  • Puntuación de resguardo: Si ningún candidato se superpone en el eje perpendicular, puntúa los candidatos restantes como gap + w * perpendicularCentreOffset y elige el mínimo. Usa una ponderación w >= 2 como punto de partida y, luego, ajústala para el diseño. Una ponderación mayor favorece a los candidatos que están más alineados.
  • Sin candidato: No se devuelve nada. El controlador de eventos no debe llamar a preventDefault(), lo que permite que el navegador se desplace de forma nativa.

La implementación exacta puede variar, pero mantén la prueba de dirección basada en el perímetro y aplica la preferencia de superposición antes del resguardo ponderado. Este es el punto crucial del algoritmo; evita presentar una caché o un peso fijo como universalmente correcto.


Integración de pulsaciones de teclas y cooperación de desplazamiento

El router de eventos debe evitar el desplazamiento estándar del viewport solo cuando se produce una transición de enfoque espacial válida:

  • No interceptes el evento cuando se mantenga presionada una tecla modificadora, cuando e.defaultPrevented ya sea verdadero o cuando el destino del evento más interno consuma las teclas de flecha. Usa e.composedPath()[0] en lugar de solo e.target, porque e.target se puede reorientar a un host de sombra. Una verificación práctica de editabilidad/control es target.matches(':read-write, select, input[type="range"], [role="slider"]'); las raíces de sombra cerradas no se pueden inspeccionar.
  • Limita el alcance del objeto de escucha al contenedor del widget para que puedan coexistir varios widgets espaciales en una página.
  • Actúa solo cuando el elemento enfocado es un candidato. Si los elementos contienen sus propios controles enfocables, haz que esos controles también sean candidatos o déjalos como están; no resuelvas un elemento superior con closest().
  • Cuando se encuentre un candidato, llama a preventDefault(), actualiza el tabindex itinerante, llama a focus() y usa scrollIntoView({ block: 'nearest', inline: 'nearest' }).
  • Cuando no se encuentre ningún candidato, devuelve el resultado sin llamar a preventDefault() para que continúe el desplazamiento nativo.

Evita agregar combinaciones de teclas de un solo carácter, como h/j/k/l o W/A/S/D, para la navegación espacial. Las teclas de flecha no son teclas de caracteres, y conservar el desplazamiento nativo cuando no hay ningún candidato evita bloquear innecesariamente el movimiento de la vista.