Creación de sistemas de menús accesibles

Publicado: 2022-03-10
Resumen rápido ↬ Hay muchos tipos diferentes de menú en la web. Crear experiencias inclusivas es una cuestión de usar los patrones de menú correctos en los lugares correctos, con el marcado y el comportamiento correctos.

Nota del editor : este artículo apareció originalmente en Inclusive Components. Si desea obtener más información sobre artículos de componentes inclusivos similares, siga a @inclusicomps en Twitter o suscríbase a la fuente RSS. Al respaldar inclusive-components.design en Patreon, puede ayudar a que sea la base de datos más completa de componentes de interfaz robustos disponible.

La clasificación es difícil. Tome los cangrejos, por ejemplo. Los cangrejos ermitaños, los cangrejos de porcelana y los cangrejos herradura no son, taxonómicamente hablando, verdaderos cangrejos. Pero eso no impide que usemos el sufijo "cangrejo". Se vuelve más confuso cuando, con el tiempo y gracias a un proceso llamado carcinización , los cangrejos falsos evolucionan para parecerse más a los cangrejos verdaderos. Este es el caso de los cangrejos rey, que se cree que fueron cangrejos ermitaños en el pasado. ¡Imagina el tamaño de sus caparazones!

En diseño, a menudo cometemos el mismo error de dar el mismo nombre a diferentes cosas. Parecen similares, pero las apariencias pueden ser engañosas. Esto puede tener un efecto desafortunado en la claridad de su biblioteca de componentes. En términos de inclusión, también puede llevarlo a reutilizar un componente inapropiado desde el punto de vista semántico y conductual. Los usuarios esperarán una cosa y obtendrán otra.

El término "desplegable" nombra un ejemplo clásico. Muchas cosas se "desplegan" en las interfaces, incluido el conjunto de <option> s de un elemento <select> y la lista de enlaces revelados por JavaScript que constituyen un submenú de navegación. Mismo nombre; cosas bastante diferentes. (Algunas personas los llaman "pulldowns", por supuesto, pero no entremos en eso).

Los menús desplegables que constituyen un conjunto de opciones a menudo se denominan "menús", y quiero hablar de ellos aquí. Diseñaremos un menú verdadero , pero hay mucho que decir sobre los menús que no son realmente verdaderos en el camino.

¡Más después del salto! Continúe leyendo a continuación ↓

Empecemos con un cuestionario. ¿El cuadro de enlaces que cuelga de la barra de navegación en la ilustración es un menú?

Una barra de navegación incluye un enlace a la tienda, debajo del cual cuelga un conjunto de tres enlaces adicionales a disfraces para perros, planchas para gofres y orbes mágicos, respectivamente.
Una barra de navegación incluye un enlace a la tienda, debajo del cual cuelga un conjunto de tres enlaces adicionales a disfraces para perros, planchas para gofres y orbes mágicos, respectivamente. (Vista previa grande)

La respuesta es no, no es un verdadero menú.

Es una convención de larga data que los esquemas de navegación se componen de listas de enlaces. Una convención casi tan antigua dicta que la navegación secundaria debe proporcionarse como listas anidadas de enlaces. Si tuviera que eliminar el CSS para el componente ilustrado arriba, debería ver algo como lo siguiente, excepto en color azul y en Times New Roman.

Hablando semánticamente, las listas anidadas de enlaces son correctas en este contexto. Los sistemas de navegación son realmente tablas de contenido y así es como se estructuran las tablas de contenido. Lo único que realmente nos hace pensar en "menú" es el estilo de las listas anidadas y la forma en que se revelan al pasar el mouse o enfocar.

Ahí es donde algunos se equivocan y comienzan a agregar semántica WAI-ARIA: aria-haspopup="true" , role="menu" , role="menuitem" etc. Hay un lugar para estos, como veremos, pero no aquí. . Aquí hay dos razones por las cuales:

  1. Los menús de ARIA no están diseñados para la navegación sino para el comportamiento de la aplicación. Imagine el sistema de menús de una aplicación de escritorio.
  2. El enlace de nivel superior debe poder usarse como un enlace , lo que significa que no se comporta como un botón de menú.

Con respecto a (2): Al atravesar una región de navegación con submenús, uno esperaría que cada submenú apareciera al pasar el mouse o enfocar el enlace de "nivel superior" ("Comprar" en la ilustración). Esto revela el submenú y coloca sus propios enlaces en orden de enfoque. Con un poco de ayuda de JavaScript para capturar eventos de enfoque y desenfoque para mantener la apariencia de los submenús mientras sea necesario, alguien que use el teclado debería poder desplazarse por cada enlace de cada nivel, a su vez.

Los botones de menú que toman la aria-haspopup="true" no se comportan así. Se activan al hacer clic y no tienen otro propósito que revelar un menú secreto.

ebook descuentos bieten wir
Izquierda: un botón de menú etiquetado como 'menú' con un icono de flecha que apunta hacia abajo y el estado aria-expanded = false. Derecha: El mismo botón de menú pero con el menú abierto. Este botón está en el estado aria-expanded = true. (Vista previa grande)

Como se muestra en la imagen, si ese menú está abierto o cerrado debe comunicarse con aria-expanded . Solo debe cambiar este estado al hacer clic, no al enfocar. Los usuarios no suelen esperar un cambio de estado explícito en un mero evento de foco. En nuestro sistema de navegación, el estado realmente no cambia; es solo un truco de estilo. Desde el punto de vista del comportamiento, podemos desplazarnos por la navegación como si no se estuvieran produciendo tales trucos de mostrar/ocultar.

El problema con los submenús de navegación

Los submenús de navegación (o "desplegables" para algunos) funcionan bien con un mouse o teclado, pero no son tan atractivos cuando se trata de tocar. Cuando presiona el enlace "Comprar" de nivel superior en nuestro ejemplo por primera vez, le está diciendo que abra el submenú y siga el enlace.

Aquí hay dos posibles soluciones:

  1. Evite el comportamiento predeterminado de los enlaces de nivel superior ( e.preventDefault() ) y la secuencia de comandos en la semántica y el comportamiento completos del menú WAI-ARIA.
  2. Asegúrese de que cada página de destino de nivel superior tenga una tabla de contenido como alternativa al submenú.

(1) es insatisfactorio porque, como señalé anteriormente, este tipo de semántica y comportamiento no se esperan en este contexto, donde los enlaces son los controles del sujeto. Además, los usuarios ya no podían navegar a una página de nivel superior, si existe.

Nota al margen: ¿Qué dispositivos son dispositivos táctiles?

Es tentador pensar, "esta no es una gran solución, pero solo la agregaré para interfaces táctiles". El problema es: ¿cómo se detecta si un dispositivo tiene pantalla táctil?

Ciertamente no deberías equiparar "pantalla pequeña" con "toque activado". Habiendo trabajado en la misma oficina que la gente que hace pantallas táctiles para museos, puedo asegurarles que algunas de las pantallas más grandes que existen son pantallas táctiles. Las computadoras portátiles con teclado dual y entrada táctil también se están volviendo cada vez más prolíficas.

De la misma manera, muchos pero no todos los dispositivos más pequeños son dispositivos táctiles. En el diseño inclusivo, no puede darse el lujo de hacer suposiciones.

La resolución (2) es más inclusiva y sólida porque proporciona un "retroceso" para los usuarios de todas las entradas. Pero las comillas de miedo sobre el término alternativo aquí son bastante deliberadas porque en realidad creo que las tablas de contenido en la página son una forma superior de proporcionar navegación.

El galardonado equipo de Servicios Digitales Gubernamentales parece estar de acuerdo. Es posible que también los hayas visto en Wikipedia.

Las tablas de contenido de Gov.uk son mínimas con guiones como estilos de lista. Wikipedia proporciona un cuadro gris bordeado con elementos numerados. Ambos son contenidos etiquetados.
Las tablas de contenido de Gov.uk son mínimas con guiones como estilos de lista. Wikipedia proporciona un cuadro gris bordeado con elementos numerados. Ambos son contenidos etiquetados.

Tablas de contenido

Las tablas de contenido son navegación para páginas relacionadas o secciones de página y deben ser semánticamente similares a las regiones de navegación del sitio principal, utilizando un elemento <nav> , una lista y un mecanismo de etiquetado de grupo.

 <nav aria-labelledby="sections-heading"> <h2>Products</h2> <ul> <li><a href="/products/dog-costumes">Dog costumes</a></li> <li><a href="/products/waffle-irons">Waffle irons</a></li> <li><a href="/products/magical-orbs">Magical orbs</a></li> </ul> </nav> <!-- each section, in order, here -->

