给力星

Web Developer

HTML属性tabindex_控制tab切换顺序&&让元素可获得焦点

在网页中可以使用 Tab 键在链接、输入框之间切换(Shift + Tab 是逆序切换),这在纯键盘操作、填写表格等场景下非常有用。tabindex 是 HTML 的一个全局属性,跟 Tab 键的这个行为密切相关,主要有两个用处:

  1. 使用 Tab 键时,设定可聚焦元素的切换顺序;
  2. 使元素可聚焦,例如 div 元素默认无法获得焦点,但设置 tabindex 属性后就可以。

本文主要内容如下:

  • 描述我在 tabindex 方面踩过的一个坑(无法触发 focus / onkeyup / onkeydown)
  • 介绍 focusable 元素、tabindex 属性的取值和效果
  • 介绍网页中使用 Tab 键切换存在的一些问题

我踩过的坑

8月份的时候我做了大象 Web 的一个需求“搜索suggest优化,上下键的时候不离开输入框”,如下图所示,就是在搜索时,让光标仍位于输入框中,但是可以通过上下键来选择搜索项,以增强用户体验,只用键盘就能完成搜索、选人、更改搜索词等操作。

搜索时上下键的时候不离开输入框搜索时上下键的时候不离开输入框

实现上难度不大,思路是对 input 监听 onKeyUp 事件,对上下键做出响应,通过给搜索项增加、删除 class 来表示选中、取消选中状态。比较值得注意的一点是,应该对上下键取消默认动作(否则按向上键时, 光标会跑到最前面, 要按退格键删除搜索关键词还得将光标移动到最后面)。

没多久,又提了一个需求,转发的联系人列表也应该支持上下键选择,这跟上面提到的需求大同小异,唯一的区别就是监听 onKeyUp 的地方从 input 换成 div/li 而已,如下图所示:

搜索时上下键的时候不离开输入框搜索时上下键的时候不离开输入框

我就自觉接下需求了,寻思着10分钟就妥妥搞定了,然而花了1个小时才搞定,满满的都是泪啊… 期间百思不得其解的一个问题是,为什么 li 元素始终无法触发 onKeyUp ?

问题描述

我所遇到的问题可以描述为:有如下代码,为何 onfocus / onkeyup 不会触发?(如果把div换成input则可以正常触发)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
</head>
<body>
    <div id="div">div</div>
    <script type="text/javascript">
        var element = document.getElementById('div');
        element.onfocus = function() {
            console.log('focus');
        }
        element.onkeyup = function() {
            console.log('keyup');
        }
        element.focus();
        element.click();
    </script>
</body>
</html>

不得已去翻了一下旧代码(旧代码的 suggest 也支持上下键搜索,但是是在 li 上触发的),才发现旧代码中的 li 元素,都设置了一个属性 tabindex="-1",原来问题的根源就在于没有设置这个属性,onkeyup 之所以无法触发,就是由于 li 元素默认无法获得焦点,因此也就无法对键盘输入做出响应。而我做的前一个需求,是在 input 元素中监听键盘输入的,所以没有这个问题。

Focusable

HTML 元素中,并不是所有元素都可以获得焦点,有如下元素可以获得焦点: a, area, button, input, object, select, textarea,这些元素就是 focusable 元素。

而除了上述元素之外,还可以通过如下方式使元素变为 focusable 元素:

  • 设置了 tabindex 属性的元素
  • 设置了 contenteditable="true" 属性的元素

要想一个 focusable 元素获得焦点,则有三种方式:

  • 使用鼠标点击元素
  • 调用元素的 focus() 方法
  • 通过 Tab 键进行却换

tabindex

tabindex 的一大作用就是让元素可以获得焦点,从而触发 focus 状态。给联系人列表的元素 li 都加上该属性后,我遇到的问题也就解决了。

tabindex 的另一个作用,就是设定 Tab 键切换的顺序。tabindex 有如下几个值:

  • 1 ~ 32767: 通过 Tab 键切换时,切换顺序将遵循数字的大小(从小到大),数字相同则按出现的先后次序进行切换
  • 0: 默认值,当 tabindex > 0 的元素都切换之后,才会切换到 tabindex = 0 的元素,并且按出现的先后次序进行切换
  • 负数(通常为 -1): 通过 Tab 键无法切换到该元素,但鼠标点击可以获取焦点

