面试官:来说一说你是如何实现H5无障碍适配的?

H5无障碍适配的开发背景主要源于对视觉障碍人士使用网页的需求考虑。盲人视觉障碍人士在浏览网页时,主要依赖声音来判断操作,这通常通过开启手机的旁白模式来实现。在默认情况下,页面的所有元素标签,包括图片和文字,都能被识别。然而,为了提升盲人用户的体验,前端开发代码需要进行一些特定的处理。例如,对于不能识别的字段,需要添加额外的标签来确保其被正确识别和朗读。

基础

1,常见的读屏软件:

  1. NVDA (NonVisual Desktop Access) :一款免费的开源读屏软件,支持Windows操作系统。它提供了文字转语音和语音提示功能,使得视力受损的人士可以通过听觉来获取信息。

  2. JAWS (Job Access With Speech) :JAWS是一款商业化的读屏软件,它也提供了文本到语音的功能,并且支持多种应用程序和网页浏览器。

  3. VoiceOver:苹果公司在iOS和macOS设备上自带的读屏功能,适用于iPhone、iPad和Mac电脑。VoiceOver能够朗读屏幕上的文本,并支持手势控制。

  4. TalkBack:这是Android手机和平板电脑上的内置辅助功能,它提供了语音输出和触摸操作反馈,使得用户能够通过声音和手势进行导航。

2,常见的操作手势:

  1. 单指轻点:通常用于选择或打开应用程序、链接或按钮。

  2. 单指滑动:用于在屏幕上浏览内容,比如滚动网页或列表。

  3. 双指轻点:一些读屏软件会使用双指轻点作为模拟“单击”操作,因为单指轻点可能被用于其他功能。

  4. 三指滑动:用于快速在屏幕上移动,例如跳到页面的顶部或底部。

  5. 四指滑动:在某些设备上用于返回上一个屏幕或执行其他特定功能。

  6. 摇动设备:有些设备允许用户通过摇晃设备来触发特定的操作,比如撤销上一步或执行其他命令。

3,常见的属性和功能:

  1. aria-controls="main" :这个属性用于给操作按钮关联控制区域。在PC读屏软件中,添加了该属性后,用户可以把焦点从按钮快速移动到被控制区域,从而更高效地浏览和操作。
  2. aria-live="true" :此属性适用于那些会动态刷新的元素。当元素内容发生变化时,读屏软件会自动读出变化后的新内容。例如,它可以用于发现卡片上的“XXX人参与活动”或书城的换一批功能,以监听实时变化的数据。
  3. aria-hidden="true" :这个属性用于隐藏屏幕外的元素,使其不被读屏软件读出。这有助于确保用户只听到与他们当前关注的内容相关的信息,避免不必要的干扰。

项目适配方案

1. 语言设置

为页面设置正确的语言属性至关重要,这有助于读屏软件正确识别并播报内容。在HTML的<html>标签中,通过lang属性可以设定页面的语言。

英文页面设置:

<html lang="en">

中文页面设置:

<html lang="zh-CN">

确保中文项目设置为中文语言,以避免数字等内容的错误播报。

2. 层级属性与焦点管理

在无障碍适配中,管理页面上的焦点非常重要。初始状态下,div和span等标签可能会默认成为焦点,导致播报内容分散。为此,可以通过给外层元素添加tabindex属性来控制焦点,将UI模块整合为单一焦点进行播报。

例如:

<div class="content" tabindex="0">
  <!-- 内容区域 -->
</div>

注意:tabindex的值通常设为0或正整数,其中0表示该元素可通过键盘导航访问,但不会成为默认的焦点顺序中的一部分。

3. 聚焦样式

当元素获得焦点时,浏览器通常会为其添加默认的黄色边框。为了提升用户体验和视觉一致性,可以通过CSS来移除或自定义这些边框。

全局去除聚焦边框的CSS样式:

:focus {
  outline: none;
  /* 可添加自定义聚焦样式 */
  box-shadow0 0 3px rgba(002550.5); /* 示例:蓝色聚焦阴影 */
}

4. 图片播报

对于图片的播报,应当谨慎使用alt属性。如果图片仅用于装饰或布局,应设置alt=""以避免不必要的播报。若图片具有实际的信息内容,并需要被读屏软件识别,可以使用aria-label来提供替代文本。

<img src="close.png" aria-label="关闭" alt=""/>

此外,若要去除图片作为图形的默认播报,可以将role属性设置为presentation,而不是row

<img src="title.png" tabindex="0" aria-label="超级会员送您1个红包" role="presentation"/>

5. 按钮播报

为了让按钮正确播报其内容和交互指引,需要为按钮元素设置正确的tabindexrole以及aria-label(如果需要自定义播报内容)。

<button tabindex="0" class="btn" aria-label="立即使用按钮,点按可激活">立即使用</button>

对于img作为按钮的情况,应确保它的语义与实际用途相匹配。

<button tabindex="0" class="btn" aria-label="开,按钮,点按可激活">
  <img src="open.png" alt=""/>