notas

  • En este ejemplo, estamos imaginando que cada sección es su propia página, como lo habría sido en el submenú desplegable.
  • Es importante que cada una de estas páginas de "Tienda" tenga la misma estructura, con esta tabla de contenido de "Productos" presente en el mismo lugar. La consistencia apoya la comprensión.
  • La lista agrupa los elementos y los enumera en la salida de tecnología de asistencia, como la voz sintética de un lector de pantalla.
  • El <nav> está etiquetado recursivamente por el encabezado usando aria-labelledby . Esto significa que la "navegación de productos" se anunciará en la mayoría de los lectores de pantalla al ingresar a la región mediante Tab . También significa que la "navegación de productos" se detallará en las interfaces de elementos del lector de pantalla, desde las cuales los usuarios pueden navegar directamente a las regiones.

Todo en una página

Si puede incluir todas las secciones en una página sin que se vuelva demasiado larga y ardua para desplazarse, aún mejor. Simplemente enlace al identificador hash de cada sección. Por ejemplo, href="#waffle-irons" debe apuntar a .

 <nav aria-labelledby="sections-heading"> <h2>Products</h2> <ul> <li><a href="#dog-costumes">Dog costumes</a></li> <li><a href="#waffle-irons">Waffle irons</a></li> <li><a href="#magical-orbs">Magical orbs</a></li> </ul> </nav> <!-- dog costumes section here --> <section tabindex="-1"> <h2>Waffle Irons</h2> </section> <!-- magical orbs section here -->

( Nota: algunos navegadores son deficientes para enviar el foco a fragmentos de páginas enlazadas. tabindex="-1" en el fragmento de destino, esto se soluciona).

Cuando un sitio tiene mucho contenido, una arquitectura de información cuidadosamente construida, expresada a través del uso liberal de "menús" de tablas de contenido, es infinitamente preferible a un sistema desplegable precario y difícil de manejar. No solo es más fácil de hacer receptivo y requiere menos código para hacerlo, sino que aclara las cosas: donde los sistemas desplegables ocultan la estructura, las tablas de contenido la dejan al descubierto.

Algunos sitios, incluido el gov.uk del Government Digital Service, incluyen páginas de índice (o "tema") que son simplemente tablas de contenido. Es un concepto tan poderoso que el popular generador de sitios estáticos Hugo genera dichas páginas de forma predeterminada.

Diagrama de estilo de árbol genealógico con página de inicio de tema en la parte superior con dos ramificaciones de página individuales. Cada una de las ramificaciones de página individuales tiene múltiples ramificaciones de sección de página
Diagrama de estilo de árbol genealógico con página de inicio de tema en la parte superior con dos ramificaciones de página individuales. Cada una de las ramificaciones de página individuales tiene múltiples ramificaciones de sección de página (vista previa grande)

La arquitectura de la información es una gran parte de la inclusión. Un sitio mal organizado puede ser tan técnicamente compatible como desee, pero aun así alejará a muchos usuarios, especialmente aquellos con discapacidades cognitivas o aquellos que están presionados por el tiempo.

Botones del menú de navegación

Si bien estamos en el tema de los menús falsos relacionados con la navegación, sería negligente de mi parte no hablar sobre los botones del menú de navegación. Es casi seguro que los haya visto indicados por un ícono de "hamburguesa" o "navicon" de tres líneas.

Incluso con una arquitectura de información reducida y solo un nivel de enlaces de navegación, el espacio en las pantallas pequeñas es escaso. Ocultar la navegación detrás de un botón significa que hay más espacio para el contenido principal en la ventana gráfica.

Un botón de navegación es lo más parecido que hemos estudiado hasta ahora a un verdadero botón de menú. Dado que tiene el propósito de alternar la disponibilidad de un menú al hacer clic, debería:

  1. Identifíquese como un botón, no como un enlace;
  2. Identificar el estado expandido o colapsado de su menú correspondiente (que, en términos estrictos, es solo una lista de enlaces).

mejora progresiva

Pero no nos adelantemos. Deberíamos tener en cuenta la mejora progresiva y considerar cómo funcionaría esto sin JavaScript.

