ChrAlpha's Blog

纯 CSS 下拉菜单与 focus 二三事

2022-01-30·笔记本

「Cards」主题的导航栏做下拉菜单已经是好几个版本之前(v0.5)的事情了,由于特殊一些特殊癖好一直没有引入 JavaScript 实现这个操作,而是采用了纯 CSS 的 :focus 伪类相应点击事件。

就这么用了整整一年半,终于有小伙伴发邮件反馈,下来菜单在 iOS 上只能点开却没法收回,具体来说就是能 focus 但是点击其他地方没法解除。

当初也只是东拼西凑(抄 Spectre CSS)做出来的下拉菜单,没仔细研究。今天就这个机会尝试挖掘一番。

focus 伪类

focus 伪类 :focus 表示被点击、触摸或 tab 选中的元素,笼统地说就是「获得焦点」的元素。

当初实现这个需求的时候同样考虑过采用 :hover 或者 :active

hover 算是比较熟悉的了,在 PC 上鼠标悬停于此时 :hover 伪类生效,比如 链接 的样式正是采用 :hover 实现鼠标经过时反馈,以提示用户这是可点击的。在移动端上稍微有些不同,毕竟所有控制——无论单击、长按抑或划动——都由接触开始,也没有鼠标的「悬停」逻辑,为了方便判定,移动端上若想激活 :hover 也是单击(触摸)。导航栏之所以不直接显示而是放进下拉菜单,也是为了在移动端等小尺寸设备中显示得优雅一点,因此这个单击判定其实是优势。不过还是有点问题,比如一台 iPad mini 这种中尺寸设备,竖屏 + 鼠标情况下,但凡鼠标掠过就会调出下拉菜单;或者即便是 PC,把窗口缩小也同样是掠过频繁调出下拉菜单……毕竟是为了小尺寸设备设计的而其中并非全是触摸设备,还是补充个「点击」的判定更为周到。至此,hover 被淘汰。

active 这里便简单许多了,毕竟一开始就被刷下去。相较于 hover 是悬停、focus 是获得焦点,active 是「正在交互」——从按下鼠标左键(主要按键)到松开、或者是从触摸到松开,一松开便解除 active 状态,而下拉菜单显然是要按下后保持住展开状态的,虽然 active 在移动端的响应是三个中和桌面端最贴合的,但并不适用于此场景。active 被淘汰。

桌面端移动端
focus持续到失去焦点松开时进入,持续到失去焦点
hover悬停期间按下时进入,持续到失去焦点
active单击按下期间触摸按下期间

综合来看,focus 是最合适的。不过后面还有坑等着呢。

tabindex 选中

默认不显示,:focus 激活时显示,很快码出几行代码。

.dropdown-menus {
  display: none;
}
.dropdown-icon:focus + .dropdown-menus {
  display: block;
}

一运行测试,立马傻眼——这怎么压根没反应啊?到回头仔细阅览 Spectre CSS 的描述,看到这样一句话。

You also need to add tabindex to make the buttons focusable.

究竟何为 tabindex,当时并没有深究,只知道加上后确实点击有反应了。当然出问题后又仔细翻了翻这方面的内容,就不按照平时我喜欢的讲故事般的时间顺序整理,直接放上来。

这里有两个问题:

  1. 为什么要加 tabindex
  2. 为什么值要填 0

Spectre 解释是这样让按钮可获得焦点,事实上,并非所有元素默认支持聚焦。本来 <a> 是可以获得焦点的,只不过要 href 属性。而 <a> 标签在这里只是作为一个按钮使用,并不想被点击后有任何跳转,所以不会给它带上 href 属性,自然也就不可聚焦。稍微查询就会发现,tabindex 是个全局属性,也就是说可以给几乎任何元素加上以使其可以聚焦,如 <div><p> 等,当然也包含不带 href 属性的 <a>。所以无论原先元素是否可以聚焦,加上 tabindex 总是可以聚焦的,从而发挥按钮的功能,Spectre 的解释大概就是旨在这保底上了。

