Créer un composant de boîte de dialogue

Présentation de base sur la création de mini-modales et de méga-modales adaptées aux couleurs, responsives et accessibles avec l'élément <dialog>.

Dans cet article, je souhaite partager mes réflexions sur la façon de créer des mini-modaux et des méga-modaux adaptatifs aux couleurs, responsifs et accessibles avec l'élément <dialog>. Essayez la démo et consultez la source.

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

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

Présentation

L'élément <dialog> est idéal pour les informations ou les actions contextuelles sur une page. Réfléchissez aux cas où l'expérience utilisateur peut bénéficier d'une action sur la même page plutôt que d'une action sur plusieurs pages : peut-être parce que le formulaire est petit ou que la seule action requise de l'utilisateur est de confirmer ou d'annuler.

L'élément <dialog> est récemment devenu stable dans les navigateurs :

Browser Support

  • Chrome: 37.
  • Edge: 79.
  • Firefox: 98.
  • Safari: 15.4.

Source

J'ai constaté qu'il manquait quelques éléments à l'élément. Dans ce défi d'interface utilisateur, j'ajoute les éléments d'expérience du développeur que j'attends : des événements supplémentaires, la fermeture légère, des animations personnalisées, ainsi qu'un type mini et mega.

Annoter

Les éléments essentiels d'un élément <dialog> sont modestes. L'élément sera automatiquement masqué et des styles intégrés permettront de superposer votre contenu.

<dialog>
  …
</dialog>

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

Traditionnellement, un élément de boîte de dialogue partage de nombreux points communs avec une boîte modale, et les noms sont souvent interchangeables. Je me suis permis d'utiliser l'élément de boîte de dialogue pour les petites boîtes de dialogue pop-up (mini) et les boîtes de dialogue en plein écran (méga). Je les ai appelés "méga" et "mini", avec des dialogues 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 boîtes de dialogue mini et méga dans les thèmes clair et sombre.

Pas toujours, mais en général, les éléments de boîte de dialogue sont utilisés pour recueillir des informations sur l'interaction. Les formulaires à l'intérieur des éléments de boîte de dialogue sont conçus pour fonctionner ensemble. Il est conseillé d'encapsuler le contenu de votre boîte de dialogue dans un élément de formulaire afin que JavaScript puisse accéder aux données saisies par l'utilisateur. De plus, les boutons à l'intérieur 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>

Méga boîte de dialogue

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 informations et les champs de saisie des formulaires. 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 comporte autofocus et un gestionnaire d'événements intégré onclick. L'attribut autofocus sera sélectionné lorsque la boîte de dialogue s'ouvrira. Je trouve qu'il est préférable de le placer sur le bouton "Annuler" plutôt que sur le bouton "Confirmer". Cela permet de s'assurer 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, à la différence qu'elle ne contient pas d'élément <header>. Cela lui permet d'être plus petit et plus intégré.

<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 qui peut collecter des données et des interactions utilisateur. Ces éléments essentiels peuvent donner lieu à des interactions très intéressantes et efficaces sur votre site ou dans votre application.

Accessibilité

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

Restaurer la sélection

Comme nous l'avons fait manuellement dans Créer un composant de navigation latérale, il est important que l'ouverture et la fermeture d'un élément mettent correctement l'accent sur les boutons d'ouverture et de fermeture correspondants. Lorsque la barre de navigation latérale s'ouvre, le bouton de fermeture est sélectionné. Lorsque l'utilisateur appuie sur le bouton de fermeture, le focus est rétabli sur le bouton qui a ouvert la boîte de dialogue.

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é.

Piéger la mise au point

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 de la sélection d'un élément, auquel cas il l'intercepte et le remet en place.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 112.
  • Safari: 15.5.

Source

Après inert, toutes les parties du document peuvent être "gelées" de sorte qu'elles ne soient plus des cibles de sélection ni interactives avec une souris. Au lieu de piéger la sélection, elle est guidée vers la seule partie interactive du document.

Ouvrir et sélectionner automatiquement un élément