En un documento HTML no mejorado, no hay mucho que pueda hacer con los botones (excepto los botones de envío, pero eso ni siquiera está estrechamente relacionado con lo que queremos lograr aquí). En cambio, ¿tal vez deberíamos comenzar con solo un enlace que nos lleve a la navegación?

 <a href="#navigation">navigation</a> <!-- some content here perhaps --> <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/content">Content</a></li> </ul> </nav>

No tiene mucho sentido tener el enlace a menos que haya mucho contenido entre el enlace y la navegación. Dado que la navegación del sitio casi siempre debe aparecer cerca de la parte superior del orden de origen, no es necesario. Así que, en realidad, un menú de navegación en ausencia de JavaScript debería ser simplemente... un poco de navegación.

 <nav> <ul> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/content">Content</a></li> </ul> </nav>

Mejora esto agregando el botón, en su estado inicial, y ocultando la navegación (usando el atributo hidden ):

 <nav> <button aria-expanded="false">Menu</button> <ul hidden> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/contact">Contact</a></li> </ul> </nav>

Algunos navegadores antiguos, ya sabes cuáles, no son compatibles con hidden , así que recuerda poner lo siguiente en tu CSS. Soluciona el problema porque display: none tiene el mismo efecto de ocultar el menú de las tecnologías de asistencia y eliminar los enlaces del orden de enfoque.

 [hidden] { display: none; }

Hacer todo lo posible para admitir software antiguo es, por supuesto, un acto de diseño inclusivo. Algunos no pueden o no quieren actualizar.

Colocación

Donde mucha gente se equivoca es al colocar el botón fuera de la región. Esto significaría que los usuarios de lectores de pantalla que se muevan a <nav> usando un acceso directo encontrarán que está vacío, lo que no es muy útil. Con la lista oculta de los lectores de pantalla, solo encontrarían esto:

 <nav> </nav>

Así es como podemos alternar el estado:

 var navButton = document.querySelector('nav button'); navButton.addEventListener('click', function() { let expanded = this.getAttribute('aria-expanded') === 'true' || false; this.setAttribute('aria-expanded', !expanded); let menu = this.nextElementSibling; menu.hidden = !menu.hidden; });

aria-controles

Como escribí en Aria-controls Is Poop, el atributo aria-controls , destinado a ayudar a los usuarios de lectores de pantalla a navegar desde un elemento de control a un elemento controlado, solo es compatible con el lector de pantalla JAWS. Así que simplemente no puedes confiar en él.

Sin un buen método para dirigir a los usuarios entre elementos, debe asegurarse de que uno de los siguientes sea cierto:

  1. El primer enlace de la lista expandida es el siguiente en orden de enfoque después del botón (como en el ejemplo de código anterior).
  2. El primer enlace se centra programáticamente en revelar la lista.

En este caso, recomendaría (1). Es mucho más simple ya que no tiene que preocuparse por volver a enfocar el botón y en qué evento (s) hacerlo. Además, actualmente no hay nada que advierta a los usuarios que su enfoque se moverá a un lugar diferente. En los menús verdaderos que discutiremos en breve, este es el trabajo de aria-haspopup="true" .

Emplear aria-controls realmente no hace mucho daño, excepto que hace que la lectura en los lectores de pantalla sea más detallada. Sin embargo, algunos usuarios de JAWS pueden esperarlo. Así es como se aplicaría, usando la id de la lista como cifra:

 <nav> <button aria-expanded="false" aria-controls="menu-list">Menu</button> <ul hidden> <li><a href="/">Home</a></li> <li><a href="/about">About</a></li> <li><a href="/shop">Shop</a></li> <li><a href="/contact">Contact</a></li> </ul> </nav>

Los roles de menu y menuitem

Un verdadero menú (en el sentido de WAI-ARIA) debe identificarse a sí mismo como tal utilizando el rol de menu (para el contenedor) y, por lo general, menuitem secundarios de elementos de menú (pueden aplicarse otros roles secundarios). Estos roles de padre e hijo trabajan juntos para proporcionar información a las tecnologías de asistencia. Así es como se puede aumentar una lista para tener una semántica de menú:

 <ul role="menu"> <li role="menuitem">Item 1</li> <li role="menuitem">Item 2</li> <li role="menuitem">Item 3</li> </ul>

