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 le mie opinioni su come creare modelli mini e megamodali adattabili, reattivi e accessibili con l'elemento <dialog>. Prova la demo e visualizza la fonte.

Dimostrazione di mega e mini dialoghi a tema 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 quando 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 da parte dell'utente è la conferma o l'annullamento.

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

Supporto dei browser

  • 37
  • 79
  • 98
  • 15,4

Origine

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

Markup

Gli elementi di base di un elemento <dialog> sono modesti. L'elemento verrà nascosto automaticamente e ha stili integrati per sovrapporsi 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 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). Le ho denominate mega e mini, con entrambe le finestre di dialogo leggermente adattate ai 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 mini e mega con temi chiari e scuri.

Non sempre, ma in genere gli elementi delle finestre di dialogo verranno utilizzati per raccogliere alcune informazioni sulle interazioni. I moduli all'interno degli elementi delle finestre di dialogo sono realizzati per interagire. È 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 utilizza 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 dà un titolo alla finestra modale e offre un pulsante di chiusura. L'articolo serve per input e informazioni sui 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 di menu ha autofocus e un gestore di eventi incorporati onclick. L'attributo autofocus verrà attivato all'apertura della finestra di dialogo e secondo me è consigliabile inserirlo sul pulsante Annulla, non sul pulsante di conferma. Ciò garantisce che la conferma sia intenzionalmente 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 è 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 rendere le interazioni molto interessanti e potenti sul 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 al solito, molte sono già presenti.

Ripristino dello stato attivo in corso...

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

Nell'elemento finestra di dialogo, il comportamento predefinito è integrato:

Sfortunatamente, se vuoi animare la finestra di dialogo dentro e fuori, questa funzionalità andrà persa. Nella sezione JavaScript ripristinerò questa funzionalità.

Messa a fuoco in primo piano

L'elemento della finestra 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 ripristinava.

Supporto dei browser

  • 102
  • 102
  • 112
  • 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 mettere in evidenza l'elemento attivo, viene guidato nell'unica parte interattiva del documento.

Aprire e impostare lo stato attivo su un elemento automaticamente

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, utilizza l'attributo autofocus. Come descritto in precedenza, credo che sia consigliabile applicare questa opzione al pulsante Annulla e non al pulsante di conferma. Ciò garantisce che la conferma sia intenzionale e non accidentale.

Chiusura con il tasto Esc

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

Stili

È disponibile un percorso semplice per definire lo stile dell'elemento della finestra di dialogo e un percorso difficile. Il percorso semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo e risolvendo i suoi limiti. 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.

Stili con oggetti di scena aperti

Per accelerare i colori adattivi e l'uniformità complessiva del design, ho spudoratamente portato nella mia libreria di variabili CSS Open Props. Oltre alle variabili fornite senza costi, importo anche un file di normalizzazione e alcuni pulsanti, entrambi forniti da OpenProps come importazioni facoltative. Queste importazioni mi aiutano a personalizzare la finestra di dialogo e la demo senza aver bisogno di molti stili per supportarla e renderla ottimale.

Stile dell'elemento <dialog>

Possedere la proprietà display

Il comportamento predefinito per mostrare e nascondere un elemento di una 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 eseguire l'animazione sia in entrata che in uscita e il primo passaggio consiste nell'impostare la mia proprietà display personalizzata:

dialog {
  display: grid;
}

Modificando il valore della proprietà display, come mostrato nello snippet CSS precedente, cambiandone la proprietà, è necessario gestire una notevole quantità di stili per facilitare l'esperienza utente corretta. Per prima cosa, viene chiuso lo stato predefinito di una finestra di dialogo. 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

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

Anche se color-scheme attiva il tuo documento in un tema a colori adattivo fornito dal browser per le preferenze di sistema chiaro e scuro, volevo personalizzare ulteriormente l'elemento della finestra di dialogo. Gli oggetti Open Props forniscono alcuni colori di superficie che si adattano automaticamente alle preferenze di sistema relative a luci e bui, in modo simile all'uso di color-scheme. Sono ideali per creare livelli in un progetto e mi piace usare i colori per supportare visivamente l'aspetto delle superfici degli strati. Il colore di sfondo è var(--surface-1); per collocare il livello sopra, 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 colori adattivi per gli elementi secondari, come l'intestazione e il piè di pagina. Li ritengo extra come elemento di dialogo, ma molto importante per creare un design accattivante e ben progettato.

Dimensionamento dei dialoghi adattabili

Per impostazione predefinita, la finestra di dialogo delega le sue dimensioni ai suoi contenuti, il che in genere è ottimo. Il mio obiettivo è vincolare 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 estende perimetralmente sui dispositivi mobili e non sarà così larga sullo schermo di un computer da essere difficoltosa 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;
}

