vivo 场景下的 H5无障碍适配实践

作者:vivo 互联网前端团队- Zhang Li、Dai Wenkuan

随着信息无障碍的建设越来越受重视,开发人员在无障碍适配中也遇到了越来越多的挑战。本文是笔者在vivo开发H5项目做无障碍适配的实践总结。本文主要介绍了在前端项目中常用的无障碍手势和无障碍属性,并且结合具体的开发案例为开发者真实展示了适配要点,提供组件适配思路。希望本文能为前端开发者带来更多的参考和帮助。

一、背景

1.1 无障碍适配认知

无障碍(Accessibility)是指为各种能力水平的人们提供公平和平等的机会和体验,以便他们可以更容易地访问、使用和参与社会中的各种产品、服务和环境。这些人包括身体残疾、听力、视觉、认知和学习障碍等各种能力水平的人。

1.2 无障碍适配原因

常见的障碍类型包括:视觉障碍、听觉障碍、认知障碍、行动障碍。这些"有障碍"的群体,在使用软件时无法像普通人一样操作,他们或许看不清,需要更大的字体,或许看不到,需要语音播报,又或许听不清听不到,需要依赖视觉反馈,或许肢体操作不方便,无法自由操纵手机。

所以,国家和企业需要站在"有障碍"的群体思考,为各种能力水平的人们提供公平和平等的机会和体验。

  • 【政策】:2011年《中国残疾人事业"十二五"规划纲要》指出了, 建设无障碍环境的主要任务之 一就是加强信息无障碍建设,并明确了相关的政策措施。

  • 【包容性】:无障碍适配是一个重要的设计和开发目标,可以帮助您的网站或应用程序更加包容和可访问。

  • 【经济性】:无障碍适配不仅是一种道德义务,也是一种经济利益,因为它可以为您的网站带来更广泛的受众和更好的用户体验,带动业务增长。

1.3 无障碍适配引导

除了系统默认支持的大字体、内容播报、语音识别、字幕、语音控制等基础无障碍适配,更多的页面交互还是需要前端工程师们完成适配,所以开发者们需要开发出可识别的web页面,让障碍群体可以正常访问和使用。本文主要针对视障人群进行无障碍适配。

二、无障碍操作

在无障碍开发前,前端工程师们需要了解下无障碍的操作手势,站在障碍人群的角度体验操作,这样才能开发出更好的交互体验。也能避免因操作错误导致开发错误和重复开发。

2.1 读屏幕软件

首先我们需要了解下针对视力障碍人员使用的读屏设备或软件,常见的读屏软件,如下列表:

由于我们是在vivo手机内做移动端安卓手机无障碍适配,以下的方案、案例等介绍均是基于Android-TalkBack。

2.2 常用操作手势

借助 TalkBack 手势,可以在 Android 设备上进行导航和执行常用操作,以下操作为安卓9.1版及更高版本,仅在vivo手机的操作。官方链接:Talkback 手势

三、常用属性介绍

3.1 aria属性

ARIA全称"Accessible Rich Internet Applications(可访问的富互联网应用)",是W3C的Web无障碍推进组织在2014年3月20日发布的可访问富互联网应用实现指南。是一个为残疾人士等提供无障碍访问动态、可交互Web内容的技术规范。ARIA 用于提高使用 HTML、CSS、JavaScript、AJAX 和相关技术开发的动态内容和高级 UI 控件的辅助功能。ARIA 现在正式成为 HTML5 规范的一部分,还可以嵌入在常用的 JavaScript 库中。

3.2 aria状态

3.3 role属性

role将元素标记为不同的属性,常用属性"button(按钮),region(图形)",根据定义的不同属性,播报不同的内容。

3.4 tabindex 属性

tabindex 属性的使用可以使得原本无法取得焦点的元素获取焦点。目的是为了使用户可以用键盘访问任何可以用鼠标访问的元素。我们知道,使用 Tab 键可以按文档顺序 tab 到所有可以获取焦点的元素。tabindex 可以设置为 -1, 0 或者是任何自然数。

