Creazione di un componente della finestra di dialogo

Una panoramica di base su come creare mini e megamodali adattabili al colore, adattabili e accessibili con l'elemento <dialog>.

In questo post voglio condividere le mie idee su come creare mini e mega modali adattabili al colore, adattabili e accessibili con l'elemento <dialog>. Prova la demo e visualizza la fonte.

Dimostrazione dei mega e mini dialoghi nei loro temi chiaro e scuro.

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

Panoramica

L'elemento <dialog> è ottimo per azioni o informazioni contestuali in-page. Prendi in considerazione i casi in cui l'esperienza utente può trarre vantaggio da un'azione sulla stessa pagina anziché da un'azione su più pagine, ad esempio perché il modulo è di piccole dimensioni o l'unica azione richiesta all'utente è la conferma o l'annullamento.

Di recente, l'elemento <dialog> è diventato stabile su tutti i browser:

Supporto dei browser

  • 37
  • 79
  • 98
  • 15,4

Fonte

Ho notato che all'elemento mancavano alcune cose, quindi in questa Sfida GUI aggiungo gli elementi dell'esperienza sviluppatore che mi aspettavo: eventi aggiuntivi, eliminazione leggera, animazioni personalizzate, un tipo mini e mega.

Markup

Gli elementi essenziali di un elemento <dialog> sono modesti. L'elemento verrà automaticamente nascosto e includerà stili che si sovrappongono ai tuoi contenuti.

<dialog>
  …
</dialog>

Possiamo migliorare questo valore di riferimento.

Tradizionalmente, un elemento di dialogo condivide molto con un modale e spesso i nomi sono interscambiabili. Qui mi sono presa la libertà di utilizzare l'elemento di dialogo sia per i piccoli popup delle finestre di dialogo (mini) che per quelli a pagina intera (mega). Le ho denominate mega e mini, con entrambe le finestre di dialogo leggermente adattate a casi d'uso diversi. Ho aggiunto un attributo modal-mode per consentirti di specificare il tipo:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

Screenshot dei dialoghi mini e mega con tema chiaro e scuro.

Non sempre, ma in genere vengono utilizzati gli elementi delle finestre di dialogo per raccogliere alcune informazioni sulle interazioni. I moduli negli elementi delle finestre di dialogo vengono creati insieme. È buona norma inserire un elemento del modulo che aggreghi i contenuti delle finestre di dialogo in modo che JavaScript possa accedere ai dati inseriti dall'utente. Inoltre, i pulsanti all'interno di un modulo che utilizzano method="dialog" possono chiudere una finestra di dialogo senza JavaScript e trasmettere dati.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Finestra di dialogo Mega

Una mega finestra di dialogo contiene tre elementi all'interno del modulo: <header>, <article> e <footer>. Questi fungono da container semantici, nonché da target di stile per la presentazione della finestra di dialogo. L'intestazione assegna il titolo alla finestra modale e offre un pulsante Chiudi. L'articolo riguarda gli input e le informazioni nei moduli. Il piè di pagina contiene <menu> di pulsanti di azione.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    <header>
      <h3>Dialog title</h3>
      <button onclick="this.closest('dialog').close('close')"></button>
    </header>
    <article>...</article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

Il primo pulsante del menu ha autofocus e un gestore di eventi incorporato onclick. L'attributo autofocus sarà attivo all'apertura della finestra di dialogo e penso che la best practice sia impostarlo sul pulsante Annulla, non su quello di conferma. Ciò garantisce che la conferma sia intenzionale e non accidentale.

Mini finestra di dialogo

La mini finestra di dialogo è molto simile alla mega finestra di dialogo, è solo mancante un elemento <header>. In questo modo, è più piccolo e più in linea.

<dialog id="MiniDialog" modal-mode="mini">
  <form method="dialog">
    <article>
      <p>Are you sure you want to remove this user?</p>
    </article>
    <footer>
      <menu>
        <button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
        <button type="submit" value="confirm">Confirm</button>
      </menu>
    </footer>
  </form>
