Créer un composant à sélection multiple

Présentation des principes de base de la création d'un composant multi-sélection réactif, adaptatif et accessible pour trier et filtrer les expériences utilisateur.

Dans cet article, je vais vous expliquer comment créer un composant à sélection multiple. Essayez la démonstration.

Démonstration

Si vous préférez la vidéo, voici une version YouTube de ce post:

Présentation

Les utilisateurs voient souvent des éléments, parfois beaucoup d'éléments. Dans ce cas, il peut être judicieux de fournir un moyen de réduire la liste pour éviter la surcharge de choix. Cet article de blog explique comment filtrer l'interface utilisateur pour réduire le choix. Pour ce faire, il présente des attributs d'élément 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 balayage rapide des options de filtrage pour tous les utilisateurs et leurs différents types d'entrées. Il sera livré avec une paire de composants adaptable et réactive. Barre latérale traditionnelle avec des cases à cocher pour les ordinateurs, le clavier et les lecteurs d'écran, ainsi qu'une <select multiple> pour les utilisateurs tactiles.

Capture d&#39;écran d&#39;un comparatif montrant un écran clair et sombre sur ordinateur avec une barre latérale composée de cases à cocher, et un appareil mobile iOS et Android avec un élément multi-sélection.

Cette décision d'utiliser la sélection multiple intégrée pour l'écran tactile plutôt que pour les ordinateurs de bureau permet d'économiser du travail et de créer du travail, mais je pense qu'elle permet d'offrir des expériences appropriées avec moins de contraintes de code que de créer l'expérience responsive complète en un seul composant.

Tactile

Le composant tactile permet de gagner de l'espace et d'améliorer la précision des interactions sur les appareils mobiles. 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 intégrée <select>. Elle améliore la précision des entrées en affichant une grande expérience de superposition tactile fournie par le système.

Capture d&#39;écran de l&#39;élément à 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 offre une expérience unique optimisée pour la taille de l&#39;écran.

Clavier et manette de jeu

Vous trouverez ci-dessous une démonstration de l'utilisation d'un <select multiple> à partir du 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 ne convenant pas à présenter de nombreuses options. Vous voyez comment vous ne pouvez pas vraiment voir l’étendue des options dans cette petite boîte ? Vous pouvez modifier sa taille, mais elle n'est toujours pas aussi utilisable qu'une barre latérale de cases à cocher.

Markup

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 ils pourront également être envoyés à un serveur.

<form>

</form>

Composant "Cases à cocher"

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

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

Une fois le regroupement en place, ajoutez <label> et <input type="checkbox"> pour chacun des filtres. J'ai choisi d'encapsuler le mien dans un <div> afin que la propriété CSS gap puisse les espacer uniformément et maintenir l'alignement lorsque les libellés passent à 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 jeu de champs, affichant la couleur et le nom de l&#39;élément

Composant <select multiple>

multiple est une fonctionnalité rarement utilisée de l'élément <select>. Lorsque l'attribut est utilisé avec un élément <select>, l'utilisateur est autorisé à en choisir un grand nombre dans la liste. C'est comme changer l'interaction d'une liste d'options à une liste de cases à cocher.

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

Pour ajouter des libellés et créer des groupes dans un élément <select>, utilisez l'élément <optgroup>, et attribuez-lui un attribut et une valeur label. Cet élément et cette valeur d'attribut s'apparentent 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 du rendu d&#39;un élément multi-sélection sur ordinateur de bureau.

Suivi des entrées à l'aide de compteurs pour alimenter les technologies d'assistance

La technique du rôle d'état est utilisée dans cette expérience utilisateur pour suivre et gérer les filtres pour les lecteurs d'écran et d'autres technologies d'assistance. La vidéo YouTube présente cette fonctionnalité. L'intégration commence par 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 entrées et de l'élément d'état.

aside {
  counter-reset: filters;
}

Par défaut, ce nombre est 0, ce qui est très bien. 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 change l'état des entrées, le compteur filters augmente.

aside :checked {
  counter-increment: filters;
}

