3 puntos por GN⁺ 2024-12-08 | 1 comentarios | Compartir por WhatsApp
  • El elemento HTML `` crea cuadros de diálogo modales y no modales nativos del navegador; los modales ponen el resto de la página en estado inert para bloquear la interacción
  • Las formas básicas de abrir y cerrar son showModal(), show() y close(); también es posible el control declarativo con ``, la Invoker Commands API y la Popover API
  • El atributo closedby divide las formas en que el usuario puede cerrar el cuadro de diálogo en any, closerequest y none, y el comportamiento predeterminado cambia según cómo se haya abierto
  • En accesibilidad, son importantes el foco inicial y un botón de cierre explícito; los cuadros de diálogo abiertos con showModal() ofrecen por defecto el primer elemento enfocable y cierre con la tecla Esc
  • Para el estilo se usan :modal, :open y ::backdrop; para animaciones se requiere manejo de transiciones discretas como display, overlay, @starting-style y transition-behavior: allow-discrete

Rol de `` y comportamiento modal

  • El elemento HTML `` sirve para crear cuadros de diálogo modales y no modales
  • Un cuadro de diálogo modal impide la interacción con otros elementos de la UI y pone el resto de la página en estado inert
  • Un cuadro de diálogo no modal permite seguir interactuando con el resto de la página mientras está abierto

Estado abierto y política de cierre

  • `` incluye atributos globales, pero no debe usarse el atributo tabindex
    • `` en sí no es un elemento interactivo y no recibe foco
    • El contenido interno y el botón de cierre sí pueden recibir foco e interactuar
  • El atributo open indica que el cuadro de diálogo está activo y puede recibir interacción
    • Si no tiene el atributo open, no es visible para el usuario
    • Para mostrar un cuadro de diálogo, se recomienda usar .show() o .showModal() en lugar del atributo open
    • Un `` abierto con el atributo open es no modal
    • Aunque se puede cambiar el estado abierto/cerrado de un cuadro de diálogo no modal alternando el atributo open, no se recomienda
  • El atributo closedby especifica de qué formas puede el usuario cerrar el cuadro de diálogo
    • any: se puede cerrar de las tres formas
      • light dismiss, como hacer clic o tocar fuera del cuadro de diálogo
      • Acciones específicas de la plataforma, como la tecla Esc, volver atrás en móviles o un gesto de descarte
      • Formas definidas por el desarrollador, como un botón que llama a HTMLDialogElement.close() o el envío de ``
    • closerequest: se puede cerrar mediante acciones específicas de la plataforma y formas definidas por el desarrollador
    • none: solo se puede cerrar mediante formas definidas por el desarrollador
    • Si no hay un valor válido de closedby, cuando se abre con showModal() se comporta como closerequest; en los demás casos, como none

JavaScript y control declarativo

  • Con JavaScript se puede controlar directamente la visualización y el cierre de ``
    • showModal(): muestra un cuadro de diálogo modal
    • show(): muestra un cuadro de diálogo no modal
    • close(): cierra el cuadro de diálogo
    • También se puede cerrar enviando un dentro de con method="dialog"
    • Los cuadros de diálogo modales también pueden cerrarse con la tecla Esc
  • La Invoker Commands API permite abrir y cerrar cuadros de diálogo modales solo con atributos de botones
    • Se especifican los atributos commandfor y command en ``
    • Los comandos disponibles para un cuadro de diálogo son "show-modal", "close" y "request-close"
Open dialog

  This dialog was opened using an invoker command.

  Close

  • La Popover API permite abrir, cerrar y alternar declarativamente cuadros de diálogo no modales
    • Se agrega el atributo popover a `` para convertirlo en popover
    • Se especifican popovertarget y popovertargetaction en botones o elementos de entrada
    • Como los cuadros de diálogo popover son no modales, se pueden cerrar haciendo clic fuera
    • Si no se especifica un valor para popover, se usa el valor predeterminado "auto", que activa light dismiss
    • popover="manual" desactiva light dismiss y requiere una forma aparte, como un botón de cierre
    • Si se omite popovertargetaction, se usa el valor predeterminado toggle

