哇牛叉学院之基于 VoiceOver 的移动端无障碍阅读访问

本集出场人物:米亚、起床气、5号、阿力、软大
特别感谢:馒头

今日,学院布置了一个新任务,要去佘山拜访一位名为『飞天蝙蝠』的前辈,亲自上门取一件重要的宝物。并特别指定了『米亚』『起床气』『5号』三个感知型同学前往,同时安排『阿力』一同前往,保护诸位同学以及宝物的安全护送。至于这个宝物是什么,上面并没有明说,只是说到了那里自然就会知道。

按照地图指引,穿过眉公钓鱼矶,黄巢洞,远远就看到一座古味浓郁的联排建筑,走近一看,『飞天山庄』,哈,就是这里,正要敲门,突然就传来缓慢但略显尖利的声音:「来着何人?」

「前辈你好,我们是『哇牛叉学院』的学生,过来拜访前辈,顺便取一件宝物。」

「亮出凭证。」

『米亚』取出收据,正要奇怪给谁的时候,突然门就开了,「你们进来吧,直接去大厅,我稍后就到!」

片刻后,一位三四十岁中年人左手抓着个白色的盒子快步进来,直接坐在了主座上。突然大伙心中一惊,因为大家发现这人的眼睛全白,略显瘆人。于是,『5号』忍不住问道,「请问您是『飞天蝙蝠』前辈?」

「你难道感觉不出来吗?」

「这…」,突然的一顿憋让『5号』一时不知如何,结果好死不活又问了句,「前辈是不是有眼疾?」

「哈?你感觉不出来,难道还看不出来吗?」

「…」,突然发现这人都这么冲,大家都没在说话,反而老者先开口了:「剩下的一半钱款和刚才订金的收据留下,这个你们学院要的‘货物’就可以拿走了。」

大家互相看了看,估计心中的想法都是一样的,咱们赶快人完成任务,赶快撤,连口茶都没有的地方,实在没有久留的必要,「好!」只见『米亚』拿出一个小瓶子压住订金收据,回复道,「这是你要的东西,前辈!」

只见中年人右手准确抓起小瓶子,然后立马一层浅浅的『泉之力』包围这密封的放置『泉钻』的小瓶子,眉头一皱,似乎在发力,然后立马又舒展开,「恩,不错,你们学院产出的‘泉钻’品质还是一如既往的好,东西你们可以拿走了。」说完左手一扔。

我擦,众人心里一万个草泥马奔过,这么贵重的宝贝居然扔得这么荡气回肠,可是值我们一年的工资啊!

好在『阿力』反应快,稳稳接住了,不然就玩大发了。

「那好,前辈,我们还有事就先告辞了!」说完,众人起身准备离去,刚走出去没几步,背后就传来尖利地听起来怪怪的声音:「你们不验货倒是看得起我,我倒是蛮开心的,但是就是不知道没有我的指点,你们中有没有人会使用这个东西呢?」

众人一听立马停住了脚步,正在疑惑之时,又有声音传来,「你们学院应该把感知型学生都安排到这次任务里面了吧,难道你们认为这是巧合?」众人面面相觑,中年人继续道:「不管怎样,货还是要验的,不然你们出去被调了包,岂不成了我的锅?」

恩,说得很有道理,于是,大家又都回到座位上,当着中年人面打开那个白色的盒子,结果映入眼帘的是一个巴掌大长方形薄薄的有圆角的物体,有明显的正反两面,反面还有一个缺了一口的苹果图标。

中年人自己主动开口了:「这就是我花了数年时间制作的无障碍访问神器,我称之为 iPhone,特别适合感知型异能者使用,通过特定操作把感知到的精灵波动输入其中,就可以大致知道精灵的想法,我现在就教你们如何使用。」

先了解一些 ARIA 无障碍和 VoiceOver 的基本知识

首先,你们需要了解无障碍访问中的 ARIA。

ARIA 全称 Accessible Rich Internet Applications(可访问的富互联网应用),在当下讨论 ARIA 基本上都是为读屏设备或软件,而读屏设备或软件的使用者多是视力障碍人员,因此,ARIA 约定俗成就成了对视障用户的无障碍访问支持技术的代称了。

