Construindo Sistemas de Menu Acessíveis

Publicados: 2022-03-10
Resumo rápido ↬ Existem muitos tipos diferentes de menu na web. Criar experiências inclusivas é uma questão de usar os padrões de menu certos nos lugares certos, com a marcação e o comportamento certos.

Nota do editor : Este artigo foi publicado originalmente em Componentes Inclusivos. Se você quiser saber mais sobre artigos de componentes inclusivos semelhantes, siga @inclusicomps no Twitter ou assine o feed RSS. Ao oferecer suporte a inclusive-components.design no Patreon, você pode ajudar a torná-lo o banco de dados mais abrangente de componentes de interface robustos disponíveis.

A classificação é difícil. Pegue os caranguejos, por exemplo. Caranguejos eremitas, caranguejos de porcelana e caranguejos-ferradura não são – taxonomicamente falando – verdadeiros caranguejos. Mas isso não nos impede de usar o sufixo "caranguejo". Fica mais confuso quando, com o tempo e graças a um processo chamado carcinização , os caranguejos falsos evoluem para se parecerem mais com os verdadeiros. Este é o caso dos caranguejos-rei, que se acredita terem sido caranguejos eremitas no passado. Imagine o tamanho de suas conchas!

No design, muitas vezes cometemos o mesmo erro de dar o mesmo nome a coisas diferentes. Eles parecem semelhantes, mas as aparências podem enganar. Isso pode ter um efeito infeliz na clareza de sua biblioteca de componentes. Em termos de inclusão, também pode levar você a redirecionar um componente semanticamente e comportamentalmente inadequado. Os usuários esperam uma coisa e obtêm outra.

O termo "dropdown" nomeia um exemplo clássico. Muitas coisas "descem" nas interfaces, incluindo o conjunto de <option> s de um elemento <select> e a lista de links revelada por JavaScript que constitui um submenu de navegação. Mesmo nome; coisas bem diferentes. (Algumas pessoas chamam isso de "pulldowns", é claro, mas não vamos entrar nisso.)

As listas suspensas que constituem um conjunto de opções costumam ser chamadas de "menus", e quero falar sobre elas aqui. Estaremos criando um menu verdadeiro , mas há muito a ser dito sobre menus não-realmente verdadeiros ao longo do caminho.

Mais depois do salto! Continue lendo abaixo ↓

Vamos começar com um quiz. A caixa de links pendurada na barra de navegação na ilustração é um menu?

Uma barra de navegação inclui um link de loja, abaixo do qual há um conjunto de três links adicionais para fantasias de cachorro, ferros de waffle e orbes mágicos, respectivamente.
Uma barra de navegação inclui um link de loja, abaixo do qual há um conjunto de três links adicionais para fantasias de cachorro, ferros de waffle e orbes mágicos, respectivamente. (Visualização grande)

A resposta é não, não é um menu verdadeiro.

É uma convenção de longa data que os esquemas de navegação são compostos de listas de links. Uma convenção quase tão antiga dita que a sub-navegação deve ser fornecida como listas aninhadas de links. Se eu fosse remover o CSS para o componente ilustrado acima, deveria ver algo como o seguinte, exceto colorido em azul e em Times New Roman.

Semanticamente falando, listas aninhadas de links estão corretas neste contexto. Os sistemas de navegação são realmente tabelas de conteúdo e é assim que as tabelas de conteúdo são estruturadas. A única coisa que realmente nos faz pensar em “menu” é o estilo das listas aninhadas e a forma como elas são reveladas ao passar o mouse ou focar.

É aí que alguns erram e começam a adicionar a semântica WAI-ARIA: aria-haspopup="true" , role="menu" , role="menuitem" etc. Há um lugar para isso, como abordaremos, mas não aqui . Aqui estão dois motivos:

  1. Os menus ARIA não são designados para navegação, mas para o comportamento do aplicativo. Imagine o sistema de menus para um aplicativo de desktop.
  2. O link de nível superior deve ser usado como um link , o que significa que não se comporta como um botão de menu.

