Uno de los problemas de accesibilidad que presentan los menús dentro de una aplicación, es la navegación por ellos mediante el uso de teclado haciendo uso de la tecla tab y dado que estos menús por lo general son creados para personas que pueden ver, entonces los desarrolladores crean menús muy bonitos usando CSS pero suelen usar display:none para ocultar los menús desplegables y esto puede que no lo detecte un lector de pantalla
Por esta razón y para mejorar la accesibilidad la W3C creo la iniciativa WAI-ARIA la cual por medio de sus atributos y roles ayudan a que las herramientas asistidas puedan reconocer los menús.
A continuación se mostraran dos ejemplos de menús accesibles usando ARIA, el primero será un ejemplo simple y el segundo será un ejemplo de un nivel más técnico que incluye scripts para mejorar la interacción del teclado con el menú.
Un menú dinámico puede incluir las siguientes etiquetas ARIA:
role="menubar" en el elemento raíz de cada uno de los menús desplegables, normalmente es un <ul>.
role="menu" en el elemento raiz de cada uno de los menús desplegables, normalmente es un <ul> o un <ol>.
role="menuitem" en cada elemento accionable del sistema de menús, incluye los de los menús, menús desplegables y submenús.
aria-haspopup="true" En cualquier elemento que active la visualización de un menú desplegable. Esto se aplica típicamente a todos los elementos de menú de nivel superior y a cualquier elemento de menú que tenga submenús.
aria-hidden="true" en cada elemento del contenedor del menú desplegable, normalmente un <ul> o un <div>. Cuando JavaScript se utiliza para hacer que el menu sea visible, también debe cambiar este atributo a aria-hidden= "false".
aria-expanded="false" se inicia en false pero cambia a true cuando se abre el submenú, lo cual obliga al lector de pantalla a anunciar que el elemento del menú ahora esta expandido.
Juntando todos estos elementos juntos, un menú ARIA accesible sencillo sería algo así:
<div role="navigation" aria-label ="Main menu"> <ul id="nav" role="menubar"> <li role="menuitem" tabindex="0" aria-haspopup="true"> About <ul role="menu" aria-hidden="true"> <li role="menuitem" tabindex="-1"> <a href="#">News</a> </li> <li role="menuitem" tabindex="-1"> <a href="#">Contact Us</a> </li> </ul> </li> <li role="menuitem" tabindex="0" aria-haspopup="true"> Academics <ul role="menu" aria-hidden="true"> <li role="menuitem" tabindex="-1"> <a href="#">Degree Programs</a> </li> <li role="menuitem" tabindex="-1"> <a href="#">Faculty</a> </li> </ul> </li> </ul> </div>
Además de los atributos aria-expanded y aria-haspopup, se utilizan también los siguientes roles usados en el siguiente ejemplo.
aria-menubar: Representa usualmente un menú horizontal.
aria-menu: Representa un conjunto de enlaces o comandos en una barra de menús, este se usa en menús desplegables.
aria-menuitem: Representa un elemento de menú individual.
<div role="menubar"> <ul role="menu" aria-label="functions" id="appmenu"> <li role="menuitem" aria-haspopup="true"> Menús <ul role="menu"> <li role="menuitem">menu1</li> <li role="menuitem">menu2</li> <li role="menuitem">menu3</li> </ul> </li> <li role="menuitem" aria-haspopup="true"> Listas <ul role="menu"> <li role="menuitem">lista1</li> <li role="menuitem">lista2</li> <li role="menuitem">lista3</li> <li role="menuitem">lista4</li> <li role="menuitem">lista5</li> </ul> </li> <li role="menuitem" aria-haspopup="true"> Links <ul role="menu"> <li role="menuitem">link1</li> <li role="menuitem">link2</li> </ul> </li> <li role="menuitem" aria-haspopup="true"> Forms <ul role="menu"> <li role="menuitem">form1</li> <li role="menuitem">form2</li> <li role="menuitem">form3</li> </ul> </li> <li role="menuitem">Tables</li> </ul> </div>
#appmenu { width:80%; float: left; margin: 0; padding: 0; color: #fff; background-color: #369; padding: .25em; } #appmenu li { white-space: nowrap; display:block; padding: .25em .75em; border: 1px solid #fff; } #appmenu > li { float: left; background-color: #036; text-align: center; position:relative; cursor: pointer; } #appmenu :hover, #appmenu :focus { background-color: #fff; color: #036; border: 1px solid #036; text-decoration: underline; } #appmenu :hover li, #appmenu :focus li { color: #fff; background-color: #036; } #appmenu > li > ul { display: none; position:absolute; left:0; right:0; top:100%; padding:0; margin:0; background-color: #036; width: 200%; text-align: left; } #appmenu > li[aria-expanded="true"] > ul { display:block; }
var appsMenuItems = document.querySelectorAll('#appmenu > li'); var subMenuItems = document.querySelectorAll('#appmenu > li li'); var keys = { tab: 9, enter: 13, esc: 27, space: 32, left: 37, up: 38, right: 39, down: 40 }; var currentIndex, subIndex; var gotoIndex = function(idx) { if (idx == appsMenuItems.length) { idx = 0; } else if (idx < 0) { idx = appsMenuItems.length - 1; } appsMenuItems[idx].focus(); currentIndex = idx; }; var gotoSubIndex = function (menu, idx) { var items = menu.querySelectorAll('li'); if (idx == items.length) { idx = 0; } else if (idx < 0) { idx = items.length - 1; } items[idx].focus(); subIndex = idx; } Array.prototype.forEach.call(appsMenuItems, function(el, i){ if (0 == i) { el.setAttribute('tabindex', '0'); el.addEventListener("focus", function() { currentIndex = 0; }); } else { el.setAttribute('tabindex', '-1'); } el.addEventListener("focus", function() { subIndex = 0; Array.prototype.forEach.call(appsMenuItems, function(el, i){ el.setAttribute('aria-expanded', "false"); }); }); el.addEventListener("click", function(event){ if (this.getAttribute('aria-expanded') == 'false' || this.getAttribute('aria-expanded') == null) { this.setAttribute('aria-expanded', "true"); } else { this.setAttribute('aria-expanded', "false"); } event.preventDefault(); return false; }); el.addEventListener("keydown", function(event) { switch (event.keyCode) { case keys.right: gotoIndex(currentIndex + 1); break; case keys.left: gotoIndex(currentIndex - 1); break; case keys.tab: if (event.shiftKey) { gotoIndex(currentIndex - 1); } else { gotoIndex(currentIndex + 1); } break; case keys.enter: case keys.down: this.click(); subindex = 0; gotoSubIndex(this.querySelector('ul'), 0); break; case keys.up: this.click(); var submenu = this.querySelector('ul'); subindex = submenu.querySelectorAll('li').length - 1; gotoSubIndex(submenu, subindex); break; case keys.esc: document.querySelector('a[href="#related"]').focus(); } event.preventDefault(); }); }); Array.prototype.forEach.call(subMenuItems, function(el, i){ el.setAttribute('tabindex', '-1'); el.addEventListener("keydown", function(event) { switch (event.keyCode) { case keys.tab: if (event.shiftKey) { gotoIndex(currentIndex - 1); } else { gotoIndex(currentIndex + 1); } break; case keys.right: gotoIndex(currentIndex + 1); break; case keys.left: gotoIndex(currentIndex - 1); break; case keys.esc: gotoIndex(currentIndex); break; case keys.down: gotoSubIndex(this.parentNode, subIndex + 1); break; case keys.up: gotoSubIndex(this.parentNode, subIndex - 1); break; case keys.enter: case keys.space: alert(this.innerText); break; } event.preventDefault(); event.stopPropagation(); return false; }); el.addEventListener("click", function(event) { alert(this.innerHTML); event.preventDefault(); event.stopPropagation(); return false; }); });