</dialog>

L'elemento della finestra di dialogo fornisce una solida base per un elemento dell'area visibile completa in grado di raccogliere dati e l'interazione dell'utente. Questi elementi essenziali possono essere determinanti per interazioni molto interessanti e potenti sul tuo sito o nella tua app.

Accessibilità

L'elemento di dialogo ha un'ottima accessibilità integrata. Invece di aggiungere queste funzionalità come faccio di solito, molte sono già presenti.

Ripristino dello stato attivo

Come abbiamo fatto manualmente in Creazione di un componente di navigazione laterale, è importante che l'apertura e la chiusura di un elemento mettano l'attenzione nei relativi pulsanti di apertura e chiusura. Quando si apre la barra di navigazione laterale, lo stato attivo viene impostato sul pulsante di chiusura. Quando viene premuto il pulsante di chiusura, lo stato attivo viene ripristinato sul pulsante che l'aveva aperto.

Nell'elemento finestra di dialogo, si tratta di un comportamento predefinito integrato:

Purtroppo, se vuoi animare la finestra di dialogo, questa funzionalità andrà persa. Nella sezione JavaScript ripristinerò questa funzionalità.

Trappola messa a fuoco

L'elemento della finestra di dialogo gestisce inert per te nel documento. Prima del giorno inert, si usava JavaScript per controllare l'elemento attivo che lasciava un elemento, dopodiché lo intercetta e lo rimette.

Supporto dei browser

  • 102
  • 102
  • 112
  • 15.5

Fonte

Dopo il giorno inert, qualsiasi parte del documento può essere "bloccata", nel senso che non sono più obiettivi o sono interattive con il mouse. Invece di intrappolare l'elemento attivo, viene indirizzato all'unica parte interattiva del documento.

Aprire e mettere a fuoco automaticamente un elemento

Per impostazione predefinita, l'elemento della finestra di dialogo assegna lo stato attivo al primo elemento attivabile nel markup della finestra di dialogo. Se questo non è l'elemento migliore che l'utente deve utilizzare per impostazione predefinita, utilizza l'attributo autofocus. Come descritto in precedenza, la best practice sia sul pulsante Annulla e non su quello di conferma. Ciò garantisce che la conferma sia intenzionale e non accidentale.

Chiusura con il tasto Esc

È importante semplificare la chiusura di questo elemento potenzialmente invasivo. Fortunatamente, l'elemento finestra di dialogo gestirà il tasto ESC per te, liberandoti dal carico di lavoro dell'orchestrazione.

Stili

È disponibile un percorso facile per applicare uno stile all'elemento della finestra di dialogo e un percorso difficile. Per farlo, non devi modificare la proprietà di visualizzazione della finestra di dialogo e non rispettare le sue limitazioni. Procedo nel percorso difficile per fornire animazioni personalizzate per l'apertura e la chiusura della finestra di dialogo, prendendo il controllo della proprietà display e altro ancora.

Stilizzare con oggetti aperti

Per accelerare i colori adattivi e la coerenza generale del design, ho aggiunto senza vergogna la mia libreria di variabili CSS Open Props. Oltre alle variabili fornite senza costi, importerò anche un file di normalizzazione e alcuni pulsanti, entrambi forniti da Open Props come importazioni facoltative. Queste importazioni mi aiutano a concentrarmi sulla personalizzazione della finestra di dialogo e della demo senza bisogno di molti stili per supportarla e renderla corretta.

Applicare uno stile all'elemento <dialog>

Proprietà della proprietà display

Il comportamento predefinito per mostrare e nascondere un elemento di finestra di dialogo attiva/disattiva la proprietà di visualizzazione da block a none. Purtroppo, non può essere animata in entrata e in uscita, ma solo in entrata. Vorrei animare sia in entrata che in uscita e il primo passaggio consiste nell'impostare la mia proprietà display:

dialog {
  display: grid;
}