Dado que nuestro menú de navegación comienza a comportarse como un menú "verdadero", ¿no deberían estar presentes?

La respuesta corta es no. La respuesta larga es: no, porque los elementos de nuestra lista contienen enlaces y los menuitem de elementos de menú no están destinados a tener descendientes interactivos. Es decir, son los controles de un menú.

Por supuesto, podríamos suprimir la semántica de lista de los <li> usando role="presentation" o role="none" (que son equivalentes) y colocar el rol de elemento de menuitem en cada enlace. Sin embargo, esto suprimiría la función de vínculo implícito. En otras palabras, el ejemplo a seguir se anunciaría como “Inicio, elemento de menú”, no “Inicio, enlace” o “Inicio, elemento de menú, enlace”. Los roles de ARIA simplemente anulan los roles de HTML.

 <!-- will be read as "Home, menu item" --> <li role="presentation"> <a href="/" role="menuitem">Home</a> </li>

Queremos que el usuario sepa que está usando un enlace y que puede esperar el comportamiento del enlace, por lo que esto no es bueno. Como dije, los menús verdaderos son para el comportamiento de la aplicación (impulsado por JavaScript).

Lo que nos queda es una especie de componente híbrido, que no es un menú real, pero al menos les dice a los usuarios si la lista de enlaces está abierta, gracias al estado aria-expanded . Este es un patrón perfectamente satisfactorio para los menús de navegación.

Nota al margen: El elemento <select>

Si ha estado involucrado en el diseño receptivo desde el principio, puede recordar un patrón en el que la navegación se condensaba en un elemento <select> para ventanas de visualización estrechas.

auricular con elemento de selección que muestra "casa" seleccionado en la parte superior de la ventana gráfica
Auricular con elemento de selección que muestra "casa" seleccionado en la parte superior de la ventana gráfica.

Al igual que con los botones de alternancia basados ​​en casillas de verificación que discutimos, usar un elemento nativo que se comporte de alguna manera según lo previsto sin secuencias de comandos adicionales es una buena opción para la eficiencia y, especialmente en dispositivos móviles, el rendimiento. Y los elementos <select> son una especie de menús, con una semántica similar al menú activado por botones que pronto construiremos.

Sin embargo, al igual que con el botón de alternancia de la casilla de verificación, estamos usando un elemento asociado con la entrada de datos, no simplemente con una elección. Es probable que esto cause confusión a muchos usuarios, especialmente porque este patrón usa JavaScript para hacer que la <option> seleccionada se comporte como un enlace. El cambio inesperado de contexto que esto provoca se considera una falla de acuerdo con el criterio 3.2.2 En entrada (Nivel A) de WCAG.

Menús verdaderos

Ahora que hemos tenido la discusión sobre los menús falsos y los cuasi-menús, ha llegado el momento de crear un menú verdadero , abierto y cerrado por un botón de menú verdadero. De ahora en adelante me referiré al botón y al menú juntos como simplemente un "botón de menú".

Pero, ¿en qué aspectos será cierto nuestro botón de menú? Bueno, será un componente de menú destinado a elegir opciones en la aplicación en cuestión, que implementa toda la semántica esperada y los comportamientos correspondientes para ser considerado convencional para tal herramienta.

Como ya se mencionó, estas convenciones provienen del diseño de aplicaciones de escritorio. Se necesita la atribución de ARIA y la gestión de enfoque gobernada por JavaScript para imitarlos por completo. Parte del propósito de ARIA es ayudar a los desarrolladores web a crear experiencias web ricas sin romper con las convenciones de usabilidad forjadas en el mundo nativo.

En este ejemplo, imaginamos que nuestra aplicación es una especie de juego o cuestionario. Nuestro botón de menú permitirá al usuario elegir un nivel de dificultad. Con toda la semántica en su lugar, el menú se ve así:

 <button aria-haspopup="true" aria-expanded="false"> Difficulty <span aria-hidden="true">&#x25be;</span> </button> <div role="menu"> <button role="menuitem">Easy</button> <button role="menuitem">Medium</button> <button role="menuitem">Incredibly Hard</button> </div>

