Buat Panel Konten Ekspansi Dan Kontrak Anda Sendiri

Diterbitkan: 2022-03-10
Ringkasan cepat Di UI/UX, pola umum yang diperlukan berulang kali adalah panel pembuka dan penutup animasi sederhana, atau 'laci'. Anda tidak perlu perpustakaan untuk membuatnya. Dengan beberapa HTML/CSS dan JavaScript dasar, kita akan belajar bagaimana melakukannya sendiri.

Kami telah menyebutnya sebagai 'panel pembuka dan penutup' sejauh ini, tetapi mereka juga digambarkan sebagai panel ekspansi, atau lebih sederhana, panel ekspansi.

Untuk memperjelas apa yang sedang kita bicarakan, lihat contoh ini di CodePen:

Menampilkan/menyembunyikan laci (Kelipatan) dengan mudah oleh Ben Frain di CodePen.

Menampilkan/menyembunyikan laci (Kelipatan) dengan mudah oleh Ben Frain di CodePen.

Itulah yang akan kita bangun dalam tutorial singkat ini.

Dari sudut pandang fungsionalitas, ada beberapa cara untuk mencapai animasi buka dan tutup yang kami cari. Setiap pendekatan dengan manfaat dan trade-off-nya sendiri. Saya akan membagikan detail metode 'go-to' saya secara rinci di artikel ini. Mari kita pertimbangkan pendekatan yang mungkin terlebih dahulu.

Pendekatan

Ada variasi pada teknik-teknik ini, tetapi secara umum, pendekatan-pendekatan tersebut termasuk dalam salah satu dari tiga kategori:

  1. Animasikan/transisikan height atau tinggi max-height konten.
  2. Gunakan transform: translateY untuk memindahkan elemen ke posisi baru, memberikan ilusi penutupan panel dan kemudian merender ulang DOM setelah transformasi selesai dengan elemen di posisi akhir.
  3. Gunakan perpustakaan yang melakukan beberapa kombinasi/variasi 1 atau 2!
Lebih banyak setelah melompat! Lanjutkan membaca di bawah ini

Pertimbangan Setiap Pendekatan

Dari perspektif kinerja, menggunakan transformasi lebih efektif daripada menganimasikan atau mentransisikan tinggi/tinggi maksimum. Dengan transformasi, elemen yang bergerak dirasterisasi dan digeser oleh GPU. Ini adalah operasi yang murah dan mudah untuk GPU sehingga kinerjanya cenderung jauh lebih baik.

Langkah-langkah dasar saat menggunakan pendekatan transformasi adalah:

  1. Dapatkan ketinggian konten yang akan diciutkan.
  2. Pindahkan konten dan semuanya setelahnya dengan ketinggian konten yang akan diciutkan menggunakan transform: translateY(Xpx) . Operasikan transformasi dengan transisi pilihan untuk memberikan efek visual yang menyenangkan.
  3. Gunakan JavaScript untuk mendengarkan acara transitionend akhir. Saat diaktifkan, display: none konten dan hapus transformasi dan semuanya harus berada di tempat yang tepat.

Kedengarannya tidak terlalu buruk, bukan?

Namun, ada sejumlah pertimbangan dengan teknik ini jadi saya cenderung menghindarinya untuk implementasi biasa kecuali jika kinerja benar-benar penting.

Misalnya, dengan pendekatan transform: translateY Anda perlu mempertimbangkan z-index elemen. Secara default, elemen yang berubah adalah setelah elemen pemicu di DOM dan karena itu muncul di atas hal-hal sebelum mereka ketika diterjemahkan ke atas.

