Créer un composant de boîte de dialogue

Présentation générale de la création de mini et méga-modals adaptables aux couleurs, responsifs et accessibles avec l'élément <dialog>.

Dans cet article, je vais vous expliquer comment créer des mini et méga-modals adaptables aux couleurs, responsifs et accessibles avec l'élément <dialog>. Essayez la démonstration et consultez la source.

Démonstration des mégas et mini-boîtes de dialogue dans leurs thèmes clair et sombre.

Si vous préférez la vidéo, voici une version YouTube de cet article:

Présentation

L'élément <dialog> est idéal pour les actions ou les informations contextuelles intégrées sur la page. Déterminez à quel moment l'expérience utilisateur peut bénéficier d'une action sur une même page plutôt que sur plusieurs pages: peut-être parce que le formulaire est de taille réduite ou que la seule action requise de la part de l'utilisateur est de confirmer ou d'annuler.

L'élément <dialog> est depuis peu stable dans tous les navigateurs:

Navigateurs pris en charge

  • 37
  • 79
  • 98
  • 15,4

Source

J'ai constaté que l'élément manquait quelques éléments. Dans ce défi de l'IUG, j'ajoute les éléments d'expérience de développement attendus: des événements supplémentaires, un effet de suppression de la lumière, des animations personnalisées, ainsi qu'un mini-type et un méga-type.

Markup

Les éléments de base d'un élément <dialog> sont simples. L'élément est automatiquement masqué et des styles sont intégrés pour se superposer à votre contenu.

<dialog>
  …
</dialog>

Nous pouvons améliorer cette référence.

Traditionnellement, un élément de boîte de dialogue a de nombreuses similitudes avec un modal, et les noms sont souvent interchangeables. Je me suis permis d'utiliser l'élément de boîte de dialogue pour les petites fenêtres pop-up de boîte de dialogue (mini) et les boîtes de dialogue en pleine page (méga). Je les ai nommés "mega" et "mini", les deux dialogues étant légèrement adaptés à 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>

Capture d&#39;écran des mini et méga boîtes de dialogue dans les thèmes clair et sombre.

Pas toujours, mais les éléments de boîte de dialogue sont généralement utilisés pour collecter des informations sur les interactions. Les formulaires dans les éléments de boîte de dialogue sont conçus pour s'assembler. Il est recommandé d'utiliser un élément de formulaire encapsulant 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 qui utilisent 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>

Méga boîte de dialogue

Une méga boîte de dialogue contient trois éléments : <header>, <article> et <footer>. Ceux-ci servent de conteneurs sémantiques et de cibles de style pour la présentation de la boîte de dialogue. L'en-tête intitule la fenêtre modale et propose un bouton de fermeture. Cet article concerne les entrées et informations de formulaire. Le pied de page contient un <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 affiche autofocus et un gestionnaire d'événements intégré onclick. L'attribut autofocus sera sélectionné lorsque la boîte de dialogue sera ouverte. Il est recommandé 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, mais il lui manque simplement un élément <header>. Cela lui permet d'être plus petit et plus aligné.

<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 constitue une base solide pour un élément de fenêtre d'affichage complet capable de collecter des données et des interactions utilisateur. Ces éléments essentiels peuvent donner lieu à des interactions très intéressantes et puissantes sur votre site ou votre application.

Accessibilité

L'élément de boîte de dialogue offre une très bonne accessibilité intégrée. Au lieu d'ajouter ces caractéristiques comme je le fais habituellement, beaucoup sont déjà là.

Restauration de la mise au point...

Comme nous l'avons fait dans la section Créer un composant de navigation latérale, il est important que l'ouverture et la fermeture d'un élément se concentrent sur les boutons d'ouverture et de fermeture pertinents. Lorsque ce panneau de navigation s'ouvre, le curseur est placé sur le bouton de fermeture. Lorsque l'utilisateur a appuyé sur le bouton de fermeture, le curseur est restauré sur le bouton qui l'a ouvert.

Pour l'élément de boîte de dialogue, il s'agit du comportement intégré par défaut:

Malheureusement, si vous souhaitez animer la boîte de dialogue à l'intérieur et à l'extérieur, cette fonctionnalité sera perdue. Je vais restaurer cette fonctionnalité dans la section JavaScript.

Reprise de la mise au point

L'élément de boîte de dialogue gère inert pour vous sur le document. Avant inert, JavaScript était utilisé pour surveiller la sortie d'un élément, auquel il intercepte et le replace.

