Creazione di un componente di dialogo

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

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

Dimostrazione delle finestre di dialogo mega e mini nei temi chiaro e scuro.

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

Panoramica

L'elemento <dialog> è ideale per informazioni o azioni contestuali nella pagina. Valuta quando l'esperienza utente può trarre vantaggio da un'azione sulla stessa pagina anziché da un'azione su più pagine: magari perché il modulo è piccolo o l'unica azione richiesta all'utente è la conferma o l'annullamento.

L'elemento <dialog> è diventato stabile di recente in tutti i browser:

Browser Support

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Source

Ho notato che l'elemento mancava di alcune cose, quindi in questa sfida GUI aggiungo gli elementi dell'esperienza dello sviluppatore che mi aspetto: eventi aggiuntivi, chiusura rapida, animazioni personalizzate e un tipo mini e mega.

Segni e linee

Gli elementi essenziali di un elemento <dialog> sono modesti. L'elemento verrà nascosto automaticamente e ha stili integrati per sovrapporre i contenuti.

<dialog>
  …
</dialog>

Possiamo migliorare questo valore di riferimento.

Tradizionalmente, un elemento di dialogo condivide molte caratteristiche con una finestra modale e spesso i nomi sono intercambiabili. Mi sono presa la libertà di utilizzare l'elemento di dialogo per sia i piccoli popup di dialogo (mini) sia le finestre di dialogo a pagina intera (mega). Li ho chiamati mega e mini e ho adattato leggermente entrambe le finestre di dialogo per diversi casi d'uso. 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 delle finestre di dialogo sia mini che mega nei temi chiaro e scuro.

Non sempre, ma in genere gli elementi di dialogo vengono utilizzati per raccogliere alcune informazioni sull'interazione. I moduli all'interno degli elementi di dialogo sono progettati per essere utilizzati insieme. È consigliabile che un elemento del modulo contenga i contenuti della finestra di dialogo in modo che JavaScript possa accedere ai dati inseriti dall'utente. Inoltre, i pulsanti all'interno di un modulo che utilizza method="dialog" possono chiudere una finestra di dialogo senza JavaScript e passare 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 finestra di dialogo di grandi dimensioni ha tre elementi all'interno del modulo: <header>, <article>, e <footer>. Questi elementi fungono da contenitori semantici e da target di stile per la presentazione della finestra di dialogo. Il titolo dell'intestazione del modale e offre un pulsante di chiusura. L'articolo riguarda gli input e le informazioni del modulo. Il piè di pagina contiene un <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 riceverà il focus quando viene aperta la finestra di dialogo e ritengo che sia una best practice posizionarlo sul pulsante Annulla, non su quello di conferma. In questo modo, la conferma è deliberata e non accidentale.

Mini finestra di dialogo

La mini finestra di dialogo è molto simile alla mega finestra di dialogo, manca solo un elemento <header>. In questo modo, può essere più piccolo e 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 di dialogo fornisce una base solida per un elemento di visualizzazione completo che può raccogliere dati e interazioni degli utenti. Questi elementi essenziali possono creare interazioni molto interessanti ed efficaci nel tuo sito o nella tua app.

Accessibilità

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

Ripristino della messa a fuoco

Come abbiamo fatto manualmente in Creazione di un componente sidenav, è importante che l'apertura e la chiusura di un elemento mettano correttamente a fuoco i pulsanti di apertura e chiusura pertinenti. 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 lo ha aperto.

Con l'elemento dialog, questo è il comportamento predefinito integrato:

Purtroppo, se vuoi animare l'apertura e la chiusura della finestra di dialogo, questa funzionalità viene persa. Nella sezione JavaScript ripristinerò questa funzionalità.

Trapping focus

L'elemento di dialogo gestisce inert per te nel documento. Prima di inert, JavaScript veniva utilizzato per monitorare la perdita dello stato attivo di un elemento, a quel punto lo intercettava e lo ripristinava.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

Dopo inert, qualsiasi parte del documento può essere "congelata" in modo che non sia più un target di messa a fuoco o interattiva con un mouse. Anziché bloccare lo stato attivo, questo 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 per l'utente da impostare come predefinito, utilizza l'attributo autofocus. Come descritto in precedenza, ritengo che la best practice sia di inserirla nel pulsante Annulla e non in quello Conferma. In questo modo, la conferma è intenzionale e non accidentale.

Chiudere con il tasto Esc

È importante che sia facile chiudere questo elemento potenzialmente interruttivo. Fortunatamente, l'elemento di dialogo gestirà il tasto Esc per te, liberandoti dal carico di lavoro di orchestrazione.

Stili

Esiste un percorso semplice per applicare uno stile all'elemento di dialogo e un percorso difficile. Il percorso semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo e lavorando con i suoi limiti. Seguo il percorso più difficile per fornire animazioni personalizzate per l'apertura e la chiusura della finestra di dialogo, assumendo il controllo della proprietà display e altro ancora.

Stili con Open Props

Per accelerare i colori adattivi e la coerenza complessiva del design, ho introdotto senza vergogna la mia libreria di variabili CSS Open Props. Oltre alle variabili fornite senza costi, importo anche un file normalize e alcuni pulsanti, entrambi forniti da Open Props come importazioni facoltative. Queste importazioni mi aiutano a concentrarmi sulla personalizzazione della dialogo e della demo senza richiedere molti stili per supportarla e renderla accattivante.

Applicare uno stile all'elemento <dialog>

Proprietà di visualizzazione

Il comportamento predefinito di visualizzazione e nascondimento di un elemento di dialogo attiva/disattiva la proprietà display da block a none. Purtroppo, ciò significa che non può essere animato in entrata e in uscita, ma solo in entrata. Voglio animare sia l'entrata che l'uscita e il primo passo è impostare la mia proprietà display:

dialog {
  display: grid;
}

Modificando e quindi possedendo il valore della proprietà di visualizzazione, come mostrato nello snippet CSS precedente, è necessario gestire una notevole quantità di stili per facilitare la corretta esperienza utente. 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 può essere utilizzata quando non è aperta. In un secondo momento aggiungerò del codice JavaScript per gestire l'attributo inert nella finestra di dialogo, assicurandomi che anche gli utenti che utilizzano la tastiera e gli screen reader non possano raggiungere la finestra di dialogo nascosta.

Assegnare alla finestra di dialogo un tema cromatico adattivo

Finestra di dialogo Mega che mostra il tema chiaro e scuro, con i colori della superficie.

Anche se color-scheme attiva un tema cromatico adattivo fornito dal browser per le preferenze di sistema chiare e scure, volevo personalizzare l'elemento di dialogo in modo più approfondito. Open Props fornisce alcuni colori di superficie che si adattano automaticamente alle preferenze di sistema chiare e scure, in modo simile all'utilizzo di color-scheme. Questi sono ideali per creare livelli in un design e mi piace usare il colore per supportare visivamente l'aspetto delle superfici dei livelli. Il colore di sfondo è var(--surface-1); per posizionarlo sopra questo livello, utilizza 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 altri colori adattivi per gli elementi secondari, come l'intestazione e il piè di pagina. Li considero un extra per un elemento di dialogo, ma sono molto importanti per creare un design di dialogo accattivante e ben progettato.

Dimensioni adattabili delle finestre di dialogo

Per impostazione predefinita, la finestra di dialogo delega le dimensioni ai contenuti, il che è generalmente ottimo. Il mio obiettivo è limitare le dimensioni di 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 occuperà l'intero schermo di un dispositivo mobile e non sarà così ampia su uno schermo del computer da risultare difficile da leggere. Poi aggiungo un max-block-size in modo che la finestra di dialogo non superi l'altezza della pagina. Ciò significa anche che dovremo specificare dove si trova l'area scorrevole della finestra di dialogo, nel caso in cui si tratti di 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;
}

Nota come ho max-block-size due volte. Il primo utilizza 80vh, un'unità viewport fisica. Quello che voglio davvero è mantenere la finestra di dialogo all'interno del flusso relativo, per gli utenti internazionali, quindi utilizzo l'unità dvb logica, più recente e supportata solo parzialmente nella seconda dichiarazione per quando diventa più stabile.

Posizionamento della finestra di dialogo Mega

Per facilitare il posizionamento di un elemento di dialogo, è utile suddividerlo in due parti: lo sfondo a schermo intero e il contenitore di dialogo. Lo sfondo deve coprire tutto, fornendo un effetto di ombreggiatura per contribuire a indicare che la finestra di dialogo è in primo piano e che i contenuti dietro sono inaccessibili. Il contenitore della finestra di dialogo è libero di centrarsi su questo sfondo e assumere la forma richiesta dai suoi contenuti.

