Accessibility

Tại sao là Accessibility?

Web accessibility (cũng được đề cập ở đây a11y) là sự thiết kế và tạo dựng website sao cho tất cả mọi người đều có thể sử dụng được. Sự hỗ trợ của Accessibility cho phép các công nghệ hỗ trợ có thể sử dụng trong các trang web.

React hỗ trợ đầy đủ để bạn thực hiện được điều này, bằng cách sử dụng các kỹ thuật HTML chuẩn.

Tiêu chuẩn và Hướng dẫn

WCAG

Hướng dẫn Web Content Accessibility cung cấp hướng dẫn để bạn tạo được những trang web có khả năng truy cập.

Danh sách WCAG sau đây cho bạn một cái nhìn tổng quan:

WAI-ARIA

Sáng kiến Web Accessibility - Truy cập trang web giàu ứng dụng tài liệu có chứa nhiều kỹ thuật để bạn có thể xây dựng các JavaScript widgets có thể truy cập.

Chú ý rằng tất cả thuộc tính HTML aria-* đều được hỗ trợ đầy đủ trong JSX. Trong khi hầu hết DOM properties và thuộc tính trong React là camelCased, những thuộc tính đó nên được gạch-nối (hay còn gọi là kebab-case, lisp-case, etc) giống như trong HTML thuần:

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

Tính ngữ nghĩa của HTML

Tính ngữ nghĩa của HTML là nền tảng của accessibility trong một ứng dụng web. Sử dụng một số HTML elements để gia cố thêm về mặt ý nghĩa của thông tin trong những websites của chúng ta sẽ thường mang lại accessibility miễn phí.

Đôi khi chúng ta phá vỡ tính ngữ nghĩa của HTML khi thêm thẻ <div> vào JSX để giúp nó hoạt động, đặc biệt khi làm việc với lists (<ol>, <ul><dl>) và HTML <table>. Trong những tình huống đó chúng ta nên sử dụng React Fragments để nhóm nhiều elements lại với nhau

Ví dụ,

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>
  );
}

Bạn có thể map một items collection vào một mảng fragments array như cách bạn làm với bất kỳ loại element nào khác:

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>
  );
}

Khi bạn không cần bất kỳ props nào trong thẻ Fragment, bạn có thể sử dụng cú pháp rút gọn, nếu công cụ của bạn có hỗ trợ:

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

Tham khảo thêm, truy cập Tài liệu về Fragments.

Accessible Forms

Labeling

Mỗi HTML form control, như <input><textarea>, cần được dán nhãn accessibly. Chúng ta cần cung cấp những labels có tính mô tả, nó sẽ giúp tiếp cận tốt hơn đến người đọc.

Những nguồn sau đây cho chúng ta thấy cách thực hiện điều đó:

Mặc dù HTML tiêu chuẩn có thể sử dụng được trong React, tuy nhiên chú ý rằng thuộc tính for phải được viết thành htmlFor trong JSX:

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

Thông báo lỗi đến người dùng

User luôn cần được cung cấp thông tin về các tình huống lỗi. Đường dẫn sau đây cho chúng ta thấy cách đưa text error đến người dùng:

Focus Control

Hãy chắc chắn rằng ứng dụng web của bạn có thể hoạt động tốt khi chỉ cần sử dụng bàn phím:

Keyboard focus and focus outline

Keyboard focus là việc hiển thị element hiện tại trong DOM đang được select bằng cách sử dụng bàn phím. Chúng ta thấy chúng ở mọi nơi ví dụ như hình dưới đây:

Blue keyboard focus outline around a selected link.

Chỉ khi sử dụng CSS mới có thể remove outline này, ví dụ như chỉnh outline: 0,nếu bạn muốn thay thế nó với một focus outline khác.

Cơ chế skip đến nội dung mong muốn

Bạn nên cung cấp một cơ chế nào đó cho phép users bỏ qua điều hướng trước đó trong ứng dụng của bạn, cũng là để tăng tốc điều hướng từ bàn phím.

Skiplinks hay Skip Navigation Links là những link điều hướng ẩn chỉ hiển thị khi bàn phím của người dùng tương tác đến trang web. Chúng rất dễ implement với internal page anchors và một ít styling:

