JavaScript é uma ferramenta poderosa para criar interfaces dinâmicas e interativas, mas, sem os devidos cuidados, pode se tornar uma barreira para pessoas com deficiência. A acessibilidade web (a11y) garante que todos os usuários, independentemente de suas capacidades, possam perceber, entender, navegar e interagir com a web. Nesta aula, exploraremos como JavaScript pode ser usado para melhorar a acessibilidade, em vez de prejudicá-la.

Abordaremos três pilares fundamentais: o uso correto de ARIA (Accessible Rich Internet Applications) para melhorar a semântica para leitores de tela, o gerenciamento de foco e o suporte à navegação por teclado, e, por fim, boas práticas gerais para garantir que sua aplicação seja inclusiva. Ao final, você terá conhecimentos práticos para auditar e corrigir problemas de acessibilidade em seus projetos.

ARIA com JS

ARIA (Accessible Rich Internet Applications) é uma especificação do W3C que define atributos HTML para tornar conteúdos dinâmicos e componentes JavaScript acessíveis a tecnologias assistivas, como leitores de tela. Ela não altera o comportamento visual ou funcional do elemento, mas fornece informações semânticas adicionais, como papéis (roles), estados e propriedades.

Com JavaScript, podemos manipular dinamicamente esses atributos ARIA para refletir o estado atual da interface. Por exemplo, em um menu acordeão, devemos atualizar o atributo aria-expanded para indicar se o painel está aberto ou fechado. É crucial usar ARIA com moderação e somente quando não houver uma alternativa HTML nativa. Por exemplo, prefira o elemento <button> a um <div> com role="button".

Exemplo prático: criar um botão que controla a visibilidade de um conteúdo, atualizando o atributo aria-expanded e aria-controls:

const toggleButton = document.getElementById('toggle');
const content = document.getElementById('content');

toggleButton.addEventListener('click', () => {
  const isExpanded = toggleButton.getAttribute('aria-expanded') === 'true';
  toggleButton.setAttribute('aria-expanded', !isExpanded);
  content.hidden = isExpanded; // usando hidden para ocultar, mas pode usar classe
});

No código acima, o atributo aria-expanded é atualizado a cada clique, e o conteúdo é mostrado/ocultado. Isso informa ao leitor de tela se o conteúdo está visível ou não. Lembre-se de que o atributo hidden é uma forma semântica de ocultar conteúdo, mas você também pode usar display: none via CSS; o importante é que o leitor de tela respeite a ocultação.

Outro exemplo comum é o uso de aria-live para anunciar mudanças dinâmicas, como atualizações de carrinho de compras ou mensagens de erro. Podemos criar uma região viva e inserir conteúdo nela via JavaScript:

const liveRegion = document.getElementById('live-region');
function announce(message) {
  liveRegion.textContent = message;
}
// Chamar announce('Item adicionado ao carrinho') quando necessário

No HTML, a região deve ter aria-live="polite" (ou assertive para urgência) e, inicialmente, estar vazia. Isso permite que leitores de tela anunciem a mensagem sem interromper o usuário de forma brusca.

Foco e teclado

O gerenciamento de foco é essencial para usuários que navegam pelo teclado, incluindo pessoas com mobilidade reduzida. Quando criamos componentes dinâmicos, como modais, menus suspensos ou carrosséis, devemos garantir que o foco seja movido corretamente e que o usuário possa operar tudo com o teclado.

Um dos problemas mais comuns é o foco ficar preso em um modal ou em um elemento que não pode ser fechado. A técnica de "armadilha de foco" (focus trap) impede que o Tab saia do modal enquanto ele estiver aberto. Vamos ver como implementar uma armadilha de foco em um modal simples:

const modal = document.getElementById('modal');
const openBtn = document.getElementById('open-modal');
const closeBtn = document.getElementById('close-modal');
const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';

function trapFocus(event) {
  const focusable = modal.querySelectorAll(focusableElements);
  const first = focusable[0];
  const last = focusable[focusable.length - 1];
  if (event.key === 'Tab') {
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  }
}

openBtn.addEventListener('click', () => {
  modal.hidden = false;
  closeBtn.focus();
  document.addEventListener('keydown', trapFocus);
});

closeBtn.addEventListener('click', () => {
  modal.hidden = true;
  openBtn.focus();
  document.removeEventListener('keydown', trapFocus);
});

Além disso, é importante restaurar o foco ao elemento que abriu o modal, como fizemos acima. Isso mantém a continuidade da navegação.

Também devemos garantir que todos os elementos interativos sejam acessíveis por teclado. Elementos como <div> com tabindex="0" podem ser focados, mas é preferível usar elementos nativos como <button> e <a>. Se você precisar criar um componente personalizado, use tabindex e implemente os eventos de teclado apropriados, como Enter e Espaço para ativação.

Outra técnica é o uso de atalhos de teclado. Por exemplo, em um menu, permitir que as setas naveguem entre itens. Isso requer gerenciar o foco nos itens do menu. Aqui está um exemplo parcial:

const menuItems = document.querySelectorAll('.menu-item');
let currentIndex = 0;

menuItems.forEach((item, index) => {
  item.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
      e.preventDefault();
      currentIndex = (index + 1) % menuItems.length;
      menuItems[currentIndex].focus();
    } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
      e.preventDefault();
      currentIndex = (index - 1 + menuItems.length) % menuItems.length;
      menuItems[currentIndex].focus();
    }
  });
});

Lembre-se de que a implementação de padrões de teclado deve seguir as diretrizes WAI-ARIA Authoring Practices para garantir consistência.