首先,了解下常见的读屏软件,如下列表:

移动端:

  • Android: TalkBack
  • Android: Funtouch
  • iPhone: VoiceOver

桌面端:

  • Windows: NVDA, JAWS
  • Chrome: ChromeVox
  • OSX: VoiceOver

ARIA 总共有3大部分组成,如下:

  • role 属性值
  • aria 属性
  • aria 状态属性

这篇文章「WAI-ARIA无障碍网页应用属性完全展示①」有非常详尽的翻译展示,我这里列举一些常用的:

role属性值

1
2
3
4
5
6
role="tab",  
role="button",
role="radio",
role="checkbox",
role="link",

aria属性

1
2
3
4
aria-haspopup,  
aria-label,
aria-owns,

aria状态属性

1
2
3
4
5
6
7
aria-checked,
aria-checked,
aria-selected,
aria-expanded,
aria-hidden,
aria-invalid,

然后,需要知道如何开启 iPhone 的 VoiceOver ?

最快捷的是使用 Siri,「打开 VoiceOver」,如果 Siri 卖萌说不知道 VoiceOver 是什么鬼?那就试试「请打开 VoiceOver」,或者手动开始,路径为:设置→通用→辅助功能→VoiceOver(在第一个,见下图)

在 iPhone上 VoiceOver 开启后,操作方式有个根本的变化:

  1. Touchstart 或 Touchmove 行为完全变成了内容识别与读取,没有任何 Web 交互行为的发生!
  2. 如果你想点击某元素,必须先 Touchstart 或 Touchmove 选中,然后再双击,直接双击是没有用的;
  3. 如果你想滚动,三指滑动;
  4. 如果你想拖动滑块,先 Touchstart 或 Touchmove 选中,然后连续轻触并滑动(手指不要抬起);

VoiceOver 开启后的手势如下图:

所以,很多人不小心打开VoiceOver后发现关不了了,就是因为还是老的操作习惯。想要关闭,最快捷siri;如果只卖萌不行动,那就手动按部就班关闭:(轻触,再双击)设置→(轻触,再双击)通用→(轻触,再双击)辅助功能→(轻触,再双击)VoiceOver->(轻触,再双击)关闭。

下面开始教一些真正的实战。

基于 VoiceOver 的移动端无障碍阅读访问实战

VoiceOver 下无障碍访问基础特性

首先,大家应该都知道,HTML 元素还可以分为非替换元素和替换元素,常见的替换元素包括 <img>,<input>,<img>,<button>,<iframe>,<video>,<object> 等。

除了内容可替换以及一些CSS行为差异外,在ARIA无障碍访问这一领域也是有着巨大的差异的。

一. 非替换元素

① touch页面任何区域一定会有信息读取!

非 VoiceOver 状态,你点击页面空白,是不会有什么反应的,但是开启 VoiceOver 后则完全不同,Touch 页面任何区域一定会有信息读取,包括空白区域,而轻触空白区域的读取遵循下面2个规则:

  1. 就近原则;
  2. 穿透规则;

所谓“就近原则”,比方说点击下图所示的位置:

结果选中和读取的内容是“免费 链接 导航 标志性内容”,哪个近读哪个。

所谓“穿透规则”,比方说点击下图所示的半透明黑色蒙层位置,请问读出来的信息是?

结果不是“信仰神国”,也不是“读至…”,读取的信息而是蒙层下面的图片列表信息!没错,直接穿透了。这个和正常浏览方式下的点击世界观是不一样的。但是,显然,此处穿透不是我们想要的,怎么避免呢?这个后面会介绍。

② 必须有文字内容才会读取

1、如果元素不含文字内容,是不会读取的,例如,下图图标轻触是被直接忽略的:

相关HTML代码如下:

1
2
3
4
<a href>    
   <i class="icon-free"></i> <!-- VoiceOver忽略 -->
   <h4>免费</h4>
</a>

2、还没完,如果我们增加 title 属性描述内容,那会不会读取呢?

1
<i class="icon-free" title="图标"></i> <!-- VoiceOver读不读呢? -->

答案是:依旧忽略!

