Cómo compilar un componente de diálogo

Una descripción general fundamental de cómo compilar mini y megamodales adaptables, responsivas y accesibles con el elemento <dialog>.

En esta publicación, compartiré mis pensamientos sobre cómo crear colores adaptables, mini y megamodales responsivos y accesibles con el elemento <dialog>. Prueba la demostración y mira la fuente.

Demostración de los diálogos mega y mini en los temas claros y oscuros

Si prefieres ver un video, aquí tienes una versión de YouTube de esta publicación:

Descripción general

El <dialog> es ideal para la acción o la información contextual de los anuncios in-page. Ten en cuenta cuándo la experiencia del usuario puede beneficiarse de una misma acción de página en lugar de de varias páginas acción: quizás porque el formulario es pequeño o la única acción requerida de la el usuario es confirmar o cancelar.

Recientemente, el elemento <dialog> se volvió estable en todos los navegadores:

Navegadores compatibles

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

Origen

Creo que al elemento le faltan algunos elementos, así que en esta GUI Desafío Agrego la experiencia del desarrollador elementos que espero: eventos adicionales, descarte ligero, animaciones personalizadas y una y megatipo.

Marca

Los elementos esenciales de un elemento <dialog> son sencillos. El elemento ocultarse automáticamente y tiene estilos incorporados para superponer el contenido.

<dialog>
  …
</dialog>

Podemos mejorar este modelo de referencia.

Tradicionalmente, un elemento de diálogo comparte mucho con un modal y, a menudo, los nombres son intercambiables. Aquí me tomé la libertad de usar el elemento de diálogo para los diálogos pequeños (mini) y los de página completa (mega). Nombré mega y mini, con diálogos apenas adaptados para diferentes casos de uso. Agregué un atributo modal-mode para que puedas especificar el tipo:

<dialog id="MegaDialog" modal-mode="mega"></dialog>
<dialog id="MiniDialog" modal-mode="mini"></dialog>

Captura de pantalla de los diálogos mini y mega en los temas claro y oscuro.

No siempre, pero, por lo general, los elementos de diálogo se usarán para recopilar algunos información de interacción. Los formularios dentro de los elementos de diálogo están hechos para usarse de aprendizaje automático. Es recomendable que un elemento de formulario envuelva el contenido del diálogo JavaScript puede acceder a los datos que ingresó el usuario. Además, los botones dentro un formulario que usa method="dialog" puede cerrar un diálogo sin JavaScript y pasar de datos no estructurados.

<dialog id="MegaDialog" modal-mode="mega">
  <form method="dialog">
    …
    <button value="cancel">Cancel</button>
    <button value="confirm">Confirm</button>
  </form>
</dialog>

Megadiálogo

Un diálogo combinado tiene tres elementos dentro del formulario: <header>: <article>, y <footer> Estos sirven como contenedores semánticos y como objetivos de estilo para las presentación del diálogo. El encabezado titula la ventana modal y ofrece un cierre . Este artículo contiene información y entradas de formularios. El pie de página contiene un <menu> de botones de acción.

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

El primer botón de menú tiene autofocus y un controlador de eventos intercalados onclick. El atributo autofocus recibirá enfoque cuando se abre el cuadro de diálogo, y creo que es una buena práctica colocar esto el botón Cancelar, no el botón Confirmar. Esto garantiza que la confirmación esté deliberada y no accidental.

Minidiálogo

El minidiálogo es muy similar al megadiálogo, solo le falta un <header>. Esto permite que sea más pequeño y esté más intercalado.

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

El elemento de diálogo proporciona una base sólida para un elemento del viewport completo que para recopilar datos e interacción del usuario. Estos elementos esenciales pueden hacer que algunos interacciones interesantes y eficaces en su sitio o aplicación.

Accesibilidad

El elemento de diálogo tiene una muy buena accesibilidad integrada. En lugar de agregar estos como suelo hacerlo, muchos ya están ahí.

Restableciendo el enfoque

Como lo hicimos manualmente en Cómo crear una navegación lateral , es importante que abrir y cerrar algo de forma correcta se enfoca en la apertura y el cierre relevantes botones. Cuando se abre ese panel de navegación lateral, se enfoca el botón de cierre. Cuando si presionas el botón Cerrar, el enfoque se restablece al botón que lo abrió.

