Cómo mejoramos la accesibilidad de nuestro menú de aplicaciones de una sola página

Recientemente comencé a trabajar en una aplicación web progresiva (PWA) para un cliente con mi equipo. Estamos usando React con enrutamiento del lado del cliente a través de React Router , y uno de los primeros elementos que creamos fue el menú principal. Los menús son un componente clave de cualquier sitio o aplicación. Así es como se mueve la gente, por lo que hacerlo accesible era una prioridad muy alta para el equipo.
Pero en el proceso, aprendimos que crear un menú principal accesible en una PWA no es tan obvio como podría parecer. Pensé en compartir algunas de esas lecciones contigo y cómo las superamos.
En lo que respecta a los requisitos, queríamos un menú en el que los usuarios pudieran navegar no solo usando un mouse, sino también usando un teclado, el criterio de aceptación era que un usuario debería poder desplazarse por los elementos del menú de nivel superior y los submenús. -elementos de menú que de otro modo solo serían visibles si un usuario pasara el mouse sobre un elemento de menú de nivel superior. Y, por supuesto, queríamos un anillo de enfoque que siguiera los elementos que tienen enfoque.
Lo primero que tuvimos que hacer fue actualizar el CSS existente que estaba configurado para revelar un submenú cuando se coloca el cursor sobre un elemento del menú de nivel superior. Anteriormente estábamos usando la visibility
propiedad, cambiando entre visible
y hidden
sobre el estado suspendido del contenedor principal. Esto funciona bien para los usuarios de mouse, pero para los usuarios de teclado, el foco no se mueve automáticamente a un elemento configurado en visibility: hidden
(lo mismo se aplica a los elementos dados display: none
). Entonces eliminamos la visibility
propiedad y en su lugar usamos un valor de posición negativo muy grande:
.menu-item { position: relative;}.sub-menu { position: absolute left: -100000px; /* Kicking off the page instead of hiding visiblity */}.menu-item:hover .sub-menu { left: 0;}
Esto funciona perfectamente bien para los usuarios de mouse. Pero para los usuarios de teclado, el submenú todavía no estaba visible a pesar de que el foco estaba dentro de ese submenú. Para que el submenú sea visible cuando un elemento dentro de él tiene el foco, necesitábamos hacer uso de :focus
y :focus-within
en el contenedor principal:
.menu-item { position: relative;}.sub-menu { position: absolute left: -100000px;}.menu-item:hover .sub-menu,.menu-item:focus .sub-menu,.menu-item:focus-within .sub-menu { left: 0;}
Este código actualizado permite que aparezcan los submenús a medida que se enfoca cada uno de los enlaces dentro de ese menú. Tan pronto como el foco pasa al siguiente submenú, el primero se oculta y el segundo se vuelve visible. ¡Perfecto! Consideramos que esta tarea estaba completa, por lo que se creó una solicitud de extracción y se fusionó con la rama principal.
Pero luego usamos el menú nosotros mismos al día siguiente durante la preparación para crear otra página y nos encontramos con un problema. Al seleccionar un elemento del menú, independientemente de si se trata de un clic o una pestaña, el menú en sí no se oculta. Los usuarios del mouse tendrían que hacer clic a un lado en algún espacio en blanco para borrar el foco, ¡y los usuarios del teclado estaban completamente atascados! No pudieron presionar la esctecla para despejar el enfoque, ni ninguna otra combinación de teclas. En cambio, los usuarios del teclado tendrían que presionar la tabtecla suficientes veces para mover el foco a través del menú y a otro elemento que no causara que un gran menú desplegable oscureciera su vista.
La razón por la que el menú permanecería visible es porque el elemento del menú seleccionado mantuvo el foco. El enrutamiento del lado del cliente en una aplicación de página única (SPA) significa que solo se actualizará una parte de la página; no hay una recarga de página completa.
Hubo otro problema que notamos: era difícil para un usuario de teclado usar nuestro enlace “Saltar al contenido”. Los usuarios web normalmente esperan que al presionar la tabtecla una vez se resalte el enlace “Saltar al contenido”, pero nuestra implementación del menú no cumplió con eso. Tuvimos que idear un patrón para replicar de manera efectiva la “limpieza de enfoque” que los navegadores nos brindarían de forma gratuita al recargar la página completa.
La primera opción que probamos fue la más fácil: agregar un onClick
accesorio al componente de React Router Link
, llamando document.activeElement.blur()
cuando se selecciona un enlace en el menú:
const Menu = () = { const clearFocus = () = { document.activeElement.blur(); } return ( ul className="menu" li className="menu-item" Link to="/" onClick={clearFocus}Home/Link /li li className="menu-item" Link to="/products" onClick={clearFocus}Products/Link ul className="sub-menu" li Link to="/products/tops" onClick={clearFocus}Tops/Link /li li Link to="/products/bottoms" onClick={clearFocus}Bottoms/Link /li li Link to="/products/accessories" onClick={clearFocus}Accessories/Link /li /ul /li /ul );}
Este enfoque funcionó bien para “cerrar” el menú después de hacer clic en un elemento. Sin embargo, si un usuario del teclado presiona la tabtecla después de seleccionar uno de los enlaces del menú, el siguiente enlace se enfocará. Como se mencionó anteriormente, presionar la tabtecla después de un evento de navegación idealmente se enfocaría primero en el enlace “Saltar al contenido”.
En este punto, sabíamos que íbamos a tener que forzar mediante programación el enfoque en otro elemento, preferiblemente uno que esté en lo alto del DOM. De esa manera, cuando un usuario comienza a tabular después de un evento de navegación, llegará a la parte superior de la página o cerca de ella, similar a una recarga de página completa, lo que hace que sea mucho más fácil acceder al enlace de salto.
Inicialmente intentamos forzar el enfoque en el body
elemento en sí, pero esto no funcionó porque el cuerpo no es algo con lo que el usuario pueda interactuar. No había manera de que recibiera atención.
La siguiente idea fue forzar el enfoque en el logotipo en el encabezado, ya que este en sí mismo es solo un enlace a la página de inicio y puede recibir enfoque. Sin embargo, en este caso particular, el logotipo estaba debajo del enlace “Saltar al contenido” en el DOM, lo que significa que un usuario tendría que hacer shift+ tabpara acceder a él. No es bueno.
Finalmente decidimos que teníamos que representar un elemento con el que se pudiera interactuar, por ejemplo, un elemento ancla, en el DOM, en un punto que esté por encima del enlace “Saltar al contenido”. Este nuevo elemento ancla tendrá un estilo que lo hará invisible y que los usuarios no podrán concentrarse en él mediante interacciones web “normales” (es decir, se eliminará del flujo de pestañas normal). Cuando un usuario selecciona un elemento del menú, el foco se forzaría mediante programación a este nuevo elemento de anclaje, lo que significa que al presionar tabnuevamente se enfocaría directamente en el enlace “Saltar al contenido”. También significaba que el submenú se ocultaría inmediatamente una vez que se seleccionara un elemento del menú.
const App = () = { const focusResetRef = React.useRef(); const handleResetFocus = () = { focusResetRef.current.focus(); }; return ( Fragment a ref={focusResetRef} href="javascript:void(0)" tabIndex="-1" style={{ position: "fixed", top: "-10000px" }} aria-hidden Focus Reset/a a href="#main" className="jump-to-content-a11y-styles"Jump To Content/a Menu onSelectMenuItem={handleResetFocus} / ... /Fragment )}
Algunas notas de este nuevo elemento ancla “Focus Reset”:
href
está configuradojavascript:void(0)
para que si un usuario logra interactuar con el elemento, en realidad no sucede nada. Por ejemplo, si un usuario presiona la returntecla inmediatamente después de seleccionar un elemento del menú, eso desencadenará la interacción. En ese caso, no queremos que la página haga nada ni que cambie la URL.tabIndex
está configurado-1
para que un usuario no pueda “normalmente” mover el foco a este elemento. También significa que la primera vez que un usuario presiona la tabtecla al cargar una página, este elemento no estará enfocado, sino el enlace “Saltar al contenido”.style
simplemente mueve el elemento fuera de la ventana gráfica. La configuración enposition: fixed
garantiza que se elimine del flujo de documentos, por lo que no hay ningún espacio vertical asignado al elemento.aria-hidden
les dice a los lectores de pantalla que este elemento no es importante, así que no lo anuncie a los usuarios
¡Pero pensamos que podríamos mejorar esto aún más! Imaginemos que tenemos un mega menú y el menú no se oculta automáticamente cuando un usuario del mouse hace clic en un enlace. Eso va a causar frustración. Un usuario tendrá que mover con precisión el mouse a una sección de la página que no contiene el menú para borrar el :hover
estado y, por lo tanto, permitir que el menú se cierre.
Lo que necesitamos es “forzar el borrado” del estado de desplazamiento. Podemos hacerlo con la ayuda de React y una clearHover
clase:
// Menu.jsxconst Menu = (props) = { const { onSelectMenuItem } = props; const [clearHover, setClearHover] = React.useState(false); const closeMenu= () = { onSelectMenuItem(); setClearHover(true); } React.useEffect(() = { let timeout; if (clearHover) { timeout = setTimeout(() = { setClearHover(false); }, 0); // Adjust this timeout to suit the applications' needs } return () = clearTimeout(timeout); }, [clearHover]); return ( ul className={`menu ${clearHover ? "clearHover" : ""}`} li className="menu-item" Link to="/" onClick={closeMenu}Home/Link /li li className="menu-item" Link to="/products" onClick={closeMenu}Products/Link ul className="sub-menu" {/* Sub Menu Items */} /ul /li /ul );}
Este código actualizado oculta el menú inmediatamente cuando se hace clic en un elemento del menú. También se oculta inmediatamente cuando un usuario del teclado selecciona un elemento del menú. Al presionar la tabtecla después de seleccionar un enlace de navegación, el foco se mueve al enlace “Saltar al contenido”.
En este punto, nuestro equipo había actualizado el componente del menú hasta un punto en el que estábamos muy contentos. Tanto los usuarios de teclado como de mouse obtienen una experiencia consistente, y esa experiencia sigue lo que hace un navegador de manera predeterminada para recargar una página completa.
Nuestra implementación real es ligeramente diferente al ejemplo aquí, por lo que podríamos usar el patrón en otros proyectos. Lo colocamos en un contexto de React, con el proveedor configurado para envolver el componente de encabezado y el elemento Focus Reset se agrega automáticamente justo antes del del proveedor children
. De esa manera, el elemento se coloca antes del enlace “Saltar al contenido” en la jerarquía DOM. También nos permite acceder a la función de reseteo del enfoque con un simple gancho, en lugar de tener que taladrarlo.
Hemos creado un Code Sandbox que te permite jugar con las tres soluciones diferentes que cubrimos aquí. Definitivamente verás los puntos débiles de la implementación anterior y luego verás cuánto mejor se siente el resultado final.
¡Nos encantaría escuchar comentarios sobre esta implementación! Creemos que funcionará bien, pero aún no se ha lanzado al mercado, por lo que no tenemos datos definitivos ni comentarios de los usuarios. Ciertamente no somos todos expertos , simplemente hacemos lo mejor que podemos con lo que sabemos y somos muy abiertos y dispuestos a aprender más sobre el tema.
Deja un comentario