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

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

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

Demonstração

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

Visão geral

Muitas vezes, os usuários recebem itens, às vezes muitos, e, casos, pode ser uma boa ideia fornecer uma forma de reduzir a lista para evitar sobrecarga de opções. Isso postagem do blog explora a interface de filtragem como uma forma de reduzir as opções. Ele faz isso apresentando atributos de item que os usuários podem marcar ou desmarcar, reduzindo os resultados reduzindo a sobrecarga de escolhas.

Interações

O objetivo é possibilitar a passagem rápida das opções de filtro para todos os usuários e seus diferentes tipos de entrada. Ela será entregue com uma resposta um par de componentes. Uma barra lateral tradicional de caixas de seleção para computador, teclado e leitores de tela, e um <select multiple> para usuários de toque.

Captura de tela comparativa mostrando a área de trabalho clara e escura com uma barra lateral de
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 computador, economiza trabalho e cria trabalho, mas acredito que ofereça experiências adequadas com menos débito de código do que a criação de 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 dispositivos móveis. Para economizar espaço, recolhe uma barra lateral inteira de caixas de seleção em uma <select> experiência de toque de sobreposição integrada. Ele ajuda na precisão da entrada, mostrando uma grande experiência de sobreposição de toque fornecida pelo sistema.

Um
captura de tela do elemento de seleção múltipla no Chrome no Android, iPhone e
iPad. O iPad e o iPhone estão com a seleção múltipla aberta, e cada um deles recebe uma
experiência exclusiva otimizada para o tamanho da tela.

Teclado e gamepad

Abaixo está uma demonstração de como usar uma <select multiple> do teclado.

Esta seleção múltipla integrada não pode ser estilizada e só é oferecida em um formato compacto o layout não é adequado para apresentar muitas opções. Veja como você não pode realmente vê a amplitude de opções naquela pequena caixa? Embora seja possível alterar o tamanho, ainda não são tão utilizáveis quanto uma barra lateral de caixas de seleção.

Marcação

Os dois componentes estarão no mesmo elemento <form>. Os resultados de este formulário, seja de caixas de seleção ou de seleção múltipla, será observado e usado para filtrar a grade, mas também podem ser enviados a um servidor.

<form>

</form>

Componente "Caixas de seleção"

Os grupos de caixas de seleção devem ser inseridos <fieldset> elemento e recebe um <legend> Quando o HTML é estruturado dessa forma, leitores de tela e FormData vai a entender automaticamente a relação entre os elementos.

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

Com o agrupamento pronto, 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 gap do CSS pode espaçá-los uniformemente e manter o alinhamento quando os rótulos ficam 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 a legenda e
  elementos fieldset, 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árias da lista. É como mudar a interação de uma lista de opções a 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 <optgroup> e atribuir a ele um atributo e um valor label. Esse elemento e 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 <option> elementos 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 computadores de um elemento de seleção múltipla.

Acompanhamento de entradas com contadores para informar a tecnologia assistiva

O status cargo é usada nesta experiência de usuário, para acompanhar e manter o registro de filtros para leitores de tela e outras tecnologias assistivas. 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>

Este elemento lê em voz alta as mudanças feitas no conteúdo. Podemos atualizar conteúdos com CSS contadores à 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, nada é :checked por padrão neste design.

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

aside :checked {
  counter-increment: filters;
}

O CSS agora está ciente da contagem geral da interface da caixa de seleção e da função de status está vazio e aguardando valores. Como o CSS está mantendo o número em a memória, os counter() permite acessar o valor de pseudo do elemento:

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

O HTML do elemento de função de status agora anunciará "2 filtros " para uma tela leitor de tela. Esse é um bom começo, mas podemos fazer melhor. Por exemplo, compartilhar os resultados resultados atualizados pelos filtros. Faremos isso em JavaScript, pois é fora do que os contadores podem fazer.

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

Empolgação com o aninhamento

O algoritmo de contadores funcionou muito bem com o CSS nesting-1, porque consegui colocar todas 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 estilos de layout são para o componente de caixa de seleção para computadores.

O formulário

Para otimizar a legibilidade e a facilidade de leitura para os usuários, o formulário recebe um máximo largura 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 espaçar o Conjuntos de campo.

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

O elemento <select>

A lista de marcadores e caixas de seleção ocupam muito espaço em dispositivos móveis. Portanto, o layout verifica se é possível mudar o dispositivo apontador principal do usuário a experiência do toque.

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

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

Os camposets

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

Normalmente, para espaçar meus elementos filhos, eu usaria a propriedade gap, mas o o posicionamento do <legend> dificulta a criação de um conjunto com espaçamento uniforme das crianças. Em vez de gap, o irmão adjacente seletor e margin-block-start são usados.

fieldset {
  padding: 2ch;

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

Isso evita que o <legend> tenha o espaço ajustado, segmentando apenas o <div> crianças.

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 um filho direto de um <fieldset> e dentro da largura máxima do elemento 30ch, o texto do rótulo poderá ser quebrado se for muito longo. Ajustar 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 verificação se alinha a
    a primeira linha de texto em um cenário de quebra de linha de várias linhas.
Jogue mais no Codepen

A grade animada

A animação do layout é feita por Isotope. Um um plug-in eficiente e eficiente para classificação e filtro interativos.

JavaScript

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

Normalizar a entrada do usuário

Esse design tem um formato com duas maneiras diferentes de fornecer informações não serialize o mesmo. Com um pouco de JavaScript, é possível normalizar os dados.

Captura de tela do console JavaScript do DevTools que
  mostra o objetivo, os resultados
dos dados normalizados.

Escolhi alinhar a estrutura de dados do elemento <select> às caixas de seleção agrupadas na estrutura dos preços. Para fazer isso, um input o listener de eventos é adicionado ao elemento <select>, quando selectedOptions são mapeados.

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
  }, [])
})

Você já pode enviar o formulário ou, no caso desta demonstração, instrua a Isotope sobre os itens a serem filtrados.

Finalizar o elemento de função de status

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

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

Na seção de normalização de dados, um listener já foi criado na entrada. Em ao final desta função, o número de filtros escolhidos e o número de resultados para esses filtros são conhecidas. Os valores podem ser passados para o elemento de papel de estado assim.

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 observar a interação com as caixas de seleção e, depois de filtrar o grade, adicione 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`
})

Juntos, esse trabalho completa o anúncio "2 filtros que fornecem 25 resultados".

Uma captura de tela do leitor de tela MacOS anunciando os resultados.

Agora, nossa excelente experiência de tecnologia assistiva será entregue a todos os independentemente da interação dos usuários com eles.

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 adicione os links acesse a seção "Remixes da comunidade" abaixo.

Remixes da comunidade

Não há nada aqui ainda.