3、但是,有一个例外,那就是元素,如果这里的标签换成,则 title 属性就可以被 VoiceOver 宠幸。

1
<a href class="icon" title="图标"></a>       <!-- 读:图标 链接 -->

4、还是标签,但是,我们不是使用 title 属性,还是 ARIA 原生的规范的 aria-label 信息描述属性,那会不会读取呢?

1
<i class="icon-free" aria-label="图标"></i>

答案是:忽略,不会读取!

5、 还没完,如果我们里面写入了文字,但是是 display: none 隐藏的,那会不会读取呢?

1
2
3
<i class="icon-free">       
<span hidden>图标</span>
</i>

答案是:忽略,不会读取!

6、 还没完,如果我们里面写入了文字,但是是用的是 visibility: hidden 隐藏,那会不会读取呢?

1
2
3
<i class="icon-free">        
<span style="visibility:hidden;">图标</span>
</i>

答案是:忽略,不会读取!

7、 还没完,假设我们使用 text-indent 缩进让文字隐藏到屏幕之外,那总该要读取了吧?

1
<i class="icon" style="text-indent:-999px;">图标</i>

答案是:忽略,不会读取!

8、 还没完,假如我们的 text-indent 缩进不要那么猛,仅仅是图标容器外,但是在屏幕内,那会不会读取呢?

1
<i class="icon" style="text-indent:-10%;">图标</i>

答案是:会读取!但是,选中时候的外框明显不在图标所在的位置,而是文字所在的位置。

9、 还没完,假设我们使用 CSS clip 属性隐藏,那会不会读取呢?

1
2
3
<i class="icon">        
<span style="position:absolute;clip:rect(0,0,0,0);">图标</span>
</i>

答案是:会读取!

10、 还没完,假设我们使用绝对定位屏幕外隐藏,那会不会读取呢?

1
2
3
<i class="icon">        
<span style="position:absolute;left:-999px;">图标</span>
</i>

答案是:忽略,不会读取!

11、 还没完,如果是相对定位屏幕外隐藏呢?

1
2
3
<i class="icon">        
<span style="position:relative;left:-999px;">图标</span>
</i>

答案是:忽略,不会读取!

12、 还没完,我还有最后一口气,终极绝招,如果文字是透明的,那会不会读取呢?

1
<i class="icon" style="color:transparent;">图标</i>

恭喜你,会读取!

③ 文字基于内联盒子片段读取

如下一段简单常见的HTML代码:

1
<p>总共消费<output>500</output></p>

请问,touchstarttouchmove 该元素的时候读取的信息是?

结果:要么「总共消费」,要么「五百」,要么「元」。是不会连续读取「总共消费500元」的。完全是基于内联盒子来读取的,例如这里「总共消费」和「元」是两个「匿名内联盒子」,外面有 output 标签的「500」是内联盒子。

但是,如果是 <h1>~<h6> 标题元素,则例外,例如:

1
<h6>总共消费<output>500</output></h6>

读「总共消费500元-标题级别6」。

二. 对于替换元素

① 替换元素永远不会穿透

比方说一开始的这个图:

要避免穿透,我们使用 role 属性将标签角色变成替换元素,例如设置 role=”button” ,如下截图:

② 替换元素title属性读取

还是上面那个免费图形:

1、 如果HTML是下面这样:

1
<i class="icon" role="img" title="图标"></i>

则 title 属性值是会读取的,这里会读「图标 图像」。

2、 如果角色是替换元素,则 aria-label 描述信息也会读取:

1
<i class="icon" role="img" aria-label="图标"></i>

3、 role=”img” 元素里面文本不读取,这个很好理解,原生的图片标签里面是不能哟文字内容的。

1
<i class="icon" role="img">图标</i>

此时只会读“图像”,里面的文字“图标”被忽略了。

4、 role=”button”display 隐藏文本均读取,也就是 color 透明隐藏,font-size: 0 隐藏,visibility: hidden 隐藏,text-indent 缩进隐藏,absolute 屏幕外隐藏。都是读取的。

但是,如果是 display: none,则忽略:

1
2
3
4
<i class="icon" role="button">      
<span hidden>图标</span>    
<!-- “图标”不读取 -->
</i>

