Creazione di un componente toast

Una panoramica di base su come creare un componente toast adattivo e accessibile.

In questo post voglio condividere il mio pensiero su come creare un componente di notifica. Prova la demo.

Demo

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

I toast sono messaggi brevi non interattivi, passivi e asincroni per gli utenti. In genere vengono utilizzati come pattern di feedback dell'interfaccia per informare l'utente sui risultati di un'azione.

Interazioni

I toast sono diversi da notifiche, avvisi e richieste perché non sono interattivi, non sono pensati per essere chiusi o rimanere sullo schermo. Le notifiche riguardano informazioni più importanti, messaggi sincroni che richiedono interazione o messaggi a livello di sistema (anziché a livello di pagina). I toast sono più passivi rispetto ad altre strategie di avviso.

Segni e linee

L'elemento <output> è una buona scelta per il toast perché viene annunciato ai lettori dello schermo. L'HTML corretto fornisce una base sicura per il miglioramento con JavaScript e CSS e ci sarà molto JavaScript.

Un brindisi

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

Può essere più inclusivo aggiungendo role="status". In questo modo viene fornito un fallback se il browser non assegna agli elementi <output> il ruolo implicito come da specifica.

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

Un contenitore di toast

È possibile visualizzare più di un toast alla volta. Per orchestrare più avvisi, viene utilizzato un contenitore. Questo contenitore gestisce anche la posizione dei toast sullo schermo.

<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>

Layout

Ho scelto di bloccare i toast nella inset-block-end della finestra e, se vengono aggiunti altri toast, questi si impilano dal bordo dello schermo.

Contenitore GUI

Il contenitore dei toast esegue tutto il lavoro di layout per la presentazione dei toast. È fixed alla finestra e utilizza la proprietà logica inset per specificare a quali bordi ancorarsi, oltre a un po' di padding dallo stesso bordo block-end.

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

Screenshot con le dimensioni e il padding della casella DevTools sovrapposti a un elemento .gui-toast-container.

Oltre a posizionarsi all'interno dell'area visibile, il contenitore dei toast è un contenitore a griglia che può allineare e distribuire i toast. Gli elementi sono centrati come gruppo con justify-content e centrati singolarmente con justify-items. Aggiungi un po' di gap in modo che i toast non si tocchino.

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

Screenshot con l&#39;overlay della griglia CSS sul gruppo di toast, questa volta
che evidenzia lo spazio e gli spazi vuoti tra gli elementi secondari del toast.

GUI Toast

Un singolo toast ha alcuni padding, alcuni angoli più morbidi con border-radius, e una funzione min() per aiutare a dimensionare i toast su dispositivi mobili e computer. La dimensione adattabile nel seguente CSS impedisce ai toast di diventare più larghi del 90% dell'area visibile o 25ch.

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

Screenshot di un singolo elemento .gui-toast, con il padding e il raggio del bordo
mostrati.

Stili

Una volta impostati il layout e il posizionamento, aggiungi il CSS che ti aiuta ad adattarti alle impostazioni e alle interazioni dell'utente.

Contenitore dei toast

I toast non sono interattivi, toccarli o scorrerli non fa nulla, ma al momento utilizzano gli eventi puntatore. Impedisci ai toast di intercettare i clic con il seguente CSS.

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

GUI Toast

Assegna ai toast un tema adattivo chiaro o scuro con proprietà personalizzate, HSL e una query multimediale delle preferenze.

.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%;
  }
}

Animazione

Un nuovo toast dovrebbe presentarsi con un'animazione quando entra nella schermata. L'adattamento al movimento ridotto viene eseguito impostando i valori di translate su 0 per impostazione predefinita, ma aggiornando il valore di movimento a una lunghezza in una query multimediale di preferenza di movimento . Tutti vedono un'animazione, ma solo alcuni utenti vedono il messaggio di notifica spostarsi per una certa distanza.

Ecco i fotogrammi chiave utilizzati per l'animazione del toast. Il CSS controllerà l'ingresso, l'attesa e l'uscita della notifica di tipo toast, tutto in un'unica animazione.

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

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

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

L'elemento toast configura quindi le variabili e orchestra i fotogrammi chiave.

