Ajankohtaista
Tekniikka2 min lukuaika

Pudotusvalikko, jota ei suostuttu klikkaamaan

Valikko piirtyi täydellisesti, istui juuri oikeassa paikassa ja nielaisi jokaisen klikkauksen. Syyllinen oli sisääntuloanimaatio, joka ei koskaan oikeasti päättynyt. Lyhyt kierros CSS:n pinoamiskonteksteihin, animaatioiden fill-modeihin ja siihen, miten jahdataan bugia jota ei näe.

BTBookatun tekniikkatiimi

Julkaisimme pudotusvalikon, jota ei voinut klikata. Se aukesi ihan hyvin. Se piirtyi oikeaan paikkaan, oli täysin näkyvissä, z-index oli kunnossa eikä konsolissa ollut virheitä. Klikkaukset menivät suoraan sen läpi alla olevaan paneeliin, kuin valikko olisi ollut aave. Jos olet joskus taistellut tällaisen bugin kanssa, tiedät jo hämmentävimmän osan: kaikki näyttää oikealta, eikä mikään toimi.

Kuka oikeasti on päällimmäisenä

Ensimmäinen rehellinen vastaus tuli selaimelta, ei koodin lukemisesta. document.elementFromPoint kertoo, mikä elementti oikeasti vastaanottaa klikkauksen tietyssä kohdassa. Osoitimme sillä valikkorivin keskikohtaa, ja se palautti kortin, joka sijaitsi sivulla myöhemmin. Valikko oli visuaalisesti päällä ja fyysisesti alla.

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);}

Käynti läpi elementtipuun löysi sen. Valikon sisältävä header ilmoitti transformista, vaikka mikään tyylitiedosto ei ollut sellaista antanut. Ainoa transform sen lähimaastossa oli sisääntuloanimaatio, pieni häivähdys ylöspäin, joka oli päättynyt puoli sekuntia sivun latauksen jälkeen.

Fill-modet elävät animaatioitaan pidempään

animation-fill-mode: both tarkoittaa, että elementti pitää ensimmäisen keyframen voimassa ennen animaation alkua ja viimeisen keyframen sen päätyttyä. Pitää, ikuisesti. Jos jokin keyframe animoi transformia, päättynyt animaatio soveltaa yhä transformia, ja transformoitu elementti muuttuu pinoamiskontekstiksi. Jokaisen jälkeläisen z-index on nyt vangittuna sen sisään, kilpailemassa vain paikallisten sisarusten kanssa, ei koskaan muun sivun kanssa.

Sivullamme oli headerin jälkeen paneeleja, jotka käyttivät backdrop-filteriä, ja se luo oman pinoamiskontekstinsa. Kaksi suljettua laatikkoa, ja niiden välisen piirtojärjestyksen ratkaisi puun järjestys, ei se z-index, jota nostimme hämmentyneinä yhä ylemmäs. Valikolla olisi voinut olla z-index miljoona. Oman laatikkonsa sisällä sitä ei kuullut kukaan ulkopuolella.

Tässä on ansa ansan sisällä. Vaihdoimme viimeisen keyframen arvoksi transform: none ja päättelimme, että jäljelle jäävä arvo olisi silloin harmiton. Laskettu tyyli ilmoitti silti matriisin. Selaimet ratkaisevat voimassa olevan animaation transformin matriisiarvoksi, ja yksikkömatriisikin lasketaan transformiksi pinoamisen kannalta. Pinoamiskonteksti selvisi korjauksestamme hengissä.

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; }}

Sääntö, jonka kirjoitimme ylös

Säiliöiden sisääntuloanimaatiot käyttävät fill-modea backwards, ei koskaan both. Backwards kattaa ainoan aukon jolla on väliä, eli ruudut ennen viivästetyn animaation alkua. Kun animaatio päättyy, elementti irtautuu siitä täysin ja lakkaa olemasta pinoamiskonteksti. Lopputulos näyttää identtiseltä, koska hyvin rakennettu sisääntulo päättyy elementin luonnolliseen tilaan.

Pudotusvalikko aukeaa oikein huurrelasipaneelien yläpuolelle tummassa hallintanäkymässä
Korjauksen jälkeen: valikko piirtyy huurrepaneelien päälle ja, mikä olennaisempaa, ottaa omat klikkauksensa vastaan.
  • Luota elementFromPointiin enemmän kuin silmiisi. Visuaalinen järjestys ja osumatestauksen järjestys voivat olla eri mieltä, ja vain toinen niistä ottaa klikkauksen.
  • Kun z-index lakkaa käyttäytymästä järkevästi, lopeta sen nostaminen ja ala jahdata pinoamiskonteksteja. Yllä kuvattu kulku elementtipuussa vie minuutin.
  • Tarkista, mitä päättyneet animaatiot jättävät jälkeensä. getComputedStyle rauhallisella sivulla kertoo, mikä on yhä voimassa.
  • Jäljelle jäänyt transform on transform, vaikka keyframessa lukisi none.

Animaatio kesti puoli sekuntia. Sen sivuvaikutuksella ei ollut päättymisaikaa lainkaan.

css pinoamiskontekstianimation fill modez-index ei toimifrontendin debuggauscss transform pinoaminen
Haluatko ottaa tämän käyttöön?

Bookatu antaa sinulle oman näköisesi varaussivun, ennakkomaksut, jäsenyydet, lahjakortit ja muistutukset, 0 % komissiolla varauksistasi.

Aloita ilmaiseksi