Construindo um show/hide disclosure component acessível com vanilla JS
Tags: javascript, a11y, programming, portugues
Published: 2023-12-04 02:10:53

_**Nota:** apenas traduzi o texto abaixo e postei aqui._

Um `disclosure component` é o nome formal do pattern em que você clica em um botão para revelar ou ocultar o conteúdo.

Isso inclui coisas como uma interação "mostrar mais/mostrar menos" para algum texto descritivo abaixo de um vídeo do YouTube ou um menu de hambúrguer que é revelado e ocultado quando você clica nele.

Veremos agora uma maneira de construir `disclosure component` de forma acessível.

## O HTML inicial

Começamos com algum HTML básico.

Incluiremos uma `div` para nosso conteúdo, com um ID `#now-you-see-me`. Também adicionaremos um botão que podemos usar para alternar a visibilidade do conteúdo.

```html
<button>Mostrar mais</button>

<div id="now-you-see-me">
    Agora você não vê.
</div>
```

Por razões semânticas, o "toggle" deve ser sempre um botão.

Links implicam para os dispositivos assistivos que clicar neles o levará a algum lugar, enquanto os botões implicam que alguma interatividade é acionada.

## Adicionando interatividade

A primeira coisa que precisamos fazer é "listen" quando o botão é clicado, para podermos alternar a visibilidade do nosso conteúdo.

Para começar, vamos usar a `event delegation` para "listen" eventos de clique.

```javascript
document.addEventListener('click', function (event) {
    // Faça coisas...
});
```

## Somente execute em disclosure buttons

No momento, estamos detectando todos os cliques no `document`. Precisamos de uma maneira de filtrar todos os cliques que não estejam em nosso `toggle button`.

Vamos adicionar o atributo `data-disclosure` ao nosso botão e filtrar os cliques em qualquer elemento que não tenha esse atributo.

```html
<button data-disclosure>Show More</button>
```
```javascript
document.addEventListener('click', function (event) {
    // Execute apenas em elementos que possuem o atributo [data-disclosure]
    // Se event.target não tiver o atributo, return encerra a callback function
    if (!event.target.hasAttribute('data-disclosure')) return;

    console.log('deu match!');
});
```

## Obtendo nosso conteúdo

Também precisamos de uma forma de associar nosso botão ao conteúdo correspondente. Podemos usar o atributo `[aria-controls]` para isso.

O atributo `[aria-controls]` informa aos screen readers que um botão controla o comportamento de outro conteúdo. Você atribui o ID desse conteúdo como o valor do atributo.

```html
<button data-disclosure aria-controls="now-you-see-me">Mostrar mais</button>
```

Em nosso event listener, podemos obter o valor do atributo `aria-controls` e passá-lo para `document.querySelector()` para obter nosso conteúdo.

Precisaremos prefixá-lo com # porque é um seletor de ID.

```javascript
document.addEventListener('click', function (event) {
    // Execute apenas em elementos que possuem o atributo [data-disclosure]
    // Se event.target não tiver o atributo, return encerra a callback function
    if (!event.target.hasAttribute('data-disclosure')) return;

    // Obtêm o conteúdo para toggle
    // Se nenhum conteúdo correspondente for encontrado, encerra
    // a function com return
    var content = document.querySelector('#' + event.target.getAttribute('aria-controls'));
    if (!content) return;

    console.log(content);
});
```

## Toggling a visibilidade de conteúdo

A última consideração sobre acessibilidade é que o botão também deve ter um atributo `[aria-expanded]`. Isso informa aos screen readers qual é o estado atual do conteúdo.

Se o atributo `[aria-expanded]` tiver o valor `true`, o conteúdo será expandido. Se tiver um valor `false`, o conteúdo será colapsado.

```html
<button data-disclosure aria-controls="now-you-see-me" aria-expanded="true">Show More</button>
```

Em nosso script, verificaremos qual é o valor do atributo `[aria-expanded]`.

Se for true, adicionaremos a alteração para false e adicionaremos o atributo `[hidden]` ao nosso conteúdo para ocultá-lo. Caso contrário, mudaremos para `true` e removeremos o atributo `[hidden]` para revelá-lo.

Embora `true` e `false` sejam booleanos, o método `getAttribute()` os retorna como uma string. Precisaremos verificar 'true' como uma string em nosso código.

```javascript
document.addEventListener('click', function (event) {
    // Execute apenas em elementos que possuem o atributo [data-disclosure]
    // Se event.target não tiver o atributo, return encerra a callback function
    if (!event.target.hasAttribute('data-disclosure')) return;

    // Obtêm o conteúdo para toggle
    // Se nenhum conteúdo correspondente for encontrado, encerra a function com return
    var content = document.querySelector(
        '#' + event.target.getAttribute('aria-controls')
    );
    if (!content) return;

    // Se o conteúdo está visível, esconda ele
    // Caso contrário, mostre ele
    if (event.target.getAttribute('aria-expanded') === 'true') {
        event.target.setAttribute('aria-expanded', false);
        content.setAttribute('hidden', '');
    } else {
        event.target.setAttribute('aria-expanded', true);
        content.removeAttribute('hidden');
    }
});

```

Agora nosso conteúdo irá realmente aparecer e ocultar quando clicado!

## Ocultando conteúdo por default

Atualmente, nosso conteúdo está visível para começar. Em uma application real, provavelmente desejaríamos que eles fossem ocultados por default e revelados quando o botão fosse clicado.

Podemos alterar o valor `[aria-expanded]` para false e adicionar o atributo `[hidden]` a todo o nosso conteúdo por default para que tudo seja collapsed desde o início.

```html
<button data-disclosure aria-controls="now-you-see-me" aria-expanded="false">Mostrar mais</button>

<div id="now-you-see-me" hidden>
    Agora você não vê.
</div>
```

Mas... antes que o JavaScript seja carregado e executado, os usuários não poderão expandir o conteúdo.

Você pode clicar no botão, mas nada acontecerá, o que é confuso. E se o arquivo JS falhar por algum motivo, eles nunca poderão visualizar o conteúdo.

## Aprimoramento progressivo

Uma pequena pitada de aprimoramento progressivo resolverá isso.

Primeiro, removeremos o atributo `[hidden]` do nosso conteúdo, mas iremos adicioná-lo aos elementos do botão e manteremos o atributo `[aria-expanded]` definido como `false`.

```html
<button
    data-disclosure
    aria-controls="now-you-see-me"
    aria-expanded="false"
    hidden
>Mostrar mais</button>

<div id="now-you-see-me">
    Agora você não vê.
</div>
```

Agora, por default, o conteúdo ficará visível, mas o botão ficará oculto.

Quando o JavaScript carrega, queremos mostrar todos os botões e ocultar todo o conteúdo. Usaremos `document.querySelectorAll()` para obter todos os nossos elementos de botão, encontrar seu conteúdo correspondente e ocultá-lo.

Para melhor suporte do navegador, devemos converter nosso `NodeList` em um `array` antes de usar o método `Array.forEach()`.

```javascript
// Obtêm disclosure buttons
var disclosures = Array.prototype.slice.call(
    document.querySelectorAll('[data-disclosure]')
);

// Itera dentro deles com Array.forEach()
disclosures.forEach(function (disclosure) {
    // Obtêm o conteúdo associado com o button
    var content = document.querySelector(
        '#' + disclosure.getAttribute('aria-controls')
    );

    // Se não há conteúdo, não mostra o button
    if (!content) return;

    // Mostra o button e esconde o conteúdo
    disclosure.removeAttribute('hidden');
    content.setAttribute('hidden', '');
});
```

E com isso, temos um `disclosure component` acessível e progressivamente aprimorado.

## Fonte

Newsletter de [Go Make Things](https://gomakethings.com/)