Anda juga perlu mempertimbangkan berapa banyak hal yang muncul setelah konten yang ingin diciutkan di DOM. Jika Anda tidak ingin lubang besar di tata letak Anda, Anda mungkin merasa lebih mudah menggunakan JavaScript untuk membungkus semua yang ingin Anda pindahkan dalam elemen penampung dan pindahkan saja. Dapat dikelola tetapi kami baru saja memperkenalkan lebih banyak kerumitan! Namun, ini adalah jenis pendekatan yang saya gunakan saat memindahkan pemain ke atas dan ke bawah di In/Out. Anda dapat melihat bagaimana hal itu dilakukan di sini.

Untuk kebutuhan yang lebih kasual, saya cenderung menggunakan transisi max-height konten. Pendekatan ini tidak berkinerja sebaik transformasi. Alasannya karena browser sedang mengubah ketinggian elemen yang runtuh selama transisi; yang menyebabkan banyak perhitungan tata letak yang tidak semurah komputer host.

Namun, pendekatan ini menang dari sudut pandang kesederhanaan. Hasil dari penderitaan pukulan komputasi yang disebutkan di atas adalah bahwa aliran ulang DOM menangani posisi dan geometri segalanya. Kami memiliki sangat sedikit cara perhitungan untuk menulis ditambah JavaScript yang dibutuhkan untuk melakukannya dengan baik relatif sederhana.

Gajah Di Kamar: Detail Dan Elemen Ringkasan

Mereka yang memiliki pengetahuan mendalam tentang elemen HTML akan mengetahui bahwa ada solusi HTML asli untuk masalah ini dalam bentuk details dan elemen summary . Berikut beberapa contoh markup:

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

Secara default, browser menyediakan segitiga pengungkapan kecil di sebelah elemen ringkasan; klik ringkasan dan isi di bawah ringkasan terungkap.

Hei? Detail bahkan mendukung acara toggle dalam JavaScript sehingga Anda dapat melakukan hal semacam ini untuk melakukan hal-hal berbeda berdasarkan apakah itu terbuka atau tertutup (jangan khawatir jika ekspresi JavaScript semacam itu tampak aneh; kita akan membahasnya lebih lanjut detailnya sebentar lagi):

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

Oke, saya akan menghentikan kegembiraan Anda di sana. Detail dan elemen ringkasan tidak bernyawa. Tidak secara default dan saat ini tidak memungkinkan untuk membuat animasi/transisi terbuka dan tertutup dengan CSS dan JavaScript tambahan.

Jika Anda tahu sebaliknya, saya ingin dibuktikan salah.

Sayangnya, karena kita membutuhkan estetika pembuka dan penutup, kita harus menyingsingkan lengan baju dan melakukan pekerjaan terbaik dan paling mudah diakses yang kita bisa dengan alat lain yang kita miliki.

Benar, dengan menyingkirnya berita yang menyedihkan, mari kita mulai mewujudkannya.

Pola Markup

Markup dasar akan terlihat seperti ini:

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

Kami memiliki wadah luar untuk membungkus expander dan elemen pertama adalah tombol yang berfungsi sebagai pemicu aksi. Perhatikan atribut type di tombol? Saya selalu menyertakannya karena secara default tombol di dalam formulir akan melakukan pengiriman. Jika Anda mendapati diri Anda menghabiskan beberapa jam bertanya-tanya mengapa formulir Anda tidak berfungsi dan tombol terlibat dalam formulir Anda; pastikan Anda memeriksa atribut type!

Elemen berikutnya setelah tombol adalah laci konten itu sendiri; semua yang ingin Anda sembunyikan dan tunjukkan.

Untuk menghidupkan semuanya, kami akan menggunakan properti kustom CSS, transisi CSS, dan sedikit JavaScript.

Logika Dasar