Navigateurs pris en charge

  • 102
  • 102
  • 112
  • 15.5

Source

Après inert, toutes les parties du document peuvent être "figées" de sorte qu'elles ne sont plus des cibles d'intérêt ou qu'elles interagissent avec la souris. Au lieu de piéger le curseur, celui-ci est placé dans la seule partie interactive du document.

Ouvrir un élément et effectuer la mise au point automatique

Par défaut, l'élément de boîte de dialogue sélectionne le premier élément sélectionnable dans le balisage de la boîte de dialogue. Si ce n'est pas le meilleur élément à utiliser par défaut pour l'utilisateur, utilisez l'attribut autofocus. Comme décrit précédemment, il est recommandé de le placer sur le bouton d'annulation plutôt que 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 intrusive. Heureusement, l'élément de boîte de dialogue gère automatiquement la touche d'échappement, ce qui vous évite d'avoir à gérer l'orchestration.

Styles

Il existe un moyen simple de styliser l'élément de boîte de dialogue et un chemin d'accès difficile. Le parcours facile est obtenu en ne modifiant pas la propriété d'affichage de la boîte de dialogue et en utilisant ses limites. Je m'efforce de fournir des animations personnalisées pour ouvrir et fermer la boîte de dialogue, en reprenant la propriété display, etc.

Appliquer un style avec des accessoires ouverts

Pour accélérer les couleurs adaptatives et la cohérence globale de la conception, j'ai ajouté sans honte à 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, qui fournissent tous deux des 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 les prendre en charge et les rendre attrayantes.

Appliquer un style à 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é à l'intérieur et à l'extérieur, mais seulement à l'intérieur. J'aimerais effectuer des animations en entrée et en sortie. La première étape consiste à définir ma propre propriété display:

dialog {
  display: grid;
}

En modifiant, et donc en détenant la valeur de la propriété d'affichage, comme indiqué dans l'extrait CSS ci-dessus, vous devez gérer un nombre considérable de styles afin d'offrir une 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 il n'est pas possible d'interagir avec elle lorsqu'elle n'est pas ouverte. Par la suite, j'ajouterai du code JavaScript pour gérer l'attribut inert de la boîte de dialogue, afin que les utilisateurs de claviers et de lecteurs d'écran ne puissent pas accéder à la boîte de dialogue masquée.

Attribuer à la boîte de dialogue un thème de couleurs adaptatifs

Méga boîte de dialogue montrant les thèmes clair et sombre, montrant les couleurs de la surface.

Bien que color-scheme active votre document dans un thème de couleurs adaptatif fourni par le navigateur aux préférences système claires et sombres, je voulais personnaliser davantage l'élément de boîte de dialogue. Open Props propose quelques couleurs de surface qui s'adaptent automatiquement aux préférences système claires et sombres, de la même manière que pour color-scheme. Ils sont parfaits pour créer des calques dans une conception et j'aime utiliser de la couleur pour rehausser visuellement l'apparence des surfaces des calques. La couleur d'arrière-plan est var(--surface-1). Pour vous placer au-dessus de 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 supplémentaires en tant qu'éléments de dialogue, mais c'est très important pour créer une conception de dialogues convaincante et bien conçue.

Taille de la boîte de dialogue responsive

Par défaut, la boîte de dialogue délègue sa taille à son contenu, ce qui est généralement recommandé. Mon objectif ici est de limiter 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'affichera pas d'un bord à l'autre sur un appareil mobile, et ne sera pas si large sur un écran d'ordinateur qu'elle sera 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 devons spécifier l'emplacement de la zone déroulante de la boîte de dialogue, au cas où il s'agissait 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;
}

Vous voyez que j'ai max-block-size deux fois ? La première utilise 80vh, une unité de fenêtre d'affichage physique. Ce que je veux vraiment, c'est que la boîte de dialogue reste dans un flux relatif, pour les utilisateurs internationaux. J'utilise donc l'unité dvb logique, plus récente et partiellement compatible dans la deuxième déclaration, quand elle devient plus stable.

Positionnement de la méga-boîte de dialogue

Pour vous aider à positionner un élément de boîte de dialogue, il est utile de décomposer ses deux parties: l'arrière-plan en plein écran et le conteneur de boîte de dialogue. Le fond doit couvrir tout, fournissant un effet d'ombre pour faire en sorte que cette boîte de dialogue soit au premier plan et que le contenu situé derrière soit inaccessible. Le conteneur de boîte de dialogue est libre de se centrer sur ce fond et de prendre la forme dont son contenu a besoin.

Les styles suivants fixent l'élément de boîte de dialogue à la fenêtre, l'étirent 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îte de dialogue "Méga" mobiles

Pour les petites fenêtres d'affichage, le style de cette mégamodale pleine page est légèrement différent. Je définis la marge inférieure sur 0, ce qui ramène le contenu de la boîte de dialogue au bas de la fenêtre d'affichage. En apportant quelques ajustements de style, je peux transformer la boîte de dialogue en feuille d'actions, plus près 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;
  }
}

Capture d&#39;écran des outils de développement superposant l&#39;espacement des marges sur la méga boîte de dialogue pour ordinateur et mobile lorsqu&#39;elle est ouverte.

Positionnement de la mini-boîte de dialogue

Lorsque vous utilisez une fenêtre d'affichage plus grande, par exemple sur un ordinateur de bureau, j'ai choisi de positionner les mini boîtes de dialogue sur l'élément qui les a appelées. Pour cela, j'ai besoin de JavaScript. Vous pouvez consulter la technique que j'utilise ici, mais je pense qu'elle dépasse le cadre de cet article. Sans JavaScript, la mini boîte de dialogue s'affiche au centre de l'écran, tout comme la grande boîte de dialogue.

Faites ressortir votre audience

Enfin, ajoutez du style à la boîte de dialogue pour qu'elle ressemble à une surface souple située bien au-dessus de la page. La douceur est obtenue en arrondissant les coins des dialogues. La profondeur est obtenue avec l'un des accessoires d'ombres soigneusement conçus d'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 le fond, en ajoutant simplement un effet de flou avec backdrop-filter à la grande boîte de dialogue:

Navigateurs pris en charge

  • 76
  • 79
  • 103
  • 9

Source

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

J'ai également choisi d'ajouter une transition à backdrop-filter, dans l'espoir que les navigateurs autoriseront la transition de l'élément Backdrop à l'avenir:

dialog::backdrop {
  transition: backdrop-filter .5s ease;
}

Capture d&#39;écran de la méga boîte de dialogue superposée à un arrière-plan flouté avec des avatars colorés.

Bonus de style

J'appelle cette section "extras", car elle est davantage liée à la démonstration de l'élément de boîte de dialogue qu'à l'élément de boîte de dialogue en général.

Confinement du défilement

Lorsque la boîte de dialogue est affichée, l'utilisateur peut toujours faire défiler la page située derrière, ce que je ne veux pas:

Normalement, overscroll-behavior serait ma solution habituelle, mais selon les spécifications, cela 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 conteneur de défilement. Il n'y a donc rien à empêcher. Je pourrais utiliser JavaScript pour surveiller les nouveaux événements de ce guide, tels que "closed" (fermé) et "opened", et activer/désactiver overflow: hidden sur le document, ou attendre que :has() soit stable dans tous les navigateurs:

Navigateurs pris en charge

  • 105
  • 105
  • 121
  • 15,4

Source

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

Désormais, lorsqu'une grande boîte de dialogue est ouverte, le document HTML contient overflow: hidden.

La 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 l'en-tête, le pied de page et les éléments d'article. Avec cette mise en page, j'ai l'intention d'articuler l'enfant de l'article en tant que zone à faire défiler. J'y parviens avec grid-template-rows. L'élément d'article reçoit 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 fermes permettent de contraindre l'élément de l'article et de le faire défiler lorsqu'il dépasse:

dialog > form {
  display: grid;
  grid-template-rows: auto 1fr auto;
  align-items: start;
  max-block-size: 80vh;
  max-block-size: 80dvb;
}

Capture d&#39;écran des outils de développement superposant les informations de mise en page en grille sur les lignes.

Appliquer un style à la boîte de dialogue <header>

Le rôle de cet élément est de fournir un titre pour le 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'elle semble se trouver derrière le contenu de l'article de la boîte de dialogue. Ces exigences impliquent l'utilisation d'un conteneur Flexbox, d'éléments alignés verticalement et espacés par rapport à leurs bords, ainsi que d'une marge intérieure et d'espaces vides pour donner 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);
  }
}

Capture d&#39;écran des outils pour les développeurs Chrome superposant les informations de mise en page Flexbox sur l&#39;en-tête de la boîte de dialogue.