notas

  • La propiedad aria-haspopup simplemente indica que el botón secreta un menú. Actúa como una advertencia de que, cuando se presiona, el usuario se moverá al menú "emergente" (cubriremos el comportamiento de enfoque en breve). Su valor no cambia, permanece como true en todo momento.
  • El <span> dentro del botón contiene el punto Unicode para un pequeño triángulo negro que apunta hacia abajo. Esta convención indica visualmente lo que aria-haspopup hace de manera no visual: que al presionar el botón se revelará algo debajo. La atribución aria-hidden="true" evita que los lectores de pantalla anuncien "triángulo que apunta hacia abajo" o similar. Gracias a aria-haspopup , no es necesario en el contexto no visual.
  • La propiedad aria-haspopup se complementa con aria-expanded . Esto le dice al usuario si el menú está actualmente en un estado abierto (expandido) o cerrado (contraído) alternando entre valores true y false .
  • El menú en sí toma el rol de menu (acertadamente llamado). Toma descendientes con el rol menuitem . No es necesario que sean elementos secundarios directos del elemento del menu , pero lo son en este caso, por simplicidad.

Comportamiento del teclado y el enfoque

Cuando se trata de hacer que el teclado de controles interactivos sea accesible, lo mejor que puede hacer es usar los elementos correctos. Debido a que estamos usando elementos <button> aquí, podemos estar seguros de que los eventos de clic se activarán al presionar las teclas Intro y Espacio , como se especifica en la interfaz HTMLButtonElement. También significa que podemos deshabilitar los elementos del menú usando la propiedad disabled asociada al botón.

Sin embargo, hay mucho más en la interacción del teclado con los botones del menú. Aquí hay un resumen de todo el enfoque y el comportamiento del teclado que vamos a implementar, basado en las prácticas de creación de WAI-ARIA 1.1:

Ingrese , Espacio o en el botón de menú Abre el menú
en un elemento del menú Mueve el foco al siguiente elemento del menú, o al primer elemento del menú si está en el último
en un elemento del menú Mueve el foco al elemento de menú anterior, o al último elemento de menú si está en el primero
en el botón de menú Cierra el menú si está abierto.
Esc en un elemento del menú Cierra el menú y enfoca el botón de menú

La ventaja de mover el foco entre los elementos del menú usando las teclas de flecha es que la pestaña se conserva para salir del menú. En la práctica, esto significa que los usuarios no tienen que moverse a través de cada elemento del menú para salir del menú, una gran mejora para la usabilidad, especialmente donde hay muchos elementos del menú.

La aplicación de tabindex="-1" hace que los elementos del menú no puedan ser enfocados por Tab , pero conserva la capacidad de enfocar los elementos mediante programación, al capturar pulsaciones de teclas en las teclas de flecha.

 <button aria-haspopup="true" aria-expanded="false"> Difficulty <span aria-hidden="true">&#x25be;</span> </button> <div role="menu"> <button role="menuitem" tabindex="-1">Easy</button> <button role="menuitem" tabindex="-1">Medium</button> <button role="menuitem" tabindex="-1">Incredibly Hard</button> </div>

El método abierto

Como parte de un buen diseño de API, podemos construir métodos para manejar varios eventos.

Por ejemplo, el método open debe cambiar el valor aria-expanded a "verdadero", cambiar la propiedad oculta del menú a false y enfocar el primer menuitem del menú que no está deshabilitado:

 MenuButton.prototype.open = function () { this.button.setAttribute('aria-expanded', true); this.menu.hidden = false; this.menu.querySelector(':not(\[disabled])').focus(); return this; }

Podemos ejecutar este método donde el usuario presiona la tecla hacia abajo en una instancia de botón de menú enfocada:

 this.button.addEventListener('keydown', function (e) { if (e.keyCode === 40) { this.open(); } }.bind(this));

Además, un desarrollador que use este script ahora podrá abrir el menú mediante programación:

 exampleMenuButton = new MenuButton(document.querySelector('\[aria-haspopup]')); exampleMenuButton.open();

Nota al margen: el truco de la casilla de verificación

En la medida de lo posible, es mejor no usar JavaScript a menos que sea necesario. Involucrar una tercera tecnología además de HTML y CSS es necesariamente un aumento en la complejidad y fragilidad del sistema. Sin embargo, no todos los componentes pueden construirse satisfactoriamente sin JavaScript en la combinación.

