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.
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.
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.
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>
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>
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.
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:
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>
.
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.](https://web.dev/static/articles/building/a-multi-select-component/image/screenshot-showing-the-c-b135285ef7d8a.png?authuser=6&hl=pt)
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.
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".
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.