❤️

テストから始めるWebアクセシビリティ

2024/10/10に公開

自己紹介

すずきゆーだい(@szkyudi)です。株式会社ゆめみという会社でWebフロントエンドエンジニアをやっています。秋になると柿を200個食べます。

アクセシビリティは誰のためにあるか

アクセシビリティは特定の人々だけでなく、あらゆる人のためにあるものです。 というような言葉はよく耳にすると思います。ただ、この言葉にピンときていない方々も多いと思いますし、私自身、まだまだ理解を深めていく必要があると感じています。

そこで、この記事では、エンジニア自身がアクセシビリティ向上の恩恵を受ける当事者であることを認識してもらうとともに、少しでもアクセシビリティに興味を持ってもらうために、テストから始めるアクセシビリティについて紹介したいと思います。

Webフロントエンドにおけるテスト

Webフロントエンドにおけるテストには様々な種類がありますが、今回はその中でもUIコンポーネントのテストにおけるアクセシビリティについて紹介します。

例として用いる技術とコンポーネント

今回は、例としてReact Testing Libraryを用いて紹介いたします。他の技術でも概ね同じような考え方ができると思うので、その場合は適宜読み替えていただけると幸いです。

また、例として以下のように選択中の項目が非活性になる Pulldown コンポーネントを用いて紹介いたします。

選択中の項目が非活性になるプルダウンのサンプル。現地参加、オンライン参加、不参加の3択。

コードと挙動を合わせて確認できるStackblitz Projectはこちら

アクセシビリティを考慮していない例

この例では、単純な button 要素だけを用いた Pulldown コンポーネントを紹介します。

コンポーネントのコード

function Pulldown() {
  const [isOpen, setIsOpen] = useState(false);
  const [selected, setSelected] = useState('不参加');

  const toggleOpen = () => setIsOpen(!isOpen);

  // 選択されたボタンのテキストをStateに格納してメニューを閉じる
  const handleSelect = (e) => {
    setSelected(e.target.textContent)
    setIsOpen(false);
  };

  return (
    <div>
      {/* プルダンを開くためのボタン */}
      <button onClick={toggleOpen}>{selected}</button>

      {/* 選択項目の一覧メニュー */}
      {isOpen && (
        <ul>
          <li>
            <button
              onClick={handleSelect}
              disabled={selected === "現地参加"}
            >
              現地参加
            </button>
          </li>
          <li>
            <button
              onClick={handleSelect}
              disabled={selected === "オンライン参加"}
            >
              オンライン参加
            </button>
          </li>
          <li>
            <button
              onClick={handleSelect}
              disabled={selected === "不参加"}
            >
              不参加
            </button>
          </li>
        </ul>
      )}
    </div>
  );
};

テストコード

test('選択されている項目がdisabledになっていること', () => {
  // プルダウンを描画する
  render(<Pulldown />);

  // プルダウンを開くためのボタンをクリックする
  screen.getByRole('button', { name: '不参加' }).click();

  // ここで role="button" の「不参加」ボタンが2つ存在するため、エラーとなってしまう
  expect(screen.getByRole('button', { name: '不参加' })).toBeDisabled();
});

解説

開いた状態のプルダウンにrole="button"かつ不参加のボタンが2つある
このプルダウンは、選択されている項目がプルダウンを開くためのボタンのテキストとして表示されています。そのため、「不参加」ボタンが2つ存在する状況が発生してしまいます。

これのテストする正常に実行するためには意図的に2つ目の要素を取得したりする必要があります。
これにより、以下のような問題が発生してしまいます。

  • 対象の「不参加」ボタンにアクセスしづらくなり、テストがしづらくなる
  • コードからテストの意図が読みとりづらくなり、テストの目的が不明瞭になる

アクセシビリティを考慮した例(不完全)

この例では、特定の要素に role 属性を付与することで、各要素を識別しやすくした Pulldown コンポーネントを紹介します。

コンポーネントのコード