Logika dasarnya begini:

  1. Biarkan halaman dimuat, ukur ketinggian konten.
  2. Tetapkan tinggi konten ke penampung sebagai nilai Properti Kustom CSS.
  3. Segera sembunyikan konten dengan menambahkan atribut aria-hidden: "true" ke dalamnya. Menggunakan aria-hidden memastikan teknologi bantu mengetahui bahwa konten juga disembunyikan.
  4. Hubungkan CSS sehingga max-height kelas konten adalah nilai dari properti kustom.
  5. Menekan tombol pemicu kami akan mengaktifkan properti aria-hidden dari true ke false yang pada gilirannya akan mengubah max-height konten antara 0 dan tinggi yang disetel di properti kustom. Transisi pada properti itu memberikan bakat visual — sesuaikan dengan selera!

Catatan: Sekarang, ini akan menjadi kasus sederhana untuk mengubah kelas atau atribut jika max-height: auto sama dengan tinggi konten. Sayangnya tidak. Pergi dan teriakkan tentang itu ke W3C di sini.

Mari kita lihat bagaimana pendekatan itu terwujud dalam kode. Komentar bernomor menunjukkan langkah-langkah logika yang setara dari atas dalam kode.

Berikut JavaScriptnya:

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

CSSnya:

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

Poin Catatan

Bagaimana dengan banyak laci?

Bila Anda memiliki sejumlah laci buka-dan-sembunyikan pada halaman, Anda harus mengulang semuanya karena kemungkinan ukurannya akan berbeda.

Untuk mengatasinya, kita perlu melakukan querySelectorAll untuk mendapatkan semua wadah dan kemudian menjalankan kembali pengaturan variabel khusus Anda untuk setiap konten di dalam forEach .

Waktu habis yang ditetapkan itu

Saya memiliki setTimeout dengan durasi 0 sebelum mengatur wadah agar disembunyikan. Ini bisa dibilang tidak dibutuhkan tetapi saya menggunakannya sebagai pendekatan 'ikat pinggang dan kawat gigi' untuk memastikan halaman telah dirender terlebih dahulu sehingga ketinggian konten tersedia untuk dibaca.

Hanya nyalakan ini ketika halaman sudah siap

Jika Anda memiliki hal-hal lain yang terjadi, Anda dapat memilih untuk membungkus kode laci Anda dalam fungsi yang diinisialisasi pada pemuatan halaman. Misalnya, fungsi laci dibungkus dalam fungsi yang disebut initDrawers kita dapat melakukan ini:

 window.addEventListener("load", initDrawers);

Bahkan, kami akan menambahkannya segera.

Atribut data-* tambahan pada wadah

Ada atribut data pada wadah luar yang juga diaktifkan. Ini ditambahkan jika ada sesuatu yang perlu diubah dengan pemicu atau wadah saat laci membuka/menutup. Misalnya, mungkin kita ingin mengubah warna sesuatu atau mengungkapkan atau mengganti ikon.

Nilai default pada properti khusus

Ada nilai default yang ditetapkan pada properti kustom dalam CSS 1000px . Itu sedikit setelah koma di dalam value: var(--containerHeight, 1000px) . Ini berarti jika --containerHeight menjadi kacau dalam beberapa cara, Anda masih harus memiliki transisi yang layak. Anda jelas dapat mengaturnya ke apa pun yang sesuai dengan kasus penggunaan Anda.

Mengapa Tidak Menggunakan Nilai Default 100000px?

Mengingat max-height: auto tidak bertransisi, Anda mungkin bertanya-tanya mengapa Anda tidak memilih ketinggian yang ditetapkan dengan nilai yang lebih besar daripada yang Anda perlukan. Misalnya, 10000000px?

Masalah dengan pendekatan itu adalah bahwa ia akan selalu bertransisi dari ketinggian itu. Jika durasi transisi Anda diatur ke 1 detik, transisi akan 'berjalan' 10000000px dalam satu detik. Jika konten Anda hanya setinggi 50px, Anda akan mendapatkan efek pembukaan/penutupan yang cukup cepat!

Operator ternary untuk matikan

