Créer un composant de toast

Présentation de base sur la création d'un composant Toast adaptatif et accessible.

Dans cet article, je vais vous expliquer comment créer un composant Toast. 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 toasts sont des messages courts non interactifs, passifs et asynchrones destinés aux utilisateurs. En règle générale, ils sont utilisés comme modèle de commentaires d'interface pour informer l'utilisateur des résultats d'une action.

Interactions

Les toasts ne sont pas comme les notifications, les alertes et les invites, car ils ne sont pas interactifs. Ils ne sont pas destinés à être ignorés ni à persister. Les notifications sont destinées aux informations plus importantes, aux messages synchrones nécessitant une interaction ou aux messages au niveau du système (par opposition au niveau de la page). Les notifications toast sont plus passives que les autres stratégies de notification.

Annoter

L'élément <output> est un bon choix pour le toast, car il est annoncé aux lecteurs d'écran. Un code HTML correct constitue une base sûre que nous pouvons améliorer avec JavaScript et CSS. Il y aura beaucoup de JavaScript.

Un toast

<output class="gui-toast">Item added to cart</output>

Vous pouvez le rendre plus inclusif en ajoutant role="status". Cela fournit un élément de substitution si le navigateur n'attribue pas le rôle implicite aux éléments <output> conformément aux spécifications.

<output role="status" class="gui-toast">Item added to cart</output>

Conteneur de toast

Vous pouvez afficher plusieurs notifications à la fois. Pour orchestrer plusieurs toasts, un conteneur est utilisé. Ce conteneur gère également la position des toasts à l'écran.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Mises en page

J'ai choisi d'épingler les toasts au inset-block-end de la vue du navigateur. Si d'autres toasts sont ajoutés, ils se superposent à partir de ce bord de l'écran.

Conteneur d'IUG

Le conteneur de toasts effectue tout le travail de mise en page pour présenter les toasts. Il s'agit d'un fixed pour le viewport et utilise la propriété logique inset pour spécifier les bords auxquels épingler, ainsi qu'un peu de padding à partir du même bord block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Capture d&#39;écran avec la taille et la marge intérieure de la boîte DevTools superposées à un élément .gui-toast-container.

En plus de se positionner dans la fenêtre d'affichage, le conteneur de toasts est un conteneur de grille qui peut aligner et distribuer les toasts. Les éléments sont centrés en groupe avec justify-content et individuellement avec justify-items. Ajoutez un peu de gap pour que les toasts ne se touchent pas.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Capture d&#39;écran avec la superposition de la grille CSS sur le groupe de toasts, cette fois en mettant en évidence l&#39;espace et les écarts entre les éléments enfants du toast.

Toast de l'IUG

Un toast individuel comporte des padding, des coins plus doux avec border-radius et une fonction min() pour faciliter le dimensionnement sur mobile et ordinateur. La taille responsive du CSS suivant empêche les toasts de dépasser 90% de la fenêtre d'affichage ou 25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Capture d&#39;écran d&#39;un seul élément .gui-toast, avec la marge intérieure et le rayon de la bordure affichés.

Styles

Une fois la mise en page et le positionnement définis, ajoutez du CSS qui permet de s'adapter aux paramètres et aux interactions des utilisateurs.

Conteneur de pain perdu

Les toasts ne sont pas interactifs. Si vous appuyez dessus ou balayez l'écran, rien ne se passe, mais ils consomment actuellement des événements de pointeur. Empêchez les toasts de voler des clics avec le code CSS suivant.

.gui-toast-group {
  pointer-events: none;
}

Toast de l'IUG

Attribuez aux toasts un thème adaptatif clair ou sombre avec des propriétés personnalisées, HSL et une requête multimédia de préférence.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animation

Un nouveau toast doit s'afficher avec une animation lorsqu'il apparaît à l'écran. Pour prendre en compte la réduction du mouvement, définissez les valeurs translate sur 0 par défaut, mais mettez à jour la valeur de mouvement sur une longueur dans une requête multimédia de préférences de mouvement . Tout le monde voit une animation, mais seuls certains utilisateurs voient le toast se déplacer.

Voici les images clés utilisées pour l'animation du toast. Le CSS contrôlera l'entrée, l'attente et la sortie du toast, le tout dans une seule animation.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

