Reemplace los diálogos de JavaScript con un nuevo diálogo HTML trucos CSS
¿Sabe cómo funcionan los diálogos de JavaScript para alertar, verificar y pedirle a un usuario que actúe? Supongamos que desea reemplazar los cuadros de diálogo de JavaScript con el nuevo cuadro de diálogo HTML.
Dejame explicar.
Recientemente trabajé en un proyecto con muchas llamadas de API y comentarios de usuarios recopilados con cuadros de diálogo de JavaScript. Mientras esperaba que otro desarrollador codificara <Modal />
componente que usé alert()
, confirm()
y prompt()
en mi código. Por ejemplo:
const deleteLocation = confirm('Delete location');
if (deleteLocation) {
alert('Location deleted');
}
Entonces pégame: lo entiendes muchas características relacionadas con la moda gratis con alert()
, confirm()
y prompt()
que a menudo se pasan por alto:
- Este es un modal real. Como en este caso, siempre estará en la parte superior de la pila, incluso en la parte superior
<div>
sz-index: 99999;
. - Disponible con teclado. imprenta
Enter
aceptar yEscape
cancelar. - Conveniente para el lector de pantalla. Cambia el enfoque y permite que el contenido modal se lea en voz alta.
- Captura el foco. Prensado
Tab
no llegará a ningún elemento enfocado en la página de inicio, pero en Firefox y Safari realmente cambia el enfoque a la interfaz de usuario del navegador. Sin embargo, lo extraño es que no puede cambiar el enfoque a los botones "aceptar" o "rechazar" para todos los tipos navegador usandoTab
llave. - Mantiene las preferencias del consumidor. Recibimos soporte automático para el modo claro y oscuro directamente desde la caja.
- Detiene la ejecución del código.Además, está esperando la entrada del usuario.
Estos tres métodos de JavaScript funcionan el 99 % de las veces cuando necesito una de estas funciones. Entonces, ¿por qué yo, o cualquier otro desarrollador web, no los uso? Probablemente porque parecen errores del sistema que no se pueden estilizar. Otra gran consideración: hay un movimiento hacia su retiro, primero de los marcos incrustados entre dominios y, como dicen, completamente de la plataforma web, aunque también parece que los planes para esto se han retrasado.
Dada esta gran atención, ¿cuáles son alert()
, confirm()
y prompt()
¿Tenemos que reemplazar alternativas? Es posible que haya oído hablar de <dialog>
elemento HTML y esto es lo que quiero ver en este artículo, usándolo junto con JavaScript class
.
Es imposible para completamente reemplazar los diálogos de Javascript con una funcionalidad idéntica, pero si usamos showModal()
método de <dialog>
combinado con un Promise
esto también puede resolve
(aceptar) o reject
(rechazo) - entonces tenemos algo casi Demonios, mientras estamos en eso, agreguemos sonido al cuadro de diálogo HTML, ¡como los cuadros de diálogo reales del sistema!
Si desea ver la demostración de inmediato, esta aquí.
Clase de diálogo
Primero, necesitamos JavaScript básico Class
s settings
objeto que se fusionará con la configuración predeterminada. Estos ajustes se utilizarán para todos los cuadros de diálogo, a menos que los sobrescriba cuando los llame (pero hablaremos de eso más adelante).
export default class Dialog {
constructor(settings = {}) {
this.settings = Object.assign(
{
/* DEFAULT SETTINGS - see description below */
},
settings
)
this.init()
}
Los ajustes son:
accept
: Esta es la etiqueta del botón "Aceptar".bodyClass
: Esta es una clase CSS que se agrega a<body>
elemento cuando el cuadro de diálogo estáopen
y<dialog>
no es compatible con el navegador.cancel
: Esta es la etiqueta del botón "Cancelar".dialogClass
: Esta es una clase CSS personalizada agregada a<dialog>
elemento.message
: Este es el contenido interior<dialog>
.soundAccept
: Esta es la URL del archivo de sonido que reproduciremos cuando el usuario haga clic en el botón "Aceptar".soundOpen
: Esta es la URL del archivo de sonido que reproduciremos cuando el usuario abra el cuadro de diálogo.template
: Esta es una pequeña plantilla HTML opcional que se inyecta en<dialog>
.
La plantilla original para reemplazar los diálogos de JavaScript
EN init
método, agregaremos una función de ayuda para encontrar soporte para el cuadro de diálogo HTML en los navegadores y configurar el HTML básico:
init() {
// Testing for <dialog> support
this.dialogSupported = typeof HTMLDialogElement === 'function'
this.dialog = document.createElement('dialog')
this.dialog.dataset.component = this.dialogSupported ? 'dialog' : 'no-dialog'
this.dialog.role="dialog"
// HTML template
this.dialog.innerHTML = `
<form method="dialog" data-ref="form">
<fieldset data-ref="fieldset" role="document">
<legend data-ref="message" id="${(Math.round(Date.now())).toString(36)}">
</legend>
<div data-ref="template"></div>
</fieldset>
<menu>
<button data-ref="cancel" value="cancel"></button>
<button data-ref="accept" value="default"></button>
</menu>
<audio data-ref="soundAccept"></audio>
<audio data-ref="soundOpen"></audio>
</form>`
document.body.appendChild(this.dialog)
// ...
}
Comprobación de mantenimiento
La forma de soportar navegadores <dialog>
Fue un tiempo largo. Safari se lo llevó Recientemente. Firefox más aúnAunque no <form method="dialog">
parte Entonces, debemos agregar type="button"
a los botones "Aceptar" y "Cancelar", que imitamos. De lo contrario, lo harán POST
formulario y causar una actualización de la página y queremos evitar eso.
<button${this.dialogSupported ? '' : ` type="button"`}...></button>
Enlaces de nodos DOM
¿Todos se dieron cuenta? data-ref
-atributos? Los usaremos para obtener referencias a los nodos DOM:
this.elements = {}
this.dialog.querySelectorAll('[data-ref]').forEach(el => this.elements[el.dataset.ref] = el)
Hasta aquí, this.elements.accept
es una referencia al botón "Aceptar" y this.elements.cancel
se refiere al botón "Cancelar".
Atributos del botón
Para los lectores de pantalla necesitamos aria-labelledby
un atributo que indica el ID de la etiqueta que describe el cuadro de diálogo, es decir <legend>
etiqueta y contendrá message
.
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
Ese id
• Esta es una referencia única a esta parte de <legend>
elemento:
this.dialog.setAttribute('aria-labelledby', this.elements.message.id)
Botón cancelar.
¡Buenas noticias! El cuadro de diálogo HTML tiene un cancel()
método que hace que sea más fácil reemplazar los diálogos de JavaScript que llaman confirm()
Liberemos este evento cuando hagamos clic en el botón Cancelar:
this.elements.cancel.addEventListener('click', () => {
this.dialog.dispatchEvent(new Event('cancel'))
})
Este es el marco para el nuestro. <dialog>
para reemplazar alert()
, confirm()
y prompt()
.
Reponer navegadores no compatibles
Necesitamos ocultar el cuadro de diálogo HTML para los navegadores que no lo admiten. Para hacer esto, envolveremos la lógica de mostrar y ocultar el cuadro de diálogo en un nuevo método, toggle()
:
toggle(open = false) {
if (this.dialogSupported && open) this.dialog.showModal()
if (!this.dialogSupported) {
document.body.classList.toggle(this.settings.bodyClass, open)
this.dialog.hidden = !open
/* If a `target` exists, set focus on it when closing */
if (this.elements.target && !open) {
this.elements.target.focus()
}
}
}
/* Then call it at the end of `init`: */
this.toggle()
Navegación por teclado
Luego, implementemos una forma de capturar el foco para que el usuario pueda dividir los botones en el cuadro de diálogo sin salir accidentalmente del cuadro de diálogo. Hay muchas maneras de hacer esto. Me gusta cómo es el CSS, pero desafortunadamente no es confiable. En cambio, tomemos todos los elementos enfocados del cuadro de diálogo como un NodeList
y almacenarlo en this.focusable
:
getFocusable() {
return [...this.dialog.querySelectorAll('button,[href],select,textarea,input:not([type="hidden"]),[tabindex]:not([tabindex="-1"])')]
}
Luego agregaremos un keydown
detector de eventos, procesando toda nuestra lógica de navegación del teclado:
this.dialog.addEventListener('keydown', e => {
if (e.key === 'Enter') {
if (!this.dialogSupported) e.preventDefault()
this.elements.accept.dispatchEvent(new Event('click'))
}
if (e.key === 'Escape') this.dialog.dispatchEvent(new Event('cancel'))
if (e.key === 'Tab') {
e.preventDefault()
const len = this.focusable.length - 1;
let index = this.focusable.indexOf(e.target);
index = e.shiftKey ? index-1 : index+1;
if (index < 0) index = len;
if (index > len) index = 0;
this.focusable[index].focus();
}
})
Para Ingresardebemos prevenir <form>
desde el envío a los navegadores donde <dialog>
el elemento no es compatible. Escape
transmitirá un cancel
evento. Prensado Sección key encontrará el elemento actual en la lista de nodos con elementos que se pueden enfocar, this.focusable
y concéntrese en el elemento siguiente (o en el anterior si mantiene presionado Cambio tecla al mismo tiempo).
Show <dialog>
Ahora mostremos el cuadro de diálogo, por lo que necesitamos un pequeño método que combine opciones settings
objeto con valores predeterminados En este objeto, al igual que el valor predeterminado settings
objeto: podemos agregar o cambiar la configuración de un cuadro de diálogo específico.
open(settings = {}) {
const dialog = Object.assign({}, this.settings, settings)
this.dialog.className = dialog.dialogClass || ''
/* set innerText of the elements */
this.elements.accept.innerText = dialog.accept
this.elements.cancel.innerText = dialog.cancel
this.elements.cancel.hidden = dialog.cancel === ''
this.elements.message.innerText = dialog.message
/* If sounds exists, update `src` */
this.elements.soundAccept.src = dialog.soundAccept || ''
this.elements.soundOpen.src = dialog.soundOpen || ''
/* A target can be added (from the element invoking the dialog */
this.elements.target = dialog.target || ''
/* Optional HTML for custom dialogs */
this.elements.template.innerHTML = dialog.template || ''
/* Grab focusable elements */
this.focusable = this.getFocusable()
this.hasFormData = this.elements.fieldset.elements.length > 0
if (dialog.soundOpen) {
this.elements.soundOpen.play()
}
this.toggle(true)
if (this.hasFormData) {
/* If form elements exist, focus on that first */
this.focusable[0].focus()
this.focusable[0].select()
}
else {
this.elements.accept.focus()
}
}
¡Puaj! Eso fue todo mucho códigoAhora podemos mostrar <dialog>
elemento en todos los navegadores. Sin embargo, debemos imitar la funcionalidad que espera la entrada del usuario después de la ejecución, como nativo alert()
, confirm()
y prompt()
métodos.. Para esto necesitamos un Promise
y un nuevo método que estoy llamando waitForUser()
:
waitForUser() {
return new Promise(resolve => {
this.dialog.addEventListener('cancel', () => {
this.toggle()
resolve(false)
}, { once: true })
this.elements.accept.addEventListener('click', () => {
let value = this.hasFormData ?
this.collectFormData(new FormData(this.elements.form)) : true;
if (this.elements.soundAccept.src) this.elements.soundAccept.play()
this.toggle()
resolve(value)
}, { once: true })
})
}
Este método devuelve un Promise
Como parte de esto, estamos agregando oyentes a los eventos de "opción de exclusión" y "aceptación" que están permitidos false
(rechazo), o true
(Acepto). Si formData
existe (para diálogos personalizados o prompt
), se recogerán con un metodo auxiliarluego vuelve al objeto:
collectFormData(formData) {
const object = {};
formData.forEach((value, key) => {
if (!Reflect.has(object, key)) {
object[key] = value
return
}
if (!Array.isArray(object[key])) {
object[key] = [object[key]]
}
object[key].push(value)
})
return object
}
Podemos eliminar inmediatamente a los oyentes de los eventos usando { once: true }
.
Para ser simple, no uso reject()
más bien, solo decide false
.
Esconder <dialog>
Anteriormente añadimos oyentes a los eventos para el integrado cancel
evento. Llamamos a este evento cuando el usuario hace clic en el botón "Cancelar". o presión Escapar llave cancel
evento elimina open
atributo en <dialog>
ocultándolo así.
A donde :focus
?
En nuestro open()
método, nos enfocamos en el primer campo del formulario de enfoque o Botón Aceptar:
if (this.hasFormData) {
this.focusable[0].focus()
this.focusable[0].select()
}
else {
this.elements.accept.focus()
}
¿Pero es esto correcto? El ejemplo de "diálogo modal" de W3este es de hecho el caso El ejemplo de Scott Ohara sin embargo, la atención se centra en el diálogo en sí, lo que tiene sentido si el lector de pantalla necesita leer el texto que definimos en aria-labelledby
atributo anterior. No estoy seguro de cuál es el correcto o el mejor, pero si queremos usar el método de Scott, debemos agregar tabindex="-1"
a <dialog>
en nuestro init
método:
this.dialog.tabIndex = -1
Entonces, en open()
método, reemplazaremos el código de enfoque con esto:
this.dialog.focus()
podemos comprobar activeElement
(el elemento que tiene el foco) en cualquier momento en DevTools haciendo clic en el icono del "ojo" y escribiendo document.activeElement
en la consola Pruebe las pestañas para ver cómo se actualiza:
Agregar una advertencia, confirmación y aviso
Finalmente estamos listos para agregar alert()
, confirm()
y prompt()
de nuestro Dialog
clase. Estos serán pequeños métodos auxiliares que reemplazarán los diálogos de JavaScript y la sintaxis original de estos métodos. todos gritan open()
un método que creamos antes, pero con un settings
un objeto que corresponde a la forma en que activamos los métodos originales.
Comparemos con la sintaxis original.
alert()
generalmente se desencadena de la siguiente manera:
window.alert(message);
En nuestro diálogo agregaremos alert()
un método que imitará esto:
/* dialog.alert() */
alert(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { cancel: '', message, template: '' })
this.open(settings)
return this.waitForUser()
}
lo configuramos cancel
y template
a cadenas vacías para que, incluso si establecimos valores predeterminados anteriormente, no se ocultarán y solo message
y accept
son exhibidos.
confirm()
generalmente se desencadena de la siguiente manera:
window.confirm(message);
En nuestra versión, similar a alert()
creamos un método personalizado que muestra message
, cancel
y accept
elementos:
/* dialog.confirm() */
confirm(message, config = { target: event.target }) {
const settings = Object.assign({}, config, { message, template: '' })
this.open(settings)
return this.waitForUser()
}
prompt()
generalmente se desencadena de la siguiente manera:
window.prompt(message, default);
Aquí debemos agregar un template
s <input>
que nos envolveremos <label>
:
/* dialog.prompt() */
prompt(message, value, config = { target: event.target }) {
const template = `
<label aria-label="${message}">
<input name="prompt" value="${value}">
</label>`
const settings = Object.assign({}, config, { message, template })
this.open(settings)
return this.waitForUser()
}
{ target: event.target }
es una referencia al elemento DOM que llama al método. Lo usaremos para centrarnos en este elemento nuevamente cuando cerremos <dialog>
devolviendo al usuario a donde estaba antes de que se activara el cuadro de diálogo.
Necesitamos probar esto
Es hora de probar y asegurarse de que todo funcione como se esperaba. Vamos a crear un nuevo archivo HTML, importar la clase e instanciar:
<script type="module">
import Dialog from './dialog.js';
const dialog = new Dialog();
</script>
¡Pruebe los siguientes casos de uso uno por uno!
/* alert */
dialog.alert('Please refresh your browser')
/* or */
dialog.alert('Please refresh your browser').then((res) => { console.log(res) })
/* confirm */
dialog.confirm('Do you want to continue?').then((res) => { console.log(res) })
/* prompt */
dialog.prompt('The meaning of life?', 42).then((res) => { console.log(res) })
Luego mire la consola mientras haga clic en "Aceptar" o "Cancelar". Vuelva a intentarlo mientras presiona Escapar o Ingresar llaves en su lugar.
Asíncrono / Espera
También podemos usar async/await
manera de hacer esto. Estamos reemplazando aún más los diálogos de JavaScript al imitar la sintaxis original, pero esto requiere que la función contenedora sea async
mientras que el código en él requiere await
palabra clave:
document.getElementById('promptButton').addEventListener('click', async (e) => {
const value = await dialog.prompt('The meaning of life?', 42);
console.log(value);
});
Diseño de navegadores cruzados
¡Ahora tenemos un cuadro de diálogo HTML completamente funcional, útil para navegadores cruzados y un lector de pantalla que reemplaza los cuadros de diálogo de JavaScript! Cubrimos mucho. Pero el estilo puede usar mucho amor. Usemos el existente data-component
y data-ref
-Atributos para agregar estilo a diferentes navegadores: ¡no se necesitan clases adicionales u otros atributos!
Usaremos CSS :where
pseudo-selector para mantener nuestros estilos predeterminados libres de detalles:
:where([data-component*="dialog"] *) {
box-sizing: border-box;
outline-color: var(--dlg-outline-c, hsl(218, 79.19%, 35%))
}
:where([data-component*="dialog"]) {
--dlg-gap: 1em;
background: var(--dlg-bg, #fff);
border: var(--dlg-b, 0);
border-radius: var(--dlg-bdrs, 0.25em);
box-shadow: var(--dlg-bxsh, 0px 25px 50px -12px rgba(0, 0, 0, 0.25));
font-family:var(--dlg-ff, ui-sansserif, system-ui, sans-serif);
min-inline-size: var(--dlg-mis, auto);
padding: var(--dlg-p, var(--dlg-gap));
width: var(--dlg-w, fit-content);
}
:where([data-component="no-dialog"]:not([hidden])) {
display: block;
inset-block-start: var(--dlg-gap);
inset-inline-start: 50%;
position: fixed;
transform: translateX(-50%);
}
:where([data-component*="dialog"] menu) {
display: flex;
gap: calc(var(--dlg-gap) / 2);
justify-content: var(--dlg-menu-jc, flex-end);
margin: 0;
padding: 0;
}
:where([data-component*="dialog"] menu button) {
background-color: var(--dlg-button-bgc);
border: 0;
border-radius: var(--dlg-bdrs, 0.25em);
color: var(--dlg-button-c);
font-size: var(--dlg-button-fz, 0.8em);
padding: var(--dlg-button-p, 0.65em 1.5em);
}
:where([data-component*="dialog"] [data-ref="accept"]) {
--dlg-button-bgc: var(--dlg-accept-bgc, hsl(218, 79.19%, 46.08%));
--dlg-button-c: var(--dlg-accept-c, #fff);
}
:where([data-component*="dialog"] [data-ref="cancel"]) {
--dlg-button-bgc: var(--dlg-cancel-bgc, transparent);
--dlg-button-c: var(--dlg-cancel-c, inherit);
}
:where([data-component*="dialog"] [data-ref="fieldset"]) {
border: 0;
margin: unset;
padding: unset;
}
:where([data-component*="dialog"] [data-ref="message"]) {
font-size: var(--dlg-message-fz, 1.25em);
margin-block-end: var(--dlg-gap);
}
:where([data-component*="dialog"] [data-ref="template"]:not(:empty)) {
margin-block-end: var(--dlg-gap);
width: 100%;
}
Puedes diseñarlos como quieras, por supuesto. Esto es lo que le dará el CSS anterior:
Para sobrescribir estos estilos y usar los suyos propios, agregue una clase a dialogClass
,
dialogClass: 'custom'
... luego agregue la clase en CSS y actualice los valores de las propiedades CSS personalizadas:
.custom {
--dlg-accept-bgc: hsl(159, 65%, 75%);
--dlg-accept-c: #000;
/* etc. */
}
Ejemplo de un diálogo personalizado
¿Qué pasa si el estándar alert()
, confirm()
y prompt()
los métodos que imitamos no funcionarán para su uso específico? De hecho, podemos hacer un poco más para hacer <dialog>
más flexible para cubrir una mayor parte del contenido, los botones y la funcionalidad que hemos cubierto hasta ahora, y no mucho más trabajo.
Solía bromear con la idea de agregar sonido al cuadro de diálogo. Vamos a hacerlo.
Puedes usar template
propiedad de settings
objeto para inyectar más HTML. Aquí hay un ejemplo personalizado llamado por un <button>
s id="btnCustom"
que activa un pequeño sonido divertido de un archivo MP3:
document.getElementById('btnCustom').addEventListener('click', (e) => {
dialog.open({
accept: 'Sign in',
dialogClass: 'custom',
message: 'Please enter your credentials',
soundAccept: 'https://assets.yourdomain.com/accept.mp3',
soundOpen: 'https://assets.yourdomain.com/open.mp3',
target: e.target,
template: `
<label>Username<input type="text" name="username" value="admin"></label>
<label>Password<input type="password" name="password" value="password"></label>`
})
dialog.waitForUser().then((res) => { console.log(res) })
});
Demo en vivo
¡Aquí hay un bolígrafo con todo lo que creamos! Abra la consola, haga clic en los botones y juegue con los cuadros de diálogo haciendo clic en los botones y usando el teclado para aceptar y cancelar.
¿Entonces, qué piensas? ¿Es esta una buena manera de reemplazar los cuadros de diálogo de JavaScript con el cuadro de diálogo HTML más nuevo? ¿O has probado a hacerlo de otra forma? Dime en los comentarios!
Deja una respuesta