DEV Community

Franklin
Franklin

Posted on

Los popovers nativos no reemplazaron a las bibliotecas de JS; reemplazaron al código de unión.

Hermano estructural de Los Cascade Layers no reemplazaron la especificidad — Reemplazaron la negociación: la misma distinción, en una capa diferente.

También disponible en Inglés

El problema

Llega un ticket de soporte con una captura de pantalla: un data grid, docenas de filas, y un menú de acciones —renombrar, archivar, eliminar— abierto en el lugar equivocado. No por unos pocos píxeles. Está anclado a la última fila de la tabla, sin importar en el botón de qué fila se hizo clic. Clic en el menú de la fila dos, posición de la fila cuarenta. Clic en la fila veinte, el mismo resultado.

Nadie escribió un bug que dijera "usa siempre la última fila". El equipo ya había hecho la parte más difícil y moderna correctamente: atributos popover nativos en lugar de un menú desplegable administrado por JavaScript, CSS anchor positioning en lugar de coordenadas calculadas manualmente. El menú está exactamente donde el código le dijo que estuviera. El código simplemente no declaró la relación que todos asumieron que contenía.

Por qué existe el problema

Antes de que el navegador proporcionara un top layer de propósito general para este tipo de superposición (overlay), un elemento anclado a algo enterrado dentro de un contenedor con desplazamiento (scrollable) o recortado (clipped) tenía un problema real: podía quedar cortado, oculto detrás de otra cosa o atrapado dentro de una región de desplazamiento que no coincidía con el resto de la página. La solución habitual era estructural. Renderizar la superposición en otro lugar, adjunta al final de <body> o donde viviera un contenedor portal compartido, de modo que su posición en el DOM ya no coincidiera con su posición en la interfaz. JavaScript se encargaba de todo lo que el DOM ya no expresaba por sí mismo: mostrarlo, ocultarlo, calcular dónde debía ubicarse en relación con lo que lo activaba, y mantener esa posición actualizada a medida que la página se desplazaba o cambiaba de tamaño.

Eso no era un truco para corregir un error. Era una respuesta correcta a un navegador que realmente carecía de una forma de propósito general para que este tipo de superposición escapara al recorte de sus ancestros. La superposición tenía que vivir separada de su disparador (trigger), porque vivir cerca de él era lo que causaba el problema.

El primer principio

La plataforma ahora separa varias responsabilidades que las implementaciones de superposición más antiguas tenían que reconstruir en JavaScript. El atributo popover, combinado con popovertarget en un control, le da al navegador una relación declarativa entre el control y el popover. Al abrirse, el popover entra al top layer del navegador, fuera de las restricciones habituales de recorte y apilamiento de sus ancestros, y esa relación con el activador (invoker) también proporciona una referencia de anchor implícita.

El CSS anchor positioning maneja el enlace espacial de forma independiente: un elemento posicionado se puede ubicar en relación con un anchor sin que JavaScript calcule las coordenadas.

El límite importante aparece cuando un componente necesita un anchor explícito en lugar del propio control activador del popover. Los nombres de anchor explícitos no se limitan automáticamente al subárbol DOM (subtree) de un componente. Cuando múltiples anchors exponen el mismo nombre, un elemento posicionado mediante anchor puede resolverse con respecto al último anchor coincidente en el orden del código fuente.

anchor-scope cambia esa relación al restringir explícitamente dónde se puede resolver un anchor con nombre.

Por lo tanto, el navegador puede asumir el límite de la superposición y la mecánica de posicionamiento sin eliminar la responsabilidad arquitectónica. HTML establece la relación de interacción. CSS establece la relación espacial y su alcance (scope). La pregunta restante es si el DOM realmente declara el límite del que depende el componente.

Demostrando el principio

Redúcelo a dos elementos repetidos, cada uno con un anchor y un hermano posicionado:

<div class="item-wrapper">
  <div class="item">
    <div class="thumbnail"></div>
  </div>
  <div class="badge" popover></div>
</div>

<div class="item-wrapper">
  <div class="item">
    <div class="thumbnail"></div>
  </div>
  <div class="badge" popover></div>
</div>
Enter fullscreen mode Exit fullscreen mode
.thumbnail {
  anchor-name: --thumb;
}

.badge {
  position-anchor: --thumb;
  position-area: top right;
}
Enter fullscreen mode Exit fullscreen mode

.thumbnail y .badge son hermanos dentro de cada .item-wrapper. Comparten el mismo nombre de anchor explícito, pero ese nombre no se limita automáticamente al subárbol del wrapper. Sin un anchor scope, los nombres --thumb repetidos permanecen visibles para los elementos posicionados en cualquier otra parte del documento. Cuando múltiples anchors comparten ese nombre, el elemento posicionado se resuelve con respecto al último anchor coincidente en el orden del código fuente.

.item-wrapper {
  anchor-scope: --thumb;
}
Enter fullscreen mode Exit fullscreen mode

Una sola línea establece el límite faltante. Ahora cada .badge puede resolver --thumb solo dentro de su propio .item-wrapper, por lo que el nombre de anchor idéntico ya no es ambiguo entre los componentes repetidos.

La parte importante no es que el navegador haya aprendido de repente qué es un componente. La parte importante es que el componente finalmente declaró el límite del que dependía la relación del anchor.

El punto de dolor

Esto surgió en un data grid en producción: filas repetidas, cada una con un botón de acción al final que abre un menú. El menú debía alinearse con el propio borde derecho de la fila, de forma consistente, independientemente de dónde se ubicara exactamente el botón dentro de la fila. Eso hizo que la opción más simple de la plataforma fuera insuficiente: el anchor implícito de un popover sigue a su propio control activador, pero el control aquí era un botón pequeño, no la fila en sí. Anclar a la fila significaba nombrarla explícitamente, lo cual fue una decisión razonable y deliberada. Los popovers asociados con un activador reciben una referencia de anchor implícita a ese activador; una asociación de anchor explícita es un mecanismo separado.

Lo que no se reconsideró fue dónde vivía el menú en el marcado (markup). Antes de que el equipo adoptara el atributo popover nativo, sus componentes de superposición siempre se renderizaban como hermanos de lo que los activaba, un nivel más arriba en el DOM: un hábito estructural de cuando una superposición anidada cerca de una fila corría el riesgo de ser recortada por el propio overflow: hidden de esa fila. El comportamiento de top layer del atributo nativo hizo que ese riesgo desapareciera. La ubicación como hermano nunca se volvió a examinar, porque nada en el cambio a popover parecía requerirlo. Los popovers abiertos se colocan en el top layer y no se ven influenciados por el estilo de posición ni por el overflow de sus ancestros.

Cada fila compartía el mismo nombre de anchor, definido a través de la misma clase que cada fila ya usaba para su diseño (layout). El menú de ninguna fila estaba dentro de la fila a la que pertenecía, y el nombre de anchor explícito no estaba limitado al subárbol de esa fila.

Sin ese límite presente, el nombre de anchor repetido permaneció visible en todo el grid. La resolución cayó hasta el último anchor coincidente en el documento. Cada menú, en cada fila, apuntaba al mismo lugar, porque el DOM no tenía un scope declarado para la relación.

La solución no tocó ni el nombre del anchor ni el botón. Envolvió cada fila y su propio menú en un contenedor compartido, el ancestro más pequeño que contenía a ambos, y limitó el nombre del anchor a ese contenedor:

.row-wrapper {
  anchor-scope: --row-anchor;
}
Enter fullscreen mode Exit fullscreen mode

El Notas desde el Pase del sábado reproduce el menú fuera de lugar exactamente como se envió a producción, confirma por qué el navegador no tenía nada que preferir y muestra la solución con scope manteniéndose en cada fila del grid, no solo en la que se estaba probando.

La lección más amplia

Esta no es una historia sobre CSS anchor positioning estando incompleto. El anclaje implícito cubre el caso ordinario —un popover vinculado a su propio activador— sin pedir una sola línea de nombres o de scoping. El trabajo arquitectónico adicional aparece cuando un requisito real sale de ese caso. La plataforma puede establecer la relación de superposición, pero una relación de anchor explícita aún necesita un límite explícito.

Aquí, el problema no era que la plataforma no pudiera posicionar el menú. Era que el DOM conservaba una estructura de la implementación previa a popover sin declarar el límite de componente que la nueva relación de posicionamiento requería.

El Artículo 4 encontró la misma forma en el código de la aplicación: lógica que seguía administrando una responsabilidad que el navegador ya había asumido. Este es ese mismo patrón apareciendo en la estructura en lugar del comportamiento. Una forma de DOM construida para resolver un problema no deja de existir automáticamente una vez que el problema que resolvió desaparece. Se queda ahí, correcta por hábito, hasta que algo construido sobre un supuesto más nuevo choca directamente contra ella.

Adoptar una característica de la plataforma no es solo cuestión de cambiar un script por un atributo. Es una invitación a preguntar qué partes de la estructura circundante se construyeron para compensar una limitación que ese atributo acaba de eliminar, y cuáles de esas partes aún deben estar allí, ahora declaradas a propósito en lugar de heredadas por accidente.

Top comments (0)