Mac 上浏览器 a11y 陷阱
1 序
1.1 a11y 何物
a11y,即 accessibility,专门指代计算机软件领域的用户可访问性。计算机软件需要在设计和实现上让残障人士,尤其是视力有问题的人,也能够明确地知道自己目前所处的位置,以及可以进行什么样的操作。
其实,对于正常人而言,a11y 也很重要,比如有些人喜欢使用键盘做一些原本需要用鼠标的操作,因为那样快,而且看起来会很牛逼的样子。
a11y 是个很大很复杂的话题,这里只讨论网页中最最基础的 a11y 需求——使用键盘进行导航和操作。
1.1 tabIndex
当你在网页中按「TAB」键,会发现网页上某个元素获得了焦点;这时如果你按住「TAB」不放,这个焦点会依次往后传递给下一个能接受焦点的元素,那么这些依次经过的元素就组成一个「TAB」流(「TAB」 flow)。
HTML 提供了一个叫作 tabIndex
的属性告诉浏览器,这个元素是否可以获得焦点,在获得焦点的时候又是如何一个顺序。一般来说,所有的表单元素、按钮和链接(带 href
的 a
元素)都_应该_可以获得焦点的;而像 <form>
、<div>
、<span>
、<p>
、<ul>
、<li>
则无法获得焦点。
The following elements support the
tabindex
attribute: A, AREA, BUTTON, INPUT, OBJECT, SELECT, and TEXTAREA.
tabIndex
的取值会产生如下效果:
- 负数(一般用
-1
):表示元素不会出现在「TAB」流,但点击后可获得焦点; 0
:元素跟原生的表单元素一样,既能获得焦点,又会出现在「TAB」流中;- 正数:用于调整 tab 的优先级,越小的优先级越高。
也就是说,在「TAB」流中的元素一定是可以获得焦点的,而能获得焦点的元素则不一定出现在「TAB」流中。
注意:在 HTML(XHTML 除外)中的 `tabIndex` 属性,大小写是无关的,可以写成任意鬼样子,如 `tAbInDEx`;但在 JavaScript 中,则必须使用 `tabIndex`。浏览器为每个元素设了默认的 `tabIndex`,可想而知,表单元素、按钮及连接的 `tabIndex` 值默认为 `0`;有趣的是,当你去看那些不能获取焦点的元素的时候,会发现 `tabIndex` 值居然是 `-1`!莫慌,用 `hasOwnProperty` 检查一下就会发现这个 `-1` 不是它本身所有的,所以要真正让 `tabIndex` 生效,必须用 HTML 或者 JS 的方式,让它真正获得自己 own 的 `tabIndex` 属性。
参考:Making Elements Focusable With TabIndex
2. 网页的键盘操作
一般用户在访问网页的时候会同时使用键盘和鼠标,而盲人用户一般就只能使用键盘的某些键(「TAB」,「SPACE」,「ENTER」以及方向键)来进行导航。
「TAB」的作用是让某个元素获得焦点;「SPACE」和「ENTER」在有焦点元素的前提下,可以代替鼠标的点击;方向键的作用是在一个局限的范围内进行导航,比如在 select
内部选择 option
。
如果要让用户只用键盘,是不是仅仅使用浏览器自带的一些 a11y 的解决方案就行了呢?
假设用户需要访问一个表单(<form>
),表单(原生的)元素包括有:
input
(:text
、:hidden
、:radio
、:checkbox
、:submit
、:reset
、:button
、:number
、:date
、:url
、:range
)textarea
select
button
当然,表单里面可能有一些链接(<a>
元素);也有一些自定义的 JS 控件,可能需要用户点击操作。
接下来我们通过几个案例来分析(若不做特别说明,浏览器所处的平台为_Mac OSX 10.10_)。
2.1 「TAB」导航
键盘访问页面元素是一切的开端,我们使用「TAB」选择下一个或上一个元素。
2.1.1 案例
测试 bin:http://jsbin.com/vatuv。
这里包含了典型的表单元素,以及一个 tabIndex
设置为 0
的 <p>
(权当它是一个 JS 控件)。由于有些浏览器对某些元素取消了获焦时的 outline
,例子中默认加了 outline
,可以用最上面的复选框来取消。
2.1.2 发现问题
- Safari 7 跳过的元素最多,
input:range
、input:radio
、input:checkbox
、a
、button
,但却可以聚焦到tabIndex
为0
的<p>
。 - Firefox 33 跳过
a
(Window 的 Firefox 不会) - Chrome 39 OK
- Opera 25 跳过
a
- IE 10(Win8) OK
我原以为这是 Safari 的一个很严重的 bug,Google 之后发现,这其实是它的一个 feature:Mac Safari 默认「TAB」到「重要的」表单元素,如果想「TAB」到其它元素,需要按 「Option」 + 「TAB」。Window 平台没有 「Option」,所以 Windows 上的 Safari 表现完全不一样,其结果跟 Mac 上的 Firefox 一样。但对于 HTML 标准来说,这的确是个 bug。
本节参考:
- Enabling keyboard navigation in Mac OS X Web browsers
- Results of browser testing for when elements take the focus(但似乎跟我在Mac上的测试结果有些出入)
2.1.3 解决问题
Safari 通过组合键「Option」 + 「TAB」可以「TAB」到所有的元素,但 a
元素在 Firefox 和 Opera 下却还是不行。不能「TAB」到,即不能获得焦点,意味着不能被用户感知,更不用说跟它进行交互,如何解决呢?
Opera 可以通过为 a
手动添加 tabIndex
来解决;但 Firefox 不行…继续 Google 之后,发现了代码做不到的两个解决方案。
方法一:调教浏览器
浏览器地址栏输入 about:config
,添加或修改 accessibility.tabfocus
项,整型(字符串类型无效),值为 7
。Windows 的 Firefox 中默认值为 7
,如果改成 3
就跟 Mac 下一个德行了;但 Mac 下却根本没这个项,这大概就是这个问题的根本原因所在。
方法二:调教系统
如果不改 Firefox 的 about:config
,还有一个办法就是 System Preferences > Keyboard > Shortcuts,最下面选择「All controls」:
系统的,即是大家的,那么它对 Safari 和 Opera 是否同样有效呢?答案是:有点效果,只是有点…Safari 上原本无法「TAB」到的 button
、radio
和 checkbox
等都可以了,但 a
还是不行,即使加 tabIndex
也不行,还是必须「Option」 + 「TAB」;Opera 仍旧只能靠 tabIndex
。看来,这个系统设定是为 Firefox 量身定制的,真是「百撕不得骑姐」。
本节参考:
2.1.4 总结陈词
让 a
获得焦点的只能靠组合大招:Safari 让用户按「Option」 + 「TAB」;Firefox 让用户改 config;Opera 加 tabIndex
。
我们能做的也只有加 tabIndex
了,所以尽量不要用 a
做JS控件的主节点了,用 div
或 span
加 tabIndex="0"
比较靠谱。
2.2 「ENTER」/「SPACE」点击
元素获得焦点之后,如果是可以点击的,则希望用「ENTER」和「SPACE」来操作。
2.2.1 案例
测试bin:http://jsbin.com/zipiqi。
在之前的案例上做了一点点改动,为最后面的 a
、button
和 p
加了 onclick
。
2.2.2 发现问题
- Safari 7 (「Option」 + 「TAB」)
a
只对「ENTER」有反应;button
接受「ENTER」和「SPACE」;而p
,看下去就知道哪哪都不行; - Firefox 33
a
接受「ENTER」(若不解决「TAB」跳过问题,必须通过别的方式使其获焦,如使用Firefox的搜索框选中它后按ESC
);button
接受「ENTER」和「SPACE」;p
不行; - Chrome 39
a
接受「ENTER」;button
接受「ENTER」和「SPACE」;p
不行; - Opera 25
a
接受「ENTER」(若不加tabIndex
,似乎很难获得焦点,从而失败);button
接受「ENTER」和「SPACE」;p
不行; - IE 10(Win8)
a
接受「ENTER」;button
接受「ENTER」和「SPACE」;p
不行;
2.2.3 解决问题
不是所有获得焦点的元素都会在「ENTER」以及「SPACE」的时候执行 click
事件。
悲催的是,HTML 并没有提供任何属性告诉浏览器,「把我当 button 一样蹂躏吧」,所以即使对某个元素设置了 role="button"
,也无法让它自己接受「ENTER」和「SPACE」。幸运的是,这是代码可以解决的。既然,一个元素获得了焦点,那它就能绑定键盘事件(keyup
、keydown
、keypress
),需要注意的是有些按键会触发浏览器的默认事件,比如「SPACE」导致浏览器滚动,需要 preventDefault
一下。
2.2.4 总结陈词
这里只需要一点点 JS 绑定事件的技巧即可。
参考: