Créer un composant à sélection multiple

Présentation de base sur la création d'un composant multisélection responsif, adaptatif et accessible pour trier et filtrer les expériences utilisateur.

Dans ce message, je vais vous expliquer comment créer un composant à sélection multiple. Essayez la démo.

Démo

Si vous préférez regarder une vidéo, voici une version YouTube de cet article :

Présentation

Les utilisateurs se voient souvent présenter des éléments, parfois beaucoup d'éléments. Dans ce cas, il peut être judicieux de fournir un moyen de réduire la liste afin d'éviter une surcharge de choix. Cet article de blog explore l'UI de filtrage comme moyen de réduire les choix. Pour ce faire, il présente des attributs d'article que les utilisateurs peuvent sélectionner ou désélectionner, ce qui réduit les résultats et donc la surcharge de choix.

Interactions

L'objectif est de permettre un parcours rapide des options de filtrage pour tous les utilisateurs et leurs différents types d'entrées. Cela sera fourni avec une paire de composants adaptables et responsifs. Barre latérale de cases à cocher classique pour ordinateur, clavier et lecteurs d'écran, et <select multiple> pour les utilisateurs tactiles.

Capture d&#39;écran de comparaison montrant le thème clair et sombre pour ordinateur avec une barre latérale de cases à cocher, par rapport aux versions iOS et Android pour mobile avec un élément de sélection multiple.

Cette décision d'utiliser la sélection multiple intégrée pour les appareils tactiles, et non pour les ordinateurs, permet de gagner du temps et de créer du travail, mais je pense qu'elle offre des expériences appropriées avec moins de dette de code que de créer l'ensemble de l'expérience responsive dans un seul composant.

Toucher

Le composant tactile permet de gagner de l'espace et de faciliter la précision des interactions utilisateur sur mobile. Elle permet de gagner de l'espace en réduisant toute une barre latérale de cases à cocher en une expérience tactile en superposition <select> intégrée. Il améliore la précision des entrées en affichant une grande expérience de superposition tactile fournie par le système.

Aperçu d&#39;une capture d&#39;écran de l&#39;élément de sélection multiple dans Chrome sur Android, iPhone et iPad. La sélection multiple est activée sur l&#39;iPad et l&#39;iPhone, et chacun bénéficie d&#39;une expérience unique optimisée pour la taille de l&#39;écran.

Clavier et manette

Vous trouverez ci-dessous une démonstration de l'utilisation d'un <select multiple> depuis le clavier.

Cette fonctionnalité de sélection multiple intégrée ne peut pas être stylisée et n'est proposée que dans une mise en page compacte qui n'est pas adaptée à la présentation d'un grand nombre d'options. Vous voyez que vous ne pouvez pas vraiment voir l'étendue des options dans cette petite boîte ? Bien que vous puissiez modifier sa taille, elle n'est toujours pas aussi utilisable qu'une barre latérale de cases à cocher.

Annoter

Les deux composants seront contenus dans le même élément <form>. Les résultats de ce formulaire, qu'il s'agisse de cases à cocher ou d'une sélection multiple, seront observés et utilisés pour filtrer la grille, mais peuvent également être envoyés à un serveur.

<form>

</form>

Composant de cases à cocher

Les groupes de cases à cocher doivent être encapsulés dans un élément <fieldset> et étiquetés avec un <legend>. Lorsque le code HTML est structuré de cette manière, les lecteurs d'écran et FormData comprennent automatiquement la relation entre les éléments.

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

Une fois le regroupement en place, ajoutez une <label> et une <input type="checkbox"> pour chacun des filtres. J'ai choisi de les encapsuler dans un <div> afin que la propriété CSS gap puisse les espacer de manière uniforme et maintenir l'alignement lorsque les libellés sont sur plusieurs lignes.

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

Capture d&#39;écran avec une superposition informative pour les éléments de légende et de fieldset, montrant la couleur et le nom de l&#39;élément.

Composant <select multiple>

multiple est une caractéristique rarement utilisée de l'élément <select>. Lorsque l'attribut est utilisé avec un élément <select>, l'utilisateur est autorisé à en choisir plusieurs dans la liste. C'est comme si vous passiez d'une liste de cases d'option à une liste de cases à cocher.

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

Pour ajouter un libellé et créer des groupes dans un <select>, utilisez l'élément <optgroup> et attribuez-lui un attribut et une valeur label. Cet élément et la valeur de l'attribut sont semblables aux éléments <fieldset> et <legend>.

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

Ajoutez maintenant les éléments <option> pour le filtre.

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

Capture d&#39;écran de l&#39;affichage sur ordinateur d&#39;un élément de sélection multiple.

Suivi des entrées avec des compteurs pour informer les technologies d'assistance

La technique de rôle d'état est utilisée dans cette expérience utilisateur pour suivre et gérer le nombre de filtres pour les lecteurs d'écran et les autres technologies d'assistance. La vidéo YouTube présente cette fonctionnalité. L'intégration commence par le code HTML et l'attribut role="status".

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

Cet élément lit à voix haute les modifications apportées au contenu. Nous pouvons mettre à jour le contenu avec des compteurs CSS lorsque les utilisateurs interagissent avec les cases à cocher. Pour ce faire, nous devons d'abord créer un compteur avec un nom sur un élément parent des éléments d'entrée et d'état.

aside {
  counter-reset: filters;
}

Par défaut, le nombre sera 0, ce qui est excellent, car rien n'est :checked par défaut dans cette conception.

Ensuite, pour incrémenter le compteur que nous venons de créer, nous allons cibler les enfants de l'élément <aside> qui sont :checked. Lorsque l'utilisateur modifie l'état des entrées, le compteur filters s'accumule.

aside :checked {
  counter-increment: filters;
}

Le CSS est désormais au courant du total général de l'interface utilisateur de la case à cocher, et l'élément de rôle d'état est vide et attend des valeurs. Comme le CSS gère le décompte en mémoire, la fonction counter() permet d'accéder à la valeur du contenu d'un pseudo-élément:

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

Le code HTML de l'élément de rôle d'état annoncera désormais "2 filtres" à un lecteur d'écran. C'est un bon début, mais nous pouvons faire mieux, par exemple en partageant le nombre de résultats mis à jour par les filtres. Nous allons effectuer ce travail à partir de JavaScript, car il ne relève pas des compteurs.

Capture d&#39;écran du lecteur d&#39;écran macOS annonçant le nombre de filtres actifs.

Imbriquer l'enthousiasme

L'algorithme des compteurs s'est avéré excellent avec CSS nesting-1, car j'ai pu placer toute la logique dans un seul bloc. Se sent portable et centralisée pour la lecture et la mise à jour.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

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

Mises en page

Cette section décrit les mises en page entre les deux composants. La plupart des styles de mise en page sont destinés au composant de case à cocher pour ordinateur.

Le formulaire

Pour optimiser la lisibilité et la lisibilité du formulaire, le formulaire est limité à 30 caractères, ce qui correspond essentiellement à une épaisseur de ligne optique pour chaque libellé de filtre. Le formulaire utilise la mise en page en grille et la propriété gap pour espacer les champs de formulaire.

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

L'élément <select>

La liste des libellés et des cases à cocher occupe trop d'espace sur les appareils mobiles. Par conséquent, la mise en page vérifie le dispositif de pointage principal de l'utilisateur pour modifier l'expérience pour le toucher.

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

Une valeur coarse indique que l'utilisateur ne pourra pas interagir avec l'écran avec une grande précision avec son périphérique d'entrée principal. Sur un appareil mobile, la valeur du pointeur est souvent coarse, car l'interaction principale est le toucher. Sur un ordinateur de bureau, la valeur du pointeur est souvent fine, car il est courant d'avoir une souris ou un autre périphérique d'entrée haute précision connecté.

Les jeux de champs

Le style et la mise en page par défaut d'un <fieldset> avec un <legend> sont uniques:

Capture d&#39;écran des styles par défaut d&#39;un fieldset et d&#39;une légende.