Con el elemento de diálogo, este es el comportamiento predeterminado integrado:

Por desgracia, si quieres animar el diálogo para que aparezca y salga, esta funcionalidad se pierde. En la sección de JavaScript, lo restableceré funcionalidad.

Enfoque de captura

El elemento de diálogo administra inert en el documento. Antes del inert, se usaba JavaScript para detectar el foco dejando un elemento, y en ese momento lo intercepta y lo vuelve a colocar.

Navegadores compatibles

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

Origen

Después del inert, se puede "inmovilizar" cualquier parte del documento en tanto que son dejan de enfocar los objetivos o son interactivos con el mouse. En lugar de atrapar el enfoque, se guía hacia la única parte interactiva del documento.

Cómo abrir y enfocar automáticamente un elemento

De forma predeterminada, el elemento del diálogo asignará el foco al primer elemento enfocable. en el lenguaje de marcado del diálogo. Si este no es el mejor elemento para el usuario de forma predeterminada usa el atributo autofocus. Como dijimos antes, creo que es una práctica recomendada para colocar esto en el botón Cancelar y no en el botón Confirmar. Esto garantiza que la confirmación es deliberada y no accidental.

Cómo cerrar con la tecla Escape

Es importante facilitar el cierre de este elemento potencialmente disruptivo. Afortunadamente, el elemento de diálogo controlará la tecla Escape por ti, lo que te liberará de la carga de la organización.

Estilos

Hay una ruta sencilla para aplicar estilo al elemento del diálogo y una ruta fija. La facilidad ruta se logra sin cambiar la propiedad de visualización del diálogo y funciona con sus limitaciones. Voy por el camino difícil para crear animaciones personalizadas abrir y cerrar el diálogo, tomar el control de la propiedad display y mucho más

Cómo dar estilo con objetos abiertos

Para acelerar los colores adaptables y la coherencia general del diseño, abrí mi biblioteca de variables CSS Open Props. En Además de las variables gratuitas que se proporcionan, también importo un normalize y algunos botones, que abren Props proporciona como importaciones opcionales. Estas importaciones me ayudan a enfocarme en personalizar el diálogo y la demostración, y no se necesitan muchos estilos para respaldarlo y hacer que se vea. bien.

Cómo aplicar diseño al elemento <dialog>

Cómo ser propietario de la propiedad de visualización

El comportamiento predeterminado mostrar y ocultar de un elemento de diálogo activa o desactiva la pantalla propiedad de block a none. Esto significa que no se puede animar dentro y fuera, solo en la entrada. quiero agregar animaciones, y el primer paso es para establecer una display:

dialog {
  display: grid;
}

Al cambiar y, por lo tanto, poseer, el valor de la propiedad de visualización, como se muestra en el sobre el fragmento de CSS, se debe administrar una cantidad considerable de estilos para y facilitar la experiencia del usuario adecuada. Primero, el estado predeterminado de un diálogo es cerrado. Puedes representar este estado visualmente y evitar que el diálogo que reciben interacciones con los siguientes estilos:

dialog:not([open]) {
  pointer-events: none;
  opacity: 0;
}

Ahora el diálogo es invisible y no se puede interactuar con él si no está abierto. Más tarde Agregaré JavaScript para administrar el atributo inert en el diálogo y asegurarme que los usuarios del teclado y lector de pantalla tampoco puedan acceder al diálogo oculto.

Dar al diálogo un tema de color adaptable

MegaCuadro de diálogo que muestra los temas claro y oscuro, lo que demuestra los colores de la superficie.

Por el contrario, color-scheme habilita tu documento en un archivo proporcionado por el navegador el tema de color adaptable a las preferencias claras y oscuras del sistema, quería personalizar el elemento de diálogo más que eso. Open Props proporciona algunas plataformas de colores que se adaptan automáticamente a preferencias del sistema claro y oscuro, similares al uso de color-scheme. Estos son geniales para crear capas en un diseño y me encanta usar el color para ayudar admiten visualmente esta apariencia de las superficies de las capas. El color de fondo es var(--surface-1); para ubicarte sobre esa capa, usa 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);
  }
}