Em relação a (2): Ao percorrer uma região de navegação com submenus, seria de se esperar que cada submenu aparecesse ao passar o mouse ou focalizar o link de "nível superior" ("Loja" na ilustração). Isso revela o submenu e coloca seus próprios links em ordem de foco. Com uma pequena ajuda do JavaScript capturando eventos de foco e desfoque para persistir a aparência dos submenus enquanto necessário, alguém usando o teclado deve ser capaz de navegar por cada link de cada camada, por sua vez.

Os botões de menu que usam a propriedade aria-haspopup="true" não se comportam assim. Eles são ativados ao clicar e não têm outra finalidade senão revelar um menu secreto.

descontos em ebooks bieten wir
Esquerda: um botão de menu chamado 'menu' com um ícone de seta apontando para baixo e o estado aria-expanded = false. Direita: O mesmo botão de menu, mas com o menu aberto. Este botão está no estado aria-expanded = true. (Visualização grande)

Conforme ilustrado, se esse menu está aberto ou fechado, deve ser comunicado com aria-expanded . Você só deve alterar esse estado no clique, não no foco. Os usuários geralmente não esperam uma mudança explícita de estado em um mero evento de foco. Em nosso sistema de navegação, o estado realmente não muda; é apenas um truque de estilo. Comportamentalmente, podemos guiar através da navegação como se nenhum truque de mostrar/ocultar estivesse ocorrendo.

O problema com os submenus de navegação

Os submenus de navegação (ou "dropdowns" para alguns) funcionam bem com um mouse ou teclado, mas não são tão quentes quando se trata de toque. Quando você pressiona o link "Comprar" de nível superior em nosso exemplo pela primeira vez, você está dizendo para abrir o submenu e seguir o link.

Há duas resoluções possíveis aqui:

  1. Impedir o comportamento padrão de links de nível superior ( e.preventDefault() ) e script na semântica e comportamento completos do menu WAI-ARIA.
  2. Certifique-se de que cada página de destino de nível superior tenha um índice como alternativa ao submenu.

(1) é insatisfatório porque, como observei anteriormente, esses tipos de semântica e comportamentos não são esperados nesse contexto, onde os links são os controles do sujeito. Além disso, os usuários não podem mais navegar para uma página de nível superior, se ela existir.

Nota lateral: Quais dispositivos são dispositivos de toque?

É tentador pensar: “esta não é uma ótima solução, mas vou adicioná-la apenas para interfaces de toque”. O problema é: como detectar se um dispositivo tem uma tela sensível ao toque?

Você certamente não deve equiparar "tela pequena" com "ativado por toque". Tendo trabalhado no mesmo escritório que pessoas fazendo telas sensíveis ao toque para museus, posso garantir que algumas das maiores telas ao redor são telas sensíveis ao toque. Os laptops de teclado duplo e entrada de toque também estão se tornando cada vez mais prolíficos.

Da mesma forma, muitos, mas nem todos, dispositivos menores são dispositivos de toque. No design inclusivo, você não pode se dar ao luxo de fazer suposições.

A resolução (2) é mais inclusiva e robusta, pois fornece um "retorno" para usuários de todas as entradas. Mas as citações assustadoras em torno do termo de fallback aqui são bastante deliberadas porque eu realmente acho que as tabelas de conteúdo na página são uma maneira superior de fornecer navegação.

A premiada equipe de Serviços Digitais do Governo parece concordar. Você também pode tê-los visto na Wikipedia.

As tabelas de conteúdo do Gov.uk são mínimas com hífens como estilos de lista. A Wikipedia fornece uma caixa cinza com itens numerados. Ambos são conteúdos rotulados.
As tabelas de conteúdo do Gov.uk são mínimas com hífens como estilos de lista. A Wikipedia fornece uma caixa cinza com itens numerados. Ambos são conteúdos rotulados.

Tabelas de conteúdo

