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.
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:
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>
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.
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
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;
}
}
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:
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;
}
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:
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;
}
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);
}
}
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;
}
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);
}
}
Cómo aplicar diseño al diálogo <footer>
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);
}
}
Cómo definir el diseño del menú del pie de página del diálogo
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;
}
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é:
- El movimiento reducido es la transición predeterminada, un fundido simple de opacidad de entrada y salida.
- Si el movimiento es correcto, se agregan animaciones de deslizamiento y escala.
- 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
- @GrimLink con un 3-in-1 .
- @mikemai2awesome con un
un remix que no cambia la
propiedad
display
. - @geoffrich_ con Elegante y agradable Pulido elegante FLIP.
Recursos
- Código fuente en GitHub
- Avatares de garabatos