FocusOnNavigate でページ遷移時の初期フォーカスを設定
Blazor アプリケーションプロジェクトを、SDK 標準のプロジェクトテンプレートから作成すると、App.razor
などには以下のように記述されています。
<Router AppAssembly="typeof(Program).Assembly" AdditionalAssemblies="new[] { typeof(Client._Imports).Assembly }">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<!-- 👇 今回とりあげるのは、コレ -->
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
...
</Router>
この <FocusOnNavigate />
コンポーネントは、「ページ遷移時、Selector
に指定された CSS セレクタに合致する要素にフォーカスを設定する」という作用を持つコンポーネントです。
さてこの、SDK 標準テンプレートから作成された Blazor アプリケーションプロジェクトにおける <FocusOnNavigate />
コンポーネントのマークアップなのですが、Selector
の指定が "h1"
になっています。
でも、どうして "h1"
なんでしょう?
...
<!-- なぜ、"h1" ? 👇 -->
<FocusOnNavigate RouteData="routeData" Selector="h1" />
...
"[autofocus]"
のほうが良くなくて?
個人的には autofocus
属性を持つ要素にページ遷移時の初期フォーカスを設定、すなわち、Selector
パラメーターには "[autofocus]"
を指定するほうがよいのでは? と思ってしまいました。
...
<!-- "[autofocus]" のほうがよくなくて? 👇 -->
<FocusOnNavigate RouteData="routeData" Selector="[autofocus]" />
...
autofocus
属性は、HTML 標準で定められている HTML 要素の属性です。ページや <dialog />
要素の初回表示時に、autofocus
属性を持つ要素があればそれに初期フォーカスを移動する、という働きを持ちます。
しかしながら、対話モードの Blazor アプリケーションは、いわゆる SPA、すなわち「動的に DOM ツリーを変更して表示を更新しつつ、History API を使ってブラウザのページ履歴とアドレスバーに表示される URL とを更新」という仕組みでページ遷移を表現しています。つまり、ブラウザにとっては、「対話モード Blazor のページ遷移」は「DOM の書き換え」にしか見えず、ページ遷移のたびに「ページの初回表示」は発生していないことになります。そのため、この種の SPA では、autofocus
属性を持つ要素が新たに出現したとしても、ブラウザとしては、もうページは初回表示は済んでしまっているので、その要素にフォーカスを当てることはしません。つまり、autofocus
属性は、いわゆる SPA におけるページ遷移においては機能しないのです。
そこで、ブラウザの動作に任せるのではなく、SPA の仕組み側から、ページ遷移完了時に、能動的に autofocus
属性を持つ要素を探し出し、その要素にフォーカスを設定するように実装すれば、クラシカルな MPA と同じ振る舞いを再現できるようになります。
ということで、Blazor においては、<FocusOnNavigate />
コンポーネントを用い、その Selector
に "[autofocus]"
を指定することで、これを実現できるわけです。
ページ遷移先が何かの入力フォームだったときに、そのフォーム内の入力要素に autofocus
属性を付けておけば、遷移後すぐにその入力要素にフォーカスがあたっているので、いちいちその入力要素をクリックしたり Tab キーでフォーカス移動したりせずに、すぐに入力を開始できるので、ユーザー体験的によいのでは? と思われたんですよね。
しかし SDK 標準テンプレートが生成する Blazor アプリケーションのコードでは、そうはなっておらず、入力要素ですらない h1
要素に初期フォーカスを当てるコードが生成されています。どうしてなんでしょうか??
どうやらスクリーンリーダーはじめアクセシビリティの考慮の結果らしい
ここで、今一度、<FocusOnNavigate />
コンポーネントについての Microsoft Learn での説明や、autofocus
属性についての MDN 上での説明を読み直してみます。以下に引用します。
FocusOnNavigate クラス
"これを使用して、スクリーン リーダーと互換性のあるアクセシビリティ対応のナビゲーション システムを構築できます"
(FocusOnNavigate クラス | Microsoft Learn)
アクセシビリティの考慮
フォームコントロールに自動的にフォーカスを合わせると、画面読み上げ技術を使用する視覚障碍者や認知障碍者を混乱させる可能性があります。autofocus が割り当てられている場合、スクリーンリーダーは事前に警告することなく、ユーザーをフォームコントロールに「テレポート」します。
autofocus 属性を使用する際には、アクセシビリティに十分配慮してください。 コントロールに自動的にフォーカスが当たると、読み込む際にページのスクロールが発生する可能性があります。また、タッチ端末によっては、フォーカスが動的なキーボードを表示させることもあります。スクリーンリーダーはフォーカスを受けたフォームコントロールのラベルをアナウンスしますが、ラベルの前には何もアナウンスしないので、小さな機器にいる目の見えるユーザーは、前のコンテンツによって作成されたコンテキストを同様に見逃してしまうでしょう。
(autofocus - HTML | MDN)
なるほど、一見、便利と思われる autofocus
属性も、アクセシビリティの観点からは、使い方は慎重になるべきようであることがわかりました。
その点、ページ遷移時の初期フォーカスを h1
要素に適用するのは、スクロール位置がそこから開始したり、スクリーンリーダーがそこから読み始めたりすることを考えると、たしかに、理にかなっているように思われます。ページ遷移時のたびに毎回、スクリーンリーダーが、アプリケーションヘッダーやナビゲーションメニューから読み上げ始めても、なるほど、不便かもしれませんね。その点、ページ遷移ごとに、h1
要素で示されるような "いちばん大きな見出し" から読み上げてもらう方が・スクロール位置をリセットしてもらうほうが、ユーザー体験的には快適であることが予想されます。
まとめ
ということで、ページ遷移ごとに初期フォーカスを指定できる <FocusOnNavigate />
コンポーネントですが、その Selector
指定が、既定で生成されるコードでは "h1"
となっているのは、アクセシビリティの観点からの結果であるっぽいこと、および、これを "[autofocus]"
に設定するのはアクセシビリティ上の懸念があるため慎重になるべきことが見えてきました。
やっぱり物事にはいろいろと背景事情があるものですね。