As tabelas de conteúdo são navegação para páginas ou seções de páginas relacionadas e devem ser semanticamente semelhantes às principais regiões de navegação do site, usando um elemento <nav> , uma lista e um mecanismo de rotulagem 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

  • Neste exemplo, estamos imaginando que cada seção é sua própria página, como seria no submenu suspenso.
  • É importante que cada uma dessas páginas "Loja" tenha a mesma estrutura, com esta tabela de conteúdo "Produtos" presente no mesmo local. A consistência apoia a compreensão.
  • A lista agrupa os itens e os enumera na saída de tecnologia assistiva, como a voz sintética de um leitor de tela.
  • O <nav> é rotulado recursivamente pelo cabeçalho usando aria-labelledby . Isso significa que a "navegação de produtos" será anunciada na maioria dos leitores de tela ao entrar na região por Tab . Isso também significa que a "navegação de produtos" será discriminada nas interfaces dos elementos do leitor de tela, a partir das quais os usuários podem navegar diretamente para as regiões.

Tudo em uma página

Se você conseguir encaixar todas as seções em uma página sem que ela se torne muito longa e árdua para rolar, melhor ainda. Basta vincular ao identificador de hash de cada seção. Por exemplo, href="#waffle-irons" deve apontar para .

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

( Observação: alguns navegadores são ruins em enviar foco para fragmentos de página vinculados. Colocar tabindex="-1" no fragmento de destino corrige isso.)

Onde um site tem muito conteúdo, uma arquitetura de informação cuidadosamente construída, expressa através do uso liberal de tabelas de “menus” de conteúdo é infinitamente preferível a um sistema suspenso precário e pesado. Não só é mais fácil tornar responsivo e requer menos código para fazê-lo, mas torna as coisas mais claras: onde os sistemas suspensos escondem a estrutura, as tabelas de conteúdo a expõem.

Alguns sites, incluindo o gov.uk do Government Digital Service, incluem páginas de índice (ou “tópicos”) que são apenas tabelas de conteúdo. É um conceito tão poderoso que o popular gerador de sites estáticos Hugo gera essas páginas por padrão.

Diagrama de estilo de árvore genealógica com página de destino do tópico no topo com duas ramificações de páginas individuais. Cada uma das ramificações de página individual tem várias ramificações de seção de página
Diagrama de estilo de árvore genealógica com página de destino do tópico no topo com duas ramificações de páginas individuais. Cada uma das ramificações de página individual tem várias ramificações de seção de página (visualização grande)

A arquitetura da informação é uma grande parte da inclusão. Um site mal organizado pode ser tão compatível tecnicamente quanto você quiser, mas ainda assim alienará muitos usuários – especialmente aqueles com deficiências cognitivas ou aqueles que estão com pressa.

Botões do Menu de Navegação

Enquanto estamos no assunto de menus falsos relacionados à navegação, seria negligente da minha parte não falar sobre os botões do menu de navegação. Você quase certamente já viu estes indicados por um ícone de "hambúrguer" ou "navicon" de três linhas.

Mesmo com uma arquitetura de informações reduzida e apenas uma camada de links de navegação, o espaço em telas pequenas é valioso. Ocultar a navegação atrás de um botão significa que há mais espaço para o conteúdo principal na janela de visualização.

Um botão de navegação é a coisa mais próxima que estudamos até agora de um verdadeiro botão de menu. Como tem a finalidade de alternar a disponibilidade de um menu ao clicar, deve:

  1. Identifique-se como um botão, não um link;
  2. Identifique o estado expandido ou recolhido de seu menu correspondente (que, em termos estritos, é apenas uma lista de links).

Melhoria progressiva

Mas não vamos nos antecipar. Devemos estar atentos ao aprimoramento progressivo e considerar como isso funcionaria sem JavaScript.

Em um documento HTML não aprimorado, não há muito o que fazer com botões (exceto botões de envio, mas isso não está nem intimamente relacionado ao que queremos alcançar aqui). Em vez disso, talvez devêssemos começar com apenas um link que nos leva à navegação?

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

