O escenario é case sempre o mesmo, que é unha táboa de datos dentro dun contedor desprazable. Cada fila ten un menú de accións, un pequeno menú despregable con algunhas opcións, como Editar, Duplicar e Eliminar. Constrúeso, parece que funciona perfectamente de forma illada, e entón alguén o mete dentro dese div que se pode desprazar e as cousas destrúense. Vin este erro exacto en tres bases de código diferentes: o contenedor, a pila e o marco, todos diferentes. O erro, porén, é totalmente idéntico. O menú despregable córtase no bordo do contedor. Ou aparece detrás do contido que loxicamente debería estar por debaixo. Ou funciona ben ata que o usuario se despraza e, a continuación, vai á deriva. Alcanzas o índice z: 9999. Ás veces axuda, pero outras non fai absolutamente nada. Esa incoherencia é a primeira pista de que algo máis profundo está a suceder. O motivo polo que segue volvendo é que están implicados tres sistemas de navegador separados, e a maioría dos desenvolvedores entenden cada un por si só, pero nunca pensan no que ocorre cando os tres chocan: desbordamento, contextos de apilado e bloques que conteñen.
Unha vez que entendes como interactúan os tres, os modos de falla deixan de parecer aleatorios. De feito, fanse previsibles. As tres cousas que realmente causan isto Vexamos cada un destes elementos en detalle. O problema do desbordamento Cando estableces desbordamento: oculto, desbordamento: desprazamento ou desbordamento: automático nun elemento, o navegador recortará todo o que se estenda máis aló dos seus límites, incluídos os descendentes en posición absoluta. .scroll-container { desbordamento: automático; altura: 300px; /* Isto recortará o menú despregable, punto */ }
.menú desplegable { posición: absoluta; /* Non importa -- aínda recortado por .scroll-container */ }
Iso sorprendeume a primeira vez que me topei con el. Eu asumira a posición: o absoluto deixaría escapar un elemento do recorte dun recipiente. Non o fai. Na práctica, iso significa que un menú en posición absoluta pode ser cortado por calquera antepasado que teña un valor de desbordamento non visible, aínda que ese antepasado non sexa o bloque que contén o menú. O recorte e o posicionamento son sistemas separados. Simplemente chocan de xeitos que parecen completamente aleatorios ata que entendes ambos.
Aquí tes un exemplo de React usando createPortal:
importar { createPortal } desde 'react-dom'; importar { useState, useEffect, useRef } desde 'react';
función desplegable ({ anchorRef, isOpen, fillos }) { const [posición, setPosition] = useState ({ arriba: 0, esquerda: 0 });
useEffect(() => { if (isOpen && anchorRef.current) { const rect = anchorRef.current.getBoundingClientRect(); setPosition({ arriba: rect.bottom + window.scrollY, esquerda: rect.left + window.scrollX, }); } }, [isOpen, anchorRef]);
if (!isOpen) devolve nulo;
devolver createPortal(
E, por suposto, non podemos ignorar a accesibilidade. Os elementos fixos que aparecen sobre o contido deben seguir sendo accesibles mediante o teclado. Se a orde de foco non se move naturalmente ao menú despregable fixo, terás que xestionala mediante código. Tamén paga a pena comprobar que non se sitúa sobre outros contidos interactivos sen xeito de descartalo. Ese te morde nas probas de teclado. Posicionamento da ancoraxe CSS: onde creo que se dirixe O posicionamento da ancoraxe CSS é a dirección que máis me interesa neste momento. Non estaba seguro de canto da especificación era realmente utilizable cando o mirei por primeira vez. Permíteche declarar a relación entre un menú despregable e o seu disparador directamente en CSS, e o navegador xestiona as coordenadas. .gatillo { nome-áncora: --o meu-trigger; }
.menú desplegable { posición: absoluta; posición-áncora: --o meu-gatillo; arriba: áncora (abaixo); esquerda: áncora(esquerda); posición-try-fallbacks: flip-block, flip-inline; }
A propiedade position-try-fallbacks é o que fai que valga a pena usar isto nun cálculo manual. O navegador proba colocacións alternativas antes de renunciar, polo que un menú despregable na parte inferior da ventana gráfica xira automaticamente cara arriba en lugar de cortarse. A compatibilidade do navegador é sólida nos navegadores baseados en Chromium e crece en Safari. Firefox necesita un polyfill. O paquete @oddbird/css-anchor-positioning cobre as especificacións básicas. Atopei con el casos de deseño que requirían alternativas que non anticipaba, así que trátao como unha mellora progresiva ou combínao cunJavaScript alternativo para Firefox. En resumo, prometedor pero aínda non universal. Proba nos teus navegadores de destino. E no que a accesibilidade se refire, declarar unha relación visual en CSS non lle di nada á árbore de accesibilidade. aria-controls, aria-expanded, aria-haspopup — esa parte aínda está en ti. Ás veces, a corrección é só mover o elemento Antes de buscar un portal ou facer cálculos de coordenadas, sempre fago unha pregunta: ¿De feito, este menú despregable ten que vivir dentro do contedor de desprazamento? Se non o fai, mover o marcado a un envoltorio de nivel superior elimina o problema por completo, sen JavaScript e sen cálculos de coordenadas. Isto non sempre é posible. Se o botón e o menú despregable están encapsulados no mesmo compoñente, mover un sen outro significa repensar toda a API. Pero cando podes facelo, non hai nada que depurar. O problema simplemente non existe. O que CSS moderno aínda non resolve CSS percorreu un longo camiño aquí, pero aínda hai lugares nos que che defrauda. A posición: problemas solucionados e transformados aínda están aí. Está na especificación intencionalmente, o que significa que non existe ningunha solución CSS. Se estás a usar unha biblioteca de animacións que envolve o teu deseño nun elemento transformado, volves a necesitar portais ou posicionamento de ancoraxe. O posicionamento da ancoraxe CSS é prometedor, pero novo. Como se mencionou anteriormente, Firefox aínda necesita un polyfill no momento en que estou escribindo isto. Atopei con el casos de borde de deseño que requirían alternativas que non anticipaba. Se hoxe precisas un comportamento consistente en todos os navegadores, aínda estás buscando JavaScript para as partes complicadas. A adición pola que cambiei o meu fluxo de traballo é a API HTML Popover, agora dispoñible en todos os navegadores modernos. Os elementos co atributo popover aparecen na capa superior do navegador, sobre todo, sen necesidade de posicionamento JavaScript.
O manexo de escape, a eliminación ao facer clic no exterior e a sólida semántica de accesibilidade son gratuítos para información sobre ferramentas, widgets de divulgación e superposicións sinxelas. É a primeira ferramenta á que alcanzo polo momento. Dito isto, non resolve o posicionamento. Resolve a estratificación. Aínda necesitas posicionamento de ancoraxe ou JavaScript para aliñar un popover co seu disparador. A API Popover xestiona as capas. O posicionamento da áncora xestiona a colocación. Usados xuntos, cobren a maior parte do que antes podías facer unha biblioteca. Unha guía de decisións para a súa situación Despois de pasar por todo isto do xeito difícil, aquí é como realmente penso na elección agora.
Usa un portal. Usaríao cando o disparador vive no fondo de contedores de desprazamento anidados. Utilicei este patrón para os menús de accións de táboa e combinémolo con verificacións de accesibilidade e restauración do foco. É a opción máis fiable, pero tempo de orzamento para o cableado adicional. Usa o posicionamento fixo. Isto é para cando estás en JavaScript vainilla ou nun marco lixeiro e podes verificar que ningún antepasado aplique transformacións ou filtros. É sinxelo de configurar e de depurar, sempre que se manteña esa única restrición. Usa CSS Anchor Positioning. Achégate a isto cando o soporte do teu navegador o permita. Se é necesario compatibilidade con Firefox, emparéllao co polyfill @oddbird. Aquí é onde se dirixe a plataforma e, finalmente, converterase no teu enfoque. Reestrutura o DOM. Usa isto cando a arquitectura o permita e queres cero complexidade de tempo de execución. Creo que é probablemente a opción máis subestimada. Combina patróns. Fai isto cando queiras que o posicionamento da ancoraxe sexa o teu enfoque principal, combinado cunha alternativa de JavaScript para navegadores non compatibles. Ou un portal para a colocación de DOM emparejado con getBoundingClientRect() para a precisión das coordenadas.
Conclusión Adoitaba tratar este erro como un problema único, algo que correr e seguir. Pero unha vez que me sentín con el o tempo suficiente para comprender os tres sistemas implicados: recorte de desbordamento, contextos de apilado e bloques que conteñen, deixou de sentirse aleatorio. Puiden mirar un menú despregable roto e rastrexar inmediatamente cal era o ancestro responsable. Ese cambio na forma de lin o DOM foi a verdadeira conclusión. Non hai unha única resposta correcta. O que cheguei dependía do que puidese controlar na base de código: portais cando a árbore dos antepasados era imprevisible; posicionamento fixo cando estaba limpo e sinxelo; movendo o elemento cando nada me paraba; e posicionamento de ancoraxe agora,onde podo. Sexa cal sexa o que acabes elixindo, non trates a accesibilidade como o último paso. Segundo a miña experiencia, é exactamente cando se omite. As relacións ARIA, a xestión do foco, o comportamento do teclado, non son polaco. Son parte do que fai que a cousa funcione realmente. Consulte o código fonte completo no meu repositorio de GitHub. Lecturas complementarias Estas son as referencias ás que seguín volvendo mentres traballaba nisto:
Contexto de apilamiento (MDN) “CSS Anchor Positioning Guide”, Juan Diego Rodríguez "Comezando coa API Popover", Godstime Aburu IU flotante (floating-ui.com) Desbordamento de CSS (MDN)