I seguenti stili fissano l'elemento della finestra di dialogo alla finestra, allungandolo fino 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);
}
Stili di finestre di dialogo mega per dispositivi mobili

Sulle finestre di visualizzazione piccole, lo stile di questo mega modale a pagina intera è leggermente diverso. Ho impostato il margine inferiore su 0, in modo che i contenuti della finestra di dialogo si trovino nella parte inferiore dell'area visibile. Con un paio di modifiche allo stile, posso trasformare la finestra di dialogo in un foglio delle azioni, 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 
  sia alla finestra di dialogo principale del computer sia a quella del dispositivo mobile quando è aperta.

Posizionamento della mini finestra di dialogo

Quando utilizzo un'area visibile più grande, ad esempio su un computer desktop, ho scelto di posizionare le mini finestre di dialogo sopra l'elemento che le ha chiamate. Per farlo, ho bisogno di JavaScript. Puoi trovare la tecnica che utilizzo qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza JavaScript, la mini finestra di dialogo viene visualizzata al centro dello schermo, proprio come la mega finestra di dialogo.

Dai risalto alle miniature

Infine, aggiungi un po' di stile alla finestra di dialogo in modo che sembri una superficie morbida molto sopra la pagina. La morbidezza si ottiene arrotondando gli angoli della finestra di dialogo. La profondità viene ottenuta con una delle proprietà ombra di Open Props realizzate con cura:

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

Personalizzare lo pseudo elemento di sfondo

Ho scelto di lavorare in modo molto leggero con lo sfondo, aggiungendo solo un effetto sfocato con backdrop-filter alla finestra di dialogo principale:

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

Source

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

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

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

Screenshot della finestra di dialogo principale sovrapposta a uno sfondo sfocato di avatar colorati.

Extra per lo styling

Chiamo questa sezione "extra" perché riguarda più la demo dell'elemento di dialogo che l'elemento di dialogo in generale.

Contenimento dello scorrimento

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

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

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Source

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

Ora, quando è aperta una finestra di dialogo di grandi dimensioni, il documento HTML contiene overflow: hidden.

Layout <form>

Oltre a essere un elemento molto importante per la raccolta delle informazioni sull'interazione dell'utente, lo utilizzo qui per definire gli elementi di intestazione, piè di pagina e articolo. Con questo layout intendo definire l'articolo secondario come un'area scorrevole. Raggiungo questo obiettivo con grid-template-rows. L'elemento articolo ha 1fr e il modulo stesso ha la stessa altezza massima dell'elemento finestra di dialogo. L'impostazione di questa altezza fissa e di questa dimensione fissa della riga consente all'elemento articolo di essere vincolato e di scorrere quando si verifica un overflow:

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

Screenshot degli strumenti di sviluppo che sovrappongono le informazioni sul layout a griglia alle righe.

Applicare uno stile alla finestra di dialogo <header>

Il ruolo di questo elemento è fornire un titolo per i contenuti della finestra di dialogo e offrire un pulsante di chiusura facile da trovare. Inoltre, è stato assegnato un colore di superficie per farlo apparire dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti portano a un contenitore flexbox, elementi allineati verticalmente e distanziati fino ai bordi, nonché a un po' di spaziatura interna e 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 le informazioni sul layout Flexbox all&#39;intestazione della finestra di dialogo.

Stilizzazione del pulsante di chiusura dell'intestazione

Poiché la demo utilizza i pulsanti Open Props, il pulsante di chiusura è personalizzato in un pulsante centrale con icona rotonda, come segue:

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 le informazioni su dimensioni e spaziatura interna per il pulsante di chiusura dell&#39;intestazione.

Applicare uno stile alla finestra di dialogo <article>

L'elemento articolo ha un ruolo speciale in questa finestra di dialogo: è uno spazio destinato allo scorrimento nel caso di una finestra di dialogo alta o lunga.

A questo scopo, l'elemento del modulo principale ha stabilito alcuni valori massimi per se stesso, che forniscono vincoli per l'elemento dell'articolo da raggiungere se diventa troppo alto. Imposta overflow-y: auto in modo che le barre di scorrimento vengano visualizzate solo quando necessario, contieni lo scorrimento al suo interno con overscroll-behavior: contain e il resto saranno 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 è quello di 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 viene aggiunto 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 sul layout flexbox all&#39;elemento del piè di pagina.