Más adelante, se agregarán más colores adaptables para los elementos secundarios, como el encabezado y pie de página. Los considero adicionales para un elemento de diálogo, pero son muy importantes en crear un diseño de diálogo atractivo y bien diseñado.

Tamaño del diálogo responsivo

De forma predeterminada, el diálogo delega su tamaño a su contenido, que generalmente es genial. Mi objetivo aquí es limitar max-inline-size a un tamaño legible (--size-content-3 = 60ch) o el 90% del ancho del viewport. Esta garantiza que el diálogo no se extienda de borde a borde en un dispositivo móvil y tampoco lo sea ancha en una pantalla de escritorio que es difícil de leer. A continuación, agrego un max-block-size para que el diálogo no supere la altura de la página. Esto también significa que debes especificar dónde se encuentra el área desplazable del diálogo, en caso de que sea alta elemento de diálogo.

dialog {
  
  max-inline-size: min(90vw, var(--size-content-3));
  max-block-size: min(80vh, 100%);
  max-block-size: min(80dvb, 100%);
  overflow: hidden;
}

¿Notas que tengo max-block-size dos veces? El primero usa 80vh, una máquina virtual viewport. Quiero que el diálogo se mantenga dentro del flujo relativo para los usuarios internacionales, así que uso la lógica, la más reciente y Se admite la unidad dvb en la segunda declaración para el momento en que se vuelva más estable.

Posicionamiento de los grandes diálogos

Para ayudar a posicionar un elemento de diálogo, vale la pena desglosar sus dos partes: el fondo de pantalla completa y el contenedor de diálogo. El fondo debe cubren todo y proporcionan un efecto de sombreado para ayudar a respaldar que este diálogo al frente y el contenido detrás es inaccesible. El contenedor de diálogo es gratuito centro sobre este fondo y tomar la forma que requiera su contenido.

Los siguientes estilos fijan el elemento de diálogo en la ventana y lo estiran a cada y usa margin: auto para centrar el contenido:

dialog {
  
  margin: auto;
  padding: 0;
  position: fixed;
  inset: 0;
  z-index: var(--layer-important);
}
Estilos de diálogo combinado para dispositivos móviles

En viewports pequeñas, el estilo de esta megamodal completa de página completa de un modo un poco diferente. me establece el margen inferior en 0, lo que lleva el contenido del diálogo a la parte inferior de el viewport. Con algunos ajustes de estilo, puedo convertir el diálogo en un más cerca del dedo pulgar del usuario:

@media (max-width: 768px) {
  dialog[modal-mode="mega"] {
    margin-block-end: 0;
    border-end-end-radius: 0;
    border-end-start-radius: 0;
  }
}

Captura de pantalla de Herramientas para desarrolladores que se superpone el espaciado de margen 
  en el cuadro de diálogo combinado para computadoras y dispositivos móviles mientras está abierto.

Posicionamiento de minidiálogos

Cuando utilizo una ventana de visualización más grande, como en una computadora de escritorio, elegí ubicar los minidiálogos sobre el elemento que los llamó. Para hacer esto, necesito JavaScript. Puedes encontrar técnica que uso aquí, pero creo que está fuera del alcance de este artículo. Sin JavaScript, el aparece un minidiálogo en el centro de la pantalla, al igual que los megadiálogos.

Haz que se destaque

Por último, agrega estilo al diálogo para que se vea como una superficie suave que está lejos. arriba de la página. La suavidad se logra redondeando las esquinas del diálogo. La profundidad se logra con una de las sombras cuidadosamente elaboradas de Open Props. objetos:

dialog {
  
  border-radius: var(--radius-3);
  box-shadow: var(--shadow-6);
}

Cómo personalizar el seudoelemento del fondo

Elegí trabajar muy suavemente con el fondo y agregar un efecto de desenfoque con backdrop-filter al megadiálogo:

Navegadores compatibles

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

Origen

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

También elegí una transición en backdrop-filter, con la esperanza de que los navegadores permitirá la transición del elemento de fondo en el futuro:

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

Captura de pantalla del megadiálogo superpuesto sobre un fondo desenfocado de avatares coloridos.

Elementos de diseño adicionales

A esta sección la llamo "extras". porque tiene más que ver con mi elemento de diálogo demo que lo hace con el elemento del diálogo en general.

Contención del desplazamiento