5、 设置 aria-hidden=”true” 元素不可读不可点。

1
2
3
<i class="icon" role="button">      
<span aria-hidden="true">图标</span>    <!-- “图标”不读取 -->
</i>

aria-hidden=”true” 是 ARIA 无障碍处理售后非常常用的一个属性,所有的装饰性元素或者点击区域太小的元素都应该设置 aria-hidden=”true”,避免无谓信息对用户的干扰。

6、 当内容,标题等信息同时存在时候的读取顺序是:优先内容,然后类型,然后标题,例如:

1
<i class="icon" role="button" title="示意">图标</i>

读:「图标,按钮」,明显停顿后,「示意」。注意,读 title 属性之前有个非常非常明显的停顿,停顿时间之长,感觉就像朗读人断片了一样。

7、 aria-label 作用和文字内容类似,但是优先级更高。也就是同时存在的时候,文字内容读取会被忽略,例如:

1
<i role="button" aria-label="图标1" title="示意">图标2</i>

读:「图标1,按钮」,明显停顿后,「示意」。「图标2」不会读取,被忽略!

如果希望读内部信息,同时又自定义一些信息,可以使用 aria-describedbyaria-describedby 的行为表现和语义有些类似于 title 属性,aria-describedby 的属性值只能是元素 id,这非常适合保证原始标签语义同时告知辅助信息的场景,例如:

1
2
<h3 aria-describedby="ariaDesc1">热门小说</h3>
<span id="ariaDesc1" aria-hidden="true">编辑推荐</span>

8、 大段描述可以使用 aria-labelledbyaria-describedby,例如

1
2
<i role="button" aria-labelledby="id">图标2</i>
<p hidden id="id">这是一个...图标</p>

读:「这是一个…图标,按钮」。

可以看到,目前描述元素即使 display: none 也是可以读取的。aria-labelledbyaria-describedby 的属性值对应描述信息元素的 id 属性值,可以是多个,使用空格分隔,例如:

1
<i role="button" aria-labelledby="id1 id2 id3">图标2</i>

9、 <nav> 会读「导航,标志性内容」,<header> 读「横幅,标志性内容」,<footer> 读「页脚,标志性内容」。

然后,若触发的是子元素,仅第一次触发读取上面的信息。例如,导航中有两个链接,则轻触第一个的时候,读「xx, 链接,导航,标志性内容」,紧接着轻触第二个链接的时候,仅仅会读「xx, 链接」。

出乎意料的是,<ul>, <ol> 不会读列表,需要增加 role="listbox",这样,触发 <li;> 元素的时候会读“列表,第一个”。

所有 form 原生控件都能准确读取,包括状态。因此,一定要养成基于元素表单控件实现交互效果的习惯,控件丑没关系,透明度 opacity:0 覆盖处理之,例如,单选项,复选框效果等。

10、 后面这些角色设置可让断片文本连读:role="button", role="tab", role="heading", role="combobox" 等。

1
<p role="button">总共消费<output>500</output></p>

读:「共消费500元,按钮」。

虽然很好地解决了多个内联元素读取「断片」的问题,但是,不合语义,明明不是按钮,你说是按钮,问题很大。

后来,经过我的各种尝试,发现了一个无语义文本连读技巧,就是使用: role="option"

option 值源自 HTML 下拉列表的 <option> 标签。option 可以让里面文字连读的原理是,原生的 <option> 标签中只能是纯文本,会忽略一切的 HTML 标签,于是,设置了 role="option",就会当 <output> 标签不存在,从而连读,并且选中的高亮框范围也更加合理。

11、 使用 role="option" 连读的时候会可能会存在内容断句不清的情况。例如:

1
2
3
4
5
6
7
8
<li class="fans-li" role="option">        
<div class="rel">        
<aria>粉丝第1名:我是大帅哥</aria>    
</div>    
<div class="rel">        
<aria>粉丝等级:</aria>LV4        
</div>
</li>

此时朗读效果为「粉丝第1名 我是大帅哥粉丝等级 LV4」(空格表示停顿)。实际上,「我是大帅哥粉丝等级」中间应该有明显的断句。怎么办呢?可以使用英文句号,也就是点结束符,可以产生明显断句,比全角的冒号「:」似乎还要断句明显。修改如下(「我是大帅哥后面加了个点,隐藏」):

