Scenariul este aproape întotdeauna același, care este un tabel de date în interiorul unui container derulabil. Fiecare rând are un meniu de acțiuni, un mic drop-down cu câteva opțiuni, cum ar fi Editare, Duplicare și Ștergere. Îl construiești, pare să funcționeze perfect izolat, apoi cineva îl pune în interiorul acelui div derulabil și lucrurile se destramă. Am văzut această eroare exactă în trei baze de cod diferite: container, stivă și cadru, toate diferite. Bug-ul, totuși, este total identic. Meniul derulant este tăiat la marginea containerului. Sau apare în spatele conținutului care, în mod logic, ar trebui să fie sub el. Sau funcționează bine până când utilizatorul defilează și apoi se deplasează. Ajungi la z-index: 9999. Uneori ajută, dar alteori nu face absolut nimic. Acea inconsecvență este primul indiciu că se întâmplă ceva mai profund. Motivul pentru care continuă să revină este că sunt implicate trei sisteme de browser separate, iar majoritatea dezvoltatorilor le înțeleg pe fiecare pe cont propriu, dar nu se gândesc niciodată la ce se întâmplă atunci când toate trei se ciocnesc: debordare, contexte de stivuire și blocuri care conțin.
Odată ce înțelegi cum interacționează toate trei, modurile de eșec încetează să se mai simtă aleatorii. De fapt, ele devin previzibile. Cele trei lucruri care cauzează de fapt acest lucru Să ne uităm la fiecare dintre aceste elemente în detaliu. Problema preaplinului Când setați overflow: ascuns, overflow: scroll sau overflow: automat pe un element, browserul va decupa orice se extinde dincolo de limitele sale, inclusiv descendenții poziționați absolut. .scroll-container { preaplin: automat; înălțime: 300px; /* Aceasta va decupa meniul derulant, punct */ }
.dropdown { poziție: absolută; /* Nu contează -- încă decupat de .scroll-container */ }
Asta m-a surprins prima dată când am dat peste el. Mi-am asumat poziția: absolutul ar lăsa un element să scape de tăierea unui container. Nu este. În practică, asta înseamnă că un meniu poziționat absolut poate fi tăiat de orice strămoș care are o valoare de depășire invizibilă, chiar dacă acel strămoș nu este blocul care conține meniul. Decuparea și poziționarea sunt sisteme separate. Se întâmplă să se ciocnească în moduri care par complet aleatorii până când le înțelegi pe amândouă.
Iată un exemplu React folosind createPortal:
import { createPortal } din 'react-dom'; import { useState, useEffect, useRef } din 'react';
funcția dropdown({ anchorRef, isOpen, children }) { const [poziție, setPosition] = useState({ sus: 0, stânga: 0});
useEffect(() => { if (isOpen && anchorRef.current) { const rect = anchorRef.current.getBoundingClientRect(); setPosition({ sus: rect.bottom + window.scrollY, stânga: rect.left + window.scrollX, }); } }, [isOpen, anchorRef]);
if (!isOpen) returnează null;
returneaza createPortal(
Și, desigur, nu putem ignora accesibilitatea. Elementele fixe care apar peste conținut trebuie să fie încă accesibile de la tastatură. Dacă ordinea de focalizare nu se mută în mod natural în meniul derulant fix, va trebui să o gestionați folosind cod. De asemenea, merită să verificați dacă nu se află peste alt conținut interactiv fără nicio modalitate de a-l respinge. Acela te mușcă la testarea tastaturii. Poziționarea ancorelor CSS: unde cred că se îndreaptă Poziționarea ancorelor CSS este direcția de care sunt cel mai interesat în acest moment. Nu eram sigur cât de mult din specificații era de fapt utilizabil când m-am uitat prima dată la el. Vă permite să declarați relația dintre un dropdown și declanșarea acestuia direct în CSS, iar browserul se ocupă de coordonatele. .trigger { nume-ancoră: --declanșatorul-meu; }
.meniu-derulant { poziție: absolută; poziție-ancoră: --my-trigger; sus: ancora(jos); stânga: ancora(stânga); position-try-fallbacks: flip-block, flip-inline; }
Proprietatea position-try-fallbacks este ceea ce face ca aceasta să merite să fie folosită peste un calcul manual. Browserul încearcă plasări alternative înainte de a renunța, așa că un meniu drop-down din partea de jos a ferestrei de vizualizare se întoarce automat în sus, în loc să fie întrerupt. Suportul pentru browser este solid în browserele bazate pe Chromium și crește în Safari. Firefox are nevoie de un polyfill. Pachetul @oddbird/css-anchor-positioning acoperă specificațiile de bază. Am lovit cu ele cazuri de margine de aspect care necesitau alternative pe care nu le-am anticipat, așa că tratați-o ca pe o îmbunătățire progresivă sau asociați-o cu unJavaScript de rezervă pentru Firefox. Pe scurt, promițător, dar nu universal încă. Testați în browserele dvs. țintă. Și în ceea ce privește accesibilitatea, declararea unei relații vizuale în CSS nu spune nimic arborelui de accesibilitate. aria-controls, aria-expanded, aria-haspopup — acea parte este încă pe tine. Uneori, soluția este doar mutarea elementului Înainte de a ajunge la un portal sau de a face calcule de coordonate, pun întotdeauna o întrebare mai întâi: este necesar ca acest meniu derulant să trăiască în interiorul containerului de defilare? Dacă nu, mutarea marcajului într-un înveliș de nivel superior elimină în întregime problema, fără JavaScript și fără calcule de coordonate. Acest lucru nu este întotdeauna posibil. Dacă butonul și meniul drop-down sunt încapsulate în aceeași componentă, mutarea unuia fără celălalt înseamnă regândirea întregului API. Dar când poți face asta, nu ai nimic de depanat. Problema pur și simplu nu există. Ceea ce CSS modern încă nu rezolvă CSS a parcurs un drum lung aici, dar există încă locuri în care te dezamăgesc. Poziția: problemele remediate și de transformare sunt încă acolo. Este în specificație în mod intenționat, ceea ce înseamnă că nu există o soluție CSS. Dacă utilizați o bibliotecă de animații care vă înglobează aspectul într-un element transformat, ați revenit la nevoie de portaluri sau poziționarea ancorelor. Poziționarea ancorelor CSS este promițătoare, dar nouă. După cum am menționat mai devreme, Firefox încă are nevoie de o completare polivalentă în momentul în care scriu acest lucru. Am lovit cazuri de margine de aspect cu ele care necesitau soluții de rezervă pe care nu le-am anticipat. Dacă aveți nevoie de un comportament consecvent în toate browserele astăzi, încă căutați JavaScript pentru părțile dificile. Adăugarea pentru care mi-am schimbat fluxul de lucru este API-ul HTML Popover, disponibil acum în toate browserele moderne. Elementele cu atributul popover sunt afișate în stratul superior al browserului, deasupra tuturor, fără a fi necesară poziționarea JavaScript.
Gestionarea evadării, respingerea la clic în exterior și semantica solidă a accesibilității sunt gratuite pentru lucruri precum sfaturi cu instrumente, widget-uri de dezvăluire și suprapuneri simple. Este primul instrument la care ajung acum. Acestea fiind spuse, nu rezolvă poziționarea. Rezolvă stratificarea. Mai aveți nevoie de poziționarea ancorei sau JavaScript pentru a alinia un popover la declanșatorul său. API-ul Popover se ocupă de stratificare. Poziționarea ancorei se ocupă de plasare. Folosite împreună, ele acoperă cea mai mare parte a ceea ce ați putea face anterior o bibliotecă. Un ghid de decizie pentru situația dvs După ce am trecut prin toate acestea pe calea grea, iată cum mă gândesc de fapt la alegere acum.
Folosește un portal. L-aș folosi atunci când declanșatorul trăiește adânc în containere de defilare imbricate. Am folosit acest model pentru meniurile de acțiuni ale tabelului și l-am asociat cu restabilirea focalizării și verificări de accesibilitate. Este cea mai fiabilă opțiune, dar bugetul de timp pentru cablarea suplimentară. Utilizați poziționarea fixă. Acest lucru este pentru atunci când sunteți în JavaScript vanilla sau într-un cadru ușor și nu puteți verifica că niciun strămoș nu aplică transformări sau filtre. Este simplu de configurat și simplu de depanat, atâta timp cât această constrângere este valabilă. Utilizați CSS Anchor Positioning. Atingeți acest lucru atunci când suportul pentru browser vă permite acest lucru. Dacă este necesar suport pentru Firefox, asociați-l cu @oddbird polyfill. Aici se îndreaptă în cele din urmă platforma și, în cele din urmă, va deveni abordarea ta de referință. Restructurați DOM. Folosiți-l atunci când arhitectura vă permite și doriți o complexitate de rulare zero. Cred că este probabil cea mai subestimată opțiune. Combinați modele. Faceți acest lucru atunci când doriți poziționarea ancorelor ca abordare principală, asociată cu o alternativă JavaScript pentru browsere neacceptate. Sau un portal pentru plasarea DOM asociat cu getBoundingClientRect() pentru precizia coordonatelor.
Concluzie Obișnuiam să tratez această eroare ca pe o problemă unică - ceva de care să corectăm și să treci mai departe. Dar odată ce am stat cu ea suficient de mult ca să înțeleg toate cele trei sisteme implicate - decuparea excesivă, contextele de stivuire și blocurile care conțin - a încetat să se mai simtă aleatoriu. Am putut să mă uit la un meniu derulant defalcat și să urmăresc imediat ce strămoș este responsabil. Acea schimbare în modul în care am citit DOM a fost adevărata concluzie. Nu există un singur răspuns corect. Ceea ce am ajuns depindea de ceea ce puteam controla în baza de cod: portaluri când arborele strămoșilor era imprevizibil; poziționare fixă atunci când era curată și simplă; mișcarea elementului când nimic nu mă oprește; și poziționarea ancorei acum,unde pot. Orice ați alege, nu tratați accesibilitatea ca pe ultimul pas. Din experiența mea, exact atunci este omis. Relațiile ARIA, gestionarea focalizării, comportamentul tastaturii - acestea nu sunt lucioase. Ele fac parte din ceea ce face ca lucrul să funcționeze cu adevărat. Consultați codul sursă complet în depozitul meu GitHub. Lectură suplimentară Acestea sunt referințele la care am tot revenit în timp ce lucram prin asta:
Contextul de stivuire (MDN) „Ghid de poziționare a ancorelor CSS”, Juan Diego Rodriguez „Noțiuni introductive cu API-ul Popover”, Godstime Aburu Interfață de utilizare flotantă (floating-ui.com) CSS Overflow (MDN)