当用户使用 Tab 键浏览页面时,元素获取焦点的顺序是按照 HTML 代码里面元素出现的顺序排列的,有时跟实际看到的页面顺序并不一致。

四、无障碍适配案例

4.1 项目适配案例

1.语言设置

在全局元素中的 lang 属性设置页面的语言:

英文:

ini 复制代码
<html lang="en"> 

中文:

ini 复制代码
<html lang="zh-CN"></html>

国内的中文项目一般需要设置为中文,如果设置成了英文,有些数字会播报成英文。

2.层级属性

如果项目还没开始适配,则开启无障碍播报,会发现标签div和span默认作为焦点并播报,这样的话焦点和播报内容就非常分散。

如下图2,就会有3个焦点,这样过于分散,一般适配会将ui的模块作为一个大焦点整体播报,此时在外层div添加tabindex=1聚焦。

图1:原图

图2:分散焦点

图3:一个大焦点

备注:绿色框为焦点标记

(1)聚焦播报

因为div标签会自动播报,所以就算焦点聚焦到外层,但是内层还是会自动播报。

java 复制代码
<div class="content"  tabindex="1">
  <div class="amount">
    <div class="num">100</div>
    <div class="unit">元</div>
  </div>
  <div class="desc">话费充值</div>
</div>

播报内容:100元话费充值。

(2)自定义播报

但是会存在div里写的内容和需要播报的内容不一致,则可在外层tabindex="1"聚焦后,通过aria-label写上自定义内容

java 复制代码
<div class="content" tabindex="1" aria-label="获得100元的话费充值券">
  <div class="amount">
    <div class="num">100</div>
    <div class="unit">元</div>
  </div>
  <div class="desc">话费充值</div>
</div>

播报内容:获得100元话费充值券

3.聚焦样式

聚焦可通过tabindex实现,但是聚焦后样式会有黄色的边框,可通过outline: none去除,但是页面中的焦点太多,可通过全局去除。

在公共css文件里设置:

java 复制代码
*[tabindex] {
    outline: none;
}

4.图片播报

(1)图片的alt属性设置

图片通过img标签实现,如果图片不可聚焦,则设置**alt=""**即可。如果可聚焦并需要播报内容,建议通过aria-label设置。如果使用alt,一旦图片加载不出来,就会把alt的内容显示出来,而且alt内容没有样式,在H5页面上会显得很突兀。

java 复制代码
<img src="close.png" aria-label="关闭" alt=""/>
<img src="dog.png" alt=""/>

(2)去除图片默认属性(图形)播报

可在img标签里,将role属性改为row

java 复制代码
<img
  src="title.png"
  tabindex="1"
  aria-label="超级会员送您1个红包"
  role="row"
/>

5.按钮播报

通常ui上的按钮在选中后,需要播报按钮内容+按钮+引导操作(如:点按两次即可激活)

首先需要聚焦(tabindex="1")到节点,然后需要在按钮上的节点上写上role="button",加上这个属性后,后面会自动播报"按钮,点按两次即可激活"

案例:div中的按钮

java 复制代码
<div tabindex="1" role="button" class="btn">立即使用</div>

播报内容:立即使用,按钮,点按两次即可激活

案例:img中的按钮

java 复制代码
<img
  class="btn"
  tabindex="1"
  role="button"
  aria-label="开"
  src="open.png"
/>

播报内容:开,按钮,点按两次即可激活

6.数字播报

如:手机号:181****8805中的星号不能正确播报,而是播报成乘,数字播报成整数。

错误播报:一百八十一乘乘乘乘八八零五

需正确播报:幺八幺星号星号星号星号八八零五

可写一个公共方法映射数字、*号和X号播报文案,在需要的转换方法里调用该方法形成手机号播报文案。

公共方法如下:

java 复制代码
function $broadcastNumber(number) {
  if (number === 0) return '零'
  if (!number) return ''
  const numberMap = ['零', '幺', '二', '三', '四', '五', '六', '七', '八', '九']
  const specialMap = { '*': '星号', 'X': '叉号', 'x': '叉号' }
  return number.toString().split('').map(item => numberMap[item] || specialMap[item] || item).join('')
}

