Como criar um componente de seleção múltipla

Uma visão geral básica de como criar um componente de seleção múltipla responsivo, adaptável e acessível para experiências de usuários de classificação e filtragem.

Neste post, quero compartilhar uma forma de criar um componente de seleção múltipla. Teste a demonstração.

Demo

Se preferir vídeos, confira a versão desta postagem no YouTube:

Visão geral

Os usuários geralmente são apresentados a itens, às vezes muitos, e, nesses casos, pode ser uma boa ideia oferecer uma maneira de reduzir a lista para evitar sobrecarga de escolhas. Esta postagem do blog analisa a interface de filtragem como uma maneira de reduzir as opções. Isso é feito apresentando atributos de itens que os usuários podem selecionar ou desmarcar, reduzindo os resultados e, portanto, a sobrecarga de escolhas.

Interações

O objetivo é permitir a travessia rápida das opções de filtro para todos os usuários e os diferentes tipos de entrada. Isso será entregue com um par de componentes adaptável e responsivo. Uma barra lateral tradicional de caixas de seleção para computadores, teclados e leitores de tela e um <select multiple> para usuários de telas touch.

Captura de tela de comparação mostrando o modo claro e escuro do computador com uma barra lateral de
caixas de seleção em comparação com o iOS e o Android para dispositivos móveis com um elemento de seleção múltipla.

Essa decisão de usar a seleção múltipla integrada para toque, e não para computadores, economiza e cria trabalho, mas acredito que oferece experiências adequadas com menos dívida de código do que criar toda a experiência responsiva em um componente.

Toque

O componente de toque economiza espaço e ajuda na precisão da interação do usuário em dispositivos móveis. Ele economiza espaço ao recolher uma barra lateral inteira de caixas de seleção em uma experiência de toque de sobreposição integrada <select>. Ele ajuda a precisão da entrada mostrando uma grande experiência de sobreposição de toque fornecida pelo sistema.

Uma
visualização de captura de tela do elemento de seleção múltipla no Chrome para Android, iPhone e
iPad. O iPad e o iPhone têm a seleção múltipla ativada, e cada um tem uma
experiência única otimizada para o tamanho da tela.

Teclado e gamepad

Confira abaixo uma demonstração de como usar um <select multiple> no teclado.

Essa seleção múltipla integrada não pode ser estilizada e é oferecida apenas em um layout compacto que não é adequado para apresentar muitas opções. Percebeu como não é possível ver a amplitude de opções nessa caixa pequena? Embora seja possível mudar o tamanho, ele ainda não é tão útil quanto uma barra lateral de caixas de seleção.

Marcação

Ambos os componentes vão estar contidos no mesmo elemento <form>. Os resultados desse formulário, sejam caixas de seleção ou uma seleção múltipla, serão observados e usados para filtrar a grade, mas também poderão ser enviados a um servidor.

<form>

</form>

Componente de caixas de seleção

Os grupos de caixas de seleção precisam ser unidos em um elemento <fieldset> e receber um <legend>. Quando o HTML é estruturado dessa maneira, os leitores de tela e o FormData entendem automaticamente a relação dos elementos.

<form>
  <fieldset>
    <legend>New</legend>
    … checkboxes …
  </fieldset>
</form>

Com o agrupamento em vigor, adicione um <label> e um <input type="checkbox"> para cada um dos filtros. Escolhi envolver o meu em um <div> para que a propriedade CSS gap pudesse espaçá-los uniformemente e manter o alinhamento quando os rótulos forem multilinha.

<form>
  <fieldset>
    <legend>New</legend>
    <div>
      <input type="checkbox" id="last 30 days" name="new" value="last 30 days">
      <label for="last 30 days">Last 30 Days</label>
    </div>
    <div>
      <input type="checkbox" id="last 6 months" name="new" value="last 6 months">
      <label for="last 6 months">Last 6 Months</label>
    </div>
   </fieldset>
</form>

Uma captura de tela com uma sobreposição informativa para a legenda e
  os elementos do conjunto de campos mostra a cor e o nome do elemento.

Componente <select multiple>