Boas práticas

Além de ARIA e foco, existem várias boas práticas para garantir que seu código JavaScript seja acessível. Primeiro, sempre que possível, use HTML semântico e aprimore com JavaScript, em vez de substituir elementos nativos por divs e spans. Isso reduz a necessidade de ARIA e melhora a compatibilidade.

Segundo, forneça alternativas para conteúdo não textual, como imagens (atributo alt) e ícones (texto ou aria-label). Em JavaScript, você pode adicionar dinamicamente esses atributos, mas certifique-se de que eles estejam presentes.

Terceiro, teste sua aplicação com leitores de tela e navegação por teclado. Use ferramentas como o Lighthouse do Chrome, axe ou WAVE para identificar problemas. Automatize testes de acessibilidade em seu pipeline de CI/CD.

Quarto, evite alterar o foco de forma inesperada. Por exemplo, não mova o foco para um elemento que não é relevante para a ação do usuário. Se uma página carregar e o foco for movido automaticamente, pode confundir usuários de leitor de tela.

Quinto, preste atenção à ordem de leitura. O conteúdo dinâmico deve ser inserido em uma ordem lógica no DOM. Use aria-live para anunciar mudanças, mas não exagere, pois muitas interrupções podem ser irritantes.

Por fim, lembre-se de que acessibilidade é um processo contínuo. Envolva pessoas com deficiência nos testes e mantenha-se atualizado com as diretrizes WCAG e WAI-ARIA.

Referências

Exercícios

  1. Implemente um botão que controla a visibilidade de um painel usando aria-expanded e aria-controls. O painel deve ser ocultado com o atributo hidden e o atributo aria-expanded deve ser atualizado corretamente.

    ✓ Resposta:
    const button = document.getElementById('toggle');
    const panel = document.getElementById('panel');
    
    button.addEventListener('click', () => {
      const isExpanded = button.getAttribute('aria-expanded') === 'true';
      button.setAttribute('aria-expanded', !isExpanded);
      panel.hidden = isExpanded;
    });

    No HTML, o botão deve ter aria-expanded="false" e aria-controls="panel".

  2. Crie uma região aria-live="polite" e use JavaScript para anunciar uma mensagem quando um formulário for enviado com sucesso (simule com um evento de clique).

    ✓ Resposta:
    const liveRegion = document.getElementById('live-region');
    const form = document.getElementById('my-form');
    
    form.addEventListener('submit', (e) => {
      e.preventDefault();
      liveRegion.textContent = 'Formulário enviado com sucesso!';
    });

    No HTML: <div id="live-region" aria-live="polite"></div>

  3. Implemente uma armadilha de foco para um modal. O modal deve conter um botão de fechar e um link. Ao abrir, o foco deve ir para o botão de fechar. Ao pressionar Tab, o foco deve circular entre os elementos do modal, sem sair.

    ✓ Resposta:
    const modal = document.getElementById('modal');
    const openBtn = document.getElementById('open');
    const closeBtn = document.getElementById('close');
    const focusableElements = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
    
    function trapFocus(e) {
      const focusable = modal.querySelectorAll(focusableElements);
      const first = focusable[0];
      const last = focusable[focusable.length - 1];
      if (e.key === 'Tab') {
        if (e.shiftKey && document.activeElement === first) {
          e.preventDefault();
          last.focus();
        } else if (!e.shiftKey && document.activeElement === last) {
          e.preventDefault();
          first.focus();
        }
      }
    }
    
    openBtn.addEventListener('click', () => {
      modal.hidden = false;
      closeBtn.focus();
      document.addEventListener('keydown', trapFocus);
    });
    
    closeBtn.addEventListener('click', () => {
      modal.hidden = true;
      openBtn.focus();
      document.removeEventListener('keydown', trapFocus);
    });
  4. Crie uma lista de itens de menu que possa ser navegada com as setas do teclado. Use a rotação ao chegar ao final. Destaque os itens com tabindex="0" e gerencie o foco.

    ✓ Resposta:
    const items = document.querySelectorAll('.menu-item');
    let current = 0;
    
    items.forEach((item, index) => {
      item.setAttribute('tabindex', index === 0 ? '0' : '-1');
      item.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
          e.preventDefault();
          current = (index + 1) % items.length;
          items[current].focus();
        } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
          e.preventDefault();
          current = (index - 1 + items.length) % items.length;
          items[current].focus();
        }
      });
    });
  5. Escreva um trecho de código que adicione dinamicamente um atributo aria-label a um ícone de botão (um elemento <i> dentro de um <button>) para que ele seja anunciado corretamente.

    ✓ Resposta:
    const iconButton = document.querySelector('.icon-button');
    iconButton.setAttribute('aria-label', 'Fechar');
    // Ou, se o ícone for filho:
    const icon = iconButton.querySelector('i');
    icon.setAttribute('aria-hidden', 'true');
    iconButton.setAttribute('aria-label', 'Fechar');

Observações finais

Acessibilidade não é um recurso opcional; é uma exigência para criar produtos inclusivos. Com JavaScript, temos o poder de melhorar ou piorar a experiência de usuários com deficiência. Sempre teste suas implementações com tecnologias assistivas e siga as diretrizes estabelecidas.

Lembre-se de que a acessibilidade também beneficia usuários sem deficiência, como aqueles que preferem navegação por teclado ou estão em ambientes com restrições. Invista tempo em aprender e aplicar esses conceitos, e seus projetos serão mais robustos e universais.