Creazione di un componente della finestra di dialogo

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

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

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

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

Panoramica

L'elemento <dialog> è ottimo per le informazioni contestuali o le azioni in-page. Valuta se l'esperienza utente può trarre vantaggio da un'azione nella stessa pagina anziché da un'azione su più pagine: ad esempio, perché il modulo è piccolo o perché l'unica azione richiesta all'utente è confermare o annullare.

L'elemento <dialog> è recentemente diventato stabile nei vari browser:

Supporto dei browser

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

Origine

Ho notato che nell'elemento mancavano alcuni elementi, quindi in questa sfida sulla GUI ho aggiunto gli elementi dell'esperienza dello sviluppatore che mi aspetto: eventi aggiuntivi, dismiss light, animazioni personalizzate e un tipo mini e mega.

Segni e linee

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

<dialog>
  …
</dialog>

Possiamo migliorare questo valore di riferimento.

Tradizionalmente, un elemento di dialogo condivide molto con un modale e spesso i nomi sono intercambiabili. Mi sono presa la libertà di usare l'elemento della finestra di dialogo sia per i popup di piccole dimensioni (mini) sia per le finestre di dialogo a pagina intera (mega). Li ho chiamati mega e mini, con entrambe le conversazioni leggermente adattate per 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 delle finestre di dialogo mini e mega nei temi chiaro e scuro.

Non sempre, ma in genere gli elementi della finestra 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 racchiuda 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 utilizzano method="dialog" possono chiudere una finestra di dialogo senza JavaScript e passare i 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 indica il titolo della finestra modale e offre un pulsante di chiusura. L'articolo riguarda i dati inseriti nei moduli e le informazioni. 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 in linea onclick. L'attributo autofocus riceverà il focus quando viene aperta la finestra di dialogo e ritengo che sia buona prassi impostarlo sul pulsante Annulla, non su quello di conferma. In questo modo, la conferma è deliberata e non accidentale.

Finestra di dialogo mini

La mini finestra di dialogo è molto simile alla mega finestra di dialogo, manca solo 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 finestra di dialogo fornisce una solida base per un elemento area visibile completa in grado di 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 della finestra di dialogo ha un'accessibilità integrata molto buona. Invece di aggiungere queste funzionalità come faccio di solito, molte sono già presenti.

Ripristino dello stato attivo in corso...

Come abbiamo fatto manualmente in Creare un componente del menu laterale, è importante che l'apertura e la chiusura di un elemento mettano correttamente in primo piano i pulsanti Apri e Chiudi pertinenti. Quando si apre la barra 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à.

Messa a fuoco automatica

L'elemento di dialogo gestisce inert per te nel documento. Prima di inert, JavaScript veniva utilizzato per controllare l'uscita da un elemento, che a quel punto lo intercettava e lo restituisce.

Supporto dei browser

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

Origine

Dopo il giorno inert, qualsiasi parte del documento può essere "bloccata", tanto che non sono più aree target o interattive con il mouse. Invece di bloccare lo stato attivo, lo stato attivo viene indirizzato all'unica parte interattiva del documento.

Aprire un elemento e impostare la messa a fuoco automatica

Per impostazione predefinita, l'elemento dialog assegna lo stato attivo al primo elemento attivabile nel markup della finestra di dialogo. Se questo non è l'elemento migliore per l'utente come impostazione predefinita, utilizza l'attributo autofocus. Come descritto in precedenza, ritengo che sia buona prassi inserire questo messaggio sul pulsante Annulla e non su quello di conferma. In questo modo, la conferma è deliberata e non accidentale.

Chiusura con il tasto Esc

È importante semplificare la chiusura di questo elemento potenzialmente di interruzione. Fortunatamente, l'elemento di dialogo gestirà la chiave di interruzione per te, liberandoti dall'onere dell'orchestrazione.

Stili

Esistono un percorso semplice e uno difficile per applicare lo stile all'elemento della finestra di dialogo. Il percorso più semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo e lavorando con le relative limitazioni. Seguiamo il percorso difficile per fornire animazioni personalizzate per aprire e chiudere la finestra di dialogo, assumere il controllo della proprietà display e altro ancora.

