Crea i tuoi pannelli di contenuto in espansione e in contrazione

Pubblicato: 2022-03-10
Riepilogo rapido ↬ In UI/UX, uno schema comune che è necessario più e più volte è quello di un semplice pannello animato di apertura e chiusura, o 'cassetto'. Non hai bisogno di una libreria per realizzarli. Con un po' di HTML/CSS e JavaScript di base, impareremo come farlo da soli.

Finora li abbiamo chiamati "pannelli di apertura e chiusura", ma sono anche descritti come pannelli di espansione, o più semplicemente pannelli espandibili.

Per chiarire esattamente di cosa stiamo parlando, vai a questo esempio su CodePen:

Facile mostra/nascondi drawer (Multiples) di Ben Frain su CodePen.

Facile mostra/nascondi drawer (Multiples) di Ben Frain su CodePen.

Questo è ciò che costruiremo in questo breve tutorial.

Dal punto di vista della funzionalità, ci sono alcuni modi per ottenere l'apertura e la chiusura animate che stiamo cercando. Ogni approccio con i suoi vantaggi e compromessi. Condividerò i dettagli del mio metodo "go-to" in dettaglio in questo articolo. Consideriamo prima possibili approcci.

Si avvicina

Ci sono variazioni su queste tecniche, ma in generale, gli approcci rientrano in una delle tre categorie:

  1. Animare/trasferire l' height o l'altezza max-height del contenuto.
  2. Usa transform: translateY per spostare gli elementi in una nuova posizione, dando l'illusione di un pannello che si chiude e quindi renderizza nuovamente il DOM una volta completata la trasformazione con gli elementi nella loro posizione finale.
  3. Usa una libreria che esegue una combinazione/variazione di 1 o 2!
Altro dopo il salto! Continua a leggere sotto ↓

Considerazioni su ogni approccio

Dal punto di vista delle prestazioni, l'uso di una trasformazione è più efficace dell'animazione o della transizione dell'altezza/altezza massima. Con una trasformazione, gli elementi in movimento vengono rasterizzati e spostati dalla GPU. Questa è un'operazione economica e facile per una GPU, quindi le prestazioni tendono ad essere molto migliori.

I passaggi di base quando si utilizza un approccio di trasformazione sono:

  1. Ottieni l'altezza del contenuto da comprimere.
  2. Sposta il contenuto e tutto ciò che segue in base all'altezza del contenuto da comprimere usando transform: translateY(Xpx) . Azionare la trasformazione con la transizione di scelta per dare un piacevole effetto visivo.
  3. Usa JavaScript per ascoltare l'evento di transitionend . Quando si attiva, display: none il contenuto e rimuovi la trasformazione e tutto dovrebbe essere nel posto giusto.

Non suona male, vero?

Tuttavia, ci sono una serie di considerazioni con questa tecnica, quindi tendo a evitarla per implementazioni casuali a meno che le prestazioni non siano assolutamente cruciali.

Ad esempio, con l'approccio transform: translateY devi considerare lo z-index degli elementi. Per impostazione predefinita, gli elementi che si trasformano sono dopo l'elemento trigger nel DOM e quindi appaiono in cima alle cose prima di loro quando vengono tradotti in alto.

Devi anche considerare quante cose appaiono dopo il contenuto che vuoi comprimere nel DOM. Se non vuoi un grande buco nel tuo layout, potresti trovare più facile usare JavaScript per racchiudere tutto ciò che vuoi spostare in un elemento contenitore e spostarlo semplicemente. Gestibile ma abbiamo appena introdotto più complessità! Questo è, tuttavia, il tipo di approccio che ho adottato quando ho spostato i giocatori su e giù in In/Out. Puoi vedere come è stato fatto qui.

Per esigenze più casuali, tendo a passare max-height del contenuto. Questo approccio non funziona bene come una trasformazione. Il motivo è che il browser sta modificando l'altezza dell'elemento compresso durante la transizione; ciò causa molti calcoli di layout che non sono così economici per il computer host.

Tuttavia, questo approccio vince dal punto di vista della semplicità. Il vantaggio di subire il suddetto colpo computazionale è che il riflusso del DOM si prende cura della posizione e della geometria di tutto. Abbiamo molto poco in termini di calcoli da scrivere, inoltre il JavaScript necessario per eseguirlo bene è relativamente semplice.

L'elefante nella stanza: dettagli ed elementi di riepilogo

Coloro che hanno una conoscenza approfondita degli elementi HTML sapranno che esiste una soluzione HTML nativa a questo problema sotto forma di details ed elementi di summary . Ecco alcuni esempi di markup:

 <details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>