Envío de formularios y manejo del cierre

  • Todo `` necesita un mecanismo de cierre, y debe funcionar también en dispositivos sin teclado físico
  • Hay varias formas de cierre
    • Configurar method="dialog" en un dentro de y enviarlo
    • Hacer clic fuera del cuadro de diálogo cuando light dismiss está activado
    • Presionar la tecla Esc en un cuadro de diálogo que lo permita
    • Llamar a HTMLDialogElement.close()
  • formmethod="dialog" en `` o en un botón de envío cierra el cuadro de diálogo
    • El estado de los controles del formulario se conserva, pero no se envía
    • returnValue se establece con el valor del botón activado
  • En formularios con campos obligatorios, el user agent impide cerrar el cuadro de diálogo mediante un envío normal hasta que se proporcionen los valores
    • Usar formnovalidate en el botón de cierre permite omitir la validación del formulario
    • También se puede cerrar llamando a dialog.close() desde JavaScript

Foco y accesibilidad

  • Al abrir `` con showModal(), por defecto el foco se establece en el primer elemento enfocable anidado
  • Para especificar la posición de foco inicial más adecuada en un cuadro de diálogo concreto, se puede usar el atributo autofocus
    • Si no hay un elemento para interactuar de inmediato, se recomienda poner autofocus en el botón de cierre
    • Si la posición de foco inicial es incierta, como en un cuadro de diálogo renderizado dinámicamente, `` en sí puede ser una posición de foco inicial adecuada
  • La forma más robusta es incluir botones explícitos como confirmar, cancelar o cerrar para que todos los usuarios puedan cerrarlo
  • Los cuadros de diálogo invocados con showModal() se cierran por defecto con la tecla Esc
    • Los cuadros de diálogo no modales no se cierran por defecto con la tecla Esc
    • Los usuarios de teclado esperan que un cuadro de diálogo modal se cierre con la tecla Esc
    • Si hay varios cuadros de diálogo modales abiertos, la tecla Esc debe cerrar solo el último que se mostró; al usar ``, el navegador ofrece este comportamiento
  • El `` nativo ofrece funciones de usabilidad y accesibilidad que habría que replicar manualmente en cuadros de diálogo personalizados creados con otros elementos
  • El navegador expone `` de manera similar a un cuadro de diálogo personalizado que usa ARIA role="dialog"
    • Un `` invocado con showModal() tiene implícitamente aria-modal="true"
    • Un `` mostrado con show(), el atributo open o cambiando el display predeterminado se expone como aria-modal="false"
    • Al implementar un cuadro de diálogo modal, todo lo que esté fuera de `` y su contenido debe renderizarse como inert; al usar showModal(), el navegador ofrece este comportamiento

Estilos CSS y backdrop

  • `` se puede seleccionar por nombre de elemento como un elemento normal
  • Para estilos basados en estado se pueden usar las pseudoclases :modal y :open
  • El fondo de un cuadro de diálogo modal puede estilizarse con el pseudoelemento ::backdrop
    • Se aplica al backdrop que aparece detrás de `` cuando el cuadro de diálogo se muestra con showModal()
    • Puede desenfocar, oscurecer u ocultar el contenido posterior que está en estado inert

Patrones de uso con ejemplos

  • El ejemplo de Invoker Commands API abre el cuadro de diálogo con un botón command="show-modal" y lo cierra con un botón command="close"
    • Se puede abrir con el botón “Open dialog”
    • Se puede cerrar con el botón “Close” o con la tecla Esc
  • El ejemplo de Popover API abre y cierra un cuadro de diálogo no modal con popover, popovertarget y popovertargetaction
    • Se puede cerrar con el botón “Close”, la tecla Esc o seleccionando fuera del cuadro de diálogo
    • Si se usa popover="manual", light dismiss queda desactivado
  • El ejemplo del atributo open crea un cuadro de diálogo no modal solo con HTML que ya está abierto al cargar la página
    • Se puede cerrar con el botón “OK” de ``
    • No ofrece una forma de volver a abrirlo después de cerrarlo
    • Para mostrar cuadros de diálogo no modales, se prefiere usar HTMLDialogElement.show()
  • El ejemplo de cuadro de diálogo modal se abre con .showModal() y se cierra con close()
    • Estiliza un fondo con degradado usando ::backdrop
    • Cuando el cuadro de diálogo está abierto, el exterior queda en estado inert y no se puede interactuar con el documento
  • El ejemplo de returnValue maneja el valor de retorno del cuadro de diálogo con un formulario y valores de botones
    • El returnValue predeterminado es una cadena vacía o el valor del botón que envió el formulario dentro del cuadro de diálogo
    • El botón “Confirm” pasa el valor seleccionado a close() para usarlo como valor de retorno
    • Si se cierra con la tecla Esc, returnValue no se actualiza y tampoco se dispara el evento close, por lo que el texto de `` no se actualiza

Animaciones de ``

  • Un oculto tiene `display: none`; un mostrado tiene display: block
  • Cuando cambia el estado de visualización, se agrega o elimina de la top layer y del árbol de accesibilidad
  • Para animar ``, la propiedad display debe ser animable
    • Los navegadores compatibles tratan display como una animación discreta
    • Al cambiar de none a block, pasa a block en el punto 0%, por lo que se ve durante toda la animación
    • Al cambiar de block a none, pasa a none en el punto 100%, por lo que se ve durante toda la animación
  • Animación con CSS transition

    • Para animar `` con CSS transition se necesitan las siguientes funciones
    • @starting-style: proporciona los valores iniciales de transición cada vez que se abre el cuadro de diálogo
    • transición de display: mantiene el cuadro de diálogo con un valor de display visible durante la transición
    • transición de overlay: retrasa la eliminación de la top layer hasta que termine la transición
    • transition-behavior: allow-discrete: activa transiciones discretas de display y overlay, que por defecto no se animan
    • En navegadores que no admiten la pseudoclase :open, se puede usar el selector de atributo dialog[open] para estilizar el estado abierto
    • Como `` cambia de display: none a display: block cada vez que se muestra, en cada transición de entrada se pasa desde @starting-style al estilo dialog:open
  • Animación con keyframes

    • Las animaciones CSS con keyframes tienen restricciones distintas a las transition
    • No proporcionan @starting-style
    • Incluyen el valor de display dentro de los keyframes
    • No hace falta activar explícitamente allow-discrete
    • Tampoco hace falta configurar overlay en los keyframes
    • El fade-out del backdrop no puede animarse porque, cuando el cuadro de diálogo se cierra, el backdrop se elimina inmediatamente del DOM

Resumen técnico

  • Categoría de contenido: flow content, sectioning root
  • Contenido permitido: flow content
  • Las etiquetas de inicio y cierre son obligatorias; no se omiten etiquetas
  • Padres permitidos: cualquier elemento que acepte flow content
  • Rol ARIA implícito: dialog
  • Rol ARIA permitido: alertdialog
  • Interfaz DOM: HTMLDialogElement
  • Especificación: HTML # the-dialog-element

1 comentarios

 
GN⁺ 2024-12-08
Opiniones en Hacker News
  • Entre los elementos HTML interactivos están el selector de archivos, selector de color, selector de fecha/hora, slider numérico, opciones sugeridas en campos de texto, resumen/detalle desplegable, FAQ y reproductores multimedia con controles.
    Estos elementos son buenos porque son ligeros y semánticos, y los <details> que comparten el mismo name incluso pueden comportarse de forma que, al abrir uno, se cierre el elemento anterior.

    • Hoy en día, al final, como su comportamiento no es lo bastante predecible entre navegadores o versiones, muchas veces se reemplazan por toolkits.
    • Los reproductores multimedia con controles se ven completamente distintos en cada navegador y no se pueden estilizar sin JavaScript.
      Ojalá estuvieran mejor implementados.
  • Uso <dialog> desde 2019 y, aunque Firefox y Safari tardaron varios años más en soportarlo, el polyfill de Google era de buena calidad, así que lo usamos sin problemas en producción para un SaaS empresarial.
    Lo que más decepciona es que el elemento casi no venía estilizado: apenas un borde negro grueso de píxeles y esquinas rectas, muy distinto de lo que esperaba, que era que el navegador siguiera las convenciones del sistema operativo para diálogos/ventanas y lo hiciera verse como un diálogo nativo de macOS, iOS o Windows.
    Como el elemento abierto está en una capa superior separada, también esperaba que pudiera salirse del viewport del navegador, pero esa expectativa tampoco se cumplió. Desde el punto de vista del usuario, nadie quiere un popup inmóvil o un diálogo modal que cubra por completo el contenido del documento.
    Parece que los fabricantes de navegadores no quieren que usemos alert(), prompt() y confirm() porque bloquean JavaScript/el hilo principal, pero no han ofrecido una API alternativa igual de simple, efectiva y que no exija habilidades de diseño de UI. En vez de intentar convencernos de que <dialog> es mejor en todos los casos, estaría bien que ofrecieran una API no bloqueante basada en Promise para alert/prompt/confirm.

    • La posibilidad de salirse del viewport del navegador y de estilizarse como un diálogo del sistema probablemente sea algo que desean más los desarrolladores que los usuarios o los fabricantes de navegadores.
      Que las nuevas funciones no se comporten como la vieja familia de alert() no es solo una decepción accidental; también tiene ventajas.
      Aun así, hacía falta prestar un poco más de atención al estilo por defecto de <dialog>; aunque no se viera ni se comportara al 100% como un diálogo del sistema fuera del DOM, habría sido mucho mejor con un estilo predeterminado que al menos encajara con el estilo base del navegador.
      En el caso de apps web con más permisos que una página común, como las PWA, esta dirección podría ser más aceptable, igual que funciones separadas para estilizar ventanas o interactuar con el sistema.
    • Si los vendors hacen que <dialog> se vea como una ventana nativa del navegador, podrían difuminar la line of death.
      A un sitio malicioso le resultaría más fácil mostrar un popup de “actualización del navegador” en medio de la página y redirigir a una página de descarga de Chrome convincente para que el usuario baje un ejecutable modificado.
      https://textslashplain.com/2017/01/14/the-line-of-death/
    • Un modal que bloquea el foco de toda la ventana del navegador no es una buena idea.
      Los usuarios suelen tener varias pestañas abiertas, y quizá en otra pestaña haya información necesaria para completar el diálogo.
      También hay que tener mucho cuidado con cuánto control visual se le da a un diálogo real. En especial si se lo hace parecer al sistema operativo: incluso hoy mucha gente cae en alertas falsas de virus del navegador, y un diálogo que se vea auténtico y con el que sea obligatorio interactuar podría aumentar mucho la tasa de éxito de los ataques.
    • El hecho de que casi no se puedan estilizar parece ser la razón principal que impide un uso amplio de los componentes integrados del navegador.
      La mayoría de las empresas donde trabajé tenían algún tipo de lenguaje de diseño coherente y, diga lo que diga la gente que prefiere HTML/CSS puro, usar los componentes integrados del navegador tal cual casi siempre se veía tosco y amateur.
      Si este problema no se resuelve, será difícil que se usen de forma masiva.
    • Estoy probando la idea de crear reemplazos para alert(), prompt() y confirm() que funcionen como await Prompts.alert("This is an alert message!");, await Prompts.confirm(...), await Prompts.prompt(...).
      Demo: https://tools.simonwillison.net/prompts-js
      El código lo escribió o1: https://chatgpt.com/share/67539c28-4df0-8006-b021-4f468e011f...
  • Escribí un artículo llamado “The HTML dialog element API is a mess”: https://lapcatsoftware.com/articles/2024/2/1.html

    • No creo que haya mucha gente que diga que los estándares web son excelentes, bien diseñados y bien mantenidos.
      Aun así, su valor está en que son estándares. La web es genial porque funciona en todas partes, y por eso inevitablemente es desordenada.
      En este contexto, creo que <dialog> es un éxito, sobre todo para herramientas internas de administración. No me importan las modas modernas del frontend; solo quiero ahorrar espacio en pantalla y mostrar contenido como una superposición modal encima de la vista principal.
    • En la práctica, casi nunca debería pasar que se abra dos veces el mismo cuadro de diálogo como modal.
      Para abrir un cuadro de diálogo se necesita una entrada del usuario, y un cuadro de diálogo modal bloquea la entrada del usuario, así que para que eso ocurra, una entrada dentro del cuadro tendría que volver a abrir ese mismo cuadro.
      Si se abre un cuadro de diálogo mediante una operación asíncrona, hay que llevar registro de qué está abierto y qué está cerrado, y en entornos como Qt pasa algo parecido.
    • Esperaba que <dialog> se convirtiera en una versión potenciada de await confirm/prompt, personalizable y sin advertencias distintas según el navegador al abrirse varias veces.
      Pero en realidad se parece más a un div embellecido.
    • Google Chrome también creó URLPattern, y esperaba que Chrome y Safari no lo soportaran.
      La Compression Streams API no estaba mal, pero era una API muy pequeña.
      Viendo el patrón, parece que Google no maneja muy bien la experiencia de usuario ni la experiencia de desarrollador.
      Además, al buscar la postura de los estándares, vi que ambos soportan URLPattern.
  • Independientemente de la implementación, creo que fue un paso en la dirección correcta.
    También hay una propuesta en curso que parece una mejora de <select>: https://open-ui.org/components/combobox.explainer/
    Para notificaciones tipo toast, el navegador ya incluye la Popover API: https://mdn.github.io/dom-examples/popover-api/
    También hay una propuesta de popover hint para tooltips: https://open-ui.org/components/popover-hint.research.explain...

  • Me gusta el elemento <dialog>, especialmente por las consideraciones de accesibilidad integradas y estandarizadas.
    Espero el día en que pueda usarlo sin polyfill cuando Safari anterior a 15.4 quede fuera de los criterios de soporte.
    Dicho eso, mi mayor queja es su dependencia de JavaScript. Casi todos los visitantes de un sitio vendrán con JavaScript activado, pero también da cierta satisfacción hacer que no sea estrictamente necesario.
    No entiendo por qué no se puede controlar el estado abierto de un cuadro de diálogo con CSS o con un botón objetivo, y me gustaría saberlo si estoy equivocado.

    • Por eso suelo usar un elemento personalizado con el atributo popover="".
      Se puede apuntar fácilmente con un botón, no requiere JavaScript del lado del cliente y también puede cerrarse al hacer clic afuera. Es un comportamiento que <dialog> no tiene por defecto.
      No estoy seguro de la accesibilidad, pero creo que no es peor que las formas antiguas hechas con etiquetas, checkboxes y elementos de formulario ocultos; además es mucho más simple y se siente menos como un hack.
    • Esto será posible con invokers.
      https://css-tricks.com/invoker-commands-additional-ways-to-w...
  • Me gustaría que HTML soportara el concepto de una etiqueta tipo <page>, que permitiera definir varias páginas dentro de un mismo archivo y mostrar una a la vez.
    Que no se viera como un cuadro de diálogo, y que cada PAGE pudiera traer secciones comunes dentro de la misma página, como header, sidebar y footer, según cuál esté seleccionada.
    Hoy se puede hacer ocultando y mostrando div, pero podría ser útil que una etiqueta HTML específica soportara este enfoque.

    • Creo que recientemente hubo mejoras en las hojas de estilo para impresión que permiten hacer algo así, pero hasta donde sé todavía no existe para visualización en pantalla.
  • Hoy, al usarlo, tuve un problema que no pude esquivar.
    Si hay un formulario dentro del cuadro de diálogo, al presionar Enter con foco en un input o Space con foco en el botón de envío, el cuadro de diálogo se cierra junto con el envío del formulario.
    No encontré una forma limpia de impedirlo. Normalmente un formulario recarga la página, así que quizá no sea un problema común, pero en este caso estaba usando htmx.

    • Es muy probable que la última frase sea correcta. Por defecto, un formulario envía una solicitud de red.
      Uso un editor que actualiza un iframe mediante un formulario dentro de <dialog>, y solo se recarga el iframe de destino; el cuadro de diálogo no se cierra.
      Tampoco pude reproducir que el cuadro de diálogo se cierre al presionar Enter. Debería ser igual que submit, y submit tampoco cierra el cuadro de diálogo.
      Por cierto, ayer aprendí que un botón sí puede cerrar un cuadro de diálogo.
    • ¿No bastaría con usar preventDefault y stopPropagation?
    • Tal vez convenga reportar un bug a HTMX.
  • Mientras buscaba un gestor de consentimiento de cookies para incluir en una build recién optimizada, no me gustó que las opciones open source superaran los 100 KB, así que hice uno propio: https://github.com/replete/biscuitman
    Intenté hacerlo lo más pequeño posible apoyándome en <dialog>, y con solo unas pocas reglas CSS funciona de forma nativa incluso sin estilos.
    Al final también terminé escribiendo una herramienta de build que compila hasta IE11 y versiones de navegadores realmente antiguas.
    <dialog> funciona bastante bien en general, y en navegadores viejos requiere algunos trucos de CSS, pero no es difícil de manejar. Es una buena incorporación a la plataforma web, aunque después de 20 años haciendo esto, no quiero volver a crear un control personalizado de selección múltiple cada pocos años. Los controles nativos son buenos.

  • El comportamiento típico de cierre de la mayoría de los ejemplos no funciona en Android Firefox

    • Ayer, al usarlo en un proyecto, el atributo autofocus del botón de cierre no funcionaba
      Al final, cada vez que llamaba a show(), agregué una línea como $('#thatModal *[autofocus]').focus()
      Según MDN, debería funcionar de forma predeterminada como se espera
    • ¿Podrías explicar mejor a qué te refieres con “el cierre típico de la mayoría de los ejemplos”? Todos los ejemplos que vi tenían un fragmento de JavaScript que agregaba un event listener al botón de cierre, y funcionaban bien en Firefox para Android
    • Lo mismo ocurre en Chrome en Windows 10
      Parece que solo funcionan correctamente los listeners agregados con JavaScript