En el caso de los botones de menú, el entusiasmo por hacerlos “funcionar sin JavaScript” ha llevado a algo llamado el truco de la casilla de verificación. Aquí es donde se usa el estado marcado (o no marcado) de una casilla de verificación oculta para alternar la visibilidad de un elemento de menú usando CSS.

 /* menu closed */ [type="checkbox"] + [role="menu"] { display: none; } /* menu open */ [type="checkbox"]:checked + [role="menu"] { display: block; }

Para los usuarios de lectores de pantalla, la función de casilla de verificación y el estado marcado no tienen sentido en este contexto. Esto se puede solucionar en parte agregando role="button" a la casilla de verificación.

 <input type="checkbox" role="button" aria-haspopup="true">

Desafortunadamente, esto suprime la comunicación de estado marcada implícita, privándonos de retroalimentación de estado libre de JavaScript (pobre aunque hubiera sido como "marcada" en este contexto).

Pero es posible falsificar aria-expanded . Solo necesitamos suministrar nuestra etiqueta con dos tramos como se muestra a continuación.

 <input type="checkbox" role="button" aria-haspopup="true" class="vh"> <label for="toggle" data-opens-menu> Difficulty <span class="vh expanded-text">expanded&lt;/span> <span class="vh collapsed-text">collapsed</span> <span aria-hidden="true">&#x25be;</span> </label>

Ambos están visualmente ocultos usando la clase visually-hidden , pero, según el estado en el que nos encontremos, solo uno está oculto para los lectores de pantalla. Es decir, solo uno tiene display: none , y esto está determinado por el estado verificado existente (pero no comunicado):

 /* class to hide spans visually */ .vh { position: absolute !important; clip: rect(1px, 1px, 1px, 1px); padding: 0 !important; border: 0 !important; height: 1px !important; width: 1px !important; overflow: hidden; } /* reveal the correct state wording to screen readers based on state */ [type="checkbox"]:checked + label .expanded-text { display: inline; } [type="checkbox"]:checked + label .collapsed-text { display: none; } [type="checkbox"]:not(:checked) + label .expanded-text { display: none; } [type="checkbox"]:not(:checked) + label .collapsed-text { display: inline; }

Esto es inteligente y todo, pero nuestro botón de menú aún está incompleto ya que los comportamientos de enfoque esperados que hemos estado discutiendo simplemente no se pueden implementar sin JavaScript.

Estos comportamientos son convencionales y esperados, lo que hace que el botón sea más útil. Sin embargo, si realmente necesita implementar un botón de menú sin JavaScript, esto es lo más cerca que puede estar. Teniendo en cuenta que el botón de menú de navegación recortado que cubrí anteriormente ofrece contenido de menú que no depende de JavaScript en sí mismo (es decir, enlaces), este enfoque puede ser una opción adecuada.

Por diversión, aquí hay un codePen que implementa un botón de menú de navegación sin JavaScript.

Consulte el ejemplo del botón de menú Pen Navigation sin JS de Heydon (@heydon) en CodePen.

( Nota: Solo Espacio abre el menú.)

El evento "elegir"