至于为什么要填 0,这还要从 tabindex 另外两个作用说起。上面是 tabindex 决定元素是否可以被聚焦,其实 tabindex 还可以决定元素能如何被聚焦以及被聚焦的顺序,而这些就在赋给 tabindex 的值控制的范畴。先说决定如何被聚焦,这里分为负值(一般是 -1)与非负值,若为负值则该元素 不可以被键盘 Tab 聚焦、但可以被 JavaScript 或者鼠标单击聚焦,一般希望被 JavaScript 接管的设为此值,以降低其他操作干扰。再说决定聚焦顺序,非负值也分为两部分,0 与正值,若为 0 则该元素可以被键盘 Tab 聚焦或 JavaScript、点击聚焦且按照默认顺序聚焦;若为正值则按照数值从小到大的顺序聚焦且 优先于所有 tabindex 值为 0 的

iOS Safari 出错

是的,iOS Safari 上的这个错误是促成本文最主要的缘故。

首先,第一个坑——iOS Sasfari 浏览器中点击 <a>button 的时候是不会有 :focus 状态的,倒是原本在 PC 上表示悬停的 :hover 可以在点击(触摸)后被激活。若希望 <a> 在点击后保持 :focus 状态,则需要额外声明 tabindex 参数(不论是否有 href 参数)。碰巧的是,前面我们刚好设置了 tabindex,这个坑算是无意间跳过去了。

其次,当一个元素被聚焦时,点击一般的空白处无法使它失焦。这个问题很迷,在 iOS Safari 上 100% 复现而在 iOS Chrome 上完全无法复现。上面表述中的「一般」表示这其实是有例外的,比如点击其他默认可聚焦的元素(如 <a>button 等等)就会使新聚焦的元素顶替原聚焦的元素让先前的元素失焦。因此,「Cards」主题在 iOS Safari 上会发生点击下拉菜单可以展开、但是点击空白地方无法收回的问题,除非之后点击的是链接之类的。

你可以对比尚未更新的 Theme Cards Demo 与本博客的下拉菜单,以实践认识上述内容。

至于如何修复,方才说到只要让其它元素聚焦就可以顶替掉这个聚焦的元素使其失焦,那么我们只需要让一个层级足够高的元素可以被聚焦——设置 tabindex 参数(最好为 -1,原因自己往上翻)。这样一来,点击「空白」位置就可以使下拉菜单正常失焦了。

<div class="app" tabindex="-1">
  // ...
  <a class="dropdown-icon" tabindex="0">
    // ...
  </a>
</div>

至此,我们可以更新下上面的表格。

PCiOSAndroid
focus持续到失去焦点默认不可用松开时进入,持续到失去焦点
hover悬停期间按下时进入,持续到失去焦点按下时进入,持续到失去焦点
active单击按下期间默认不可用触摸按下期间

本来还想揣摩一下 Apple 为此的意图,苦思冥想未尝一通,罢了罢了。我又双叒叕想到在 iPhone 13 Pro 初体验 中用到苏联笑话:

苏联大清洗运动是苏维埃政府雇佣大量顶级斯大林经过海量计算得到的结果,他们比我懂,更比你懂。

还真就总能精辟诠释 Apple。害,苏联笑话无愧世界文化瑰宝……

感谢那位发邮件提醒我的热心网友。我甚至最初在没有对照印证的情况下,就无端将其归结为没理解下拉菜单用意——点击其它位置收回而非二次点击按钮。尽管这确实是最常遇到的询问。

事不目见耳闻,而臆断其有无,可乎?

不可。共勉。


参考链接:

纯 CSS 下拉菜单与 focus 二三事
本文作者
ChrAlpha
发布日期
2022-01-30
更新日期
2022-01-30
转载或引用本文时请遵守 CC BY-NC-SA 4.0 许可协议,注明出处、不得用于商业用途!