CSS 网格中的可扩展部分

Avatar of Kev Bonett
Kev Bonett

DigitalOcean 提供适合您旅程每个阶段的云产品。立即开始使用 价值 200 美元的免费积分!

我喜欢 CSS Grid。我喜欢它如何仅用几行代码,我们就能实现完全响应式的网格布局,通常无需任何媒体查询。我对使用 CSS Grid 来创建有趣的布局非常满意,同时保持 HTML 标记简洁干净。

但最近,我遇到了一个独特的 UI 难题需要解决。本质上,任何给定的网格单元格都可以有一个按钮,该按钮可以打开另一个更大的区域,该区域也是网格的一部分。但这个新的更大的网格单元格需要

  1. 正好位于打开它的单元格下方,并且
  2. 宽度全屏。

事实证明,它有一个很好的解决方案,并且本着 CSS Grid 本身的精神,它只涉及几行代码。在本文中,我将结合三个单行 CSS Grid“技巧”来解决这个问题。完全不需要 JavaScript。

我需要解决的实际问题的解释

这是一个我需要执行的最小 UI 示例

这是我们在 Storybook 组件库中渲染的实际产品卡网格

A grid of product cards in a three by two layout. Each card has a placeholder gray image, product name, descriptions, price, and small text.

每个产品卡都需要添加一个新的“快速查看”按钮,以便在点击时,它会

  • 动态地“注入”一张新的全屏卡(包含更详细的产品信息)到点击的产品卡下方,
  • 而不影响现有的卡网格(即保留 DOM 源代码顺序以及浏览器中渲染的卡的视觉顺序),并且
  • 仍然完全响应。

嗯……这在我们当前的 CSS Grid 实现中可能吗?

我肯定需要求助于 JavaScript 来重新计算卡的位置并移动它们,尤其是在浏览器调整大小的时候?对吗?

谷歌帮不了我。我找不到任何能帮助我的东西。即使搜索“快速查看”的实现也只产生了使用模态或覆盖层来渲染注入卡的示例。毕竟,模态通常是这种情况的唯一选择,因为它让用户专注于新内容,而无需打扰页面的其余部分。

我睡了一晚上,最后通过结合 CSS Grid 中一些最强大、最有用的功能,找到了一个可行的解决方案。

CSS Grid 技巧 #1

我已经在我们的默认网格系统中使用了第一个技巧,并且产品卡网格是该方法的一个特定示例。这是一些(简化的)代码

.grid {
  display: grid;
  gap: 1rem;
  grid-template-columns: repeat(auto-fit, 20rem);
}

这段代码中的“秘密酱料”grid-template-columns: repeat(auto-fit, 20rem);,它为我们提供了一个网格,其中包含在可用空间中自动排列的列(在此示例中为20rem宽),当没有足够的空间时会换行。

好奇auto-fitauto-fill的区别吗?Sara Soueidan 已经写了 关于如何工作的精彩解释。Sara 还解释了如何将minmax()合并以使列宽“灵活”,但是为了本文的目的,为了简单起见,我想要定义固定列宽。

CSS Grid 技巧 #2

接下来,我必须为网格中新的全屏卡提供空间

.fullwidth {
  grid-column: 1 / -1;
}

这段代码有效,因为技巧 #1 中的grid-template-columns创建了一个“显式”网格,因此可以为.fullwidth卡定义起始列和结束列,其中1 / -1表示“从第 1 列开始,跨越所有列直到最后一列。”

太棒了。一个注入网格的全屏卡。但是……现在全屏卡上方有间隙。

Two rows of four rectangles. All of the rectangles are light gray and numbered, except one that has a wheat-colored background and another box beneath it containing text, and taking up the full container width.

CSS Grid 技巧 #3

填补间隙——我之前用过一种仿砌体的方法

.grid {
  grid-auto-flow: dense;
}

就是这样!需要的布局已实现。

grid-auto-flow 属性控制 CSS Grid 自动放置算法的工作方式。在这种情况下,dense 填充算法尝试填充网格中较早的空洞。

  • 我们所有的网格列都具有相同宽度。如果列宽是灵活的,例如使用minmax(20rem, 1f),密集填充也能工作。
  • 我们每行中的所有网格“单元格”都具有相同高度。这是 CSS Grid 的默认行为。网格容器隐式具有align-items: stretch,导致单元格占据可用行高度的 100%。

所有这些的结果是,我们网格中的空洞被填补了——最棒的是,原始源代码顺序在渲染输出中得以保留。从可访问性的角度来看,这一点很重要。

请参阅 MDN 以详细了解 CSS Grid 自动放置的工作方式

完整的解决方案

这三个技巧组合在一起提供了一个简单的布局解决方案,它只需要很少的 CSS。不需要媒体查询,也不需要 JavaScript。

但是……我们仍然需要 JavaScript 吗?

是的,我们需要。但不是为了任何布局计算。它纯粹是用于管理点击事件、焦点状态、注入卡显示等方面的功能性

出于原型演示的目的,全屏卡已在 HTML 中以硬编码形式,位于 DOM 中的正确位置,并且 JavaScript 只需切换其显示属性。

但是,在生产环境中,注入的卡可能会使用 JavaScript 获取并在正确位置放置。像电子商务网站上的产品这样的网格布局往往具有非常重的 DOM,我们希望避免通过添加大量额外的“隐藏”内容来进一步无谓地增加页面大小。

快速查看应该被视为一种渐进增强,因此如果 JavaScript 无法加载,用户只会跳转到相应的商品详情页面。

可访问性考虑

我对使用正确的语义 HTML 标记、在绝对必要时添加aria-属性以及确保 UI 仅使用键盘以及在屏幕阅读器中都能正常工作充满热情。

因此,以下是对使此模式尽可能易于访问的考虑因素的概述

  • 产品卡网格使用<ul><li>结构,因为我们显示的是产品列表。因此,辅助技术(例如屏幕阅读器)将了解卡之间存在关系,并且用户将被告知列表中有多少个项目。
  • 产品卡本身是<article>元素,带有适当的标题等。
  • 当注入.fullwidth卡时,卡的 HTML 源代码顺序得以保留,从而为注入的内容提供了一个良好的自然选项卡顺序,并从注入的内容回到下一张卡。
  • 整个卡网格都包装在一个aria-live区域中,以便将 DOM 更改通知屏幕阅读器.
  • 焦点管理确保注入的卡接收键盘焦点,并且在关闭卡时,键盘焦点将返回到最初触发卡可见性的按钮。

虽然原型中没有展示,但这些额外的增强功能可以添加到任何生产实现中

  • 确保注入的卡在获得焦点时具有适当的标签。这可以像在内容中将标题作为第一个元素一样简单。
  • ESC键绑定到关闭注入的卡。
  • 滚动浏览器窗口,使注入的卡在视窗内完全可见。

总结

所以,你觉得怎么样?

这可能是一个很好的替代方案,可以替代模态窗口,让我们在想要显示额外内容时使用,但不会在过程中劫持整个视窗。这在其他情况下也可能很有趣——想想图片网格中的图片说明、辅助文本等等。它甚至可能替代某些情况下我们通常会使用 `<details>`/`<summary>` 的情况(正如我们所知,它们只在 特定环境 中使用最佳)。

无论如何,我对您如何使用它,甚至如何以不同方式处理它很感兴趣。请在评论中告诉我!

更新

首先,我真的很高兴这篇文章对其他前端开发人员很有帮助。我知道我不会是唯一遇到类似困境的人。

其次,在收到一些建设性反馈后,我在上面的一些具体的无障碍考虑方面添加了删除线,并在我的 CodePen 演示中添加了以下更改

  • 卡片网格不需要包装在 `aria-live` 区域中。相反,我让快速查看打开和关闭按钮的行为像“切换”按钮,并使用相应的 `aria-expanded` 和 `aria-controls` 属性。我确实将这种模式用于披露小部件(显示/隐藏、选项卡、手风琴),但在这种情况下,我设想的是更类似于模态界面,尽管是内联而不是覆盖。(感谢 Adrian 提出的建议!)
  • 我不再以编程方式将焦点放在注入的卡片上。相反,我只是添加了一个 `tabindex="0"`,这样键盘用户可以选择是否移动到注入的卡片上,或者他们也可以再次关闭“切换”按钮。
  • 我仍然认为,对于产品卡片列表,使用 `<ul><li>` 结构是一个合适的方法。提供的语义表明卡片之间存在明确的关系。