Le CSS connaît désormais le décompte général de l'interface utilisateur des cases à cocher, et l'élément du rôle d'état est vide et affiche des valeurs en attente. Étant donné que CSS maintient le décompte en mémoire, la fonction counter() permet d'accéder à la valeur à partir du contenu du pseudo-élément:

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

Le code HTML de l'élément de rôle d'état annonce désormais "2 filtres" à un lecteur d'écran. C'est un bon début, mais nous pouvons faire mieux, comme partager le nombre de résultats que les filtres ont mis à jour. Nous allons effectuer cette tâche à partir de JavaScript, car elle ne fait pas partie des capacités des compteurs.

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

L'engouement pour l'imbrication

L'algorithme de compteurs était idéal avec l'imbrication CSS 1, car j'ai pu rassembler toute la logique en un seul bloc. Se sent portable et centralisé pour la lecture et les mises à jour.

aside {
  counter-reset: filters;

  & :checked {
    counter-increment: filters;
  }

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

Mises en page

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

Le formulaire

Afin d'optimiser la lisibilité et la numérisation pour les utilisateurs, une largeur maximale de 30 caractères est attribuée au formulaire, ce qui définit essentiellement une épaisseur de ligne optique pour chaque étiquette de filtre. Le formulaire utilise une mise en page sous forme de grille et la propriété gap pour espacer les ensembles de champs.

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

L'élément <select>

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

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

La 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 qu'une souris ou un autre périphérique d'entrée de haute précision soit connecté.

Champs

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

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

Normalement, pour espacer les éléments enfants, j'utiliserais 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 régulière. Au lieu d'gap, le sélecteur associé et margin-block-start sont utilisés.

fieldset {
  padding: 2ch;

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

Cela permet d'éviter l'ajustement de l'espace de <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é du filtre et case à cocher

En tant qu'enfant direct d'un <fieldset> et dans la largeur maximale de l'30ch du formulaire, le texte du libellé peut être renvoyé à la ligne s'il est trop long. Le renvoi à la ligne du texte est parfait, mais le désalignement entre le texte et la case à cocher ne l'est pas. Flexbox est l'appareil 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 multiligne.
Jouez plus dans ce Codepen

La grille animée

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

JavaScript

En plus d'aider à orchestrer une grille animée et interactive soignée, JavaScript est utilisé pour peaufiner quelques apparences brutes.

Normaliser l'entrée utilisateur

Cette conception comporte un formulaire avec deux façons différentes de fournir des entrées, qui ne se sérialisent pas 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 affiche les résultats de données normalisés concernant les objectifs.

J'ai choisi d'aligner la structure de données de l'élément <select> sur la structure des cases à cocher groupées. Pour ce faire, un écouteur d'événements input est ajouté à l'élément <select>. Il est alors mappé sur 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
  }, [])
})

Vous pouvez maintenant envoyer le formulaire ou, dans le cas de cette démonstration, indiquer à Isotope les éléments de filtrage à utiliser.

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

L'élément ne compte et annonce le nombre de filtres qu'en fonction de l'interaction avec la case à cocher, mais il m'a semblé judicieux de partager également le nombre de résultats et de s'assurer que les choix d'éléments <select> sont également comptabilisés.

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

Dans la section de normalisation des données, un écouteur a déjà été créé à l'entrée. À la fin de cette fonction, le nombre de filtres choisis et le nombre de résultats de 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 de rôle d'état, mais manque de visibilité sur le nombre de résultats filtrés. JavaScript peut surveiller les interactions avec les cases à cocher. Après avoir filtré la grille, ajoutez textContent comme l'a fait 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`
})

L'annonce "2 filtres donnant 25 résultats" est maintenant finalisée.

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

Notre excellente expérience de technologie d'assistance va maintenant être proposée à tous les utilisateurs, quelle que soit la façon dont ils interagissent avec elle.

Conclusion

Maintenant que vous savez comment je l'ai fait, comment le feriez-vous‽ 😃 ?

Diversissons nos approches et apprenons toutes les façons de créer sur le Web. Créez une démo, cliquez sur les liens tweet me, et je l'ajouterai à la section "Remix" de la communauté ci-dessous.

Remix de la communauté

Aucun élément à afficher pour l'instant