Per impostazione predefinita, i browser forniscono un piccolo triangolo di apertura accanto all'elemento di riepilogo; fare clic sul riepilogo e viene visualizzato il contenuto sotto il riepilogo.

Ottimo, eh? I dettagli supportano anche l'evento toggle in JavaScript, quindi puoi fare questo genere di cose per eseguire cose diverse a seconda che sia aperto o chiuso (non preoccuparti se quel tipo di espressione JavaScript sembra strano; ci arriveremo in più dettagli a breve):

 details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })

OK, fermerò la tua eccitazione proprio lì. I dettagli e gli elementi di riepilogo non si animano. Non per impostazione predefinita e al momento non è possibile aprirli e chiuderli con CSS e JavaScript aggiuntivi.

Se sai altrimenti, mi piacerebbe essere smentito.

Purtroppo, poiché abbiamo bisogno di un'estetica di apertura e chiusura, dovremo rimboccarci le maniche e fare il lavoro migliore e più accessibile possibile con gli altri strumenti a nostra disposizione.

Bene, con le notizie deprimenti fuori mano, andiamo avanti con la realizzazione di questa cosa.

Modello di markup

Il markup di base sarà simile a questo:

 <div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>

Abbiamo un contenitore esterno per avvolgere l'espansore e il primo elemento è il pulsante che funge da trigger per l'azione. Notare l'attributo type nel pulsante? Lo includo sempre poiché per impostazione predefinita un pulsante all'interno di un modulo eseguirà un invio. Se ti ritrovi a perdere un paio d'ore chiedendoti perché il tuo modulo non funziona e i pulsanti sono coinvolti nel tuo modulo; assicurati di controllare l'attributo type!

L'elemento successivo dopo il pulsante è il cassetto dei contenuti stesso; tutto quello che vuoi nascondere e mostrare.

Per dare vita alle cose, utilizzeremo proprietà personalizzate CSS, transizioni CSS e un po' di JavaScript.

Logica di base

La logica di base è questa:

  1. Lascia che la pagina si carichi, misura l'altezza del contenuto.
  2. Imposta l'altezza del contenuto sul contenitore come valore di una proprietà personalizzata CSS.
  3. Nascondi immediatamente il contenuto aggiungendo un attributo aria-hidden: "true" . L'uso aria-hidden assicura che la tecnologia assistiva sappia che anche il contenuto è nascosto.
  4. Collega il CSS in modo che l' max-height della classe di contenuto sia il valore della proprietà personalizzata.
  5. Premendo il nostro pulsante di attivazione si alterna la proprietà aria-hidden da true a false che a sua volta alterna l' max-height del contenuto tra 0 e l'altezza impostata nella proprietà personalizzata. Una transizione su quella proprietà fornisce il tocco visivo: adattati al gusto!

Nota: ora, questo sarebbe un semplice caso di commutazione di una classe o di un attributo se max-height: auto è uguale all'altezza del contenuto. Purtroppo non è così. Vai a gridarlo al W3C qui.

Diamo un'occhiata a come questo approccio si manifesta nel codice. I commenti numerati mostrano i passaggi logici equivalenti dall'alto nel codice.

Ecco il 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"); })

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

Punti di nota

E i cassetti multipli?

Quando hai un numero di cassetti apri e nascondi su una pagina, dovrai scorrerli tutti in quanto probabilmente avranno dimensioni diverse.

Per gestirlo dovremo eseguire querySelectorAll per ottenere tutti i contenitori e quindi eseguire nuovamente l'impostazione delle variabili personalizzate per ogni contenuto all'interno di un forEach .

Quel setTimeout

Ho un setTimeout con durata 0 prima di impostare il contenitore da nascondere. Questo è probabilmente non necessario, ma lo uso come approccio "cintura e bretelle" per garantire che la pagina sia stata renderizzata prima in modo che le altezze del contenuto siano disponibili per la lettura.

Attivalo solo quando la pagina è pronta

Se hai altre cose in corso, potresti scegliere di racchiudere il codice del cassetto in una funzione che viene inizializzata al caricamento della pagina. Ad esempio, supponiamo che la funzione drawer sia stata racchiusa in una funzione chiamata initDrawers , potremmo farlo:

 window.addEventListener("load", initDrawers);

In effetti, lo aggiungeremo a breve.

Dati aggiuntivi-* attributi sul contenitore

C'è un attributo di dati sul contenitore esterno che viene anche attivato. Questo viene aggiunto nel caso ci sia qualcosa che deve cambiare con il grilletto o il contenitore mentre il cassetto si apre/si chiude. Ad esempio, forse vogliamo cambiare il colore di qualcosa o rivelare o attivare o disattivare un'icona.

