Problema y casos de uso
Una navegación de documento completa reemplaza el documento y su título. Una navegación suave en una SPA mantiene el mismo documento, por lo que el foco puede permanecer en un enlace cuyo contenido circundante desapareció. Un usuario de lector de pantalla podría no escuchar ninguna indicación útil de que un nuevo producto, paso del checkout o página de error está listo.
La política debe cubrir cinco transiciones distintas: carga inicial, un cambio de ruta iniciado por el usuario, un filtro o reordenamiento in situ, una navegación reemplazada o cancelada, y Atrás/Adelante del navegador. También debe coexistir con el comportamiento propio de los componentes, como los diálogos. El objetivo es un contexto predecible, no mover el foco tras cada mutación de la URL.
Qué evalúa el entrevistador
El entrevistador busca un modelo de decisión semántico. "Hacer foco en el encabezado en cada cambio de ruta" suena accesible, pero rompe los filtros, los cambios de hash, la hidratación inicial y la restauración del historial. Una respuesta sólida primero decide si la tarea del usuario ha cambiado.
También esperan conocimientos del navegador: un encabezado puede recibir foco programático con tabIndex={-1} sin unirse al orden secuencial de Tab; focus() normalmente desplaza el elemento; preventScroll separa la restauración del foco de la del desplazamiento (scroll); y los valores positivos de tabindex crean un orden de foco frágil.
Finalmente, la respuesta debe manejar la concurrencia. Si la ruta B se resuelve después de la ruta C, B no debe actualizar el título ni robar el foco. Las comprobaciones automatizadas pueden verificar el elemento activo, el título y el orden, pero no pueden probar lo que anuncia cada combinación de navegador y tecnología de asistencia.
Preguntas clarificadoras antes de responder
- ¿Qué transiciones inician una nueva tarea? De producto a checkout sí lo hace; cambiar un criterio de ordenación normalmente no. Esto determina si el foco se mueve o se queda en el control iniciador.
- ¿Cuándo se confirma (commit) una ruta? El foco solo debe ejecutarse cuando la ruta ganadora haya renderizado su encabezado principal final, no en el clic, en la mutación de la URL o en la visualización del skeleton.
- ¿Quién controla las superposiciones (overlays)? Un diálogo mantiene su propio foco inicial, su trampa de foco y su contrato de retorno de foco. La política de rutas no debe competir con él.
- ¿Qué restaura el historial? Decide si el producto requiere foco, scroll o ambos por cada entrada del historial. Ambos necesitan coordinación para evitar un doble salto.
- ¿Qué navegadores y tecnologías de asistencia se soportan? El comportamiento de los anuncios varía, por lo que la matriz de compatibilidad debe ser explícita.
- ¿Puede cada ruta proporcionar un título descriptivo y un encabezado principal? Haz que estos formen parte del contrato de la ruta; utiliza el landmark principal solo como un respaldo controlado.
Estructura de respuesta en 30 segundos
"Clasifico la navegación antes de mover el foco. La carga inicial no hace nada. Un cambio de tarea iniciado por el usuario y confirmado actualiza el título del documento y enfoca el nuevo H1 descriptivo, que se vuelve enfocable programáticamente con tabIndex=-1. Los filtros dentro de la misma tarea conservan el control y notifican los resultados completados a través de una región de estado 'polite' persistente. Cada navegación tiene un token, de modo que solo la última ruta confirmada pueda aplicar el título y el foco. En Atrás/Adelante, restauro un objetivo semántico válido guardado para esa entrada del historial; de lo contrario, uso el H1, coordinando el foco con la restauración del scroll. Pruebo la carga inicial, navegaciones rápidas, errores, filtros, historial, orden de teclado, foco visible y lectores de pantalla reales".
Análisis detallado paso a paso
Comienza con una tabla de transiciones:
| Transición | Acción de foco | Anuncio |
|---|---|---|
| Carga inicial o hidratación | No forzar el foco | Comportamiento nativo del documento/título |
| PUSH a una nueva tarea | Enfocar el H1 de la ruta confirmada | Título actualizado más el encabezado enfocado |
| Filtro, ordenamiento o paginación en la misma tarea | Conservar el control iniciador | Actualización de resultado/estado 'polite' si es útil |
| POP desde Atrás/Adelante | Restaurar objetivo válido guardado; si no, H1 | Contexto restaurado o encabezado enfocado |
| Redirección o ruta de error | Enfocar el H1 confirmado de esa ruta | Su título y encabezado finales |
| Apertura/cierre de diálogo | Delegar al contrato del diálogo | Etiqueta del diálogo y objetivo de retorno |
El contrato de la ruta suministra un título final, una referencia al H1 descriptivo y una clave de ruta semántica. Asígnale al H1 tabIndex={-1}. Permanece fuera de la secuencia normal de Tab, pero el código puede enfocarlo. Mantén un indicador de foco visible. Un enlace de salto (skip link) sigue siendo el primer control enfocable por teclado y apunta a main; resuelve el bypass de navegación repetida y sigue teniendo valor cuando se gestiona el foco de la ruta.
El foco pertenece al límite de confirmación (commit boundary). Asigna a cada intento de navegación un token monótonamente creciente. Cuando los datos y la UI de una ruta terminen, aplica los efectos solo si su token sigue siendo el actual y el H1 está montado. No utilices un temporizador fijo: la velocidad de la red y el trabajo de renderizado lo hacen poco fiable.
beginNavigation(kind):
token = nextToken()
rememberCurrentFocus(historyEntryKey)
commitRoute(token, kind, title, heading):
if token != currentToken or heading is not connected:
return
document.title = title
if kind is PUSH or semantic-task REPLACE:
heading.focus()
else if kind is POP:
focus(validSavedTarget(historyEntryKey) or heading)Guarda un ID de foco local de la ruta que sea estable en lugar de una ruta CSS generada. En un POP, restáuralo solo si el elemento todavía existe, es visible, está habilitado y es significativo en el estado restaurado. De lo contrario, utiliza el H1. Si el router restaura el scroll de forma independiente, utiliza focus({ preventScroll: true }) y luego restaura el scroll una sola vez; para un PUSH normal, permitir que el foco revele el H1 suele ser más claro.
Evita la duplicación de voz. Enfocar el nuevo H1 tras actualizar el título a menudo proporciona suficiente contexto, aunque la lectura exacta varía según la tecnología de asistencia. No anuncies además el mismo título en una región 'live' por defecto. Para un filtro que mantiene el foco, una región persistente con role="status" o aria-live="polite" puede anunciar el recuento final de resultados. Reserva los anuncios asertivos ('assertive') para información genuinamente urgente, ya que pueden interrumpir el habla actual.
El oráculo de pruebas sigue la política. En la hidratación inicial, la aplicación no debe robar el foco. En un PUSH, tras la confirmación final, document.activeElement es el nuevo H1, el título es definitivo y el siguiente Tab llega al primer elemento interactivo lógico. En un cambio de filtro, el disparador retiene el foco y el texto de estado se actualiza una vez. En una carrera A→B→C donde B se resuelve al final, C mantiene el título y el foco. Las rutas de error y redirección enfocan su propio encabezado. POP restaura un objetivo válido guardado o recurre a una alternativa de forma determinista.
Ejecuta comprobaciones automatizadas del DOM para todos esos casos, incluidos los desmontajes de objetivos y los diseños con zoom o movimiento reducido. Luego realiza un recorrido manual con teclado y combinaciones compatibles como VoiceOver/Safari y NVDA o JAWS con un navegador soportado. Verifica el foco visible, el orden lógico, que no haya scrolls inesperados y que los anuncios sean comprensibles. Registra las versiones del navegador y de la tecnología de asistencia porque la salida de voz es un resultado de integración, no una garantía del DOM.
Ejemplo de respuesta de alta calidad
"Haría que el comportamiento del foco fuera parte del contrato semántico del router. Cada ruta expone un título final del documento y una referencia a un H1 descriptivo. Clasifico la transición: la hidratación inicial no mueve el foco; un PUSH iniciado por el usuario que cambia la tarea enfoca el H1 final tras la confirmación; un filtro u ordenamiento conserva el control iniciador; y POP intenta restaurar un objetivo estable guardado antes de recurrir al H1.
El H1 tiene tabIndex=-1, por lo que se puede enfocar programáticamente sin agregar una parada de Tab adicional. Preservo un estilo de foco visible y mantengo un enlace de salto que apunta a main. El título y el foco cambian juntos solo después de que se confirma la ruta ganadora. Cada navegación recibe un token, y se ignora una finalización asíncrona anterior, lo que evita que una ruta cancelada robe el foco.
Para el historial, almaceno un ID de foco semántico local a la ruta por cada entrada del historial. Restauro solo un objetivo conectado, visible y habilitado. Si la restauración del scroll es independiente, enfoco con preventScroll y restauro el scroll una vez. Las actualizaciones asíncronas de la misma tarea utilizan una región de estado 'polite' persistente; evito repetir el título de la ruta tanto en el encabezado como en la región 'live'.
Automatizaría las aserciones para el elemento activo, el título, el orden de Tab, la retención de foco en filtros, la navegación rápida A→B→C, redirecciones, errores y el fallback de POP. Luego probaría la voz real, el foco visible y el desplazamiento con la matriz soportada de teclado y lectores de pantalla. Eso separa el comportamiento determinista del DOM del comportamiento de la tecnología de asistencia que debemos observar".
Errores comunes
- Enfocar en cada cambio de URL → los filtros y cambios de hash interrumpen la tarea → clasifica primero las transiciones semánticas.
- Enfocar cuando se hace clic en el enlace → el destino puede no existir o haber sido cancelado → enfoca solo la ruta ganadora confirmada.
- Usar un temporizador (timeout) → los renders lentos y rápidos compiten de manera diferente → utiliza el ciclo de vida de la ruta y un token de navegación.
- Enfocar
bodyo agregartabindexpositivo → el contexto y el orden se vuelven confusos → utiliza un H1 descriptivo contabIndex=-1. - Saltar siempre al H1 en POP → volver atrás pierde la posición del usuario → restaura un objetivo de historial válido con un fallback determinista.
- Anunciar el título dos veces → los usuarios escuchan voz redundante → prioriza el contexto del encabezado enfocado y usa estado 'live' para actualizaciones in situ.
- Tratar un escaneo automatizado de accesibilidad como prueba definitiva → no puede validar la salida hablada real → agrega pruebas manuales con teclado y tecnologías de asistencia.
Preguntas de seguimiento y respuestas
Pregunta de seguimiento 1: ¿Por qué no enfocar siempre el landmark main?
Un H1 suele proporcionar un nombre más específico para la nueva tarea. main es un respaldo razonable cuando el contrato de la ruta no puede suministrar un encabezado, pero hacer que cada ruta proporcione uno mejora la estructura visible, el esquema del documento y la capacidad de prueba.
Pregunta de seguimiento 2: ¿La navegación con el ratón también debería mover el foco?
Si una acción iniciada por el usuario reemplaza la tarea principal, mantener un contexto coherente es útil independientemente del dispositivo de entrada, incluidos los usuarios de lectores de pantalla que hacen clic. Basa la decisión en la navegación semántica, no en una suposición sobre el uso del teclado. Evita mover el foco en actualizaciones en segundo plano.
Pregunta de seguimiento 3: ¿Qué sucede si B termina después de C en una navegación rápida?
El token de B ya no coincide con la navegación actual, por lo que su efecto de confirmación retorna sin cambiar el título, el foco o el estado. Cancelar la solicitud de B ahorra trabajo, pero la comprobación del token sigue siendo necesaria porque la cancelación puede ser tardía o no estar soportada.
Pregunta de seguimiento 4: ¿Cómo interactúan el foco y la restauración del scroll?
Normalmente focus() puede desplazar el objetivo a la vista. En un POP, si el router restaura por separado una posición de scroll guardada, utiliza preventScroll, valida y enfoca el elemento guardado, y luego realiza una única restauración de scroll. Define un único propietario para que el código de foco y el del router no entren en conflicto.
Pregunta de seguimiento 5: ¿Cuándo debe ser asertiva una región 'live'?
Solo cuando el retraso en escucharla genera un problema grave, como un mensaje urgente de sesión o seguridad. Los recuentos de resultados ordinarios y las actualizaciones de finalización deben ser 'polite'. El contexto de la ruta proviene normalmente del título final y del encabezado enfocado, evitando un segundo anuncio.
Pregunta de seguimiento 6: ¿Garantiza esto por sí solo el cumplimiento de las WCAG?
No. Ayuda a lograr un orden de foco y un contexto comprensibles en una aplicación dinámica, pero el cumplimiento también depende de la semántica, los nombres, la operación por teclado, el contraste, el manejo de errores y otros criterios. Prueba el recorrido completo del usuario frente al objetivo de conformidad del producto.