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émonstration.

Démo

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

Présentation

Les toasts sont des messages courts, passifs, asynchrones et non interactifs destinés aux utilisateurs. Ils sont généralement utilisés comme modèle de commentaires d'interface pour informer l'utilisateur des résultats d'une action.

Interactions

Les toasts sont différents des notifications, des alertes et des invites, car ils ne sont pas interactifs. Ils ne sont pas censés être ignorés ni persister. Les notifications sont réservées aux informations importantes, aux messages synchrones nécessitant une interaction ou aux messages au niveau du système (par opposition à ceux au niveau de la page). Les toasts sont plus passifs 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. Et il y aura beaucoup de JavaScript.

Un toast

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

Il peut être plus inclusif en ajoutant role="status". Cela fournit une solution de secours si le navigateur n'attribue pas aux éléments <output> le rôle implicite conformément à la spécification.

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

Un conteneur de toast

Plusieurs toasts peuvent être affichés en même temps. Un conteneur est utilisé pour orchestrer plusieurs toasts. 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 fenêtre d'affichage. Si d'autres toasts sont ajoutés, ils s'empilent à partir de ce bord de l'écran.

Conteneur d'UI

Le conteneur de toasts se charge de la mise en page pour présenter les toasts. Il est fixed à la fenêtre d'affichage et utilise la propriété logique inset pour spécifier les bords à é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 d&#39;outils de développement superposées à un élément .gui-toast-container.

En plus de se positionner dans la fenêtre d'affichage, le conteneur de toast est un conteneur de grille qui peut aligner et distribuer les toasts. Les éléments sont centrés en tant que 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 grille CSS superposée au 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 coins padding et des coins plus doux avec border-radius, ainsi qu'une fonction min() pour faciliter le dimensionnement sur mobile et ordinateur. La taille responsive dans le CSS suivant empêche les toasts de s'étendre au-delà de 90 % de la fenêtre d'affichage ou de 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 bordure affichés.

Styles

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

Conteneur de toast

Les toasts ne sont pas interactifs. Appuyer dessus ou balayer l'écran ne fait rien, mais ils consomment actuellement des événements de pointeur. Empêchez les toasts de voler des clics avec le CSS suivant.

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

Toast de l'IUG

Donnez aux toasts un thème adaptatif clair ou sombre avec des propriétés personnalisées, HSL et une requête mé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 entre dans l'écran. Pour tenir compte de la réduction du mouvement, les valeurs translate sont définies sur 0 par défaut, mais la valeur de mouvement est mise à jour sur une longueur dans une requête média de préférence de mouvement . Tout le monde bénéficie d'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 au lecteur d'écran prêts, JavaScript est nécessaire pour orchestrer la création, l'ajout et la suppression 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 ceci :

import Toast from './toast.js'

Toast('My first toast')

Créer le groupe de toasts et les toasts

Lorsque le module de toast se charge à partir de JavaScript, il doit créer un conteneur de toast et l'ajouter à la page. J'ai choisi d'ajouter l'élément avant body. Les problèmes d'empilement de z-index sont ainsi peu probables, car le conteneur se trouve au-dessus du conteneur de 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 au module, en stockant l'élément sous la forme Toaster :

const Toaster = init()

La création d'éléments HTML de toast est effectuée 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. Vérifiez d'abord le nombre de toasts et si le mouvement est correct, puis utilisez ces informations pour ajouter le toast ou effectuer 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 d'entrée, attente 3s, animation de sortie. 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 marquez l'emplacement actuel du grille-pain, celui où il va se trouver, puis que vous l'animez de son emplacement initial à son emplacement final.

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 se charge de 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 observé pour la fin de l'animation CSS (les trois animations de keyframe) pour la résolution de 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() 
  })
}

La partie déroutante de ce code se trouve dans la fonction Promise.allSettled() et le mappage toast.getAnimations(). Comme j'ai utilisé plusieurs animations de keyframes pour le toast, pour être sûr 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 la fin. allSettled fonctionne pour nous, en se résolvant comme terminé une fois que toutes ses promesses ont été tenues. 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 de 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 chose après l'affichage du toast, 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 feriez-vous ? 🙂

Diversifions nos approches et découvrons toutes les façons de créer sur le Web. Créez une démo, tweetez-moi les liens et je l'ajouterai à la section des remix de la communauté ci-dessous !

Remix de la communauté