Cuando se muestra el diálogo, el usuario aún puede desplazarse por la página detrás de él que no quiero:

Normalmente, overscroll-behavior sería mi solución habitual, pero según el especificaciones, no tiene efecto en el diálogo porque no es un puerto de desplazamiento, un desplazador, por lo que no hay nada que evitar. podría usar JavaScript para detectar los nuevos eventos de esta guía, como "cerrado" y "abierto", y activar o desactivar overflow: hidden en el documento, o podría esperar a que :has() permanezca estable en todos los navegadores:

Navegadores compatibles

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

Origen

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

Ahora, cuando se abre un diálogo combinado, el documento HTML tiene overflow: hidden.

El diseño de <form>

Además de ser un elemento muy importante para recopilar la interacción información del usuario, lo utilizo aquí para diseñar el encabezado, el pie de página y . Con este diseño, quiero articular el artículo secundario como área desplazable. Lo logro con grid-template-rows El elemento del artículo recibe 1fr y el formulario en sí tiene el mismo máximo altura como elemento del diálogo. ¿Qué es establecer esta altura y un tamaño de fila firmes? Permite restringir el elemento del artículo y desplazarse cuando se desborda:

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

Captura de pantalla de Herramientas para desarrolladores superpone la información de diseño de cuadrícula en las filas.

Cómo aplicar diseño al diálogo <header>

La función de este elemento es proporcionar un título para el contenido del diálogo y la oferta. un botón de cierre fácil de encontrar. También se le asigna un color de superficie para que parezca detrás del contenido del artículo de diálogo. Estos requisitos dan lugar a una flexbox contenedores, elementos alineados verticalmente que están espaciados a sus bordes y algunos relleno y espacios para dar espacio al título y a los botones de cierre:

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

Captura de pantalla de Herramientas para desarrolladores de Chrome que se superpone con la información de diseño de flexbox en el encabezado del diálogo.

Cómo aplicar estilo al botón para cerrar el encabezado

Como en la demostración se usa el botón Open Props, el botón de cierre es personalizado. en un botón redondo centrado en el icono de la siguiente manera:

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

Captura de pantalla de Herramientas para desarrolladores de Chrome que superpone información sobre el tamaño y el relleno del botón para cerrar el encabezado.

Cómo aplicar diseño al diálogo <article>

El elemento "article" tiene un rol especial en este diálogo: es un espacio destinado a desplazarse en el caso de un diálogo alto o largo.

Para ello, el elemento del formulario superior ha establecido algunos máximos para que proporciona restricciones para que alcance este elemento del artículo si se obtiene demasiado alto. Configura overflow-y: auto para que las barras de desplazamiento solo se muestren cuando sea necesario. contienen desplazamiento dentro de ella con overscroll-behavior: contain, y el resto serán estilos de presentación personalizados:

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

El rol del pie de página es contener menús de botones de acción. Flexbox se usa para lo siguiente: alinea el contenido con el final del eje intercalado del pie de página y, luego, coloca un espacio para dales espacio a los botones.

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

Captura de pantalla de Herramientas para desarrolladores de Chrome que superpone información de diseño de flexbox en el elemento del pie de página.

La menu se usa para contener los botones de acción del diálogo. Se usa un envoltorio Diseño de Flexbox con gap para proporcionar espacio entre los botones Elementos del menú tienen padding, como <ul>. También quito ese estilo porque no lo necesito.

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

Captura de pantalla de Herramientas para desarrolladores de Chrome que superpone información de flexbox en los elementos del menú del pie de página.

Animación

Los elementos de diálogo suelen estar animados porque entran y salen de la ventana. Darles a los diálogos un movimiento de apoyo para esta entrada y salida ayuda a los usuarios a orientarse en el flujo.

Normalmente, el elemento de diálogo solo se puede animar hacia adentro, no hacia afuera. Esto se debe a que el navegador activa o desactiva la propiedad display en el elemento. Anteriormente, en la guía configura la pantalla en cuadrícula y nunca la establece en Ninguna. Esto desbloquea la capacidad de animar la entrada y la salida.

Open Props incluye muchos fotogramas clave como animaciones, lo que facilita organización sea sencilla y legible. Estos son los objetivos de la animación enfoque que apliqué:

  1. El movimiento reducido es la transición predeterminada, un fundido simple de opacidad de entrada y salida.
  2. Si el movimiento es correcto, se agregan animaciones de deslizamiento y escala.
  3. El diseño responsivo para dispositivos móviles del megadiálogo se ajusta para deslizarse hacia afuera.

Una transición predeterminada segura y significativa

Aunque Open Props incluye fotogramas clave para fundido de entrada y salida, prefiero esto. enfoque en capas de transiciones de forma predeterminada con animaciones de fotogramas clave, como posibles actualizaciones. Anteriormente, ya diseñamos la visibilidad del diálogo con opacidad, y se organizan 1 o 0 según el atributo [open]. Para entre 0% y 100%, indícale al navegador la duración y el tipo de flexibilización te gusta:

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

Cómo agregar movimiento a la transición

Si el usuario está de acuerdo con el movimiento, los diálogos mega y mini deben deslizarse. escalar como su entrada y escalar horizontalmente como su salida. Puedes lograr esto con el Consulta de medios de prefers-reduced-motion y algunos objetos de 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;
  }
}

Adaptar la animación de salida para dispositivos móviles

Anteriormente en la sección de estilos, el estilo de diálogo combinado se adapta para dispositivos móviles. los dispositivos se asemejan más a una hoja de acciones, como si se deslizara un pequeño trozo de papel hacia arriba desde la parte inferior de la pantalla y sigue conectado a la parte inferior. La escala de salida no se adapta bien a este nuevo diseño, y podemos adaptar esto con algunas consultas de medios y algunas propiedades abiertas:

@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

Existen varias opciones para agregar con 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
}

Estas adiciones surgen del deseo de descartar la luz (hacer clic en el botón fondo), animaciones y algunos eventos adicionales para mejorar los tiempos. los datos del formulario.

Agregando el descarte claro

Esta tarea es sencilla y un gran complemento para un elemento de diálogo la animación. La interacción se logra observando los clics en el diálogo y aprovechar el evento burbujeante para evaluar qué se hizo clic y solo close() si es el elemento superior:

export default async function (dialog) {
  dialog.addEventListener('click', lightDismiss)
}

const lightDismiss = ({target:dialog}) => {
  if (dialog.nodeName === 'DIALOG')
    dialog.close('dismiss')
}

Observa dialog.close('dismiss'). Se llama al evento y se proporciona una cadena. Otro JavaScript puede recuperar esta cadena para obtener estadísticas sobre cómo se cerró el diálogo. Verás que también proporcioné cadenas de cierre cada vez que llamo la función desde varios botones, para proporcionar contexto a mi aplicación sobre la interacción del usuario.

Agrega eventos de cierre y de cierre

El elemento de diálogo incluye un evento de cierre: se emite inmediatamente cuando el elemento diálogo close(). Como estamos animando este elemento, es tener eventos antes y después de la animación, para que un cambio capte o restablecer el formulario de diálogo. Lo utilizo aquí para administrar la adición de el atributo inert en el diálogo cerrado y, en la demostración, los uso para modificar la lista de avatares si el usuario ha enviado una imagen nueva.

Para lograrlo, crea dos eventos nuevos llamados closing y closed. Después escuchar el evento de cierre integrado en el diálogo. Desde aquí, configura el cuadro de diálogo para inert y envía el evento closing. La siguiente tarea es esperar al animaciones y transiciones para terminar de ejecutarse en el diálogo y, luego, enviar la closed evento.

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 función animationsComplete, que también se usa en Cómo crear un aviso componente, muestra una promesa basada en la la finalización de las promesas de animación y transición. Es por eso que dialogClose es una forma de trabajo asíncrono función; puede entonces await la promesa dio como resultado y avanzar con confianza hacia el evento cerrado.

Cómo agregar eventos abiertos y de apertura

Estos eventos no son tan fáciles de agregar, ya que el elemento de diálogo integrado no se proporciona un evento abierto, como lo hace con un cierre. Utilizo un MutationObserver para proporcionar estadísticas sobre el cambio de atributos del diálogo. En este observador, Observaré los cambios en el atributo open y administraré los eventos personalizados. según corresponda.

De manera similar a como comenzamos los eventos de cierre y de cierre, crea dos eventos nuevos. llamados opening y opened. Se cierra el diálogo en el que escuchamos anteriormente esta vez, usa un observador de mutación creado para ver la atributos.


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

Se llamará a la función de devolución de llamada del observador de mutaciones cuando el cuadro de diálogo atributos se cambian, lo que proporciona la lista de cambios como un array. Iterar en el atributo cambia y busca que attributeName esté abierto. Luego, verifica si el elemento tiene el atributo o no: esto indica si el diálogo o no se abrió Si se abrió, quita el atributo inert y establece el foco. a un elemento que solicita autofocus o el primer elemento button que se encuentra en el diálogo. Por último, similar a la técnica de cierre cerrado, enviar el evento de apertura de inmediato, esperar las animaciones para finalizar y, luego, despachar el evento abierto.

Cómo agregar un evento quitado

En las aplicaciones de una sola página, a menudo se agregan y quitan diálogos en función de las rutas u otras necesidades y estados de la aplicación. Puede ser útil limpiar eventos o cuando se quita un diálogo.

Puedes lograrlo con otro observador de mutaciones. Esta vez, en lugar de observando atributos en un elemento de diálogo, observaremos los elementos secundarios del cuerpo y observa si se eliminan los elementos de diálogo.


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

Se llama a la devolución de llamada del observador de mutaciones cada vez que se agregan o quitan elementos secundarios en el cuerpo del documento. Las mutaciones específicas que se observan son removedNodes que incluyen nodeName de un diálogo. Si se quitó un diálogo, los eventos de clic y cierre se quitan de liberar memoria, y el evento de eliminación personalizado se despacha.

Quita el atributo de carga

Para evitar que la animación del diálogo reproduzca su animación de salida cuando se la agregue a la página o cuando se carga la página, se agregó un atributo de carga al cuadro de diálogo. El siguiente secuencia de comandos espera a que terminen de ejecutarse las animaciones del diálogo y, luego, quita el atributo. Ahora el diálogo está libre para entrar y salir, y para ocultar de manera efectiva una animación que de otro modo distraería.

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

Obtén más información sobre el problema de evitar animaciones de fotogramas clave durante la carga de página aquí.

Todo junto

Ahora que explicamos cada sección, aquí está dialog.js completo. individualmente:

// 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')
}

Cómo usar el módulo dialog.js

Se espera que se llame a la función exportada del módulo y se la pase un diálogo al que se le deben agregar estos nuevos eventos y funcionalidades:

import GuiDialog from './dialog.js'

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

GuiDialog(MegaDialog)
GuiDialog(MiniDialog)

Así, los dos diálogos se actualizan con descarte claro, animación cargando correcciones y más eventos con los que trabajar.

Escucha los nuevos eventos personalizados

Cada elemento de diálogo actualizado ahora puede escuchar cinco eventos nuevos, como este:

MegaDialog.addEventListener('closing', dialogClosing)
MegaDialog.addEventListener('closed', dialogClosed)

MegaDialog.addEventListener('opening', dialogOpening)
MegaDialog.addEventListener('opened', dialogOpened)

MegaDialog.addEventListener('removed', dialogRemoved)

A continuación, se incluyen dos ejemplos de cómo controlar esos eventos:

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()
  }
}

En la demostración que creé con el elemento de diálogo, uso ese evento cerrado los datos del formulario para agregar un nuevo elemento de avatar a la lista. Es un buen momento que el diálogo completó su animación de salida, y luego se animan algunas secuencias de comandos en el nuevo avatar. Gracias a los nuevos eventos, se organiza la experiencia del usuario puede ser más fluido.

Observa dialog.returnValue, que contiene la cadena de cierre que se pasa cuando el valor diálogo close(). En el evento dialogClosed, es fundamental lograr lo siguiente: saber si el diálogo se cerró, canceló o confirmó. Si está confirmada, el secuencia de comandos toma los valores del formulario y lo restablece. El restablecimiento es útil para para que, cuando se vuelva a mostrar el diálogo, esté en blanco y listo para un nuevo envío.

Conclusión

Ahora que sabes cómo lo hice, ¿cómo lo harías?‽ 🙂

Diversifiquemos nuestros enfoques y aprendamos todas las formas de desarrollar en la Web.

Crear una demostración, twittearme vínculos y la agregaré. a la sección de remixes de la comunidad.

Remixes de la comunidad

Recursos