Noti che max-block-size fa due volte? La prima utilizza 80vh, un'unità di area visibile 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 di finestre di dialogo mega

Per posizionare più facilmente un elemento di finestra di dialogo, è consigliabile scomporre le due parti: lo sfondo a schermo intero e il contenitore delle finestre di dialogo. Lo sfondo deve coprire tutto, creando un effetto ombra per favorire il fatto che questa finestra di dialogo sia in primo piano e i contenuti dietro siano inaccessibili. Il contenitore di dialogo può essere centrato liberamente sullo 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 mobile

Nelle aree visibili di piccole dimensioni, lo stile della megamodale a pagina intera è leggermente diverso. Ho impostato il margine inferiore su 0, in modo da portare i contenuti della finestra di dialogo nella parte inferiore dell'area visibile. Con un paio di modifiche allo stile, posso trasformare la finestra di dialogo in un foglio di 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 si sovrappongono alla spaziatura dei margini su una finestra di dialogo Mega per computer e dispositivi mobili aperti.

Posizionamento di mini finestre di dialogo

Quando utilizzo un'area visibile più grande, ad esempio su un computer desktop, ho scelto di posizionare le mini finestre di dialogo sull'elemento che le chiamava. Per farlo, mi serve JavaScript. Puoi trovare la tecnica che utilizzo qui, ma credo 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.

Metti in risalto

Infine, aggiungi un tocco di originalità alla finestra di dialogo in modo che appaia come una superficie morbida sopra la pagina. La morbidezza si ottiene arrotondando gli angoli della finestra di dialogo. La profondità si ottiene con uno degli oggetti di scena aperti accuratamente realizzati con uno strumento:

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

Personalizzazione dello pseudoelemento di sfondo

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

Supporto dei browser

  • 76
  • 79
  • 103
  • 9

Origine

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

Ho anche scelto di applicare una transizione a 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

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

Contenimento della pergamena

Quando viene visualizzata la finestra di dialogo, l'utente riesce comunque a scorrere la pagina visualizzata, 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 controllare i nuovi eventi di questa guida, come "chiuso" e "aperto" e attivare/disattivare overflow: hidden nel documento, oppure potrei aspettare che :has() sia stabile in tutti i browser:

Supporto dei browser

  • 105
  • 105
  • 121
  • 15,4

Origine

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

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

Layout <form>

Oltre a essere un elemento molto importante per raccogliere le informazioni sull'interazione da parte dell'utente, le uso qui per definire il layout degli elementi di intestazione, piè di pagina e articolo. Con questo layout intendo articolare l'articolo figlio come un'area scorrevole. Ottengo questo risultato con grid-template-rows. All'elemento dell'articolo viene assegnato un valore 1fr e il modulo stesso ha la stessa altezza massima dell'elemento della finestra 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 si sovrappongono alle informazioni del layout a griglia sulle righe.

Definizione dello stile della 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. Viene anche assegnato un colore alla superficie per farlo sembrare dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti portano a un contenitore flexbox con elementi allineati verticalmente con i bordi e ad alcuni spazi 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 informazioni di layout flexbox all&#39;intestazione della finestra di dialogo.

Definizione dello stile del pulsante di chiusura dell'intestazione

Poiché la demo utilizza i pulsanti Open Props, il pulsante di chiusura è personalizzato in un pulsante centrato con un'icona rotonda, 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 con informazioni su dimensioni e spaziatura interna 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: si tratta di uno spazio che deve essere fatto scorrere nel caso di una finestra di dialogo lunga o alta.

A questo scopo, l'elemento modulo principale ha stabilito alcuni valori massimi che forniscono vincoli che l'elemento dell'articolo deve raggiungere se è troppo alto. Imposta overflow-y: auto in modo che le barre di scorrimento vengano visualizzate solo quando necessario, che contengano lo scorrimento al suo interno con overscroll-behavior: contain, mentre il resto avrà 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 con una certa spaziatura per dare ai pulsanti un po' di spazio.

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 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 con wrapping con gap per lasciare spazio tra i pulsanti. Gli elementi del menu hanno una spaziatura interna come <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. L'assegnazione di un movimento di supporto per l'entrata e l'uscita consente agli utenti di orientarsi nel flusso.

Normalmente l'elemento della finestra di dialogo può essere solo animato all'interno, non all'esterno. Questo perché il browser attiva/disattiva la proprietà display sull'elemento. In precedenza la guida ha impostato la visualizzazione su griglia e non la imposta 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 a più livelli 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 della mega finestra di dialogo viene modificato in modo da scorrere verso l'esterno.

Una transizione predefinita sicura e significativa

