Magazín
Technika3 min čtení

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.

TTTechnický tým Bookatu

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.

js
// 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.

css
/* 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.

Rozbalovací nabídka se správně otevírá nad panely z matného skla v tmavém přehledu
Po opravě: nabídka se vykresluje nad matnými panely a hlavně si chytá vlastní kliknutí.
  • 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ý.

css stacking kontextrežim výplně animacez-index nefungujeladění frontenducss transform a vrstvení
Chcete to uvést do praxe?

Bookatu vám dá rezervační stránku ve vašem stylu, zálohy, členství, dárkové poukazy a připomínky, s 0% provizí z vašich rezervací.

Začít zdarma