Planteamiento y contexto
Diseña una barra lateral móvil arrastrable. Los usuarios de escritorio esperan que Escape la cierre, los usuarios de Android esperan el gesto o botón de retroceso, y un formulario no guardado debe requerir confirmación. Usa CloseWatcher para un flujo de cierre único y explica cancel, close, requestClose(), destroy(), el foco, múltiples watchers, navegadores no compatibles y los límites del historial.
MDN describe CloseWatcher como la interfaz para hacer que los componentes personalizados respondan a acciones de cierre específicas del dispositivo. El estándar HTML también define la agrupación de close-watchers y las protecciones contra el abuso de las acciones del historial. Este artículo sintetiza material público y no pretende ser una pregunta de entrevista específica de ninguna empresa.
Qué está evaluando el entrevistador
El entrevistador quiere ver si distingues entre una solicitud de cierre y el cierre inmediato, si previenes el cierre durante la fase cancel y si mantienes un único manejador close responsable de la limpieza de la UI. Una respuesta sólida menciona la activación del usuario, la agrupación de múltiples watchers, la duración de AbortSignal, el retorno del foco y un fallback con botón explícito; una respuesta débil solo escucha el evento keydown.
Preguntas para clarificar primero
- ¿La barra lateral es modal, no modal o está vinculada a un estado de navegación del historial?
- ¿El contenido no guardado debe bloquear cada solicitud de cierre o solo cuando campos específicos están modificados (dirty)?
- ¿Un gesto de retroceso debe cerrar el componente o navegar a la entrada anterior del historial?
- ¿Los navegadores de destino admiten CloseWatcher y la funcionalidad puede degradarse a un botón de cierre explícito?
Una respuesta de 30 segundos
“Normalizaría cada punto de entrada de cierre como una solicitud de cierre. Cuando la barra lateral se abre, creo un CloseWatcher con un AbortSignal. En cancel, verifico si el formulario está modificado; prevengo la solicitud y muestro confirmación cuando lo está, de lo contrario la permito. close oculta el componente, restaura el foco y limpia los recursos, y el botón de cierre explícito sigue el mismo camino. Los navegadores no compatibles mantienen el botón y un fallback limitado para Escape; el navegador o el enrutador son dueños de la navegación hacia atrás cuando no hay ningún componente cerrable.”
Solución paso a paso
Separa las dos acciones. requestClose() simula una solicitud de cierre del dispositivo y dispara cancel; si no se previene, le sigue close. close() dispara close inmediatamente sin cancel, mientras que destroy() solo desactiva el watcher. Después de guardar, desmontar o salir de una ruta, elige explícitamente una solicitud de cierre o una limpieza forzada en lugar de mezclar los significados.
function openDrawer() {
const controller = new AbortController();
const watcher = new CloseWatcher({ signal: controller.signal });
watcher.addEventListener("cancel", (event) => {
if (!formIsDirty()) return;
event.preventDefault();
showDiscardConfirmation(() => watcher.close());
});
watcher.addEventListener("close", () => {
hideDrawer();
restoreFocusToTrigger();
controller.abort();
});
return { watcher, controller };
}El diálogo de confirmación no debe crear recursivamente un watcher incerrable. Después de una confirmación explícita de descarte, llama a close() del watcher actual; cancelar la confirmación deja la barra lateral abierta. Después de guardar, limpia el estado modificado y llama a requestClose() para que se ejecute la misma ruta de limpieza.
El comportamiento del foco depende del componente. Una barra lateral modal debe mover el foco a un encabezado comprensible o al primer control y devolverlo al activador al cerrarse. Un panel no modal no debe robar el foco, pero aún necesita un botón de cierre accesible y un estado visible. Una solicitud de cierre debe actualizar los nombres accesibles, el scrim, el bloqueo de desplazamiento y el orden del teclado, no solo alternar CSS.
Múltiples watchers tienen un límite especial. Sin activación del usuario, la especificación permite que los watchers se agrupen, por lo que una solicitud de cierre puede cerrar varios de ellos. No crees un watcher incondicional para cada panel pequeño. Prefiere que un componente cerrable de nivel superior sea el propietario del watcher; los hijos solicitan el cierre a través de eventos, y el desmontaje llama a destroy() o aborta la señal asociada.
Un gesto de retroceso no es simplemente un clic. La plataforma puede tratarlo como una navegación por el historial o como una solicitud de cierre, y el administrador de close-watchers del navegador selecciona el objetivo. La aplicación debe confirmar solo cuando puede interceptar y el componente está abierto; sin ningún componente cerrable, el retroceso normal del historial debe continuar. No bloquees globalmente popstate o los gestos de retroceso para proteger un único formulario local.
La detección de capacidades protege la ruta principal. Verifica window.CloseWatcher antes de construir uno. Cuando no sea compatible, mantén el botón de cierre explícito, agrega un listener de Escape limitado si es necesario y deja que el enrutador existente maneje el comportamiento de retroceso. No afirmes que los listeners personalizados reproducen completamente la semántica de retroceso de Android. Mide el soporte, las solicitudes bloqueadas, los descartes posteriores a la confirmación y los fallos de restauración del foco.
Ejemplo de una respuesta sólida
Normalizaría cada entrada de cierre de la barra lateral como una solicitud de cierre. Al abrir, creo un CloseWatcher con un AbortSignal. cancel solo verifica el estado no guardado: cuando está modificado, llamo a preventDefault() y muestro la confirmación; cuando está limpio, permito la solicitud. Después de confirmar el descarte, llamo a close(); después de guardar, limpio el estado modificado y llamo a requestClose(). close es el único punto de limpieza de la UI: oculta el panel, restaura el foco al activador, elimina el bloqueo de desplazamiento y termina el watcher.
Limitaría la cantidad de watchers para que las instancias no activadas no se agrupen accidentalmente; el desmontaje o los cambios de ruta las destruyen. Los componentes modales y no modales reciben diferentes reglas de foco y scrim, y los gestos de retroceso llegan al historial del navegador cuando no hay ningún componente abierto. Los navegadores sin CloseWatcher mantienen un botón explícito y un fallback de teclado limitado sin bloquear la navegación central. Las métricas validan el comportamiento de cierre, confirmación y accesibilidad.
Errores comunes
- Síntoma → Usar
close()para cada punto de entrada; por qué falla → Omite la fase de confirmación de no guardado; solución → La intención del usuario y de la plataforma usarequestClose(), mientras que la limpieza forzada usaclose(). - Síntoma → Escuchar solo Escape; por qué falla → Se pierden el retroceso de Android y otras acciones de cierre del dispositivo; solución → Usa CloseWatcher y mantén un botón explícito.
- Síntoma → Crear un watcher para cada panel secundario; por qué falla → Los watchers no activados pueden agruparse; solución → Deja que el componente cerrable de nivel superior posea una única instancia.
- Síntoma → Dejar el foco en un nodo oculto; por qué falla → Los usuarios de teclado y tecnologías de asistencia pierden su posición; solución → Almacena el activador y restaura el foco en
close. - Síntoma → Bloquear los eventos de retroceso globalmente; por qué falla → La navegación por el historial se rompe cuando no hay ningún componente abierto; solución → Bloquea solo la solicitud de cierre de un componente abierto cuando se requiera confirmación.
Preguntas de seguimiento y respuestas
¿Cuándo deberías usar requestClose() frente a close()?
Usa requestClose() para la intención del usuario o de la plataforma porque le da a cancel la oportunidad de evitar el cierre. Usa close() después de una confirmación explícita de descarte, durante la limpieza al desmontar o cuando el cierre deba ser inmediato. Ambos deben converger en el mismo manejador de limpieza close.
¿El diálogo de confirmación de no guardado debe tener su propio CloseWatcher?
No necesariamente. Dale al diálogo de confirmación un botón de cierre explícito y una ruta de foco accesible para evitar la recursión o el cierre agrupado con el watcher padre. El padre llama a close() después de la confirmación; cerrar el hijo solo cambia el estado de confirmación.
¿Cómo manejas varios paneles abiertos?
Define una pila o propiedad de nivel superior: una solicitud cierra solo el componente superior que sea realmente cerrable, mientras que los demás permanecen abiertos. No confíes en la agrupación implícita de watchers no activados como tu pila de negocio; registra el orden y devuelve el foco en el estado de la aplicación.
¿Cuál es el fallback cuando CloseWatcher no es compatible?
Mantén un botón de cierre explícito y un manejo básico de Escape, reutilizando las mismas funciones de confirmación de estado modificado, restauración de foco y limpieza. No interceptes globalmente los gestos de retroceso ni el historial. Mide la cohorte degradada antes de decidir si ampliar la mejora.