Una panoramica di base su come creare mini e mega modali adattabili al colore, adattabili e accessibili con l'elemento <dialog>
.
In questo post voglio condividere la mia opinione su come creare mini e mega modali adattabili al colore, adattabili e accessibili con l'elemento <dialog>
.
Prova la demo e visualizza il codice fonte.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
L'elemento
<dialog>
è ideale per informazioni o azioni contestuali in-page. Valuta se l'esperienza utente può trarre vantaggio da un'azione nella stessa pagina anziché da un'azione su più pagine: ad esempio, perché il modulo è piccolo o perché l'unica azione richiesta all'utente è confermare o annullare.
Di recente, l'elemento <dialog>
è diventato stabile su tutti i browser:
Ho notato che nell'elemento mancavano alcuni elementi, quindi in questa sfida sulla GUI ho aggiunto gli elementi dell'esperienza dello sviluppatore che mi aspetto: eventi aggiuntivi, dismiss light, animazioni personalizzate e un tipo mini e mega.
Segni e linee
Gli elementi essenziali di un elemento <dialog>
sono modesti. L'elemento verrà nascosto automaticamente e include stili integrati per sovrapporre i contenuti.
<dialog>
…
</dialog>
Possiamo migliorare questo benchmark.
Tradizionalmente, un elemento di dialogo condivide molto con un modale e spesso i nomi
sono intercambiabili. Mi sono preso la libertà di utilizzare l'elemento di dialogo sia per le piccole finestre popup (mini) sia per le finestre di dialogo a pagina intera (mega). Li ho chiamati mega e mini, con entrambe le conversazioni leggermente adattate per casi d'uso diversi.
Ho aggiunto un attributo modal-mode
per consentirti di specificare il tipo:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Non sempre, ma in genere gli elementi della finestra di dialogo vengono utilizzati per raccogliere alcune informazioni sull'interazione. I moduli all'interno degli elementi di dialogo sono progettati per essere utilizzati insieme.
È buona prassi inserire un elemento di modulo per avvolgere i contenuti della finestra di dialogo in modo che
JavaScript possa accedere ai dati inseriti dall'utente. Inoltre, i pulsanti all'interno di un modulo che utilizzano method="dialog"
possono chiudere una finestra di dialogo senza JavaScript e passare i dati.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Finestra di dialogo Mega
Una mega finestra di dialogo contiene tre elementi:
<header>
,
<article>
,
e
<footer>
.
Questi elementi fungono da contenitori semantici e come target di stile per la presentazione della finestra di dialogo. L'intestazione indica il titolo della finestra modale e offre un pulsante di chiusura. L'articolo riguarda i dati inseriti nel modulo e le informazioni. Il piè di pagina contiene un
<menu>
di pulsanti di azione.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
<header>
<h3>Dialog title</h3>
<button onclick="this.closest('dialog').close('close')"></button>
</header>
<article>...</article>
<footer>
<menu>
<button autofocus type="reset" onclick="this.closest('dialog').close('cancel')">Cancel</button>
<button type="submit" value="confirm">Confirm</button>
</menu>
</footer>
</form>
</dialog>
Il primo pulsante del menu ha
autofocus
e un gestore di eventi in linea onclick
. L'attributo autofocus
riceverà il focus quando viene aperta la finestra di dialogo e ritengo che sia buona prassi impostarlo sul pulsante Annulla, non su quello di conferma. In questo modo, la conferma è deliberata e non accidentale.
Finestra di dialogo mini
La finestra di dialogo mini è molto simile a quella mega, manca solo un elemento<header>
. In questo modo, le dimensioni sono ridotte e il testo è 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 dialog fornisce una base solida per un elemento viewport completo che può raccogliere dati e interazioni utente. Questi elementi essenziali possono creare interazioni molto interessanti e potenti 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 Creare un componente del menu laterale, è importante che l'apertura e la chiusura di un elemento mettano correttamente in primo piano i pulsanti Apri e Chiudi pertinenti. Quando si apre la barra laterale, lo stato attivo viene impostato sul pulsante di chiusura. Quando viene premuto il pulsante di chiusura, lo stato attivo viene ripristinato sul pulsante che lo ha aperto.
Con l'elemento dialog, questo è il comportamento predefinito integrato:
Purtroppo, se vuoi animare l'apertura e la chiusura della finestra di dialogo, questa funzionalità viene persa. Nella sezione JavaScript ripristinerò questa funzionalità.
Messa a fuoco forzata
L'elemento di dialogo gestisce
inert
per te nel documento. Prima di inert
, JavaScript veniva utilizzato per rilevare quando lo stato attivo lasciava un elemento, a quel punto lo intercettava e lo rimetteva.
Dopo inert
, qualsiasi parte del documento può essere "congelata" in modo che non sia più un target di attivazione o non sia interattiva con un mouse. Invece di bloccare lo stato attivo, lo stato attivo viene indirizzato all'unica parte interattiva del documento.
Aprire un elemento e impostare la messa a fuoco automatica
Per impostazione predefinita, l'elemento dialog assegna lo stato attivo al primo elemento attivabile nel markup della finestra di dialogo. Se questo non è l'elemento migliore per l'utente come impostazione predefinita,
utilizza l'attributo autofocus
. Come descritto in precedenza, ritengo che sia buona prassi inserire questo messaggio sul pulsante Annulla e non su quello di conferma. In questo modo, la conferma è deliberata e non accidentale.
Chiusura con il tasto Esc
È importante semplificare la chiusura di questo elemento potenzialmente di interruzione. Fortunatamente, l'elemento di dialogo gestirà la chiave di interruzione per te, liberandoti dall'onere dell'orchestrazione.
Stili
Esistono un percorso semplice e uno difficile per applicare lo stile all'elemento della finestra di dialogo. Il percorso più semplice si ottiene non modificando la proprietà di visualizzazione della finestra di dialogo e lavorando con le relative limitazioni. Scelgo la strada più difficile per fornire animazioni personalizzate per aprire e chiudere la finestra di dialogo, acquisire la proprietà display
e altro ancora.
Stile con oggetti aperti
Per velocizzare i colori adattivi e la coerenza complessiva del design, ho integrato senza vergogna la mia libreria di variabili CSS Open Props. Oltre alle variabili senza costi fornite, importo anche un file normalize e alcuni pulsanti, entrambi forniti da Open Props come importazioni facoltative. Queste importazioni mi aiutano a concentrarmi sulla personalizzazione della dialog e della demo senza dover utilizzare molti stili per supportarla e renderla piacevole.
Definizione dello stile dell'elemento <dialog>
Proprietà della proprietà di visualizzazione
Il comportamento predefinito di visualizzazione e occultamento di un elemento della finestra di dialogo attiva/disattiva la proprietà di visualizzazione da block
a none
. Purtroppo, ciò significa che non può essere animato
in entrata e in uscita, solo in entrata. Vorrei animare sia l'entrata che l'uscita e il primo passaggio consiste nell'impostare la mia proprietà display:
dialog {
display: grid;
}
Se modifichi e quindi possiedi il valore della proprietà display, come mostrato nello snippet CSS riportato sopra, devi gestire una quantità considerevole di stili per facilitare un'esperienza utente adeguata. Innanzitutto, lo stato predefinito di una finestra di dialogo è chiuso. Puoi rappresentare questo stato visivamente 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 è possibile interagire con essa quando non è aperta. In seguito, aggiungerò del codice JavaScript per gestire l'attributo inert
nella finestra di dialogo, assicurandomi che anche gli utenti con tastiera e screen reader non possano raggiungere la finestra di dialogo nascosta.
Assegnare alla finestra di dialogo un tema a colori adattivo
Anche se color-scheme
imposta il documento su un tema a colori adattivo fornito dal browser in base alle preferenze di sistema chiare e scure, volevo personalizzare maggiormente l'elemento della finestra di dialogo. Open Props fornisce alcuni colori
delle superfici che si adattano automaticamente alle preferenze di sistema chiare e scure, in modo simile all'utilizzo di color-scheme
. Sono ideali per creare livelli in un design e adoro utilizzare il colore per supportare visivamente questa immagine delle superfici dei livelli. Il colore di sfondo è
var(--surface-1)
; per posizionarlo sopra questo livello, utilizza var(--surface-2)
:
dialog {
…
background: var(--surface-2);
color: var(--text-1);
}
@media (prefers-color-scheme: dark) {
dialog {
border-block-start: var(--border-size-1) solid var(--surface-3);
}
}
In un secondo momento verranno aggiunti altri colori adattabili per gli elementi secondari, come l'intestazione e il piè di pagina. Li considero un elemento extra per una finestra di dialogo, ma molto importanti per creare un design accattivante e ben progettato.
Dimensioni delle finestre di dialogo adattabili
Per impostazione predefinita, la finestra di dialogo delega le dimensioni ai contenuti, il che in genere è ottimo. Il mio obiettivo è limitare il
max-inline-size
a dimensioni leggibili (--size-content-3
= 60ch
) o al 90% della larghezza dell'area visibile. In questo modo, la finestra di dialogo non occuperà tutto lo schermo su un dispositivo mobile e non sarà così ampia su uno schermo di computer da risultare difficile da leggere. Poi aggiungo un
max-block-size
in modo che la finestra di dialogo non superi l'altezza della pagina. Ciò significa anche che dobbiamo specificare dove si trova l'area scorrevole della finestra di dialogo, nel caso in cui si tratti di un elemento della finestra di dialogo alto.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Hai notato che ho max-block-size
due volte? Il primo utilizza 80vh
, un'unità del viewport fisico. Quello che voglio davvero è mantenere la finestra di dialogo all'interno del flusso relativo per gli utenti internazionali, quindi utilizzo l'unità dvb
logica, più recente e supportata solo parzialmente nella seconda dichiarazione per quando diventa più stabile.
Posizionamento della finestra di dialogo Mega
Per facilitare il posizionamento di un elemento della finestra di dialogo, vale la pena suddividerlo in due parti: lo sfondo a schermo intero e il contenitore della finestra di dialogo. Lo sfondo devecoprire tutto, creando un effetto ombra che metta in evidenza la finestra di dialogo in primo piano e renda inaccessibili i contenuti in secondo piano. Il contenitore della finestra di dialogo è libero di centrarsi su questo sfondo e assumere la forma richiesta dai contenuti.
I seguenti stili fissano l'elemento della finestra di dialogo alla finestra, allungandolo fino a ogni canto, e utilizzano margin: auto
per centrare i contenuti:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Stili di mega finestre di dialogo mobile
Su viewport di piccole dimensioni, stilo questo mega modale a pagina intera in modo leggermente diverso. Ho impostato il margine inferiore su 0
, in modo che i contenuti della finestra di dialogo vengano visualizzati nella parte inferiore dell'area visibile. Con un paio di aggiustamenti dello stile, posso trasformare la finestra di dialogo in un
action sheet, più vicino ai pollici dell'utente:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Posizionamento della finestra di dialogo mini
Quando utilizzo un'area visibile più grande, ad esempio su un computer, ho scelto di posizionare le mini finestre di dialogo sopra l'elemento che le ha richiamate. Per farlo, ho bisogno di JavaScript. Puoi trovare la tecnica che uso qui, ma ritengo che non rientri nell'ambito di questo articolo. Senza il codice JavaScript, la mini finestra di dialogo viene visualizzata al centro dello schermo, proprio come la mega finestra di dialogo.
Dai risalto alle miniature
Infine, aggiungi un tocco alla finestra di dialogo in modo che assomigli a una superficie morbida sopra la pagina. La morbidezza viene ottenuta arrotondando gli angoli della finestra di dialogo. La profondità viene ottenuta con uno degli oggetti con ombre di Open Props, realizzati con cura:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personalizzare l'elemento pseudo sfondo
Ho scelto di lavorare molto delicatamente con lo sfondo, aggiungendo solo un effetto di sfocatura con
backdrop-filter
alla mega finestra di dialogo:
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 di applicare la transizione all'elemento di sfondo in futuro:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Elementi aggiuntivi per lo stile
Ho chiamato questa sezione "extra" perché ha più a che fare con la demo dell'elemento dialog che con l'elemento dialog in generale.
Contenimento dello scorrimento
Quando viene visualizzata la finestra di dialogo, l'utente è ancora in grado di scorrere la pagina al di sotto, cosa che non voglio:
Normalmente,
overscroll-behavior
sarebbe la mia soluzione abituale, ma secondo le specifiche,
non ha alcun effetto sulla finestra di dialogo perché non è una porta di scorrimento, ovvero non è
uno scorrevole, quindi non c'è nulla da impedire. Potrei utilizzare JavaScript per monitorare i nuovi eventi di questa guida, ad esempio "closed" e "opened", e attivare/disattivare overflow: hidden
nel documento oppure attendere che :has()
sia stabile in tutti i browser:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Ora, quando è aperta una mega finestra di dialogo, il documento HTML contiene overflow: hidden
.
Il layout <form>
Oltre a essere un elemento molto importante per raccogliere le informazioni sull'interazione dell'utente, lo utilizzo qui per impaginare gli elementi di intestazione, piè di pagina e articolo. Con questo layout intendo articolare l'articolo secondario come area scorrevole. Lo faccio con
grid-template-rows
.
All'elemento articolo viene assegnato 1fr
e il modulo stesso ha la stessa altezza massima dell'elemento di dialogo. L'impostazione di questa altezza fissa e di queste dimensioni di riga consente di limitare l'elemento articolo 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;
}
Aggiunta di uno stile alla finestra di dialogo <header>
Il ruolo di questo elemento è fornire un titolo per i contenuti della finestra di dialogo e offrire un pulsante di chiusura facile da trovare. Inoltre, viene assegnato un colore di superficie per farlo apparire come se fosse dietro i contenuti dell'articolo della finestra di dialogo. Questi requisiti richiedono un contenitore flexbox, elementi allineati verticalmente distanziati dai bordi e un po' di spaziatura e spazi per lasciare spazio al titolo e ai pulsanti di chiusura:
dialog > form > header {
display: flex;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
background: var(--surface-2);
padding-block: var(--size-3);
padding-inline: var(--size-5);
}
@media (prefers-color-scheme: dark) {
dialog > form > header {
background: var(--surface-1);
}
}
Aggiungere uno stile al pulsante di chiusura dell'intestazione
Poiché la demo utilizza i pulsanti Apri oggetti di scena, il pulsante di chiusura viene personalizzato in un pulsante con icona circolare, 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;
}
Aggiunta di stili alla finestra di dialogo <article>
L'elemento articolo ha un ruolo speciale in questa finestra di dialogo: è uno spazio destinato a essere scorrevole nel caso di una finestra di dialogo alta o lunga.
Per farlo, l'elemento del modulo principale ha stabilito alcuni valori massimi per sé stesso che forniscono vincoli per l'elemento dell'articolo da raggiungere se diventa troppo alto. Imposta overflow-y: auto
in modo che le barre di scorrimento vengano mostrate solo quando necessario,
contengono scorrimento al loro interno con overscroll-behavior: contain
e il resto
saranno stili di presentazione personalizzati:
dialog > form > article {
overflow-y: auto;
max-block-size: 100%; /* safari */
overscroll-behavior-y: contain;
display: grid;
justify-items: flex-start;
gap: var(--size-3);
box-shadow: var(--shadow-2);
z-index: var(--layer-1);
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: light) {
dialog > form > article {
background: var(--surface-1);
}
}
Aggiunta di uno stile alla finestra di dialogo <footer>
Il piè di pagina ha il compito di contenere menu di pulsanti di azione. Flexbox viene utilizzato per allineare i contenuti alla fine dell'asse in linea del piè di pagina, quindi viene applicata una spaziatura per lasciare spazio ai pulsanti.
dialog > form > footer {
background: var(--surface-2);
display: flex;
flex-wrap: wrap;
gap: var(--size-3);
justify-content: space-between;
align-items: flex-start;
padding-inline: var(--size-5);
padding-block: var(--size-3);
}
@media (prefers-color-scheme: dark) {
dialog > form > footer {
background: var(--surface-1);
}
}
Aggiungere stili al 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 con wrapping con gap
per creare spazio tra i pulsanti. Gli elementi del menu hanno un'area di a capo, 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 sono spesso animati perché entrano ed escono dalla finestra. Aggiungere un movimento di supporto alle finestre di dialogo per l'ingresso e l'uscita aiuta gli utenti a orientarsi nel flusso.
Normalmente, l'elemento di dialogo può essere animato solo in entrata, non in uscita. Questo accade perché il browser attiva/disattiva la proprietà display
sull'elemento. In precedenza, la guida impostava la visualizzazione su griglia e non la impostava mai su nessuna. In questo modo puoi animare l'animazione di avvicinamento e allontanamento.
Open Props include molte animazioni con keyframe, che semplificano e rendono leggibile l'orchestrazione. Ecco gli obiettivi dell'animazione e l'approccio stratificato che ho adottato:
- Movimento ridotto è la transizione predefinita, una semplice opacità che aumenta e diminuisce.
- Se il movimento è corretto, vengono aggiunte animazioni di scorrimento e scala.
- Il layout mobile adattabile per la finestra di dialogo mega viene modificato in modo da scorrere verso l'esterno.
Una transizione predefinita sicura e significativa
Anche se Open Props include fotogrammi chiave per l'effetto di dissolvenza in entrata e in uscita, preferisco questo approccio alle transizioni a più livelli come impostazione predefinita con animazioni dei fotogrammi chiave come potenziali upgrade. In precedenza abbiamo già impostato lo stile della visibilità della finestra di dialogo con l'opacità, orchestrando 1
o 0
a seconda dell'attributo [open]
. Per eseguire la transizione tra 0% e 100%, indica al browser la durata e il tipo di transizione graduale che preferisci:
dialog {
transition: opacity .5s var(--ease-3);
}
Aggiunta di movimento alla transizione
Se l'utente accetta il movimento, sia la finestra di dialogo mega che quella mini devono scorrere verso l'alto all'ingresso e diminuire di dimensioni all'uscita. Puoi farlo con la query mediaprefers-reduced-motion
e alcuni elementi Open Props:
@media (prefers-reduced-motion: no-preference) {
dialog {
animation: var(--animation-scale-down) forwards;
animation-timing-function: var(--ease-squish-3);
}
dialog[open] {
animation: var(--animation-slide-in-up) forwards;
}
}
Adattare l'animazione di uscita per i dispositivi mobili
Nella sezione relativa allo stile, lo stile della mega finestra di dialogo è adattato ai dispositivi mobili in modo da assomigliare di più a un riquadro di azioni, come se un piccolo pezzo di carta fosse scorrevole dalla parte inferiore dello schermo ed fosse ancora attaccato alla parte inferiore. L'animazione di chiusura con scalata non si adatta bene a questo nuovo design, ma possiamo adattarla con alcune query sui media e alcuni elementi Open Props:
@media (prefers-reduced-motion: no-preference) and @media (max-width: 768px) {
dialog[modal-mode="mega"] {
animation: var(--animation-slide-out-down) forwards;
animation-timing-function: var(--ease-squish-2);
}
}
JavaScript
Esistono diverse cose da aggiungere con JavaScript:
// dialog.js
export default async function (dialog) {
// add light dismiss
// add closing and closed events
// add opening and opened events
// add removed event
// removing loading attribute
}
Queste aggiunte derivano dal desiderio di una chiusura rapida (facendo clic sullo sfondo della finestra di dialogo), animazione e alcuni eventi aggiuntivi per una migliore tempistica di acquisizione dei dati del modulo.
Aggiunta di una dismissione della luce
Questa operazione è semplice e rappresenta un'ottima aggiunta a un elemento della finestra di dialogo che non è animato. L'interazione viene raggiunta osservando i clic sull'elemento della finestra di dialogo e sfruttando la creazione di eventi in serie per valutare su cosa è stato fatto clic. L'interazione avviene solo se si tratta dell'elemento più in alto:close()
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 dialog. Vedrai che ho anche fornito stringhe di chiusura ogni volta che chiamo la funzione da vari pulsanti per fornire alla mia applicazione il contesto dell'interazione dell'utente.
Aggiunta di eventi di chiusura e chiusi
L'elemento dialog è dotato di un evento di chiusura: viene emesso immediatamente quando viene chiamata la funzione dialog close()
. Poiché stiamo animando questo elemento, è utile avere eventi prima e dopo l'animazione, per poter acquisire i dati o reimpostare il modulo della finestra di dialogo. Lo uso qui per gestire l'aggiunta dell'attributo inert
nella finestra di dialogo chiusa e nella demo li uso per modificare l'elenco di avatar se l'utente ha inviato una nuova immagine.
Per farlo, crea due nuovi eventi denominati closing
e closed
. Poi,
ascolta l'evento di chiusura integrato nella finestra di dialogo. Da qui, imposta la finestra di dialogo su
inert
e invia l'evento closing
. L'attività successiva consiste nell'attesa del completamento dell'esecuzione delle animazioni e delle transizioni nella finestra di dialogo, quindi nell'invio dell'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 codice del componente di un messaggio popup, restituisce una promessa in base al completamento delle promesse di animazione e transizione. Ecco perché dialogClose
è una funzione asincrona; può quindi await
la promessa restituita ed eseguire tranquillamente l'evento chiuso.
Aggiunta di eventi di apertura e aperti
Questi eventi non sono facili da aggiungere perché l'elemento di dialogo integrato non fornisce un evento di apertura come accade per close. Utilizzo un MutationObserver per fornire informazioni sulla modifica degli attributi della finestra di dialogo. In questo osservatore, monitorerò le modifiche all'attributo open e gestirò gli eventi personalizzati di conseguenza.
Come abbiamo fatto per gli eventi di chiusura e chiusi, crea due nuovi eventi chiamati opening
e opened
. Se in precedenza ascoltavamo l'evento di chiusura della finestra di dialogo, questa volta utilizziamo un osservatore delle mutazioni creato per monitorare gli attributi della finestra di dialogo.
…
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
export default async function (dialog) {
…
dialogAttrObserver.observe(dialog, {
attributes: true,
})
}
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
La funzione di callback dell'osservatore delle mutazioni viene chiamata quando gli attributi della finestra di dialogo vengono modificati, fornendo l'elenco delle modifiche sotto forma di array. Esegui l'iterazione sulle modifiche degli attributi, cercando che attributeName
sia aperto. Successivamente, 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 il focus su un elemento che richiede autofocus
o sul primo elemento button
trovato nella finestra di dialogo. Infine, in modo simile all'evento di chiusura
e a quello chiuso, invia subito l'evento di apertura, attendi il completamento delle animazioni
e poi invia l'evento di apertura.
Aggiunta di un evento rimosso
Nelle applicazioni a pagina singola, le finestre di dialogo vengono spesso aggiunte e rimosse in base ai percorsi o ad altre esigenze e stati dell'applicazione. Può essere utile ripulire gli eventi o i dati quando una finestra di dialogo viene rimossa.
Puoi farlo con un altro osservatore delle mutazioni. Questa volta, anziché osservare gli attributi di un elemento di dialogo, osserveremo gli elementi secondari dell'elemento del corpo e cercheremo di rilevare 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 delle mutazioni viene chiamato ogni volta che gli elementi secondari vengono aggiunti o rimossi dal corpo del documento. Le mutazioni specifiche monitorate riguardano
removedNodes
con il
nodeName
di
una finestra di dialogo. Se una finestra di dialogo è stata rimossa, gli eventi di clic e chiusura vengono rimossi per liberare memoria e viene inviato l'evento di rimozione personalizzato.
Rimozione dell'attributo loading
Per impedire all'animazione di chiusura della finestra di dialogo di essere riprodotta quando viene aggiunta alla pagina o al caricamento della pagina, è stato aggiunto un attributo di caricamento alla finestra di dialogo. Lo script seguente attende il completamento dell'esecuzione delle animazioni della finestra di dialogo, quindi rimuove l'attributo. Ora la finestra di dialogo può essere animata in entrata e in uscita e abbiamo nascosto efficacemente un'animazione altrimenti distraente.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Scopri di più sul problema della prevenzione delle animazioni delle keyframe al caricamento della pagina qui.
Tutti insieme
Ora che abbiamo spiegato ogni sezione singolarmente, ecco dialog.js
nella sua interezza:
// custom events to be added to <dialog>
const dialogClosingEvent = new Event('closing')
const dialogClosedEvent = new Event('closed')
const dialogOpeningEvent = new Event('opening')
const dialogOpenedEvent = new Event('opened')
const dialogRemovedEvent = new Event('removed')
// track opening
const dialogAttrObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(async mutation => {
if (mutation.attributeName === 'open') {
const dialog = mutation.target
const isOpen = dialog.hasAttribute('open')
if (!isOpen) return
dialog.removeAttribute('inert')
// set focus
const focusTarget = dialog.querySelector('[autofocus]')
focusTarget
? focusTarget.focus()
: dialog.querySelector('button').focus()
dialog.dispatchEvent(dialogOpeningEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogOpenedEvent)
}
})
})
// track deletion
const dialogDeleteObserver = new MutationObserver((mutations, observer) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(removedNode => {
if (removedNode.nodeName === 'DIALOG') {
removedNode.removeEventListener('click', lightDismiss)
removedNode.removeEventListener('close', dialogClose)
removedNode.dispatchEvent(dialogRemovedEvent)
}
})
})
})
// wait for all dialog animations to complete their promises
const animationsComplete = element =>
Promise.allSettled(
element.getAnimations().map(animation =>
animation.finished))
// click outside the dialog handler
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
const dialogClose = async ({target:dialog}) => {
dialog.setAttribute('inert', '')
dialog.dispatchEvent(dialogClosingEvent)
await animationsComplete(dialog)
dialog.dispatchEvent(dialogClosedEvent)
}
// page load dialogs setup
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
dialog.addEventListener('close', dialogClose)
dialogAttrObserver.observe(dialog, {
attributes: true,
})
dialogDeleteObserver.observe(document.body, {
attributes: false,
subtree: false,
childList: true,
})
// remove loading attribute
// prevent page load @keyframes playing
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Utilizzo del modulo dialog.js
La funzione esportata dal modulo si aspetta di essere chiamata e di ricevere un elemento di dialogo per cui vuoi 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 sottoposte ad upgrade con la dismissione rapida, correzioni al caricamento delle animazioni e altri eventi con cui lavorare.
Ascoltare i nuovi eventi personalizzati
Ogni elemento della finestra di dialogo di cui è stato eseguito l'upgrade ora può ascoltare cinque nuovi eventi, ad esempio:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Ecco due esempi di gestione di questi eventi:
const dialogOpening = ({target:dialog}) => {
console.log('Dialog opening', dialog)
}
const dialogClosed = ({target:dialog}) => {
console.log('Dialog closed', dialog)
console.info('Dialog user action:', dialog.returnValue)
if (dialog.returnValue === 'confirm') {
// do stuff with the form values
const dialogFormData = new FormData(dialog.querySelector('form'))
console.info('Dialog form data', Object.fromEntries(dialogFormData.entries()))
// then reset the form
dialog.querySelector('form')?.reset()
}
}
Nella demo che ho creato con l'elemento di dialogo, utilizzo l'evento di chiusura e i dati del modulo per aggiungere un nuovo elemento avatar all'elenco. I tempi sono buoni in quanto la finestra di dialogo ha completato l'animazione di uscita e alcuni script si animano nel nuovo avatar. Grazie ai nuovi eventi, l'orchestrazione dell'esperienza utente può essere più semplice.
Avviso dialog.returnValue
: contiene la stringa di chiusura passata quando viene chiamato l'evento dialog close()
. Nell'evento dialogClosed
è fondamentale sapere se la finestra di dialogo è stata chiusa, annullata o confermata. Se la verifica va a buon fine, lo script acquisisce i valori del modulo e lo reimposta. Il ripristino è utile perché quando la finestra di dialogo viene visualizzata di nuovo, è vuota e pronta per un nuovo invio.
Conclusione
Ora che sai come ho fatto, come faresti? 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web.
Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.
Remix della community
- @GrimLink con una dialogo 3 in 1.
- @mikemai2awesome con un bel
remix che non modifica la proprietà
display
. - @geoffrich_ con Svelte e un bel miglioramento di Svelte FLIP.
Risorse
- Codice sorgente su GitHub
- Avatar Doodle