Kami telah menggunakan operator ternary beberapa kali untuk beralih atribut. Beberapa orang membenci mereka tetapi saya, dan yang lain, mencintai mereka. Mereka mungkin tampak agak aneh dan sedikit 'kode golf' pada awalnya, tetapi begitu Anda terbiasa dengan sintaksnya, saya pikir mereka lebih mudah dibaca daripada if/else standar.

Untuk yang belum tahu, operator ternary adalah bentuk ringkas dari if/else. Mereka ditulis sedemikian rupa sehingga hal yang harus diperiksa adalah yang pertama, lalu ? memisahkan apa yang harus dijalankan jika pemeriksaan itu benar, dan kemudian : untuk membedakan apa yang harus dijalankan jika pemeriksaan itu salah.

 isThisTrue ? doYesCode() : doNoCode();

Toggles atribut kami bekerja dengan memeriksa apakah suatu atribut disetel ke "true" dan jika demikian, setel ke "false" , jika tidak, setel ke "true" .

Apa yang terjadi pada pengubahan ukuran halaman?

Jika pengguna mengubah ukuran jendela browser, ada kemungkinan besar ketinggian konten kami akan berubah. Oleh karena itu, Anda mungkin ingin menjalankan kembali pengaturan ketinggian wadah dalam skenario itu. Sekarang kami sedang mempertimbangkan kemungkinan seperti itu, sepertinya saat yang tepat untuk sedikit memperbaiki hal-hal.

Kita dapat membuat satu fungsi untuk mengatur ketinggian dan fungsi lain untuk menangani interaksi. Kemudian tambahkan dua pendengar di jendela; satu untuk saat dokumen dimuat, seperti yang disebutkan di atas, dan satu lagi untuk mendengarkan acara pengubahan ukuran.

Sedikit tambahan A11Y

Anda dapat menambahkan sedikit pertimbangan ekstra untuk aksesibilitas dengan memanfaatkan atribut aria-expanded , aria-controls dan aria-labelledby . Ini akan memberikan indikasi yang lebih baik untuk teknologi yang dibantu ketika laci telah dibuka/diperbesar. Kami menambahkan aria-expanded="false" ke markup tombol kami bersama aria-controls="IDofcontent" , di mana IDofcontent adalah nilai id yang kami tambahkan ke wadah konten.

Kemudian kami menggunakan operator ternary lain untuk mengaktifkan atribut aria-expanded saat klik di JavaScript.

Bersama

Dengan pemuatan halaman, beberapa laci, pekerjaan A11Y ekstra, dan penanganan peristiwa pengubahan ukuran, kode JavaScript kami terlihat seperti ini:

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

Anda juga dapat memainkannya di CodePen di sini:

Menampilkan/menyembunyikan laci (Kelipatan) dengan mudah oleh Ben Frain di CodePen.

Menampilkan/menyembunyikan laci (Kelipatan) dengan mudah oleh Ben Frain di CodePen.

Ringkasan

Anda dapat terus menyempurnakan dan memenuhi lebih banyak situasi untuk beberapa waktu, tetapi mekanisme dasar untuk membuat laci pembuka dan penutup yang andal untuk konten Anda sekarang harus berada dalam jangkauan Anda. Mudah-mudahan, Anda juga menyadari beberapa bahaya. Elemen details tidak dapat dianimasikan, max-height: auto tidak melakukan apa yang Anda harapkan, Anda tidak dapat dengan andal menambahkan nilai max-height yang besar dan mengharapkan semua panel konten terbuka seperti yang diharapkan.

Untuk mengulangi pendekatan kami di sini: ukur wadah, simpan tingginya sebagai properti kustom CSS, sembunyikan kontennya, lalu gunakan sakelar sederhana untuk beralih antara max-height 0 dan tinggi yang Anda simpan di properti khusus.

Ini mungkin bukan metode dengan kinerja terbaik, tetapi saya telah menemukan untuk sebagian besar situasi, metode ini sangat memadai dan mendapat manfaat dari penerapannya yang relatif mudah.