方向フォーカス ナビゲーション(矢印キーまたは D-Pad を使用)を使用すると、キーボードのみを使用するユーザーや代替入力を使用するユーザーが、視覚的な 2D レイアウトに基づいてインタラクティブなアイテムを操作できます。これは、空間ユーザー インターフェース(メディアレール、ダッシュボード パネル、ネストされたカタログなど)に非常に役立ちますが、ビューポート スクロールなどのネイティブ ブラウザの動作を妨げないように慎重に実装する必要があります。
このガイドでは、複合ウィジェット内でパフォーマンスに優れた、アクセスしやすい 2D 方向ナビゲーションを構築する方法について説明します。
アーキテクチャの基本原則
カスタムの矢印キー空間フォーカスを実装する場合は、次の基本ルールに従ってください。
- 複合ウィジェットに限定する: デフォルトの順次 Tab ナビゲーションが想定される通常のドキュメント テキストフローやシンプルな垂直リスト レイアウトでは、矢印キーをオーバーライドしないでください。一般的なドキュメント フローのガイダンスについては、ユーザー補助機能のコーディング ガイドラインをご覧ください。
- ネイティブの境界スクロールを保持する: 押された方向にフォーカス可能な要素がない場合、デフォルトのブラウザの動作を妨げない。イベントをバブルアップさせて、ブラウザがビューポートをネイティブにスクロールできるようにします。矢印キーを無条件にオーバーライドすると、ユーザー補助機能が損なわれます(WCAG 1.4.10)。
- フォーカスの可視性と配置を確保する: フォーカスが変更されたときに、新しくフォーカスされた要素がスクロールして表示されるようにします(例:
scrollIntoView({ block: 'nearest', inline: 'nearest' })を使用)。これにより、要素が完全に表示されたままになります。 - レイアウト システムと連携する: 各キー押下を処理しながら、ライブ境界ジオメトリ(
getBoundingClientRect())からフォーカス候補の位置を決定します。DOM の順序や固定グリッド構造がビジュアル レイアウトを表していると想定しないでください。最新のレイアウトに関する推奨事項については、CSS レイアウトとレスポンシブ デザインをご覧ください。
アクセス可能なフォーカス管理
空間コンテナは、連続するページタブ ナビゲーションに単一のタブストップを公開する必要があります。ウィジェット内では、Roving Tabindex パターンを使用してキーボード フォーカスを管理します。
- アクティブなアイテムに
tabindex="0"が含まれています。 - アクティブでないアイテムにはすべて
tabindex="-1"が設定されています。 - ナビゲーション時に、
tabindex="0"属性をシフトしてfocus()を呼び出すことで、プログラムによってフォーカスを移動します。
ビジュアル候補の選択
非対称、ラップ、グリッドのレイアウトでは、インデックスベースのマッピングは失敗します。代わりに、現在のキー押下に対応するライブの視覚的ジオメトリから候補を導出します。
- 候補:
checkVisibility()を渡すウィジェット コンテナのフォーカス可能な子孫を使用します。非表示の要素は、(0, 0)でサイズがゼロの rect を持つことができ、誤ってupまたはleftのターゲットにならないようにする必要があります。 - ジオメトリ: キー押下の処理中に、フォーカスされた要素とすべての候補の
getBoundingClientRect()を読み取ります。これらの Rect をグローバルにキャッシュに保存しないでください。レイアウトはさまざまな理由で変更される可能性があり、ユーザーのペースに合わせたキーイベントを処理する場合、少数のジオメトリ読み取りは低コストです。 - 方向フィルタ: 候補が押された方向にあるのは、その軸上で候補の近い方の端が現在の要素の遠い方の端と同じかそれより先にある場合のみです。サブピクセルの丸め処理のために、わずかな許容範囲が認められます。たとえば、
rightの候補にはcandidate.left >= current.right - toleranceが必要です。中心を比較して、候補が方向にあるかどうかを判断しないでください。 - 整列された候補を優先: 方向フィルタを通過した候補のうち、垂直軸上の範囲が現在の要素の範囲と重複する候補を優先します。ナビゲーション軸上で最もギャップが小さいものを選択し、垂直方向の中心オフセットが最も小さいものとのタイを解消します。これにより、アイテムが異常に広い場合でも、隣接する視覚的な行または列でナビゲーションが維持されます。
- フォールバック スコアリング: 垂直軸に重なる候補がない場合は、残りの候補を
gap + w * perpendicularCentreOffsetとしてスコア付けし、最小値を選択します。重みw >= 2を開始点として使用し、レイアウトに合わせて調整します。重みが大きいほど、より近い候補が優先されます。 - 候補なし: 何も返しません。イベント ハンドラは
preventDefault()を呼び出してはなりません。これにより、ブラウザはネイティブにスクロールできます。
正確な実装は異なる場合がありますが、方向テストをエッジベースに保ち、重み付けされたフォールバックの前に重複設定を適用します。これはアルゴリズムの核心です。キャッシュや固定の重みを普遍的に正しいものとして提示することは避けてください。
キー押下統合とスクロール連携
イベント ルーターは、有効な空間フォーカス遷移が発生した場合にのみ、標準のビューポート スクロールを防止する必要があります。
- 修飾キーが押されている場合、
e.defaultPreventedがすでに true の場合、または最も内側のイベント ターゲットが矢印キー自体を使用している場合は、イベントをインターセプトしないでください。e.targetはシャドウホストにリターゲットされる可能性があるため、e.targetだけでなくe.composedPath()[0]を使用します。実用的な編集可能 / 制御チェックはtarget.matches(':read-write, select, input[type="range"], [role="slider"]')です。クローズド シャドウルートは検査できません。 - リスナーのスコープをウィジェット コンテナに設定して、複数の空間ウィジェットが 1 つのページに共存できるようにします。
- フォーカスされた要素自体が候補である場合にのみ動作します。アイテムに独自のフォーカス可能なコントロールが含まれている場合は、それらのコントロールも候補にするか、そのままにしておきます。
closest()を使用して親を解決しないでください。 - 候補が見つかったら、
preventDefault()を呼び出し、ロービングtabindexを更新し、focus()を呼び出して、scrollIntoView({ block: 'nearest', inline: 'nearest' })を使用します。 - 候補が見つからない場合は、
preventDefault()を呼び出さずに戻り、ネイティブ スクロールを続行します。
空間ナビゲーションに h/j/k/l や W/A/S/D などの 1 文字のショートカットを追加しないでください。矢印キーは文字キーではありません。候補がない場合にネイティブ スクロールを維持することで、ビューポートの移動が不必要にブロックされるのを防ぐことができます。