Appliquer 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 centré sur une icône ronde, comme ceci:

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;
}

Capture d&#39;écran des outils pour les développeurs Chrome superposant les informations sur la taille et la marge intérieure du bouton de fermeture de l&#39;en-tête.

Appliquer un style à la boîte de dialogue <article>

L'élément article a un rôle particulier dans cette boîte de dialogue: il s'agit d'un espace destiné à être fait défiler dans le cas d'une boîte de dialogue grande ou longue.

Pour ce faire, l'élément de formulaire parent a établi lui-même des limites qui fournissent des contraintes à cet élément d'article à atteindre s'il devient trop haut. Définissez overflow-y: auto pour que les barres de défilement ne s'affichent qu'en cas de besoin, incluez le défilement à l'intérieur avec overscroll-behavior: contain, et le reste aura 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);
  }
}

Le rôle du pied de page est de contenir des menus de boutons d'action. Flexbox permet d'aligner le contenu à la fin de l'axe aligné du pied de page, puis d'espacer les 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);
  }
}

Capture d&#39;écran des outils pour les développeurs Chrome superposant les informations de mise en page Flexbox sur l&#39;élément de pied de page.

L'élément menu permet de contenir les boutons d'action de la boîte de dialogue. Il utilise une mise en page Flexbox encapsulante avec gap pour laisser de l'espace entre les boutons. Les éléments de menu comportent 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;
}

Capture d&#39;écran des outils pour les développeurs Chrome superposant les informations Flexbox sur les éléments du menu de pied de page.

Animation

Les éléments de boîte de dialogue sont souvent animés, car ils entrent et sortent de la fenêtre. Intégrer dans les boîtes de dialogue un mouvement de soutien à l'entrée et à la sortie permet aux utilisateurs de s'orienter dans le flux.

Normalement, 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 ou désactive la propriété display sur l'élément. Auparavant, le guide a défini l'affichage sur une grille, mais jamais sur "Aucune". Cela débloque la capacité à effectuer des animations à l'intérieur et à l'extérieur.

Open Props propose de nombreuses animations d'images clés, ce qui facilite l'orchestration et la lisibilité. Voici les objectifs de l'animation et l'approche multicouche que j'ai adoptée:

  1. Le mouvement réduit est la transition par défaut, un fondu à l'ouverture et à la fermeture d'opacité simple.
  2. Si le mouvement est autorisé, des animations de glissement et de mise à l'échelle sont ajoutées.
  3. La mise en page responsive pour mobile de la méga boîte de dialogue est ajustée pour glisser vers l'extérieur.

Une transition par défaut sécurisée et efficace

Bien qu'Open props fournisse des images clés pour un fondu à l'ouverture et à la fermeture, je préfère cette approche multicouche de transitions par défaut, avec des animations d'images clés comme mises à niveau potentielles. Précédemment, nous avons stylisé la visibilité de la boîte de dialogue avec l'opacité, en orchestrant 1 ou 0 en fonction de l'attribut [open]. Pour passer de 0% à 100%, indiquez au navigateur la durée et le type de lissage de vitesse que vous souhaitez appliquer:

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

Ajouter du mouvement à la transition

Si l'utilisateur est d'accord avec les mouvements, les boîtes de dialogue "méga" et "mini" doivent glisser vers le haut lors de l'entrée et s'adapter à la sortie. Pour ce faire, utilisez la requête média prefers-reduced-motion et quelques propositions ouvertes:

@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 aux appareils mobiles

Plus tôt dans la section consacrée aux styles, le style de la boîte de dialogue "méga" a été adapté pour les appareils mobiles afin de ressembler davantage à une feuille d'action, comme si une petite feuille de papier glissant du bas de l'écran vers le haut est toujours attachée au bas de l'écran. L'animation de sortie du scaling horizontal ne s'adapte pas bien à cette nouvelle conception. Nous pouvons l'adapter avec quelques requêtes média et certaines propositions ouvertes:

@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

Vous devez ajouter plusieurs éléments 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 sont dus à la volonté d'un abandon léger (en cliquant sur l'arrière-plan de la boîte de dialogue), d'animations et d'autres événements permettant de réduire le délai d'obtention des données de formulaire.