La ejecución de algunos métodos debería emitir eventos para que podamos configurar oyentes. Por ejemplo, podemos emitir un evento de choose cuando un usuario hace clic en un elemento del menú. Podemos configurar esto usando CustomEvent , que nos permite pasar un argumento a la propiedad de detail del evento. En este caso, el argumento ("elección") sería el nodo DOM del elemento de menú elegido.

 MenuButton.prototype.choose = function (choice) { // Define the 'choose' event var chooseEvent = new CustomEvent('choose', { detail: { choice: choice } }); // Dispatch the event this.button.dispatchEvent(chooseEvent); return this; }

Hay todo tipo de cosas que podemos hacer con este mecanismo. Quizás tengamos una región en vivo configurada con una id de menuFeedback :

 <div role="alert"></div>

Ahora podemos configurar un oyente y llenar la región en vivo con la información secretada dentro del evento:

 exampleMenuButton.addEventListener('choose', function (e) { // Get the node's text content (label) var choiceLabel = e.details.choice.textContent; // Get the live region node var liveRegion = document.getElementById('menuFeedback'); // Populate the live region liveRegion.textContent = 'Your difficulty level is ${choiceLabel}'; });
Cuando un usuario elige una opción, el menú se cierra y el foco vuelve al botón de menú. Es importante que los usuarios vuelvan al elemento de activación después de cerrar el menú.
Cuando un usuario elige una opción, el menú se cierra y el foco vuelve al botón de menú. Es importante que los usuarios vuelvan al elemento de activación después de cerrar el menú. (Vista previa grande)

Cuando se selecciona un elemento del menú, el usuario del lector de pantalla escuchará: "Usted eligió [etiqueta del elemento del menú]" . Una región activa (definida aquí con la atribución role=“alert” ) anuncia su contenido en los lectores de pantalla cada vez que ese contenido cambia. La región en vivo no es obligatoria, pero es un ejemplo de lo que podría suceder en la interfaz como respuesta a que el usuario haga una elección en el menú.

Elecciones persistentes

No todos los elementos del menú son para elegir configuraciones persistentes. Muchos simplemente actúan como botones estándar que hacen que suceda algo en la interfaz cuando se presionan. Sin embargo, en el caso de nuestro botón de menú de dificultad, nos gustaría indicar cuál es la configuración de dificultad actual, la última elegida.

El atributo aria-checked="true" funciona para elementos que, en lugar de menuitem , toman el rol de menuitemradio . El marcado mejorado, con el segundo elemento marcado ( set ) se ve así:

 <button aria-haspopup="true" aria-expanded="false"> Difficulty <span aria-hidden="true">&#x25be;</span> </button> <div role="menu"> <button role="menuitemradio" tabindex="-1">Easy</button> <button role="menuitemradio" aria-checked="true" tabindex="-1">Medium</button> <button role="menuitemradio" tabindex="-1">Incredibly Hard</button> </div>

Los menús nativos en muchas plataformas indican los elementos elegidos mediante marcas de verificación. Podemos hacerlo sin problemas usando un poco de CSS adicional:

 [role="menuitem"] [aria-checked="true"]::before { content: '\2713\0020'; }

Mientras recorre el menú con un lector de pantalla en ejecución, al enfocar este elemento marcado aparecerá un anuncio como "marca de verificación, elemento de menú medio, marcado" .

El comportamiento al abrir un menú con un menuitemradio difiere ligeramente. En lugar de enfocar el primer elemento (habilitado) en el menú, el elemento marcado se enfoca en su lugar.

El botón de menú comienza con el menú sin abrir. Al abrir se enfoca el segundo ajuste de dificultad (Medio). Tiene el prefijo de una marca de verificación basada en la presencia del atributo aria-checked.
El botón de menú comienza con el menú sin abrir. Al abrir se enfoca el segundo ajuste de dificultad (Medio). Tiene el prefijo de una marca de verificación basada en la presencia del atributo aria-checked. (Vista previa grande)

¿Cuál es el beneficio de este comportamiento? Al usuario (cualquier usuario) se le recuerda su opción previamente seleccionada. En los menús con numerosas opciones incrementales (por ejemplo, un conjunto de niveles de zoom), las personas que operan por teclado se colocan en la posición óptima para realizar su ajuste.

Uso del botón de menú con un lector de pantalla

En este video, le mostraré cómo es usar el botón de menú con el lector de pantalla Voiceover y Chrome. El ejemplo utiliza elementos con menuitemradio , aria-checked y se analiza el comportamiento del foco. Se pueden esperar experiencias similares en toda la gama de software popular de lectores de pantalla.

Botón de menú inclusivo en Github

Kitty Giraudel y yo hemos trabajado juntos en la creación de un componente de botón de menú con las funciones de API que he descrito y más. Tiene que agradecer a Hugo por muchas de estas características, ya que se basaron en el trabajo que hicieron en a11y-dialog, un diálogo modal accesible. Está disponible en Github y NPM.

 npm i inclusive-menu-button --save

Además, Kitty ha creado una versión React para tu deleite.

Lista de Verificación

  • No utilice la semántica de menús de ARIA en los sistemas de menús de navegación.
  • En sitios con mucho contenido, no oculte la estructura en los menús de navegación anidados con menús desplegables.
  • Use aria-expanded para indicar el estado abierto/cerrado de un menú de navegación activado por botones.
  • Asegúrese de que dicho menú de navegación sea el siguiente en el orden de enfoque después del botón que lo abre/cierra.
  • Nunca sacrifiques la usabilidad en la búsqueda de soluciones libres de JavaScript. es vanidad.