Um recurso raramente usado do elemento <select> é multiple. Quando o atributo é usado com um elemento <select>, o usuário pode escolher vários itens da lista. É como mudar a interação de uma lista de opções para uma lista de caixas de seleção.

<form>
  <select multiple="true" title="Filter results by category">
    …
  </select>
</form>

Para rotular e criar grupos em um <select>, use o elemento <optgroup> e atribua um atributo e valor label a ele. Esse elemento e o valor do atributo são semelhantes aos elementos <fieldset> e <legend>.

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      …
    </optgroup>
  </select>
</form>

Agora adicione os elementos <option> para o filtro.

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      <option value="last 30 days">Last 30 Days</option>
      <option value="last 6 months">Last 6 Months</option>
    </optgroup>
  </select>
</form>

Uma captura de tela da renderização para computador de um elemento de seleção múltipla.

Rastrear entradas com contadores para informar a tecnologia assistiva

A técnica de papel de status é usada nessa experiência do usuário para rastrear e manter a contagem de filtros para leitores de tela e outras tecnologias adaptativas. O vídeo do YouTube demonstra o recurso. A integração começa com HTML e o atributo role="status".

<div role="status" class="sr-only" id="applied-filters"></div>

Esse elemento vai ler em voz alta as mudanças feitas no conteúdo. Podemos atualizar o conteúdo com contadores CSS à medida que os usuários interagem com as caixas de seleção. Para isso, primeiro precisamos criar um contador com um nome em um elemento pai das entradas e do elemento de estado.

aside {
  counter-reset: filters;
}

Por padrão, a contagem será 0, o que é ótimo, nada é :checked por padrão neste design.

Em seguida, para incrementar o contador recém-criado, vamos segmentar os filhos do elemento <aside> que são :checked. Conforme o usuário muda o estado das entradas, o contador filters vai ser somado.

aside :checked {
  counter-increment: filters;
}

O CSS agora tem conhecimento da contagem geral da interface da caixa de seleção, e o elemento de função de status está vazio e aguardando valores. Como o CSS mantém a contagem na memória, a função counter() permite acessar o valor do conteúdo do pseudo elemento:

aside #applied-filters::before {
  content: counter(filters) " filters ";
}

O HTML do elemento de função de status agora vai anunciar "2 filtros " para um leitor de tela. Esse é um bom começo, mas podemos melhorar, como compartilhar a contagem de resultados que os filtros atualizaram. Vamos fazer esse trabalho usando JavaScript, já que ele está fora do que os contadores podem fazer.

Uma captura de tela do leitor de tela do MacOS anunciando o número de filtros ativos.

Aninhamento de empolgação

O algoritmo de contadores foi ótimo com o CSS nesting-1, porque consegui colocar toda a lógica em um bloco. Parece portátil e centralizado para leitura e atualização.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

  & #applied-filters::before {
    content: counter(filters) " filters ";
  }
}

Layouts

Esta seção descreve os layouts entre os dois componentes. A maioria dos estilos de layout é para o componente de caixa de seleção para computador.

O formulário

Para otimizar a legibilidade e a leitura para os usuários, o formulário tem uma largura máxima de 30 caracteres, definindo essencialmente uma largura de linha óptica para cada rótulo de filtro. O formulário usa o layout de grade e a propriedade gap para distribuir os campos.

form {
  display: grid;
  gap: 2ch;
  max-inline-size: 30ch;
}

O elemento <select>

A lista de rótulos e caixas de seleção consome muito espaço em dispositivos móveis. Portanto, o layout verifica se o dispositivo de apontar principal do usuário é usado para mudar a experiência de toque.

@media (pointer: coarse) {
  select[multiple] {
    display: block;
  }
}

Um valor de coarse indica que o usuário não poderá interagir com a tela com alta precisão usando o dispositivo de entrada principal. Em um dispositivo móvel, o valor do ponteiro geralmente é coarse, porque a interação principal é por toque. Em um dispositivo de computador, o valor do ponteiro geralmente é fine, porque é comum conectar um mouse ou outro dispositivo de entrada de alta precisão.

Os fieldsets

