Web如何适配无障碍?

8,617 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情

背景

之前公司要求我们所有网页必须实现无障碍适配,有幸接触了下「无障碍适配」。

上篇文章介绍了《什么是无障碍适配?》,推荐大家先读一下。

今天我给大家介绍,前端开发者如何实现无障碍适配。

1. 认识ARIA

你需要了解一下ARIA

ARIA (Accessible Rich Internet Applications) 是一组属性,用于定义使残障人士更容易访问 Web 内容和 Web 应用程序(尤其是使用 JavaScript 开发的应用程序)的方法。

它补充了 HTML,以便在没有其他机制时可以将应用程序中常用的交互和小部件传递给辅助技术。例如,ARIA 支持 HTML4 中的可访问导航地标、JavaScript 小部件、表单提示和错误消息、实时内容更新等。

警告:  许多这些小部件后来被合并到 HTML5 中,如果存在这样的元素,开发人员应该更喜欢使用正确语义的 HTML 元素而不是使用 ARIA。例如,原生元素具有内置的键盘可访问性、角色和状态。但是,如果您选择使用 ARIA,则您有责任在脚本中模仿(等效)浏览器行为。

常见的属性

这里列举了2个最常用的属性。更多属性,建议你去阅读官方文档,浏览一遍,看看都有什么属性,到时候有需要了,再去详查用法:ARIA相关属性

aria-label

aria-label,给元素设置一段描述性的文字,可以由屏幕阅读器读出,它内部的文字将被忽略。

<div>
你好,我是HullQin。
</div>

上方这个div被选中时,屏幕阅读器会播报「你好,我是HullQin」。

<div aria-label="HullQin的自我介绍">
你好,我是HullQin。
</div>

上方这个div被选中时,屏幕阅读器会播报「HullQin的自我介绍」,而不会播报「你好,我是HullQin」。

role="button"

role="button",将元素标记为按钮,。

<div role="button" onclick="alert(123)">
哈哈
</div>

上方这个div被选中时,屏幕阅读器会播报「哈哈,按钮」。但是没有role这个属性时,只会播报「哈哈」,视障用户并不知道它可以被点击。

2. 尽量使用Html5语义化标签

在body中正确使用这些标签: <header><footer><article><section><p><div><span><img><aside><audio><canvas><datalist><details><embed><nav><output><progress><video> 等等。

经典误区:给div设置onclick事件。

有时候为了方便,你可能直接把div当作button了,并绑定了onclick事件。这是不对的,无障碍软件可能无法识别到它是有点击事件的,就不会播报出来。

建议点击事件尽量只绑定在<a><button>这种原生clickable的元素上,而不是<div>上。

3. 隐藏无意义元素

如果是无用元素(如不影响业务流程的logo、图片),在最内层的Dom结点设置aria-hidden="true",或在一组无用元素的容器结点设置aria-hidden="true"

那么这些元素永远不会被激活(选中)了。

4. 打包(合并)密集内容

针对密集的文字内容,需要打包阅读。什么意思呢?

<div>
<div>名称:</div>
<div>HullQin</div>
</div>

这段话会被拆分为2个元素,「名称」和「HullQin」分别可以被选中和播报。这对视障群体并不友好,因为焦点多、密集,明明是同一块内容,却分散到2个焦点上,这不方便他们摸索整个页面

他们期望的结果是「名称:HullQin」,通过这样打包阅读,就把Key、Value绑定起来了,也减少了焦点数量。

打包阅读,有以下几种方案:

方案优点缺点
【推荐】父结点设置aria-label,值为所有子结点内容拼接的字符串,子结点设置aria-hidden="true"兼容性最好维护成本高(若子结点需要动态改变,父结点的aria-label也需要随之改变)
【推荐】父结点设置aria-labelledby,值为所有子结点的id(用空格拼接),子结点设置aria-hidden="true",注意使用该方法,每个子结点都需要设置id维护成本低(若子结点需要动态改变,父结点无需变化)存在兼容性问题,低版本屏幕阅读器会忽略aria-labelledby或aria-describedby。
【不推荐】父结点设置role="option"方式最简单option表明这是个select下拉框的选项,读屏软件会错误理解该控件的作用,部分安卓机会播报“单指双击即可执行”。

我通常会选择方案一,兼容性最好。

5. 合并的结点包含链接时

例如:

<div>
您已阅读并同意<a>《协议》</a></div>

这种场景最好是打包阅读的,并且还要求点击《协议》后,能跳转至协议页面。

打包阅读的内容,包含了链接,有以下几种方案:

方案适用场景优点缺点
在打包阅读方案基础上,链接结点不设置aria-hidden="true"通用不影响非读屏模式下的行为iOS通过转子可聚焦链接,安卓也有类似方法。部分读屏软件可能无法打开链接
将链接的onclick事件放在父结点触发,父节点设置role="button"或"link"只有1个链接,且整体内容与该链接强相关(如“您已阅读并同意《协议》”)打开链接很方便影响了非读屏模式下的行为,普通用户点击非链接内容,也会打开链接

6. 管理焦点

如果需要主动管理焦点(例如页面初始焦点放在大标题上、弹窗打开时切换焦点至弹窗标题、弹窗关闭时恢复之前的焦点位置),需要通过element.focus()方法来控制焦点,但只有<button><a>这种可交互结点才会被focus成功,<div>这种纯展示结点不会被focus,需要设置tabindex="-1"(不要设置为非负整数,非负整数会允许键盘通过tab激活该焦点),再设置样式outline:0(因为浏览器默认样式在结点focus时会有边框,样式选择器是:focus-visible)。

注意事项:设置tabindex="-1"后,部分安卓手机会播报“单指双击即可执行”。

7. 弹窗

需要给弹窗容器设置aria-modal="true"role="dialog"

页面内有弹窗,需要管理焦点,开启弹窗时,focus弹窗标题(也可focus“关闭”)。关闭弹窗时,focus打开弹窗之前的焦点。

打开弹窗时,如果弹窗有移动动画(例如从下往上进入屏幕),需要在动画结束后,再调用focus(通过setTimeout或动画结束事件)。否则在iOS上焦点会不准(纵向偏移了一些像素)。

打开弹窗时,弹窗下的所有元素都不应该被激活(通过左右滑动手势,不应该选中弹窗底部元素)。具体方案:给弹窗下所有元素增加class="under_dialog"(只需要给最外层的容器结点加一次这个类名即可),打开弹窗时,调用$('.under_dialog').attr('aria-hidden', 'true'),关闭弹窗时调用$('.under_dialog').attr('aria-hidden', 'false')

这里用了jQuery表达意思,能懂就行。原生语法是:

Array.from(document.getElementsByClassName('under_dialog')).forEach(el => {
  el.setAttrbute('aria-hidden', 'true');
});`

如果你用React或者Vue,可以设置个全局的State来控制。

8. 结点动态变更

例如按钮状态可能会在js中变为disabled,注意最好直接用原生的disabled属性,否则,你还需要手动设置aria-disabledtrue

写在最后

我还写了文章《小程序如何适配无障碍?》,大家可以继续阅读。

我感到十分荣幸,能在互联网时代,为“障碍群体”开发无障碍的Web应用,让他们也能跟上时代潮流。

我是HullQin,公众号线下聚会游戏的作者(欢迎关注公众号,联系我,交个朋友),转发本文前需获得作者HullQin授权。我独立开发了《联机桌游合集》,是个网页,可以很方便的跟朋友联机玩斗地主、五子棋等游戏,不收费无广告。还独立开发了《合成大西瓜重制版》。还开发了《Dice Crush》参加Game Jam 2022。喜欢可以关注我噢~我有空了会分享做游戏的相关技术,会在这2个专栏里分享:《教你做小游戏》《极致用户体验》