Não há muito sentido em ter o link, a menos que haja muito conteúdo entre o link e a navegação. Como a navegação do site quase sempre deve aparecer no topo da ordem de origem, não há necessidade. Então, na verdade, um menu de navegação na ausência de JavaScript deve ser apenas… alguma navegação.

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

Você aprimora isso adicionando o botão, em seu estado inicial, e ocultando a navegação (usando o 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>

Alguns navegadores mais antigos - você sabe quais - não suportam hidden , então lembre-se de colocar o seguinte em seu CSS. Ele corrige o problema porque display: none tem o mesmo efeito de ocultar o menu de tecnologias assistivas e remover os links da ordem de foco.

 [hidden] { display: none; }

Fazer o melhor para dar suporte a softwares mais antigos é, obviamente, um ato de design inclusivo. Alguns são incapazes ou não querem atualizar.

Canal

Onde muitas pessoas erram é colocando o botão fora da região. Isso significaria que os usuários de leitores de tela que se movem para o <nav> usando um atalho o encontrariam vazio, o que não é muito útil. Com a lista oculta dos leitores de tela, eles encontrariam isso:

 <nav> </nav>

Veja como podemos alternar o 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; });

controles de ária

Como escrevi em Aria-controls Is Poop, o atributo aria-controls , destinado a ajudar os usuários do leitor de tela a navegar de um elemento de controle para um elemento controlado, é suportado apenas no leitor de tela JAWS. Então você simplesmente não pode confiar nele.

Sem um bom método para direcionar os usuários entre os elementos, você deve certificar-se de que uma das seguintes opções seja verdadeira:

  1. O primeiro link da lista expandida é o próximo na ordem de foco após o botão (como no exemplo de código anterior).
  2. O primeiro link é focado programaticamente na revelação da lista.

Neste caso, eu recomendaria (1). É muito mais simples, pois você não precisa se preocupar em mover o foco de volta para o botão e em qual(is) evento(s) fazer isso. Além disso, atualmente não há nada para avisar os usuários de que seu foco será movido para algum lugar diferente. Nos menus verdadeiros que discutiremos em breve, este é o trabalho de aria-haspopup="true" .

Empregar aria-controls não faz muito mal, exceto que torna a leitura em leitores de tela mais detalhada. No entanto, alguns usuários do JAWS podem esperar isso. Aqui está como seria aplicado, usando o id da lista como a 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>

As funções de menu e item de menu

Um menu verdadeiro (no sentido WAI-ARIA) deve identificar-se como tal usando a função de menu (para o contêiner) e, normalmente, filhos de menuitem de menu (outras funções filhas podem ser aplicadas). Essas funções pai e filho trabalham juntas para fornecer informações às tecnologias assistivas. Veja como uma lista pode ser aumentada para ter semântica de menu:

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

Como nosso menu de navegação está começando a se comportar como um menu “verdadeiro”, eles não deveriam estar presentes?

A resposta curta é não. A resposta longa é: não, porque nossos itens de lista contêm links e elementos de menuitem não devem ter descendentes interativos. Ou seja, eles são os controles em um menu.

Podemos, é claro, suprimir a semântica da lista dos <li> s usando role="presentation" ou role="none" (que são equivalentes) e colocar o papel do menuitem de menu em cada link. No entanto, isso suprimiria a função de link implícita. Em outras palavras, o exemplo a seguir seria anunciado como “Home, item de menu”, não “Home, link” ou “Home, item de menu, link”. As funções ARIA simplesmente substituem as funções HTML.

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

Queremos que o usuário saiba que está usando um link e pode esperar o comportamento do link, então isso não é bom. Como eu disse, os menus verdadeiros são para o comportamento do aplicativo (orientado por JavaScript).

O que nos resta é um tipo de componente híbrido, que não é bem um menu de verdade, mas pelo menos informa aos usuários se a lista de links está aberta, graças ao estado aria-expanded . Este é um padrão perfeitamente satisfatório para menus de navegação.

Nota lateral: O elemento <select>