例如,有如下代码片段:

<body>
    <a tabindex="1">a01</a>
    <a tabindex="3">a02</a>
    <div>
        <ul>
            <li>text01 - <a tabindex="1">a03</a></li>
            <li>text03 - <a tabindex="-1">a04</a></li>
            <li>text03 - <a tabindex="0">a05</a></li>
            <li>text03 - <a tabindex="0">a06</a></li>
        </ul>
    </div>
</body>

切换的顺序将是: a01 -> a03 -> a02 -> a05 -> l06,a04 将不会获得焦点。

tab键切换的坑

通过设定 tabindex 来控制切换顺序,在一些场景下下非常好用,例如填写表单时,可以准确的控制填写的顺序,也可以跳过无关的元素。

然而,利用 Tab 进行切换在实际使用中难免会出现一些坑。

Safari

Safari 默认情况下,只支持通过 Tab 键对 input、textarea 等输入型元素、设置了 tabindex 属性的 div 等元素进行切换的,却无法切换到 a 元素… 需要用户更改 Safari 的偏好设置,勾选”按下 Tab 键以高亮显示网页上的每一项”才行,或者对 Tab 键进行监听,使用 js 进行处理。

Stackoverflow 上有相关的讨论: Safari ignoring tabindex

Skip Link

介绍”Skip Link”前先对锚点链接做下介绍。对于锚点链接 <a name="content"></a><a id="content"></a>,前端er是再清楚不过了,通过改变url的hash值(在后面加上#id),可以实现页面内容的精准指向。

但关于锚点链接,还有如下知识:

  • <a name="content"></a><a id="content"></a> 在各个浏览器中效果一样;
  • 对于 input、textarea,也可以将其设置为锚点,只要定义 id 属性即可,访问 #id 时会自动聚焦;
  • IE中可以识别 <input id="email" /><input name="email" /> 的锚点,但 Chrome/Safari 只能识别 <input id="email" /> 的锚点,因此,要做锚点的话,最好使用 id 属性
  • 有定义 id 属性的 div 等元素也可以作为锚点。

我的博客 用到了 _s 主题,该主题出于增强 accessibility 的考虑,在 navigation 前面加了一个 <a class="skip-link" href="#content">Skip to content</a>,这样,使用键盘的用户,在聚焦到这个锚点时,可以直接跳到内容部分。

通常 navigation 都位于内容区的前面,并且链接数不少,因此得按 N 次 Tab 键才能切换到内容部分。通过 Skip Link,用户只需要 1 次 Tab + Enter 便可做到,这样的设计无疑增强了键盘党的使用体验。

但在早期版本的 Chrome 中,存在一个 bug:Skip Link 无法正常工作,表现在虽然可以正确定位到 #content 的位置,但是定位后,按 Tab 键还是会从页面中第一个 focusable 元素开始切换,达不到跳过 navigation 区域的效果。正确的表现应该是定位后将 #content 后的第一个 focusable 元素作为 Tab 键切换的起点。

Javascript 红宝书的作者 Nicholas C. Zakas 给出的解决方法如下(_s 主题中的”skip-link-focus-fix.js”用到了该解决方法):

window.addEventListener("hashchange", function(event) {

    var element = document.getElementById(location.hash.substring(1));

    if (element) {

        if (!/^(?:a|select|input|button|textarea)$/i.test(element.tagName)) {
            element.tabIndex = -1;
        }

        element.focus();
    }

}, false); 

这段代码的作用就是给定位的锚点元素加上 tabindex 元素,让其可以获得焦点,然后对该元素进行聚焦。这样,之后的 Tab 键操作就会以该元素作为切换起点了。

Ps: 该 bug 在较新版本的 Chrome 中已经修复了。

总结

  • tabindex 属性可使非 focusable 元素获得焦点,进而可以触发 focus 状态、触发执行 onkeyup / onkeydown 事件;
  • tabindex 规定了 Tab 键切换的顺序,数字大于1的按从小到大的顺序优先切换、数字相同的按出现次序切换,数字为-1的则无法切换到;
  • 通过 Tab 键对元素进行切换的表现在不同的浏览器下有差异。

参考资料

发表评论

电子邮件地址不会被公开。