Creazione di un componente toast

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

In questo post vorrei condividere riflessione su come creare un componente di un annuncio toast. Prova la demo.

Demo

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

Panoramica

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

Interazioni

Gli avvisi toast sono diversi dalle notifiche, dagli avvisi e dai messaggi perché non sono interattivi e non sono pensati per essere ignorati o mantenuti. 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 di altre strategie di notifica.

Markup

L'elemento <output> è una buona scelta per il toast perché viene annunciato agli screen reader. Il codice HTML corretto ci offre una base sicura per il miglioramento con JavaScript e CSS e ci sarà molto codice JavaScript.

Un toast

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

Può essere più inclusivo aggiungendo role="status". Viene fornito un riserva se il browser non assegna agli elementi <output> il ruolo implicito in base alla specifica.

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

Un contenitore di toast

È possibile mostrare più di un toast alla volta. Per orchestrare più toast, viene usato un container. 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 in inset-block-end dell'area visibile e, se vengono aggiunti altri toast, vengono impilati dal bordo dello schermo.

Contenitore GUI

Il contenitore dei toast fa tutto il lavoro di layout per presentare i toast. È fixed per l'area visibile e utilizza la proprietà logica inset per specificare a quali bordi fissare, più 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 della casella DevTools e la spaziatura interna sovrapposta 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 vengono centrati come un gruppo con justify-content e centrati individualmente con justify-items. Metti un po' di gap in modo che i toast non vengano toccati.

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

Screenshot con l&#39;overlay della griglia CSS sul gruppo della finestra popup, questa volta
evidenziando lo spazio e le lacune tra gli elementi secondari dell&#39;avviso.

Notifica toast con GUI

Un singolo toast ha un po' di padding, alcuni angoli più morbidi con border-radius e una funzione min() che aiuta le dimensioni di dispositivi mobili e desktop. Le dimensioni adattabili nel seguente CSS impediscono ai toast di crescere più ampie del 90% dell'area visibile o di 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 la spaziatura interna e il raggio del bordo mostrati.

Stili

Dopo aver impostato layout e posizionamento, aggiungi CSS che facilita l'adattamento alle impostazioni e alle interazioni dell'utente.

Contenitore toast

I toast non sono interattivi. Toccarli o scorrerli non funziona, ma al momento consumano eventi di puntatore. Impedisci ai toast di sottrarre clic con il seguente CSS.

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

Notifica toast con GUI

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

.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

Quando entra sullo schermo, dovrebbe apparire un nuovo avviso popup con un'animazione. Per adeguare il movimento ridotto si esegue l'impostazione dei valori translate su 0 per impostazione predefinita, ma l'aggiornamento del valore di movimento a una lunghezza in una query multimediale per le preferenze di movimento . Tutti hanno un po' di animazione, ma solo alcuni utenti hanno il toast percorrere una certa distanza.

Questi sono i fotogrammi chiave utilizzati per l'animazione del toast. CSS controllerà l'ingresso, l'attesa e l'uscita della notifica toast, il 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 l'HTML accessibile tramite screen reader pronti, è necessario JavaScript per orchestrare la creazione, l'aggiunta e l'eliminazione dei toast in base agli eventi dell'utente. L'esperienza degli sviluppatori con il componente toast dovrebbe essere minima e facile da iniziare, come questa:

import Toast from './toast.js'

Toast('My first toast')

Creazione del gruppo di toast e dei toast in corso...

Quando il modulo toast viene caricato da JavaScript, deve creare un contenitore toast e aggiungerlo alla pagina. Ho scelto di aggiungere l'elemento prima del giorno body, in modo da rendere improbabili i problemi di sovrapposizione di z-index, in quanto il container si trova sopra il container per 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 annunci toast tra i tag head e body.

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

const Toaster = init()

La creazione di elementi HTML toast viene eseguita con la funzione createToast(). La funzione richiede del testo per il toast, crea un elemento <output>, lo decora 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
}

Gestione di uno o più toast

Ora JavaScript aggiunge un container al documento per contenere i toast ed è pronto per aggiungere gli toast creati. La funzione addToast() orchestra la gestione di uno o più toast. Prima controlla il numero dei toast e controlla se il movimento è buono, poi usa queste informazioni per aggiungere il toast o creare un'animazione fantastica in modo che gli altri toast facciano 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 aggiunge il primo avviso popup, Toaster.appendChild(toast) aggiunge un avviso popup alla pagina che attiva le animazioni CSS: anima in entrata, attendi 3s, animazione fuori. flipToast() viene chiamato quando sono presenti dei toast esistenti che utilizza una tecnica chiamata FLIP di Paul Lewis. L'idea è calcolare la differenza nelle posizioni del container prima e dopo l'aggiunta del nuovo toast. È come se segnare dove si trova il tostapane, dove andrà e poi creare un'animazione dal punto in cui si trovava a dove si trova.

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',
  })
}

Il sollevamento del layout viene eseguito tramite la griglia CSS. Quando viene aggiunto un nuovo toast, la griglia lo inserisce all'inizio e lo spazia con gli altri. Nel frattempo, viene utilizzata un'animazione web per animare il contenitore dalla posizione precedente.

Inserimento di tutto il codice JavaScript

Quando viene chiamato Toast('my first toast'), viene creato un toast, aggiunto alla pagina (magari anche il contenitore è animato per inserire il nuovo toast), viene restituita una promessa e viene guardato il toast creato per completare l'animazione CSS (le tre animazioni di fotogrammi chiave) 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() 
  })
}

Ho sentito che la parte confusa di questo codice è nella funzione Promise.allSettled() e nella mappatura toast.getAnimations(). Poiché ho utilizzato più animazioni con fotogrammi chiave per il toast, per avere la certezza che siano stati tutti completati è necessario richiedere a JavaScript e ciascuna promessa finished per il completamento. allSettled funziona per noi, risolvendosi come completo una volta che tutte le promesse sono state soddisfatte. L'utilizzo di await Promise.allSettled() significa che la riga di codice successiva può rimuovere con sicurezza l'elemento e presumere che il toast abbia completato il suo ciclo di vita. Infine, la chiamata a resolve() soddisfa la promessa di Toast di alto livello in modo che gli sviluppatori possano eseguire la pulizia o eseguire altre operazioni dopo la visualizzazione del messaggio toast.

export default Toast

Infine, la funzione Toast viene esportata dal modulo, per l'importazione e l'utilizzo di altri script.

Utilizzo del componente Toast

Per utilizzare il toast, o l'esperienza di sviluppo del toast, puoi importare la funzione Toast e chiamarla con una stringa di messaggio.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se lo sviluppatore vuole eseguire un lavoro di pulizia o altro, dopo la visualizzazione del toast, può utilizzare il comando asincrono 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 ci sono riuscito, come faresti? 🙂

Diversifica i nostri approcci e scopriamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e lo aggiungerò alla sezione Remix della community di seguito.

Remix della community