L'élément toast configure ensuite les variables et orchestre les images clés.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

Une fois les styles et le code HTML accessible aux lecteurs d'écran prêts, JavaScript est nécessaire pour orchestrer la création, l'ajout et la destruction des toasts en fonction des événements utilisateur. L'expérience du développeur avec le composant Toast doit être minimale et facile à prendre en main, comme suit:

import Toast from './toast.js'

Toast('My first toast')

Créer le groupe de toasts et les toasts

Lorsque le module de notification s'affiche à partir de JavaScript, il doit créer un conteneur de notification et l'ajouter à la page. J'ai choisi d'ajouter l'élément avant body. Cela rend peu probable les problèmes d'empilement z-index, car le conteneur se trouve au-dessus du conteneur pour tous les éléments du corps.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Capture d&#39;écran du groupe de toasts entre les balises &quot;head&quot; et &quot;body&quot;.

La fonction init() est appelée en interne dans le module, en stockant l'élément en tant que Toaster:

const Toaster = init()

La création d'un élément HTML Toast se fait avec la fonction createToast(). La fonction nécessite du texte pour le toast, crée un élément <output>, l'agrémente de classes et d'attributs, définit le texte et renvoie le nœud.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Gérer un ou plusieurs toasts

JavaScript ajoute désormais un conteneur au document pour contenir les toasts et est prêt à ajouter les toasts créés. La fonction addToast() orchestre la gestion d'un ou de plusieurs toasts. Commencez par vérifier le nombre de toasts et si le mouvement est correct, puis utilisez ces informations pour ajouter le toast ou créer une animation sophistiquée afin que les autres toasts semblent "faire de la place" pour le nouveau toast.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Lorsque vous ajoutez le premier toast, Toaster.appendChild(toast) ajoute un toast à la page, ce qui déclenche les animations CSS: animation de début, attente 3s, animation de fin. flipToast() est appelé lorsqu'il existe des toasts, en utilisant une technique appelée FLIP par Paul Lewis. L'idée est de calculer la différence de position du conteneur avant et après l'ajout du nouveau toast. Imaginez que vous marquiez l'endroit où se trouve le grille-pain maintenant, l'endroit où il va se trouver, puis que vous animiez le mouvement de l'endroit où il se trouvait à l'endroit où il se trouve.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

La grille CSS effectue la mise en page. Lorsqu'un nouveau toast est ajouté, la grille le place au début et l'espace avec les autres. Pendant ce temps, une animation Web est utilisée pour animer le conteneur à partir de l'ancienne position.

Regrouper tout le code JavaScript

Lorsque Toast('my first toast') est appelé, un toast est créé, ajouté à la page (le conteneur peut même être animé pour s'adapter au nouveau toast), une promesse est renvoyée et le toast créé est surveillé pour la finalisation de l'animation CSS (les trois animations de clés-images) afin de résoudre la promesse.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

Je pense que la partie déroutante de ce code se trouve dans la fonction Promise.allSettled() et la mise en correspondance toast.getAnimations(). Comme j'ai utilisé plusieurs animations de clés-images pour le toast, pour savoir avec certitude qu'elles sont toutes terminées, chacune doit être demandée à partir de JavaScript et chacune de leurs promesses finished doit être observée pour être terminée. allSettled s'en charge pour nous, et se résout comme étant terminée une fois que toutes ses promesses ont été remplies. L'utilisation de await Promise.allSettled() signifie que la ligne de code suivante peut supprimer l'élément en toute confiance et supposer que le toast a terminé son cycle de vie. Enfin, l'appel de resolve() remplit la promesse Toast de haut niveau afin que les développeurs puissent nettoyer ou effectuer d'autres tâches une fois le toast affiché.

export default Toast

Enfin, la fonction Toast est exportée du module pour que d'autres scripts puissent l'importer et l'utiliser.

Utiliser le composant Toast

Pour utiliser le toast ou l'expérience de développement du toast, importez la fonction Toast et appelez-la avec une chaîne de message.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Si le développeur souhaite effectuer un nettoyage ou autre, une fois le toast affiché, il peut utiliser async et await.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Conclusion

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

Diversifions nos approches et découvrons toutes les façons de créer 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é