3 points par GN⁺ 2024-12-08 | 1 commentaires | Partager sur WhatsApp
  • L’élément HTML <dialog> permet de créer des boîtes de dialogue modales et non modales natives du navigateur ; en mode modal, le reste de la page passe à l’état inert, ce qui bloque toute interaction
  • Les méthodes d’ouverture et de fermeture de base sont showModal(), show() et close() ; un contrôle déclaratif est aussi possible via <form method="dialog">, l’Invoker Commands API et la Popover API
  • L’attribut closedby répartit les chemins de fermeture accessibles à l’utilisateur en any, closerequest et none, avec un comportement par défaut qui varie selon la manière dont la boîte de dialogue a été ouverte
  • Côté accessibilité, le focus initial et un bouton de fermeture explicite sont importants ; une boîte de dialogue ouverte avec showModal() place par défaut le focus sur le premier élément focalisable et permet la fermeture avec la touche Échap
  • Le style s’appuie sur :modal, :open et ::backdrop, et les animations nécessitent la gestion de transitions discrètes avec display, overlay, @starting-style et transition-behavior: allow-discrete

Rôle de <dialog> et comportement modal

  • L’élément HTML <dialog> sert à créer des boîtes de dialogue modales et non modales
  • Une boîte de dialogue modale bloque l’interaction avec les autres éléments de l’interface et place le reste de la page dans l’état inert
  • Une boîte de dialogue non modale permet de continuer à interagir avec le reste de la page même lorsqu’elle est ouverte

État d’ouverture et politique de fermeture

  • <dialog> prend en charge les attributs globaux, mais l’attribut tabindex ne doit pas être utilisé
    • <dialog> lui-même n’est pas un élément interactif et ne reçoit pas le focus
    • Son contenu interne et son bouton de fermeture peuvent recevoir le focus et être utilisés
  • L’attribut open indique qu’une boîte de dialogue est active et interactive
    • Sans l’attribut open, elle n’est pas visible pour l’utilisateur
    • Pour afficher une boîte de dialogue, il est recommandé d’utiliser .show() ou .showModal() plutôt que l’attribut open
    • Un <dialog> ouvert via l’attribut open est non modal
    • Il est possible de changer l’état ouvert/fermé d’une boîte de dialogue non modale en basculant l’attribut open, mais ce n’est pas recommandé
  • L’attribut closedby précise de quelles façons l’utilisateur peut fermer la boîte de dialogue
    • any : fermeture possible par les trois méthodes
      • light dismiss via un clic ou un appui en dehors de la boîte de dialogue
      • action propre à la plateforme, comme la touche Échap, le retour arrière sur mobile ou un geste de fermeture
      • méthode définie par le développeur, comme un bouton appelant HTMLDialogElement.close() ou l’envoi d’un <form>
    • closerequest : fermeture possible par action propre à la plateforme et par méthode définie par le développeur
    • none : fermeture possible uniquement par une méthode définie par le développeur
    • En l’absence de valeur closedby valide, le comportement est celui de closerequest si l’ouverture s’est faite avec showModal(), sinon celui de none

JavaScript et contrôle déclaratif

  • Il est possible de contrôler directement l’affichage et la fermeture de <dialog> en JavaScript
    • showModal() : affiche une boîte de dialogue modale
    • show() : affiche une boîte de dialogue non modale
    • close() : ferme la boîte de dialogue
    • On peut aussi la fermer en soumettant un <form> à l’intérieur de <dialog> avec method="dialog"
    • Une boîte de dialogue modale peut aussi être fermée avec la touche Échap
  • L’Invoker Commands API permet d’ouvrir et de fermer une boîte de dialogue modale via les seuls attributs d’un bouton
    • On définit les attributs commandfor et command sur un <button>
    • Les commandes disponibles pour une boîte de dialogue sont "show-modal", "close" et "request-close"
Open dialog

  This dialog was opened using an invoker command.

  Close

  • La Popover API permet d’ouvrir, fermer et basculer déclarativement une boîte de dialogue non modale
    • On ajoute l’attribut popover à <dialog> pour en faire un popover
    • On définit popovertarget et popovertargetaction sur un bouton ou un champ de saisie
    • Comme une boîte de dialogue popover est non modale, on peut la fermer en cliquant à l’extérieur
    • Si aucune valeur n’est donnée à popover, la valeur par défaut "auto" est utilisée et active le light dismiss
    • popover="manual" désactive le light dismiss et nécessite un autre mécanisme, comme un bouton de fermeture
    • Si popovertargetaction est omis, la valeur par défaut toggle est utilisée