Par défaut, l'élément de boîte de dialogue attribue le focus au premier élément pouvant être sélectionné dans le balisage de la boîte de dialogue. Si cet élément n'est pas le meilleur pour l'utilisateur par défaut, utilisez l'attribut autofocus. Comme décrit précédemment, je trouve qu'il est préférable de placer cela sur le bouton "Annuler" et non sur le bouton "Confirmer". Cela permet de s'assurer que la confirmation est délibérée et non accidentelle.

Fermer avec la touche Échap

Il est important de permettre aux utilisateurs de fermer facilement cet élément potentiellement intrusif. Heureusement, l'élément de boîte de dialogue gère la touche Échap pour vous, ce qui vous libère de la charge d'orchestration.

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 choisis la voie difficile pour fournir des animations personnalisées pour l'ouverture et la fermeture de la boîte de dialogue, en prenant le contrôle de la propriété display et plus encore.

Ajouter des styles avec Open Props

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 normalize et des boutons, qui sont 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émo, sans avoir besoin de beaucoup de styles pour la prendre en charge et la rendre esthétique.

Appliquer un style à l'élément <dialog>

Propriété d'affichage

Le comportement par défaut d'affichage et de masquage d'un élément de boîte de dialogue bascule la propriété d'affichage de block à none. Malheureusement, cela signifie qu'il ne peut pas être animé à l'intérieur et à l'extérieur, mais uniquement à l'intérieur. Je souhaite animer l'entrée et la sortie, et la première étape consiste à définir ma propre propriété display :

dialog {
  display: grid;
}

En modifiant et en possédant donc la valeur de la propriété d'affichage, comme indiqué dans l'extrait CSS ci-dessus, une quantité considérable de styles doit être gérée afin de faciliter 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 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 de la boîte de dialogue, en m'assurant que les utilisateurs du clavier et du lecteur d'écran ne peuvent pas non plus accéder à la boîte de dialogue masquée.

Donner à la boîte de dialogue un thème de couleur adaptatif

Méga-boîte de dialogue affichant les thèmes clair et sombre, et illustrant les couleurs de surface.

Alors que color-scheme permet à votre document d'adopter un thème de couleur adaptatif fourni par le navigateur en fonction des préférences système claires et sombres, je souhaitais personnaliser l'élément de boîte de dialogue davantage. Open Props fournit quelques couleurs de surface qui s'adaptent automatiquement aux préférences système claires et sombres, comme avec color-scheme. Elles sont idéales pour créer des calques dans un design, et j'aime utiliser la couleur pour soutenir visuellement l'apparence des surfaces des calques. La couleur d'arrière-plan est var(--surface-1). Pour la 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);
  }
}

Des couleurs plus 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 un plus pour un élément de boîte de dialogue, mais ils sont vraiment importants pour créer une 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 une bonne chose. Mon objectif ici est de contraindre le 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 de bord à bord sur un appareil mobile et ne sera pas trop large sur un écran d'ordinateur au point d'être 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 où se trouve 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;
}

Vous avez remarqué 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 le flux relatif, pour les utilisateurs internationaux. J'utilise donc l'unité dvb logique, plus récente et partiellement prise en charge dans la deuxième déclaration pour quand elle deviendra plus stable.

Positionnement des méga-boîtes 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. L'arrière-plan doit tout couvrir, en fournissant un effet d'ombre pour indiquer que cette boîte de dialogue se trouve au 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 cette toile de 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'étirent à chaque angle et utilisent margin: auto pour centrer le contenu :

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Styles de méga-boîtes de dialogue pour mobile

Sur les petites fenêtres d'affichage, je stylise ce méga modal en plein écran 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. En ajustant le style, je peux transformer la boîte de dialogue en feuille d'action, 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;
  }
}

Capture d&#39;écran des outils de développement superposant l&#39;espacement des marges sur le méga-dialogue pour ordinateur et mobile lorsqu&#39;il est ouvert.

Positionnement des mini-boîtes de dialogue

Lorsque j'utilise une fenêtre d'affichage plus grande, comme sur un ordinateur de bureau, je choisis 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 JavaScript, la mini-boîte de dialogue s'affiche au centre de l'écran, tout comme la méga-boîte de dialogue.

Maximisez l'impact

Enfin, ajoutez une touche d'élégance à la boîte de dialogue pour qu'elle ressemble à une surface douce située loin au-dessus de la page. La douceur est obtenue en arrondissant les coins de la boîte de dialogue. La profondeur est obtenue grâce à l'une des propriétés d'ombre Open Props soigneusement conçues :

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 au méga-dialogue :

