Creați propriile panouri de conținut pentru extindere și contractare
Publicat: 2022-03-10Până acum le-am numit „panou de deschidere și închidere”, dar sunt descrise și ca panouri de expansiune sau, mai simplu, panouri de extindere.
Pentru a clarifica exact despre ce vorbim, mergeți la acest exemplu pe CodePen:
Sertar ușor de afișat/ascunde (Multiple) de Ben Frain pe CodePen.
Acesta este ceea ce vom construi în acest scurt tutorial.
Din punct de vedere al funcționalității, există câteva modalități de a realiza deschiderea și închiderea animate pe care le căutăm. Fiecare abordare cu propriile sale beneficii și compromisuri. Voi împărtăși detaliile metodei mele „go-to” în detaliu în acest articol. Să luăm în considerare mai întâi posibilele abordări.
Abordari
Există variații ale acestor tehnici, dar, în linii mari, abordările se încadrează în una din trei categorii:
- Animați/tranzițiați
height
sau înălțimeamax-height
a conținutului. - Utilizați
transform: translateY
pentru a muta elementele într-o nouă poziție, dând iluzia închiderii unui panou și apoi redați din nou DOM-ul odată ce transformarea este finalizată cu elementele în poziția lor de finisare. - Utilizați o bibliotecă care face o combinație/variație de 1 sau 2!
Considerații ale fiecărei abordări
Din punct de vedere al performanței, folosirea unei transformări este mai eficientă decât animarea sau tranziția înălțimii/înălțimii maxime. Cu o transformare, elementele în mișcare sunt rasterizate și deplasate de GPU. Aceasta este o operațiune ieftină și ușoară pentru un GPU, așa că performanța tinde să fie mult mai bună.
Pașii de bază atunci când utilizați o abordare de transformare sunt:
- Obțineți înălțimea conținutului pentru a fi restrâns.
- Mutați conținutul și totul după înălțimea conținutului care urmează să fie restrâns folosind
transform: translateY(Xpx)
. Operați transformarea cu tranziția la alegere pentru a oferi un efect vizual plăcut. - Utilizați JavaScript pentru a asculta evenimentul
transitionend
. Când se declanșează,display: none
conținut și eliminați transformarea și totul ar trebui să fie la locul potrivit.
Nu sună prea rău, nu?
Cu toate acestea, există o serie de considerații cu această tehnică, așa că tind să o evit pentru implementările ocazionale, cu excepția cazului în care performanța este absolut crucială.
De exemplu, cu abordarea transform: translateY
trebuie să luați în considerare z-index
al elementelor. În mod implicit, elementele care se transformă în sus sunt după elementul de declanșare în DOM și, prin urmare, apar deasupra lucrurilor dinaintea lor atunci când sunt traduse în sus.
De asemenea, trebuie să luați în considerare câte lucruri apar după conținutul pe care doriți să-l restrângeți în DOM. Dacă nu doriți o gaură mare în aspectul dvs., s-ar putea să vă fie mai ușor să utilizați JavaScript pentru a înfășura tot ceea ce doriți să mutați într-un element container și să mutați doar asta. Gestionabil, dar tocmai am introdus mai multă complexitate! Acesta este, totuși, genul de abordare pe care am folosit-o când mișc jucătorii în sus și în jos în In/Out. Puteți vedea cum s-a făcut asta aici.
Pentru nevoi mai ocazionale, tind să merg cu tranziția max-height
a conținutului. Această abordare nu funcționează la fel de bine ca o transformare. Motivul este acela că browserul intervine înălțimea elementului care se prăbușește pe parcursul tranziției; care provoacă o mulțime de calcule de aspect care nu sunt la fel de ieftine pentru computerul gazdă.
Totuși, această abordare câștigă din punct de vedere al simplității. Beneficiul suferinței loviturii computaționale menționate mai sus este că refluxarea DOM are grijă de poziția și geometria tuturor lucrurilor. Avem foarte puține calcule de scris, plus JavaScript-ul necesar pentru a le desfășura bine este relativ simplu.
Elefantul din cameră: detalii și elemente rezumative
Cei cu o cunoaștere intimă a elementelor HTML vor ști că există o soluție HTML nativă la această problemă sub forma details
și elementelor summary
. Iată câteva exemple de markup:
<details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>
În mod implicit, browserele oferă un mic triunghi de dezvăluire lângă elementul rezumat; faceți clic pe rezumat și se dezvăluie conținutul de sub rezumat.
Grozav, hei? Detaliile chiar acceptă evenimentul de toggle
în JavaScript, astfel încât să puteți face acest tip de lucru pentru a efectua diferite lucruri în funcție de faptul că este deschis sau închis (nu vă faceți griji dacă acest tip de expresie JavaScript pare ciudat; vom ajunge la asta în mai multe detalii pe scurt):
details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })
OK, o să-ți opresc entuziasmul chiar acolo. Detaliile și elementele de rezumat nu sunt animate. Nu în mod implicit și în prezent nu este posibil să le deschideți și să se închidă animarea/tranziția cu CSS și JavaScript suplimentar.
Dacă știi altfel, mi-ar plăcea să mi se demonstreze că mă înșel.
Din păcate, deoarece avem nevoie de o estetică de deschidere și închidere, va trebui să ne suflecăm mânecile și să facem cea mai bună și mai accesibilă treabă cu celelalte instrumente pe care le avem la dispoziție.
Corect, cu veștile deprimante la o parte, haideți să facem acest lucru să se întâmple.
Model de marcare
Markupul de bază va arăta astfel:
<div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>
Avem un container exterior pentru a înfășura expanderul, iar primul element este butonul care servește ca declanșator al acțiunii. Observați atributul tip din buton? Întotdeauna includ asta, ca implicit, un buton din interiorul unui formular va efectua o trimitere. Dacă pierzi câteva ore întrebându-te de ce formularul tău nu funcționează și butoanele sunt implicate în formularul tău; asigurați-vă că verificați atributul type!
Următorul element după buton este sertarul de conținut în sine; tot ce vrei să ascunzi și să arăți.
Pentru a aduce lucrurilor la viață, vom folosi proprietăți personalizate CSS, tranziții CSS și puțin JavaScript.
Logica de bază
Logica de bază este aceasta:
- Lăsați pagina să se încarce, măsurați înălțimea conținutului.
- Setați înălțimea conținutului pe container ca valoare a unei proprietăți personalizate CSS.
- Ascundeți imediat conținutul adăugând un atribut
aria-hidden: "true"
. Utilizareaaria-hidden
asigură că tehnologia de asistență știe că și conținutul este ascuns. - Conectați CSS-ul astfel încât
max-height
a clasei de conținut să fie valoarea proprietății personalizate. - Apăsarea butonului de declanșare comută proprietatea aria-hidden de la true la false, care, la rândul său, comută
max-height
a conținutului între0
și înălțimea setată în proprietatea personalizată. O tranziție pe această proprietate oferă flerul vizual - adaptează-te după gust!
Notă: Acum, acesta ar fi un caz simplu de comutare a unei clase sau a unui atribut dacă max-height: auto
ar fi egalat cu înălțimea conținutului. Din păcate nu. Du-te și strigă despre asta la W3C aici.
Să vedem cum se manifestă această abordare în cod. Comentariile numerotate arată pașii logici echivalenti de mai sus în cod.
Aici este JavaScript:
// Get the containing element const container = document.querySelector(".container"); // Get content const content = document.querySelector(".content"); // 1. Get height of content you want to show/hide const heightOfContent = content.getBoundingClientRect().height; // Get the trigger element const btn = document.querySelector(".trigger"); // 2. Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { document.documentElement.classList.add("height-is-set"); 3. content.setAttribute("aria-hidden", "true"); }, 0); btn.addEventListener("click", function(e) { container.setAttribute("data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true"); // 5. Toggle aria-hidden content.setAttribute("aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true"); })
CSS:

.content { transition: max-height 0.2s; overflow: hidden; } .content[aria-hidden="true"] { max-height: 0; } // 4. Set height to value of custom property .content[aria-hidden="false"] { max-height: var(--containerHeight, 1000px); }
Puncte De Notă
Dar mai multe sertare?
Când aveți un număr de sertare deschise și ascunse pe o pagină, va trebui să le parcurgeți pe toate, deoarece probabil vor avea dimensiuni diferite.
Pentru a gestiona asta, va trebui să facem o querySelectorAll
pentru a obține toate containerele și apoi să rulăm din nou setarea variabilelor personalizate pentru fiecare conținut din interiorul unui forEach
.
Acel setTimeout
Am un setTimeout
cu durata 0
înainte de a seta containerul să fie ascuns. Acest lucru este, fără îndoială, inutil, dar îl folosesc ca o abordare de tip „centru și bretele” pentru a mă asigura că pagina a fost redată mai întâi, astfel încât înălțimile conținutului să fie disponibile pentru a fi citite.
Declanșează-l doar când pagina este gata
Dacă aveți alte lucruri în curs, ați putea alege să împachetați codul sertarului într-o funcție care este inițializată la încărcarea paginii. De exemplu, să presupunem că funcția de sertar a fost învelită într-o funcție numită initDrawers
, am putea face acest lucru:
window.addEventListener("load", initDrawers);
De fapt, vom adăuga asta în scurt timp.
Date suplimentare-* atribute pe container
Există un atribut de date pe containerul exterior care este, de asemenea, comutat. Aceasta se adaugă în cazul în care există ceva care trebuie schimbat cu declanșatorul sau containerul pe măsură ce sertarul se deschide/se închide. De exemplu, poate dorim să schimbăm culoarea a ceva sau să dezvăluim sau să comutăm o pictogramă.
Valoare implicită pentru proprietatea personalizată
Există o valoare implicită setată pe proprietatea personalizată în CSS de 1000px
. Acesta este bitul de după virgulă din interiorul valorii: var(--containerHeight, 1000px)
. Aceasta înseamnă că, dacă --containerHeight
se încurcă într-un fel, ar trebui să aveți totuși o tranziție decentă. Puteți seta, evident, ceea ce este potrivit pentru cazul dvs. de utilizare.
De ce să nu folosiți o valoare implicită de 100000px?
Având în vedere că max-height: auto
nu face tranziție, s-ar putea să vă întrebați de ce nu optați doar pentru o înălțime setată cu o valoare mai mare decât ați avea vreodată nevoie. De exemplu, 10000000px?
Problema cu această abordare este că va trece întotdeauna de la acea înălțime. Dacă durata tranziției este setată la 1 secundă, tranziția va „călătorește” 10000000px într-o secundă. Dacă conținutul tău are doar 50 de pixeli înălțime, vei obține un efect de deschidere/închidere destul de rapid!
Operator ternar pentru comutare
Am folosit de câteva ori un operator ternar pentru a comuta atribute. Unii îi urăsc, dar eu și alții îi iubesc. S-ar putea să pară puțin ciudate și puțin „cod golf” la început, dar odată ce te obișnuiești cu sintaxa, cred că sunt o lectură mai simplă decât un standard dacă/altfel.
Pentru cei neinițiați, un operator ternar este o formă condensată a lui if/else. Sunt scrise astfel încât lucrul de verificat să fie primul, apoi ?
separă ce trebuie executat dacă verificarea este adevărată și apoi :
pentru a distinge ce ar trebui să ruleze dacă verificarea este falsă.
isThisTrue ? doYesCode() : doNoCode();
Comutarile noastre de atribut funcționează verificând dacă un atribut este setat la "true"
și, dacă da, setați-l la "false"
, în caz contrar, setați-l la "true"
.
Ce se întâmplă la redimensionarea paginii?
Dacă un utilizator redimensionează fereastra browserului, există o probabilitate mare ca înălțimile conținutului nostru să se schimbe. Prin urmare, este posibil să doriți să reluați setarea înălțimii pentru containere în acel scenariu. Acum luăm în considerare astfel de eventualități, pare a fi un moment bun să refactorăm puțin lucrurile.
Putem face o funcție pentru a seta înălțimile și o altă funcție pentru a face față interacțiunilor. Apoi adăugați doi ascultători pe fereastră; unul pentru când se încarcă documentul, așa cum sa menționat mai sus, și apoi altul pentru a asculta evenimentul de redimensionare.
Puțin în plus A11Y
Este posibil să adăugați puțină considerație suplimentară pentru accesibilitate utilizând atributele aria-expanded
, aria-controls
și aria-labelledby
. Acest lucru va oferi o indicație mai bună asupra tehnologiei asistate atunci când sertarele au fost deschise/extinse. Adăugăm aria-expanded="false"
la marcarea butonului nostru alături aria-controls="IDofcontent"
, unde IDofcontent
este valoarea unui id pe care îl adăugăm la containerul de conținut.
Apoi folosim un alt operator ternar pentru a comuta atributul aria-expanded
la clic în JavaScript.
Toti impreuna
Cu încărcarea paginii, sertare multiple, lucru suplimentar A11Y și gestionarea evenimentelor de redimensionare, codul nostru JavaScript arată astfel:
var containers; function initDrawers() { // Get the containing elements containers = document.querySelectorAll(".container"); setHeights(); wireUpTriggers(); window.addEventListener("resize", setHeights); } window.addEventListener("load", initDrawers); function setHeights() { containers.forEach(container => { // Get content let content = container.querySelector(".content"); content.removeAttribute("aria-hidden"); // Height of content to show/hide let heightOfContent = content.getBoundingClientRect().height; // Set a CSS custom property with the height of content container.style.setProperty("--containerHeight", `${heightOfContent}px`); // Once height is read and set setTimeout(e => { container.classList.add("height-is-set"); content.setAttribute("aria-hidden", "true"); }, 0); }); } function wireUpTriggers() { containers.forEach(container => { // Get each trigger element let btn = container.querySelector(".trigger"); // Get content let content = container.querySelector(".content"); btn.addEventListener("click", () => { btn.setAttribute("aria-expanded", btn.getAttribute("aria-expanded") === "false" ? "true" : "false"); container.setAttribute( "data-drawer-showing", container.getAttribute("data-drawer-showing") === "true" ? "false" : "true" ); content.setAttribute( "aria-hidden", content.getAttribute("aria-hidden") === "true" ? "false" : "true" ); }); }); }
Puteți juca cu el și pe CodePen aici:
Sertar ușor de afișat/ascunde (Multiple) de Ben Frain pe CodePen.
rezumat
Este posibil să continuați pentru o perioadă de timp rafinarea și satisfacerea mai multor situații, dar mecanismele de bază ale creării unui sertar de deschidere și închidere de încredere pentru conținutul dvs. ar trebui să fie acum la îndemâna dvs. Sperăm că sunteți, de asemenea, conștienți de unele pericole. Elementul de details
nu poate fi animat, max-height: auto
nu face ceea ce ați sperat, nu puteți adăuga în mod fiabil o valoare maximă masivă și vă așteptați ca toate panourile de conținut să se deschidă așa cum vă așteptați.
Pentru a reitera abordarea noastră aici: măsurați containerul, stocați-i înălțimea ca proprietate personalizată CSS, ascundeți conținutul și apoi utilizați o comutare simplă pentru a comuta între max-height
de 0 și înălțimea pe care ați stocat-o în proprietatea personalizată.
S-ar putea să nu fie cea mai performantă metodă, dar am descoperit că pentru majoritatea situațiilor este perfect adecvată și beneficiază de faptul că este relativ simplu de implementat.