Envoi de formulaire et gestion de fermeture

  • Toute boîte de dialogue <dialog> a besoin d’un mécanisme de fermeture, y compris sur les appareils sans clavier physique
  • Plusieurs méthodes de fermeture sont possibles
    • soumettre un <form> interne avec method="dialog"
    • cliquer en dehors de la boîte de dialogue lorsque le light dismiss est activé
    • appuyer sur Échap pour les boîtes de dialogue qui l’autorisent
    • appeler HTMLDialogElement.close()
  • method="dialog" sur un <form> ou formmethod="dialog" sur un bouton d’envoi ferme la boîte de dialogue
    • L’état des contrôles du formulaire est conservé, mais le formulaire n’est pas envoyé
    • returnValue reçoit la valeur du bouton activé
  • Dans un formulaire avec des champs obligatoires, l’agent utilisateur empêche qu’une soumission classique ferme la boîte de dialogue tant que les valeurs requises ne sont pas fournies
    • On peut contourner la validation du formulaire avec formnovalidate sur le bouton de fermeture
    • Il est aussi possible de fermer la boîte de dialogue en appelant dialog.close() en JavaScript

Focus et accessibilité

  • Lorsqu’on ouvre <dialog> avec showModal(), le focus est placé par défaut sur le premier élément focalisable imbriqué
  • Pour définir l’emplacement de focus initial le plus adapté dans une boîte de dialogue donnée, on peut utiliser l’attribut autofocus
    • S’il n’y a pas d’élément avec lequel interagir immédiatement, il est recommandé de placer autofocus sur le bouton de fermeture
    • Si la position de focus initiale est incertaine, comme dans une boîte de dialogue rendue dynamiquement, <dialog> lui-même peut constituer une position initiale appropriée
  • La solution la plus robuste consiste à inclure des boutons explicites tels que confirmer, annuler ou fermer afin que tous les utilisateurs puissent la fermer
  • Une boîte de dialogue ouverte via showModal() se ferme par défaut avec la touche Échap
    • Une boîte de dialogue non modale ne se ferme pas par défaut avec la touche Échap
    • Les utilisateurs clavier s’attendent à pouvoir fermer une boîte de dialogue modale avec Échap
    • Si plusieurs boîtes de dialogue modales sont ouvertes, la touche Échap ne doit fermer que la dernière affichée, et le navigateur fournit ce comportement avec <dialog>
  • Le composant natif <dialog> fournit des fonctions d’utilisabilité et d’accessibilité qu’il faut reproduire manuellement dans une boîte de dialogue personnalisée construite avec d’autres éléments
  • Le navigateur expose <dialog> de manière similaire à une boîte de dialogue personnalisée utilisant le rôle ARIA role="dialog"
    • Un <dialog> ouvert avec showModal() possède implicitement aria-modal="true"
    • Un <dialog> affiché via show(), l’attribut open ou une modification du display par défaut est exposé avec aria-modal="false"
    • Lorsqu’on implémente une boîte de dialogue modale, tout ce qui n’est ni <dialog> ni son contenu doit être rendu inert, ce que le navigateur gère automatiquement avec showModal()

Style CSS et arrière-plan

  • <dialog> peut être sélectionné par son nom d’élément comme n’importe quel élément standard
  • Pour un style basé sur l’état, on peut utiliser les pseudo-classes :modal et :open
  • L’arrière-plan d’une boîte de dialogue modale peut être stylé avec le pseudo-élément ::backdrop
    • Il s’applique au backdrop qui apparaît derrière <dialog> lorsqu’il est affiché avec showModal()
    • Il peut servir à flouter, assombrir ou masquer le contenu en arrière-plan devenu inert