7.自定义事件播报

目前带有点击事件的节点,聚焦后会默认播报点按两次即可激活(系统规范),但是这个引导不够明确,需要就具体交互场景制定播报内容,如:点按两次即可选中,点按两次即可打开红包等。

方案:在click事件节点里层包裹div,并将焦点tabindex写在这一层的div上,再自定义播报的内容即可。

java 复制代码
<div class="content" tabindex="1" @click="select">
  <div class="amount">
    <div class="num">100</div>
    <div class="unit">元</div>
  </div>
  <div class="desc">话费充值</div>
</div>

播报:100元话费充值,点按两次即可激活

java 复制代码
<div class="content" @click="select">
  <div class="container" tabindex="1" aria-label="获得100元的话费充值券,点按两次即可选中">
    <div class="amount">
      <div class="num">100</div>
      <div class="unit">元</div>
   </div>
  <div class="desc">话费充值</div>
 </div>
</div>

播报:获得100元话费充值券,点按两次即可选中

8.额外内容播报

如:第几项,共几项,点按两次即可选中

方案:可用空div加aria-label实现

java 复制代码
<div class="content" @click="select">
  <div class="container" :class="{ 'z-active' : code === item.code }" tabindex="1">
    <div v-if="code === item.code" aria-label="已选中"></div>
    <div class="amount">
      <div class="num">{{ item.num }}</div>
      <div class="unit">元</div>
    </div>
    <div class="desc">{{ item.desc }}</div>
    <div :aria-label="`第${index+1}项,共${list.length}项,${code === item.code? '' : '点按两次即可选中'}`"></div>
  </div>
</div>

9.整体播报

通常整体播报的内容较多,且播报顺序非代码书写顺序,这个时候就需要在外层焦点里,控制播报的内容,主要可通过两种方法实现,aria-label拼接参数和aria-labelledby设置id。

需要播报的内容:话费充值券,50元满减券,满199元可使用,立即使用。

(1)aria-label拼接参数

可通过在外层节点设置tabindex=1后,再添加aria-label属性按照需要的顺序添加参数

案例:

java 复制代码
<div class="content" tabindex="1" :aria-label="`${title}${num}元${type}${rule}${btn}`">
  <div class="left">
    <div class="amount">
      <span class="num">{{ num }}</span>
      <span class="unit">元</span>
    </div>
    <div class="type">{{ type }}</div>
  </div>
  <div class="right">
    <div class="desc">
      <div class="title">{{ title }}</div>
      <div class="rule">{{ rule }}</div>
    </div>
    <div role="button" class="btn">{{ btn }}</div>
  </div>
</div>

(2)aria-labelledby设置id

在外层节点设置tabindex=1后,在需要播报的内容节点里添加id值,并将id值按照需要的顺序写在外层节点aria-labelledby属性里

案例:

java 复制代码
<div class="content" tabindex="1" aria-labelledby="title amount type rule btn">
  <div class="left">
    <div id="amount" class="amount">
      <span class="num">{{ num }}</span>
      <span class="unit">元</span>
    </div>
    <div id="type" class="type">{{ type }}</div>
  </div>
  <div class="right">
    <div class="desc">
      <div id="title" class="title">{{ title }}</div>
      <div id="rule" class="rule">{{ rule }}</div>
    </div>
    <div id="btn" role="button" class="btn">{{ btn }}</div>
  </div>
</div>

10.局部特殊播报

如优惠券模块,可整体选中播报全部优惠券内容,但内部立即使用按钮又可聚焦播报跳转。

方案:外层聚焦-设置tabindex=1,播报整块内容设置aria-label拼接参数或aria-labelledby设置id,内层部分内容聚焦,聚焦内容设置tabindex=1,不聚焦的部分设置aria-hidden="true"(不然在选中外层焦点时候,内层非聚焦部分会重复播报)。