Stile con oggetti aperti

Per velocizzare i colori adattivi e la coerenza complessiva del design, ho integrato senza vergogna la mia libreria di variabili CSS Open Props. Oltre alle variabili gratuite fornite, 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 dialog e della demo senza dover utilizzare molti stili per supportarla e renderla piacevole.

Definizione dello stile dell'elemento <dialog>

Proprietà della proprietà di visualizzazione

Il comportamento predefinito di visualizzazione e occultamento di un elemento della finestra di dialogo attiva/disattiva la proprietà di visualizzazione da block a none. Questo purtroppo significa che l'animazione non può essere dentro e fuori, ma solo dentro. Vorrei animare sia l'entrata che l'uscita e il primo passaggio consiste nell'impostare la mia proprietà display:

dialog {
  display: grid;
}

Se modifichi e quindi possiedi il valore della proprietà display, come mostrato nello snippet CSS sopra, devi gestire una quantità considerevole di stili per facilitare un'esperienza utente adeguata. 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 interagito se 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 possano raggiungere la finestra di dialogo nascosta.

Impostare un tema cromatico adattivo alla finestra di dialogo

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

Anche se color-scheme imposta il documento su un tema a colori adattivo fornito dal browser in base alle preferenze di sistema chiare e scure, volevo personalizzare maggiormente l'elemento della finestra di dialogo. Open Props fornisce alcuni colori delle superfici che si adattano automaticamente alle preferenze di sistema chiare e scure, in modo simile all'utilizzo di color-scheme. Sono ideali per creare livelli in un design e adoro utilizzare il colore per supportare visivamente questa immagine 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 un secondo momento verranno aggiunti altri colori adattabili per gli elementi secondari, come l'intestazione e il piè di pagina. Li considero extra per un elemento di dialogo, ma molto importanti per creare un design accattivante e ben progettato.

Dimensioni delle finestre di dialogo adattabili

Per impostazione predefinita, la finestra di dialogo delega le dimensioni ai contenuti, il che in genere è ottimo. Il mio obiettivo è limitare il max-inline-size a dimensioni leggibili (--size-content-3 = 60ch) o al 90% della larghezza dell'area visibile. In questo modo, la finestra di dialogo non occuperà tutto lo schermo su un dispositivo mobile e non sarà così ampia su uno schermo di 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 dobbiamo specificare dove si trova l'area scorrevole della finestra di dialogo, nel caso in cui sia un elemento della finestra 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 notato che ho max-block-size due volte? Il primo utilizza 80vh, un'unità del viewport fisico. 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 di finestre di dialogo mega

Per posizionare più facilmente un elemento di finestra di dialogo, è consigliabile suddividere le sue due parti: lo sfondo a schermo intero e il contenitore delle finestre di dialogo. Lo sfondo devecoprire tutto, creando un effetto ombra che metta in evidenza la finestra di dialogo in primo piano e renda inaccessibili i contenuti in background. Il contenitore della finestra di dialogo è libero di centrarsi 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 utilizza margin: auto per centrare i contenuti:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Stili di mega finestre di dialogo mobile

Su viewport di piccole dimensioni, stilo questo mega modale a pagina intera in modo leggermente diverso. Ho impostato il margine inferiore su 0, in modo che i contenuti della finestra di dialogo vengano visualizzati nella parte inferiore dell'area visibile. Con un paio di aggiustamenti dello stile, posso trasformare la finestra di dialogo in un action sheet, 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 nella finestra di dialogo mega desktop che mobile quando è aperta.

Posizionamento di mini finestre di dialogo

Quando utilizzo un'area visibile più grande, ad esempio su un computer, ho scelto di posizionare le mini finestre di dialogo sopra l'elemento che le ha richiamate. Per farlo, ho bisogno di JavaScript. Puoi trovare la tecnica che uso qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza il codice 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 tocco alla finestra di dialogo in modo che assomigli a una superficie morbida sopra la pagina. La morbidezza si ottiene arrotondando gli angoli della finestra di dialogo. La profondità viene ottenuta con uno degli oggetti con ombre di Open Props, realizzati con cura:

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