Cũng sử dụng landmark elements và roles, như <main><aside>, để phân vùng trang web như công nghệ hỗ trợ cho phép người dùng điều hướng những sections đó một cách nhanh chóng.

Đọc thêm về cách sử dụng những elements đó để tăng cường accessibility ở đây:

Quản lý focus theo kế hoạch

Ứng dụng React của chúng ta liên tục điều chỉnh HTML DOM trong runtime, vì vậy đôi khi bàn phím khiến chúng ta mất focus hay bị set vào một element không ngờ tới. Để khắc phục vấn đề này, chúng ta cần lập trình để focus bằng bàn phím đúng hướng. Ví dụ, bằng cách reset keyboard focus vào một button đã mở một cửa sổ modal sau khi cửa sổ modal đó đã được đóng.

Tài liệu từ trang web MDN phân tích và mô tả cách chúng ta xây dưng keyboard-navigable JavaScript widgets.

Để set focus trong React, chúng ta có thể sử dụng Refs to DOM elements.

Sử dụng nó, chúng ta đầu tiên tạo một ref đến một element trong JSX của một component class:

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // Tạo một ref để lưu textInput của DOM element
    this.textInput = React.createRef();
  }
  render() {
  // Sử dụng `ref` callback để lưu một reference đến text input trong DOM
  // element trong một instance field (ví dụ, this.textInput).
    return (
      <input
        type="text"
        ref={this.textInput}
      />
    );
  }
}

Sau đó chúng ta có thể focus nó ở nơi khác trong component khi cần:

focus() {
  // Focus đoạn tẽt input một cách rõ ràng bằng cách sử dụng DOM API nguyên bản
  // Note: chúng ta đang truy cập "current" để lấy the DOM node
  this.textInput.current.focus();
}

Đôi khi một component cha cần được set focus vào một element trong component con. Chúng ta có thể thực hiện bằng cách exposing DOM refs to parent components thông qua một prop đặc biệt ở component con để chuyển tiếp ref của component cha đến DOM node của component con.

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} />
    );
  }
}

// Bây giờ bạn có thể set focus khi cần thiết.
this.inputElement.current.focus();

Khi sử dụng HOC để mở rộng components, chuyển tiếp ref được khuyên dùng để bao bọc component sử dụng forwardRef function của React. Nếu một third party HOC không implement ref forwarding, pattern bên trên vẫn có thể sử dụng như một fallback.

Một ví dụ về cách quản lý focus tốt là react-aria-modal. Đây là một ví dụ tương đối hiếm hoi nói về một cửa sổ modal có thể truy cập hoàn toàn. Nó không chỉ set focus ban đầu vào nút cancel (ngăn chặn người dùng không vô tình dùng bàn phím kích hoạt success action) và khóa focus từ bàn phím vào bên trong modal, nó cũng reset focus về lại element đã kích hoạt modal đó lúc ban đầu.

Chú ý:

Đây là một accessibility feature rất quan trọng, nó cũng là một kỹ thuật nên được sử dụng một cách không ngoan. Dùng nó để sửa chữa keyboard focus flow khi bị xáo trộn, không nên cố gắng dự đoán cách người dùng sử dụng ứng dụng.

Sự kiện Chuột và con trỏ

Hãy chắc chắn rằng tất cả chức năng được sử dụng thông qua con trỏ chuột hoặc pointer event cũng có thể cũng được truy cập chỉ bằng cách sử dụng bàn phím. Chỉ dựa vào thiết bị pointer sẽ dấn đến nhiều trường hợp mà bàn phím của người dùng không thể sử dụng ứng dụng của bạn.

Để minh họa vấn đề này, hãy nhìn vào một ví dụ liên quan đến việc accessibility bị vỡ do click events. Bên ngoài click pattern, nơi một user có thể vô hiệu hóa một popover đã đươc mở bằng cách click bên ngoài element.

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

Đây là một cách implement thường thấy bằng cách gán một sự kiện click vào windows object mà nó dùng để đóng 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>
        )}
      </div>
    );
  }
}