Se você esteve envolvido em design responsivo desde o início, você pode se lembrar de um padrão pelo qual a navegação foi condensada em um elemento <select> para janelas de visualização estreitas.

aparelho com elemento de seleção mostrando "home" selecionado na parte superior da janela de visualização
Monofone com elemento de seleção mostrando “home” selecionado na parte superior da janela de visualização.

Tal como acontece com os botões de alternância baseados em caixa de seleção que discutimos, usar um elemento nativo que se comporta um pouco como pretendido sem scripts adicionais é uma boa opção para eficiência e – especialmente em dispositivos móveis – desempenho. E os elementos <select> são tipos de menus, com semântica semelhante ao menu acionado por botão que em breve estaremos construindo.

No entanto, assim como com o botão de alternância da caixa de seleção, estamos usando um elemento associado à entrada de entrada, não simplesmente fazendo uma escolha. Isso provavelmente causará confusão para muitos usuários — especialmente porque esse padrão usa JavaScript para fazer com que a <option> selecionada se comporte como um link. A mudança inesperada de contexto que isso provoca é considerada uma falha de acordo com o critério 3.2.2 On Input (Nível A) das WCAG.

Menus Verdadeiros

Agora que discutimos sobre menus falsos e quase menus, chegou a hora de criar um menu verdadeiro , aberto e fechado por um botão de menu verdadeiro. Daqui em diante, vou me referir ao botão e ao menu juntos simplesmente como um “botão de menu”.

Mas em que aspectos nosso botão de menu será verdadeiro? Bem, será um componente de menu destinado à escolha de opções na aplicação em questão, que implementa toda a semântica esperada e os comportamentos correspondentes a serem considerados convencionais para tal ferramenta.

Como já mencionado, essas convenções vêm do design de aplicativos de desktop. A atribuição ARIA e o gerenciamento de foco governado por JavaScript são necessários para imitá-los totalmente. Parte do propósito do ARIA é ajudar os desenvolvedores web a criar experiências web ricas sem quebrar as convenções de usabilidade forjadas no mundo nativo.

Neste exemplo, vamos imaginar que nosso aplicativo é algum tipo de jogo ou quiz. Nosso botão de menu permitirá que o usuário escolha um nível de dificuldade. Com toda a semântica no lugar, o menu fica assim:

 <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

  • A propriedade aria-haspopup simplesmente indica que o botão secreta um menu. Ele age como um aviso de que, quando pressionado, o usuário será movido para o menu “popup” (vamos abordar o comportamento do foco em breve). Seu valor não muda — permanece true o tempo todo.
  • O <span> dentro do botão contém o ponto unicode para um pequeno triângulo preto apontando para baixo. Esta convenção indica visualmente o que aria-haspopup faz não visualmente - que pressionar o botão revelará algo abaixo dele. A atribuição aria-hidden="true" impede que os leitores de tela anunciem "triângulo apontando para baixo" ou similar. Graças a aria-haspopup , não é necessário no contexto não visual.
  • A propriedade aria-haspopup é complementada por aria-expanded . Isso informa ao usuário se o menu está atualmente em um estado aberto (expandido) ou fechado (recolhido) alternando entre os valores true e false .
  • O próprio menu assume o papel de menu (apropriadamente chamado). Leva descendentes com a função menuitem . Eles não precisam ser filhos diretos do elemento de menu , mas são neste caso — por simplicidade.

Comportamento do teclado e do foco

Quando se trata de tornar o teclado de controles interativos acessível, a melhor coisa que você pode fazer é usar os elementos certos. Como estamos usando elementos <button> aqui, podemos ter certeza de que os eventos de clique serão acionados nas teclas Enter e Space , conforme especificado na interface HTMLButtonElement. Isso também significa que podemos desabilitar os itens de menu usando a propriedade disabled associada ao botão.

No entanto, há muito mais na interação com o teclado do botão do menu. Aqui está um resumo de todo o foco e comportamento do teclado que vamos implementar, com base no WAI-ARIA Authoring Practices 1.1:

Enter , Space ou no botão de menu Abre o menu
em um item de menu Move o foco para o próximo item de menu ou para o primeiro item de menu se você estiver no último
em um item de menu Move o foco para o item de menu anterior ou para o último item de menu se você estiver no primeiro
no botão de menu Fecha o menu se estiver aberto
Esc em um item de menu Fecha o menu e foca o botão de menu

A vantagem de mover o foco entre itens de menu usando as teclas de seta é que Tab é preservado para sair do menu. Na prática, isso significa que os usuários não precisam percorrer todos os itens do menu para sair do menu - uma grande melhoria para usabilidade, especialmente onde há muitos itens de menu.

A aplicação de tabindex="-1" torna os itens de menu não focalizáveis ​​por Tab , mas preserva a capacidade de focar os elementos de forma programática, ao capturar toques de tecla nas teclas de seta.

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

O método aberto

Como parte de um design de API sólido, podemos construir métodos para lidar com os vários eventos.

Por exemplo, o método open precisa mudar o valor aria-expanded para “true”, alterar a propriedade oculta do menu para false e focar o primeiro menuitem de menu no menu que não está desabilitado:

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

Podemos executar este método onde o usuário pressiona a tecla para baixo em uma instância de botão de menu focado:

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

Além disso, um desenvolvedor usando este script agora poderá abrir o menu programaticamente:

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

Sidenote: O hack da caixa de seleção

Tanto quanto possível, é melhor não usar JavaScript, a menos que você precise. Envolver uma terceira tecnologia em cima de HTML e CSS é necessariamente um aumento na complexidade e fragilidade sistêmica. No entanto, nem todos os componentes podem ser construídos satisfatoriamente sem JavaScript na mistura.

No caso dos botões de menu, o entusiasmo em fazê-los “funcionar sem JavaScript” levou a algo chamado hack de checkbox. É aqui que o estado marcado (ou desmarcado) de uma caixa de seleção oculta é usado para alternar a visibilidade de um elemento de menu usando CSS.

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

Para usuários de leitores de tela, a função da caixa de seleção e o estado marcado não fazem sentido nesse contexto. Isso pode ser parcialmente superado adicionando role="button" à caixa de seleção.

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

Infelizmente, isso suprime a comunicação implícita de estado verificado, privando-nos de feedback de estado livre de JavaScript (embora fosse ruim como "verificado" neste contexto).

Mas é possível falsificar aria-expanded . Nós só precisamos fornecer nossa etiqueta com dois vãos como abaixo.

 <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 são visualmente ocultos usando a classe visually-hidden , mas, dependendo do estado em que estamos, apenas um também fica oculto para os leitores de tela. Ou seja, apenas um tem display: none , e isso é determinado pelo estado verificado existente (mas não 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; }

Isso é inteligente e tudo mais, mas nosso botão de menu ainda está incompleto, pois os comportamentos de foco esperados que discutimos simplesmente não podem ser implementados sem JavaScript.

Esses comportamentos são convencionais e esperados, tornando o botão mais utilizável. No entanto, se você realmente precisa implementar um botão de menu sem JavaScript, isso é o mais próximo possível. Considerando que o botão de menu de navegação recortado que abordei anteriormente oferece conteúdo de menu que não é dependente de JavaScript (ou seja, links), essa abordagem pode ser uma opção adequada.

Por diversão, aqui está um codePen implementando um botão de menu de navegação sem JavaScript.

Veja o exemplo do botão de menu Pen Navigation no JS by Heydon (@heydon) no CodePen.

( Nota: Apenas Espaço abre o menu.)

O evento “escolher”

A execução de alguns métodos deve emitir eventos para que possamos configurar listeners. Por exemplo, podemos emitir um evento de choose quando um usuário clica em um item de menu. Podemos configurar isso usando CustomEvent , que nos permite passar um argumento para a propriedade detail do evento. Nesse caso, o argumento (“escolha”) seria o nó DOM do item de menu escolhido.

 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; }

