製作您自己的擴展和收縮內容面板

已發表: 2022-03-10
快速總結 ↬在 UI/UX 中,經常需要的一種常見模式是簡單的動畫打開和關閉面板,或“抽屜”。 你不需要圖書館來製作這些。 通過一些基本的 HTML/CSS 和 JavaScript,我們將學習如何自己做。

到目前為止,我們稱它們為“打開和關閉面板”,但它們也被描述為擴展面板,或者更簡單地說,擴展面板。

為了明確我們在說什麼,請繼續閱讀 CodePen 上的這個示例:

Ben Frain 在 CodePen 上的簡易顯示/隱藏抽屜(倍數)。

Ben Frain 在 CodePen 上的簡易顯示/隱藏抽屜(倍數)。

這就是我們將在這個簡短教程中構建的內容。

從功能的角度來看,有幾種方法可以實現我們正在尋找的動畫打開和關閉。 每種方法都有其自身的好處和權衡。 我將在本文中詳細分享我的“首選”方法的詳細信息。 讓我們首先考慮可能的方法。

方法

這些技術存在變體,但從廣義上講,這些方法屬於以下三類之一:

  1. 動畫/過渡內容的heightmax-height
  2. 使用transform: translateY將元素移動到新位置,給人一種面板關閉的錯覺,然後在轉換完成後重新渲染 DOM,元素處於結束位置。
  3. 使用對 1 或 2 進行某種組合/變化的庫!
跳躍後更多! 繼續往下看↓

每種方法的注意事項

從性能的角度來看,使用變換比動畫或過渡高度/最大高度更有效。 通過變換,移動元素被光柵化並被 GPU 移動。 對於 GPU 而言,這是一種廉價且簡單的操作,因此性能往往要好得多。

使用變換方法的基本步驟是:

  1. 獲取要折疊的內容的高度。
  2. 使用transform: translateY(Xpx)將內容和之後的所有內容移動到要折疊的內容的高度。 使用選擇的過渡來操作變換,以提供令人愉悅的視覺效果。
  3. 使用 JavaScript 監聽transitionend事件。 當它觸發時, display: none並刪除轉換,一切都應該在正確的位置。

聽起來還不錯,對吧?

但是,這種技術有很多考慮因素,所以我傾向於避免在臨時實現中使用它,除非性能絕對至關重要。

例如,使用transform: translateY方法,您需要考慮元素的z-index 。 默認情況下,向上轉換的元素在 DOM 中的觸發元素之後,因此在向上轉換時出現在它們之前的事物之上。

您還需要考慮要在 DOM 中折疊的內容之後出現多少東西。 如果您不想在佈局中出現大洞,您可能會發現使用 JavaScript 將要移動的所有內容包裝在容器元素中並移動它們會更容易。 可管理,但我們剛剛引入了更多複雜性! 然而,是我在上下移動玩家進/出時採用的方法。 你可以在這裡看到它是如何完成的。

對於更隨意的需求,我傾向於轉換內容的max-height 。 這種方法的性能不如轉換。 原因是瀏覽器在整個過渡過程中對折疊元素的高度進行補間; 這會導致大量的佈局計算對於主機來說並不便宜。

但是,從簡單的角度來看,這種方法獲勝。 遭受上述計算衝擊的回報是 DOM 重流處理了所有內容的位置和幾何形狀。 我們編寫的計算方式很少,而且完成它所需的 JavaScript 相對簡單。

房間裡的大象:細節和總結元素

對 HTML 元素有深入了解的人會知道,有一個以detailssummary元素的形式來解決這個問題的原生 HTML 解決方案。 這是一些示例標記:

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

默認情況下,瀏覽器會在摘要元素旁邊提供一個小三角; 單擊摘要,將顯示摘要下方的內容。

太好了,嘿? 細節甚至支持 JavaScript 中的toggle事件,因此您可以根據它是打開還是關閉來執行不同的操作(如果這種 JavaScript 表達式看起來很奇怪,請不要擔心;我們將在更多內容中介紹詳細介紹):

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

好的,我要停止你的興奮。 細節和摘要元素沒有動畫。 默認情況下不是,目前無法使用額外的 CSS 和 JavaScript 讓它們動畫/過渡打開和關閉。

如果你不知道,我很樂意被證明是錯誤的。

可悲的是,由於我們需要一種打開和關閉的美學,我們必須捲起袖子,用我們可以使用的其他工具做最好、最容易完成的工作。

好吧,隨著令人沮喪的消息消失,讓我們繼續讓這件事發生。

標記模式

基本標記將如下所示:

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

我們有一個外部容器來包裝擴展器,第一個元素是用作動作觸發器的按鈕。 注意到按鈕中的 type 屬性了嗎? 我總是將其包括在內,因為默認情況下,表單內的按鈕將執行提交。 如果您發現自己浪費了幾個小時想知道為什麼您的表單無法正常工作並且表單中包含按鈕; 確保檢查類型屬性!

按鈕之後的下一個元素是內容抽屜本身; 你想要隱藏和展示的一切。

為了讓事物栩栩如生,我們將使用 CSS 自定義屬性、CSS 過渡和一些 JavaScript。

基本邏輯

基本邏輯是這樣的:

  1. 讓頁面加載,測量內容的高度。
  2. 將內容在容器上的高度設置為 CSS 自定義屬性的值。
  3. 通過添加aria-hidden: "true"屬性來立即隱藏內容。 使用aria-hidden可確保輔助技術知道內容也被隱藏。
  4. 連接 CSS,使內容類的max-height是自定義屬性的值。
  5. 按下觸發按鈕將 aria-hidden 屬性從 true 切換為 false,從而在0和自定義屬性中設置的高度之間切換內容的max-height 。 該屬性的過渡提供了視覺風格 - 適應口味!

注意:現在,如果max-height: auto等於內容的高度,這將是一個切換類或屬性的簡單情況。 可悲的是它沒有。 去這裡向 W3C 大喊大叫吧。

讓我們看看這種方法如何在代碼中體現出來。 帶編號的註釋顯示了上面代碼中的等效邏輯步驟。

這是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); }

注意事項

多個抽屜怎麼辦?

當您在頁面上有許多可打開和隱藏的抽屜時,您需要遍歷它們,因為它們的大小可能不同。

為了處理這個問題,我們需要執行querySelectorAll來獲取所有容器,然後為forEach中的每個內容重新運行自定義變量的設置。

那個 setTimeout

在將容器設置為隱藏之前,我有一個持續時間為0setTimeout 。 這可以說是不需要的,但我將其用作“帶和大括號”的方法,以確保首先呈現頁面,以便可以讀取內容的高度。

僅在頁面準備就緒時觸發

如果您還有其他事情要做,您可能會選擇將您的抽屜代碼包裝在一個在頁面加載時初始化的函數中。 例如,假設抽屜函數被封裝在一個名為initDrawers的函數中,我們可以這樣做:

 window.addEventListener("load", initDrawers);

事實上,我們很快就會添加它。

容器上的附加 data-* 屬性

外部容器上有一個數據屬性也會被切換。 這是添加的,以防在抽屜打開/關閉時需要使用觸發器或容器進行更改。 例如,也許我們想要更改某物的顏色或顯示或切換圖標。

自定義屬性的默認值

在 CSS 中的自定義屬性上設置了一個默認值1000px 。 那是值內逗號之後的位: var(--containerHeight, 1000px) 。 這意味著如果--containerHeight以某種方式搞砸了,您仍然應該有一個不錯的過渡。 您顯然可以將其設置為適合您的用例的任何內容。

為什麼不直接使用默認值 100000px?

鑑於max-height: auto不會轉換,您可能想知道為什麼不選擇一個比您需要的值更大的設置高度。 例如,10000000 像素?

這種方法的問題在於它總是從那個高度過渡。 如果您的過渡持續時間設置為 1 秒,則過渡將在一秒鐘內“移動”10000000 像素。 如果您的內容只有 50px 高,您將獲得相當快的打開/關閉效果!

切換的三元運算符

我們已經多次使用三元運算符來切換屬性。 有些人討厭他們,但我和其他人喜歡他們。 一開始它們可能看起來有點奇怪,有點“代碼高爾夫”,但一旦你習慣了語法,我認為它們比標準的 if/else 更容易閱讀。

對於初學者來說,三元運算符是 if/else 的濃縮形式。 它們被寫成首先要檢查的東西,然後是? 分隔如果檢查為真則執行什麼,然後是:以區分如果檢查為假應該運行什麼。

 isThisTrue ? doYesCode() : doNoCode();

我們的屬性切換通過檢查屬性是否設置為"true"來工作,如果是,則將其設置為"false" ,否則將其設置為"true"

頁面調整大小會發生什麼?

如果用戶調整瀏覽器窗口的大小,我們內容的高度很可能會發生變化。 因此,您可能希望在該場景中重新運行設置容器的高度。 現在我們正在考慮這樣的可能性,似乎是重構一些東西的好時機。

我們可以創建一個函數來設置高度,另一個函數來處理交互。 然後在窗口上添加兩個監聽器; 一個用於文檔加載時,如上所述,然後另一個用於偵聽調整大小事件。

多一點 A11Y

通過使用aria-expandedaria-controlsaria-labelledby屬性,可以為可訪問性添加一些額外的考慮。 當抽屜打開/展開時,這將為輔助技術提供更好的指示。 我們將aria-expanded="false"aria-controls="IDofcontent"一起添加到我們的按鈕標記中,其中IDofcontent是我們添加到內容容器的 id 的值。

然後我們使用另一個三元運算符來切換 JavaScript 中單擊時的aria-expanded屬性。

全部一起

隨著頁面加載、多個抽屜、額外的 A11Y 工作和處理調整大小事件,我們的 JavaScript 代碼如下所示:

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

你也可以在這裡在 CodePen 上玩它:

Ben Frain 在 CodePen 上的簡易顯示/隱藏抽屜(倍數)。

Ben Frain 在 CodePen 上的簡易顯示/隱藏抽屜(倍數)。

概括

可以繼續進行一段時間的進一步改進和迎合越來越多的情況,但是為您的內容創建可靠的打開和關閉抽屜的基本機制現在應該觸手可及。 希望您也意識到一些危險。 details元素無法設置動畫, max-height: auto無法達到您的預期,您無法可靠地添加大量 max-height 值並期望所有內容面板按預期打開。

在這裡重申我們的方法:測量容器,將其高度存儲為 CSS 自定義屬性,隱藏內容,然後使用簡單的切換在max-height為 0 和您存儲在自定義屬性中的高度之間切換。

它可能不是絕對性能最佳的方法,但我發現在大多數情況下它是完全足夠的,並且從相對簡單的實施中受益。