可访问性(Accessibility)

为何要需要可访问性?

Web可访问性(也被称为 a11y)是让网站对所有人群可用的的设计和发明。通过辅助技术来与页面交互对于可访问性支持是必要的。

React完全支持构建可访问性的页面, 通常通过使用标准的 HTML 技术。

标准和指导

WCAG

网络内容可访问性指南 为创建可访问性站点提供了指导。

下列的WCAG列表提供了概览:

WAI-ARIA

网络可访问倡议 - 可访问性富网络应用 文档涵盖了构建完整的可访问JavaScript工具技术。

注意JSX完全支持所有的aria-* HTML属性。然而,在React中大部分DOM属性和特性采用驼峰命名规则,这些属性应该使用连字符(也称为kebab-case,lisp-case等),因为它们是纯HTML格式:

<input
  type="text"
  aria-label={labelText}
  aria-required="true"
  onChange={onchangeHandler}
  value={inputValue}
  name="name"
/>

语义化的 HTML

语义化的 HTML 是Web应用程序可访问性的基础。 使用各种HTML元素来加强我们网站中信息的含义,往往会使我们获得更好的可访问性。

有时候我们会向 JSX 中添加 <div> 元素,会破坏 React 代码正常工作,特别是在使用列表(<ol><ul><dl>)以及 HTML <table>。在这些情况下,我们应该使用 React 片段(Fragments) 将多个元素组合在一起。

例如,

import React, { Fragment } from 'react';

function ListItem({ item }) {
  return (
    <Fragment>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </Fragment>
  );
}

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        <ListItem item={item} key={item.id} />
      ))}
    </dl>
  );
}

您可以像将任何其他类型的元素一样,将 item 集合映射到 片段(fragments)数组中:

function Glossary(props) {
  return (
    <dl>
      {props.items.map(item => (
        // Fragments should also have a `key` prop when mapping collections
        <Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </Fragment>
      ))}
    </dl>
  );
}

当您在 Fragment 标签上不需要任何 props(属性)时,您可以使用短语法,如果您的工具支持这种短语法的话:

function ListItem({ item }) {
  return (
    <>
      <dt>{item.term}</dt>
      <dd>{item.description}</dd>
    </>
  );
}

For more info, see the Fragments documentation.

可访问表单

标签

每个HTML表单控件,例如<input><textarea>,都需要被标记上的可访问的标签。我们需要提供描述性的标签同时也展示给屏幕阅读器。

下列资源展示了如何使用标签:

尽管这些标准的HTML实践可以直接用于 React,但需要注意在 JSX 中,for 特性被写作htmlFor:

<label htmlFor="namedInput">Name:</label>
<input id="namedInput" type="text" name="name"/>

告知用户异常

异常环境需要所有用户理解。下列链接也说明了如何显示错误文案给屏幕阅读器:

焦点控件

确保你的网络应用可以完全仅通过键盘来操作:

键盘焦点和焦点边框

键盘焦点涉及DOM中被键盘选中用于接受输入的当前元素。我们可以在每一处地方看见如下图所示的类似的焦点边框:

Blue keyboard focus outline around a selected link.

仅能使用CSS来移除这一边框,如果你要用其他边框来替换他,例如可以设置outline: 0

定位到期望内容的机制

在应用中提供一种机制用以允许用户跳过之前的导航部分来帮助和加速键盘导航。

Skiplinks 或 Skip Navigation Links 隐藏在导航链接中,仅当用户用键盘与页面进行交互时可见。他们非常容易通过页面内部锚点和一些样式来实现:

也可使用路标元素和角色作为辅助技术,例如<main><aside>,来将页面划分区域以允许用户快速导航到这些部分。

阅读更多了解关于使用这些元素以提高可访问性:

编程式地管理焦点

我们的React应用会在运行期间持续地修改HTML DOM元素,有时会导致键盘焦点丢失或定位到未知元素上。为修复该问题,我们需要用代码微调键盘焦点到正确的方向。例如,重设键盘焦点到一个打开模态窗口的按钮上,在模态窗口关闭之后。

Mozilla开发者网络可以查看并描述了我们如何构建键盘导航的JavaScript工具

为在React中设置焦点,我们可使用Refs to Components

为使用它,我们先在组件类的JSX中创建一个元素的ref:

render() {
  // Use the `ref` callback to store a reference to the text input DOM
  // element in an instance field (for example, this.textInput).
  return (
    <input
      type="text"
      ref={(input) => { this.textInput = input; }} />
  );
}

而后,当需要时,我们可以在我们组件的其他地方设置焦点:

focus() {
  //使用原始DOM API显式地集中文本输入
   //注意:我们正在访问 "current" 来获取DOM节点
  this.textInput.current.focus();
}

