Twórz własne rozszerzające się i kurczące panele treści
Opublikowany: 2022-03-10Do tej pory nazywaliśmy je „panelem otwierającym i zamykającym”, ale są one również opisywane jako panele rozszerzające, lub prościej, panele rozszerzające.
Aby dokładnie wyjaśnić, o czym mówimy, przejdź do tego przykładu na CodePen:
Właśnie to będziemy budować w tym krótkim samouczku.
Z punktu widzenia funkcjonalności istnieje kilka sposobów na osiągnięcie animowanego otwarcia i zamknięcia, którego szukamy. Każde podejście ma swoje zalety i kompromisy. W tym artykule opiszę szczegółowo moją metodę „idź do”. Rozważmy najpierw możliwe podejścia.
Podejścia
Istnieją różne odmiany tych technik, ale ogólnie rzecz biorąc, podejścia można podzielić na jedną z trzech kategorii:
- Animuj/przenoś
height
lubmax-height
treści. - Użyj
transform: translateY
, aby przenieść elementy do nowej pozycji, dając złudzenie zamknięcia panelu, a następnie ponownie renderuj DOM po zakończeniu transformacji z elementami w ich końcowej pozycji. - Użyj biblioteki, która wykonuje kombinację/wariację 1 lub 2!
Rozważania każdego podejścia
Z punktu widzenia wydajności użycie transformacji jest bardziej efektywne niż animowanie lub przenoszenie wysokości/maksymalnej wysokości. Dzięki transformacji ruchome elementy są rasteryzowane i przesuwane przez GPU. Jest to tania i łatwa operacja dla GPU, więc wydajność jest znacznie lepsza.
Podstawowe kroki podczas korzystania z podejścia przekształcającego to:
- Uzyskaj wysokość zawartości do zwinięcia.
- Przenieś zawartość i wszystko po niej o wysokość zawartości, która ma być zwinięta, używając
transform: translateY(Xpx)
. Obsługuj transformację z wybranym przejściem, aby uzyskać przyjemny efekt wizualny. - Użyj JavaScript, aby nasłuchiwać zdarzenia
transitionend
. Po uruchomieniudisplay: none
treści i usuń transformację, a wszystko powinno być we właściwym miejscu.
Nie brzmi tak źle, prawda?
Jest jednak kilka kwestii związanych z tą techniką, więc staram się jej unikać w przypadku zwykłych implementacji, chyba że wydajność jest absolutnie kluczowa.
Na przykład przy podejściu transform: translateY
musisz wziąć pod uwagę z-index
z elementów. Domyślnie elementy, które przekształcają się w górę, znajdują się po elemencie wyzwalającym w DOM i dlatego pojawiają się na wierzchu elementów przed nimi po przetłumaczeniu w górę.
Musisz także zastanowić się, ile rzeczy pojawia się po zawartości, którą chcesz zwinąć w DOM. Jeśli nie chcesz dużej dziury w swoim układzie, może być łatwiej użyć JavaScript, aby zawinąć wszystko, co chcesz przenieść, w elemencie kontenera i po prostu to przenieść. Zarządzane, ale właśnie wprowadziliśmy większą złożoność! Jest to jednak podejście, które wybrałem , przesuwając graczy w górę i w dół w trybie In/Out. Tutaj możesz zobaczyć, jak to zostało zrobione.
W przypadku bardziej nieformalnych potrzeb wybieram zmianę max-height
treści. To podejście nie działa tak dobrze, jak transformacja. Powodem jest to, że przeglądarka animuje wysokość zwijanego elementu podczas przejścia; powoduje to wiele obliczeń układu, które nie są tak tanie dla komputera hosta.
Jednak takie podejście wygrywa z punktu widzenia prostoty. Nagrodą za cierpienie z powodu wyżej wspomnianego uderzenia obliczeniowego jest to, że re-flow DOM dba o położenie i geometrię wszystkiego. Mamy bardzo mało obliczeń do napisania, a JavaScript potrzebny do wykonania tego jest stosunkowo prosty.
Słoń w pokoju: szczegóły i elementy podsumowania
Osoby z dogłębną znajomością elementów HTML będą wiedziały, że istnieje natywne rozwiązanie tego problemu w HTML w postaci details
i elementów summary
. Oto kilka przykładowych znaczników:
<details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>
Domyślnie przeglądarki udostępniają mały trójkąt ujawniania obok elementu podsumowania; kliknij podsumowanie, a zawartość pod podsumowaniem zostanie ujawniona.
Świetnie, hej? Szczegóły obsługują nawet zdarzenie toggle
w JavaScript, więc możesz robićtego rodzaju rzeczy, aby wykonywaćróżne rzeczy w zależności od tego, czy jest otwarte czy zamknięte (nie martw sięjeżeli tego rodzaju wyrażenie JavaScript wydaje siędziwne; omówimy to w dalszej części szczegóły wkrótce):
details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })
OK, zaraz zatrzymam twoje podekscytowanie. Szczegóły i elementy podsumowania nie są animowane. Nie domyślnie i obecnie nie jest możliwe ich animowanie/przechodzenie, otwieranie i zamykanie za pomocą dodatkowego CSS i JavaScript.
Jeśli wiesz inaczej, bardzo chciałbym, żeby udowodniono, że się mylę.
Niestety, ponieważ potrzebujemy estetyki otwierania i zamykania, będziemy musieli zakasać rękawy i wykonać najlepszą i najbardziej dostępną pracę, jaką możemy, korzystając z innych dostępnych nam narzędzi.
Racja, pomijając przygnębiające wieści, zacznijmy to robić.
Wzór znaczników
Podstawowy znacznik będzie wyglądał tak:
<div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>
Mamy zewnętrzny pojemnik do owijania ekspandera, a pierwszym elementem jest przycisk, który jest wyzwalaczem akcji. Zwróć uwagę na atrybut type w przycisku? Zawsze uwzględniam, że domyślnie przycisk w formularzu wykona przesłanie. Jeśli tracisz kilka godzin na zastanawianie się, dlaczego formularz nie działa, a przyciski są zaangażowane w formularz; upewnij się, że sprawdziłeś atrybut type!
Kolejnym elementem po przycisku jest sama szuflada zawartości; wszystko, co chcesz ukryć i pokazać.
Aby ożywić rzeczy, wykorzystamy niestandardowe właściwości CSS, przejścia CSS i trochę JavaScript.
Podstawowa logika
Podstawowa logika jest następująca:
- Załaduj stronę, zmierz wysokość treści.
- Ustaw wysokość zawartości w kontenerze jako wartość niestandardowej właściwości CSS.
- Natychmiast ukryj treść, dodając do niej atrybut
aria-hidden: "true"
. Korzystaniearia-hidden
zapewnia, że technologia wspomagająca wie, że treść również jest ukryta. - Połącz CSS, aby
max-height
klasy treści była wartością właściwości niestandardowej. - Naciśnięcie naszego przycisku wyzwalacza przełącza właściwość aria-hidden z true na false, co z kolei przełącza
max-height
treści między0
a wysokością ustawioną we właściwości niestandardowej. Przejście na tej właściwości zapewnia wizualną atmosferę — dostosuj się do gustu!
Uwaga: byłby to prosty przypadek przełączania klasy lub atrybutu, gdyby max-height: auto
było równe wysokości treści. Niestety tak nie jest. Idź i krzycz o tym do W3C tutaj.
Przyjrzyjmy się, jak to podejście manifestuje się w kodzie. Ponumerowane komentarze pokazują równoważne kroki logiczne z góry w kodzie.
Oto 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); }
Ważne punkty
A co z wieloma szufladami?
Jeśli masz na stronie kilka otwartych i ukrytych szuflad, musisz przejrzeć je wszystkie, ponieważ prawdopodobnie będą miały różne rozmiary.
Aby to obsłużyć, musimy wykonać querySelectorAll
, aby pobrać wszystkie kontenery , a następnie ponownie uruchomić ustawienia zmiennych niestandardowych dla każdej zawartości w forEach
.
Ten setTimeout
Mam setTimeout
z czasem trwania 0
przed ustawieniem kontenera do ukrycia. Jest to prawdopodobnie niepotrzebne, ale używam go jako podejścia „pasa i szelek”, aby upewnić się, że strona została wyrenderowana jako pierwsza, aby można było odczytać wysokość treści.
Uruchom to tylko wtedy, gdy strona jest gotowa
Jeśli masz inne rzeczy, możesz zdecydować się na zawinięcie kodu szuflady w funkcję, która jest inicjowana podczas ładowania strony. Załóżmy na przykład, że funkcja szuflady została opakowana w funkcję o nazwie initDrawers
, możemy to zrobić:
window.addEventListener("load", initDrawers);
W rzeczywistości dodamy to wkrótce.
Dodatkowe atrybuty data-* w kontenerze
W zewnętrznym kontenerze znajduje się atrybut danych, który również jest przełączany. Jest to dodawane w przypadku, gdy trzeba coś zmienić za pomocą spustu lub pojemnika podczas otwierania/zamykania szuflady. Na przykład, być może chcemy zmienić kolor czegoś lub odsłonić lub przełączyć ikonę.
Wartość domyślna we właściwości niestandardowej
We właściwości niestandardowej w CSS ustawiono domyślną wartość 1000px
. To jest bit po przecinku wewnątrz wartości: var(--containerHeight, 1000px)
. Oznacza to, że jeśli --containerHeight
zostanie w jakiś sposób zepsuty, nadal powinieneś mieć przyzwoite przejście. Możesz oczywiście ustawić to, co jest odpowiednie dla twojego przypadku użycia.
Dlaczego po prostu nie użyć domyślnej wartości 100000px?
Biorąc pod uwagę, że max-height: auto
nie zmienia się, możesz się zastanawiać, dlaczego nie wybierasz po prostu ustawionej wysokości o wartości większej niż kiedykolwiek potrzebna. Na przykład 10000000px?
Problem z tym podejściem polega na tym, że zawsze będzie przechodził z tej wysokości. Jeśli czas trwania przejścia jest ustawiony na 1 sekundę, przejście „przejedzie” o 10000000 pikseli w ciągu sekundy. Jeśli zawartość ma tylko 50 pikseli, uzyskasz dość szybki efekt otwierania/zamykania!
Operator trójargumentowy dla przełączników
Kilka razy korzystaliśmy z operatora potrójnego, aby przełączać atrybuty. Niektórzy ich nienawidzą, ale ja i inni ich kocham. Na początku mogą wydawać się nieco dziwne i trochę „kodować golfa”, ale kiedy przyzwyczaisz się do składni, myślę, że są one prostsze niż standardowe if/else.
Dla niewtajemniczonych operator trójskładnikowy jest skróconą formą if/else. Są napisane tak, że najpierw trzeba sprawdzić, a potem ?
oddziela, co należy wykonać, jeśli sprawdzenie jest prawdziwe, a następnie :
, aby odróżnić, co powinno zostać wykonane, jeśli sprawdzenie jest fałszywe.
isThisTrue ? doYesCode() : doNoCode();
Nasze przełączniki atrybutów działają, sprawdzając, czy atrybut jest ustawiony na "true"
, a jeśli tak, ustaw go na "false"
, w przeciwnym razie ustaw go na "true"
.
Co się dzieje po zmianie rozmiaru strony?
Jeśli użytkownik zmieni rozmiar okna przeglądarki, istnieje duże prawdopodobieństwo, że zmieni się wysokość naszych treści. Dlatego możesz chcieć ponownie uruchomić ustawianie wysokości dla kontenerów w tym scenariuszu. Teraz zastanawiamy się nad takimi ewentualnościami, wydaje się, że to dobry czas, aby trochę zmienić rzeczy.
Możemy stworzyć jedną funkcję do ustawiania wysokości, a drugą do obsługi interakcji. Następnie dodaj dwóch słuchaczy w oknie; jeden do ładowania dokumentu, jak wspomniano powyżej, a drugi do nasłuchiwania zdarzenia zmiany rozmiaru.
Trochę więcej A11Y
Można dodać trochę więcej uwagi na dostępność, korzystając z atrybutów aria-expanded
, aria-controls
i aria-labelledby
. Daje to lepsze wskazanie wspomaganej technologii, gdy szuflady zostały otwarte/rozszerzone. Dodajemy aria-expanded="false"
do naszego znacznika przycisku obok aria-controls="IDofcontent"
, gdzie IDofcontent
jest wartością identyfikatora, który dodajemy do kontenera zawartości.
Następnie używamy innego operatora potrójnego, aby przełączać atrybut aria-expanded
po kliknięciu w JavaScript.
Wszyscy razem
Z ładowaniem strony, wieloma szufladami, dodatkową pracą A11Y i obsługą zdarzeń zmiany rozmiaru, nasz kod JavaScript wygląda tak:
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" ); }); }); }
Możesz również pobawić się nim na CodePen tutaj:
Streszczenie
Możliwe jest kontynuowanie przez jakiś czas dalszego udoskonalania i zaspokajania coraz większej liczby sytuacji, ale podstawowa mechanika tworzenia niezawodnej szuflady do otwierania i zamykania zawartości powinna być teraz w Twoim zasięgu. Mamy nadzieję, że zdajesz sobie również sprawę z niektórych zagrożeń. Element details
nie może być animowany, max-height: auto
nie robi tego, czego się spodziewałeś, nie możesz niezawodnie dodać ogromnej wartości maksymalnej wysokości i oczekiwać, że wszystkie panele treści otworzą się zgodnie z oczekiwaniami.
Aby powtórzyć nasze podejście tutaj: zmierz wysokość kontenera, zapisz jego wysokość jako niestandardową właściwość CSS, ukryj zawartość, a następnie użyj prostego przełącznika, aby przełączać się między max-height
0 a wysokością zapisaną we właściwości niestandardowej.
Może nie jest to absolutnie najlepsza metoda, ale odkryłem, że w większości sytuacji jest całkowicie odpowiednia i korzysta z tego, że jest stosunkowo prosta do wdrożenia.