DEV Community

Franklin
Franklin

Posted on

El HTML no necesita más reinvención: necesita confianza en la plataforma

Hermano estructural de El CSS no necesita más poder: necesita mejores límites — la misma distinción, en otra capa.

También disponible en Inglés

El problema

Se abre un modal. Un usuario de lector de pantalla navega con la tecla Tab hacia adelante de la forma en que se supone que debe funcionar cualquier interacción basada solo en teclado: contenida, predecible, un control tras otro hasta que el modal se cierra. Luego finaliza una petición, se renderiza una nueva opción dentro del modal y la siguiente pulsación de Tab aterriza en un lugar al que nunca debió llegar: un enlace en la página detrás del modal, aún visible bajo una capa atenuada, aún técnicamente enfocable.

Nadie eliminó el focus trap. Sigue ejecutándose, sigue escuchando Tab y Shift+Tab, sigue haciendo exactamente lo que hacía en el momento en que se abrió el modal. Lo que cambió fue el DOM debajo de él. La trampa se construyó alrededor de un conjunto de elementos enfocables capturados una sola vez, al abrir. El contenido real del modal no se mantuvo tan simple.

Por qué existe el problema

Durante mucho tiempo, HTML no tuvo ningún elemento que significara diálogo modal. Un <div> estilizado para parecerlo sigue siendo solo un div: sin contrato de teclado, sin forma de decirle al navegador que debe atrapar el foco, atenuar la página detrás o devolver el foco a lo que estaba enfocado antes de abrirse. Construir ese comportamiento en JavaScript no fue un error. Era la única opción disponible, y bastantes interfaces en producción aún funcionan con la versión construida durante esa brecha.

Pero construir el comportamiento significa asumirlo. Un focus trap manual tiene que enumerar cada elemento enfocable dentro del modal, redirigir el foco en los límites, mantener inerte el fondo y restaurar el foco correctamente al cerrar. Nada de eso es un costo de una sola vez. Es una obligación continua de corrección, porque lo que era cierto sobre los contenidos del modal cuando se escribió el código no tiene garantía de seguir siendo cierto cada vez que el modal se abre en producción.

El primer principio

El navegador no es solo un motor de renderizado. Para un conjunto definido de elementos nativos y APIs del navegador, también implementa el contrato de interacción que viene con ellos: qué sucede al abrir, qué sucede al cerrar, a dónde va el foco, qué permanece accesible por teclado y qué no. Un <select> nativo siempre ha funcionado de esta manera; nadie construye navegación por teclado manual para un menú desplegable basado en el elemento real. <dialog> extiende la misma idea a las interfaces modales.

Ese contrato vive en la plataforma. Eso significa que el navegador es responsable de mantenerlo correcto a medida que su propia implementación cambia entre versiones y dispositivos. El código de la aplicación que reimplementa el mismo contrato no solo escribe más código: asume una obligación que la plataforma ya estaba cumpliendo, sin heredar los recursos que la plataforma tiene para seguir cumpliéndola.

Demostración del principio

Reduce un modal personalizado a lo que realmente tiene que coordinar:

estado de apertura
ubicación del foco al abrir
contención del foco mientras está abierto
descarte (Escape, clic en el fondo)
restauración del foco al cerrar
fondo inerte mientras está abierto
apilamiento visual sobre el resto de la página
Enter fullscreen mode Exit fullscreen mode

Cada línea de esa lista es trabajo propiedad de la aplicación: algo que el código tiene que hacer bien, mantener bien y volver a verificar cada vez que cambia el contenido del modal.

<dialog> traslada cada uno de esos puntos al contrato de la plataforma:

<dialog id="preferences">
  <form method="dialog">
    <p>Update your preferences.</p>
    <button autofocus>Save</button>
  </form>
</dialog>
Enter fullscreen mode Exit fullscreen mode
dialog::backdrop {
  background: rgb(0 0 0 / 50%);
}
Enter fullscreen mode Exit fullscreen mode
document.getElementById('preferences').showModal();
Enter fullscreen mode Exit fullscreen mode

showModal() abre el diálogo, lo promueve a la capa superior (top layer) del navegador por encima del resto de la página, hace inerte todo lo que está fuera y contiene el foco dentro de él automáticamente. Hacer clic en Save no requiere ningún controlador de clic para cerrar nada: method="dialog" significa que enviar el formulario cierra el diálogo y restaura el foco a lo que estaba enfocado antes de abrirse, por sí solo. Escape hace lo mismo sin código en absoluto. El fondo atenuado detrás no es un elemento separado que la aplicación construyó y tuvo que apilar correctamente; ::backdrop es un seudoelemento que el navegador genera para este propósito exacto en el momento en que el diálogo se vuelve modal. Para los casos sin formulario que enviar —un cierre por tiempo límite o un control fuera del diálogo— .close() ofrece el mismo efecto de forma imperativa.

La aplicación declara lo que quiere: esto es un diálogo, ábrelo de forma modal, permite que el envío nativo y Escape lo cierren. El navegador asume la mecánica intermedia.

La diferencia no es el número de líneas. Un focus trap personalizado bien escrito y una llamada a <dialog> pueden no diferir mucho en un diff. La diferencia es quién asume la responsabilidad la próxima vez que cambien los contenidos del modal: la plataforma, que ya contempla un DOM dinámico dentro de un diálogo nativo, o la aplicación, que tiene que notar que la suposición se rompió y corregirla.

El punto de dolor

Un modal de producción llevaba aproximadamente 200 líneas de lógica manual de gestión de foco: enumerar hijos enfocables al abrir, redirigir Tab y Shift+Tab en el primero y el último de ellos, restaurar el foco al cerrar. Funcionaba, y había funcionado durante un tiempo.

El fallo se remonta a una suposición arraigada en esa lógica: el conjunto de elementos enfocables dentro del modal se capturó una sola vez, al abrir, y se trató como fijo mientras el modal permaneciera abierto. Eso se mantuvo para el contenido original del modal. Dejó de mantenerse en el momento en que una variante del modal comenzó a cargar opciones adicionales de forma asíncrona después de abrirse: la trampa siguió redirigiendo el foco contra una lista de elementos que ya no coincidía con lo que realmente estaba en pantalla, y el foco pudo salir directamente del modal hacia la página detrás de él.

La aplicación había escrito un algoritmo estático alrededor de un documento que no era estático. Eso no es un error de código en el sentido ordinario. Es el costo continuo de asumir un contrato que la plataforma ya define: cada suposición que el código hace sobre el DOM tiene que seguir siendo cierta indefinidamente, o el código tiene que seguir actualizándose para coincidir.

Notas desde el pase del sábado elimina la trampa manual, reconstruye el mismo modal sobre <dialog> y rastrea exactamente qué responsabilidades se trasladan al navegador y cuáles no, porque el <dialog> nativo pone fin a la carga de mantenimiento del focus trap, pero no hace automática cada decisión restante sobre los contenidos del diálogo.

La lección general

Esto no es un argumento de que JavaScript no deba tocar los modales, ni de que los elementos nativos sean siempre la opción correcta. Bastantes patrones interactivos realmente no tienen un equivalente nativo, y el código de la aplicación es el único lugar donde puede vivir ese comportamiento. La pregunta que vale la pena hacerse primero es más estrecha: ¿este comportamiento ya tiene un propietario?

El Artículo 3 preguntaba a dónde pertenece una decisión estructural cuando el contexto necesario para tomarla está dividido entre los límites de los componentes. Esta es la misma pregunta orientada al comportamiento en lugar de la estructura: quién es el propietario de un contrato de interacción cuando la plataforma ya define uno. En ambos casos, el error no es construir algo. Es construir algo sin verificar primero si ya existe, completamente especificado, una capa más abajo.

Cada sistema acumula código que reimplementa algo que una capa inferior ya proporciona, generalmente porque nadie lo verificó antes de escribirlo. La verificación cuesta unos minutos. Mantener una suposición incorrecta sobre un contrato que no necesitabas asumir cuesta mucho más, de forma indefinida, hasta que alguien se da cuenta.

Top comments (0)