Ajout de la fonctionnalité Ignorer

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 utilisant l'événement bouillonnant pour évaluer ce qui a fait l'objet d'un clic. close() ne s'affichera que s'il s'agit de l'élément le plus haut:

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 fichiers JavaScript pour savoir comment la boîte de dialogue a été fermée. Vous trouverez également des chaînes fermées chaque fois que j'appelle la fonction à partir de différents boutons, afin de fournir du contexte à mon application sur l'interaction utilisateur.

Ajouter des événements de fermeture et de fermeture

L'élément de boîte de dialogue est fourni avec un événement de fermeture: il est émis 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 judicieux d'avoir des événements avant et après l'animation, afin de permettre la récupération des données ou la réinitialisation de la boîte de dialogue. Je l'utilise ici pour gérer l'ajout de l'attribut inert sur la boîte de dialogue fermée. Dans la démonstration, je les utilise pour modifier la liste d'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. À partir de là, définissez la boîte de dialogue sur inert et envoyez l'événement closing. La tâche suivante consiste à attendre la fin de l'exécution des animations et des transitions sur la boîte de dialogue, puis à envoyer 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, également utilisée dans le composant de création d'un toast, renvoie une promesse basée sur l'achèvement de l'animation et des promesses de transition. C'est pourquoi dialogClose est une fonction asynchrone. Elle peut alors await renvoyer la promesse et passer en toute confiance à l'événement fermé.

Ajouter des événements d'ouverture et d'ouverture

Ces événements ne sont pas aussi faciles à ajouter, car l'élément de boîte de dialogue intégré ne fournit pas d'événement ouvert comme il le fait pour la fonction de fermeture. J'utilise MutationObserver pour fournir des insights sur la modification des attributs de la boîte de dialogue. Dans cet observateur, je surveille les modifications apportées à l'attribut "open" et je gère les événements personnalisés en conséquence.

De la même manière que nous avons démarré les événements de fermeture et de fermeture, créez deux événements appelés opening et opened. Lorsque nous avons précédemment écouté l'événement de fermeture de la boîte de dialogue, cette fois, nous utilisons 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, en fournissant la liste des modifications sous forme de tableau. Itérez les modifications d'attributs en recherchant que attributeName est 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 et placez le curseur sur un élément demandant autofocus ou sur le premier élément button trouvé dans la boîte de dialogue. Enfin, comme pour les événements de fermeture et de fermeture, envoyez immédiatement l'événement d'ouverture, attendez la fin des animations, puis envoyez l'événement ouvert.

Ajouter un événement supprimé

Dans les applications monopages, des 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 de nettoyer les événements ou les 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 sur un élément de boîte de dialogue, nous allons observer les enfants de l'élément "body" et surveiller la suppression des éléments de boîte de dialogue.

…
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 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 personnalisé supprimé est envoyé.

Supprimer l'attribut de chargement

Un attribut de chargement a été ajouté à la boîte de dialogue pour empêcher la lecture de son animation de sortie lorsqu'elle est ajoutée à la page ou lors du chargement de la page. Le script suivant attend la fin de l'exécution des animations de boîte de dialogue, puis supprime l'attribut. Les animations peuvent désormais apparaître librement dans la boîte de dialogue, et nous avons masqué une animation autrement gênante.

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

Découvrez comment empêcher l'animation d'images clés lors du chargement de la page.

L'union fait la force

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 transmise à un élément de boîte de dialogue qui souhaite ajouter ces nouveaux événements et fonctionnalités:

import GuiDialog from './dialog.js'

const MegaDialog = document.querySelector('#MegaDialog')
const MiniDialog = document.querySelector('#MiniDialog')

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

De la même manière, les deux boîtes de dialogue ont été mises à niveau avec une fermeture légère, des correctifs de chargement de l'animation et davantage d'événements à traiter.

É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 ceci:

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 nouvel élément avatar à la liste. Le timing est bon, car la boîte de dialogue a terminé son animation de sortie, puis certains scripts s'animent dans le nouvel avatar. Grâce aux nouveaux événements, l'expérience utilisateur peut être plus fluide.

Notez dialog.returnValue: ce fichier contient la chaîne de fermeture transmise lors de l'appel de l'événement close() de la boîte de dialogue. 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 c'est confirmé, 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 pour un nouvel envoi.

Conclusion

Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 😃

Diversifiez nos approches et découvrons toutes les manières de créer des applications sur le Web.

Créez une démo, envoyez-moi des tweets via des liens et je l'ajouterai à la section "Remix de la communauté" ci-dessous.

Remix de la communauté

Ressources