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

Uma visão geral básica de como criar um componente responsivo, adaptável e acessível com seleção múltipla para classificar e filtrar experiências do usuário.

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

Demonstração

Se preferir vídeo, aqui está uma versão do YouTube desta postagem:

Visão geral

Muitas vezes, os usuários recebem muitos itens e, nesses casos, pode ser uma boa ideia oferecer uma maneira de reduzir a lista e evitar a sobrecarga de opções. Esta postagem do blog explora a filtragem da interface como uma maneira de reduzir as opções. Para fazer isso, eles apresentam atributos do item que os usuários podem marcar ou desmarcar, reduzindo os resultados e, portanto, a sobrecarga de escolhas.

Interações

O objetivo é permitir uma travessia rápida de opções de filtros para todos os usuários e os variados tipos de entrada deles. Isso será fornecido com um par de componentes adaptável e responsivo. Uma barra lateral tradicional de caixas de seleção para computador, teclado e leitores de tela, e uma <select multiple> para usuários com tela touchscreen.

Captura de tela da comparação mostrando um 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 multisseleção integrada para toque, e não para computador, economiza trabalho e cria trabalho, mas acredito que entrega 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. Para economizar espaço, recolhe uma barra lateral de caixas de seleção em uma experiência de toque de sobreposição integrada do <select>. Ele ajuda na precisão de entrada mostrando uma grande experiência de sobreposição de toque fornecida pelo sistema.

Uma
captura de tela do elemento de seleção múltipla no Chrome no Android, iPhone e
iPad. O iPad e o iPhone têm a opção de seleção múltipla aberta, e cada um deles 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, inadequado para apresentar muitas opções. Vê como não dá para enxergar a amplitude de opções naquela caixa pequena? Embora seja possível mudar o tamanho, ele ainda não é tão utilizável 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 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"

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 vão entender automaticamente a relação dos elementos.

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

Com o agrupamento implementado, adicione um <label> e um <input type="checkbox"> para cada um dos filtros. Optei por unir o meu em um <div> para que a propriedade CSS gap possa espaçá-los uniformemente e manter o alinhamento quando os rótulos ficarem multilinhas.

<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 de legenda e conjunto de campos, que 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 muitos 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 dentro de uma <select>, use o elemento <optgroup> e atribua a ele um atributo e um valor label. Esse elemento e valor de 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>

Captura de tela da renderização da área de trabalho de um elemento de seleção múltipla.

Rastreamento de entradas com contadores para informar a tecnologia adaptativa

A técnica de 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 os contadores CSS à medida que os usuários interagem com as caixas de seleção. Para fazer 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. Nesse caso, nada é :checked por padrão.

Em seguida, para incrementar nosso contador recém-criado, segmentaremos os filhos do elemento <aside> que são :checked. À medida que o usuário muda o estado das entradas, o contador filters é calculado.

aside :checked {
  counter-increment: filters;
}

O CSS agora está ciente da contagem geral da interface da caixa de seleção, e o elemento do papel de status está vazio e aguardando valores. Como o CSS está mantendo o cálculo 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 do papel de status agora anunciará "2 filtros " para um leitor de tela. Esse é um bom começo, mas podemos melhorar, como compartilhar o resultado dos resultados atualizados pelos filtros. Faremos esse trabalho no JavaScript, porque está fora do que os contadores podem fazer.

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

Empolgação durante o nascimento

O algoritmo de contadores foi ótimo com o CSS nesting-1, já que consegui colocar toda a lógica em um bloco. Parece portátil e centralizada 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 da área de trabalho.

O formulário

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

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

O elemento <select>

A lista de marcadores e as caixas de seleção consomem muito espaço em dispositivos móveis. Portanto, o layout verifica o dispositivo indicador principal do usuário 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 grandes quantidades de precisão com 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 desktop, o valor do ponteiro geralmente é fine, porque é comum ter um mouse ou outro dispositivo de entrada de alta precisão conectado.

Os campos

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 os elementos filhos, eu usaria a propriedade gap, mas o posicionamento exclusivo do <legend> dificulta a criação de um conjunto de filhos espaçados uniformemente. Em vez de gap, são usados o seletor de irmão secundário e margin-block-start.

fieldset {
  padding: 2ch;

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

Isso evita que a <legend> tenha o espaço ajustado segmentando apenas os filhos da <div>.

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

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

Como filho direto de um <fieldset> e dentro da largura máxima da 30ch do formulário, o texto do rótulo poderá ser unido se for muito longo. Ajustar o texto é ó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 ajuste de várias linhas.
Assistir mais neste Codepen

A grade animada

A animação do layout é feita pelo isotope. Um plug-in eficiente e de alto desempenho para classificação e filtro interativos.

JavaScript

Além de ajudar a orquestrar uma grade interativa e animada, 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 serializam da mesma forma. No entanto, com um pouco de JavaScript, podemos normalizar os dados.

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

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>. Nesse momento, 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 o que filtrar.

Concluindo o elemento do papel de status

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

Escolha de elemento <select> refletido 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 para esses filtros são conhecidos. Os valores podem ser passados para o elemento do papel do estado desta forma.

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

Resultados refletidos no elemento role="status"

:checked fornece uma maneira integrada de transmitir o número de filtros escolhidos para o elemento do papel de status, mas não tem visibilidade para o número filtrado de resultados. O JavaScript pode acompanhar a interação com as caixas de seleção e, depois de filtrar a grade, adicionar textContent da mesma forma que o elemento <select>.

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

No total, esse trabalho completa o anúncio "2 filtros com 25 resultados".

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

Agora nossa excelente experiência em tecnologia assistiva será disponibilizada a todos os usuários, independentemente de como eles interajam com ela.

Conclusão

Agora que você sabe como eu fiz isso, o que você faria ‽ 🙂

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

Remixes da comunidade

Ainda não temos nada aqui.