Kendi Genişleyen ve Daralan İçerik Panellerinizi Yapın
Yayınlanan: 2022-03-10Bunları şimdiye kadar bir 'açma ve kapama paneli' olarak adlandırdık, ancak bunlar aynı zamanda genişleme panelleri veya daha basit bir ifadeyle genişleyen paneller olarak da tanımlanmaktadır.
Tam olarak neden bahsettiğimizi netleştirmek için CodePen'deki bu örneğe gidin:
Bu kısa eğitimde inşa edeceğimiz şey budur.
İşlevsellik açısından, aradığımız animasyonlu açma ve kapamaya ulaşmanın birkaç yolu vardır. Her yaklaşımın kendi faydaları ve takasları vardır. Bu yazıda 'git' yöntemimin detaylarını detaylı olarak paylaşacağım. Önce olası yaklaşımları ele alalım.
Yaklaşımlar
Bu tekniklerin varyasyonları vardır, ancak genel olarak konuşursak, yaklaşımlar üç kategoriden birine girer:
- İçeriğin
height
veyamax-height
yüksekliğini canlandırın/geçiş yapın. - Öğeleri yeni bir konuma taşımak için
transform: translateY
kullanın, bu da bir panelin kapandığı yanılsaması verir ve ardından dönüşüm, öğeler bitiş konumlarındayken dönüşüm tamamlandığında DOM'yi yeniden oluşturur. - 1 veya 2'nin bazı kombinasyonlarını/varyasyonlarını yapan bir kitaplık kullanın!
Her Yaklaşımın Hususları
Performans açısından bakıldığında, bir dönüşüm kullanmak, yükseklik/maksimum yüksekliği canlandırmak veya geçiş yapmaktan daha etkilidir. Bir dönüşümle, hareketli öğeler rasterleştirilir ve GPU tarafından kaydırılır. Bu, bir GPU için ucuz ve kolay bir işlemdir, bu nedenle performans çok daha iyi olma eğilimindedir.
Bir dönüştürme yaklaşımı kullanırken temel adımlar şunlardır:
- Daraltılacak içeriğin yüksekliğini alın.
- İçeriği ve sonraki her şeyi,
transform: translateY(Xpx)
kullanarak daraltılacak içeriğin yüksekliğine göre taşıyın. Hoş bir görsel efekt vermek için dönüşümü tercih edilen geçişle çalıştırın. -
transitionend
olayı olayını dinlemek için JavaScript kullanın. Ateşlendiğinde, şunudisplay: none
içeriği ve dönüşümü kaldırın ve her şey doğru yerde olmalıdır.
Kulağa çok kötü gelmiyor, değil mi?
Bununla birlikte, bu teknikle ilgili bir takım hususlar vardır, bu yüzden performans kesinlikle çok önemli olmadıkça, sıradan uygulamalar için bundan kaçınma eğilimindeyim.
Örneğin, transform: translateY
yaklaşımıyla, öğelerin z-index
dikkate almanız gerekir. Varsayılan olarak, dönüşen öğeler DOM'daki tetikleyici öğeden sonradır ve bu nedenle, çevrildiğinde onlardan önceki öğelerin üstünde görünür.
Ayrıca DOM'da daraltmak istediğiniz içerikten sonra kaç tane şeyin göründüğünü de göz önünde bulundurmalısınız. Mizanpajınızda büyük bir delik istemiyorsanız, taşımak istediğiniz her şeyi bir kapsayıcı öğesinde sarmak için JavaScript'i kullanmayı ve yalnızca onu taşımayı daha kolay bulabilirsiniz. Yönetilebilir, ancak daha fazla karmaşıklık getirdik! Ancak bu, oyuncuları İçeri/Dışarıda yukarı ve aşağı hareket ettirirken izlediğim yaklaşım türüdür . Bunun nasıl yapıldığını buradan görebilirsiniz.
Daha gündelik ihtiyaçlar için, içeriğin max-height
değiştirme eğilimindeyim. Bu yaklaşım, bir dönüşüm kadar iyi performans göstermez. Tarayıcının geçiş boyunca çöken öğenin yüksekliğinin arasını doldurmasının nedeni; bu, ana bilgisayar için ucuz olmayan birçok yerleşim hesaplamasına neden olur.
Ancak, bu yaklaşım basitlik açısından kazanır. Yukarıda bahsedilen hesaplama isabetinden muzdarip olmanın getirisi, DOM yeniden akışının her şeyin konumu ve geometrisi ile ilgilenmesidir. Yazacak çok az hesaplamamız var ve bunu başarmak için gereken JavaScript nispeten basit.
Odadaki Fil: Ayrıntılar ve Özet Öğeler
HTML öğeleri hakkında derin bilgiye sahip olanlar, bu soruna details
ve summary
öğeler biçiminde yerel bir HTML çözümü olduğunu bileceklerdir. İşte bazı örnek işaretleme:
<details> <summary>Click to open/close</summary> Here is the content that is revealed when clicking the summary... </details>
Varsayılan olarak, tarayıcılar özet öğesinin yanında küçük bir açıklama üçgeni sağlar; özeti tıklayın ve özetin altındaki içerik ortaya çıkıyor.
Harika, hey? Ayrıntılar, JavaScript'te toggle
olayını bile destekler, böylece açık veya kapalı olmasına bağlı olarak farklı şeyler gerçekleştirmek için bu tür şeyler yapabilirsiniz (bu tür bir JavaScript ifadesi tuhaf görünüyorsa endişelenmeyin; buna daha fazla değineceğiz). kısaca detay):
details.addEventListener("toggle", () => { details.open ? thisCoolThing() : thisOtherThing(); })
Tamam, heyecanını orada keseceğim. Ayrıntılar ve özet öğeler canlandırılmıyor. Varsayılan olarak değil ve şu anda ek CSS ve JavaScript ile animasyon/geçiş yapmalarını ve kapatmalarını sağlamak mümkün değil.
Aksini biliyorsan, haksız çıkmayı çok isterim.
Ne yazık ki, bir açma ve kapama estetiğine ihtiyacımız olduğu için kolları sıvamamız ve elimizdeki diğer araçlarla elimizden gelenin en iyi ve en erişilebilir işini yapmamız gerekecek.
Pekala, iç karartıcı haberler aradan çıktığına göre, hadi bu şeyi gerçekleştirmeye başlayalım.
İşaretleme Deseni
Temel işaretleme şöyle görünecek:
<div class="container"> <button type="button" class="trigger">Show/Hide content</button> <div class="content"> All the content here </div> </div>
Genişleticiyi sarmak için bir dış kabımız var ve ilk öğe, eylemi tetikleyen düğmedir. Düğmedeki type niteliğine dikkat ettiniz mi? Her zaman varsayılan olarak bir form içindeki bir düğmenin bir gönderme gerçekleştireceğini ekliyorum. Formunuzun neden çalışmadığını ve formunuzda düğmeler olduğunu merak ederek birkaç saatinizi boşa harcıyorsanız; type niteliğini kontrol ettiğinizden emin olun!
Düğmeden sonraki öğe, içerik çekmecesinin kendisidir; gizlemek ve göstermek istediğiniz her şey.
Bir şeyleri hayata geçirmek için CSS özel özelliklerini, CSS geçişlerini ve biraz JavaScript'i kullanacağız.
Temel Mantık
Temel mantık şudur:
- Sayfanın yüklenmesine izin verin, içeriğin yüksekliğini ölçün.
- İçeriğin kapsayıcı üzerindeki yüksekliğini bir CSS Özel Özelliğinin değeri olarak ayarlayın.
- Bir
aria-hidden: "true"
özniteliği ekleyerek içeriği hemen gizleyin.aria-hidden
kullanmak, yardımcı teknolojinin içeriğin de gizli olduğunu bilmesini sağlar. - İçerik sınıfının
max-height
özel özelliğin değeri olacak şekilde CSS'yi bağlayın. - Tetik düğmemize basmak, aria-hidden özelliğini true'dan false'a değiştirir, bu da içeriğin
max-height
0
ile özel özellikte ayarlanan yükseklik arasında değiştirir. Bu özellik üzerinde bir geçiş, görsel yetenek sağlar - zevke göre ayarlayın!
Not: max-height: auto
, içeriğin yüksekliğine eşitse, bu, bir sınıf veya özniteliği değiştirmenin basit bir durumu olacaktır. Ne yazık ki öyle değil. Git ve bunu W3C'ye buradan bağır.
Bu yaklaşımın kodda nasıl ortaya çıktığına bir göz atalım. Numaralandırılmış yorumlar, kodda yukarıdan eşdeğer mantık adımlarını gösterir.
İşte 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); }
Dikkat Edilecek Noktalar
Peki ya çoklu çekmeceler?
Bir sayfada çok sayıda aç ve gizle çekmeceniz olduğunda, büyük olasılıkla farklı boyutlarda olacağından, hepsini gözden geçirmeniz gerekir.
Bununla başa çıkmak için tüm kapsayıcıları almak için bir querySelectorAll
ve ardından bir forEach
içindeki her içerik için özel değişkenler ayarınızı yeniden çalıştırmamız gerekecek.
Bu setTimeout
Kapsayıcıyı gizlenecek şekilde ayarlamadan önce 0
süreli bir setTimeout
var. Bu muhtemelen gereksizdir, ancak sayfanın önce işlenmesini sağlamak için bir 'kemer ve parantez' yaklaşımı olarak kullanıyorum, böylece içeriğin yükseklikleri okunabilir.
Bunu yalnızca sayfa hazır olduğunda ateşleyin
Devam eden başka işleriniz varsa, çekmece kodunuzu sayfa yüklendiğinde başlatılan bir işleve sarmayı seçebilirsiniz. Örneğin, çekmece işlevinin initDrawers
adlı bir işleve sarıldığını varsayalım, bunu yapabiliriz:
window.addEventListener("load", initDrawers);
Aslında onu da birazdan ekleyeceğiz.
Kapsayıcıdaki ek data-* öznitelikleri
Dış kapta da değiştirilen bir veri özniteliği vardır. Bu, çekmece açılırken/kapanırken tetik veya kap ile değişmesi gereken herhangi bir şey olması durumunda eklenir. Örneğin, belki bir şeyin rengini değiştirmek veya bir simgeyi ortaya çıkarmak veya değiştirmek istiyoruz.
Özel mülkte varsayılan değer
1000px
özel özellikte varsayılan bir değer ayarlanmıştır. Bu, değerin içindeki virgülden sonraki kısımdır: var(--containerHeight, 1000px)
. Bu, --containerHeight
bir şekilde bozulursa, yine de düzgün bir geçişe sahip olmanız gerektiği anlamına gelir. Bunu, kullanım durumunuza uygun olana açıkça ayarlayabilirsiniz.
Neden Sadece 100000px Varsayılan Değerini Kullanmıyorsunuz?
max-height: auto
geçiş yapmadığı göz önüne alındığında, neden ihtiyaç duyacağınızdan daha yüksek bir değere sahip bir yüksekliği seçmediğinizi merak ediyor olabilirsiniz. Örneğin, 10000000 piksel?
Bu yaklaşımla ilgili sorun, her zaman o yükseklikten geçiş yapacak olmasıdır. Geçiş süreniz 1 saniyeye ayarlanmışsa, geçiş bir saniyede 10000000 piksel 'seyahat eder'. İçeriğiniz yalnızca 50 piksel yüksekliğindeyse, oldukça hızlı bir açılış/kapanış efekti elde edeceksiniz!
Geçişler için üçlü operatör
Nitelikleri değiştirmek için birkaç kez üçlü operatör kullandık. Bazı insanlar onlardan nefret ediyor ama ben ve diğerleri onları seviyorum. İlk başta biraz garip ve biraz 'kod golf' gibi görünebilirler, ancak sözdizimine alıştığınızda, standart bir if/else'den daha basit bir okuma olduklarını düşünüyorum.
Başlatılmamış olanlar için, üçlü bir operatör, if/else'nin yoğunlaştırılmış bir şeklidir. Kontrol edilecek şey önce, sonra ?
kontrol doğruysa neyin yürütüleceğini ayırır ve ardından kontrol yanlışsa neyin çalışması gerektiğini ayırt etmek için :
.
isThisTrue ? doYesCode() : doNoCode();
Öznitelik geçişlerimiz, bir özniteliğin "true"
olarak ayarlanıp ayarlanmadığını kontrol ederek çalışır ve öyleyse, "false"
olarak ayarlayın, aksi takdirde "true"
olarak ayarlayın.
Sayfa yeniden boyutlandırıldığında ne olur?
Bir kullanıcı tarayıcı penceresini yeniden boyutlandırırsa, içeriğimizin yüksekliklerinin değişme olasılığı yüksektir. Bu nedenle, bu senaryoda kapların yüksekliğini ayarlamayı yeniden çalıştırmak isteyebilirsiniz. Şimdi bu tür olasılıkları düşünüyoruz, bazı şeyleri yeniden gözden geçirmek için iyi bir zaman gibi görünüyor.
Yükseklikleri ayarlamak için bir işlev ve etkileşimlerle başa çıkmak için başka bir işlev yapabiliriz. Ardından pencereye iki dinleyici ekleyin; biri yukarıda belirtildiği gibi belge yüklendiğinde, diğeri yeniden boyutlandırma olayını dinlemek için.
Biraz fazladan A11Y
aria-expanded
, aria-controls
ve aria-labelledby
özniteliklerinden yararlanarak erişilebilirlik için biraz fazladan değerlendirme eklemek mümkündür. Bu, çekmeceler açıldığında/genişletildiğinde destekli teknolojiye daha iyi bir gösterge verecektir. Düğme işaretlememize aria-controls="IDofcontent"
ile birlikte aria aria-expanded="false"
ekleriz; burada IDofcontent
, içerik kabına eklediğimiz kimliğin değeridir.
Ardından, JavaScript'te tıklandığında aria-expanded
özniteliği değiştirmek için başka bir üçlü operatör kullanırız.
Hep birlikte
Sayfa yükleme, çoklu çekmeceler, ekstra A11Y çalışması ve yeniden boyutlandırma olaylarını işleme ile JavaScript kodumuz şöyle görünür:
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" ); }); }); }
Bununla CodePen'de de oynayabilirsiniz:
Özet
Giderek daha fazla durum için daha fazla rafine etme ve yiyecek sağlama bir süre daha devam etmek mümkündür, ancak içeriğiniz için güvenilir bir açma ve kapama çekmecesi oluşturmanın temel mekaniği artık elinizin altında olmalıdır. Umarım, bazı tehlikelerin de farkındasınızdır. details
öğesi canlandırılamaz, max-height: auto
umduğunuz şeyi yapmaz, güvenilir bir şekilde büyük bir maksimum yükseklik değeri ekleyemez ve tüm içerik panellerinin beklendiği gibi açılmasını bekleyemezsiniz.
Buradaki yaklaşımımızı tekrarlamak için: kabı ölçün, yüksekliğini bir CSS özel özelliği olarak saklayın, içeriği gizleyin ve ardından max-height
0 ile özel özellikte sakladığınız yükseklik arasında geçiş yapmak için basit bir geçiş kullanın.
Mutlak en iyi performans gösteren yöntem olmayabilir, ancak çoğu durumda tamamen yeterli olduğunu ve uygulanmasının nispeten basit olmasından fayda sağladığını gördüm.