問題とユースケース
完全なドキュメントナビゲーションでは、ドキュメントとそのタイトルが置き換わります。SPAのソフトナビゲーションでは同じドキュメントが維持されるため、周囲のコンテンツが消去されたリンク上にフォーカスが残ってしまうことがあります。スクリーンリーダーのユーザーは、新しい商品、購入手続きのステップ、またはエラーページが準備できたという有用な通知を受け取れない可能性があります。
このポリシーは、初回ロード、ユーザー操作によるルート変更、同一画面内でのフィルターやソート、上書きまたはキャンセルされたナビゲーション、ブラウザの「戻る/進む」という5つの異なる遷移をカバーする必要があります。また、ダイアログなどのコンポーネント固有の動作とも共存しなければなりません。目標は予測可能なコンテキストを提供することであり、URLが変更されるたびにフォーカスを移動させることではありません。
面接官が評価しているポイント
面接官はセマンティックな決定モデルを求めています。「すべてのルート変更で見出しにフォーカスを当てる」という方針はアクセシブルに聞こえますが、フィルター処理、ハッシュ変更、初回のハイドレーション、履歴復元を破綻させます。優れた回答は、まずユーザーのタスクが変更されたかどうかを判断します。
また、ブラウザの知識も期待されています。見出しは tabIndex={-1} を使うことで順次タブ移動の順序に加わることなくプログラムによるフォーカスを受け取れること、focus() は通常要素をスクロールさせること、preventScroll はフォーカスの復元とスクロールの復元を分離すること、正の tabindex 値はフォーカス順序を脆弱にすることなどが挙げられます。
最後に、回答では並行処理を扱う必要があります。ルートBがルートCの後に解決した場合、Bがタイトルを更新したりフォーカスを奪ったりしてはなりません。自動テストによってアクティブな要素、タイトル、順序を検証することはできますが、すべてのブラウザと支援技術の組み合わせが何を読み上げるかまでを証明することはできません。
回答前の確認事項
- どの遷移が新しいタスクを開始するのか? 商品からチェックアウトへの移動は該当しますが、ソート順の変更は通常該当しません。これにより、フォーカスを移動させるか、操作を開始したコントロール上に維持するかを決定します。
- ルートはいつコミットされるのか? フォーカスの処理は、クリック時やURLの変更時、スケルトン表示時ではなく、最終的に勝者となったルートが最終的なプライマリ見出しをレンダリングしたときにのみ実行されるべきです。
- オーバーレイの制御主体は何か? ダイアログは、自身の初期フォーカス、フォーカストラップ、および戻りフォーカスの規約を保持します。ルートポリシーはこれと競合してはなりません。
- 履歴は何を復元するのか? 履歴エントリーごとに、プロダクトがフォーカス、スクロール、またはその両方の復元を必要とするかを決定します。二重のジャンプを避けるために、この2つの調整が必要です。
- どのブラウザと支援技術がサポートされているか? 読み上げの動作は異なるため、リリースマトリクスを明確にする必要があります。
- すべてのルートが説明的なタイトルと1つのプライマリ見出しを提供できるか? これらをルート規約の一部とします。メインのランドマークは制御されたフォールバックとしてのみ使用します。
30秒の回答フレームワーク
「フォーカスを移動する前にナビゲーションを分類します。初回ロードでは何もしません。ユーザー操作によるコミット済みのタスク変更では、ドキュメントタイトルを更新し、tabIndex=-1 でプログラム的にフォーカス可能にした新しい説明的なH1にフォーカスします。同一タスクのフィルターではコントロールを維持し、持続的な polite ステータス領域を通じて完了した結果を報告します。各ナビゲーションにはトークンが付与され、最新のコミット済みルートのみがタイトルとフォーカスを適用できます。『戻る/進む』では、その履歴エントリーに保存された有効なセマンティックターゲットを復元し、存在しない場合はH1を使用し、フォーカスとスクロールの復元を調整します。初回ロード、連続ナビゲーション、エラー、フィルター、履歴、キーボード順序、可視フォーカス、そして実際のスクリーンリーダーでテストを行います。」
ステップごとの詳細解説
まずは遷移テーブルから始めます:
| 遷移 | フォーカスのアクション | アナウンス |
|---|---|---|
| 初回ロードまたはハイドレーション | フォーカスを強制しない | ネイティブのドキュメント/タイトル動作 |
| 新しいタスクへの PUSH | コミットされたルートのH1にフォーカス | 更新されたタイトル+フォーカスされた見出し |
| 同一タスク内のフィルター、ソート、ページネーション | 操作を開始したコントロールを維持 | 有用な場合は polite な結果/ステータス更新 |
| 「戻る/進む」による POP | 保存された有効なターゲットを復元、なければH1 | 復元されたコンテキストまたはフォーカスされた見出し |
| リダイレクトまたはエラールート | そのルートのコミットされたH1にフォーカス | その最終的なタイトルと見出し |
| ダイアログの開閉 | ダイアログの規約に委譲 | ダイアログのラベルと復帰ターゲット |
ルート規約は、最終タイトル、説明的なH1への参照(ref)、およびセマンティックなルートキーを提供します。H1には tabIndex={-1} を付与します。これにより通常のTabシーケンス外にとどまりますが、コードからフォーカスを当てることができます。フォーカスインジケータは視覚的に維持します。スキップリンクはキーボード操作可能な最初のコントロールとして維持され、main をターゲットとします。これは重複ナビゲーションのバイパスを解決し、ルートのフォーカスが管理されている場合でも依然として有用です。
フォーカス処理はコミット境界に属します。試行されるすべてのナビゲーションに単調増加するトークンを割り当てます。あるルートのデータとUIの処理が完了した際、そのトークンが現在も最新であり、かつH1がマウントされている場合にのみエフェクトを適用します。固定のタイムアウトは使用しないでください。ネットワーク速度やレンダリング処理の違いにより信頼性が低くなります。
beginNavigation(kind):
token = nextToken()
rememberCurrentFocus(historyEntryKey)
commitRoute(token, kind, title, heading):
if token != currentToken or heading is not connected:
return
document.title = title
if kind is PUSH or semantic-task REPLACE:
heading.focus()
else if kind is POP:
focus(validSavedTarget(historyEntryKey) or heading)生成されたCSSパスではなく、安定したルートローカルなフォーカスIDを保存します。POP時には、要素がまだ存在し、可視であり、有効化されており、復元された状態において意味を成す場合にのみ復元します。そうでない場合はH1を使用します。ルーターがスクロールを個別に復元する場合は、focus({ preventScroll: true }) を使用してからスクロールを1回だけ復元します。通常のPUSHでは、フォーカスによってH1を表示させるほうが通常はわかりやすくなります。
音声の重複を避けます。タイトルを更新した後に新しいH1にフォーカスを当てることで十分なコンテキストが提供されることが多いですが、具体的な読み上げは支援技術によって異なります。デフォルトでライブリージョンにも同じタイトルをアナウンスすることは避けてください。フォーカスを維持するフィルターの場合、持続的な role="status" または aria-live="polite" 領域が完了した結果件数をアナウンスできます。assertive なアナウンスは現在の読み上げを中断する可能性があるため、真に緊急性の高い情報のみに限定してください。
テストのオラクル(期待値)はポリシーに従います。初回のハイドレーションでは、アプリケーションはフォーカスを奪ってはなりません。PUSHでは、最終コミット後に document.activeElement が新しいH1となり、タイトルが確定し、次のTab操作で最初の論理的なインタラクティブ要素に移動します。フィルター変更では、トリガーがフォーカスを保持し、ステータステキストが1回更新されます。Bが最後に解決する A→B→C の競合状態では、Cがタイトルとフォーカスを維持します。エラーおよびリダイレクトルートは独自の見出しにフォーカスします。POPは保存された有効なターゲットを復元するか、決定論的にフォールバックします。
ターゲットのアンマウントや reduced-motion / ズームレイアウトを含むこれらすべてのケースについて、自動化されたDOMチェックを実行します。その後、キーボードおよび、VoiceOver/Safari、サポート対象ブラウザとNVDAまたはJAWSなどの組み合わせを使用して手動で動作を確認します。可視フォーカス、論理的な順序、予期しないスクロールの有無、理解可能なアナウンスを検証します。音声出力は統合の結果でありDOMの保証ではないため、ブラウザと支援技術のバージョンを記録してください。
高品質な回答例
「私はフォーカスの動作をルーターのセマンティック規約の一部として設計します。各ルートは最終的なドキュメントタイトルと説明的なH1の参照を公開します。遷移を分類し、初回のハイドレーションではフォーカスを移動せず、タスクを変更するユーザー起点のPUSHではコミット後に最終的なH1にフォーカスし、フィルターやソートでは開始したコントロールを維持し、POPではH1にフォールバックする前に安定した保存済みターゲットの復元を試みます。
H1には tabIndex=-1 を設定するため、余分なTabストップを追加することなくプログラムからフォーカス可能です。視覚的なフォーカススタイルを維持し、main をターゲットとするスキップリンクを維持します。タイトルとフォーカスは、勝者となったルートがコミットされた後にのみ一緒に変更されます。すべてのナビゲーションはトークンを受け取り、古い非同期完了は無視されるため、キャンセルされたルートがフォーカスを奪うのを防ぎます。
履歴については、履歴エントリーごとにセマンティックでルートローカルなフォーカスIDを保存します。接続されており、可視で、有効なターゲットのみを復元します。スクロール復元が個別の場合は、preventScroll でフォーカスを当て、スクロールを1回復元します。同一タスクの非同期更新には持続的な polite ステータス領域を使用し、見出しとライブ領域の両方でルートタイトルを重複してアナウンスすることは避けます。
アクティブ要素、タイトル、Tab順序、フィルターのフォーカス維持、連続する A→B→C ナビゲーション、リダイレクト、エラー、POPのフォールバックに対するアサーションを自動化します。その後、サポート対象のキーボードおよびスクリーンリーダーのマトリクスで実際の読み上げ、可視フォーカス、スクロールをテストします。これにより、決定論的なDOMの動作と、観察する必要がある支援技術の動作を分離します。」
よくある間違い
- すべてのURL変更でフォーカスする → フィルターやハッシュの変更がタスクを中断させてしまう → まずセマンティックな遷移を分類する。
- リンクがクリックされたときにフォーカスする → 遷移先が存在しないかキャンセルされる可能性がある → コミットされた勝者ルートのみにフォーカスする。
- タイムアウトを使用する → 低速なレンダリングと高速なレンダリングで競合の仕方が変わる → ルートのライフサイクルとナビゲーショントークンを使用する。
bodyにフォーカスするか正のtabindexを追加する → コンテキストと順序が不明瞭になる →tabIndex=-1を持つ説明的なH1を使用する。- POPで常にH1にジャンプする → 『戻る』操作でユーザーが見ていた位置が失われる → 決定論的なフォールバックを備えた有効な履歴ターゲットを復元する。
- タイトルを2回アナウンスする → ユーザーが冗長な音声を聞くことになる → フォーカスされた見出しによるコンテキストを優先し、インプレース更新にはライブステータスを使用する。
- 自動アクセシビリティスキャンを証明として扱う → 実際の読み上げ出力を検証することはできない → 手動のキーボードテストおよび支援技術テストを追加する。
フォローアップの質問と回答
フォローアップ1: なぜ常に main ランドマークにフォーカスしないのですか?
H1のほうが通常、新しいタスクに対してより具体的な名前を提供するためです。main はルート規約が見出しを提供できない場合の合理的なフォールバックですが、すべてのルートが見出しを提供するようにすることで、視覚的な構造、ドキュメントのアウトライン、テスト容易性が向上します。
フォローアップ2: マウスによるナビゲーションでもフォーカスを移動すべきですか?
ユーザー操作によってメインタスクが置き換わる場合、クリックするスクリーンリーダーユーザーを含め、入力デバイスに関係なく一貫したコンテキストが有用です。キーボード使用の推測ではなく、セマンティックなナビゲーションに基づいて判断します。バックグラウンドの更新でフォーカスを移動することは避けてください。
フォローアップ3: 素早いナビゲーションでCの後にBが完了した場合はどうなりますか?
Bのトークンは現在のナビゲーションと一致しなくなるため、そのコミットエフェクトはタイトル、フォーカス、ステータスを変更せずにリターンします。Bのリクエストを中止することで処理を節約できますが、キャンセルが遅れたりサポートされていない場合があるため、トークンチェックは依然として必要です。
フォローアップ4: フォーカスとスクロールの復元はどのように相互作用しますか?
通常の focus() はターゲットをビュー内にスクロールさせることがあります。POP時にルーターが保存されたスクロール位置を個別に復元する場合は、preventScroll を使用して保存された要素を検証・フォーカスし、その後1回のスクロール復元を実行します。フォーカス処理とルーターコードが競合しないように、責任者を1つに定義します。
フォローアップ5: ライブリージョンはどのような場合に assertive にすべきですか?
緊急のセッション情報や安全に関するメッセージなど、聞き取りの遅れが深刻な問題を引き起こす場合に限られます。通常の結果件数や完了の更新は polite にするべきです。ルートのコンテキストは通常、最終タイトルとフォーカスされた見出しから得られるため、2回目のアナウンスは避けます。
フォローアップ6: これだけでWCAGに準拠したことになりますか?
いいえ。これは動的アプリケーションにおいて理解しやすいフォーカス順序とコンテキストを支援しますが、準拠にはセマンティクス、名前、キーボード操作、コントラスト、エラー処理、その他の基準も関係します。プロダクトの適合目標に照らして、ユーザー体験全体のジャーニーをテストしてください。