纯 CSS 下拉菜单与 focus 二三事
给「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
,当时并没有深究,只知道加上后确实点击有反应了。当然出问题后又仔细翻了翻这方面的内容,就不按照平时我喜欢的讲故事般的时间顺序整理,直接放上来。
这里有两个问题:
- 为什么要加
tabindex
? - 为什么值要填
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>
至此,我们可以更新下上面的表格。
PC | iOS | Android | |
---|---|---|---|
focus | 持续到失去焦点 | 默认不可用 | 松开时进入,持续到失去焦点 |
hover | 悬停期间 | 按下时进入,持续到失去焦点 | 按下时进入,持续到失去焦点 |
active | 单击按下期间 | 默认不可用 | 触摸按下期间 |
本来还想揣摩一下 Apple 为此的意图,苦思冥想未尝一通,罢了罢了。我又双叒叕想到在 iPhone 13 Pro 初体验 中用到苏联笑话:
苏联大清洗运动是苏维埃政府雇佣大量顶级斯大林经过海量计算得到的结果,他们比我懂,更比你懂。
还真就总能精辟诠释 Apple。害,苏联笑话无愧世界文化瑰宝……
后
感谢那位发邮件提醒我的热心网友。我甚至最初在没有对照印证的情况下,就无端将其归结为没理解下拉菜单用意——点击其它位置收回而非二次点击按钮。尽管这确实是最常遇到的询问。
事不目见耳闻,而臆断其有无,可乎?
不可。共勉。
参考链接:
- 细说iOS Safari下focus的行为 - 张鑫旭-鑫空间
- 点击态样式:focus, active, hover 的区别与兼容性 - Harttle
tabindex
- MDN Web Docs