Modificando e di conseguenza proprietario del valore della proprietà display, come mostrato nello snippet CSS riportato sopra, occorre gestire una notevole quantità di stili per facilitare l'esperienza utente corretta. Innanzitutto, lo stato predefinito di una finestra di dialogo è chiuso. Puoi rappresentare visivamente questo stato e impedire alla finestra di dialogo di ricevere interazioni con i seguenti stili:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

Ora la finestra di dialogo è invisibile e non è possibile interagire quando non è aperta. In seguito aggiungerò codice JavaScript per gestire l'attributo inert nella finestra di dialogo, assicurando che anche gli utenti di tastiera e screen reader non riescano ad accedere alla finestra di dialogo nascosta.

Impostazione di un tema cromatico adattivo ai dialoghi

Mega finestra di dialogo che mostra il tema chiaro e scuro e i colori della superficie.

Anche se color-scheme attiva per il tuo documento un tema a colori adattivo fornito dal browser in base alle preferenze di sistema chiaro e scuro, volevo personalizzare ulteriormente l'elemento della finestra di dialogo. Open Props fornisce alcuni colori di superficie che si adattano automaticamente alle preferenze di sistema chiaro e scuro, in modo simile all'utilizzo di color-scheme. Sono perfetti per creare livelli in un design e adoro usare i colori per dare un aspetto visivo alle superfici dei livelli. Il colore di sfondo è var(--surface-1). Per posizionarti sopra questo livello, usa var(--surface-2):

dialog {
  …
  background: var(--surface-2);
  color: var(--text-1);
}

@media (prefers-color-scheme: dark) {
  dialog {
    border-block-start: var(--border-size-1) solid var(--surface-3);
  }
}

In seguito verranno aggiunti colori più adattivi per gli elementi secondari, come l'intestazione e il piè di pagina. Le considero un elemento aggiuntivo per i dialoghi, ma sono molto importanti per la progettazione di dialoghi avvincenti e ben progettata.

Dimensioni delle finestre di dialogo adattabili

Per impostazione predefinita, la dimensione della finestra di dialogo viene delegata ai contenuti, il che in genere è ottimale. Il mio obiettivo è quello di limitare l'elemento max-inline-size a una dimensione leggibile (--size-content-3 = 60ch) o al 90% della larghezza dell'area visibile. In questo modo la finestra di dialogo non si estenderà a livello perimetrale sui dispositivi mobili e non sarà così larga sullo schermo del desktop da risultare difficile da leggere. Quindi aggiungo un elemento max-block-size in modo che la finestra di dialogo non superi l'altezza della pagina. Ciò significa anche che dovremo specificare la posizione dell'area scorrevole della finestra di dialogo, nel caso in cui sia un elemento di dialogo alto.

dialog {
  …
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

Hai visto come ho max-block-size due volte? La prima utilizza 80vh, un'unità viewport fisica. Quello che voglio davvero è mantenere la finestra di dialogo entro un flusso relativo per gli utenti internazionali, quindi uso l'unità dvb logica, più recente e solo parzialmente supportata nella seconda dichiarazione per quando diventa più stabile.

Posizionamento superfluo delle finestre di dialogo

Per facilitare il posizionamento di un elemento della finestra di dialogo, è consigliabile analizzarne le due parti: lo sfondo a schermo intero e il contenitore della finestra di dialogo. Lo sfondo deve coprire tutto, fornendo un effetto tonalità che fa capire che la finestra di dialogo è in primo piano e che il contenuto dietro è inaccessibile. Il contenitore della finestra di dialogo può essere centrato su questo sfondo e assumere la forma richiesta dai contenuti.

I seguenti stili fissano l'elemento della finestra di dialogo alla finestra, estendendolo a ogni angolo e utilizzano margin: auto per centrare i contenuti:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Mega stili di finestre di dialogo per dispositivi mobili

Nelle aree visibili di piccole dimensioni, lo stile di questo mega modale a pagina intera è leggermente diverso. Ho impostato il margine inferiore su 0. In questo modo i contenuti della finestra di dialogo vengono visualizzati nella parte inferiore dell'area visibile. Con un paio di modifiche dello stile, posso trasformare la finestra di dialogo in un foglio di lavoro, più vicino ai pollici dell'utente:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Screenshot di devtools che sovrappone la spaziatura dei margini sulla mega finestra di dialogo desktop e mobile all&#39;apertura.

Posizionamento mini dialoghi

Quando utilizzo un'area visibile più grande, come su un computer, ho scelto di posizionare le mini finestre di dialogo sull'elemento che le chiama. Per farlo, ho bisogno di JavaScript. Puoi trovare la tecnica che uso qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza JavaScript, la mini finestra di dialogo appare al centro dello schermo, proprio come in una mega finestra di dialogo.

Metti in risalto

Infine, aggiungi un tocco di originalità alla finestra di dialogo in modo che appaia come una superficie morbida molto sopra la pagina. La morbidezza si ottiene arrotondando gli angoli del dialogo. La profondità si ottiene con uno degli oggetti di scena attentamente realizzati da Open Props:

dialog {
  …
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

Personalizzazione dello pseudo elemento di sfondo

Ho scelto di lavorare in modo molto leggero con lo sfondo, aggiungendo solo un effetto di sfocatura con backdrop-filter al mega dialogo:

Supporto dei browser

  • 76
  • 17
  • 103
  • 9

Fonte

dialog[modal-mode="mega"]::backdrop {
  backdrop-filter: blur(25px);
}

Ho anche scelto di impostare una transizione su backdrop-filter, nella speranza che i browser permettano la transizione dell'elemento di sfondo in futuro:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Screenshot della mega finestra di dialogo che si sovrappone a uno sfondo sfocato di avatar colorati.

Extra per lo stile

Chiama questa sezione "extra" perché ha a che fare più con la demo dell'elemento di dialogo che con la demo dell'elemento di dialogo in generale.

Contenimento dello scorrimento

Quando viene visualizzata la finestra di dialogo, l'utente può comunque scorrere la pagina alle spalle, cosa che non voglio:

Normalmente, overscroll-behavior sarebbe la mia soluzione solita, ma secondo le specifiche, non ha alcun effetto sulla finestra di dialogo perché non è una porta di scorrimento, ovvero non è uno scorrimento, quindi non c'è nulla da impedire. Potrei utilizzare JavaScript per controllare i nuovi eventi di questa guida, come "chiuso" e "aperto", e attivare/disattivare overflow: hidden sul documento oppure potrei aspettare che :has() sia stabile in tutti i browser:

Supporto dei browser

  • 105
  • 105
  • 121
  • 15,4

Fonte

html:has(dialog[open][modal-mode="mega"]) {
  overflow: hidden;
}

Ora, quando una mega finestra di dialogo è aperta, il documento HTML ha overflow: hidden.

Il layout <form>

Oltre a essere un elemento molto importante per raccogliere le informazioni sull'interazione da parte dell'utente, lo uso per impaginare gli elementi di intestazione, piè di pagina e articoli. Con questo layout intendiamo spiegare il bambino dell'articolo come un'area scorrevole. A questo scopo, utilizza grid-template-rows. All'elemento Article viene assegnato 1fr e il modulo stesso ha la stessa altezza massima dell'elemento della finestra di dialogo. L'impostazione di un'altezza fissa e di dimensioni della riga stabili consente di limitare l'elemento dell'articolo e di scorrere quando supera il limite:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Screenshot di devtools che sovrappone le informazioni sul layout della griglia sulle righe.

Stile della finestra di dialogo <header>

Il ruolo di questo elemento è fornire un titolo per i contenuti delle finestre di dialogo e offrire un pulsante di chiusura facile da trovare. Inoltre, gli viene assegnato un colore di superficie per far sembrare che si trovi dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti portano alla creazione di un contenitore Flexbox, di elementi allineati verticalmente distanziati ai bordi e di una spaziatura interna e di spazi vuoti per dare spazio al titolo e ai pulsanti di chiusura:

dialog > form > header {
  display: flex;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  background: var(--surface-2);
  padding-block: var(--size-3);
  padding-inline: var(--size-5);
}

@media (prefers-color-scheme: dark) {
  dialog > form > header {
    background: var(--surface-1);
  }
}

Screenshot di Chrome DevTools che sovrappone informazioni sul layout flexbox all&#39;intestazione della finestra di dialogo.

Stile del pulsante di chiusura dell'intestazione

Poiché nella demo vengono usati i pulsanti Open Props, il pulsante di chiusura è personalizzato con un'icona rotonda con un'icona rotondo come questo:

dialog > form > header > button {
  border-radius: var(--radius-round);
  padding: .75ch;
  aspect-ratio: 1;
  flex-shrink: 0;
  place-items: center;
  stroke: currentColor;
  stroke-width: 3px;
}

Screenshot di Chrome DevTools che sovrappone informazioni su dimensioni e spaziatura interna per il pulsante di chiusura dell&#39;intestazione.

Stile della finestra di dialogo <article>

L'elemento articolo ha un ruolo speciale in questa finestra di dialogo: è uno spazio che deve essere fatto scorrere in caso di finestre di dialogo alte o lunghe.

A questo scopo, l'elemento principale del modulo ha fissato dei valori massimi che possono prevedere dei limiti da raggiungere se l'elemento articolo diventa troppo alto. Imposta overflow-y: auto in modo che le barre di scorrimento vengano visualizzate solo quando necessario, contiene lo scorrimento al suo interno con overscroll-behavior: contain e il resto sarà stili di presentazione personalizzati:

dialog > form > article {
  overflow-y: auto; 
  max-block-size: 100%; /* safari */
  overscroll-behavior-y: contain;
  display: grid;
  justify-items: flex-start;
  gap: var(--size-3);
  box-shadow: var(--shadow-2);
  z-index: var(--layer-1);
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: light) {
  dialog > form > article {
    background: var(--surface-1);
  }
}

Il ruolo del piè di pagina è contenere i menu dei pulsanti di azione. Flexbox viene utilizzato per allineare i contenuti alla fine dell'asse in linea del piè di pagina, quindi aggiungere un po' di spazio per dare spazio ai pulsanti.

dialog > form > footer {
  background: var(--surface-2);
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  justify-content: space-between;
  align-items: flex-start;
  padding-inline: var(--size-5);
  padding-block: var(--size-3);
}

@media (prefers-color-scheme: dark) {
  dialog > form > footer {
    background: var(--surface-1);
  }
}

Screenshot di Chrome DevTools che sovrappone le informazioni di layout flexbox all&#39;elemento piè di pagina.

L'elemento menu viene utilizzato per contenere i pulsanti di azione per la finestra di dialogo. Utilizza un layout flexbox a capo con gap per lasciare spazio tra i pulsanti. Gli elementi di menu presentano una spaziatura interna, ad esempio <ul>. Lo rimuovo anche perché non mi serve.

dialog > form > footer > menu {
  display: flex;
  flex-wrap: wrap;
  gap: var(--size-3);
  padding-inline-start: 0;
}

dialog > form > footer > menu:only-child {
  margin-inline-start: auto;
}

Screenshot di Chrome DevTools che sovrappone informazioni relative a flexbox agli elementi del menu a piè di pagina.

Animazione

Gli elementi della finestra di dialogo sono spesso animati perché entrano ed escono dalla finestra. Aggiungere alle finestre di dialogo un movimento di supporto per l'ingresso e l'uscita aiuta gli utenti a orientarsi nel flusso.

Normalmente l'elemento della finestra di dialogo può essere animato solo in entrata, non in uscita. Questo perché il browser attiva/disattiva la proprietà display dell'elemento. In precedenza, la guida impostava la visualizzazione su griglia e non la impostava mai su "nessuna". In questo modo, è possibile animarsi dentro e fuori.

Open Props è dotato di molte animazioni per i fotogrammi chiave che rendono l'orchestrazione facile e leggibile. Ecco gli obiettivi dell'animazione e l'approccio a livelli che ho adottato:

  1. La modalità Movimento ridotto è la transizione predefinita, con una semplice dissolvenza in entrata e in uscita.
  2. Se il movimento è accettabile, vengono aggiunte animazioni di scorrimento e scala.
  3. Il layout mobile adattabile della mega finestra di dialogo è regolato in modo da scorrere.

Una transizione predefinita sicura e significativa

Anche se Open Props include fotogrammi chiave per la dissolvenza in entrata e in uscita, preferisco questo approccio a livelli delle transizioni come impostazione predefinita con animazioni con fotogrammi chiave come potenziali upgrade. In precedenza abbiamo già assegnato uno stile alla visibilità della finestra di dialogo con opacità, orchestrando 1 o 0 in base all'attributo [open]. Per eseguire la transizione tra 0% e 100%, comunica al browser la durata e il tipo di easing:

dialog {
  transition: opacity .5s var(--ease-3);
}

Aggiungere movimento alla transizione

Se l'utente è d'accordo con il movimento, sia la mega che la mini finestra di dialogo devono scorrere verso l'alto come punto di ingresso e fare lo scale out come uscita. Puoi ottenere questo risultato con la query multimediale prefers-reduced-motion e alcuni elementi aperti:

@media (prefers-reduced-motion: no-preference) {
  dialog {
    animation: var(--animation-scale-down) forwards;
    animation-timing-function: var(--ease-squish-3);
  }

  dialog[open] {
    animation: var(--animation-slide-in-up) forwards;
  }
}

Adattare l'animazione di uscita per i dispositivi mobili

In precedenza, nella sezione Stili, lo stile dei mega dialoghi è stato adattato ai dispositivi mobili per essere più simile a un foglio di azione, come se un foglio di carta scivolasse verso l'alto dalla parte inferiore dello schermo e fosse ancora attaccato alla parte inferiore. L'animazione di uscita con scale out non si adatta bene a questo nuovo design e possiamo adattarla con un paio di query multimediali e alcuni componenti aperti:

@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    animation: var(--animation-slide-out-down) forwards;
    animation-timing-function: var(--ease-squish-2);
  }
}

JavaScript

Ci sono diversi elementi da aggiungere con JavaScript:

// dialog.js
export default async function (dialog) {
  // add light dismiss
  // add closing and closed events
  // add opening and opened events
  // add removed event
  // removing loading attribute
}

Queste aggiunte nascono dal desiderio di ignorare in modo leggero (facendo clic sullo sfondo della finestra di dialogo), dall'animazione e da alcuni eventi aggiuntivi che consentono di velocizzare il recupero dei dati del modulo.

Aggiunta della funzionalità per spegnere la luce

Questa attività è semplice ed è un'ottima aggiunta a un elemento di dialogo che non viene animato. L'interazione si ottiene osservando i clic sull'elemento della finestra di dialogo e sfruttando la bubbling dell'evento per valutare l'elemento su cui è stato fatto clic. close() solo se è l'elemento più in alto:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Avviso dialog.close('dismiss'). L'evento viene chiamato e viene fornita una stringa. Questa stringa può essere recuperata da un altro codice JavaScript per ottenere informazioni su come è stata chiusa la finestra di dialogo. Troverai anche che ho inserito stringhe di chiusura ogni volta che chiamo la funzione da vari pulsanti, per fornire contesto alla mia applicazione in merito all'interazione dell'utente.

Aggiunta di eventi di chiusura e di chiusura

L'elemento di dialogo include un evento di chiusura: viene emesso subito quando viene richiamata la funzione close() della finestra di dialogo. Poiché stiamo animando questo elemento, è utile avere eventi prima e dopo l'animazione per una modifica che consenta di recuperare i dati o reimpostare la finestra di dialogo. Lo utilizzo qui per gestire l'aggiunta dell'attributo inert nella finestra di dialogo chiusa e nella demo le uso per modificare l'elenco di avatar se l'utente ha inviato una nuova immagine.

Per ottenere questo risultato, crea due nuovi eventi denominati closing e closed. Quindi ascolta l'evento di chiusura integrato nella finestra di dialogo. Da qui, imposta la finestra di dialogo su inert e invia l'evento closing. L'attività successiva è attendere il completamento dell'esecuzione delle animazioni e delle transizioni nella finestra di dialogo, quindi inviare l'evento closed.

const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')

export default async function (dialog) {
  …
  dialog.addEventListener('close', dialogClose)
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

La funzione animationsComplete, utilizzata anche nel campo Creazione di un componente toast, restituisce una promessa basata sul completamento dell'animazione e delle promesse di transizione. Questo è il motivo per cui dialogClose è una funzione asinc; può await restituire la promessa e andare avanti con sicurezza verso l'evento chiuso.

Aggiunta di eventi di apertura e aperti

Questi eventi non sono così facili da aggiungere perché l'elemento di dialogo integrato non fornisce un evento aperto come accade con la chiusura. Utilizzo un MutationObserver per fornire insight sull'evoluzione degli attributi della finestra di dialogo. In questo osservatore, guarderò le modifiche all'attributo open e gestirò di conseguenza gli eventi personalizzati.

Analogamente a come abbiamo avviato gli eventi di chiusura e di chiusura, crea due nuovi eventi chiamati opening e opened. In precedenza, mentre abbiamo ascoltato l'evento di chiusura della finestra di dialogo, questa volta utilizza un osservatore delle mutazioni creato per esaminare gli attributi della finestra di dialogo.

…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')

export default async function (dialog) {
  …
  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })
}

const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

La funzione di callback dell'osservatore delle mutazioni viene chiamata quando gli attributi della finestra di dialogo vengono modificati, fornendo l'elenco delle modifiche sotto forma di array. Esegui l'iterazione sulle modifiche all'attributo cercando che il valore attributeName sia aperto. Dopodiché controlla se l'elemento ha l'attributo o meno: in questo modo puoi capire se la finestra di dialogo è diventata aperta o meno. Se è stato aperto, rimuovi l'attributo inert, imposta lo stato attivo su un elemento che richiede autofocus o sul primo elemento button trovato nella finestra di dialogo. Infine, come per l'evento di chiusura e di chiusura, invia subito l'evento di apertura, attendi il completamento delle animazioni, quindi invia l'evento aperto.

Aggiungere un evento rimosso

Nelle applicazioni a pagina singola, le finestre di dialogo vengono spesso aggiunte e rimosse in base alle route o ad altre esigenze e stato dell'applicazione. Può essere utile ripulire eventi o dati quando rimuovi una finestra di dialogo.

Puoi ottenere questo risultato con un altro osservatore delle mutazioni. Questa volta, invece di osservare gli attributi di un elemento di finestra di dialogo, osserveremo gli elementi secondari dell'elemento body e verificheremo la rimozione degli elementi delle finestre di dialogo.

…
const dialogRemovedEvent = new Event('removed')

export default async function (dialog) {
  …
  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })
}

const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

Il callback "osservatore delle mutazioni" viene chiamato ogni volta che vengono aggiunti o rimossi bambini dal corpo del documento. Le mutazioni specifiche che vengono guardate si riferiscono a removedNodes che presentano i nodeName di una finestra di dialogo. Se una finestra di dialogo è stata rimossa, gli eventi di clic e chiusura vengono rimossi per liberare memoria e viene inviato l'evento rimosso personalizzato.

Rimozione dell'attributo di caricamento

Per impedire che l'animazione della finestra di dialogo riproduca l'animazione di uscita quando è stata aggiunta alla pagina o al caricamento della pagina, alla finestra di dialogo è stato aggiunto un attributo di caricamento. Lo script seguente attende che l'esecuzione delle animazioni delle finestre di dialogo termini, quindi rimuove l'attributo. Ora la finestra di dialogo può animarsi liberamente e abbiamo effettivamente nascosto un'animazione che altrimenti distrae.

export default async function (dialog) {
  …
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Scopri di più sul problema di impedire le animazioni dei fotogrammi chiave al caricamento della pagina.

Tutti insieme

Ecco dialog.js nella sua interezza, ora che abbiamo illustrato ogni sezione singolarmente:

// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent  = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent  = new Event('opened')
const dialogRemovedEvent = new Event('removed')

// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(async mutation => {
    if (mutation.attributeName === 'open') {
      const dialog = mutation.target

      const isOpen = dialog.hasAttribute('open')
      if (!isOpen) return

      dialog.removeAttribute('inert')

      // set focus
      const focusTarget = dialog.querySelector('[autofocus]')
      focusTarget
        ? focusTarget.focus()
        : dialog.querySelector('button').focus()

      dialog.dispatchEvent(dialogOpeningEvent)
      await animationsComplete(dialog)
      dialog.dispatchEvent(dialogOpenedEvent)
    }
  })
})

// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
  mutations.forEach(mutation => {
    mutation.removedNodes.forEach(removedNode => {
      if (removedNode.nodeName === 'DIALOG') {
        removedNode.removeEventListener('click', lightDismiss)
        removedNode.removeEventListener('close', dialogClose)
        removedNode.dispatchEvent(dialogRemovedEvent)
      }
    })
  })
})

// wait for all dialog animations to complete their promises
const animationsComplete = element =>
  Promise.allSettled(
    element.getAnimations().map(animation => 
      animation.finished))

// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

const dialogClose = async ({target:dialog}) => {
  dialog.setAttribute('inert', '')
  dialog.dispatchEvent(dialogClosingEvent)

  await animationsComplete(dialog)

  dialog.dispatchEvent(dialogClosedEvent)
}

// page load dialogs setup
export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
  dialog.addEventListener('close', dialogClose)

  dialogAttrObserver.observe(dialog, { 
    attributes: true,
  })

  dialogDeleteObserver.observe(document.body, {
    attributes: false,
    subtree: false,
    childList: true,
  })

  // remove loading attribute
  // prevent page load @keyframes playing
  await animationsComplete(dialog)
  dialog.removeAttribute('loading')
}

Utilizzo del modulo dialog.js

La funzione esportata dal modulo dovrebbe essere chiamata e passato un elemento di finestra di dialogo che vuole aggiungere i nuovi eventi e le seguenti funzionalità:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

In questo modo, viene eseguito l'upgrade delle due finestre di dialogo con chiusura leggera, correzioni per il caricamento dell'animazione e altri eventi con cui lavorare.

Ascoltare i nuovi eventi personalizzati

Ogni elemento della finestra di dialogo aggiornato può ora rimanere in ascolto di cinque nuovi eventi, come questo:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

Di seguito sono riportati due esempi di gestione di questi eventi:

const dialogOpening = ({target:dialog}) => {
  console.log('Dialog opening', dialog)
}

const dialogClosed = ({target:dialog}) => {
  console.log('Dialog closed', dialog)
  console.info('Dialog user action:', dialog.returnValue)

  if (dialog.returnValue === 'confirm') {
    // do stuff with the form values
    const dialogFormData = new FormData(dialog.querySelector('form'))
    console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))

    // then reset the form
    dialog.querySelector('form')?.reset()
  }
}

Nella demo creata con l'elemento finestra di dialogo, uso questo evento chiuso e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. Il tempismo è corretto perché la finestra di dialogo ha completato l'animazione di uscita e alcuni script si animano nel nuovo avatar. Grazie ai nuovi eventi, l'orchestrazione dell'esperienza utente può essere più fluida.

Avviso dialog.returnValue: contiene la stringa di chiusura passata quando viene richiamato l'evento close() della finestra di dialogo. Nell'evento dialogClosed è fondamentale sapere se la finestra di dialogo è stata chiusa, annullata o confermata. Se viene confermato, lo script recupera i valori del modulo e lo reimposta. Il ripristino è utile perché, quando viene mostrata di nuovo, la finestra di dialogo è vuota e pronta per un nuovo invio.

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

Risorse