Advent Calendar 2023

令和最新Route Announcer事情

この記事はyamanoku Advent Calendar 2023の15日目の記事になります。

Route Announcerとは何か

Single Page Application(以下SPA)は一枚のindex.htmlを元にJavaScript側の操作で画面遷移している風に見えるルーティングの実装をしています。このためスクリーンリーダーでは、サーバーサイドから次のページ情報を送ってくる方式では問題なかった画面遷移後の情報を検知することができません。つまりSPAでは今どの画面に行ったか、どの位置にいるかが分からず操作が混乱してしまう可能性があります。

この問題を解決するためにRoute Announcer(あるいはRoute Announcement)という仕組みが存在します。

Route Announcerのおおまかな仕組みはライブリージョンというものを活用し、画面内で動的な変化があった際にそれを支援技術へ伝えるというものです。具体的には、画面遷移の挙動が発生した際にライブリージョンに遷移先のページタイトルを伝えるようにすることで、支援技術側がそれを読み上げることができ、遷移したことが分かります。

Route Announcerを活用したフレームワークはGatsbyNext.jsSvelteKit、Angular(Angular Material)、Astroが挙げられます。これらのフレームワークでのRoute Announcer挙動では、大見出し(<h1>)・ページタイトル(<title>)が存在する順から、フォールバックとしてページのURLが読み上げられます。


ちなみにRoute Announcerの起源について調べてみたところ、Ember.jsのa11y-announcerがそれに相当するものかなと思っていました。ですが、さらに遡るとpatrickfox/a11ykitから相当する挙動を参考にしているみたいです。おそらくこれが始祖に当たるものかと思っていますが、もしこれよりも前に発明されたものやアイデアがあれば是非教えてください…!

具体的な実装方法

次に具体的な実装方法に触れていきます。実装コードは「Webアプリケーションアクセシビリティ──今日から始める現場からの改善 WEB+DB PRESS」より引用になります。

まずスクリーンリーダーに読み上げてもらう部分になります。

<p id="announcer" role="alert" aria-live="assertive"></p>

role="alert"はユーザーに即座に重要かもしれないメッセージを通知させるためのWAI-ARIAのロールになります。

aria-live="assertive"は要素の内容が変更された瞬間に読み上げるする指定です。この値には他にも読み上げないoffという値と、すべての読み上げが終わった後に通知するpoliteがあります。

仕様上ではrole="alert"にはaria-live="assertive"を暗黙的に含んでいるため書く必要はないのですが、支援技術がすべて対応しているかはわからないため、ユーザーの環境を考慮して付与されている形になっています。

読み上げてもらう部分は視覚的には見えなくて良いのでVisually Hiddenのスタイルを付与してあげます。

const onNavigationEnd = () => {
  const announcer = document.getElementById("announcer");
  const title = document.querySelector('title').textContent;
  if (announcer instanceof HTMLElement) {
    announcer.textContent = title;  // title要素のテキストを代入する
  }
}

挙動については onNavigationEndという関数を用意して、画面遷移が完了した際にこちらを呼び出すようにします。この関数では、title要素のテキストを取得してきて、要素のIDがannouncerの中に代入しています。

これがRoute Announcerとして最低限必要な実装になります。実装を見て分かる通りtitle要素を取得してくるため、ページごとで適切な内容を設定しておくことが重要です。

画面遷移後の挙動も考慮する

Route Announcerの挙動は画面遷移の開始時に発火するものです。そのため画面遷移後の挙動についても考慮する必要があります。例えば画面遷移後にキーボード操作を上から始めたい場合は、ページ上部にフォーカス地点を移動させるようにする必要があります。

一番分かりやすい対応としては、画面遷移後にRoute Announcerにフォーカスを当てるというものがあります。

<body>
+ <p id="announcer" role="alert" aria-live="assertive"></p>

まずはRoute Announcer要素を<body>内での一番上に設置します。

<p
  id="announcer"
+ tabindex="-1"
  role="alert"
  aria-live="assertive">
</p>

読み上げる要素にtabindex="-1"を付与します。この値があることでTabキーで順番に要素へフォーカス移動する際に、その要素だけをスキップできるようになります。

  if (announcer instanceof HTMLElement) {
    announcer.textContent = title;  // title要素のテキストを代入する
+   announcer.focus();  // 読み上げる要素にフォーカスを当てる
  }

テキストを代入後にfocus()を使ってフォーカスを当てることができます。これによりSPAでの画面遷移後にはページ上部へとフォーカスが移動して、通常の画面遷移と同様にフォーカス順序が上から始められるようになります。

しかし、フォーカスの挙動としてはこの形で問題ないのですが、Windowsで使用できるスクリーンリーダー「NVDA」での読み上げでは「(ページタイトル)、警告」というように読み上げられてしまいます。なぜそのように読みあげられてしまうかというと、スクリーンリーダーでは対応するHTMLの名前と役割を取得してくるため、role="alert"が付与されていることで「警告」という読み上げになります。

Route Announcer自体はSPAにおけるアクセシビリティ向上させる実装になりますが、あくまでもテクニックとしてのもので、HTMLの中における適切なセマンティクスな役割といったものは存在しません。なのでroleによっては違和感のある読み上げになってしまうこともあります。

それを加味するとセマンティクスな要素にフォーカス移動するのがスクリーンリーダー利用者にとっても理解しやすいのかなと思っています。<main>要素にフォーカスさせるというのも1つの手ですが、これはページ内コンテンツをすべて読み上げてしまうことにもつながってしまいます。

そのため読み上げる対象をより小さくさせたいと思っており、私の中で現時点で最適解と思っているのがページ内の大見出し(あるいはページ内に準ずる見出し)にフォーカスさせることだと考えています。

const onNavigationEnd = () => {
  const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
  let focusHeading = headings[0];

  headings.forEach((heading) => {
    if (heading.tagName < focusHeading.tagName) {
      focusHeading = heading;
    }
  });

  if (focusHeading instanceof HTMLElement) {
    focusHeading.tabIndex = -1;  // 見出しにtabindexを付与する
    focusHeading.focus();  // 読み上げる要素にフォーカスを当てる
  }
}

以上より私が現時点でもっている最適解はページの大見出し(あるいはページ内に準ずる見出し)にフォーカスさせるだと思っているのですが、ページによってはレイアウトが異なったり、大見出しの位置が必ず保証されているわけでもありません。現状の対応ではレイアウトを固定するような制約をもたせない限りは完全には解決できないと思っています。

また、画面が遷移したかどうかはJavaScriptだけの管轄になっており、実装状況や利用環境によっては読み上げをしてくれるタイミングが変わる可能性もあるため、setTimeout()にてタイミングをズラす必要があったりします。

結局のところ共通で使えるRoute Announcerの実装は難しいというのが現状です。Ubieさんの事例1のように各プロダクトや各環境での状況に併せた実装が求められるのが現実です。

この独自で解決しなければならない問題に対しての銀の弾丸になりえると期待しているのが「Navigation API」です。こちらのAPIについてを今年のJSConfJPでLTにて紹介させていただきました2

Navigation APIは、Location APIHistory APIの後継として開発されている新しいWeb APIです。このAPIを利用することで、画面遷移の開始と終了のタイミングを通知することができたり、フォーカスマネジメントやスクロール位置の復元といったことも調整しやすくなります。

2023年現在ではChromeのみでしか使えないものですが、Interop 2024にてNavigation APIも相互運用に注力するAPIとして投票されております。アクセシビリティ以外の観点でも各種ルーターライブラリにて活用ができそうなため期待が高まっています3。SPAでの画面遷移におけるアクセシビリティを大いに向上させてくれるWeb APIのため、早くクロスブラウザで実装されて、各スクリーンリーダーでも対応できるようになると良いなと思っています。

参考情報

脚注

  1. SPA(Next.js)のスクリーンリーダーによる画面遷移で工夫したこと

  2. 画面遷移のアクセシビリティ課題を解決しうる Navigation API への期待 - Google スライド

  3. ReactRouterTanStack/routerとでNavigation APIに関するDiscussionsが登録されています

この記事に関する修正依頼
トップへ戻る