有时,父组件需要将焦点设置为子组件中的一个元素。 我们可以通过 将 DOM refs 公开给父组件 来做到这一点 通过一个子组件上指定的 prop 将父对象的引用(ref)转发给子节点的DOM节点。

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.inputElement = React.createRef();
  }
  render() {
    return (
      <CustomTextInput inputRef={this.inputElement} />
    );
  }
}
/>

// 现在您可以根据需要设置焦点。
this.inputElement.current.focus();

当使用 HOC 扩展组件时,建议使用 React 的 forwardRef 函数 将ref转发到 包装组件上。 建议使用 React 的 forwardRef 函数将ref 转发到包装组件。 如果第三方 HOC 没有实现 ref 转发, 上述模式仍然可以用作后备。

一个不错的焦点管理的例子是react-aria-modal。这是个相对罕见的完全可访问的模态窗口的例子。不仅将初始焦点设在取消按钮(阻止用户意外地激活成功操作)和在模态对话框内记录键盘焦点,其还重置焦点回到最初触发对话框的元素上。

注意:

尽管这对于可访问性特性非常重要,其也应该审慎地应用。当被中断时使用其来修复键盘的焦点,而不是尝试和期望用户如何使用应用。

鼠标和指针事件

确保仅使用键盘也可以访问通过鼠标或指针事件公开的所有功能。仅依赖于指针设备将会导致许多键盘用户无法使用您的应用程序的情况。

为了说明这一点,让我们看一个关于点击事件导致的可访问性破坏的例子。这是外部点击模式,用户可以通过单击元素外部来禁用打开的 popover(弹出框) 。

A toggle button opening a popover list implemented with the click outside pattern and operated with a mouse showing that the close action works.

这通常通过将一个 click 事件附加到 window 对象,来关闭 popover(弹出框) :

class OuterClickExample extends React.Component {
constructor(props) {
    super(props);

    this.state = { isOpen: false };
    this.toggleContainer = React.createRef();

    this.onClickHandler = this.onClickHandler.bind(this);
    this.onClickOutsideHandler = this.onClickOutsideHandler.bind(this);
  }

  componentDidMount() {
    window.addEventListener('click', this.onClickOutsideHandler);
  }

  componentWillUnmount() {
    window.removeEventListener('click', this.onClickOutsideHandler);
  }

  onClickHandler() {
    this.setState(currentState => ({
      isOpen: !currentState.isOpen
    }));
  }

  onClickOutsideHandler(event) {
    if (this.state.isOpen && !this.toggleContainer.current.contains(event.target)) {
      this.setState({ isOpen: false });
    }
  }

  render() {
    return (
      <div ref={this.toggleContainer}>
        <button onClick={this.onClickHandler}>Select an option</button>
        {this.state.isOpen ? (
          <ul>
            <li>Option 1</li>
            <li>Option 2</li>
            <li>Option 3</li>
          </ul>
        ) : null}
      </div>
    );
  }
}

对于使用指针设备的用户来说,比如使用鼠标,这可能会很好用,但是当 window 对象从来没有收到 click 事件时,只使用键盘操作时就会导致该功能不能正常使用。这可能会导致功能模糊,从而阻止用户使用您的应用程序。

A toggle button opening a popover list implemented with the click outside pattern and operated with the keyboard showing the popover not being closed on blur and it obscuring other screen elements.

通过使用适当的事件处理程序,例如 onBluronFocus ,可以实现相同的功能:

class BlurExample extends React.Component {
  constructor(props) {
    super(props);

    this.state = { isOpen: false };
    this.timeOutId = null;

    this.onClickHandler = this.onClickHandler.bind(this);
    this.onBlurHandler = this.onBlurHandler.bind(this);
    this.onFocusHandler = this.onFocusHandler.bind(this);
  }

  onClickHandler() {
    this.setState(currentState => ({
      isOpen: !currentState.isOpen
    }));
  }

  // We close the popover on the next tick by using setTimeout.
  // This is necessary because we need to first check if
  // another child of the element has received focus as
  // the blur event fires prior to the new focus event.
  onBlurHandler() {
    this.timeOutId = setTimeout(() => {
      this.setState({
        isOpen: false
      });
    });
  }

  // If a child receives focus, do not close the popover.
  onFocusHandler() {
    clearTimeout(this.timeOutId);
  }

  render() {
    // React assists us by bubbling the blur and
    // focus events to the parent.
    return (
      <div onBlur={this.onBlurHandler}
           onFocus={this.onFocusHandler}>
        <button onClick={this.onClickHandler}
                aria-haspopup="true"
                aria-expanded={this.state.isOpen}>
          Select an option
        </button>
        {this.state.isOpen ? (
          <ul>
            <li>Option 1</li>
            <li>Option 2</li>
            <li>Option 3</li>
          </ul>
        ) : null}
      </div>
    );
  }
}

这段代码将功能公开给指针设备和键盘用户。还要注意添加的 aria-* props 以支持屏幕阅读器用户。为简单起见,我们在这两个例子中尚未实现用于启用 popover(弹出框) 选项的 arrow key 交互的键盘事件。