Browser Support

  • Chrome: 76.
  • Edge: 79.
  • Firefox: 103.
  • Safari: 18.

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 autorisent la transition de l'élément d'arrière-plan à l'avenir :

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

Capture d&#39;écran du méga-dialogue superposé à un arrière-plan flouté d&#39;avatars colorés.

Extras de style

J'appelle cette section "extras" car elle concerne davantage la démo de mon élément de boîte de dialogue que l'élément de boîte de dialogue en général.

Contenir le défilement

Lorsque la boîte de dialogue s'affiche, l'utilisateur peut toujours faire défiler la page en arrière-plan, ce que je ne souhaite pas :

Normalement, overscroll-behavior serait ma solution habituelle, mais selon la spécification, 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 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 "closed" et "opened", et activer/désactiver overflow: hidden sur le document, ou attendre que :has() soit stable dans tous les navigateurs :

Browser Support

  • Chrome: 105.
  • Edge: 105.
  • Firefox: 121.
  • Safari: 15.4.

Source

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

Désormais, lorsqu'une méga boîte de dialogue 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 définir les éléments d'en-tête, de pied de page et d'article. Avec cette mise en page, j'ai l'intention d'articuler l'enfant de l'article en tant que zone de défilement. Pour ce faire, j'utilise grid-template-rows. L'élément article reçoit 1fr et le formulaire lui-même a la même hauteur maximale que l'élément boîte de dialogue. La définition de cette hauteur et de cette taille de ligne fixes permet à l'élément article d'être contraint et de défiler lorsqu'il dépasse la limite :

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 d'offrir 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 et espacés jusqu'à leurs bords, ainsi qu'à des marges intérieures et des espaces pour donner de la place 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 des informations sur la mise en page Flexbox à l&#39;en-tête de la boîte de dialogue.

Personnaliser le bouton de fermeture de l'en-tête

Étant donné que la démo utilise les boutons Open Props, le bouton de fermeture est personnalisé en une icône ronde centrée 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;
}

Capture d&#39;écran des outils pour les développeurs Chrome affichant des 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 spécial 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 maximums 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 qu'en cas de besoin, limitez le défilement à l'intérieur avec overscroll-behavior: contain, et le reste sera constitué de 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 est utilisé pour aligner le contenu à la fin de l'axe en ligne du pied de page, puis un espacement est ajouté pour donner de l'espace 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);
  }
}

Capture d&#39;écran de Chrome DevTools superposant des informations sur la mise en page Flexbox à 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 d'habillage avec gap pour fournir de l'espace entre les boutons. Les éléments de menu ont une marge intérieure, comme <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 de Chrome DevTools superposant des informations sur la mise en page 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. L'ajout d'un mouvement de soutien pour l'entrée et la sortie des boîtes de dialogue aide les utilisateurs à s'orienter dans le flux.

Normalement, l'élément de boîte de dialogue ne peut être animé qu'à l'entrée, et non à la sortie. En effet, le navigateur active ou désactive la propriété display de l'élément. Auparavant, le guide définissait l'affichage sur "grille" et jamais sur "aucun". Cela permet d'animer l'entrée et la sortie.

Open Props est fourni avec de nombreuses animations de keyframes à utiliser, ce qui facilite l'orchestration et la rend lisible. Voici les objectifs d'animation et l'approche par calques que j'ai adoptés :

  1. La transition "Mouvement réduit" est la transition par défaut. Il s'agit d'un simple fondu en entrée et en sortie de l'opacité.
  2. Si le mouvement est correct, des animations de glissement et de mise à l'échelle sont ajoutées.
  3. La mise en page mobile responsive de la méga boîte de dialogue est ajustée pour être affichée.

Une transition par défaut sûre et pertinente

Bien qu'Open Props soit fourni avec des images clés pour les transitions d'entrée et de sortie, je préfère cette approche de transitions par défaut, avec des animations d'images clés comme améliorations potentielles. Nous avons déjà stylisé la visibilité de la boîte de dialogue avec l'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 d'interpolation souhaités :

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

Ajouter du mouvement à la transition