Valore predefinito sulla proprietà personalizzata

C'è un valore predefinito impostato sulla proprietà personalizzata in CSS di 1000px . Questo è il bit dopo la virgola all'interno del valore: var(--containerHeight, 1000px) . Ciò significa che se --containerHeight viene incasinato in qualche modo, dovresti comunque avere una transizione decente. Ovviamente puoi impostarlo su ciò che è adatto al tuo caso d'uso.

Perché non utilizzare semplicemente un valore predefinito di 100000px?

Data quell'altezza max-height: auto non effettua la transizione, ti starai chiedendo perché non opti semplicemente per un'altezza impostata di un valore maggiore di quello di cui avresti mai bisogno. Ad esempio, 10000000px?

Il problema con quell'approccio è che passerà sempre da quell'altezza. Se la durata della transizione è impostata su 1 secondo, la transizione "viaggerà" di 10000000 pixel in un secondo. Se il tuo contenuto è alto solo 50px, otterrai un effetto di apertura/chiusura piuttosto rapido!

Operatore ternario per commutatori

Abbiamo utilizzato un operatore ternario un paio di volte per alternare gli attributi. Alcune persone li odiano, ma io e altri li amo. All'inizio potrebbero sembrare un po' strani e un po' "codice golf", ma una volta che ti sei abituato alla sintassi, penso che siano una lettura più semplice di un if/else standard.

Per chi non lo sapesse, un operatore ternario è una forma condensata di if/else. Sono scritti in modo che la cosa da controllare sia prima, poi il ? separa cosa eseguire se il controllo è vero, quindi : per distinguere cosa deve essere eseguito se il controllo è falso.

 isThisTrue ? doYesCode() : doNoCode();

I nostri alternatori di attributi funzionano controllando se un attributo è impostato su "true" e, in tal caso, impostandolo su "false" , altrimenti impostandolo su "true" .

Cosa succede al ridimensionamento della pagina?

Se un utente ridimensiona la finestra del browser, è molto probabile che l'altezza dei nostri contenuti cambi. Pertanto potresti voler rieseguire l'impostazione dell'altezza per i contenitori in quello scenario. Ora stiamo considerando tali eventualità, sembra un buon momento per riformulare un po' le cose.

Possiamo creare una funzione per impostare le altezze e un'altra funzione per gestire le interazioni. Quindi aggiungi due ascoltatori nella finestra; uno per quando il documento viene caricato, come menzionato sopra, e poi un altro per ascoltare l'evento di ridimensionamento.

Un po' di A11Y in più

È possibile aggiungere una piccola considerazione in più per l'accessibilità utilizzando gli attributi aria-expanded , aria-controls e aria-labelledby . Ciò darà una migliore indicazione alla tecnologia assistita quando i cassetti sono stati aperti/espansi. Aggiungiamo aria-expanded="false" al markup del pulsante insieme ad aria-controls="IDofcontent" , dove IDofcontent è il valore di un id che aggiungiamo al contenitore di contenuti.

Quindi utilizziamo un altro operatore ternario per attivare l'attributo aria-expanded al clic nel JavaScript.

Tutti insieme

Con il caricamento della pagina, più cassetti, lavoro extra A11Y e gestione degli eventi di ridimensionamento, il nostro codice JavaScript è simile al seguente:

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

Puoi anche giocarci su CodePen qui:

Facile mostra/nascondi drawer (Multiples) di Ben Frain su CodePen.

Facile mostra/nascondi drawer (Multiples) di Ben Frain su CodePen.

Sommario

È possibile continuare per un po' di tempo perfezionando ulteriormente e provvedendo a un numero sempre maggiore di situazioni, ma i meccanismi di base per creare un cassetto di apertura e chiusura affidabile per i tuoi contenuti dovrebbero ora essere alla tua portata. Si spera che anche tu sia a conoscenza di alcuni dei rischi. L'elemento dei details non può essere animato, max-height: auto non fa ciò che speravi, non puoi aggiungere in modo affidabile un enorme valore di altezza massima e aspettarti che tutti i pannelli dei contenuti si aprano come previsto.

Per ripetere il nostro approccio qui: misura il contenitore, memorizza la sua altezza come proprietà personalizzata CSS, nascondi il contenuto e quindi usa un semplice interruttore per passare max-height di 0 all'altezza che hai memorizzato nella proprietà personalizzata.

Potrebbe non essere il metodo con le migliori prestazioni in assoluto, ma ho scoperto che per la maggior parte delle situazioni è perfettamente adeguato e trae vantaggio dall'essere relativamente semplice da implementare.