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 adattabili al colore, modelli mini e megamodali adattabili e accessibili con l'elemento <dialog>. Prova la demo e guarda il Fonte.

Dimostrazione dei mega e mini dialoghi con temi chiari e scuri.

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

Panoramica

La <dialog> è ottimo per le informazioni contestuali o le azioni in-page. Pensa a quando l'esperienza utente può trarre vantaggio da un'azione sulla stessa pagina anziché su più pagine azione: forse perché il modulo è piccolo o è l'unica azione richiesta dal conferma o annulla.

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 GUI Sfida Aggiungo l'esperienza di sviluppo elementi previsti: eventi aggiuntivi, chiusura di luci, animazioni personalizzate e un mini e mega-type.

Aumento

Gli elementi di base di un elemento <dialog> sono modesti. L'elemento vengono nascosti automaticamente e presentano stili integrati per sovrapporsi ai 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 finestra di dialogo per sia piccole finestre di dialogo (mini) sia finestre di dialogo a pagina intera (mega). Ho denominato grandi 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 alcuni informazioni sull'interazione. I moduli all'interno di elementi di finestre di dialogo sono pensati per essere subito insieme. È consigliabile che un elemento modulo racchiuda i contenuti delle finestre di dialogo in modo che JavaScript può accedere ai dati inseriti dall'utente. Inoltre, i pulsanti all'interno un modulo che utilizza method="dialog" può chiudere una finestra di dialogo senza JavaScript e passare e i dati di Google Cloud.

<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, oltre a target di stile per presentazione della finestra di dialogo. L'intestazione dà un titolo alla modale e offre una . L'articolo serve per input e informazioni sui moduli. 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 di menu ha autofocus e un gestore di eventi in linea onclick. L'attributo autofocus riceverà lo stato attivo quando la finestra di dialogo è aperta. Secondo me è una best practice sul pulsante Annulla, non sul pulsante di conferma. In questo modo ti assicuri che la conferma intenzionale e non accidentale.

Mini finestra di dialogo

La mini finestra di dialogo è molto simile alla mega finestra di dialogo, manca 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 dell'area visibile completa che possono raccogliere dati e interazioni degli utenti. Questi elementi essenziali possono rendere molto interazioni più interessanti ed efficaci sul tuo sito o nella tua app.

Accessibilità

L'elemento della finestra di dialogo ha un'accessibilità integrata molto buona. Invece di aggiungerli come al solito, molte sono già presenti.

Ripristino dello stato attivo in corso...

Come abbiamo fatto manualmente in Creazione di una navigazione laterale , è importante che l'apertura e la chiusura di qualcosa pone correttamente l'attenzione sull'apertura e la chiusura pertinenti pulsanti. Quando si apre la barra di navigazione laterale, lo stato attivo viene impostato sul pulsante di chiusura. Quando premuto il pulsante di chiusura, viene ripristinato lo stato attivo sul pulsante che l'ha aperto.

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

Sfortunatamente, se vuoi animare la finestra di dialogo dentro e fuori, questa funzionalità è perduto. Ripristinerò lo stesso file nella sezione JavaScript. funzionalità.

Messa a fuoco in primo piano

L'elemento finestra di dialogo gestisce inert per te sul documento. Prima del giorno inert, veniva utilizzato JavaScript per controllare lo stato attivo lasciando un elemento, a quel punto lo intercetta e lo restituisce.

Supporto dei browser

  • 102
  • 102
  • 112
  • 15.5

Origine

Dopo il giorno inert, qualsiasi parte del documento potrebbe essere "bloccata" in quanto sono non sono più incentrati sui target o sono interattivi con il mouse. Invece di intrappolare lo stato 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 delle finestre di dialogo. Se questo non è l'elemento migliore per l'utente per impostazione predefinita, utilizza l'attributo autofocus. Come spiegato in precedenza, è una best practice per metterlo sul pulsante Annulla e non sul pulsante di conferma. Ciò garantisce che la conferma è 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 dall'onere dell'orchestrazione.

Stili

È disponibile un percorso semplice per lo stile dell'elemento della finestra di dialogo e un percorso difficile. Il modo più semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo con i suoi limiti. Seguiamo il percorso difficile per fornire animazioni personalizzate l'apertura e la chiusura della finestra di dialogo, prendendo il controllo della proprietà display e altre ancora.

Stili con oggetti di scena aperti

Per accelerare i colori adattivi e l'uniformità del design generale, ho spudoratamente trasferito nella libreria di variabili CSS Open Props. Nella oltre alle variabili fornite senza costi, importo anche un normalizza il file e alcuni pulsanti, entrambi aperti fornisce come importazioni facoltative. Queste importazioni mi permettono di concentrarmi sulla personalizzazione una finestra di dialogo e una demo pur non avendo bisogno di molti stili per supportarla bene.

Stile dell'elemento <dialog>

Possedere la proprietà display

Il comportamento predefinito di visualizzazione e occultamento di un elemento di finestra di dialogo attiva/disattiva la visualizzazione proprietà da block a none. Ciò significa che non è possibile animare dentro e fuori, solo dentro. Vorrei eseguire l'animazione sia in entrata che in uscita e il primo passaggio è per impostare il mio proprietà display:

dialog {
  display: grid;
}

Modificando, e quindi possedendo, il valore della proprietà display, come mostrato nell' precedente snippet CSS, è necessario gestire una notevole quantità di stili per per creare un'esperienza utente corretta. Innanzitutto, lo stato predefinito di una finestra di dialogo chiuso. Puoi rappresentare visivamente questo stato e impedire che la finestra di dialogo 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. Più tardi Aggiungerò codice JavaScript per gestire l'attributo inert nella finestra di dialogo, assicurando che gli utenti di tastiera e screen reader non riescono a 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.

Mentre color-scheme attiva il tuo documento in un servizio fornito dal browser il tema cromatico adattivo alle preferenze di sistema chiare e scure, volevo personalizzare l'elemento della finestra di dialogo. Gli oggetti aperti offrono una certa superficie colori che si adattano automaticamente preferenze di sistema chiare e scure, in modo simile all'uso di color-scheme. Questi sono ideali per creare strati in un progetto e mi piace usare i colori per supportano visivamente l'aspetto delle superfici degli strati. Il colore di sfondo è var(--surface-1); per posizionare il documento su quel 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 piè di pagina. Li ritengo un elemento extra come elemento di dialogo, ma molto importante realizzare finestre di dialogo convincenti e ben progettate.

Dimensionamento dei dialoghi adattabili

Per impostazione predefinita, la finestra di dialogo delega le proprie dimensioni ai contenuti, il che in genere bene. Il mio obiettivo qui è limitare max-inline-size a una dimensione leggibile (--size-content-3 = 60ch) o al 90% della larghezza dell'area visibile. Questo assicura che la finestra di dialogo non si spinga bordo a bordo su un dispositivo mobile e non accadrà largo su uno schermo di computer di difficile lettura. Quindi aggiungo un max-block-size in modo che la finestra di dialogo non superi l'altezza della pagina. Ciò significa anche che devi specificare dove si trova l'area scorrevole della finestra di dialogo, nel caso sia alta finestra di dialogo.

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? Il primo utilizza 80vh, un indirizzo area visibile. Quello che voglio davvero è mantenere il dialogo all'interno del flusso relativo, per gli utenti internazionali, quindi uso la logica, la più recente e solo parzialmente dvb unità supportata nella seconda dichiarazione, che indica quando diventa più stabile.

Posizionamento di finestre di dialogo mega

Per posizionare più facilmente un elemento di finestra di dialogo, è consigliabile scomporre i due parti: lo sfondo a schermo intero e il contenitore delle finestre di dialogo. Lo sfondo deve coprire tutto, creando un effetto sfumatura per contribuire a rendere questa finestra di dialogo davanti e i contenuti dietro sono inaccessibili. Il contenitore di dialogo è libero posizionarsi al centro dello 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);
}
Mega stili di finestre di dialogo mobile

Nelle aree visibili di piccole dimensioni, lo stile della megamodale a pagina intera è leggermente diverso. IO imposta il margine inferiore su 0, in modo da portare i contenuti della finestra di dialogo in fondo nell'area visibile. Con un paio di modifiche allo stile, posso trasformare il dialogo in un foglio d'azione, 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 mega finestra di dialogo desktop che mobile mentre è aperta.

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 l'elemento che li ha chiamati. Per farlo, mi serve JavaScript. Puoi trovare le tecnica che uso qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza il codice JavaScript, una mini finestra di dialogo viene visualizzata al centro dello schermo, proprio come una mega finestra di dialogo.

Metti in risalto

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

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 sfocato con backdrop-filter alla mega finestra di dialogo:

Supporto dei browser

  • 76
  • 79
  • 103
  • 18

Origine

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

Ho anche scelto di eseguire una transizione su backdrop-filter, nella speranza che i browser consentirà 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 l'elemento della finestra di dialogo rispetto all'elemento di dialogo in generale.

Contenimento della pergamena

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

Generalmente, overscroll-behavior sarebbe la mia solita soluzione, ma secondo la specifiche, non ha alcun effetto sulla finestra di dialogo perché non è una porta di scorrimento, ovvero a scorrimento, quindi non c'è nulla da impedire. potrei usare JavaScript per controllare i nuovi eventi di questa guida, ad esempio "chiuso" e "aperto" e attiva/disattiva overflow: hidden sul documento oppure potrei attendere che :has() si stabilizzi in modo stabile 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 la raccolta delle interazioni informazioni da parte dell'utente, lo uso qui per impaginare intestazioni, piè di pagina elementi dell'articolo. Con questo layout intendo articolare l'articolo figlio come un in cui è possibile scorrere. Ottengo questo risultato grid-template-rows Per l'elemento dell'articolo è assegnato un valore 1fr e il modulo stesso ha lo stesso valore massimo l'altezza dell'elemento della finestra di dialogo. L'impostazione di un'altezza fissa e di una dimensione di riga stabili è consente di vincolare l'elemento articolo e di scorrere quando ne straripa:

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 apparire dietro i contenuti dell'articolo di dialogo. Questi requisiti generano un flexbox contenitore, elementi allineati verticalmente e distanziati rispetto ai bordi e alcuni 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 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 rotondo con un'icona al centro, come questo:

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

Screenshot di Chrome DevTools 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: è uno spazio destinato a essere fatto scorrere nel caso di una finestra di dialogo lunga o lunga.

A questo scopo, l'elemento modulo padre ha stabilito alcuni valori massimi per che forniscono vincoli che questo elemento dell'articolo deve raggiungere se ottiene troppo alto. Imposta overflow-y: auto in modo che le barre di scorrimento vengano mostrate solo quando necessario. contiene 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 piè di pagina deve contenere menu di pulsanti di azione. Flexbox viene utilizzato allinea i contenuti alla fine dell'asse in linea del piè di pagina, quindi una certa spaziatura per di 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 informazioni di layout flexbox all&#39;elemento piè di pagina.

La menu viene utilizzato per contenere i pulsanti di azione per la finestra di dialogo. Utilizza un wrapping layout flexbox con gap per lasciare spazio tra i pulsanti. Elementi del menu presentano una spaziatura interna, ad esempio <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 ai dialoghi aiuta gli utenti 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 dell'elemento. In precedenza, la guida imposta la visualizzazione su griglia e non la imposta mai su nessuno. In questo modo, è possibile si animano dentro e fuori.

Gli oggetti Open Props sono dotati di molti fotogrammi chiave animazioni, il che rende orchestrazione semplice e leggibile. Ecco gli obiettivi delle animazioni l'approccio 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 aperti sono dotati di fotogrammi chiave per la dissolvenza in entrata e in uscita, preferisco questo approccio a più livelli delle transizioni come impostazione predefinita, mentre le animazioni dei fotogrammi chiave potenziali upgrade. In precedenza abbiamo già assegnato uno stile alla visibilità della finestra di dialogo opacità, orchestrando 1 o 0 in base all'attributo [open]. A transizione tra 0% e 100%, comunicare al browser la durata e il tipo dell'output 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 lo scale out come uscita. Puoi farlo con prefers-reduced-motion query multimediale e alcuni 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 finestre di dialogo mega è stato adattato per i dispositivi mobili. i dispositivi in modo che siano più simili a un foglio azioni, come se un pezzo di carta verso l'alto dalla parte inferiore dello schermo ed è ancora attaccata alla parte inferiore. La scala l'animazione di uscita non si adatta bene a questo nuovo design e possiamo adattarla un paio di query multimediali e alcuni oggetti di scena aperti:

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

JavaScript

Ci sono diversi elementi da aggiungere con JavaScript:

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

Queste aggiunte nascono dal desiderio di ignorare la luce (facendo clic sulla finestra di dialogo sfondo), l'animazione e alcuni eventi aggiuntivi per ottimizzare la tempistica della visualizzazione i dati del modulo.

Aggiunta della funzione Ignora luce

Questa attività è semplice e rappresenta un'ottima aggiunta a un elemento di dialogo che non essere animati. L'interazione si ottiene osservando i clic sulla finestra di dialogo e sfruttare gli eventi gorgogliamento per valutare gli elementi sui quali è stato fatto clic e close() 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 altri elementi JavaScript per ottenere informazioni su come la stringa la finestra di dialogo è stata chiusa. Scoprirai che ho fornito anche stringhe di chiusura ogni volta che chiamo la funzione da vari pulsanti, per fornire un contesto alla mia applicazione l'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(). Dato che stiamo animando questo elemento, è utile avere eventi prima e dopo l'animazione, per cambiare o reimpostare il modulo della finestra di dialogo. Lo utilizzo qui per gestire l'aggiunta Attributo inert nella finestra di dialogo chiusa e nella demo li uso per modificarlo l'elenco di avatar se l'utente ha inviato una nuova immagine.

A questo scopo, crea due nuovi eventi denominati closing e closed. Poi rimanere in ascolto dell'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 animazioni e transizioni per terminare l'esecuzione sulla finestra di dialogo, quindi invia closed evento.

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 sezione Creazione di un avviso popup , restituisce una promessa basata sul il completamento dell'animazione e le promesse di transizione. Ecco perché dialogClose è una funzione asincrona funzione; può quindi await la promessa è tornata e si procede con fiducia all'evento chiuso.

Aggiunta di eventi di apertura e apertura in corso...

Aggiungere questi eventi non è facile perché l'elemento di dialogo integrato fornisce un evento aperto come fa con la chiusura. Uso un MutationObserver per fornire insight sul cambiamento degli attributi della finestra di dialogo. In questo osservatore, Cercherò le modifiche all'attributo aperto 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. Dove in precedenza abbiamo ascoltato la chiusura della finestra di dialogo , questa volta usa un osservatore delle mutazioni creato per controllare la finestra di dialogo attributi.

…
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 mutazioni verrà chiamata quando la finestra di dialogo vengono modificati, fornendo l'elenco delle modifiche sotto forma di array. Ripetizione oltre cambia l'attributo, cercando che attributeName sia aperto. Poi controlla se l'elemento ha o meno l'attributo: indica se la finestra di dialogo è diventata aperta. Se è stato aperto, rimuovi l'attributo inert e imposta lo stato attivo a un elemento che richiede autofocus o il primo elemento button trovato nella finestra di dialogo. Infine, in modo simile alla chiusura e chiuso, invia subito l'evento di apertura, attendi le animazioni per terminare, 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 alle route o altre esigenze e stato dell'applicazione. Può essere utile ripulire gli eventi quando viene rimossa una finestra di dialogo.

Puoi farlo con un altro osservatore delle mutazioni. Questa volta, invece di osservando gli attributi su un elemento di dialogo, osserveremo i figli del corpo e verifica che gli elementi delle finestre di dialogo vengano rimossi.

…
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 di osservatore delle mutazioni viene chiamato ogni volta che vengono aggiunti o rimossi bambini dal corpo del documento. Le mutazioni specifiche osservate sono per removedNodes con nodeName di una finestra di dialogo. Se è stata rimossa una finestra di dialogo, gli eventi di clic e chiusura vengono rimossi a liberare memoria e inviare 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 a la pagina o al caricamento della pagina, nella finestra di dialogo è stato aggiunto un attributo di caricamento. La lo script seguente attende il termine 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 per nascondere in modo efficace un'animazione altrimenti distraente.

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 della pagina qui.

Tutti insieme

Ora che abbiamo spiegato ogni sezione, ecco dialog.js nella sua interezza. 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 passata una 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, abbiamo eseguito l'upgrade delle due finestre di dialogo caricamento di correzioni 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. Il tempismo è buono che la finestra di dialogo abbia completato l'animazione di uscita e che alcuni script si animano nel nuovo avatar. Grazie ai nuovi eventi, l'orchestrazione dell'esperienza utente può essere più fluido.

Nota dialog.returnValue: contiene la stringa di chiusura passata quando l'elemento viene chiamato l'evento della finestra di dialogo close(). Nell'evento dialogClosed è fondamentale per sapere se la finestra di dialogo è stata chiusa, annullata o confermata. Se viene confermato, quindi recupera i valori del modulo e reimpostalo. Il ripristino è utile in modo che 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‽ 🙂

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

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

Remix della community

Risorse