Si l'utilisateur accepte les animations, les boîtes de dialogue méga et mini doivent glisser vers le haut à l'entrée et se réduire à la sortie. Pour ce faire, utilisez la requête média prefers-reduced-motion et quelques propriétés 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 sur le style, le style de la méga-boîte de dialogue est adapté aux appareils mobiles pour ressembler davantage à une feuille d'action, comme si un petit morceau de papier avait glissé du bas de l'écran et y était toujours attaché. L'animation de sortie avec mise à l'échelle ne s'adapte pas bien à cette nouvelle conception. Nous pouvons l'adapter avec quelques requêtes média et des propriétés 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

Vous pouvez ajouter de nombreux é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 découlent du souhait d'autoriser la fermeture légère (en cliquant sur l'arrière-plan de la boîte de dialogue), l'animation et certains événements supplémentaires pour mieux synchroniser l'obtention des données du formulaire.

Ajouter une fermeture légère

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 observant les clics sur l'élément de boîte de dialogue et en tirant parti de la propagation d'événements pour évaluer ce sur quoi l'utilisateur a cliqué. Elle ne close() 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')
}

Avis 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 éléments JavaScript pour obtenir des informations sur la façon dont la boîte de dialogue a été fermée. Vous verrez que j'ai également fourni des chaînes proches chaque fois que j'appelle la fonction à partir de différents boutons, afin de fournir du contexte à mon application concernant l'interaction de l'utilisateur.

Ajouter des événements de fermeture et fermés

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. Comme nous animons cet élément, il est utile d'avoir des événements avant et après l'animation, pour qu'un changement puisse 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 sur la boîte de dialogue fermée. Dans la démo, je les 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é de 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 à 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 basée sur l'exécution 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 en toute confiance à l'événement de fermeture.

Ajouter des événements d'ouverture et ouverts

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 d'ouverture comme il le fait avec la fermeture. J'utilise un MutationObserver pour obtenir des informations sur les modifications des attributs de la boîte de dialogue. Dans cet observateur, je vais surveiller les modifications apportées à l'attribut "open" et gérer 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 appelés opening et opened. Alors que nous écoutions auparavant l'événement de fermeture de la boîte de dialogue, nous allons cette fois utiliser 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 sera appelée lorsque les attributs de la boîte de dialogue seront modifiés, en fournissant la liste des modifications sous forme de tableau. Parcourez les modifications d'attributs et recherchez l'ouverture de attributeName. Ensuite, vérifiez si l'élément possède l'attribut ou non. Cela vous indique si la boîte de dialogue s'est ouverte ou non. S'il a été ouvert, supprimez l'attribut inert, puis définissez le focus 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 l'événement d'ouverture immédiatement, attendez la fin des animations, puis envoyez 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 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 d'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 observé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é est distribué.

Supprimer l'attribut de chargement

Pour empêcher l'animation de sortie de la boîte de dialogue de se lancer 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 des animations de la boîte de dialogue, puis supprime l'attribut. La boîte de dialogue peut désormais être animée pour apparaître et disparaître, et nous avons effectivement masqué une animation qui aurait pu être distrayante.

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

Pour en savoir plus sur le problème lié à l'empêchement des animations de keyframes lors du chargement de la page, cliquez ici.

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 auquel ces nouveaux événements et fonctionnalités doivent être ajoutés :

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Et voilà ! Les deux boîtes de dialogue sont mises à niveau avec la fermeture légère, les corrections de chargement d'animation et davantage 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 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émo 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 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.

Notice dialog.returnValue : contient la chaîne de fermeture transmise lorsque l'événement close() de la boîte de dialogue est appelé. Il est essentiel de savoir si la boîte de dialogue a été fermée, annulée ou confirmée lors de l'événement dialogClosed. Si la confirmation est validée, le script récupère les valeurs du formulaire et le réinitialise. La réinitialisation est utile pour que la boîte de dialogue soit vide et prête à recevoir une nouvelle saisie lorsqu'elle s'affiche à nouveau.

Conclusion

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

Diversifions nos approches et découvrons toutes les façons de créer sur le Web.

Créez une démo, tweetez-moi les liens et je l'ajouterai à la section des remix de la communauté ci-dessous !

Remix de la communauté

Ressources