Normalement, pour espacer mes éléments enfants, j'utilise la propriété gap, mais le positionnement unique de <legend> rend difficile la création d'un ensemble d'enfants espacés de manière uniforme. Au lieu de gap, le sélecteur de frère adjacent et margin-block-start sont utilisés.

fieldset {
  padding: 2ch;

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

Cela évite d'ajuster l'espace de la <legend> en ne ciblant que les enfants <div>.

Capture d&#39;écran montrant l&#39;espacement des marges entre les entrées, mais pas la légende.

Libellé et case à cocher du filtre

En tant qu'enfant direct d'un <fieldset> et dans la largeur maximale du 30ch du formulaire, le texte du libellé peut se chevaucher s'il est trop long. Le retour à la ligne du texte est une bonne chose, mais le décalage entre le texte et la case à cocher ne l'est pas. Flexbox est idéal pour cela.

fieldset > div {
  display: flex;
  gap: 2ch;
  align-items: baseline;
}
Capture d&#39;écran montrant comment la coche s&#39;aligne sur la première ligne de texte dans un scénario de retour à la ligne sur plusieurs lignes.
Essayez d'autres jeux dans cet exemple CodePen

Grille animée

L'animation de mise en page est effectuée par Isotope. Plug-in performant et puissant pour le tri et le filtrage interactifs.

JavaScript

En plus d'aider à orchestrer une grille interactive et animée soignée, JavaScript permet d'éliminer quelques aspérités.

Normaliser l'entrée utilisateur

Cette conception comporte un formulaire avec deux modes d'entrée différents, et ils ne sont pas sérialisés de la même manière. Toutefois, avec du code JavaScript, nous pouvons normaliser les données.

Capture d&#39;écran de la console JavaScript des outils de développement qui présente l&#39;objectif et les résultats de données normalisés.

J'ai choisi d'aligner la structure de données de l'élément <select> sur celle des cases à cocher groupées. Pour ce faire, un écouteur d'événements input est ajouté à l'élément <select>, auquel cas les selectedOptions sont mappés.

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

Vous pouvez maintenant envoyer le formulaire ou, dans le cas de cette démonstration, indiquer à Isotope sur quoi filtrer.

Finalisation de l'élément de rôle d'état

L'élément ne comptabilise et n'annonce que le nombre de filtres en fonction de l'interaction avec la case à cocher, mais j'ai pensé qu'il était également judicieux de partager le nombre de résultats et de s'assurer que les choix de l'élément <select> sont également comptabilisés.

Le choix de l'élément <select> est reflété dans counter()

Dans la section "Normalisation des données", un écouteur a déjà été créé pour l'entrée. À la fin de cette fonction, le nombre de filtres choisis et le nombre de résultats pour ces filtres sont connus. Les valeurs peuvent être transmises à l'élément du rôle d'état comme ceci.

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

Résultats reflétés dans l'élément role="status"

:checked fournit un moyen intégré de transmettre le nombre de filtres choisis à l'élément du rôle d'état, mais manque de visibilité sur le nombre de résultats filtrés. JavaScript peut surveiller l'interaction avec les cases à cocher. Après avoir filtré la grille, ajoutez textContent comme l'élément <select>.

document
  .querySelector('aside form')
  .addEventListener('input', e => {
    // isotope demo code
    let filterResults = IsotopeGrid.getFilteredItemElements().length
    document.querySelector('#applied-filters').textContent = `giving ${filterResults} results`
})

Au total, ce travail achève l'annonce "2 filtres donnant 25 résultats".

Capture d&#39;écran du lecteur d&#39;écran macOS annonçant les résultats.

Notre excellente expérience d'assistance technologique sera désormais proposée à tous les utilisateurs, quelle que soit leur façon d'interagir avec elle.

Conclusion

Maintenant que vous savez comment j'ai fait, comment procéderiez-vous ? 🙂

Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web. Créez une démo, tweetez-moi des liens et je les ajouterai à la section "Remix de la communauté" ci-dessous.

Remix de la communauté

Aucun élément à afficher pour l'instant.