制作您自己的扩展和收缩内容面板

已发表: 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 中的触发元素之后,因此在向上转换时出现在它们之前的事物之上。

您还需要考虑要在 D​​OM 中折叠的内容之后出现多少东西。 如果您不想在布局中出现大洞,您可能会发现使用 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 和您存储在自定义属性中的高度之间切换。

它可能不是绝对性能最佳的方法,但我发现在大多数情况下它是完全足够的,并且从相对简单的实施中受益。