我正在阅读 Chris 撰写的 这篇文章,他在文章中谈论了块级链接(您知道,比如将整个卡片元素包裹在锚点中)是一个糟糕的想法。这对于无障碍来说是糟糕的,因为它会影响屏幕阅读器。而且,它对于用户体验来说也是糟糕的,因为它阻止了简单的用户任务,例如选择文本。
但是,也许还有其他因素在起作用。也许问题不在于模式本身,而在于其实现方式。这让我相信,现在是时候写一篇后续文章,看看我们是否可以解决 Chris 指出的某些问题。
在本文中,我将使用“卡片”一词来描述使用块级链接模式的组件。以下是我们的意思。
让我们看看我们希望卡片组件如何工作
- 整个组件应该是可链接且可点击的。
- 它应该能够包含多个链接。
- 内容应该是语义化的,以便辅助技术能够理解它。
- 文本应该是可选择的,就像普通链接一样。
- 诸如右键单击和键盘快捷键之类的东西应该可以使用它
- 它的元素在 Tab 键按下时应该可聚焦。
这是一个很长的清单!而且由于浏览器没有提供任何标准的卡片小部件,所以我们也没有任何标准的指南来构建它。
就像网络上的大多数事物一样,制作卡片组件的方法不止一种。但是,我还没有找到满足我们刚刚介绍的所有要求的东西。在本文中,我们将尝试满足所有这些要求。这正是我们现在要做的!
<a>
中
方法 1:将所有内容包裹在一个 这是制作链接卡片最常见且最简单的方法。获取卡片的 HTML 代码,并将整个内容包裹在锚点标签中。
<a href="/">
<!-- Card markup -->
</a>
以下是结果
- 它是可点击的。
- 它支持右键单击和键盘快捷键。
嗯,不太好。我们仍然无法
- 将另一个链接放入卡片中,因为整个卡片是一个单独的链接
- 与屏幕阅读器一起使用 - 内容不是语义化的,因此辅助技术将从时间戳开始宣布卡片中的所有内容
- 选择文本
这足以让我们 👎 可能不应该使用它。让我们继续学习下一种技术。
方法 2:只链接需要链接的内容
这是一个不错的折衷方案,它牺牲了一点用户体验来提高无障碍性。
使用这种模式,我们实现了大部分目标
- 我们可以随意添加链接。
- 内容是语义化的。
- 我们可以从卡片中选择文本。
- 右键单击和键盘快捷键可以正常使用。
- 在 Tab 键按下时,焦点顺序是正确的。
但是它缺少我们想要在卡片中实现的主要功能:整个卡片应该是可点击的!看起来我们需要尝试其他方法。
方法 3:传统的 ::before 伪元素
在这种方法中,我们添加一个 ::before
或 ::after
元素,将其放置在卡片上方并使用绝对定位,将其拉伸到卡片的整个宽度和高度,使其可点击。
但是现在
- 我们仍然无法添加多个链接,因为任何其他链接都位于伪元素层之下。我们可以尝试将所有文本放在伪元素之上,但卡片链接本身在点击文本时将无法使用。
- 我们仍然无法选择文本。同样,我们可以交换层级,但这样我们又回到了可点击链接问题。
让我们尝试在我们最终的技术中真正勾选所有框。
方法 4:在第二种方法上撒点 JavaScript
让我们从第二种方法开始。回想一下,我们是在哪里链接所有我们想要链接的内容
<article class="card">
<time datetime="2020-03-20">Mar 20, 2020</time>
<h2><a href="https://css-tricks.cn/a-complete-guide-to-calc-in-css/" class="main-link">A Complete Guide to calc() in CSS</a></h2>
<p>
In this guide, let’s cover just about everything there is to know about this very useful function.
</p>
<a class="author-name" href="https://css-tricks.cn/author/chriscoyier/" target="_blank">Chris Coyier</a>
<div class="tags">
<a class="tag" href="https://css-tricks.cn/tag/calc/" >calc</a>
</div>
</article>
那么我们如何让整个卡片可点击呢?我们可以使用 JavaScript 作为渐进增强来实现。我们将首先向卡片添加一个 click
事件监听器,并在触发它时触发主链接的点击事件。
const card = document.querySelector(".card")
const mainLink = document.querySelector('.main-link')
card.addEventListener("click", handleClick)
function handleClick(event) {
mainLink.click();
}
暂时来说,这引入了我们无法选择文本的问题,而我们一直试图解决这个问题。以下技巧:我们将使用相对不为人知的 Web API window.getSelection
。来自 MDN
Window.getSelection()
方法返回一个Selection
对象,该对象表示用户选择的文本范围或光标的当前位置。
尽管此方法返回一个对象,但我们可以使用 toString()
将其转换为字符串。
const isTextSelected = window.getSelection().toString()
只需一行代码,无需使用事件监听器的复杂技巧,我们就知道用户是否选择了文本。让我们在 handleClick
函数中使用它。
const card = document.querySelector(".card")
const mainLink = document.querySelector('.main-link')
card.addEventListener("click", handleClick)
function handleClick(event) {
const isTextSelected = window.getSelection().toString();
if (!isTextSelected) {
mainLink.click();
}
}
这样,在没有选择文本的情况下,主链接可以被点击,而这只需要几行 JavaScript 代码。这满足了我们的要求
- 整个组件是可链接且可点击的。
- 它能够包含多个链接。
- 此内容是语义化的,以便辅助技术能够理解它。
- 文本应该是可选择的,就像普通链接一样。
- 诸如右键单击和键盘快捷键之类的东西应该可以使用它
- 它的元素在 Tab 键按下时应该可聚焦。
我们满足了所有要求,但仍然存在一些问题,例如卡片中的链接和按钮等可点击元素上的双重事件触发。我们可以通过在所有这些元素上添加一个点击事件监听器并停止事件传播来解决此问题。
// You might want to add common class like 'clickable' on all elements and use that for the query selector.
const clickableElements = Array.from(card.querySelectorAll("a"));
clickableElements.forEach((ele) =>
ele.addEventListener("click", (e) => e.stopPropagation())
);
以下是包含我们添加的所有 JavaScript 代码的最终演示
我认为我们成功了!现在您知道如何制作完美的可点击卡片组件了。
其他模式呢?例如,如果卡片包含博客文章的摘要,后面跟着一个“阅读更多”链接?该链接应该放在哪里?它会成为“主”链接吗?图片呢?
对于这些问题以及更多问题,以下是一些关于该主题的进一步阅读材料
- 卡片 由 Heydon Pickering 撰写
- 块级链接、卡片、可点击区域、行等 由 Adrian Roselli 撰写
- 块级链接很痛苦(也许只是一个糟糕的想法) 由 Chris Coyier 撰写
- 卡片 UI 的陷阱 由 Dave Rupert 撰写
您可能需要在 Firefox 中测试一下。在 Firefox 中,点击卡片中的按钮或链接时会打开两个新标签(而不是一个)。例如,当我点击“数学”按钮时,两个不同的页面在新标签中打开。
是的,我目前也可以在 Firefox 中确认这一点。Chrome 也打开了意外的链接。
可能需要测试
event.target
并在点击不是main-link
的链接时进行一些不同的处理。可以使用
.contains()
在点击检查中查看点击的元素是否包含在父节点中。或者,反过来使用
.closest()
从子节点检查是否包含在父节点中。此外,为了使这更加健壮,您可以使用它们来进行测试,以确保链中没有
onclick
、href
或表单元素。我已经找到了解决方案,并将很快更新博客。同时,您可以查看 https://codepen.io/vikas-parashar/pen/qBOwMWj 以了解提议的解决方案。
我尝试使用方法 3 和 CSS 指针事件,得到了一个非 JS 解决方案,可以帮助您完成大部分工作。唯一缺少的是文本选择。 https://codepen.io/dillonbheadley/pen/BaogrGm
使用
display: grid
,您可以向子元素添加 z 索引,而不会影响绝对定位的伪元素。这允许您将内容移动到伪链接之上。然后,对任何非锚点元素使用pointer-events: none
可以防止任何元素阻止卡片上任何位置的点击。我认为方法 3 加上一个小小的调整是最好的解决方案。只需使用 z-index 将卡片内的额外链接置于前列即可。无需使用 JS,用户可能已阻止了 JS,而且这样做也有利于提升性能。
我们可以用 z-index 将链接置于前列,但仍然无法选择文本。现在,这取决于用例,如果你不介意文本不可选,那就没问题。
我们可以通过使用 z-index 将文本置于前列来使文本可选择,但现在有些区域无法点击。
关于使用 JS,这将与方法 2 一样,整个卡片不可点击,但它将是渐进式降级,在我看来,对于某些 JS 禁用时的用例来说,这还可以。
没错 Vikas,我匆忙浏览了你的需求清单,错过了其中的一条,虽然所有内容都需要可点击,但文本也需要可选择。你无法绕过这一点,除非使用你提供的示例中的某些 JS 魔法。通常我会认为“所有内容都需要可点击”和“文本需要可选择”是矛盾的,所以这是一个有趣的解决方法。我真的很努力地想出一个用例。如果用户对链接卡片上的内容特别感兴趣,我认为他们应该点击卡片,并可能看到更多他们感兴趣的信息。毕竟,它的作用是简要介绍他们想要阅读的内容页面。
我当时也在想同样的事情 :)
很酷的演示!现在用不用 JS 的方法来做一下
它将与 JS 禁用时的方法 2 一样工作 :)
然而,方法 2 也有它的缺陷,就像 Chris 提到的那样!最终方法的无 JS 版本可能是一个有趣的挑战。
对于方法 #3,你可以使用
z-index
将其他链接置于伪元素之前,但是仍然无法选择文本。这很好 - 我们在 ABC 也遇到了这个问题。我注意到的一件小事是,除非我将鼠标悬停在
<a>
上,否则我不会在左下角看到 URL 预览。但即使如此,这也比我们的尝试要好!是的,当鼠标悬停在实际链接上时,URL 预览仍然可见,这是最终解决方案带来的折衷之一,但它是首选,因为它提高了可用性(文本选择,整个卡片可点击)和可访问性。
这种技术还缺失了一点:你将鼠标悬停,但你看不到点击后会去哪里(浏览器会在页面左下角显示一个工具提示)。
无法判断有多少用户除了开发人员以外还知道或使用该功能,但无论如何。
是的,这是最终解决方案带来的折衷之一。
方法 3. 是的,我们可以添加更多链接。
只需添加
.author-name, .tag {
position: relative;
z-index: 2;
}
是的,我们可以这样做。
这是一个在 Firefox 中也起作用的修复版本
不错的解决方案,但当链接有子元素(如 Chris 的头像)时,它不起作用,在这种情况下,将再次触发两个事件。我找到了一个修复方法,很快就会更新博客。同时,你可以查看这个笔 https://codepen.io/vikas-parashar/pen/qBOwMWj,了解提议的修复方法。
你不能简单地在卡片内使用绝对定位的链接吗?在其中放一个视觉隐藏的 span 来保持可访问性。由于链接是绝对定位的,因此卡片中的其他链接仍然可以工作。使用 z-index 将它们置于前列。
在我看来,这似乎是最好的解决方案,因为你根本不需要与 JS 作斗争。我在我参与过的网站上遇到了基于 JS 的解决方案来解决这个问题,它总是很麻烦,因为 JS 使在需要进行编辑等操作时更难调试。
这并没有考虑文本选择。这对你来说可能完全没问题,但本文的目的是为它制定严格的要求,使其成为“完美”的(或者尽可能接近),并试图实现这一目标。
很棒。上次我需要构建类似的东西时,我也采用了这种方法。
但是还有两件事是缺失的,我认为它们比单独使用方法 2 更糟糕的 UX
– url 预览(在其他评论中提到)
– 点击整个卡片时在新标签页中打开。我认为对用户来说,为什么他/她有时会得到正确的上下文菜单,有时却不会,这一点并不清楚。
我对“让我们链接整个卡片”这件事持怀疑态度。就我个人而言,我更喜欢方法 2。但你可以花几个小时讨论这件事;)
不过,文章不错!
Andy Bell 在这篇文章中探讨了一些相同的内容:https://hankchizljaw.com/wrote/create-a-semantic-breakout-button-to-make-an-entire-element-clickable/
但是,他做出了一个假设,这似乎是一个很好的假设,那就是卡片组件中只需要有一个链接。
我不太理解这种对链接上的可选择文本的渴望。
当你拥有常规链接时,你无法在链接内部开始选择时选择它的一部分 - 你必须在链接之前或之后开始选择才能选择它。在这里也是这样工作的。
这是一个经典的前端难题,但在我看来,在一个应该完全可点击的卡片内拥有一个“第二个”链接是一个奇怪的 UX 需求。
我明白为什么 - 特别是在这种情况下是作者的姓名 - 它可能有用,但我认为这基本上可以归结为,如果我们需要在卡片中使用二级链接,则整个卡片不应该可点击。
如果卡片是一个链接,使用锚点将其包装起来似乎很有道理,否则,为每个组件(标题、图像、作者)标记内部链接似乎更有意义。
你可以使用 Apple 网站的解决方案。它是一个带有类 .unit-wrapper 的 div,以及一个带有 .unit-link 类的主要锚点。因此,内部的所有内容(除了锚点)都将获得属性“pointer-events:none”。
// 将一个区域变成一个链接块
// 同时保持对内部其他链接的交互
// 在主要锚点中添加类 .unit-link
a.unit-link {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 3;
}
// 在父元素中添加类 .unit-wrapper
<
div>
.unit-wrapper {
position: relative;
overflow: hidden;
}
.unit-wrapper > div {
position: relative;
z-index: 4;
pointer-events: none;
}
.unit-wrapper a:not(.unit-link) {
z-index: 4;
pointer-events: all;
}
很棒。上次我需要构建类似的东西时,我也采用了这种方法。
但是还有两件事是缺失的,我认为它们比单独使用方法 2 更糟糕的 UX
– url 预览(在其他评论中提到)
– 点击整个卡片时在新标签页中打开。我认为对用户来说,为什么他/她有时会得到正确的上下文菜单,有时却不会,这一点并不清楚。
我对“让我们链接整个卡片”这件事持怀疑态度。就我个人而言,我更喜欢方法 2。但你可以花几个小时讨论这件事;)
不过,文章不错!
并且这是否针对多个卡片进行了测试?只有一批卡片中的第一个卡片按预期工作。其他卡片被忽略了 - 这意味着整个卡片不可点击。
我也遇到了这个问题。它需要适用于多个卡片
嗨 Vikas,
我真的很喜欢你的文章,你的解决方案听起来是一个适用于大多数用例的良好中间点。
我尝试使用这个(封装非常适合这种情况)来制作一个 web 组件。
https://webcomponents.dev/preview/dyflJofod72crJWUDyyo
它基本上替换了顶层容器,你可以使用
main-link
属性设置用于获取主要链接的选择器。代码将类似于以下内容
由于所有内容都在一个插槽中,因此你可以像往常一样设置元素的样式。
这仍然是一个非常基础的实现,如果你不介意,我可以改进一下(主要是文档和无障碍方面),并发布到 npm 上(当然我会以你希望的任何方式对你和你的文章进行署名)。
可以,请便!也许可以稍微润色一下,并包含我在帖子中链接的其他参考链接。如果你方便,可以在署名中链接这篇文章和我的推特(@vicode_in)。
当然,我会把这里引用的文章以及这篇文章都添加进去。
我会在推特上通知你,一旦我将组件完善一些。
你如何让它适用于多个项目?正如你所看到的,只有第一篇文章是完全可点击的。
我也有同样的问题。
@Leone,我最终使用了 Sara Soueidan 的实现,效果很好,而且不需要 JS。 https://css-tricks.cn/breakout-buttons/
感谢告知!
我尝试了 Sara Soueidan 的实现,但不知何故,我在移动设备上无法使其正常工作。当我点击卡片时,按钮上的悬停效果(我没有点击它)被触发了,但页面没有跳转到 URL。
因此,我不得不使用一些 jQuery 来修复我们最初的问题,即只有第一张卡片有效。
@Leone – 你在这条评论中的解决方案 https://css-tricks.cn/block-links-the-search-for-a-perfect-solution/#comment-1769257 对我非常有效。
谢谢
我不太喜欢让整个卡片都可点击,尤其是在卡片中使用多个链接的情况下。作为用户,你会期望点击“Chris Coyier”链接会跳转到一个链接,然后点击它旁边一点的地方就会跳转到另一个链接吗?