function Pulldown() {
  const [isOpen, setIsOpen] = useState(false);
  const [selected, setSelected] = useState('不参加');

  const toggleOpen = () => setIsOpen(!isOpen);

  // 選択されたボタンのテキストをStateに格納してメニューを閉じる
  const handleSelect = (e) => {
    setSelected(e.target.textContent)
    setIsOpen(false);
  };

  return (
    <div>
      {/* プルダンを開くためのボタン */}
      <button onClick={toggleOpen}>{selected}</button>

      {/* 選択項目の一覧メニュー */}
      {isOpen && (
-       <ul>
+       <ul role="listbox">
          <li>
            <button
+             role="option"
              onClick={handleSelect}
              disabled={selected === "現地参加"}
            >
              現地参加
            </button>
          </li>
          <li>
            <button
+             role="option"
              onClick={handleSelect}
              disabled={selected === "オンライン参加"}
            >
              オンライン参加
            </button>
          </li>
          <li>
            <button
+             role="option"
              onClick={handleSelect}
              disabled={selected === "不参加"}
            >
              不参加
            </button>
          </li>
        </ul>
      )}
    </div>
  );
}

テストコード

test('選択されている項目がdisabledになっていること', () => {
  // プルダウンを描画する
  render(<Pulldown />);

  // プルダウンを開くためのボタンをクリックする
  screen.getByRole('button', { name: '不参加' }).click();

  // ここでrole="option"の「不参加」ボタンが1つになるため、正しくテストができる
- expect(screen.getByRole('button', { name: '不参加' })).toBeDisabled();
+ expect(screen.getByRole('option', { name: '不参加' })).toBeDisabled();
});

解説

role="listbox"とrole="option"の不参加ボタンが1つずつあるプルダウン
このプルダウンでは、メニューにrole="listbox"、選択肢にrole="option"を付与したため、選択肢を開くためのボタンと選択肢を別の要素として認識することができます。
これにより、以下の改善がされました。

  • 対象の「不参加」ボタンにアクセスしやすくなり、テストがしやすくなった
  • このテストがどの「不参加」ボタンを対象としているかがわかりやすくなった

注意

前述の例の冒頭に「不完全」とあるように、このプルダウンではまだまだ考慮できていないアクセシビリティ上の問題が複数あります。そこで、さらに改善した例を以下に紹介いたします。

さらにアクセシビリティを考慮した例(不完全)

この例では、特定の要素にさまざまな属性を付与することで、各要素を識別しやすくするとともに、コンポーネントの状態を管理できるようになった Pulldown コンポーネントを紹介します。

コンポーネントのコード

function Pulldown() {
  const [isOpen, setIsOpen] = useState(false);
  const [selected, setSelected] = useState('不参加');

  const toggleOpen = () => setIsOpen(!isOpen);
  const handleSelect = (e) => {
    setSelected(e.target.textContent)
    setIsOpen(false);
  };

  return (
    <div>
      {/* プルダンを開くためのボタン */}
-     <button onClick={toggleOpen}>{selected}</button>
+     <button
+       onClick={toggleOpen}
+       aria-haspopup="listbox"
+       aria-expanded={isOpen}
+       id="participation-button"
+     >
+       {selected}
+     </button>

      {/* 選択項目の一覧メニュー */}
      {isOpen && (
-       <ul role="listbox">
+       <ul role="listbox" aria-labelledby="participation-button">
          <li>
            <button
              role="option"
              onClick={handleSelect}
+             aria-selected={selected === "現地参加"}
              disabled={selected === "現地参加"}
            >
              現地参加
            </button>
          </li>
          <li>
            <button
              role="option"
              onClick={handleSelect}
+             aria-selected={selected === "オンライン参加"}
              disabled={selected === "オンライン参加"}
            >
              オンライン参加
            </button>
          </li>
          <li>
            <button
              role="option"
              onClick={handleSelect}
+             aria-selected={selected === "不参加"}
              disabled={selected === "不参加"}
            >
              不参加
            </button>
          </li>
        </ul>
      )}
    </div>
  );
};

テストコード

test('選択されている項目がdisabledになっていること', () => {
  ...
});

