👓

a11yとテストを同時に改善する話

2022/05/26に公開

これまで、a11y 改善・テスト拡充にあたり「どのように改善すべきか?どのように書くべきか?」という点がハードルだと感じていました。Chrome で a11y tree を確認するには、dev tools の隅の隅をつつく必要があり、あまり体験の良いものではなく、気に入ったエクステンションもありませんでした。

Testing Library は「誰もがアクセスできるクエリー」を優先的に使用することを推奨していますが、アプリケーションがはじめから a11y に考慮された作りになっているとは限りません。これらの背景から「data-testid」のような、テスト向け属性に頼るワークアラウンドで乗り切ることも少なくありませんでした。

Full page accessibility tree

今年 1 月にリリースされたChrome98 の新機能として「Full page accessibility tree」を dev tools で確認できるようになりました。先日の Google I/O でもセッションがありましたので、詳細は動画をご確認ください。

https://www.youtube.com/watch?v=Th-nv-SCj4Q

この拡張で、DOM tree と a11y tree のビューを、スイッチできるようになりました。「role:"名前"」 による木構造ビューを提供してくれるので、ランドマークや特定 Node が、どの様なアクセシブルネームで識別されているのか即座にわかります。もし、div ばかりで構築したアプリケーションであれば、DOM tree と a11y tree の構造がひどく乖離していることでしょう。a11y tree が意図したものでないなら、改善のときです。

Full page accessibility tree の切り替え

問題のコンポーネントを確認する

a11y 改善の対象として、問題のコンポーネントをみていきましょう。このコンポーネントは一般的な<Card />コンポーネントを一覧表示した<CardList />コンポーネントです

Storybook をコミットしていれば「どこが悪いのか?」を、より鮮明に把握することができます。特定の Story ページに遷移し「canvas tab」を押下、別タブでコンポーネントを確認します(キャプチャ右が別タブで開いた状態)

canvas tab で Story を開いた様子

a11y tree はこの様になっていました。カードリストが、うまくセクショニングできていないことが分かります。

うまくセクショニングできていない a11y tree

これは、<Card />コンポーネントが<div>でマークアップされたことが原因です。div は意味を持たないグループとして扱われるため、そのままでは a11y tree には現れません。

export const Card = ({ id, title, text }: Props) => (
  <div className={styles.module}>
    <h3>{title}</h3>
    <p>{text}</p>
    <a href={`/articles/${id}`}>詳細をみる</a>
  </div>
);

section に修正する

<div>タグを<section>タグに変更することで、Card コンポーネントはセクションとして識別されます。改善されたように見えますが、まだ role と、アクセシブルネームは持っていないため、支援技術に対し、よりよいマークアップにはなっているとは言えません。

export const Card = ({ id, title, text }: Props) => (
  <section className={styles.module}>
    <h3>{title}</h3>
    <p>{text}</p>
    <a href={`/articles/${id}`}>詳細をみる</a>
  </section>
);

セクショニングされたCard一覧の a11y tree

region 化する

<Card />コンポーネントに含まれる「見出し」は、この領域をひとことで表すのに、うってつけの要素です。<h3>タグにidを付与し、<section>タグのaria-labeledbyと紐付けます。この時、React18 ならuseIdが利用できます。この Hook は一意な識別子を生成してくれるので、同一画面に繰り返し使用されるコンポーネントにはとくに便利です。先日の投稿でも紹介しているので、ご参考まで。

export const Card = ({ id, title, text }: Props) => {
  const headingId = useId();
  return (
    <section aria-labelledby={headingId} className={styles.module}>
      <h3 id={headingId}>{title}</h3>
      <p>{text}</p>
      <a href={`/articles/${id}`}>詳細をみる</a>
    </section>
  );
};

見出しを領域に関連付けたことで section に見出し相当の「アクセシブルネーム」が付与されました。同時にregionロールを持つ事になり、支援技術からもアクセシブルな領域(ランドマーク)となりました。
アクセシブルネームをもったCard一覧の a11y tree

テストの改善

<CardList />コンポーネントは a11y 改善の前から、きちんとテストコードがコミットされていました。以下は「◯◯ カードのリンク先は △△ であること」というテストケースですが、あまり良くないテストコードだとお気づきでしょうか?

「コンポーネントに含まれる全ての link ロール要素の 2 番目」という、意味をもたないクエリであり、有意義なものとは言えません。冒頭に a11y tree で確認したとおり、テストコードからも「アクセシブルではない」ことが滲み出ていますね。

const href = "https://testing.example.com/articles/2";
const links = screen.getAllByRole("link");
expect(links[1]).toHaveAttribute("href", href);

このテストコードは、施した a11y 改善により、以下の様に修正することができます。region ロールをアクセシブルネーム(タイトルそのもの)で絞り込み、含まれる link を検証しています。

const href = "https://testing.example.com/articles/2";
const title = "React18 の useId で a11y対応する";
const region = screen.getByRole("region", { name: title });
expect(within(region).getByRole("link")).toHaveAttribute("href", href);

前者・後者を見比べても、どういった行動をしているのか一目瞭然のテストとなり、a11y とテストが同時に改善できました。

コミットされたテストファイルを見れば、a11y を考慮しているコンポーネントなのか否か、これもまた一目瞭然です。コンポーネントのテストファイルを今一度確認し、a11y を考慮しているか見直してみましょう。

※ 補足

改善フローを明確にするための例をあげましたが、何でも<section>で囲ってしまい構造化するのは、かえってアクセシブルではないコンテンツになる恐れがあります。

region ロールが多数存在してしまうことに関してもしかりで、<section>の代わりに<article>で囲ったり、<div>で囲って意味をスキップした方が良い文脈もあります。各 a11y 文献を参考のうえ、改善フローの手法として、今回紹介したツールを活用してみてください。

Discussion