Điều này có thể hoạt động tốt cho người dùng với những thiết bị pointer, như chuột, nhưng thao tác nó với chỉ bàn phím sẽ khiến chức năng bị hư hỏng khi tab sang element tiếp theo window object không bao giờ nhận một sự kiện click.. Điều này có thể dẫn tới chức năng bị vô nghĩa khiến user không thể sử dụng ứng dụng của bạn.

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.

Chúng ta cũng có thể đạt được chức năng tương tự bằng cách sử dụng những event handlers thích hợp, như 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
    }));
  }

  // Chúng ta đóng popover trong tick tiếp theo bằng setTimeout.
  // Điều này là cần thiết bởi vì chúng ta cần kiểm tra trước xem nếu
  // con của element khác có nhận được focus như
  // blur event kích hoạt trước focus event mới.
  onBlurHandler() {
    this.timeOutId = setTimeout(() => {
      this.setState({
        isOpen: false
      });
    });
  }

  // Nếu một component con nhận được focus, không được đóng popover.
  onFocusHandler() {
    clearTimeout(this.timeOutId);
  }

  render() {
    // React hỗ trợ chúng ta bằng cách bubbling blur và
    // những sự kiện focus vào component cha.
    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>
        )}
      </div>
    );
  }
}

Đoạn code cho thấy chức năng của cả con trỏ và bàn phím của người dùng. Cũng lưu ý rằng thêm thuộc tính aria-* vào để hỗ trợ người dùng. Đơn giãn hơn là để sự kiện bàn phím cho phép arrow key tương tác với tùy chọn popover chưa được implement.

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

Đây là một ví dụ điển hình khi chỉ phụ thuộc vào con trỏ và sự kiện từ chuột sẽ làm hỏng chức năng cho người dùng bàn phím. Luôn luôn test với bàn phím sẽ ngay lập tức phát hiện được những khu vực có vấn đề, sau đó có thể sửa bằng cách dùng những handler để nhận input từ bàn phím.

Những Widgets phức tạp hơn

Trải nghiệm người dùng phức tạp không nên khiến mức độ accessibilty bị giảm đi. Trong khi đó accessibility dễ đạt được nhất bằng cách code sát với HTML nhất có thể, ngay cả với widget phức tạp nhất.

Ở đây chúng ta cần kiến thức từ ARIA Roles cũng như ARIA States and Properties. Đây là những công cụ có sẵn những thuộc tính HTML đã được hỗ trợ đầy đủ trong JSX và cho phép chúng ta xây dựng một trang web accessibility đầy đủ, những React component có chức năng cao cấp.

Môi loại widget có một design pattern riêng và đáp ứng chức năng nhất định bởi người dùng như:

Những điểm khác cần lưu ý

Cài đặt ngôn ngữ

Biểu thị ngôn ngữ của những đoạn văn bản giúp phần mềm cài đặt đúng loại voice:

Cài đặt title cho document

Set <title> cho đoạn văn bản để mô tả nội dung của trang hiện tại, điều này giúp chắc chắn rằng người dùng nắm được nội dung mà họ đang đọc:

Chúng ta có thể set nó trong React bằng cách sử dụng React Document Title Component.

Độ tương phản màu sắc

Hãy chắc chắn rằng tất cả đoạn text trong website của bạn có đủ độ tương phản màu sắc nhằm duy trì tối đa khả năng đọc của người dùng trong điều kiện thị lực kém:

Thật tẻ nhạt khi phải tính toán màu sắc thủ công cho tất cả trường hợp trong website của bạn, thay vào đó, bạn có thể tính toán tất cả màu sắc bằng Colorable.

Cả 2 công cụ aXe và WAVE được đề cập bên dưới đều bao gồm bộ tests kiểm tra độ tương phản màu sắc, chúng sẽ báo cáo những lỗi liên quan.

Nếu bạn muốn mở rộng khả năng kiểm tra độ tương phản, bạn có thể sử dụng những công cụ dưới đây:

Các công cụ để phát triển và kiểm tra

Có rất nhiều công cụ chúng ta có thể sử dụng để hỗ trợ trong việc tạo dựng accessibility của ứng dụng web.