Há todo tipo de coisas que podemos fazer com esse mecanismo. Talvez tenhamos uma região ativa configurada com um id de menuFeedback :

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

Agora podemos configurar um listener e preencher a região ao vivo com as informações secretadas dentro do 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}'; });
Quando um usuário escolhe uma opção, o menu fecha e o foco volta ao botão de menu. É importante que os usuários retornem ao elemento acionador depois que o menu for fechado.
Quando um usuário escolhe uma opção, o menu fecha e o foco volta ao botão de menu. É importante que os usuários retornem ao elemento acionador depois que o menu for fechado. (Visualização grande)

Quando um item de menu é selecionado, o usuário do leitor de tela ouvirá: “Você escolheu [rótulo do item de menu]” . Uma região ativa (definida aqui com a role=“alert” ) anuncia seu conteúdo em leitores de tela sempre que esse conteúdo muda. A região ao vivo não é obrigatória, mas é um exemplo do que pode acontecer na interface como resposta ao usuário fazer uma escolha no menu.

Escolhas Persistentes

Nem todos os itens de menu servem para escolher configurações persistentes. Muitos apenas agem como botões padrão que fazem algo na interface acontecer quando pressionados. No entanto, no caso do nosso botão do menu de dificuldade, gostaríamos de indicar qual é a configuração de dificuldade atual - a escolhida por último.

O atributo aria-checked="true" funciona para itens que, em vez de menuitem , assumem a função menuitemradio . A marcação aprimorada, com o segundo item marcado ( set ) se parece com isso:

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

Menus nativos em muitas plataformas indicam os itens escolhidos usando marcas de seleção. Podemos fazer isso sem problemas usando um pouco de CSS extra:

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

Ao percorrer o menu com um leitor de tela em execução, focalizar este item marcado exibirá um anúncio como "marca de seleção, item de menu Médio, verificado" .

O comportamento ao abrir um menu com um menuitemradio marcado difere um pouco. Em vez de focar o primeiro item (ativado) no menu, o item selecionado é focado.

O botão de menu começa com o menu fechado. Ao abrir a segunda dificuldade (Média) é focada. Ele é prefixado com uma marca de seleção com base na presença do atributo aria-checked.
O botão de menu começa com o menu fechado. Ao abrir a segunda dificuldade (Média) é focada. Ele é prefixado com uma marca de seleção com base na presença do atributo aria-checked. (Visualização grande)

Qual é o benefício desse comportamento? O usuário (qualquer usuário) é lembrado da opção selecionada anteriormente. Em menus com inúmeras opções incrementais (por exemplo, um conjunto de níveis de zoom), as pessoas que operam pelo teclado são colocadas na posição ideal para fazer seu ajuste.

Usando o botão Menu com um leitor de tela

Neste vídeo, mostrarei como é usar o botão de menu com o leitor de tela Voiceover e o Chrome. O exemplo usa itens com menuitemradio , aria-checked e o comportamento de foco discutido. Experiências semelhantes podem ser esperadas em toda a gama de softwares populares de leitura de tela.

Botão de menu inclusivo no Github

Kitty Giraudel e eu trabalhamos juntos na criação de um componente de botão de menu com os recursos da API que descrevi e muito mais. Você deve agradecer a Hugo por muitos desses recursos, pois eles foram baseados no trabalho que fizeram no a11y-dialog — um diálogo modal acessível. Está disponível no Github e NPM.

 npm i inclusive-menu-button --save

Além disso, Kitty criou uma versão do React para seu deleite.

Lista de controle

  • Não use a semântica de menu ARIA em sistemas de menu de navegação.
  • Em sites com conteúdo pesado, não esconda a estrutura em menus de navegação suspensos aninhados.
  • Use aria-expanded para indicar o estado aberto/fechado de um menu de navegação ativado por botão.
  • Certifique-se de que o menu de navegação seja o próximo na ordem de foco após o botão que o abre/fecha.
  • Nunca sacrifique a usabilidade em busca de soluções sem JavaScript. É vaidade.