Exemples de patterns d’utilisation

  • L’exemple avec l’Invoker Commands API ouvre la boîte de dialogue avec un bouton command="show-modal" et la ferme avec un bouton command="close"
    • Elle peut être ouverte avec le bouton « Open dialog »
    • Elle peut être fermée avec le bouton « Close » ou la touche Échap
  • L’exemple avec la Popover API ouvre et ferme une boîte de dialogue non modale avec popover, popovertarget et popovertargetaction
    • Elle peut être fermée avec le bouton « Close », la touche Échap ou une sélection en dehors de la boîte de dialogue
    • Avec popover="manual", le light dismiss est désactivé
  • L’exemple avec l’attribut open crée une boîte de dialogue non modale en HTML pur déjà ouverte au chargement de la page
    • Elle peut être fermée avec le bouton « OK » du <form>
    • Aucun mécanisme n’est prévu pour la rouvrir ensuite
    • Pour afficher une boîte de dialogue non modale, l’usage de HTMLDialogElement.show() est préférable
  • L’exemple de boîte de dialogue modale l’ouvre avec .showModal() et la ferme avec close()
    • Le fond en dégradé est stylé avec ::backdrop
    • Une fois la boîte de dialogue ouverte, tout ce qui est en dehors devient inert et il n’est plus possible d’interagir avec le document
  • L’exemple returnValue montre comment gérer la valeur de retour de la boîte de dialogue avec un formulaire et les valeurs des boutons
    • La valeur returnValue par défaut est une chaîne vide ou la valeur du bouton qui a soumis le formulaire à l’intérieur de la boîte de dialogue
    • Le bouton « Confirm » transmet la valeur choisie à close() pour l’utiliser comme valeur de retour
    • Si la boîte de dialogue est fermée avec la touche Échap, returnValue n’est pas mis à jour et l’événement close n’est pas déclenché, donc le texte du <output> n’est pas actualisé

Animations de <dialog>

  • Un <dialog> masqué a display: none, et un <dialog> affiché a display: block
  • Lors du changement d’état d’affichage, il est ajouté ou retiré de la top layer et de l’arbre d’accessibilité
  • Pour animer <dialog>, il faut que la propriété display puisse être animée
    • Les navigateurs compatibles traitent display comme une animation discrète
    • Lors du passage de none à block, la bascule vers block se fait à 0 %, ce qui rend la boîte de dialogue visible pendant toute l’animation
    • Lors du passage de block à none, la bascule vers none se fait à 100 %, ce qui la maintient visible pendant toute l’animation
  • Animation avec les transitions CSS

    • Pour animer <dialog> avec des transitions CSS, plusieurs fonctions sont nécessaires
    • @starting-style : fournit les valeurs de départ de la transition à chaque ouverture de la boîte de dialogue
    • transition de display : maintient la boîte de dialogue sur une valeur display visible pendant la transition
    • transition de overlay : retarde le retrait de la top layer jusqu’à la fin de la transition
    • transition-behavior: allow-discrete : active les transitions discrètes de display et overlay, qui ne sont normalement pas animées
    • Dans les navigateurs qui ne prennent pas en charge la pseudo-classe :open, on peut styliser l’état ouvert avec le sélecteur d’attribut dialog[open]
    • Comme <dialog> passe de display: none à display: block à chaque affichage, chaque transition d’entrée s’effectue de @starting-style vers le style dialog:open
  • Animation par keyframes

    • Les animations CSS par keyframes ont des contraintes différentes de celles des transitions
    • Elles ne fournissent pas @starting-style
    • La valeur display est incluse dans les keyframes
    • Il n’est pas nécessaire d’activer explicitement allow-discrete
    • Il n’est pas non plus nécessaire de définir overlay dans les keyframes
    • Le fade-out du backdrop ne peut pas être animé à la fermeture, car le backdrop est immédiatement retiré du DOM lorsque la boîte de dialogue se ferme

Résumé technique

  • Catégories de contenu : flow content, sectioning root
  • Contenu autorisé : flow content
  • Les balises d’ouverture et de fermeture sont toutes deux obligatoires, sans omission possible
  • Parent autorisé : tout élément acceptant du flow content
  • Rôle ARIA implicite : dialog
  • Rôle ARIA autorisé : alertdialog
  • Interface DOM : HTMLDialogElement
  • Spécification : HTML # the-dialog-element

1 commentaires

 
GN⁺ 2024-12-08
Avis sur Hacker News
  • Parmi les éléments HTML interactifs, on trouve les sélecteurs de fichiers, les sélecteurs de couleur, les sélecteurs de date/heure, les curseurs numériques, les options suggérées dans les champs texte, les résumés/détails dépliables, les FAQ, ou encore les lecteurs multimédias avec contrôles.
    Ces éléments sont appréciables parce qu’ils sont légers et sémantiques, et des éléments <details> partageant le même name peuvent aussi permettre de fermer l’élément précédent quand on en ouvre un autre.

    • Aujourd’hui, au final, leur comportement n’est pas assez prévisible selon les navigateurs ou les versions, si bien qu’ils sont souvent remplacés par des toolkits.
    • Les lecteurs multimédias avec contrôles ont une apparence complètement différente d’un navigateur à l’autre, et on ne peut pas les styliser sans JavaScript.
      Ce serait bien qu’ils soient mieux implémentés.
  • J’utilise <dialog> depuis 2019, et même si Firefox et Safari ne l’ont pas pris en charge pendant encore quelques années, le polyfill de Google était de bonne qualité, si bien que nous l’avons utilisé en production dans un SaaS professionnel sans problème.
    Le plus regrettable, c’est que l’élément n’était quasiment pas stylisé. Il n’avait qu’une bordure noire épaisse très basique, en pixels, et des coins anguleux, ce qui était loin de ce que j’espérais : que le navigateur suive les conventions du système d’exploitation pour les boîtes de dialogue et la disposition des fenêtres, afin qu’elles ressemblent à de vraies boîtes de dialogue macOS, iOS ou Windows.
    Comme l’élément ouvert se trouve dans une couche supérieure séparée, j’espérais aussi qu’il puisse sortir du viewport du navigateur, mais cette attente a également été déçue. Du point de vue de l’utilisateur, on ne souhaite pas une popup immobile ou une boîte de dialogue modale qui masque entièrement le contenu du document.
    Les éditeurs de navigateurs semblent vouloir nous dissuader d’utiliser alert(), prompt() et confirm() parce qu’ils bloquent JavaScript/le thread principal, mais ils n’ont pas proposé d’API de remplacement aussi simples, efficaces et ne nécessitant aucune compétence en design d’interface. Plutôt que d’essayer de convaincre que <dialog> est meilleur dans tous les cas, ce serait bien de fournir une API alert/prompt/confirm non bloquante basée sur des Promise.

    • Les fonctionnalités permettant de sortir du viewport du navigateur et d’être stylisé comme une boîte de dialogue système sont probablement plus désirées par les développeurs que par les utilisateurs ou les fabricants de navigateurs.
      Le fait que les nouvelles fonctionnalités ne se comportent plus comme l’ancienne famille alert() n’est pas seulement une déception accidentelle, cela a aussi des avantages.
      Cela dit, le style par défaut de <dialog> aurait mérité un peu plus d’attention, et même sans ressembler ni se comporter à 100 % comme une boîte de dialogue système hors du DOM, un style par défaut cohérent avec le style natif du navigateur aurait déjà été bien meilleur.
      Pour les applications web disposant de plus de permissions qu’une page ordinaire, comme les PWA, cette direction pourrait être mieux acceptée, par exemple avec des fonctionnalités séparées pour le style des fenêtres ou l’interaction avec le système.
    • Si un fournisseur faisait en sorte que <dialog> ressemble à une fenêtre native du navigateur, cela pourrait brouiller la line of death.
      Il deviendrait facile pour un site malveillant d’afficher au milieu de la page une popup « mise à jour du navigateur », puis de rediriger vers une page de téléchargement Chrome crédible afin de faire télécharger un exécutable modifié.
      https://textslashplain.com/2017/01/14/the-line-of-death/
    • Les modales qui bloquent le focus de toute la fenêtre du navigateur ne sont pas une bonne idée.
      Les utilisateurs ont plusieurs onglets ouverts, et l’information nécessaire pour terminer l’action dans la boîte de dialogue peut se trouver dans un autre onglet.
      Il faut aussi être très prudent quant au degré de contrôle visuel accordé à une vraie boîte de dialogue. En particulier, si on lui donne l’apparence du système d’exploitation, alors qu’aujourd’hui déjà beaucoup de gens se font avoir par de fausses alertes de virus du navigateur, une boîte de dialogue qui semble authentique et avec laquelle il faut impérativement interagir pourrait fortement augmenter le taux de réussite des attaques.
    • Le fait qu’ils soient presque impossibles à styliser semble être la principale raison qui empêche l’usage généralisé des composants intégrés au navigateur.
      La plupart des entreprises où j’ai travaillé avaient un langage de design plus ou moins cohérent, et quoi qu’en disent les partisans du HTML/CSS pur, utiliser tels quels les composants intégrés au navigateur donnait le plus souvent un rendu maladroit, amateur.
      Tant que ce problème ne sera pas résolu, il sera difficile qu’ils soient largement adoptés.
    • Je teste une idée consistant à faire fonctionner des remplaçants de alert(), prompt() et confirm() sous la forme await Prompts.alert("This is an alert message!");, await Prompts.confirm(...), await Prompts.prompt(...).
      Démo : https://tools.simonwillison.net/prompts-js
      Le code a été écrit par o1 : https://chatgpt.com/share/67539c28-4df0-8006-b021-4f468e011f...
  • J’ai écrit un billet intitulé « The HTML dialog element API is a mess » : https://lapcatsoftware.com/articles/2024/2/1.html

    • Peu de gens diraient que les standards du Web sont excellents, bien conçus et bien maintenus.
      Leur valeur tient malgré tout au fait qu’ils sont des standards. Le Web est formidable parce qu’il fonctionne partout, et il est forcément désordonné.
      Dans ce contexte, je considère que <dialog> est une réussite, surtout pour les outils d’administration internes. On ne veut pas se soucier de la dernière mode front-end ; on veut simplement économiser de l’espace à l’écran en affichant du contenu sous forme de surcouche modale au-dessus de la vue principale.
    • Il me semble très peu probable, en pratique, d’ouvrir deux fois la même boîte de dialogue en mode modal.
      Ouvrir une boîte de dialogue nécessite une entrée utilisateur, et une boîte de dialogue modale bloque les entrées utilisateur ; pour que cela se produise, il faudrait donc qu’une interaction à l’intérieur de la boîte de dialogue rouvre cette même boîte.
      Si vous ouvrez une boîte de dialogue via une opération asynchrone, vous devez suivre ce qui est ouvert ou fermé ; des situations similaires arrivent aussi dans des environnements comme Qt.
    • J’espérais que <dialog> devienne une version renforcée de await confirm/prompt, personnalisable, sans avertissements différents selon les navigateurs quand on l’ouvre plusieurs fois.
      En réalité, cela ressemble plutôt à un div enjolivé.
    • Google Chrome a aussi créé URLPattern, et j’espérais que Chrome et Safari ne le prendraient pas en charge.
      La Compression Streams API n’était pas mauvaise, mais c’était une toute petite API.
      À voir la tendance, Google semble mal gérer l’expérience utilisateur comme l’expérience développeur.
      Cela dit, en cherchant les positions des standards, j’ai constaté que les deux prennent en charge URLPattern.
  • Indépendamment de l’implémentation, je pense que c’était un pas dans la bonne direction.
    Une proposition ressemblant à un <select> amélioré est aussi en cours : https://open-ui.org/components/combobox.explainer/
    Pour les notifications toast, la Popover API est déjà arrivée dans les navigateurs : https://mdn.github.io/dom-examples/popover-api/
    Il existe aussi une proposition de popover hint pour les infobulles : https://open-ui.org/components/popover-hint.research.explain...

  • J’aime l’élément <dialog>, en particulier les considérations d’accessibilité intégrées et standardisées.
    J’attends le jour où l’on pourra l’utiliser sans polyfill, une fois que Safari avant 15.4 sortira des critères de prise en charge.
    Mais mon principal reproche est sa dépendance à JavaScript. Presque tous les visiteurs d’un site auront JavaScript activé, mais il y a une certaine satisfaction à faire en sorte que ce ne soit pas nécessaire.
    Je ne comprends pas pourquoi on ne peut pas contrôler l’état ouvert d’une boîte de dialogue avec CSS ou un bouton cible, et j’aimerais le savoir si je me trompe.

    • C’est pourquoi j’utilise surtout des éléments personnalisés avec l’attribut popover="".
      Ils peuvent facilement être ciblés par un bouton, ne nécessitent pas de JavaScript côté client, et peuvent aussi se fermer en cliquant à l’extérieur. C’est un comportement que <dialog> n’offre pas par défaut.
      Je ne suis pas sûr pour l’accessibilité, mais cela me semble au moins pas pire que les anciennes approches avec labels/cases à cocher/éléments de formulaire cachés, tout en étant beaucoup plus simple et moins bricolé.
    • Cela devrait devenir possible avec les invokers.
      https://css-tricks.com/invoker-commands-additional-ways-to-w...
  • J’aimerais que HTML prenne en charge un concept de balise comme <page>, permettant de définir plusieurs pages dans un seul fichier et d’en afficher une seule à la fois.
    Sans que cela ressemble à une boîte de dialogue, chaque PAGE pourrait, selon l’état sélectionné, réutiliser des sections communes de la même page comme l’en-tête, la barre latérale ou le pied de page.
    On peut déjà le faire aujourd’hui en masquant et affichant des div, mais un tag HTML spécifique pour prendre en charge cette approche pourrait être utile.

    • Il me semble qu’il y a eu récemment des améliorations de ce type côté feuilles de style pour l’impression, mais à ma connaissance rien encore pour l’affichage à l’écran.
  • En l’utilisant aujourd’hui, je suis tombé sur un problème que je n’ai pas réussi à contourner.
    Quand il y a un formulaire dans une boîte de dialogue, appuyer sur Entrée avec le focus dans un champ, ou sur Espace avec le focus sur le bouton d’envoi, ferme la boîte de dialogue en même temps que le formulaire est soumis.
    Je n’ai pas trouvé de moyen propre de l’empêcher. D’habitude, un formulaire recharge la page, donc ce n’est probablement pas un problème courant, mais ici j’utilisais htmx.

    • La dernière phrase est très probablement la bonne. Par défaut, un formulaire envoie une requête réseau.
      J’utilise un éditeur où un formulaire dans un <dialog> met à jour un iframe, et seul l’iframe cible se recharge ; la boîte de dialogue ne se ferme pas.
      Je n’ai pas non plus réussi à reproduire le cas où la boîte de dialogue se ferme en appuyant sur Entrée. Cela devrait être équivalent à submit, et submit ne ferme pas non plus la boîte de dialogue.
      Au passage, j’ai appris hier que les boutons peuvent fermer une boîte de dialogue.
    • preventDefault et stopPropagation ne suffiraient-ils pas ?
    • Il vaudrait peut-être mieux signaler un bug côté HTMX.
  • En cherchant un gestionnaire de consentement aux cookies à intégrer dans une build récemment optimisée, je n’aimais pas le fait que les options open source dépassaient les 100 Ko, alors j’en ai créé un moi-même : https://github.com/replete/biscuitman
    Pour le rendre aussi petit que possible, je me suis appuyé sur <dialog>, et avec seulement quelques règles CSS, il fonctionne nativement même sans style.
    J’ai finalement aussi écrit un outil de build qui compile jusqu’à IE11 et de très anciennes versions de navigateurs.
    <dialog> fonctionne globalement bien et, sur les anciens navigateurs, quelques astuces CSS sont nécessaires, mais ce n’est pas difficile à gérer. C’est un bon ajout à la plateforme Web, mais après 20 ans à faire ce métier, je n’ai pas envie de recréer un contrôle de sélection multiple personnalisé tous les quelques ans. Les contrôles natifs, c’est bien.

  • Le comportement de fermeture habituel de la plupart des exemples ne fonctionne pas dans Firefox Android

    • Hier, en l’utilisant dans un projet, l’attribut autofocus du bouton de fermeture ne fonctionnait pas
      Au final, j’ai ajouté une ligne du genre $('#thatModal *[autofocus]').focus() à chaque appel de show()
      D’après MDN, cela devrait fonctionner comme prévu par défaut
    • Peux-tu expliquer davantage ce que tu entends par « fermeture habituelle de la plupart des exemples » ? Tous les exemples que j’ai vus avaient un bout de JavaScript qui ajoutait un écouteur d’événement au bouton de fermeture, et cela fonctionnait bien dans Firefox pour Android
    • Même chose dans Chrome sous Windows 10
      Il semble que seuls les écouteurs ajoutés en JavaScript fonctionnent correctement