L'elemento menu viene utilizzato per contenere i pulsanti di azione per la finestra di dialogo. Utilizza un layout flexbox di wrapping con gap per fornire spazio tra i pulsanti. Gli elementi del menu hanno un padding, ad esempio un <ul>. Rimuovo anche questo stile 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 le informazioni di Flexbox agli elementi del menu del piè di pagina.

Animazione

Gli elementi della finestra di dialogo vengono spesso animati perché entrano ed escono dalla finestra. L'aggiunta di un movimento di supporto per l'entrata e l'uscita delle finestre di dialogo aiuta gli utenti a orientarsi nel flusso.

Normalmente, l'elemento di dialogo può essere animato solo in entrata, non in uscita. Questo perché il browser attiva/disattiva la proprietà display sull'elemento. In precedenza, la guida impostava la visualizzazione a griglia e non la impostava mai su Nessuno. In questo modo, puoi animare l'entrata e l'uscita.

Open Props include molte animazioni keyframe da utilizzare, il che rende l'orchestrazione facile e leggibile. Ecco gli obiettivi di animazione e l'approccio a più livelli che ho adottato:

  1. Movimento ridotto è la transizione predefinita, una semplice dissolvenza in entrata e in uscita dell'opacità.
  2. Se il movimento è accettabile, vengono aggiunte le animazioni di scorrimento e ridimensionamento.
  3. Il layout mobile adattabile per la finestra di dialogo grande è regolato per scorrere verso l'esterno.

Una transizione predefinita sicura e significativa

Anche se Open Props include i fotogrammi chiave per la dissolvenza in entrata e in uscita, preferisco questo approccio a più livelli delle transizioni come impostazione predefinita, con le animazioni dei fotogrammi chiave come potenziali upgrade. In precedenza abbiamo già definito lo stile della visibilità della finestra di dialogo con l'opacità, orchestrando 1 o 0 a seconda dell'attributo [open]. Per eseguire la transizione tra 0% e 100%, indica al browser la durata e il tipo di accelerazione che preferisci:

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

Aggiungere movimento alla transizione

Se l'utente accetta il movimento, sia la finestra di dialogo grande che quella piccola devono scorrere verso l'alto quando entrano e ridursi quando escono. Puoi ottenere questo risultato con la query multimediale prefers-reduced-motion e alcune proprietà aperte:

@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

Nella sezione precedente relativa allo stile, lo stile della finestra di dialogo mega è adattato per i dispositivi mobili in modo che assomigli di più a un foglio di azioni, come se un piccolo foglio di carta fosse scivolato verso l'alto dalla parte inferiore dello schermo e fosse ancora attaccato alla parte inferiore. L'animazione di uscita di ridimensionamento non si adatta bene a questo nuovo design e possiamo adattarla con un paio di media query e alcune proprietà aperte:

@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 diverse cose 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 derivano dal desiderio di chiudere facilmente la finestra di dialogo (facendo clic sullo sfondo), dall'animazione e da alcuni eventi aggiuntivi per una migliore tempistica di ricezione dei dati del modulo.

Aggiunta di chiusura rapida

Questa attività è semplice e un'ottima aggiunta a un elemento di dialogo che non viene animato. L'interazione viene ottenuta osservando i clic sull'elemento della finestra di dialogo e sfruttando il bubbling degli eventi per valutare su cosa è stato fatto clic e close() solo se si tratta dell'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 altro codice JavaScript per ottenere informazioni su come è stata chiusa la finestra di dialogo. Noterai che ho fornito anche stringhe vicine ogni volta che chiamo la funzione da vari pulsanti, per fornire al mio programma il contesto dell'interazione dell'utente.

Aggiunta di eventi di chiusura e chiusi

L'elemento di dialogo è dotato di un evento di chiusura: viene emesso immediatamente quando viene chiamata la funzione close() del dialogo. Poiché stiamo animando questo elemento, è utile avere eventi prima e dopo l'animazione, per consentire a una modifica di recuperare i dati o reimpostare il modulo della finestra di dialogo. Lo utilizzo qui per gestire l'aggiunta dell'attributo inert nella finestra di dialogo chiusa e nella demo li utilizzo per modificare l'elenco degli avatar se l'utente ha inviato una nuova immagine.