Personalizzare l'elemento pseudo sfondo

Ho scelto di lavorare molto delicatamente con lo sfondo, aggiungendo solo un effetto di sfocatura con backdrop-filter alla mega finestra di dialogo:

Supporto dei browser

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

Origine

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 mega finestra di dialogo sovrapposta a uno sfondo sfocato di avatar colorati.

Stili extra

Ho chiamato questa sezione "extra" perché ha più a che fare con la demo dell'elemento dialog che con l'elemento dialog in generale.

Contenimento dello scorrimento

Quando viene visualizzata la finestra di dialogo, l'utente è ancora in grado di scorrere la pagina al di sotto, 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 scorrimento, quindi non c'è nulla da impedire. Potrei utilizzare JavaScript per monitorare i nuovi eventi di questa guida, ad esempio "closed" e "opened", e attivare/disattivare overflow: hidden nel documento oppure attendere che :has() sia stabile in tutti i browser:

Supporto dei browser

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

Origine

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

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

Il layout <form>

Oltre a essere un elemento molto importante per raccogliere le informazioni sull'interazione dell'utente, lo utilizzo qui per impaginare gli elementi di intestazione, piè di pagina e articolo. Con questo layout intendo articolare l'articolo secondario come area scorrevole. Ottengo questo risultato con grid-template-rows. All'elemento articolo viene assegnato 1fr e il modulo stesso ha la stessa altezza massima dell'elemento di dialogo. L'impostazione di un'altezza stabili e di una dimensione di riga stabili consente di bloccare l'elemento dell'articolo e di scorrerlo in caso di overflow:

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 alle righe.

Aggiunta di 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, viene assegnato un colore di superficie per farlo apparire come se fosse dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti richiedono un contenitore flexbox, elementi allineati verticalmente distanziati dai bordi e un po' di spaziatura e spazi per lasciare 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.

Aggiungere uno stile al pulsante di chiusura dell'intestazione

Poiché la demo utilizza i pulsanti Apri oggetti, il pulsante di chiusura è personalizzato in un pulsante centrato con un'icona rotondo, come in questo caso:

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 per il pulsante di chiusura dell&#39;intestazione.

Definizione dello stile della finestra di dialogo <article>

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

Per farlo, l'elemento del modulo principale ha stabilito alcuni valori massimi per sé 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 mostrate solo quando necessario, contengono scorrimento al loro 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 piè di pagina deve contenere menu di pulsanti di azione. Flexbox viene utilizzato per allineare i contenuti alla fine dell'asse in linea del piè di pagina, quindi viene applicata una spaziatura per lasciare 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 con wrapping con gap per creare spazio tra i pulsanti. Gli elementi del menu hanno un'area di a capo, ad esempio un <ul>. Inoltre lo rimuovo 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 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 un movimento di supporto alle finestre di dialogo per l'ingresso e l'uscita aiuta gli utenti a orientarsi nel flusso.

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

Open Props è dotato di molte animazioni dei fotogrammi chiave utilizzabili, il che rende l'orchestrazione facile e leggibile. Ecco gli obiettivi dell'animazione e l'approccio stratificato che ho adottato:

  1. Il movimento ridotto è la transizione predefinita, con una semplice dissolvenza in entrata e in uscita dall'opacità.
  2. Se il movimento è corretto, vengono aggiunte animazioni di scorrimento e scala.
  3. Il layout mobile adattabile per la finestra di dialogo mega viene modificato in modo da scorrere verso l'esterno.

Una transizione predefinita sicura e significativa

Anche se Open Props include fotogrammi chiave per l'effetto di dissolvenza in entrata e in uscita, preferisco questo approccio alle transizioni a più livelli come impostazione predefinita con animazioni dei fotogrammi chiave come potenziali upgrade. In precedenza abbiamo già impostato 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 transizione graduale che preferisci:

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

Aggiunta di movimento alla transizione

Se l'utente accetta il movimento, sia la finestra di dialogo mega che quella mini devono scorrere verso l'alto all'ingresso e verso l'esterno all'uscita. Puoi farlo con la query mediaprefers-reduced-motion e alcuni elementi Open Props:

@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 relativa allo stile, lo stile della mega finestra di dialogo è adattato ai dispositivi mobili in modo da assomigliare di più a un riquadro di azioni, come se un piccolo pezzo di carta fosse scorrevole dalla parte inferiore dello schermo ed fosse ancora attaccato alla parte inferiore. L'animazione di chiusura con scalata non si adatta bene a questo nuovo design, ma possiamo adattarla con alcune query sui media e alcuni elementi Open Props:

@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

Esistono 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 ignorare la luce (facendo clic sullo sfondo della finestra di dialogo), animazione e alcuni eventi aggiuntivi per velocizzare il recupero dei dati del modulo.

Aggiunta di una dismissione della luce

Questa operazione è semplice e rappresenta un'ottima aggiunta a un elemento della finestra di dialogo che non è animato. L'interazione viene raggiunta osservando i clic sull'elemento della finestra di dialogo e sfruttando la creazione di eventi in serie per valutare su cosa è stato fatto clic. L'interazione avviene 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')
}

Nota dialog.close('dismiss'). L'evento viene chiamato e viene fornita una stringa. Questa stringa può essere recuperata da altri JavaScript per ottenere informazioni su come è stata chiusa la dialog. Scoprirai che ho anche fornito 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 chiusi

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

Per farlo, crea due nuovi eventi denominati 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 è attendere che le animazioni e le transizioni terminino l'esecuzione 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 nella creazione di un componente toast, restituisce una promessa basata sul completamento dell'animazione e delle promesse relative alla transizione. Questo è il motivo per cui dialogClose è una funzione asincrona; può quindi await la promessa restituita e andare avanti 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 accade per close. Utilizzo un MutationObserver per fornire informazioni sulla modifica degli attributi della finestra di dialogo. In questo osservatore, monitorerò le modifiche all'attributo open e gestirò gli eventi personalizzati di conseguenza.

Analogamente a come abbiamo iniziato gli eventi di chiusura e chiusi, crea due nuovi eventi chiamati opening e opened. Se in precedenza ascoltavamo l'evento di chiusura della finestra di dialogo, questa volta utilizziamo un osservatore delle 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'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 delle modifiche degli attributi, cercando che attributeName sia aperto. Poi controlla se l'elemento ha l'attributo o meno: questo ti informa se la finestra di dialogo è stata aperta o meno. Se è stata aperta, rimuovi l'attributo inert, imposta il focus 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 a quello chiuso, invia subito l'evento di apertura, attendi il completamento delle animazioni e poi invia l'evento di apertura.

Aggiunta di un evento rimosso

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

Puoi farlo con un altro osservatore delle mutazioni. Questa volta, invece di osservare gli attributi su un elemento di dialogo, osserveremo gli elementi secondari dell'elemento corpo e osservare 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'osservatore mutazioni viene chiamato ogni volta che vengono aggiunti o rimossi elementi secondari dal corpo del documento. Le mutazioni specifiche monitorate riguardano removedNodes con il 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 di rimozione personalizzato.

Rimozione dell'attributo loading

Per impedire all'animazione di chiusura della finestra di dialogo di essere riprodotta 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 dell'esecuzione 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 della prevenzione delle animazioni delle keyframe al caricamento della pagina qui.

Tutti insieme

Ora che abbiamo spiegato ogni sezione singolarmente, ecco dialog.js nella sua interezza:

// 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 passata un elemento della finestra di dialogo che richiede l'aggiunta di 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 sottoposte ad upgrade con la dismissione rapida, correzioni al caricamento delle animazioni e altri eventi con cui lavorare.

Ascoltare i nuovi eventi personalizzati

Ogni elemento della finestra di dialogo di cui è stato eseguito l'upgrade ora può ascoltare 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 di chiusura e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. I tempi sono buoni in quanto 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ù semplice.

Nota 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 la verifica va a buon fine, lo script acquisisce i valori del modulo e lo reimposta. Il ripristino è utile perché quando la finestra di dialogo viene visualizzata di nuovo, è vuota e pronta per un nuovo invio.

Conclusione

Ora che sai come ci ho fatto, come faresti‽ 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.

Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.

Remix della community

Risorse