</button>

6. 数字播报优化

对于特殊格式的数字(如手机号中的星号),需要编写特定的逻辑来确保它们被正确播报。可以通过编写公共的转换函数来实现这一点。

function broadcastNumber(number{
  // 根据实际需求,实现数字转换逻辑
  // ...
  // 示例:处理手机号中的星号
  if (number.includes('*')) {
    return number.replace(/\*/g'星号');
  }
  // 处理其他数字情况
  // ...
}

7. 自定义事件播报

当前,具有点击事件的节点在聚焦后,会按照系统规范默认播报“点按两次即可激活”。然而,这一通用性指引并不足以明确指导用户在不同交互场景下的操作。因此,需要根据具体的交互场景制定更精确的播报内容。

适配方案:在需要点击的节点内部包裹一层div,并将tabindex属性设置在这一层div上。通过aria-label属性,我们可以自定义播报内容,以提供更明确的操作指引。

<div class="content" tabindex="0" @click="select" aria-label="点按两次即可选中话费充值券">
  <div class="amount">
    <div class="num">100</div>
    <div class="unit"></div>
  </div>
  <div class="desc">话费充值</div>
</div>

播报内容:点按两次即可选中话费充值券

对于更复杂的场景,可以进一步扩展aria-label的内容,以提供更丰富的信息。

8. 额外内容播报

在某些情况下,需要播报额外的内容,如当前项的位置、总数以及操作指引等。

适配方案:使用空的div元素,并通过aria-label属性来添加这些额外的内容。这样可以确保读屏软件能够正确播报这些信息,而不会干扰到页面的视觉呈现。

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

注意:使用aria-hidden="true"来确保这些额外信息在视觉上不可见,但读屏软件可以读取。

9. 整体播报

当需要播报的内容较多且顺序与代码书写顺序不一致时,我们需要更精细地控制播报内容。

适配方案:通过aria-label拼接参数或使用aria-labelledby结合id来实现播报内容的定制。

(1)使用aria-label拼接参数

<div class="content" tabindex="0" :aria-label="`${item.title} ${item.num}元 ${item.type},${item.rule},${item.btn}`">
  <!-- 内容区域 -->
</div>

(2)使用aria-labelledby结合id

<div class="content" tabindex="0" aria-labelledby="title-id num-id type-id rule-id btn-id">
  <div id="title-id" class="title">{{ item.title }}</div>
  <div id="num-id" class="num">{{ item.num }}</div>
  <div id="type-id" class="type">{{ item.type }}</div>
  <div id="rule-id" class="rule">{{ item.rule }}</div>
  <div role="button" id="btn-id" class="btn">{{ item.btn }}</div>
</div>

aria-labelledby属性的值是一个由id值组成的空格分隔的列表,这些id值对应着需要播报的内容元素。读屏软件会按照aria-labelledbyid的顺序来播报内容。

10. 局部特殊播报

比如,在优惠券模块中,可能希望实现整体选中时播报全部优惠券内容,而针对内部的“立即使用”按钮,又需要实现单独的聚焦播报和跳转功能。

适配方案:

  • 在外层容器上设置tabindex="1",并使用aria-labelledbyaria-label来指定需要整体播报的内容的id或拼接的播报文案。
  • 对于内部需要聚焦的按钮或元素,同样设置tabindex="1",以使其可聚焦。
  • 对于不需要聚焦但需要在整体播报中提及的内容,使用aria-hidden="true"来避免重复播报。
<div class="coupon-content" tabindex="1" aria-labelledby="coupon-title coupon-amount coupon-btn">
  <div class="coupon-info" aria-hidden="true">
    <div id="coupon-amount" class="amount">
      <span class="num">{{ item.amount }}</span>
      <span class="unit"></span>
    </div>
    <div id="coupon-type" class="type">{{ item.type }}</div>
  </div>
  <div class="coupon-actions">
    <div id="coupon-title" class="title">{{ item.title }}</div>
    <div class="btn-wrapper">
      <button id="coupon-btn" tabindex="1" class="btn">{{ item.btnText }}</button>
    </div>
  </div>
</div>

aria-labelledby指向了三个id,分别是优惠券的金额、类型和按钮的文案。当外层容器被聚焦时,读屏软件会按照这三个id的顺序播报内容。内部的“立即使用”按钮设置了tabindex="1",使其可以单独聚焦和播报。其他非聚焦内容通过aria-hidden="true"避免重复播报。

11. 解决组件重复播报

在Vue中使用模板循环时,如果为循环中的每个元素都使用相同的id,并通过aria-labelledby引用这些id,则会导致读屏软件只播报第一个元素的内容。这是因为id应该是唯一的,而aria-labelledby会按照id列表的顺序依次查找并播报对应的内容。

适配方案:

  • 在循环中为每个元素生成唯一的id,可以使用元素的唯一标识(如item.id)来拼接id
  • aria-labelledby中使用这些唯一的id
<template v-for="item in items">
  <div class="content" tabindex="0" :aria-labelledby="`amount-${item.id} desc-${item.id}`">
    <div :id="`amount-${item.id}`" class="amount">{{ item.amount }}</div>
    <div :id="`desc-${item.id}`" class="desc">{{ item.desc }}</div>
    <!-- 其他内容 -->
  </div>
</template>

在上面的代码中,我们使用了:id="amount-{item.id} desc-${item.id}"来指定需要播报的内容的id列表。同样,描述元素也使用了类似的方式来生成唯一的id。这样,每个循环中的元素都会有不同的id,从而避免了重复播报的问题。

组件适配

在业务场景中,组件库的统一适配是减少各业务线单独适配工作量的关键。无障碍组件的适配主要包括播报内容管理和焦点管理两大核心部分。

播报内容管理

播报内容管理涉及对组件无障碍状态的准确描述。通常,尽量保持系统默认的播报行为,但在某些特定场景下,可以通过aria-labelaria-labelledby属性来定制播报内容,确保用户能够准确理解组件的状态或信息。

焦点管理

焦点管理主要针对元素状态会发生变化的组件,例如弹窗、轮播图、选择器等。有效的焦点管理能够确保用户在交互过程中,焦点能够按照预期的顺序移动,从而提高无障碍体验。

焦点管理的基本方法包括:

  • tabindex属性:通过为元素设置tabindex,可以控制元素是否可聚焦。例如,tabindex="-1"表示元素不可通过键盘操作聚焦,而tabindex="0"或正整数则表示元素可聚焦。
  • aria-hidden属性:当元素不需要被聚焦且不应被读屏软件播报时,可以设置aria-hidden="true"
  • el.focus()方法:在JavaScript中,可以通过调用元素的focus()方法来直接聚焦在该元素上。

典型案例

  1. switch、checkbox、radio 组件

这些组件在无障碍适配时相对简单。使用系统默认的role(如role="switch"),并通过aria-checked属性来播报开关状态,aria-disabled来播报是否禁用。无需复杂的焦点管理,只需确保播报内容的准确性。

<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>
  1. 弹窗、对话框组件

弹窗组件的适配相对复杂,涉及焦点和播报内容的双重管理。

焦点管理

  • 弹窗弹出时的焦点聚焦:对于通过属性传入的标题,组件内部可以自动聚焦并播报;对于通过slot插入的内容,组件和业务可以约定一个属性(如nx-popup-aria-auto-focus),组件在弹窗弹出时会自动查询并聚焦在这个带有特定属性的元素上。
  • 弹窗关闭时的焦点还原:在弹窗弹出前保存当前聚焦的元素(document.activeElement),弹窗关闭后将焦点手动还原到这个元素上。
  • 焦点穿透问题:针对安卓系统aria-modal属性不起作用的问题,除了对需要屏蔽的元素添加aria-hidden="true"属性外,还可以考虑使用其他技术手段(如遮罩层)来防止焦点穿透。

播报内容管理

  • 对于通过slot传入的内容,播报内容可以交由业务开发控制,确保内容的准确性和相关性。
  • 对于属性传入的情况,组件可以提供定制化播报的能力,通过组件属性来设置特定的播报内容。

3. 地址选择器组件

当弹窗弹出时,希望地址选择器能够自动聚焦到标题上并进行播报。以图示焦点排布为例,当焦点落在第一列时,应播报“河北省,滑动滚轮控件,可上下滚动切换”。此外,每当一列滚动结束时,应自动播报当前选中的完整地址,如“河北省,石家庄市,桥西区”。

焦点管理

在焦点管理方面,地址选择器组件遵循了与弹窗组件类似的策略。通过精确控制元素的tabindex属性,确保了焦点能够按照预期的顺序在组件内部移动。同时,也处理了弹窗关闭时的焦点还原问题,确保用户体验的连贯性。

播报内容管理

播报内容管理的挑战在于如何在每一列滚动结束时,准确播报变化后的地址,而此时焦点仍停留在选中的列上。为此,我们巧妙地添加了一个隐藏的元素,并通过aria-live="polite"属性使其能够实时播报内容的变化。当滚动结束时,我们动态修改该隐藏元素的ariaPickerContent值,从而触发读屏软件的播报。若不希望播报,则可将ariaPickerContent置为空字符串。此外,在弹窗关闭时,也会确保将其重置为空,避免不必要的播报。

<div class="nx-picker-scroll-aria" aria-live="polite">
  {{ ariaPickerContent }}
</div>

总结

以上是一些H5无障碍开发的适配方案,希望对大家有所启发。项目亮点不仅仅是采用XX牛X的技术解决了XX难题,在项目开始时候的调研,过程中的兼容性解决方案,沉淀出来的分享文档,后续的优化提升,都可以是亮点。近期的招聘还是很艰难,大家且行且坚持。

最后

还没有使用过我们刷题网站(https://fe.ecool.fun/)或者前端面试题宝典的同学,如果近期准备或者正在找工作,千万不要错过,我们的题库主打无广告和更新快哦~。

老规矩,也给我们团队的辅导服务打个广告。