Mercari Engineering Blog

We're the software engineers behind Mercari. Check out our blog to see the tech that powers our marketplace.

React-axe で React アプリケーションのアクセシビリティを向上させる

React-axe で React アプリケーションのアクセシビリティを向上させる

こんにちは、この 4 月にメルカリに新卒入社したフロントエンドエンジニアの @karszawa です。

この頃は Google I/O 2019 のキーノートでアクセシビリティが大きく取り上げられたり、Safari に Audit タブが追加されアクセシビリティに関する様々なテストできるようになったりと、フロントエンド界隈におけるアクセシビリティへの関心の高まりを感じます。

本記事では React アプリケーションのアクセシビリティをチェックするためのライブラリである React-axe と、その中心技術である axe-core を応用した様々なツールをご紹介します

React-axe とは

React-axe は React アプリケーションのアクセシビリティをチェックするためのツールです。チェックの結果は Chrome DevTools に表示され、開発中にアクセシビリティの問題に気づけます。React-axe は Deque Systems, Inc. によって開発されている axe-core をアクセシビリティ確認の仕組みとして使用しています。チェックできるアクセシビリティの一覧は axe-core のリポジトリ から確認できます。

導入方法は簡単で、次のように axeReactReactDOM を与えてあげるだけです1。開発環境でのみ結果を確認したいので dynamic import でライブラリをインポートしています。

if (process.env.NODE_ENV !== "production") {
  import("react-axe").then(axe => {
    axe.default(React, ReactDOM, 1000);
    ReactDOM.render(<App />, document.getElementById("root"));
  });
} else {
  ReactDOM.render(<App />, document.getElementById("root"));
}

今回は WebView で実装されているメルカリの取引ページ(下の画像のような画面)を React-axe を使用して確認してみます。

"取引画面上部のスクリーンショット""取引画面下部のスクリーンショット"
取引画面のスクリーンショット

コードを変更し、サーバーを立ち上げ、コンソールを見てみると次のようなレポートが出力されていました。

f:id:karszawa:20190617114948p:plain
Chrome DevTools にいくつかの問題が報告されています

レポートはルール別に分けられ、問題のインパクトに応じて critical / serious /moderate / minor とラベル付されています。

今回は 5 つの問題が報告されたのでそれぞれ次のように修正を試みました。

1. critical: Form elements must have labels

問題の詳細は こちら で確認できます。コメントを入力する textarea 要素に対応するラベルがなく、また aria-label も指定されていなかったことが問題のようです。この問題を解決するには、入力要素に対してラベルが存在するならラベルに aria-labelledby を設定し、ないならば入力要素に aria-label を設定する必要があります2。今回のコメントエリアの場合は、過去のコメントの列とコメントエリアのプレースホルダーが視覚的なわかりやすさを担保していると考え、見た目の変化は加えず textarea に aria-label を設定することにします(下記)3

<textarea
  value={this.state.message}
  onChange={this.handleMessageChange}
  placeholder="何か分からないことがあれば質問してみましょう"
  aria-label="コメントを入力する"
/>

2. critical: Zooming and scaling must not be disabled

問題の詳細は こちら です。この問題は meta タグでズームを禁止しているときに報告されます。ブラウザでページを閲覧する場合は、テキストの文字サイズが小さいときにページを拡大したいのでズームを禁止してはいけません4。しかしながら、今回対象としているページは モバイルアプリの WebView で表示される ので、UX的には拡大縮小はできないというネイティブの挙動と一致させたいです。アクセシビリティ上の懸念はありますが、UXの統一性を重視してズームは禁止することにしました。そういった場合は単にこの報告を無視してもいいですが、React-axe の axe 関数にオプションを渡すことでルールを無効化できます(下記)。

axe.default(React, ReactDOM, 1000, {
  rules: [{ id: "meta-viewport", enabled: false }]
});

3. serious: Elements must have sufficient color contrast