java 复制代码
<div class="content" tabindex="1" aria-labelledby="title amount type rule btn">
  <div class="left" aria-hidden="true">
    <div id="amount" class="amount">
      <span class="num">{{ num }}</span>
      <span class="unit">元</span>
    </div>
    <div id="type" class="type">{{ type }}</div>
  </div>
  <div class="right">
    <div class="desc" aria-hidden="true">
      <div id="title" class="title">{{ title }}</div>
      <div id="rule" class="rule">{{ rule }}</div>
    </div>
    <div id="btn" tabindex="1" role="button" class="btn">{{ btn }}</div>
  </div>
</div>

11. 解决组件设置aria-labelledby="[id]"后只重复播报第一条数据的内容

因组件被Vue中的模板for循环调用,每个内容不一样,用同一个id会导致播报同一个内容,且没有key值的区分,需要解决设置aria-labelledby="[id]"后只重复播报第一条数据的内容

此时需要根据唯一标识区分id:可在id后拼接唯一标识号,如:"amount-${[item.id](http://item.id)}"

案例:

java 复制代码
<div class="content" tabindex="1" :aria-labelledby="`amount-${item.id} desc-${item.id}`">
  <div id="amount" class="amount">{{ item.amount }}</div>
  <div id="desc" class="desc">{{ item.desc }}</div>
</div>

4.2 组件适配案例

除了具体业务的适配,还有一些共性的组件问题需要组件库统一适配,这样能减少各业务单独适配的工作量。

任何一个无障碍组件的适配,都包含播报内容管理、焦点管理两部分。对于播报内容管理,几乎所有的组件适配都会涉及到。无障碍aria role、states一般系统都有自己默认的播报行为,尽量保持系统默认的播报。当然,也可以通过aria-label定制播报内容。焦点管理主要针对元素会发生变化的组件,如弹窗、轮播图、各类选择器等。

焦点管理的基本方法有3种:

  1. tabindex属性;

  2. aria-hidden属性;

  3. el.focus()方法。

tabindex="undefined"即意味着元素不可聚焦,为其他值意味着元素可聚焦。aria-hidden为true,意味着不可聚焦且不播报。对于可聚焦的元素,可以通过el.focus()方法直接聚焦在该元素上。

下面通过典型案例来说明各个组件是如何处理焦点和播报内容的。另外,由于一个html中可能多次使用同一个组件,所以下面的案例都是在不使用id属性的基础上完成适配的。如果一定要在某个组件使用id属性,记得通过随机函数对id属性做随机命名。

1. switch、checkbox、radio组件

这几个组件相对简单,使用系统默认的role即可。完成播报内容的适配即可,不需要做焦点管理。

以switch为例:首先是role=switch,然后通过aria-check播报开关状态,最后通过aria-disabled来播报是否禁用。

java 复制代码
<div
  class="nx-switch"
  tabindex="-1"
  :aria-checked="!!value"
  :aria-disabled="disabled"
  @click="onClick"
  role="switch"
>
  <div :style="barStyle"></div>
  <div :style="ringStyle"></div>
</div>

2.弹窗、对话框组件

弹窗组件是一个相对复杂的组件,既涉及到焦点管理,也涉及到播报内容管理。通过弹窗组件主要介绍焦点管理的小技巧。

该组件正在业务中的使用情况:

我们的弹窗组件相对比较通用,主要包括标题、内容、按钮三个部分。其中,标题、内容既可以通过属性传入,也可以通过slot传入。对于slot传入的部分,组件不太好控制。在业务使用过程中,还普遍存在只有内容的弹窗。

适配目标:弹窗弹出时需要自动聚焦在标题上并播报。

弹窗焦点顺序:弹窗弹出时需要自动聚焦在标题上并朗读标题,然后手动移动下个焦点聚焦到说明文本,最后聚焦到操作按钮。

(1)焦点管理

首先,需要解决弹窗弹出时的焦点聚焦问题。由于弹窗组件非常灵活,所以使用场景也非常多样。对于属性传入的标题,组件内部可以获取到该元素,并完成聚焦播报;对于slot插入的就显得无能为力。