Bàn phím

Cho đến thời điểm hiện tại, cách dễ nhất và cũng là một trong những điều quan trọng nhất là kiểm tra toàn bộ trang web của bạn có thể tương tác và sử dụng được chỉ bằng bàn phím hay không. Chúng ta thực hiện điều này bằng cách:

  1. Tháo chuột của bạn ra khỏi máy tính.
  2. Sử dụng TabShift+Tab để duyệt web.
  3. Sử dụng Enter để tương tác với những phần tử trong trang web.
  4. Ở những nơi yêu cầu, sử dụng phím mũi tên để tương tác với một số phần tử, như menu và dropdown.

Sự hỗ trợ phát triển

Chúng ta có thể kiểm tra một số chức năng accessibility trực tiếp trong code JSX. Thường thì bộ kiểm tra intellisense sẽ được cung cấp sẵn trong IDE cho những vai trò ARIA, states và properties. Chúng ta cũng có thể truy cập bằng những công cụ dưới đây:

eslint-plugin-jsx-a11y

eslint-plugin-jsx-a11y plugin cho ESLint cung cấp AST phản hồi AST về những vấn đề liên quan đến accessibility trong JSX của bạn. Nhiều IDE’s cho phép bạn tích hợp trực tiếp vào code analysis và source code windows.

Create React App plugin này với một tập hợp về những quy tắc kích hoạt. Nếu bạn muốn cho phép quy tắc accessibility hơn nữa, bạn có thể tạo một .eslintrc file trong root của project bằng nội dung sau đây:

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

Kiểm tra accessibility trong trình duyệt

Một số công cụ có thể chạy audit accessibility tren trang web trong trình duyệt của bạn. Hãy dùng chúng phối hợp với những công cụ kiểm tra đã được nhắc tới ở đây, bởi vì chúng chỉ có thể kiểm tra accessibility về mặt kỹ thuật.

aXe, aXe-core và react-axe

Hệ thống Deque cho phép aXe-core tự động kiểm tra end-to-end ứng dụng của bạn. Module này bao gồm những sự tích hợp cho Selenium.

The Accessibility Engine hoặc aXe, là một extension accessibility inspector tích hợp sẵn trong aXe-core.

Bạn cũng có thể sử dụng react-axe module để report những phát hiện về accessibility trực tiếp khi đang develope và debug.

WebAIM WAVE

Web Accessibility Evaluation Tool là một extension khác.

Accessibility inspectors và the Accessibility Tree

The Accessibility Tree là một tập hợp DOM trê có chứa những accessible object cho từng DOM element mà chúng nên được cung cấp cho công nghệ hiện đại như screen readers.

Ở một số trình duyệt, chúng ta có thể dễ dàng xem thông tin về accessibility cho từng element trong accessibility tree:

Screen readers

Testing với một screen reader nên được xem như một phần của quá trình kiểm tra accessibility.

Vui lòng lưu ý rằng sự kết hợp browser / screen reader là quan trọng. Bạn nên test ứng dụng của mình bằng trình duyệt phù hợp nhất cho screen reader của bạn.

Các Screen Readers thông dụng

NVDA trong Firefox

NonVisual Desktop Access hoặc NVDA là một Windows screen reader mã nguồn mở được sử dụng rộng rãi.

Xem hướng dẫn bên dưới để biết cách dùng NVDA hiệu quả nhất:

VoiceOver trong Safari

VoiceOver là một screen reader được tích hợp sẵn trong những thiết bị của Apple.

Xem hướng dẫn sau đây để biết cách kích hoạt và sử dụng VoiceOver:

JAWS trong Internet Explorer

Job Access With Speech hoặc JAWS, là một screen reader được dùng hiệu quả trên Windows.

Xem hướng dẫn bên dưới để biết cách dùng JAWS hiệu quả nhất:

Screen Readers khác

ChromeVox trong Google Chrome

ChromeVox ilà một screen reader được tích hợp sẵn trên Chromebooks và được xem như một extension cho Google Chrome.

Xem hướng dẫn bên dưới để biết cách dùng ChromeVox hiệu quả nhất: