Creare un componente a selezione multipla

Una panoramica di base su come creare un componente multiselezione reattivo, adattivo e accessibile per le esperienze utente di ordinamento e filtro.

In questo post voglio condividere il mio pensiero su un modo per creare un componente di selezione multipla. Prova la demo.

Demo

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

Agli utenti vengono spesso presentati articoli, a volte molti, e in questi casi può essere una buona idea fornire un modo per ridurre l'elenco per evitare il sovraccarico di scelta. Questo post del blog esplora l'interfaccia utente di filtraggio come modo per ridurre le scelte. A questo scopo, presenta gli attributi degli articoli che gli utenti possono selezionare o deselezionare, riducendo i risultati e quindi il sovraccarico di scelta.

Interazioni

L'obiettivo è consentire l'attraversamento rapido delle opzioni di filtro per tutti gli utenti e i loro diversi tipi di input. Verrà fornito con una coppia di componenti adattabili e reattivi. Una barra laterale tradizionale di caselle di controllo per desktop, tastiera e screen reader e un <select multiple> per gli utenti che utilizzano il tocco.

Screenshot del confronto che mostra la modalità Chiaro e Buio per computer con una barra laterale di
caselle di controllo e la modalità Chiaro e Buio per dispositivi mobili iOS e Android con un elemento di selezione multipla.

Questa decisione di utilizzare la selezione multipla integrata per il tocco e non per il computer salva e crea lavoro, ma ritengo che offra esperienze appropriate con un debito di codice inferiore rispetto alla creazione dell'intera esperienza reattiva in un unico componente.

Tocco

Il componente Tocco consente di risparmiare spazio e migliora la precisione dell'interazione utente su dispositivo mobile. Consente di risparmiare spazio comprimendo un'intera barra laterale di caselle di controllo in un'esperienza tattile di overlay integrata <select>. Migliora la precisione dell'input mostrando una grande esperienza di overlay touch fornita dal sistema.

Anteprima di uno screenshot dell&#39;elemento di selezione multipla in Chrome su Android, iPhone e iPad. Su iPad e iPhone la selezione multipla è attivata e ogni dispositivo offre un&#39;esperienza unica ottimizzata per le dimensioni dello schermo.

Tastiera e gamepad

Di seguito è riportata una dimostrazione di come utilizzare un <select multiple> dalla tastiera.

Questa selezione multipla integrata non può essere stilizzata e viene offerta solo in un layout compatto non adatto a presentare molte opzioni. Vedi come non riesci a vedere l'ampiezza delle opzioni in quella piccola casella? Anche se puoi modificarne le dimensioni, non è comunque utilizzabile come una barra laterale di caselle di controllo.

Segni e linee

Entrambi i componenti saranno contenuti nello stesso elemento <form>. I risultati di questo modulo, siano essi caselle di controllo o una selezione multipla, verranno osservati e utilizzati per filtrare la griglia, ma potrebbero anche essere inviati a un server.

<form>

</form>

Componente Caselle di controllo

I gruppi di caselle di controllo devono essere racchiusi in un elemento <fieldset> e devono avere un <legend>. Quando l'HTML è strutturato in questo modo, gli screen reader e FormData comprenderanno automaticamente la relazione tra gli elementi.

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

Con il raggruppamento in atto, aggiungi un <label> e un <input type="checkbox"> per ciascuno dei filtri. Ho scelto di racchiuderli in un <div> in modo che la proprietà CSS gap possa spaziarli in modo uniforme e mantenere l'allineamento quando le etichette vanno su più righe.

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

Uno screenshot con un overlay informativo per la legenda e
  gli elementi del fieldset, mostra il colore e il nome dell&#39;elemento.

Componente <select multiple>

Una funzionalità usata raramente dell'elemento <select> è multiple. Quando l'attributo viene utilizzato con un elemento <select>, l'utente può scegliere più elementi dall'elenco. È come passare da un elenco di pulsanti di opzione a un elenco di caselle di controllo.

<form>
  <select multiple="true" title="Filter results by category">
    …
  </select>
</form>

Per etichettare e creare gruppi all'interno di un <select>, utilizza l'elemento <optgroup> e assegnagli un attributo e un valore label. Questo elemento e il valore dell'attributo sono simili agli elementi <fieldset> e <legend>.

<form>
  <select multiple="true" title="Filter results by category">
    <optgroup label="New">
      …
    </optgroup>
  </select>
</form>

Ora aggiungi gli <option> elementi per il 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>

Uno screenshot del rendering desktop di un elemento a selezione multipla.

Monitoraggio dell'input con contatori per informare la tecnologia assistiva

In questa esperienza utente viene utilizzata la tecnica del ruolo stato per monitorare e mantenere il conteggio dei filtri per gli screen reader e altre tecnologie per la disabilità. Il video di YouTube mostra la funzionalità. L'integrazione inizia con l'HTML e l'attributo role="status".

<div role="status" class="sr-only" id="applied-filters"></div>

Questo elemento leggerà ad alta voce le modifiche apportate ai contenuti. Possiamo aggiornare i contenuti con i contatori CSS man mano che gli utenti interagiscono con le caselle di controllo. Per farlo, dobbiamo prima creare un contatore con un nome in un elemento padre degli input e dell'elemento di stato.

aside {
  counter-reset: filters;
}

Per impostazione predefinita, il conteggio sarà 0, il che è ottimo, in questo progetto non è :checked nulla per impostazione predefinita.

Successivamente, per incrementare il contatore appena creato, selezioneremo gli elementi secondari dell'elemento <aside> che sono :checked. Man mano che l'utente modifica lo stato degli input, il contatore filters aumenterà.

aside :checked {
  counter-increment: filters;
}

CSS ora riconosce il conteggio generale dell'interfaccia utente della casella di controllo e l'elemento del ruolo di stato è vuoto e in attesa di valori. Poiché CSS mantiene il conteggio in memoria, la funzione counter() consente di accedere al valore dai contenuti dello pseudo elemento:

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

Il codice HTML dell'elemento del ruolo di stato ora annuncia "2 filtri" a un lettore di schermo. Questo è un buon inizio, ma possiamo fare di meglio, ad esempio condividere il conteggio dei risultati aggiornati dai filtri. Eseguiremo questa operazione da JavaScript, in quanto non rientra nelle funzionalità dei contatori.

Screenshot dello screen reader di macOS che annuncia il numero di filtri attivi.

L'emozione dell'annidamento

L'algoritmo dei contatori è stato perfetto con CSS nesting-1, perché ho potuto inserire tutta la logica in un unico blocco. Sembra portatile e centralizzato per la lettura e l'aggiornamento.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

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

Layout

Questa sezione descrive i layout tra i due componenti. La maggior parte degli stili di layout è per il componente casella di controllo desktop.

Il modulo

Per ottimizzare la leggibilità e la scansione per gli utenti, al modulo viene assegnata una larghezza massima di 30 caratteri, impostando essenzialmente una larghezza di riga ottica per ogni etichetta di filtro. Il modulo utilizza il layout a griglia e la proprietà gap per distanziare i fieldset.

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

Elemento <select>

L'elenco di etichette e caselle di controllo occupa troppo spazio sui dispositivi mobili. Pertanto, il layout controlla il dispositivo di puntamento principale dell'utente per modificare l'esperienza per il tocco.

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

Un valore di coarse indica che l'utente non potrà interagire con lo schermo con un elevato livello di precisione con il dispositivo di input principale. Su un dispositivo mobile, il valore del puntatore è spesso coarse, poiché l'interazione principale è il tocco. Su un computer, il valore del puntatore è spesso fine, poiché è comune avere un mouse o un altro dispositivo di input di alta precisione collegato.

I fieldset

Lo stile e il layout predefiniti di un <fieldset> con un <legend> sono unici:

Screenshot degli stili predefiniti per un fieldset e una legenda.

Normalmente, per spaziare gli elementi secondari utilizzerei la proprietà gap, ma il posizionamento unico di <legend> rende difficile creare un insieme di elementi secondari equamente distanziati. Al posto di gap, vengono utilizzati il selettore di elementi adiacenti e margin-block-start.

fieldset {
  padding: 2ch;

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

In questo modo, lo spazio di <legend> non viene modificato scegliendo come target solo gli elementi secondari <div>.

Screenshot che mostra la spaziatura del margine tra gli input, ma non la legenda.

L'etichetta e la casella di controllo del filtro

In qualità di elemento secondario diretto di un <fieldset> e all'interno della larghezza massima del 30ch del modulo, il testo dell'etichetta potrebbe andare a capo se troppo lungo. Il ritorno a capo è ottimo, ma l'allineamento errato tra testo e casella di controllo no. Flexbox è ideale per questo scopo.

fieldset > div {
  display: flex;
  gap: 2ch;
  align-items: baseline;
}
Screenshot che mostra l&#39;allineamento del segno di spunta
    alla prima riga di testo in uno scenario di wrapping multilinea.
Gioca di più in questo Codepen

La griglia animata

L'animazione del layout viene eseguita da Isotope. Un plug-in performante e potente per l'ordinamento e il filtro interattivi.

JavaScript

Oltre a contribuire a orchestrare una griglia animata e interattiva, JavaScript viene utilizzato per perfezionare alcuni dettagli.

Normalizzazione dell'input utente

Questo design ha un modulo con due modi diversi per fornire input e non serializza lo stesso. Con un po' di JavaScript, però, possiamo normalizzare i dati.

Screenshot della console JavaScript di DevTools che
  mostra l&#39;obiettivo e i risultati dei dati normalizzati.

Ho scelto di allineare la struttura dei dati dell'elemento <select> alla struttura delle caselle di controllo raggruppate. A questo scopo, al tag <select> viene aggiunto un input listener di eventi, a quel punto vengono mappati selectedOptions.

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

Ora puoi inviare il modulo o, nel caso di questa demo, indicare a Isotope in base a cosa filtrare.

Completamento dell'elemento ruolo stato

L'elemento conteggia e annuncia solo il numero di filtri in base all'interazione con la casella di controllo, ma ho pensato che fosse una buona idea condividere anche il numero di risultati e assicurarmi che vengano conteggiate anche le scelte dell'elemento <select>.

Scelta dell'elemento <select> visualizzata in counter()

Nella sezione di normalizzazione dei dati, è già stato creato un listener sull'input. Alla fine di questa funzione, il numero di filtri scelti e il numero di risultati per questi filtri sono noti. I valori possono essere passati all'elemento del ruolo di stato in questo modo.

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

Risultati riportati nell'elemento role="status"

:checked fornisce un modo integrato per passare il numero di filtri scelti all'elemento del ruolo di stato, ma non consente di visualizzare il numero di risultati filtrati. JavaScript può monitorare l'interazione con le caselle di controllo e, dopo aver filtrato la griglia, aggiungere textContent come ha fatto l'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`
})

Nel complesso, questo lavoro completa l'annuncio "2 filtri che restituiscono 25 risultati".

Screenshot dello screen reader di macOS che annuncia i risultati.

Ora la nostra eccellente esperienza di tecnologia assistiva verrà offerta a tutti gli utenti, indipendentemente dal modo in cui interagiscono con essa.

Conclusione

Ora che sai come ho fatto, come faresti tu?‽ 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community

Ancora nessun elemento da visualizzare.