A popover list correctly closing for both mouse and keyboard users.

在许多情况下,只依赖于指针和鼠标事件将破坏键盘用户的功能。始终使用键盘进行测试将立即突出显示问题区域,这些区域可以通过使用键盘感知事件处理程序来修复。

更为复杂的工具

更为复杂的用户体验并不意味着更少的可访问性特性。反之,可访问性能够通过将其尽可能编码到HTML中而非常容易实现,即使最为复杂的工具也能够进行可访问性编码。

这里我们要求具备 ARIA 角色 以及 ARIA 声明和属性的相关知识。这些工具箱涵盖了所有JSX支持的同时能够支持构建完整的可访问性高阶函数式的React组件的HTML特性。

每一类型的工具都具有特定的设计模式,并由用户和用户代理以某些方式生效:

其他要点的考量

设置语言

在页面中声明语言类型以让屏幕阅读器软件能够使用其来选择正确的发音设置:

设置文档标题

设置文档 <title> 以正确描述当前页面内容,因为这可以确保用户保持对当前页面上下文的了解:

我们可以使用 React Document Title组件 在React中进行设置。

颜色对比

确保页面上所有可读的文本都有丰富的颜色对比以让低视力用户能够最大程度的可读:

手动地计算页面上所有合适的颜色组合十分无趣,替代地,你可以通过Colorable来计算整个可访问性颜色

之前提到的aXe和WAVE也包含了颜色对比测试并会报告颜色对比错误。

若你想扩展你的对比测试能力,可以使用如下工具:

开发及测试工具

在创建可访问性网路应用时,有大量工具可以协助我们完成该工作。

键盘

目前最简单也是最重要的检查是通过键盘来整个页面是否达标和使用。做法如下:

  1. 拔掉鼠标。
  2. 使用TabShift+Tab切换到浏览器。
  3. 使用Enter激活元素。
  4. 当满足要求后,使用键盘的方向键与一些元素进行交互,例如菜单和下拉列表。

开发助手

我们可以在JSX代码里直接查看一些可访问性特性。通常在一些识别JSX语法的集成开发环境中(IDE)已经提供了为ARIA用户(roles),声明和属性的智能检查。我们也可采用如下工具:

eslint-plugin-jsx-a11y

基于ESLint的eslint-plugin-jsx-a11y 插件提供了在JSX代码中关于可访问性问题的抽象语法树检查反馈。大部分IDE能够直接在代码分析和源码窗口中直接集成这些发现。

Create React App包含了带有部分激活规则的这一插件。若想要支持更多的可访问性规则,你可以在项目的根目录下创建一个.eslintrc文件并包含如下内容:

{
  "extends": ["react-app", "plugin:jsx-a11y/recommended"],
  "plugins": ["jsx-a11y"]
}

浏览器的可访问性测试

在浏览器里已有大量工具能够在页面上运行可访问性审计。可以结合之前提到过的其他可访问性检查工具来使用他们,因为他们仅可以测试HTML中技术上的可访问性。

aXe, aXe-core and react-axe

双端系统为应用提供了自动化和端到端的可访问性测试aXe-core 。这一模块包含了Selenium的集成。

可访问性Engine 或 aXe,是一款基于aXe-core构建的可访问性检测器的浏览器插件。

你也可以在开发和调试环节,使用react-axe模块在控制台中直接报告可访问性问题。

WebAIM WAVE

Web Accessibility Evaluation Tool 是另外一个可访问性浏览器插件。

Accessibility inspectors and the Accessibility Tree

可访问树(The Accessibility Tree) 是一个DOM结构的子集,其包含每个应暴露给辅助性工具,如屏幕阅读器等DOM元素的可访问性对象。

在一些浏览器我们可以在可访问树中轻松地访问每个元素的可访问性信息:

屏幕阅读器

结合屏幕阅读器进行测试应构成可访问性测试的一部分。

注意浏览器 / 屏幕阅读器的结合。建议在浏览器中选择最适合的屏幕阅读器测试应用程序。

常用的屏幕阅读器

FireFox下的NVDA

NonVisual Desktop Access 或 NVDA是一款广泛使用的开源的窗口屏幕阅读器。

关于如何更好使用NVDA参考如下指南:

Safari下的VoiceOver

VoiceOver是一款集成在苹果设备的屏幕阅读器。

参考以下指南了解关于如何集合和使用VoiceOver:

Internet Explorer下的JAWS

Job Access With Speech or JAWS,是一款在Windows平台广泛使用的屏幕阅读器。

参考以下链接了解关于更好使用JAWS

其他屏幕阅读器

Google Chrome 中的 ChromeVox

ChromeVox 是Chromebook上的集成屏幕阅读器,可作为 Google Chrome 的一个 扩展程序

ChromeVox is an integrated screen reader on Chromebooks and is available as an extension for Google Chrome.

请参阅以下关于如何最佳使用ChromeVox的指南: