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