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 classificação e filtragem do usuário.

Nesta postagem, quero compartilhar ideias sobre uma forma de criar um componente de seleção múltipla. Confira a demonstração.

Demo

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

Visão geral

Os usuários geralmente têm acesso 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 comparativa que mostra o computador claro e escuro com uma barra lateral de
caixas de seleção em comparação com dispositivos móveis iOS e Android 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 a 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 de 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

Abaixo está uma demonstração de como usar uma <select multiple> do 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

Os dois componentes estarão 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 "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 ativado, adicione um <label> e um <input type="checkbox"> para cada um dos filtros. Escolhi envolver o meu em um <div> para que a propriedade gap do CSS 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 os elementos da legenda e
  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 de um elemento de seleção múltipla em computadores.

Rastrear entradas com contadores para informar a tecnologia assistiva

A técnica do papel de status é usada nessa experiência do usuário para acompanhar 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 nosso contador recém-criado, vamos segmentar filhos do elemento <aside> que são :checked. À medida que o usuário muda o estado das entradas, o contador filters é contabilizado.

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. Faremos isso em JavaScript, porque os contadores não podem fazer isso nele.

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

Empolgação com o aninhamento

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

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, já que a interação principal é o 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 uma <fieldset> com <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.

O rótulo do filtro e a caixa de seleção

Como uma filha direta de uma <fieldset> e dentro da largura máxima do 30ch do formulário, o texto do rótulo pode ser quebrado se for muito longo. Ajustar texto é ótimo, mas o desalinhamento entre texto e 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 refinar algumas arestas.

Como normalizar a entrada do usuário

Esse design tem um formulário com duas maneiras diferentes de fornecer entrada, 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, os resultados dos 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. No final dessa função, o número de filtros escolhidos e o número de resultados desses 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.

Uma captura de tela do leitor de tela MacOS anunciando os 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 um tweet para mim (link em inglês) e eu vou adicionar o conteúdo à seção de remixes da comunidade abaixo.

Remixes da comunidade

Ainda não há nada aqui.