A questo scopo, crea due nuovi eventi chiamati closing e closed. Poi 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 consiste nell'attendere il completamento 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 componente Creazione di un messaggio toast, restituisce una promessa basata sul completamento delle promesse di animazione e transizione. Per questo motivo, dialogClose è una funzione asincrona; può quindi await la promessa restituita e procedere con sicurezza all'evento chiuso.

Aggiunta di eventi di apertura e aperti

Questi eventi non sono facili da aggiungere perché l'elemento di dialogo integrato non fornisce un evento di apertura come fa con la chiusura. Utilizzo un MutationObserver per fornire approfondimenti sulle modifiche agli attributi della finestra di dialogo. In questo osservatore, monitorerò le modifiche all'attributo open e gestirò gli eventi personalizzati di conseguenza.

In modo simile a come hai creato gli eventi di chiusura e chiusi, crea due nuovi eventi denominati opening e opened. Dove in precedenza ascoltavamo l'evento di chiusura della finestra di dialogo, questa volta utilizziamo un osservatore di mutazioni creato per monitorare 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'observer delle mutazioni viene chiamata quando gli attributi della finestra di dialogo vengono modificati, fornendo l'elenco delle modifiche come array. Itera le modifiche dell'attributo, cercando attributeName da aprire. Poi controlla se l'elemento ha l'attributo o meno: questo indica se la finestra di dialogo è stata aperta o meno. Se è stata aperta, 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, in modo simile all'evento di chiusura e chiuso, invia immediatamente l'evento di apertura, attendi il completamento delle animazioni e poi 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 stati dell'applicazione. Può essere utile per pulire gli eventi o i dati quando una finestra di dialogo viene rimossa.

Puoi farlo con un altro observer delle mutazioni. Questa volta, anziché osservare gli attributi di un elemento di dialogo, osserveremo gli elementi secondari dell'elemento body e monitoreremo la rimozione degli elementi 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 dell'observer delle mutazioni viene chiamato ogni volta che vengono aggiunti o rimossi elementi secondari dal corpo del documento. Le mutazioni specifiche monitorate riguardano removedNodes che hanno nodeName di un dialogo. Se una finestra di dialogo è stata rimossa, gli eventi di clic e chiusura vengono rimossi per liberare memoria e viene inviato l'evento di rimozione personalizzato.

Rimozione dell'attributo di caricamento

Per impedire la riproduzione dell'animazione di uscita della finestra di dialogo quando viene aggiunta alla pagina o al caricamento della pagina, è stato aggiunto un attributo di caricamento alla finestra di dialogo. Lo script seguente attende il completamento delle animazioni della finestra di dialogo, quindi rimuove l'attributo. Ora la finestra di dialogo può essere animata in entrata e in uscita e abbiamo nascosto efficacemente un'animazione altrimenti distraente.

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

Scopri di più sul problema relativo all'impedimento delle animazioni dei fotogrammi chiave durante il caricamento della pagina qui.

Tutti insieme

Ecco dialog.js nella sua interezza, ora che abbiamo spiegato 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 prevede di essere chiamata e di ricevere un elemento di dialogo a cui aggiungere questi nuovi eventi e funzionalità:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

In questo modo, le due finestre di dialogo vengono aggiornate con la chiusura leggera, correzioni del caricamento delle animazioni e altri eventi con cui lavorare.

In ascolto dei nuovi eventi personalizzati

Ogni elemento della finestra di dialogo aggiornato ora può rilevare cinque nuovi eventi, ad esempio:

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

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

MegaDialog.addEventListener('removed', dialogRemoved)

Ecco 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 che ho creato con l'elemento di dialogo, utilizzo l'evento chiuso e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. La tempistica è buona in quanto l'animazione di uscita della finestra di dialogo è stata completata e alcuni script animano il nuovo avatar. Grazie ai nuovi eventi, l'orchestrazione dell'esperienza utente può essere più fluida.

Notice dialog.returnValue: this contains the close string passed when the dialog close() event is called. È fondamentale nell'evento dialogClosed 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 in modo che quando la finestra di dialogo viene visualizzata di nuovo, sia vuota e pronta per un nuovo invio.

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

Risorse