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.
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.
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.
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>
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>
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.
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:
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>
.
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;
}
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.
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.
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.