Présentation des principes de base pour créer des mini et méga boîtes de dialogue modales adaptatives aux couleurs, responsives et accessibles à l'aide de l'élément <dialog>
.
Dans cet article, je souhaite partager mes réflexions sur la façon de créer des mini et méga boîtes de dialogue modales adaptatives aux couleurs, responsives et accessibles à l'aide de l'élément <dialog>
.
Essayez la démo et consultez la source.
Si vous préférez regarder une vidéo, voici une version YouTube de cet article:
Présentation
L'élément <dialog>
est idéal pour les informations ou actions contextuelles sur la page. Déterminez quand l'expérience utilisateur peut bénéficier d'une action sur une même page plutôt que d'une action multipage: peut-être parce que le formulaire est petit ou que la seule action requise de l'utilisateur est la confirmation ou l'annulation.
L'élément <dialog>
est récemment devenu stable dans tous les navigateurs:
J'ai constaté que l'élément manquait de quelques éléments. C'est pourquoi, dans ce défi d'interface utilisateur, j'ajoute les éléments d'expérience pour les développeurs que j'attends: des événements supplémentaires, une fermeture légère, des animations personnalisées et un type mini et méga.
Annoter
Les éléments essentiels d'un élément <dialog>
sont modestes. L'élément est automatiquement masqué et comporte des styles intégrés pour superposer votre contenu.
<dialog>
…
</dialog>
Nous pouvons améliorer cette référence.
Traditionnellement, un élément de boîte de dialogue partage beaucoup avec un modal, et les noms sont souvent interchangeables. Je me suis permis d'utiliser l'élément de boîte de dialogue à la fois pour les petites boîtes de dialogue pop-up (mini) et les boîtes de dialogue pleine page (méga). Je les ai nommées "mega" et "mini", et les deux boîtes de dialogue ont été légèrement adaptées à différents cas d'utilisation.
J'ai ajouté un attribut modal-mode
pour vous permettre de spécifier le type:
<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>
Pas toujours, mais généralement, les éléments de boîte de dialogue sont utilisés pour recueillir certaines informations sur les interactions. Les formulaires dans les éléments de boîte de dialogue sont conçus pour fonctionner ensemble.
Il est recommandé de faire en sorte qu'un élément de formulaire encapsule le contenu de la boîte de dialogue afin que JavaScript puisse accéder aux données saisies par l'utilisateur. De plus, les boutons d'un formulaire utilisant method="dialog"
peuvent fermer une boîte de dialogue sans JavaScript et transmettre des données.
<dialog id="MegaDialog" modal-mode="mega">
<form method="dialog">
…
<button value="cancel">Cancel</button>
<button value="confirm">Confirm</button>
</form>
</dialog>
Boîte de dialogue Mega
Une méga boîte de dialogue comporte trois éléments dans le formulaire : <header>
, <article>
et <footer>
.
Ils servent de conteneurs sémantiques, ainsi que de cibles de style pour la présentation de la boîte de dialogue. L'en-tête donne un titre à la fenêtre modale et propose un bouton de fermeture. Cet article concerne les entrées et les informations dans les formulaires. Le pied de page contient une <menu>
de boutons d'action.
<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>
Le premier bouton de menu contient autofocus
et un gestionnaire d'événements intégré onclick
. L'attribut autofocus
recevra la sélection lorsque la boîte de dialogue s'ouvrira. Je pense qu'il est préférable de le placer sur le bouton d'annulation, et non sur le bouton de confirmation. Cela garantit que la confirmation est délibérée et non accidentelle.
Mini-boîte de dialogue
La mini-boîte de dialogue est très semblable à la méga-boîte de dialogue, il ne manque qu'un élément <header>
. Cela permet de le réduire et de l'intégrer plus facilement dans le texte.
<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'élément de boîte de dialogue fournit une base solide pour un élément de vue d'ensemble complet pouvant collecter des données et des interactions utilisateur. Ces éléments essentiels peuvent créer des interactions très intéressantes et puissantes sur votre site ou dans votre application.
Accessibilité
L'élément de boîte de dialogue offre une accessibilité intégrée très bonne. Au lieu d'ajouter ces fonctionnalités comme je le fais habituellement, beaucoup d'entre elles sont déjà présentes.
Restaurer la mise au point
Comme nous l'avons fait manuellement dans Créer un composant de panneau latéral, il est important que l'ouverture et la fermeture d'un élément mettent correctement en surbrillance les boutons d'ouverture et de fermeture appropriés. Lorsque ce panneau latéral s'ouvre, le focus est placé sur le bouton de fermeture. Lorsque vous appuyez sur le bouton de fermeture, le focus est rétabli sur le bouton qui l'a ouvert.
Avec l'élément de boîte de dialogue, il s'agit d'un comportement par défaut intégré:
Malheureusement, si vous souhaitez animer l'ouverture et la fermeture de la boîte de dialogue, cette fonctionnalité est perdue. Dans la section JavaScript, je vais restaurer cette fonctionnalité.
Mise au point bloquée
L'élément de boîte de dialogue gère inert
pour vous dans le document. Avant inert
, JavaScript était utilisé pour surveiller la sortie du focus d'un élément, auquel cas il l'intercepte et le remet en place.
Après inert
, toutes les parties du document peuvent être "figées", de sorte qu'elles ne soient plus des cibles de focus ni interactives avec une souris. Au lieu de piéger le focus, il est guidé vers la seule partie interactive du document.
Ouvrir un élément et mettre la mise au point automatique
Par défaut, l'élément de boîte de dialogue attribue la sélection au premier élément sélectionnable dans le balisage de la boîte de dialogue. Si ce n'est pas l'élément le plus approprié pour l'utilisateur, utilisez l'attribut autofocus
. Comme décrit précédemment, je pense qu'il est préférable de placer cette information sur le bouton "Annuler" et non sur le bouton de confirmation. Cela garantit que la confirmation est délibérée et non accidentelle.
Fermer avec la touche Échap
Il est important de faciliter la fermeture de cet élément potentiellement gênant. Heureusement, l'élément de boîte de dialogue gère la touche d'échappage à votre place, ce qui vous évite d'avoir à orchestrer la tâche.
Styles
Il existe une méthode simple et une méthode difficile pour styliser l'élément de boîte de dialogue. La méthode simple consiste à ne pas modifier la propriété d'affichage de la boîte de dialogue et à travailler avec ses limites. Je prends le chemin difficile pour fournir des animations personnalisées pour l'ouverture et la fermeture de la boîte de dialogue, la prise en charge de la propriété display
et plus encore.
Styling avec des props ouverts
Pour accélérer les couleurs adaptatives et la cohérence globale de la conception, j'ai sans vergogne intégré ma bibliothèque de variables CSS Open Props. En plus des variables fournies sans frais, j'importe également un fichier de normalisation et des boutons, tous deux fournis par Open Props en tant qu'importations facultatives. Ces importations m'aident à me concentrer sur la personnalisation de la boîte de dialogue et de la démonstration, sans avoir besoin de beaucoup de styles pour la prendre en charge et lui donner un aspect agréable.
Styliser l'élément <dialog>
Posséder la propriété d'affichage
Le comportement d'affichage et de masquage par défaut d'un élément de boîte de dialogue bascule la propriété d'affichage de block
à none
. Cela signifie malheureusement qu'il ne peut pas être animé ni en entrée ni en sortie, mais uniquement en entrée. Je souhaite animer l'entrée et la sortie. La première étape consiste à définir ma propre propriété display:
dialog {
display: grid;
}
En modifiant et en devenant propriétaire de la valeur de la propriété d'affichage, comme indiqué dans l'extrait de code CSS ci-dessus, une quantité considérable de styles doit être gérée afin de faciliter l'expérience utilisateur appropriée. Tout d'abord, l'état par défaut d'une boîte de dialogue est fermé. Vous pouvez représenter cet état visuellement et empêcher la boîte de dialogue de recevoir des interactions avec les styles suivants:
dialog:not([open]) {
pointer-events: none;
opacity: 0;
}
La boîte de dialogue est désormais invisible et ne peut pas être utilisée lorsqu'elle n'est pas ouverte. Plus tard, j'ajouterai du code JavaScript pour gérer l'attribut inert
dans la boîte de dialogue, en m'assurant que les utilisateurs de clavier et de lecteur d'écran ne peuvent pas non plus accéder à la boîte de dialogue masquée.
Attribuer à la boîte de dialogue un thème de couleur adaptatif
Bien que color-scheme
définisse votre document sur un thème de couleur adaptatif fourni par le navigateur en fonction des préférences système claires et sombres, je voulais personnaliser davantage l'élément de boîte de dialogue. Open Props fournit quelques couleurs de surface qui s'adaptent automatiquement aux préférences système claires et sombres, comme pour utiliser color-scheme
. Ils sont parfaits pour créer des calques dans une conception, et j'aime utiliser la couleur pour renforcer visuellement cette apparence des surfaces de calque. La couleur d'arrière-plan est var(--surface-1)
. Pour se superposer à ce calque, utilisez 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);
}
}
D'autres couleurs adaptatives seront ajoutées ultérieurement pour les éléments enfants, tels que l'en-tête et le pied de page. Je les considère comme des éléments supplémentaires pour un élément de boîte de dialogue, mais ils sont vraiment importants pour créer une conception de boîte de dialogue attrayante et bien conçue.
Dimensionnement des boîtes de dialogue responsives
Par défaut, la boîte de dialogue délègue sa taille à son contenu, ce qui est généralement très bien. Mon objectif ici est de limiter la max-inline-size
à une taille lisible (--size-content-3
= 60ch
) ou à 90% de la largeur de la fenêtre d'affichage. Cela garantit que la boîte de dialogue ne s'étend pas de bord à bord sur un appareil mobile et qu'elle n'est pas si large sur un écran d'ordinateur qu'elle est difficile à lire. J'ajoute ensuite un max-block-size
pour que la boîte de dialogue ne dépasse pas la hauteur de la page. Cela signifie également que nous devrons spécifier l'emplacement de la zone de défilement de la boîte de dialogue, au cas où il s'agirait d'un élément de boîte de dialogue de grande taille.
dialog {
…
max-inline-size: min(90vw, var(--size-content-3));
max-block-size: min(80vh, 100%);
max-block-size: min(80dvb, 100%);
overflow: hidden;
}
Notez que j'ai utilisé max-block-size
deux fois. Le premier utilise 80vh
, une unité de vue physique. Ce que je souhaite vraiment, c'est que la boîte de dialogue reste dans le flux relatif, pour les utilisateurs internationaux. Je vais donc utiliser l'unité dvb
logique, plus récente et uniquement partiellement prise en charge dans la deuxième déclaration pour qu'elle devienne plus stable.
Positionnement de la boîte de dialogue Mega
Pour vous aider à positionner un élément de boîte de dialogue, il est utile de diviser ses deux parties: le fond d'écran en plein écran et le conteneur de la boîte de dialogue. L'arrière-plan doit tout recouvrir, en fournissant un effet d'ombre pour indiquer que cette boîte de dialogue est en premier plan et que le contenu en arrière-plan est inaccessible. Le conteneur de boîte de dialogue est libre de se centrer sur ce fond et de prendre la forme requise par son contenu.
Les styles suivants fixent l'élément de boîte de dialogue à la fenêtre, l'étirant dans chaque coin, et utilisent margin: auto
pour centrer le contenu:
dialog {
…
margin: auto;
padding: 0;
position: fixed;
inset: 0;
z-index: var(--layer-important);
}
Styles de boîtes de dialogue méga pour mobile
Sur les petits écrans, je stylise cette méga fenêtre modale pleine page un peu différemment. J'ai défini la marge inférieure sur 0
, ce qui place le contenu de la boîte de dialogue en bas de la fenêtre d'affichage. Avec quelques ajustements de style, je peux transformer la boîte de dialogue en une actionsheet, plus proche des pouces de l'utilisateur:
@media (max-width: 768px) {
dialog[modal-mode="mega"] {
margin-block-end: 0;
border-end-end-radius: 0;
border-end-start-radius: 0;
}
}
Positionnement de la mini-boîte de dialogue
Lorsque vous utilisez un viewport plus grand, comme sur un ordinateur de bureau, j'ai choisi de positionner les mini-boîtes de dialogue au-dessus de l'élément qui les a appelées. Pour ce faire, j'ai besoin de JavaScript. Vous trouverez la technique que j'utilise ici, mais je pense qu'elle dépasse le cadre de cet article. Sans le code JavaScript, la mini-boîte de dialogue s'affiche au centre de l'écran, comme la méga-boîte de dialogue.
Maximiser l'impact
Enfin, ajoutez un peu de style à la boîte de dialogue pour qu'elle ressemble à une surface douce située bien au-dessus de la page. La douceur est obtenue en arrondissant les coins de la boîte de dialogue. La profondeur est obtenue avec l'un des accessoires d'ombre soigneusement conçus par Open Props:
dialog {
…
border-radius: var(--radius-3);
box-shadow: var(--shadow-6);
}
Personnaliser le pseudo-élément "backdrop"
J'ai choisi de travailler très légèrement avec l'arrière-plan, en ajoutant uniquement un effet de flou avec backdrop-filter
à la méga boîte de dialogue:
dialog[modal-mode="mega"]::backdrop {
backdrop-filter: blur(25px);
}
J'ai également choisi de mettre une transition sur backdrop-filter
, dans l'espoir que les navigateurs permettront de faire la transition de l'élément de fond dans le futur:
dialog::backdrop {
transition: backdrop-filter .5s ease;
}
Accessoires de style
J'ai appelé cette section "suppléments", car elle concerne davantage ma démonstration de l'élément de boîte de dialogue que l'élément de boîte de dialogue en général.
Confinement du défilement
Lorsque la boîte de dialogue s'affiche, l'utilisateur peut toujours faire défiler la page derrière elle, ce que je ne souhaite pas:
Normalement, overscroll-behavior
serait ma solution habituelle, mais selon les spécifications, elle n'a aucun effet sur la boîte de dialogue, car il ne s'agit pas d'un port de défilement, c'est-à-dire qu'il ne s'agit pas d'un défilement, donc il n'y a rien à empêcher. Je pourrais utiliser JavaScript pour surveiller les nouveaux événements de ce guide, tels que "fermé" et "ouvert", et activer/désactiver overflow: hidden
sur le document, ou attendre que :has()
soit stable dans tous les navigateurs:
html:has(dialog[open][modal-mode="mega"]) {
overflow: hidden;
}
Désormais, lorsqu'une boîte de dialogue Mega est ouverte, le document HTML contient overflow: hidden
.
Mise en page <form>
En plus d'être un élément très important pour collecter les informations d'interaction de l'utilisateur, je l'utilise ici pour mettre en page les éléments d'en-tête, de pied de page et d'article. Avec cette mise en page, je souhaite articuler l'article enfant en tant que zone à faire défiler. Je l'obtiens avec grid-template-rows
.
L'élément article est défini sur 1fr
, et le formulaire lui-même a la même hauteur maximale que l'élément de boîte de dialogue. Définir cette hauteur et cette taille de ligne fixes permet de contraindre l'élément article et de le faire défiler lorsqu'il déborde:
dialog > form {
display: grid;
grid-template-rows: auto 1fr auto;
align-items: start;
max-block-size: 80vh;
max-block-size: 80dvb;
}
Attribuer un style à la boîte de dialogue <header>
Le rôle de cet élément est de fournir un titre au contenu de la boîte de dialogue et de proposer un bouton de fermeture facile à trouver. Une couleur de surface lui est également attribuée pour qu'il semble se trouver derrière le contenu de l'article de la boîte de dialogue. Ces exigences conduisent à un conteneur flexbox, des éléments alignés verticalement espacés sur leurs bords, ainsi qu'une marge intérieure et des espaces pour laisser de l'espace au titre et aux boutons de fermeture:
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);
}
}
Attribuer un style au bouton de fermeture de l'en-tête
Étant donné que la démonstration utilise les boutons "Open Props" (Ouvrir les accessoires), le bouton de fermeture est personnalisé en un bouton circulaire centré sur une icône, comme suit:
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;
}
Attribuer un style à la boîte de dialogue <article>
L'élément article joue un rôle particulier dans cette boîte de dialogue: il s'agit d'un espace destiné à être défilé dans le cas d'une boîte de dialogue haute ou longue.
Pour ce faire, l'élément de formulaire parent a établi des valeurs maximales pour lui-même, qui fournissent des contraintes à atteindre pour cet élément d'article s'il devient trop grand. Définissez overflow-y: auto
pour que les barres de défilement ne s'affichent que si nécessaire, qu'elles contiennent un défilement avec overscroll-behavior: contain
et que le reste soit des styles de présentation personnalisés:
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);
}
}
Attribuer un style à la boîte de dialogue <footer>
Le pied de page a pour fonction de contenir des menus de boutons d'action. Flexbox permet d'aligner le contenu à la fin de l'axe en ligne du pied de page, puis d'ajouter un espace pour laisser de la place aux boutons.
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);
}
}
Mettre en forme le menu du pied de page de la boîte de dialogue
L'élément menu
permet de contenir les boutons d'action de la boîte de dialogue. Il utilise une mise en page flexbox avec gap
pour laisser un espace entre les boutons. Les éléments de menu ont une marge intérieure, comme un <ul>
. Je supprime également ce style, car je n'en ai pas besoin.
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;
}
Animation
Les éléments de boîte de dialogue sont souvent animés, car ils entrent et sortent de la fenêtre. Ajouter un mouvement de soutien aux boîtes de dialogue pour cette entrée et cette sortie aide les utilisateurs à s'orienter dans le flux.
En règle générale, l'élément de boîte de dialogue ne peut être animé que vers l'intérieur, et non vers l'extérieur. En effet, le navigateur active/désactive la propriété display
sur l'élément. Auparavant, le guide définissait l'affichage sur "grille" et ne le définissait jamais sur "aucun". Vous pouvez ainsi animer l'entrée et la sortie.
Open Props est fourni avec de nombreuses animations de clés-images, ce qui facilite et rend l'orchestration lisible. Voici les objectifs d'animation et l'approche en couches que j'ai adoptée:
- Le mouvement réduit est la transition par défaut, qui consiste en un simple fondu d'entrée et de sortie de l'opacité.
- Si le mouvement est correct, des animations de diapositive et de mise à l'échelle sont ajoutées.
- La mise en page mobile responsive de la boîte de dialogue méga est ajustée pour glisser vers l'extérieur.
Une transition par défaut sûre et pertinente
Bien que Open Props soit fourni avec des images clés pour l'apparition et la disparition, je préfère cette approche multicouche des transitions par défaut, avec des animations d'images clés comme mises à niveau potentielles. Nous avons déjà stylisé la visibilité de la boîte de dialogue avec une opacité, en orchestrant 1
ou 0
en fonction de l'attribut [open]
. Pour effectuer une transition entre 0% et 100%, indiquez au navigateur la durée et le type de transition que vous souhaitez:
dialog {
transition: opacity .5s var(--ease-3);
}
Ajouter du mouvement à la transition
Si l'utilisateur accepte le mouvement, les boîtes de dialogue méga et mini doivent glisser vers le haut à leur arrivée et se réduire à leur sortie. Pour ce faire, utilisez la requête multimédia prefers-reduced-motion
et quelques 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;
}
}
Adapter l'animation de sortie pour mobile
Plus tôt dans la section "Style", le style de la méga-fenêtre de dialogue est adapté aux appareils mobiles pour ressembler davantage à une bottom sheet, comme si un petit morceau de papier avait glissé vers le haut depuis le bas de l'écran et était toujours attaché en bas. L'animation de sortie en agrandissement ne convient pas bien à cette nouvelle conception. Nous pouvons l'adapter à l'aide de quelques requêtes multimédias et de quelques éléments 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
Il y a beaucoup de choses à ajouter avec 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
}
Ces ajouts découlent du désir d'une fermeture légère (en cliquant sur l'arrière-plan de la boîte de dialogue), d'une animation et de quelques événements supplémentaires pour mieux synchroniser l'obtention des données du formulaire.
Ajouter une option de fermeture de la fenêtre
Cette tâche est simple et constitue un excellent ajout à un élément de boîte de dialogue qui n'est pas animé. L'interaction est obtenue en surveillant les clics sur l'élément de boîte de dialogue et en exploitant le bourrage d'événements pour évaluer sur quoi l'utilisateur a cliqué. Elle ne close()
que s'il s'agit de l'élément le plus élevé:
export default async function (dialog) {
dialog.addEventListener('click', lightDismiss)
}
const lightDismiss = ({target:dialog}) => {
if (dialog.nodeName === 'DIALOG')
dialog.close('dismiss')
}
Notez dialog.close('dismiss')
. L'événement est appelé et une chaîne est fournie.
Cette chaîne peut être récupérée par d'autres scripts JavaScript pour obtenir des informations sur la façon dont la boîte de dialogue a été fermée. Vous constaterez que j'ai également fourni des chaînes proches chaque fois que j'appelle la fonction à partir de différents boutons, afin de fournir un contexte à mon application concernant l'interaction utilisateur.
Ajouter des événements de fermeture et de fermeture
L'élément de boîte de dialogue est accompagné d'un événement de fermeture: il émet immédiatement lorsque la fonction close()
de la boîte de dialogue est appelée. Étant donné que nous animons cet élément, il est utile d'avoir des événements avant et après l'animation, pour pouvoir récupérer les données ou réinitialiser le formulaire de boîte de dialogue. Je l'utilise ici pour gérer l'ajout de l'attribut inert
dans la boîte de dialogue fermée. Dans la démonstration, je l'utilise pour modifier la liste des avatars si l'utilisateur a envoyé une nouvelle image.
Pour ce faire, créez deux événements nommés closing
et closed
. Écoutez ensuite l'événement de fermeture intégré dans la boîte de dialogue. Définissez la boîte de dialogue sur inert
et distribuez l'événement closing
. La tâche suivante consiste à attendre la fin de l'exécution des animations et des transitions dans la boîte de dialogue, puis à distribuer l'événement 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 fonction animationsComplete
, qui est également utilisée dans la section Créer un composant Toast, renvoie une promesse en fonction de l'achèvement des promesses d'animation et de transition. C'est pourquoi dialogClose
est une fonction asynchrone. Elle peut ensuite await
la promesse renvoyée et passer à l'événement fermé en toute confiance.
Ajouter des événements d'ouverture et d'ouverture
L'ajout de ces événements n'est pas aussi simple, car l'élément de boîte de dialogue intégré ne fournit pas d'événement d'ouverture comme il le fait pour la fermeture. J'utilise un MutationObserver pour fournir des insights sur les modifications des attributs de la boîte de dialogue. Dans cet observateur, je surveillerai les modifications apportées à l'attribut "open" et gérerai les événements personnalisés en conséquence.
Comme nous l'avons fait pour les événements de fermeture et de fermeture, créez deux événements nommés opening
et opened
. Là où nous écoutions auparavant l'événement de fermeture de la boîte de dialogue, utilisez cette fois un observateur de mutation créé pour surveiller les attributs de la boîte de dialogue.
…
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 fonction de rappel de l'observateur de mutation est appelée lorsque les attributs de la boîte de dialogue sont modifiés. Elle fournit la liste des modifications sous forme de tableau. Itérez sur les modifications d'attributs, en recherchant l'attributeName
ouvert. Vérifiez ensuite si l'élément possède l'attribut ou non. Cela indique si la boîte de dialogue est ouverte ou non. S'il a été ouvert, supprimez l'attribut inert
, définissez la sélection sur un élément demandant autofocus
ou le premier élément button
trouvé dans la boîte de dialogue. Enfin, comme pour les événements de fermeture et de fermeture, diffusez immédiatement l'événement d'ouverture, attendez la fin des animations, puis diffusez l'événement d'ouverture.
Ajouter un événement supprimé
Dans les applications à page unique, les boîtes de dialogue sont souvent ajoutées et supprimées en fonction des routes ou d'autres besoins et états de l'application. Il peut être utile d'effacer des événements ou des données lorsqu'une boîte de dialogue est supprimée.
Pour ce faire, vous pouvez utiliser un autre observateur de mutation. Cette fois, au lieu d'observer les attributs d'un élément de boîte de dialogue, nous allons observer les enfants de l'élément body et surveiller les éléments de boîte de dialogue supprimés.
…
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)
}
})
})
})
Le rappel de l'observateur de mutation est appelé chaque fois que des enfants sont ajoutés ou supprimés du corps du document. Les mutations spécifiques surveillées concernent les removedNodes
qui ont le nodeName
d'une boîte de dialogue. Si une boîte de dialogue a été supprimée, les événements de clic et de fermeture sont supprimés pour libérer de la mémoire, et l'événement de suppression personnalisée est distribué.
Supprimer l'attribut de chargement
Pour empêcher l'animation de sortie de la boîte de dialogue de s'exécuter lorsqu'elle est ajoutée à la page ou lors du chargement de la page, un attribut de chargement a été ajouté à la boîte de dialogue. Le script suivant attend la fin de l'exécution des animations de la boîte de dialogue, puis supprime l'attribut. La boîte de dialogue peut désormais s'animer et se fermer, et nous avons efficacement masqué une animation qui aurait pu être gênante.
export default async function (dialog) {
…
await animationsComplete(dialog)
dialog.removeAttribute('loading')
}
Pour en savoir plus sur le problème d'empêchement des animations de clés-images au chargement de la page, consultez cette page.
Tous ensemble
Voici dialog.js
dans son intégralité, maintenant que nous avons expliqué chaque section individuellement:
// 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')
}
Utiliser le module dialog.js
La fonction exportée du module s'attend à être appelée et à recevoir un élément de boîte de dialogue qui souhaite que ces nouveaux événements et fonctionnalités soient ajoutés:
import GuiDialog from './dialog.js'
const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')
GuiDialog(MegaDialog)
GuiDialog(MiniDialog)
Les deux boîtes de dialogue sont ainsi mises à niveau avec une fermeture légère, des corrections de chargement d'animation et plus d'événements à utiliser.
Écouter les nouveaux événements personnalisés
Chaque élément de boîte de dialogue mis à niveau peut désormais écouter cinq nouveaux événements, comme suit:
MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)
MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)
MegaDialog.addEventListener('removed', dialogRemoved)
Voici deux exemples de gestion de ces événements:
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()
}
}
Dans la démonstration que j'ai créée avec l'élément de boîte de dialogue, j'utilise cet événement fermé et les données du formulaire pour ajouter un élément d'avatar à la liste. Le timing est bon, car la boîte de dialogue a terminé son animation de sortie, puis certains scripts animent le nouvel avatar. Grâce aux nouveaux événements, l'orchestration de l'expérience utilisateur peut être plus fluide.
Remarque dialog.returnValue
: contient la chaîne de fermeture transmise lorsque l'événement close()
de la boîte de dialogue est appelé. Dans l'événement dialogClosed
, il est essentiel de savoir si la boîte de dialogue a été fermée, annulée ou confirmée. Si la confirmation est effectuée, le script récupère les valeurs du formulaire et le réinitialise. La réinitialisation est utile pour que, lorsque la boîte de dialogue s'affiche à nouveau, elle soit vide et prête à être envoyée.
Conclusion
Maintenant que vous savez comment j'ai fait, comment procéderiez-vous ? 🙂
Diversifions nos approches et découvrons toutes les façons de créer sur le Web.
Créez une démo, tweetez-moi des liens et je les ajouterai à la section "Remix de la communauté" ci-dessous.
Remix de la communauté
- @GrimLink avec une boîte de dialogue 3 en 1.
- @mikemai2awesome avec un joli remix qui ne modifie pas la propriété
display
. - @geoffrich_ avec Svelte et un beau Svelte FLIP.
Ressources
- Code source sur GitHub
- Avatars Doodle