Créer un composant de boîte de dialogue

Présentation des principes de base sur la création de mini et mégamodaux adaptables aux couleurs, responsives et accessibles avec l'élément <dialog>.

Dans cet article, je souhaite partager mes réflexions sur la création de mini et mégamodaux adaptables aux couleurs, responsives et accessibles avec l'élément <dialog>. Essayez la démonstration et consultez la source.

Démonstration des grandes 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 ce post:

Présentation

L'élément <dialog> est idéal pour les actions ou les informations contextuelles sur la page. Déterminez dans quels cas l'expérience utilisateur peut bénéficier d'une action sur une même page plutôt qu'une action sur plusieurs pages: par exemple, si le formulaire est de petite taille, ou si la seule action requise de la part de l'utilisateur est "Confirmer" ou "Annuler".

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

Navigateurs pris en charge

  • 37
  • 79
  • 98
  • 15,4

Source

J'ai constaté qu'il manquait quelques éléments. C'est pourquoi, dans ce défi de l'IUG, j'ajoute les éléments attendus pour l'expérience développeur: événements supplémentaires, suppression de lumière, animations personnalisées, ainsi qu'un type mini et méga.

Markup

Les éléments essentiels d'un élément <dialog> sont simples. L'élément est automatiquement masqué et possède des styles 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 beaucoup de points en commun avec un modal, et les noms sont souvent interchangeables. J'ai pris la liberté ici d'utiliser l'élément de boîte de dialogue pour les petites fenêtres pop-up (mini) et les boîtes de dialogue pleine page (méga). Je les ai nommés "méga" et "mini", avec les deux boîtes de dialogue 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>

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

Pas toujours, mais généralement, des éléments de boîte de dialogue sont utilisés pour recueillir des informations sur les interactions. Les formulaires à l'intérieur des éléments de la boîte de dialogue sont conçus pour être assemblés. Il est recommandé d'encapsuler le contenu de la boîte de dialogue à l'aide d'un élément de formulaire 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>

Méga-boîte de dialogue

Le formulaire d'une mégaboî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. L'article concerne les entrées de formulaire et les informations. 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 est sélectionné lorsque la boîte de dialogue est 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 grande boîte de dialogue, mais il manque simplement un élément <header>. Cela 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 avec les utilisateurs. Ces principes 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 dispose d'une très bonne accessibilité intégrée. Au lieu d'ajouter ces fonctionnalités comme d'habitude, beaucoup sont déjà là.

Restauration de la sélection...

Comme nous l'avons fait manuellement dans la section Créer un composant de navigation latérale, il est important que l'ouverture et la fermeture d'un élément mettent correctement en évidence les boutons d'ouverture et de fermeture pertinents. Lorsque ce panneau de navigation latérale s'ouvre, le curseur est placé sur le bouton de fermeture. Lorsque vous appuyez sur le bouton de fermeture, le curseur est à nouveau placé sur le bouton qui l'a ouvert.

Avec l'élément de boîte de dialogue, il s'agit d'un 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é est perdue. Dans la section JavaScript, je vais restaurer cette fonctionnalité.

Sélection de captage

L'élément de boîte de dialogue gère à votre place inert sur le document. Avant inert, JavaScript était utilisé pour surveiller la fermeture d'un élément, auquel cas il l'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 soient plus des cibles cibles ou qu'elles soient interactives avec la souris. Au lieu de piéger l'attention, la mise au point est dirigée vers la seule partie interactive du document.

Ouvrir un élément et effectuer le focus automatique

Par défaut, l'élément de la boîte de dialogue place le premier élément sélectionnable dans le balisage de la boîte de dialogue. Si ce n'est pas le meilleur élément à configurer par défaut pour l'utilisateur, utilisez l'attribut autofocus. Comme décrit précédemment, je trouve qu'il est recommandé de mettre cela 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.

Fermeture à l'aide de 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 clé d'échappement à votre place, ce qui vous évite la charge d'orchestration.

Styles

Il existe une méthode simple pour styliser l'élément de boîte de dialogue et une voie difficile. Pour ce faire, il faut ne pas modifier la propriété d'affichage de la boîte de dialogue et utiliser ses limites. Je vais suivre la procédure difficile pour fournir des animations personnalisées pour ouvrir et fermer la boîte de dialogue, prendre en charge la propriété display et plus encore.

Ajouter des styles avec des accessoires ouverts

Pour accélérer les couleurs adaptatives et la cohérence globale de la conception, j'ai ajouté sans difficulté Open Props à ma bibliothèque de variables CSS. En plus des variables fournies sans frais, j'importe également un fichier de normalisation et certains boutons, tous deux fournis 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 attrayante.

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

Propriétaire de la propriété display

Le comportement d'affichage et de masquage par défaut d'un élément de boîte de dialogue fait passer la propriété d'affichage de block à none. Malheureusement, cela signifie qu'il ne peut pas être animé à l'intérieur ou à l'extérieur, mais seulement à l'intérieur. J'aimerais animer la création à la fois en entrée et en sortie. La première étape consiste à définir ma propre propriété display:

dialog {
  display: grid;
}

En modifiant la valeur de la propriété display, c'est-à-dire en la propriétaire, comme indiqué dans l'extrait CSS ci-dessus, vous devez gérer une quantité considérable de styles afin de proposer 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. Par la suite, j'ajouterai du code JavaScript pour gérer l'attribut inert sur la boîte de dialogue, afin de s'assurer que les utilisateurs de clavier et de lecteur d'écran ne peuvent pas non plus accéder à la boîte de dialogue masquée.

Appliquer un thème de couleurs adaptatif à la boîte de dialogue

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

Bien que color-scheme active le thème de couleurs adaptatif fourni par le navigateur aux préférences système claires et sombres pour votre document, je voulais personnaliser davantage l'élément de la 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 avec color-scheme. Ils sont parfaits pour créer des calques dans une conception et j'adore utiliser la couleur pour prendre en charge visuellement l'apparence des surfaces des calques. La couleur d'arrière-plan est var(--surface-1). Pour reposer sur 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 plus comme un élément de dialogue, mais très 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 très utile. Mon objectif ici est de contraindre 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 bord à bord sur un appareil mobile et ne sera pas si large sur un écran d'ordinateur qu'elle soit 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, s'il s'agit 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 un flux relatif pour les utilisateurs internationaux. J'utilise donc l'unité logique, plus récente et partiellement compatible, dvb dans la deuxième déclaration, pour qu'elle devienne plus stable.

Positionnement de la 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: le fond en plein écran et le conteneur de la boîte de dialogue. Le fond doit couvrir tout, fournissant un effet d'ombre permettant de confirmer que cette boîte de dialogue s'affiche au premier plan et que le contenu derrière est inaccessible. Le conteneur de la boîte de dialogue peut se centrer sur ce fond et prendre la forme requise par son contenu.

Les styles suivants fixent l'élément de boîte de dialogue à la fenêtre, en l'étirant jusqu'à chaque coin et utilisent margin: auto pour centrer le contenu:

dialog {
  …
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Grands styles de boîtes de dialogue pour mobile

Sur les petites fenêtres d'affichage, le style de cette fenêtre mégamodale pleine page est légèrement différent. J'ai défini la marge inférieure sur 0, ce qui permet d'afficher le contenu de la boîte de dialogue en bas de la fenêtre d'affichage. Après quelques ajustements de style, je peux transformer la boîte de dialogue en une 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 qui se superposent à l&#39;espacement des marges dans la grande boîte de dialogue ouverte pour ordinateur et mobile.

Positionnement de la mini boîte de dialogue

Dans une fenêtre d'affichage plus grande, comme 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 trouver 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 apparaît au centre de l'écran, tout comme la méga-boîte de dialogue.

Mettez votre texte en avant

Enfin, ajoutez une touche de style à la boîte de dialogue afin qu'elle ressemble à une surface molle bien positionnée bien au-dessus de la page. La douceur est obtenue en arrondissant les angles de la boîte de dialogue. La profondeur est obtenue à l'aide de l'un des accessoires d'ombre soigneusement conçus d'OpenProps:

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 uniquement un effet de flou avec backdrop-filter à la grande boîte de dialogue:

Navigateurs pris en charge

  • 76
  • 17
  • 103
  • 9

Source

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

J'ai également choisi de mettre en œuvre la transition sur 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 grande boîte de dialogue superposée à un arrière-plan flouté d&#39;avatars colorés.

Options de style supplémentaires

J'appelle cette section "Extras", car elle est davantage liée à la démo de l'élément de boîte de dialogue qu'à l'élément de la 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, 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 peux utiliser JavaScript pour surveiller les nouveaux événements de ce guide, tels que "closed" (fermeture) et "opened" (ouvert), 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 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 mettre en page l'en-tête, le pied de page et les éléments de l'article. Avec cette mise en page, j'ai l'intention d'articuler l'élément enfant de l'article sous la forme d'une zone déroulante. Pour ce faire, j'utilise 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 permet de contraindre l'élément d'article et de faire défiler la page en cas de dépassement:

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 superposés aux informations de mise en page de la grille sur les lignes.

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

Le rôle de cet élément est de donner un titre au 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'elle apparaisse 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 qui sont espacés sur leurs bords, ainsi qu'à des marges intérieures et des espaces 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 de développement Chrome qui se superposent aux informations de mise en page Flexbox dans 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é dans un bouton circulaire centré 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 superposant les informations de taille et de 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 d'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 s'est fixé des limites qui limitent l'exposition de 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 lorsque cela est nécessaire. Ajoutez-y le défilement avec overscroll-behavior: contain, et le reste correspond à 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 intégré 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 de développement Chrome qui se superposent aux 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. Elle utilise une mise en page Flexbox encapsulée avec gap pour laisser de l'espace entre les boutons. Les éléments de menu comportent une marge intérieure comme <ul>. Je supprime également ce style puisque je n'en ai plus 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 qui se superposent aux informations sur la Flexbox sur les éléments du menu en pied de page.

Animation

Les éléments de boîte de dialogue sont souvent animés, car ils entrent dans la fenêtre ou la quittent. Proposer aux boîtes de dialogue des mouvements de soutien pour cette entrée et cette sortie aide les utilisateurs à s'orienter dans le flux.

Généralement, l'élément de boîte de dialogue ne peut être animé qu'à l'intérieur, et non à l'extérieur. En effet, le navigateur active/désactive la propriété display sur l'élément. Précédemment, le guide définit l'affichage sur Grille et ne le définit jamais sur "Aucun". Cela débloque la possibilité d'animer à l'intérieur et à l'extérieur.

Open Props est fourni avec de nombreuses animations d'images clés, ce qui facilite l'orchestration et la lisibilité. Voici les objectifs de l'animation et l'approche en couches que j'ai adoptées:

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

Une transition par défaut sûre et significative

Bien qu'OpenProp soit fourni avec des images clés pour le fondu d'entrée et de sortie, je préfère cette approche des transitions par couches comme option par défaut, avec les animations d'images clés comme améliorations potentielles. Précédemment, nous avons déjà stylisé la visibilité de la boîte de dialogue avec de l'opacité, en orchestrant 1 ou 0 en fonction de l'attribut [open]. Pour effectuer la transition entre 0% et 100%, indiquez au navigateur la durée et le type de lissage souhaité:

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

Ajouter du mouvement à la transition

Si l'utilisateur accepte les mouvements, les grandes boîtes de dialogue et les minis doivent toutes deux glisser vers le haut en tant qu'entrée et s'agrandir en tant que 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 pour les mobiles

Précédemment dans la section sur les styles, le style "méga boîte de dialogue" a été adapté aux appareils mobiles pour ressembler davantage à une feuille d'action, comme si une petite feuille de papier glisse du bas de l'écran tout en restant 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 à quelques requêtes média et à quelques proposes 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 un certain nombre d'é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 de la volonté d'ignorer la lumière (cliquer sur le fond de la boîte de dialogue), de l'animation et de certains événements supplémentaires visant à améliorer le timing de l'obtention des données du formulaire.

Ajout de la fonctionnalité Light Ignorer

Cette tâche simple est 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 la boîte de dialogue et en utilisant l'ébullition d'événement pour évaluer l'élément sur lequel l'utilisateur a cliqué. L'action close() n'est générée 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 un autre code 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 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 utile d'avoir des événements avant et après l'animation, afin qu'une modification récupère les données ou réinitialise le formulaire de la boîte de dialogue. Je l'utilise ici pour gérer l'ajout de l'attribut inert à 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 appelé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 de déclencher 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 la section Créer un composant de toast, renvoie une promesse basée sur l'achèvement des promesses d'animation et de transition. C'est pourquoi dialogClose est une fonction asynchrone. Elle peut alors await renvoyer la promesse renvoyée et avancer en toute confiance vers 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 avec la fermeture. J'utilise un MutationObserver pour fournir des insights sur l'évolution 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.

De la même manière que nous avons lancé les événements de fermeture et les événements fermés, créez deux événements appelés opening et opened. Alors que nous avons précédemment écouté 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, en fournissant la liste des modifications sous forme de tableau. Itérez les modifications d'attribut en recherchant que attributeName soit ouvert. Vérifiez ensuite si l'élément possède l'attribut: vous saurez ainsi 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 d'ouverture.

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 de l'état de l'application. Il peut être utile de nettoyer 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 observons les enfants de l'élément du corps et surveillons la suppression des éléments de la 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 les removedNodes dont l'élément nodeName correspond à 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 supprimé personnalisé est déclenché.

Supprimer l'attribut de chargement

Pour empêcher la lecture de l'animation de sortie de la boîte de dialogue 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. L'animation de la boîte de dialogue est désormais libre, et nous avons masqué une animation autrement distrayant.

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

Découvrez comment empêcher les animations d'images clés lors du chargement de la 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 transmise à 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)

De la même manière, les deux boîtes de dialogue sont améliorées avec des fermetures légères, des corrections de chargement d'animation et d'autres événements à gérer.

É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 d'avatar à la liste. Le timing est bon dans la mesure où 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'orchestration de l'expérience utilisateur est plus fluide.

Notez dialog.returnValue: il 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 l'opération est confirmée, le script récupère les valeurs du formulaire et le réinitialise. Cette réinitialisation permet d'afficher une boîte de dialogue vide et d'être prête pour un nouvel envoi lorsque la boîte de dialogue s'affiche à nouveau.

Conclusion

Maintenant que vous savez comment je l'ai fait, comment le feriez-vous‽ 😃 ?

Diversissons nos approches et apprenons toutes les façons de créer sur le Web.

Créez une démo, cliquez sur les liens tweet me, et je l'ajouterai à la section "Remix" de la communauté ci-dessous.

Remix de la communauté

Ressources