1
2
3
4
5
6
7
8
9
<li class="fans-li" role="option">        
<div class="rel">        
<aria>粉丝第1名:我是大帅哥.</aria>    
</div>    
<div class="rel">        
<aria>粉丝等级:</aria>
LV4        
</div>
</li>

role 属性值设置会覆盖原始的语义,例如:

1
<a href role="option">  总共消费<output>500</output></a>

读:「共消费500元」,不会提示「链接」。

由于 <option> 标签里面只能是纯文本,因此,role="option" 连里面的一切语义也会忽略,于是:

1
2
3
<li role="option">        
<a href>总共消费<output>500</output></a>
</li>

读:「共消费500元」,不会提示「链接」。

如果遇到这种尴尬,可以这么处理:

1
2
3
<li role="link">      
<a href role="option">总共消费<output>500</output></a>
</li>

也就是原语义外置。此时读:「共消费500元,链接」,这下没毛病了。

三. 交互与 aria

1、 VoiceOver 开启时,原来的 touchmove 是高成本操作,因此,需要增加详细的描述信息,否则用户根本不知道是个什么鬼?例如下面一段截图示意:

复杂交互场景,或者和原生控件交互不一致的交互场景,需要添加交互场景描述,例如下面示意:



需要同步改变描述和状态,例如面板展开和收起的时候,例如:

此时,先要使用 role=”menuitem” 定义是菜单项,然后使用 aria-expanded 标记是展开还是收起,VoiceOver 会自动根据此状态值读出对应的中文状态描述的。

1
<a href class="icon-more" title="更多" role="menuitem" aria-expanded="false"></a>

展开时候 aria-expanded 设为 true:

1
<a href class="icon-more" title="收起更多" role="menuitem" aria-expanded="true"></a>

上面例子中的 aria-expanded 就是 ARIA 中的状态属性,常用的其他属性还包括(需要配合特定的 role 属性值才有效):

  • aria-expanded 展开还是收起,菜单,自定义下拉等用的比较多。
  • aria-checked 选中还是未选。
  • aria-selected 选中还是未选。
  • aria-disabled 禁用还是可用。
  • aria-hidden 隐藏还是显示。
  • aria-invalid 验证正确还是错误。

其中 aria-checkedaria-selected 含义类似,那什么时候该用什么呢?

aria-checked 多用在单选复选上,aria-selected 多用在下拉列表上,或者选项卡role=”tab”上。

说到 role=”tab” 选项卡,有一个注意点需要提一下,role=”tab” 一定要加在平级的兄弟元素上,例如:

1
2
3
4
5
<nav>      
<h3 role="tab"><a href>选项卡1</a></h3>    
<h3 role="tab"><a href>选项卡2</a></h3>    
<h3 role="tab"><a href>选项卡3</a></h3>
</nav>

千万不要这样:

1
2
3
4
5
<nav>      
<h3><a href role="tab">选项卡1</a></h3>    
<h3><a href role="tab">选项卡2</a></h3>    
<h3><a href role="tab">选项卡3</a></h3>
</nav>

因为后面每个选项卡会认为就一个单独的选项卡,读「共1个」,前者可以正确读「共3个」。另外,VoiceOver 读取的时候不是读「选项卡」,听上去是「标间」,开房吗?

Aria 交互启示

  1. 浮层半透明遮罩如果有交互,需分层,并设置 role,如果点击整片区域都有行为,不要取巧使用冒泡,因为在无障碍处理的时候会很啰嗦,直接使用一个透明图层覆盖,设置合适的 role 以及描述;

  2. 浮层右上角要有关闭按钮,哪怕是透明的,千万不要信了设计师的邪,认为例如,弹框上有取消按钮,还搞个关闭干什么,不好看啰嗦。其实对于视力不好的用户,最习惯的是去屏幕右上区域找到关闭按钮。又或者浮层可以直接手指一划移出,不需要关闭按钮,那都是很自以为是的想法。如果设计师坚持,我们做开发的也需要在右上角加一个正常用户看不见的透明的关闭按钮,读屏设备是可以获取的。浮层关不掉是非常非常体验差的一件事情。

  3. 自定义控件最好使用原生控件覆盖 opacity:0,因为你这样处理后,根本不要再有任何额外的 HTML 或者 JS 支持就能有完美的无障碍访问了。

  4. 交互样式改变可以尝试直接使用 aria 属性控制,一举两得,例如,使用 aria-checked 属性而不是 .checked 类名,可以有效降低后期无障碍支持成本。

  5. 避免自定义的 touchmove 交互,因为开启读屏模式后,touchmove 交互成本很高。

四. 其他ARIA无障碍支持经验补充

① 将视觉信息转换成文字信息

例如下图:

这是个搜索列表,其中,有两个小色块,我们一看就能看明白是「标签」,而后面2个列表右侧是灰色文字,我们可以看出来是作者名。但是,对于靠触摸的盲人用户,颜色,大小包括位置都是看不到的,因此,他就很难区分这些信息是什么意思,VoiceOver 会这么读:

  • 巫妖王作者
  • 武侠分类
  • 武说谭家三十
  • 武侠世界独孤起步

估计用户心中是这幅表情:

因此,我们需要将视觉信息转换成文字,我的做法将必要的文字内容使用 clip 隐藏放在标签中:

1
2
3
4
aria {   
position: absolute;  
clip: rect(0,0,0,0);
}

此时,设备读取就是:

  • 巫妖王 标签:作者
  • 武侠 标签:分类
  • 武说 作者:谭家三十
  • 武侠世界 作者:独孤起步

② 小区域重要标志信息合并

有些信息很重要,但是占据空间很小,或者隐蔽,此时,最好信息合并,例如,图书上的限免标志:

我们可以给「限免」标志设置 aria-hidden=”true”,然后把该信息和图片信息合并,如下代码截图示意:

③ 关于SVG的无障碍访问

请问,有个 SVG 图标代码如下,清除轻触是否会有信息读取?

1
2
3
4
<svg class="icon icon-arrow-l”>    
<title>返回</title>    
<use xlink:href="#icon-arrow-l"></use>
</svg>

结果,只会读「图像」,然后就没有然后了。这其实跟上面 role=”img” 的标签里面文字不读原理是一样的,就是原生图像里面是不会有文字信息的,因此,这里

1
2
3
4
5
6
<a href>      
<svg class="icon icon-arrow-l”>        
<title>返回</title>        
<use xlink:href="#icon-arrow-l"></use>    
</svg>
</a>

请问,会有信息读取吗?

答案是:会读「返回,链接」。因为此时 SVG 从图像性质变成了链接内容。
有人会问了,如果我想单纯 SVG 标签读取信息怎么办?可以如下处理:

1
2
3
4
<svg role="img" aria-label="返回">        
<title>返回</title>    
<use xlink:href="#icon-arrow-l"></use>
</svg>

此时,会读「图像,返回」。

「以上就是此物的使用方法,好了,你们现在可以走了!」说完,大手一挥,中年人自己信步离开了。

「前辈?」『起床气』正有疑惑想继续追问,结果一眨眼,就没了人影。大家互相看了看,只能悻悻离开。虽然此番体验不佳,但好在大家都安全回到学院,东西也亲手交给院长『软大』,也算完成了任务。倒是接下来的一些信息让大家意外了。

「柯学长没有为难你们吧?」『软大』问道。

「柯学长?学长?」众人疑惑不已,「院长,你是说的那个瞎子,哦不,『飞天前辈』是我们的学长?」「对呀,他可是学院第一届学生中感知力最强的人,因为小时候吃苹果中毒失去了视力,因此,感应反而比一般人要强好多倍,就和五感尽失的陈长生神识变得更强一样,就算是密封器物中的泉之力也能感应。尤其黑夜中更是如鱼得水,因为被人称为『飞天蝙蝠』。后来因为某些事情提前退休,不然说不定现在是你们的老师……」

「诶…」众人心中嘀咕,「还好不是我们的老师~~」

PS: 本故事世界观和背景可参见 GitHub

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。