問題の詳細は こちら です。こちらの問題は、ページの中のテキストの色が背景色に比べて薄い(コントラストが足りない)場合に報告されます。この問題は CSS で指定する色を変更することで解決できます。色の変更は操作としては簡単ですが、デザインガイドラインを守れなくなってしまうこともあるため、デザイナーとの協力は必須です。Google Chrome のインスペクタでは下の画像のようにカラーコードの左側に表示されている四角をクリックすることでインタラクティブに色を変更することが可能です。ここでは背景色と比べた文字色のコントラストレベルも表示されています。できるだけ AA 以上のレベルでコントラストを設定したいです。

f:id:karszawa:20190617115133p:plain
Chrome インスペクタで確認するコントラストレベルの例

4. moderate: Page must contain a level-one heading

問題の詳細は こちら です。この問題はページの中に h1 要素が含まれていないときに報告されます。このとき、h1 要素は他の heading 要素よりも前で使用される必要があり、コンテンツ全体と比べても初めの方に出してあげる必要があります。スクリーンリーダーを使ってページを読むユーザーは、見出し要素にジャンプするキーボードショートカットを使ってページを読みます。 そのショートカットは h1 要素がないとき h2 要素や h3 要素にジャンプしますが、ユーザーは h1 にジャンプしたつもりなので認識に齟齬が生じます。修正自体は h1 要素を指定してあげるだけなので簡単です。

- <div>{title}</div>
+ <h1>{title}</h1>

5. moderate: All page content must be contained by landmarks

問題の詳細は こちら です。この問題は HTML 中のすべてのコンテンツがランドマーク要素の下に配置されていない場合に報告されます。ランドマーク要素とは header 要素や main 要素などのことで、ページにセマンティクスを持たせるために使われます5。ランドマーク要素はスクリーンリーダーを使うユーザーにとってページのナビゲーションにとても役に立ちます。そのため、すべての要素はランドマークの下に配置される必要があります。

この問題を解決するには基本的にはマークアップを適切に行うだけで良いのですが、今回の場合は webpack の svg-splite-loader によって body 要素の直下に svg 要素が配置されていることが問題でした。svg-splite-loader はページで使用する svg を一つにまとめてロードするようにしてくれるライブラリです。ライブラリの制約上、要素を挿入する場所は変えられないようだったので、今回はトップレベルの svg 要素に 下記のように aria-hidden="true" を指定することで解決することにしました。aria-hiddentrue に設定された要素はアクセシビリティ的には不可視の要素となり、スクリーンリーダーのユーザーからは存在がわからなくなります。今回の場合は、通常の方法でページを閲覧するユーザーにさえ要素の存在は認知されないのでこの対処方法で構いません。

<svg aria-hidden="true">
  ...
</svg>

eslint-plugin-jsx-a11y で要素の修正を矯正する

eslint-plugin-jsx-a11y は axe とは別の仕組みで React アプリケーションのアクセシビリティをチェックしてくれます。 eslinteslint-plugin-jsx-a11y を入れたら .eslintrc を次のように変更しプラグインを有効化します。

npm install eslint --save-dev
npm install eslint-plugin-jsx-a11y --save-dev
{
  "extends": [
    "plugin:jsx-a11y/strict"
  ],
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true,
      "modules": true
    }
  }
}

vscode でソースコードを表示してみると下図のようにエラーが表示されます。

f:id:karszawa:20190617115205p:plain
eslint-a11y-plugin を導入した vscode でエラーが出ている

この問題は label 要素が input 要素に紐付いていないため発生しています。

ちなみに上記の問題を修正すると次のようになります。

import React from "react";

const CommentBox = () => (
  <form>
    <label id="comment-label" htmlFor="comment-input">
      コメント <input id="comment-input" type="text" />
    </label>
  </form>
);

export default CommentBox;

実際にレンダリングが行われているわけではないので、先程紹介したランドマークに関する警告や文字色と背景色のコントラストに関する警告は表示されません。確認できる項目は限定的ですが編集中に確認できるのは便利です。

jest-axe