.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

Con gli stili e il codice HTML accessibile allo screen reader pronti, è necessario JavaScript per orchestrare la creazione, l'aggiunta e l'eliminazione dei toast in base agli eventi utente. L'esperienza dello sviluppatore del componente toast deve essere minima e facile da usare, come in questo esempio:

import Toast from './toast.js'

Toast('My first toast')

Creazione del gruppo di toast e dei toast

Quando il modulo di notifica viene caricato da JavaScript, deve creare un contenitore di notifiche e aggiungerlo alla pagina. Ho scelto di aggiungere l'elemento prima di body, in questo modo i problemi di impilamento di z-index sono improbabili perché il contenitore si trova sopra il contenitore di tutti gli elementi del corpo.

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

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

Screenshot del gruppo di notifiche toast tra i tag head e body.

La funzione init() viene chiamata internamente al modulo, memorizzando l'elemento come Toaster:

const Toaster = init()

La creazione dell'elemento HTML toast viene eseguita con la funzione createToast(). La funzione richiede del testo per il toast, crea un elemento <output>, lo adorna con alcune classi e attributi, imposta il testo e restituisce il nodo.

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

  return node
}

Gestire una o più notifiche popup

JavaScript ora aggiunge un contenitore al documento per contenere i toast ed è pronto per aggiungere i toast creati. La funzione addToast() coordina la gestione di una o più notifiche. Controllando prima il numero di toast e se il movimento è accettabile, poi utilizzando queste informazioni per aggiungere il toast o eseguire un'animazione in modo che gli altri toast sembrino "fare spazio" al nuovo toast.

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

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

Quando aggiungi il primo messaggio di notifica, Toaster.appendChild(toast) aggiunge un messaggio di notifica alla pagina che attiva le animazioni CSS: animazione in entrata, attesa 3s, animazione in uscita. flipToast() viene chiamato quando sono presenti toast esistenti, utilizzando una tecnica chiamata FLIP da Paul Lewis. L'idea è di calcolare la differenza nelle posizioni del contenitore, prima e dopo l'aggiunta del nuovo messaggio di notifica. Immagina di segnare la posizione attuale del Toaster e quella futura, poi di animare il movimento dalla posizione iniziale a quella finale.

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 griglia CSS si occupa del layout. Quando viene aggiunto un nuovo toast, la griglia lo posiziona all'inizio e lo distanzia dagli altri. Nel frattempo, un'animazione web viene utilizzata per animare il container dalla vecchia posizione.

Mettere insieme tutto il codice JavaScript

Quando viene chiamato Toast('my first toast'), viene creato un toast, aggiunto alla pagina (forse anche il contenitore viene animato per ospitare il nuovo toast), viene restituita una promessa e il toast creato viene monitorato per il completamento dell'animazione CSS (le tre animazioni keyframe) per la risoluzione della promessa.

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 parte più confusa di questo codice si trova nella funzione Promise.allSettled() e nella mappatura toast.getAnimations(). Poiché ho utilizzato più animazioni keyframe per il toast, per sapere con certezza che sono tutte terminate, ognuna deve essere richiesta da JavaScript e ognuna delle promesse finished osservate per il completamento. allSettled funziona per noi, risolvendosi come completata una volta che tutte le sue promesse sono state soddisfatte. L'utilizzo di await Promise.allSettled() significa che la riga di codice successiva può rimuovere con certezza l'elemento e presupporre che il toast abbia completato il suo ciclo di vita. Infine, la chiamata a resolve() soddisfa la promessa di Toast di alto livello, quindi gli sviluppatori possono eseguire la pulizia o altre operazioni una volta visualizzato il toast.

export default Toast

Infine, la funzione Toast viene esportata dal modulo per essere importata e utilizzata da altri script.

Utilizzo del componente Toast

L'utilizzo del toast o dell'esperienza di sviluppo del toast avviene importando la funzione Toast e chiamandola con una stringa di messaggio.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se lo sviluppatore vuole eseguire operazioni di pulizia o altro, dopo la visualizzazione della notifica può utilizzare async e await.

import Toast from './toast.js'

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

Conclusione

Ora che sai come ho fatto, come faresti tu?‽ 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community