Anche se gli oggetti Open Props sono dotati di 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 la visibilità della finestra di dialogo con opacità, orchestrando 1 o 0 a seconda dell'attributo [open]. Per passare da 0% a 100%, indica al browser la durata e il tipo di adattamento desiderato:

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

Aggiunta di movimento alla transizione

Se l'utente è a suo agio con il movimento, entrambe le finestre di dialogo mega e mini dovrebbero scorrere come ingresso e ridimensionarsi man mano che esce. Puoi farlo con la prefers-reduced-motion query multimediale e alcuni oggetti oggetti 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 il mobile

In precedenza, nella sezione Stili, lo stile delle mega finestre di dialogo è stato adattato per essere i dispositivi mobili in modo da essere più simili a un foglio azioni, come se un piccolo pezzo di carta venisse sfilato verso l'alto dalla parte inferiore dello schermo ed 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 supporti e alcuni oggetti 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 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 della funzione Ignora luce

Questa attività è semplice e rappresenta un'ottima aggiunta a un elemento di dialogo che non viene animato. L'interazione si ottiene osservando i clic sull'elemento finestra di dialogo e sfruttando il bubbling degli eventi per valutare gli elementi su cui è stato fatto clic e 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')
}

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

Aggiunta di eventi di chiusura e chiusura

L'elemento della finestra di dialogo viene fornito con un evento di chiusura: viene emesso immediatamente quando viene richiamata la funzione di dialogo close(). Dal momento che stiamo animando questo elemento, è utile avere eventi prima e dopo l'animazione, in modo da ottenere i dati o reimpostare il modulo della finestra di dialogo in caso di modifica. Lo utilizzo qui per gestire l'aggiunta dell'attributo inert nella finestra di dialogo chiusa, mentre nella demo li utilizzo per modificare l'elenco di avatar se l'utente ha inviato una nuova immagine.

A questo scopo, 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 apertura in corso...

Non è facile aggiungere questi eventi perché l'elemento di dialogo integrato non fornisce un evento aperto come fa con la chiusura. Utilizzo MutationObserver per fornire insight sulla modifica degli attributi della finestra di dialogo. In questo osservatore, controllo le modifiche all'attributo open e gestirò di conseguenza gli eventi personalizzati.

Analogamente a come abbiamo iniziato gli eventi di chiusura e chiusi, crea due nuovi eventi chiamati opening e opened. Mentre in precedenza abbiamo ascoltato l'evento di chiusura della finestra di dialogo, questa volta utilizza un osservatore delle mutazioni creato per osservare 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 vengono modificati gli attributi della finestra di dialogo, fornendo l'elenco delle modifiche sotto forma di array. Esegui un'iterazione sulle modifiche agli attributi, cercando attributeName che sia aperto. Dopodiché, controlla se l'elemento ha o meno l'attributo: questo indica se la finestra di dialogo è stata 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, in modo simile all'evento di chiusura e chiusura, invia subito l'evento di apertura, attendi il completamento delle animazioni, quindi invia l'evento aperto.

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 eventi o dati quando viene rimossa una finestra di dialogo.

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 osservate sono relative a removedNodes che hanno lo nodeName di una finestra di dialogo. Se è stata rimossa una finestra di dialogo, gli eventi di clic e chiusura vengono rimossi per liberare memoria e viene inviato l'evento personalizzato rimosso.

Rimozione dell'attributo di caricamento

Per impedire che l'animazione della finestra di dialogo riproduca l'animazione di uscita 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 termine dell'esecuzione delle animazioni delle finestre di dialogo, quindi rimuove l'attributo. Ora la finestra di dialogo può animarsi dentro e fuori e abbiamo effettivamente nascosto un'animazione che altrimenti potrebbe distrarre.

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

Scopri di più sul problema di impedire le animazioni dei fotogrammi chiave durante il caricamento pagina.

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 breve, le due finestre di dialogo vengono aggiornate con chiusura leggera, correzioni di caricamento delle animazioni e altri eventi con cui lavorare.

Ascoltare i nuovi eventi personalizzati

Ora ogni elemento di finestra di dialogo aggiornato può 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)

Ecco due esempi di come gestire 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 finestra di dialogo, uso l'evento chiuso e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. I tempi sono buoni perché la finestra di dialogo ha completato l'animazione di uscita e poi 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 viene confermato, lo script recupera i valori del modulo e lo reimposta. La reimpostazione è utile in modo che la finestra di dialogo sia vuota e sia pronta per essere inviata di nuovo quando viene visualizzata di nuovo.

Conclusione

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

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

Crea una demo, twittami con i link e io la aggiungerò alla sezione dei remix della community qui sotto.

Remix della community

Risorse