jest-axe で Jest から axe を利用することもできます。アクセシビリティのチェックを自動テストに組み込めば、いちいちブラウザで要素をレンダリングしてコンソールをチェックする必要もなくなるのでとても便利です。

設定は簡単です。jest-axe を npm install して下記のように利用します。

npm install --save-dev jest-axe
// eslint-plugin-jsx-a11y の説明で使用した CommentBox コンポーネントを使用する
import React from "react";
import ReactDOMServer from "react-dom/server";
import CommentBox from "./CommentBox";
const { axe, toHaveNoViolations } = require("jest-axe");

expect.extend(toHaveNoViolations);

it("renders without any violations", async () => {
  const html = ReactDOMServer.renderToString(<CommentBox />);
  const results = await axe(html);
  expect(results).toHaveNoViolations();
});

このテストを実行すると次のような結果が出てテストが失敗してくれます。

FAIL  src/CommentBox.test.jsx
● renders without any violations

  expect(received).toHaveNoViolations(expected)

  Expected the HTML found at $('#comment-input') to have no violations:

  <input type="text" id="comment-input">

  Received:

  "Form elements must have labels (label)"

  Try fixing it with this help: https://dequeuniversity.com/rules/axe/3.1/label?application=axeAPI

     9 |   const html = ReactDOMServer.renderToString(<CommentBox />);
    10 |   const results = await axe(html);
  > 11 |   expect(results).toHaveNoViolations();
       |                   ^
    12 | });
    13 |

    at Object.toHaveNoViolations (src/CommentBox.test.jsx:11:19)

まとめ

以上のようにして、React-axe でブラウザコンソール上と Jest でアクセシビリティのチェックを行うことができるようになり、eslint のプラグインによってコーディング中でも簡単な問題になら気づくことができるようになりました。

しかし、イギリス政府のアクセシビリティチーム(GDS: Global Digital Service)の報告によれば、自動テストによって確認できるアクセシビリティ上の問題は、アクセシビリティに関するすべての問題のうちたった 30%でしかないそうです6。実際、Inside Frontend で CyberAgent の原一成さんとときまりさんから発表のあった Web App Checklist 7 のアクセシビリティの項目のうち axe でチェックできるのは半分ほどです。このことから、アクセシビリティを遵守したアプリケーションを作る際に本当に重要なのは次の 3 点ではないでしょうか。

  1. React-axe などのツールで確認できるのは最低限のアクセシビリティルールだけだと認識する
  2. 実際にユーザーに使われるスクリーンリーダーなどを使って確認する
  3. 開発工程にインクルーシブデザインを取り入れる

最後に

今回の検証で明らかになったとおり、メルカリのアプリケーションにはまだまだアクセシビリティ的には未熟な点があります。しかしながら、わたしたちはすべてのお客様が安心・満足してサービスを使っていただけるよう改善の努力をしています。メルカリではアクセシビリティの向上に熱い情熱を持ったフロントエンドエンジニアを常に募集しています。ご興味を持たれた方はぜひご応募ください。

mercari.workable.com

ちなみにサマーインターンシップの応募も始まっています。興味を持った学生の方はぜひご応募ください。

mercari.workable.com


  1. axe 関数の中で React.createElement を変更し、コンポーネント作成時にそのコンポーネントをリストに登録・アクセシビリティのチェックを行っているようです。

  2. Using the aria-label attribute - MDN web docs

  3. 実際のコンポーネントはもう少し複雑です

  4. ちなみに WebView ではなく、Safari で直接 user-scalable=no を指定したページを開いてもズームは有効になっています。アクセシビリティ上の理由で禁止はできないそうです。New Interaction Behaviors in iOS 10 | WebKit

  5. 代わりに Role を設定しても良いです。

  6. What we found when we tested tools on the world’s least-accessible webpage - Accessibility in government

  7. CyberAgent の Web アプリケーション開発のレベルの高さを感じる素晴らしい発表でした。実は、このときに(匿名で)質問させてもらった「アクセシビリティをチェックするために使っているツールはあるか」という質問が今回の記事を書くに至った出発点となっています。