a11yとテストを同時に改善する話
これまで、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 でもセッションがありましたので、詳細は動画をご確認ください。
この拡張で、DOM tree と a11y tree のビューを、スイッチできるようになりました。「role:"名前"」 による木構造ビューを提供してくれるので、ランドマークや特定 Node が、どの様なアクセシブルネームで識別されているのか即座にわかります。もし、div ばかりで構築したアプリケーションであれば、DOM tree と a11y tree の構造がひどく乖離していることでしょう。a11y tree が意図したものでないなら、改善のときです。
問題のコンポーネントを確認する
a11y 改善の対象として、問題のコンポーネントをみていきましょう。このコンポーネントは一般的な<Card />
コンポーネントを一覧表示した<CardList />
コンポーネントです
Storybook をコミットしていれば「どこが悪いのか?」を、より鮮明に把握することができます。特定の Story ページに遷移し「canvas tab」を押下、別タブでコンポーネントを確認します(キャプチャ右が別タブで開いた状態)
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>
);
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
ロールを持つ事になり、支援技術からもアクセシブルな領域(ランドマーク)となりました。
テストの改善
<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