对于slot这种情况,组件和业务约定了一个属性,如果业务想聚焦在这个dom元素上,就给该元素添加这个属性。组件会通过el.querySelector查询弹窗的后代元素,在弹窗弹出时自动添加tabindex并完成聚焦播报。

java 复制代码
<nx-popup v-model="isPopupShow.center">
  <div
    nx-popup-aria-auto-focus="true"
    aria-label="无障碍播报测试"
    class="nx-popup-center"
  >
    Popup Center
  </div>
</nx-popup>

另外,考虑到有业务要求弹出时不自动聚焦在弹窗上,所以还提供了属性,用于关闭焦点管理功能。

然后,需要解决弹窗关闭时的焦点还原问题。在弹窗弹出前保存当前聚焦的元素document.activeElement,关闭弹窗以后,通过el.focus()手动聚焦在该元素上。

还剩一个焦点穿透问题。很多安卓系统aria-modal属性不起作用,所以焦点还能够穿透弹窗,选中页面上的元素。

目前组件没有特别好的办法处理这点。开发过程中,可以对需要屏蔽的元素添加aria-hidden=true属性

(2)播报内容管理

这部分就显得比较简单。对于slot的情况,播报内容可以交给业务开发控制;对于属性传入的情况,可以添加组件属性,为业务提供定制化播报的能力。

3.地址选择器组件

地址选择器是一个滚动式交互的组件。在这个组件的开发过程中使用到了一种小技巧,即不聚焦在某元素上,也能自动播报该元素变化的内容。

该组件在业务中的使用情况:业务开发没有对该组件定制化,所以只用考虑组件内部的适配即可。

适配目标:在弹窗弹出时自动聚焦到标题上并播报。下图是焦点的排布。

以该图为例,焦点聚焦在第一列的时候,播报"河北省,滑动滚轮控件,可上下滚动切换";另外,在每一列滚动结束时,播报当前选中的地址,如"河北省,石家庄市,桥西区"

(1)焦点管理

同弹窗组件。

(2)播报内容管理

这里的难点就在于每一列滚动结束的时候,需要播报变化后的地址,但是此时焦点还在选中的这列上。

这里添加了一个隐藏的元素(不在页面上显示),并添加属性aria-live="polite ",滚动结束的时候修改ariaPickerContent 的值。如果不想播报则将ariaPickerContent置为空字符串,弹窗关闭的时候记得置为空。

java 复制代码
<div class="nx-picker-scroll-aria" aria-live="polite">
  {{ ariaPickerContent }}
</div>

五、总结

本文是在vivo体系下的无障碍适配实践,主要提炼总结了一些适配方法,对内外部的无障碍适配工作都有一定的参考和借鉴价值。下一步计划丰富和完善组件库的适配,沉淀一套高效的适配方案,尽量减少开发人员的适配成本,提高开发效率。

开发者应秉持着技术有温、代码有爱的态度,坚守"勿以善小而不为"的准则,以用户为导向,让我们的产品和服务照亮每一位用户,让"障碍群体"在这个有爱的互联网时代,紧跟时代潮流。

相关推荐
大怪v1 小时前
【Virtual World 04】我们的目标,无限宇宙!!
前端·javascript·代码规范
狂炫冰美式1 小时前
不谈技术,搞点文化 🧀 —— 从复活一句明代残诗破局产品迭代
前端·人工智能·后端
xw52 小时前
npm几个实用命令
前端·npm
!win !2 小时前
npm几个实用命令
前端·npm
代码狂想家2 小时前
使用openEuler从零构建用户管理系统Web应用平台
前端
dorisrv4 小时前
优雅的React表单状态管理
前端
蓝瑟4 小时前
告别重复造轮子!业务组件多场景复用实战指南
前端·javascript·设计模式
dorisrv4 小时前
高性能的懒加载与无限滚动实现
前端
韭菜炒大葱4 小时前
别等了!用 Vue 3 让 AI 边想边说,字字蹦到你脸上
前端·vue.js·aigc
StarkCoder4 小时前
求求你,别在 Swift 协程开头写 guard let self = self 了!
前端