Una panoramica di base su come creare mini e mega modali adattabili al colore, reattivi e accessibili con l'elemento <dialog>
.
In questo post voglio condividere le mie idee su come creare mini e mega modali adattabili al colore,
reattivi e accessibili con l'elemento <dialog>
.
Prova la demo e visualizza il
codice sorgente.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
L'elemento
<dialog>
è ideale per informazioni o azioni contestuali nella pagina. Valuta quando l'esperienza utente può trarre vantaggio da un'azione sulla stessa pagina anziché da un'azione su più pagine: magari perché il modulo è piccolo o l'unica azione richiesta all'utente è la conferma o l'annullamento.
L'elemento <dialog>
è diventato stabile di recente in tutti i browser:
Ho notato che l'elemento mancava di alcune cose, quindi in questa sfida GUI aggiungo gli elementi dell'esperienza dello sviluppatore che mi aspetto: eventi aggiuntivi, chiusura rapida, animazioni personalizzate e un tipo mini e mega.
Segni e linee
Gli elementi essenziali di un elemento <dialog>
sono modesti. L'elemento verrà
nascosto automaticamente e ha stili integrati per sovrapporre i contenuti.
<dialog>
…
</dialog>
Possiamo migliorare questo valore di riferimento.
Tradizionalmente, un elemento di dialogo condivide molte caratteristiche con una finestra modale e spesso i nomi
sono intercambiabili. Mi sono presa la libertà di utilizzare l'elemento di dialogo per
sia i piccoli popup di dialogo (mini) sia le finestre di dialogo a pagina intera (mega). Li ho chiamati mega e mini e ho adattato leggermente entrambe le finestre di dialogo per diversi casi d'uso.
Ho aggiunto un attributo modal-mode
per consentirti di specificare il tipo:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Non sempre, ma in genere gli elementi di dialogo vengono utilizzati per raccogliere alcune
informazioni sull'interazione. I moduli all'interno degli elementi di dialogo sono progettati per essere utilizzati
insieme.
È consigliabile che un elemento del modulo contenga i contenuti della finestra di dialogo in modo che
JavaScript possa accedere ai dati inseriti dall'utente. Inoltre, i pulsanti all'interno
di un modulo che utilizza method="dialog"
possono chiudere una finestra di dialogo senza JavaScript e passare
dati.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Finestra di dialogo Mega
Una finestra di dialogo di grandi dimensioni ha tre elementi all'interno del modulo:
<header>
,
<article>
,
e
<footer>
.
Questi elementi fungono da contenitori semantici e da target di stile per la
presentazione della finestra di dialogo. Il titolo dell'intestazione del modale e offre un pulsante di chiusura. L'articolo riguarda gli input e le informazioni del modulo. Il piè di pagina contiene un
<menu>
di
pulsanti di azione.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
Il primo pulsante del menu ha
autofocus
e un gestore di eventi incorporato onclick
. L'attributo autofocus
riceverà
il focus quando viene aperta la finestra di dialogo e ritengo che sia una best practice posizionarlo
sul pulsante Annulla, non su quello di conferma. In questo modo, la conferma è
deliberata e non accidentale.
Mini finestra di dialogo
La mini finestra di dialogo è molto simile alla mega finestra di dialogo, manca solo un elemento
<header>
. In questo modo, può essere più piccolo e in linea.
<dialog id="MiniDialog" modal-mode="mini">
<form method="dialog">
<article>
<p>Are you sure you want to remove this user?</p>
</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
L'elemento di dialogo fornisce una base solida per un elemento di visualizzazione completo che può raccogliere dati e interazioni degli utenti. Questi elementi essenziali possono creare interazioni molto interessanti ed efficaci nel tuo sito o nella tua app.
Accessibilità
L'elemento di dialogo ha un'accessibilità integrata molto buona. Invece di aggiungere queste funzionalità come faccio di solito, molte sono già presenti.
Ripristino della messa a fuoco
Come abbiamo fatto manualmente in Creazione di un componente sidenav, è importante che l'apertura e la chiusura di un elemento mettano correttamente a fuoco i pulsanti di apertura e chiusura pertinenti. Quando si apre la barra di navigazione laterale, lo stato attivo viene impostato sul pulsante di chiusura. Quando viene premuto il pulsante di chiusura, lo stato attivo viene ripristinato sul pulsante che lo ha aperto.
Con l'elemento dialog, questo è il comportamento predefinito integrato:
Purtroppo, se vuoi animare l'apertura e la chiusura della finestra di dialogo, questa funzionalità viene persa. Nella sezione JavaScript ripristinerò questa funzionalità.
Trapping focus
L'elemento di dialogo gestisce
inert
per te nel documento. Prima di inert
, JavaScript veniva utilizzato per monitorare la perdita dello stato attivo
di un elemento, a quel punto lo intercettava e lo ripristinava.
Dopo inert
, qualsiasi parte del documento può essere "congelata" in modo che non sia più un target di messa a fuoco o interattiva con un mouse. Anziché bloccare
lo stato attivo, questo viene indirizzato all'unica parte interattiva del documento.
Aprire e mettere a fuoco automaticamente un elemento
Per impostazione predefinita, l'elemento della finestra di dialogo assegna lo stato attivo al primo elemento attivabile
nel markup della finestra di dialogo. Se questo non è l'elemento migliore per l'utente da impostare come predefinito,
utilizza l'attributo autofocus
. Come descritto in precedenza, ritengo che la best practice
sia di inserirla nel pulsante Annulla e non in quello Conferma. In questo modo, la conferma è intenzionale e non accidentale.
Chiudere con il tasto Esc
È importante che sia facile chiudere questo elemento potenzialmente interruttivo. Fortunatamente, l'elemento di dialogo gestirà il tasto Esc per te, liberandoti dal carico di lavoro di orchestrazione.
Stili
Esiste un percorso semplice per applicare uno stile all'elemento di dialogo e un percorso difficile. Il percorso
semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo e lavorando
con i suoi limiti. Seguo il percorso più difficile per fornire animazioni personalizzate per
l'apertura e la chiusura della finestra di dialogo, assumendo il controllo della proprietà display
e altro ancora.
Stili con Open Props
Per accelerare i colori adattivi e la coerenza complessiva del design, ho introdotto senza vergogna la mia libreria di variabili CSS Open Props. Oltre alle variabili fornite senza costi, importo anche un file normalize e alcuni pulsanti, entrambi forniti da Open Props come importazioni facoltative. Queste importazioni mi aiutano a concentrarmi sulla personalizzazione della dialogo e della demo senza richiedere molti stili per supportarla e renderla accattivante.
Applicare uno stile all'elemento <dialog>
Proprietà di visualizzazione
Il comportamento predefinito di visualizzazione e nascondimento di un elemento di dialogo attiva/disattiva la proprietà
display da block
a none
. Purtroppo, ciò significa che non può essere animato
in entrata e in uscita, ma solo in entrata. Voglio animare sia l'entrata che l'uscita e il primo passo è
impostare la mia proprietà
display:
dialog {
display: grid;
}
Modificando e quindi possedendo il valore della proprietà di visualizzazione, come mostrato nello snippet CSS precedente, è necessario gestire una notevole quantità di stili per facilitare la corretta esperienza utente. Innanzitutto, lo stato predefinito di una finestra di dialogo è chiuso. Puoi rappresentare visivamente questo stato e impedire alla finestra di dialogo di ricevere interazioni con i seguenti stili:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
Ora la finestra di dialogo è invisibile e non può essere utilizzata quando non è aperta. In un secondo momento
aggiungerò del codice JavaScript per gestire l'attributo inert
nella finestra di dialogo, assicurandomi
che anche gli utenti che utilizzano la tastiera e gli screen reader non possano raggiungere la finestra di dialogo nascosta.
Assegnare alla finestra di dialogo un tema cromatico adattivo
Anche se color-scheme
attiva un tema cromatico adattivo fornito dal browser per le preferenze di sistema chiare e scure, volevo personalizzare l'elemento di dialogo in modo più approfondito. Open Props fornisce alcuni colori
di superficie che si adattano automaticamente alle
preferenze di sistema chiare e scure, in modo simile all'utilizzo di color-scheme
. Questi
sono ideali per creare livelli in un design e mi piace usare il colore per
supportare visivamente l'aspetto delle superfici dei livelli. Il colore di sfondo è
var(--surface-1)
; per posizionarlo sopra questo livello, utilizza var(--surface-2)
:
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
In seguito verranno aggiunti altri colori adattivi per gli elementi secondari, come l'intestazione e il piè di pagina. Li considero un extra per un elemento di dialogo, ma sono molto importanti per creare un design di dialogo accattivante e ben progettato.
Dimensioni adattabili delle finestre di dialogo
Per impostazione predefinita, la finestra di dialogo delega le dimensioni ai contenuti, il che è generalmente
ottimo. Il mio obiettivo è limitare le dimensioni di
max-inline-size
a una dimensione leggibile (--size-content-3
= 60ch
) o al 90% della larghezza dell'area visibile. In questo modo, la finestra di dialogo non occuperà l'intero schermo di un dispositivo mobile e non sarà così ampia su uno schermo del computer da risultare difficile da leggere. Poi aggiungo un
max-block-size
in modo che la finestra di dialogo non superi l'altezza della pagina. Ciò significa anche che dovremo
specificare dove si trova l'area scorrevole della finestra di dialogo, nel caso in cui si tratti di un elemento
di dialogo alto.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Nota come ho max-block-size
due volte. Il primo utilizza 80vh
, un'unità
viewport fisica. Quello che voglio davvero è mantenere la finestra di dialogo all'interno del flusso relativo,
per gli utenti internazionali, quindi utilizzo l'unità dvb
logica, più recente e supportata solo parzialmente
nella seconda dichiarazione per quando diventa più stabile.
Posizionamento della finestra di dialogo Mega
Per facilitare il posizionamento di un elemento di dialogo, è utile suddividerlo in due parti: lo sfondo a schermo intero e il contenitore di dialogo. Lo sfondo deve coprire tutto, fornendo un effetto di ombreggiatura per contribuire a indicare che la finestra di dialogo è in primo piano e che i contenuti dietro sono inaccessibili. Il contenitore della finestra di dialogo è libero di centrarsi su questo sfondo e assumere la forma richiesta dai suoi contenuti.
I seguenti stili fissano l'elemento della finestra di dialogo alla finestra, allungandolo fino a ogni angolo e utilizzano margin: auto
per centrare i contenuti:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Stili di finestre di dialogo mega per dispositivi mobili
Sulle finestre di visualizzazione piccole, lo stile di questo mega modale a pagina intera è leggermente diverso. Ho
impostato il margine inferiore su 0
, in modo che i contenuti della finestra di dialogo si trovino nella parte inferiore
dell'area visibile. Con un paio di modifiche allo stile, posso trasformare la finestra di dialogo in un
foglio delle azioni, più vicino ai pollici dell'utente:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Posizionamento della mini finestra di dialogo
Quando utilizzo un'area visibile più grande, ad esempio su un computer desktop, ho scelto di posizionare le mini finestre di dialogo sopra l'elemento che le ha chiamate. Per farlo, ho bisogno di JavaScript. Puoi trovare la tecnica che utilizzo qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza JavaScript, la mini finestra di dialogo viene visualizzata al centro dello schermo, proprio come la mega finestra di dialogo.
Dai risalto alle miniature
Infine, aggiungi un po' di stile alla finestra di dialogo in modo che sembri una superficie morbida molto sopra la pagina. La morbidezza si ottiene arrotondando gli angoli della finestra di dialogo. La profondità viene ottenuta con una delle proprietà ombra di Open Props realizzate con cura:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personalizzare lo pseudo elemento di sfondo
Ho scelto di lavorare in modo molto leggero con lo sfondo, aggiungendo solo un effetto sfocato con
backdrop-filter
alla finestra di dialogo principale:
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;
}
Extra per lo styling
Chiamo questa sezione "extra" perché riguarda più la demo dell'elemento di dialogo che l'elemento di dialogo in generale.
Contenimento dello scorrimento
Quando viene visualizzata la finestra di dialogo, l'utente può comunque scorrere la pagina sottostante, cosa che non voglio:
Normalmente,
overscroll-behavior
sarebbe la mia soluzione abituale, ma secondo le
specifiche,
non ha alcun effetto sulla finestra di dialogo perché non è una porta di scorrimento, ovvero non è
uno scroller, quindi non c'è nulla da impedire. Potrei utilizzare JavaScript per monitorare
i nuovi eventi di questa guida, come "closed" e "opened", e attivare/disattivare
overflow: hidden
nel documento oppure potrei aspettare che :has()
sia stabile in
tutti i browser:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Ora, quando è aperta una finestra di dialogo di grandi dimensioni, il documento HTML contiene overflow: hidden
.
Layout <form>
Oltre a essere un elemento molto importante per la raccolta delle informazioni
sull'interazione dell'utente, lo utilizzo qui per definire gli elementi di intestazione, piè di pagina e
articolo. Con questo layout intendo definire l'articolo secondario come un'area scorrevole. Raggiungo questo obiettivo con
grid-template-rows
.
L'elemento articolo ha 1fr
e il modulo stesso ha la stessa altezza massima dell'elemento finestra di dialogo. L'impostazione di questa altezza fissa e di questa dimensione fissa della riga
consente all'elemento articolo di essere vincolato e di scorrere quando si verifica un overflow:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Applicare uno stile alla finestra di dialogo <header>
Il ruolo di questo elemento è fornire un titolo per i contenuti della finestra di dialogo e offrire un pulsante di chiusura facile da trovare. Inoltre, è stato assegnato un colore di superficie per farlo apparire dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti portano a un contenitore flexbox, elementi allineati verticalmente e distanziati fino ai bordi, nonché a un po' di spaziatura interna e spazi vuoti per dare spazio al titolo e ai pulsanti di chiusura:
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
Stilizzazione del pulsante di chiusura dell'intestazione
Poiché la demo utilizza i pulsanti Open Props, il pulsante di chiusura è personalizzato in un pulsante centrale con icona rotonda, come segue:
dialog > form > header > button {
border-radius: var(--radius-round);
padding: .75ch;
aspect-ratio: 1;
flex-shrink: 0;
place-items: center;
stroke: currentColor;
stroke-width: 3px;
}
Applicare uno stile alla finestra di dialogo <article>
L'elemento articolo ha un ruolo speciale in questa finestra di dialogo: è uno spazio destinato allo scorrimento nel caso di una finestra di dialogo alta o lunga.
A questo scopo, l'elemento del modulo principale ha stabilito alcuni valori massimi per
se stesso, che forniscono vincoli per l'elemento dell'articolo da raggiungere se diventa
troppo alto. Imposta overflow-y: auto
in modo che le barre di scorrimento vengano visualizzate solo quando necessario,
contieni lo scorrimento al suo interno con overscroll-behavior: contain
e il resto
saranno stili di presentazione personalizzati:
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
Applicare uno stile alla finestra di dialogo <footer>
Il ruolo del piè di pagina è quello di contenere i menu dei pulsanti di azione. Flexbox viene utilizzato per allineare i contenuti alla fine dell'asse in linea del piè di pagina, quindi viene aggiunto un po' di spazio per dare spazio ai pulsanti.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
Stilizzazione del menu del piè di pagina della finestra di dialogo
L'elemento menu
viene utilizzato per contenere i pulsanti di azione per la finestra di dialogo. Utilizza un layout
flexbox di wrapping con gap
per fornire spazio tra i pulsanti. Gli elementi del menu
hanno un padding, ad esempio un <ul>
. Rimuovo anche questo stile perché non mi serve.
dialog > form > footer > menu {
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
padding-inline-start: 0;
}
dialog > form > footer > menu:only-child {
margin-inline-start: auto;
}
Animazione
Gli elementi della finestra di dialogo vengono spesso animati perché entrano ed escono dalla finestra. L'aggiunta di un movimento di supporto per l'entrata e l'uscita delle finestre di dialogo aiuta gli utenti a orientarsi nel flusso.
Normalmente, l'elemento di dialogo può essere animato solo in entrata, non in uscita. Questo perché
il browser attiva/disattiva la proprietà display
sull'elemento. In precedenza, la guida
impostava la visualizzazione a griglia e non la impostava mai su Nessuno. In questo modo, puoi animare
l'entrata e l'uscita.
Open Props include molte animazioni keyframe da utilizzare, il che rende l'orchestrazione facile e leggibile. Ecco gli obiettivi di animazione e l'approccio a più livelli che ho adottato:
- Movimento ridotto è la transizione predefinita, una semplice dissolvenza in entrata e in uscita dell'opacità.
- Se il movimento è accettabile, vengono aggiunte le animazioni di scorrimento e ridimensionamento.
- Il layout mobile adattabile per la finestra di dialogo grande è regolato per scorrere verso l'esterno.
Una transizione predefinita sicura e significativa
Anche se Open Props include i fotogrammi chiave per la dissolvenza in entrata e in uscita, preferisco questo
approccio a più livelli delle transizioni come impostazione predefinita, con le animazioni dei fotogrammi chiave come
potenziali upgrade. In precedenza abbiamo già definito lo stile della visibilità della finestra di dialogo con
l'opacità, orchestrando 1
o 0
a seconda dell'attributo [open]
. Per
eseguire la transizione tra 0% e 100%, indica al browser la durata e il tipo di
accelerazione che preferisci:
dialog {
transition: opacity .5s var(--ease-3);
}
Aggiungere movimento alla transizione
Se l'utente accetta il movimento, sia la finestra di dialogo grande che quella piccola devono scorrere
verso l'alto quando entrano e ridursi quando escono. Puoi ottenere questo risultato con la
query multimediale prefers-reduced-motion
e alcune proprietà aperte:
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
Adattare l'animazione di uscita per i dispositivi mobili
Nella sezione precedente relativa allo stile, lo stile della finestra di dialogo mega è adattato per i dispositivi mobili in modo che assomigli di più a un foglio di azioni, come se un piccolo foglio di carta fosse scivolato verso l'alto dalla parte inferiore dello schermo e fosse ancora attaccato alla parte inferiore. L'animazione di uscita di ridimensionamento non si adatta bene a questo nuovo design e possiamo adattarla con un paio di media query e alcune proprietà aperte:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
Ci sono diverse cose da aggiungere con JavaScript:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
Queste aggiunte derivano dal desiderio di chiudere facilmente la finestra di dialogo (facendo clic sullo sfondo), dall'animazione e da alcuni eventi aggiuntivi per una migliore tempistica di ricezione dei dati del modulo.
Aggiunta di chiusura rapida
Questa attività è semplice e un'ottima aggiunta a un elemento di dialogo che non
viene animato. L'interazione viene ottenuta osservando i clic sull'elemento
della finestra di dialogo e sfruttando il
bubbling degli eventi
per valutare su cosa è stato fatto clic e
close()
solo se si tratta dell'elemento più in alto:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Avviso dialog.close('dismiss')
. L'evento viene chiamato e viene fornita una stringa.
Questa stringa può essere recuperata da altro codice JavaScript per ottenere informazioni su come è stata chiusa la finestra di dialogo. Noterai che ho fornito anche stringhe vicine ogni volta che chiamo la funzione da vari pulsanti, per fornire al mio programma il contesto dell'interazione dell'utente.
Aggiunta di eventi di chiusura e chiusi
L'elemento di dialogo è dotato di un evento di chiusura: viene emesso immediatamente quando viene chiamata la funzione
close()
del dialogo. Poiché stiamo animando questo elemento, è
utile avere eventi prima e dopo l'animazione, per consentire a una modifica di recuperare i
dati o reimpostare il modulo della finestra di dialogo. Lo utilizzo qui per gestire l'aggiunta dell'attributo
inert
nella finestra di dialogo chiusa e nella demo li utilizzo per modificare
l'elenco degli avatar se l'utente ha inviato una nuova immagine.
A questo scopo, crea due nuovi eventi chiamati closing
e closed
. Poi
ascolta l'evento di chiusura integrato nella finestra di dialogo. Da qui, imposta la finestra di dialogo su
inert
e invia l'evento closing
. L'attività successiva consiste nell'attendere il completamento
delle animazioni e delle transizioni nella finestra di dialogo, quindi inviare l'evento
closed
.
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
export default async function (dialog) {
…
dialog.addEventListener('close', dialogClose)
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
La funzione animationsComplete
, utilizzata anche nel componente
Creazione di un messaggio toast, restituisce una promessa basata sul
completamento delle promesse di animazione e transizione. Per questo motivo, dialogClose
è una funzione
asincrona;
può quindi
await
la promessa restituita e procedere con sicurezza all'evento chiuso.
Aggiunta di eventi di apertura e aperti
Questi eventi non sono facili da aggiungere perché l'elemento di dialogo integrato non fornisce un evento di apertura come fa con la chiusura. Utilizzo un MutationObserver per fornire approfondimenti sulle modifiche agli attributi della finestra di dialogo. In questo osservatore, monitorerò le modifiche all'attributo open e gestirò gli eventi personalizzati di conseguenza.
In modo simile a come hai creato gli eventi di chiusura e chiusi, crea due nuovi eventi
denominati opening
e opened
. Dove in precedenza ascoltavamo l'evento di chiusura della finestra di dialogo, questa volta utilizziamo un osservatore di mutazioni creato per monitorare gli attributi della finestra di dialogo.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
La funzione di callback dell'observer delle mutazioni viene chiamata quando gli attributi della finestra di dialogo vengono modificati, fornendo l'elenco delle modifiche come array. Itera le modifiche
dell'attributo, cercando attributeName
da aprire. Poi controlla
se l'elemento ha l'attributo o meno: questo indica se la finestra di dialogo
è stata aperta o meno. Se è stata aperta, rimuovi l'attributo inert
, imposta lo stato attivo
su un elemento che richiede
autofocus
o sul primo elemento button
trovato nella finestra di dialogo. Infine, in modo simile all'evento di chiusura
e chiuso, invia immediatamente l'evento di apertura, attendi il completamento delle animazioni
e poi invia l'evento aperto.
Aggiungere un evento rimosso
Nelle applicazioni a pagina singola, le finestre di dialogo vengono spesso aggiunte e rimosse in base alle route o ad altre esigenze e stati dell'applicazione. Può essere utile per pulire gli eventi o i dati quando una finestra di dialogo viene rimossa.
Puoi farlo con un altro observer delle mutazioni. Questa volta, anziché osservare gli attributi di un elemento di dialogo, osserveremo gli elementi secondari dell'elemento body e monitoreremo la rimozione degli elementi di dialogo.
…
const dialogRemovedEvent = new Event('removed')
export default async function (dialog) {
…
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
}
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
Il callback dell'observer delle mutazioni viene chiamato ogni volta che vengono aggiunti o rimossi elementi secondari
dal corpo del documento. Le mutazioni specifiche monitorate riguardano
removedNodes
che hanno
nodeName
di
un dialogo. Se una finestra di dialogo è stata rimossa, gli eventi di clic e chiusura vengono rimossi per
liberare memoria e viene inviato l'evento di rimozione personalizzato.
Rimozione dell'attributo di caricamento
Per impedire la riproduzione dell'animazione di uscita della finestra di dialogo quando viene aggiunta alla pagina o al caricamento della pagina, è stato aggiunto un attributo di caricamento alla finestra di dialogo. Lo script seguente attende il completamento delle animazioni della finestra di dialogo, quindi rimuove l'attributo. Ora la finestra di dialogo può essere animata in entrata e in uscita e abbiamo nascosto efficacemente un'animazione altrimenti distraente.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Scopri di più sul problema relativo all'impedimento delle animazioni dei fotogrammi chiave durante il caricamento della pagina qui.
Tutti insieme
Ecco dialog.js
nella sua interezza, ora che abbiamo spiegato ogni sezione
singolarmente:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Utilizzo del modulo dialog.js
La funzione esportata dal modulo prevede di essere chiamata e di ricevere un elemento di dialogo a cui aggiungere questi nuovi eventi e funzionalità:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
In questo modo, le due finestre di dialogo vengono aggiornate con la chiusura leggera, correzioni del caricamento delle animazioni e altri eventi con cui lavorare.
In ascolto dei nuovi eventi personalizzati
Ogni elemento della finestra di dialogo aggiornato ora può rilevare cinque nuovi eventi, ad esempio:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Ecco due esempi di gestione di questi eventi:
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
Nella demo che ho creato con l'elemento di dialogo, utilizzo l'evento chiuso e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. La tempistica è buona in quanto l'animazione di uscita della finestra di dialogo è stata completata e alcuni script animano il nuovo avatar. Grazie ai nuovi eventi, l'orchestrazione dell'esperienza utente può essere più fluida.
Notice dialog.returnValue
: this contains the close string passed when the
dialog close()
event is called. È fondamentale nell'evento dialogClosed
sapere se la finestra di dialogo è stata chiusa, annullata o confermata. Se viene confermato, lo script recupera i valori del modulo e lo reimposta. Il ripristino è utile in modo che
quando la finestra di dialogo viene visualizzata di nuovo, sia vuota e pronta per un nuovo invio.
Conclusione
Ora che sai come ho fatto, come faresti tu?‽ 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.
Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.
Remix della community
- @GrimLink con una finestra di dialogo 3 in 1.
- @mikemai2awesome con un bel
remix che non modifica la
proprietà
display
. - @geoffrich_ con Svelte e un bel Svelte FLIP.
Risorse
- Codice sorgente su GitHub
- Avatar Doodle