Créer un composant de toast

Présentation générale de la création d'un composant de toast adaptatif et accessible.

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

Démonstration

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 général, 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 viewport. 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 de la zone des outils de développement et une marge intérieure en superposition sur 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 dans le code CSS suivant empêche les toasts de se développer plus larges que 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 du code CSS qui facilite l'adaptation aux paramètres et aux interactions de l'utilisateur.

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 GUI

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 s'affiche à l'écran. Pour prendre en charge un mouvement réduit, vous devez définir les valeurs translate sur 0 par défaut, mais mettre à jour la valeur de 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 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

Les styles et les lecteurs d'écran étant compatibles HTML, 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 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 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 planifiant l'élément en tant que Toaster:

const Toaster = init()

La création d'éléments 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é en cas de toasts existants, à l'aide de la technique appelée FLIP de 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 si vous signaliez l'emplacement actuel du grille-pain, où il allait être, puis que vous animeriez de là où il se trouvait.

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éé et ajouté à la page (peut-être même que le conteneur est animé pour s'adapter au nouveau toast), une promesse est renvoyée et le toast créé est surveillé pour vérifier que l'animation CSS (les trois animations d'images clés) est terminée.

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() 
  })
}

J'ai senti que la partie de ce code déroutante se trouvait dans la fonction Promise.allSettled() et le mappage 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 fonctionne pour nous, et se considère comme complet 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() répond à la promesse de haut niveau Toast afin que les développeurs puissent effectuer un nettoyage ou 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, vous devez importer la fonction Toast et l'appeler 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 ? 🙂

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é