Créer un composant de toast

Présentation des principes de base de la création d'un composant toast adaptatif et accessible.

Dans cet article, je vais vous expliquer comment créer un toast. Essayez la démonstration.

Démonstration

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

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 retour d'information sur l'interface afin d'informer l'utilisateur des résultats d'une action.

Interactions

Les toasts sont différents des notifications, des alertes et des invites, car elles ne sont pas interactives et ne sont pas destinées à être ignorées ni conservées. Les notifications sont destinées à des informations plus importantes, à des messages synchrones nécessitant une interaction ou à des messages au niveau du système (par opposition aux messages au niveau de la page). Les toasts sont plus passifs que les autres stratégies de notification.

Markup

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

Un toast

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

Il peut être plus inclusif en ajoutant role="status". Cela permet une solution de secours si le navigateur n'accorde 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

Vous pouvez afficher plusieurs toasts à la fois. Pour orchestrer plusieurs toasts, nous utilisons un conteneur. 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 IUG

Le conteneur de toasts effectue toutes les tâches de mise en page pour présenter des toasts. Il est fixed dans la fenêtre d'affichage et utilise la propriété logique inset pour spécifier les bords à épingler, ainsi qu'une partie 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 de la zone des outils de développement et la marge intérieure superposée à un élément &quot;.gui-toast-container&quot;.

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 centrés avec justify-items. Ajoutez un peu de gap pour ne pas toucher les toasts.

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

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

Toast de l'IUG

Un toast individuel comporte du padding, des angles plus souples avec border-radius, et une fonction min() pour faciliter le dimensionnement sur mobile et ordinateur. La taille responsive dans le code CSS suivant empêche les toasts de se développer 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 l&#39;arrondi de bordure

Styles

Une fois la mise en page et le positionnement définis, ajoutez du 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. Le fait d'appuyer dessus ou de le balayer n'a aucun effet, mais consomme actuellement les événements de pointeur. Empêchez les toasts de voler les clics avec le CSS suivant.

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

Toast de l'IUG

Appliquez un thème adaptatif clair ou sombre aux toasts avec des propriétés personnalisées, du texte 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 toast doit s'afficher avec une animation lorsque l'écran s'affiche. L'adaptation des mouvements réduits est effectuée en définissant les valeurs translate sur 0 par défaut, mais en mettant à jour la valeur du mouvement sur une durée dans une requête multimédia de préférence de mouvement . Tout le monde voit une animation, mais seuls certains utilisateurs ont le toast parcourir une distance.

Voici les images clés utilisées pour l'animation de toast. Le CSS contrôlera l'entrée, l'attente et la sortie du toast, le tout en 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 de 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 le code HTML accessible des styles et du lecteur d'écran prêt, JavaScript est nécessaire pour orchestrer la création, l'ajout et la destruction de toasts en fonction des événements utilisateur. L'expérience développeur du composant de toast doit être minime et facile à démarrer, 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 seront donc peu probables, car le conteneur est 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 toast entre les balises &quot;head&quot; et &quot;body&quot;.

La fonction init() est appelée en interne dans le module, et l'élément est stocké sous la forme Toaster:

const Toaster = init()

La création d'un élément HTML de toast est effectuée à l'aide de la fonction createToast(). La fonction requiert du texte pour le toast, crée un élément <output>, l'ajoute à des classes et des 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 des toasts et est prêt à ajouter les toasts créés. La fonction addToast() orchestre la gestion d'un ou de plusieurs toasts. Tout d'abord, vérifiez le nombre de toasts et si les mouvements sont autorisés, 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)
}

Lors de l'ajout du premier toast, Toaster.appendChild(toast) ajoute un toast sur la page déclenchant les animations CSS: animate in, attend 3s, animate out. flipToast() est appelé en cas de toasts existants, à l'aide d'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. C'est un peu comme pour indiquer l'emplacement actuel du grille-pain, où il doit se trouver, puis l'animer.

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 soulève 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.

Assembler tout le code JavaScript

Lorsque Toast('my first toast') est appelé, un toast est créé et ajouté à la page (peut-être même que le conteneur est animé pour accueillir le nouveau toast). Une promesse est renvoyée, et le toast créé est surveillé pour la fin de l'animation CSS (les trois animations d'images clés) 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 de ce code qui prête à confusion se trouve dans la fonction Promise.allSettled() et le mappage toast.getAnimations(). Étant donné que j'ai utilisé plusieurs animations d'images clés pour le toast, pour savoir avec certitude qu'elles sont toutes terminées, chacune doit être demandée auprès de JavaScript et chacune de ses promesses finished observées pour l'achèvement. allSettled fait cela pour nous, se considérant comme étant achevé une fois toutes ses promesses 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 est terminé son cycle de vie. Enfin, l'appel de resolve() remplit la promesse de toast de haut niveau afin que les développeurs puissent effectuer un nettoyage ou effectuer d'autres tâches une fois le toast affiché.

export default Toast

Enfin, la fonction Toast est exportée à partir du module pour que d'autres scripts puissent être importés et utilisés.

Utiliser le composant Toast

L'utilisation du toast, ou de l'expérience développeur du toast, se fait en important la fonction Toast et en l'appelant 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 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é