test('選択後にメニューが非表示になること', () => {
  // プルダウンを描画する
  render(<Pulldown />);

  // プルダウンを開くためのボタンをクリックする
  screen.getByRole('button', { name: '不参加' }).click();

  // 選択項目の一覧メニューの「現地参加」をクリックする
  screen.getByRole('option', { name: '現地参加' }).click();

  // プルダウンを開くためのボタンのテキストが「現地参加」に変わり、メニューが非表示になることを確認できる
  expect(screen.getByRole('button', { name: '現地参加' })).toHaveAttribute('aria-expanded', 'false');
});

解説

開閉状態によってaria-expandedの値が変化することや選択された項目がわかるプルダウン

このプルダウンでは、プルダウンの開閉状態を管理したり、選択状態を管理するための属性を付与しています。
これにより、以下のようなことが実現できるようになりました。

  • メニューの開閉状態をコンポーネントとテストできるようになった
  • 付与した属性を活用して選択された項目のテストやスタイリングがしやすくなった

注意

さらにアクセシビリティを改善した例として挙げていますが、こちらもまだまだ不完全です。
管理すべき属性や処理はMDNにも記載されていますが、そもそもプルダウン(listbox)を採用するべきではないという考え方もあります。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Roles/listbox_role

より多くの改善を施した Pulldown前述したStack BlitzのPulldown4にも公開しているのでぜひ参考にしてみてください。

テスタビリティ !== アクセシビリティ

この記事では、アクセシビリティを向上させることでテスタビリティも向上させる例を上げさせていただきましたが、テスタビリティを向上させるためにアクセシビリティに関する変更を加えることには慎重になるべきです。

不適切なWAI-ARIAの利用やHTMLの仕様をハックした使い方は、かえってアクセシビリティを下げる結果につながることもあるので、よく調べてから活用するか、不安なときは data-testid などを利用するようにしましょう。

他の要素やライブラリの利用も検討してみる

複雑なUIになればなるほど、アクセシビリティを考慮すべきポイントが増えてきて、実装やテストも難しくなります。そんなときは他の要素やUIコンポーネントライブラリの利用も検討してみましょう。

今回のプルダウンの例でいえば、select要素とoption要素、あるいは<input type="radio">で代替できるかもしれません。
HTML要素はすでにアクセシビリティを考慮した挙動をしてくれるので、強力な選択肢となるでしょう。

また、よく利用されているUIコンポーネントライブラリにはアクセシビリティまで考慮されたものも多いため、車輪の再発明を防ぐことができます。

つぎはどうする

ここまでテストから始めるWebアクセシビリティというテーマで紹介してきましたが、「つぎはどうする」という問いが生まれると思います。そこで、最後にいくつかのおすすめのNext Actionsを紹介いたします。

  • WCAGやガイドブックを読む
  • 書籍や記事を読む
  • 実践してみる
    • 挙動を理解するには、手を動かすことが一番の近道だと思っています。実際に実装しながら試してみましょう。
  • UIコンポーネントライブラリを利用してみる
    • よくできたUIコンポーネントライブラリではアクセシビリティを意識した実装がされていることが多いです。
    • 出力されるコードを読んでみたり、実際にプロダクトに組み込んでみても良いでしょう。
  • アクセシビリティ系のイベントや勉強会に参加する
    • 対面だからこそ伝わる熱量や情報交換の機会もあるため、アクセシビリティ系のイベントや勉強会に参加してみましょう。

株式会社ゆめみはアクセシビリティカンファレンス福岡のシルバースポンサーもさせていただいています。弊社からも数名参加しますので、ぜひお会いしましょう。
(つぎはどうする、というワードはアクセシビリティカンファレンス福岡のスローガンを勝手にお借りました)

まとめ

この記事では「テストから始めるアクセシビリティ」をテーマに、アクセシビリティの向上がエンジニア自身にもたらす恩恵について解説しました。

アクセシビリティを考慮したセマンティックなマークアップを行うことで、アクセシビリティの改善だけでなく、テストやスタイリングが容易になったり、コードの可読性や保守性も向上したりすることを理解していただけたかと思います。

これを機に、実践や学習を通じてアクセシビリティへの理解を深め、共により良いプロダクトを作っていきましょう!

株式会社ゆめみ

Discussion