Rozbalovací nabídka, která odmítala reagovat na kliknutí
Nabídka se vykreslila bezchybně, seděla přesně tam, kde měla, a spolkla každé kliknutí. Na vině byla nástupní animace, která ve skutečnosti nikdy neskončila. Krátká procházka po CSS stacking kontextech, režimech výplně animací a lovu chyb, které nejsou vidět.
Vypustili jsme rozbalovací nabídku, na kterou nešlo kliknout. Otevírala se normálně. Vykreslila se na správném místě, plně viditelná, se správným z-indexem a bez chyb v konzoli. Kliknutí jí ale procházela rovnou skrz do panelu pod ní, jako by ta nabídka byla duch. Pokud jste někdy s takovou chybou bojovali, znáte tu nepříjemnou část: všechno vypadá správně a nic nefunguje.
Kdo je vlastně navrchu
První poctivou odpověď nedalo čtení kódu, ale prohlížeč. document.elementFromPoint vám řekne, který prvek na daných souřadnicích kliknutí opravdu dostane. Namířili jsme ho doprostřed položky nabídky a vrátil kartu, která byla na stránce až za ní. Nabídka byla vizuálně nahoře a fyzicky pod.
// Who really gets the click at this point?const item = document.querySelector('[role="menuitem"]');const r = item.getBoundingClientRect();const top = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);console.log(top === item || item.contains(top)); // false -> something covers it // Walk the ancestors and list everything that creates a stacking contextfor (let el = item; el; el = el.parentElement) { const cs = getComputedStyle(el); const reasons = []; if (cs.transform !== 'none') reasons.push('transform'); if (cs.filter !== 'none') reasons.push('filter'); if (cs.backdropFilter !== 'none') reasons.push('backdrop-filter'); if (cs.opacity !== '1') reasons.push('opacity'); if (cs.position !== 'static' && cs.zIndex !== 'auto') reasons.push('z-index'); if (reasons.length) console.log(el.className, reasons);}Procházka po rodičích to našla. Hlavička, ve které nabídka byla, hlásila transformaci, přestože jí ji žádný styl nedával. Jediná transformace v širokém okolí byla nástupní animace, malé prolnutí s nadzvednutím, které skončilo půl vteřiny po načtení stránky.
Režimy výplně přežijí svoje animace
animation-fill-mode: both znamená, že prvek drží první klíčový snímek před startem animace a po jejím konci si nechává poslední. Nechává napořád. Pokud kterýkoli klíčový snímek animuje transform, dokončená animace pořád aplikuje transformaci a transformovaný prvek se stává stacking kontextem. Z-index každého potomka je od té chvíle uvězněný uvnitř a soupeří jen s místními sourozenci, nikdy se zbytkem stránky.
Na naší stránce byly za hlavičkou panely s backdrop-filter, který si zakládá vlastní stacking kontext. Dvě zapečetěné krabice a o pořadí vykreslování mezi nimi rozhoduje pořadí ve stromu, ne z-index, který jsme ve zmatku pořád zvyšovali. Nabídka mohla mít z-index klidně milion. Uvnitř své krabice ji zvenku nikdo neslyšel.
A teď past uvnitř pasti. Změnili jsme poslední klíčový snímek na transform: none s tím, že držená hodnota bude neškodná. Spočtený styl ale pořád hlásil matici. Prohlížeče vyhodnotí transformaci z aplikované animace jako hodnotu matice a jednotková matice se pro účely vrstvení stále počítá jako transformace. Stacking kontext naši opravu přežil.
/* BAD: the finished animation keeps applying its last keyframe forever. The header remains a stacking context for the life of the page. */.enter { animation: rise 0.5s ease-out both;} /* GOOD: fill backwards only. The element returns to its natural state after the animation, which is identical to the final keyframe anyway. */.enter { animation: rise 0.5s ease-out backwards;} @keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; }}Pravidlo, které jsme si zapsali
Nástupní animace na kontejnerech používají fill backwards, nikdy both. Backwards pokryje jedinou mezeru, na které záleží, tedy snímky před startem odložené animace. Jakmile animace skončí, prvek se od ní úplně odpojí a přestane být stacking kontextem. Vizuální výsledek je stejný, protože dobře udělaný nástup končí v přirozeném stavu prvku.

- Věřte elementFromPoint víc než svým očím. Vizuální pořadí a pořadí pro zásahy se mohou lišit a kliknutí bere jen jedno z nich.
- Když z-index přestane dávat smysl, přestaňte ho zvyšovat a začněte lovit stacking kontexty. Procházka po rodičích výše zabere minutu.
- Zkontrolujte, co po sobě dokončené animace nechávají. getComputedStyle na klidné stránce vám řekne, co je pořád aplikované.
- Držené transformace jsou transformace, i když v klíčovém snímku stojí none.
Animace trvala půl vteřiny. Její vedlejší účinek neměl konec žádný.