s Guías Prácticas de Código Accesible
Escudo de la República de Colombia

Menú Usando ARIA

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í:

Fragmento de Código: HTML
<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.

Example:










Fragmento de Código: HTML
<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>


Fragmento de Código: CSS
#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;
}


Fragmento de Código: JavaScript
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;
        });
});