O estilo e o layout padrão de um <fieldset> com um <legend> são exclusivos:

Captura de tela dos estilos padrão de um conjunto de campos e uma legenda.

Normalmente, para espaçar meus elementos filhos, eu usaria a propriedade gap, mas o posicionamento exclusivo do <legend> dificulta a criação de um conjunto de filhos espaçadamente. Em vez de gap, o seletor de irmãos adjacentes e margin-block-start são usados.

fieldset {
  padding: 2ch;

  & > div + div {
    margin-block-start: 2ch;
  }
}

Isso impede que o espaço da <legend> seja ajustado segmentando apenas os filhos <div>.

Captura de tela mostrando o espaçamento da margem entre as entradas, mas não a legenda.

Rótulo e caixa de seleção do filtro

Como uma filha direta de um <fieldset> e dentro da largura máxima do 30ch do formulário, o texto do rótulo pode ser quebrado se for muito longo. O texto quebrado é ótimo, mas o desalinhamento entre o texto e a caixa de seleção não é. O Flexbox é ideal para isso.

fieldset > div {
  display: flex;
  gap: 2ch;
  align-items: baseline;
}
Captura de tela mostrando como a marca de seleção se alinha à
    primeira linha de texto em um cenário de quebra de várias linhas.
Confira mais neste Codepen

Grade animada

A animação de layout é feita pelo Isótopo. Um plug-in eficiente e poderoso para classificação e filtro interativos.

JavaScript

Além de ajudar a orquestrar uma grade animada e interativa, o JavaScript é usado para polir algumas arestas ásperas.

Como normalizar a entrada do usuário

Esse design tem um formulário com duas maneiras diferentes de fornecer entradas, e elas não são serializadas. No entanto, com um pouco de JavaScript, podemos normalizar os dados.

Captura de tela do console JavaScript do DevTools que
  mostra a meta e os resultados de dados normalizados.

Escolhi alinhar a estrutura de dados do elemento <select> à estrutura de caixas de seleção agrupadas. Para fazer isso, um listener de eventos input é adicionado ao elemento <select>, e o selectedOptions é mapeado.

document.querySelector('select').addEventListener('input', event => {
  // make selectedOptions iterable then reduce a new array object
  let selectData = Array.from(event.target.selectedOptions).reduce((data, opt) => {
    // parent optgroup label and option value are added to the reduce aggregator
    data.push([opt.parentElement.label.toLowerCase(), opt.value])
    return data
  }, [])
})

Agora é seguro enviar o formulário ou, no caso desta demonstração, instruir o Isotope sobre como filtrar.

Como concluir o elemento de papel de status

O elemento só está contabilizando e anunciando a contagem de filtros com base na interação com a caixa de seleção, mas achei que seria uma boa ideia compartilhar o número de resultados e garantir que as opções do elemento <select> também sejam contadas.

A escolha do elemento <select> é refletida no counter()

Na seção de normalização de dados, um listener já foi criado na entrada. Ao final da função, o número de filtros escolhidos e o número de resultados para esses filtros são conhecidos. Os valores podem ser transmitidos para o elemento de função de estado como este.

let statusRoleElement = document.querySelector('#applied-filters')
statusRoleElement.style.counterSet = selectData.length

Resultados refletidos no elemento role="status"

:checked oferece uma maneira integrada de transmitir o número de filtros escolhidos para o elemento de função de status, mas não tem visibilidade para o número filtrado de resultados. O JavaScript pode detectar a interação com as caixas de seleção e, após filtrar a grade, adicionar textContent como o elemento <select> fez.

document
  .querySelector('aside form')
  .addEventListener('input', e => {
    // isotope demo code
    let filterResults = IsotopeGrid.getFilteredItemElements().length
    document.querySelector('#applied-filters').textContent = `giving ${filterResults} results`
})

Com isso, o anúncio "2 filtros com 25 resultados" foi concluído.

Captura de tela do leitor de tela do macOS anunciando resultados.

Agora, nossa excelente experiência de tecnologia adaptativa será entregue a todos os usuários, independentemente de como eles interagem com ela.

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, envie links para mim e vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.