`)のうち、最初のアイテムにのみ`tabindex=”0″`を付与し、それ以外のアイテムは`tabindex=”-1″`とします。これにより、タブキーでリストに入った際、最初のアイテムにフォーカスが当たります。
3. 矢印キーでフォーカスを移動: JavaScriptを使って、上下矢印キーが押されたら、現在フォーカスが当たっているアイテムの`tabindex`を`-1`にし、次のアイテムの`tabindex`を`0`にして`focus()`メソッドでフォーカスを移動させます。
4. 選択状態を管理: `aria-selected`属性とCSSを使い、現在選択されているアイテムを視覚的にも、スクリーンリーダー的にも明確にします。
このアプローチを取ることで、ユーザーはタブキーを一回押すだけでリスト全体に入り、その後は矢印キーで効率的にリスト内を移動できるようになります。
—
実践!矢印キーによるリスト内ナビゲーションの実装パターン
それでは、実際に矢印キーによるリスト内ナビゲーションを実装してみましょう。
HTMLの準備
まずはHTMLマークアップです。`role`属性と`tabindex`の初期値を適切に設定します。
キーボード操作可能なリストナビゲーション
キーボード操作可能なリストナビゲーション
以下のリストは、タブキーでリスト全体にフォーカスを当てた後、矢印キーでアイテム間を移動できます。
- プロジェクトAの進捗報告
- タスクBの仕様検討会議
- 週次チームミーティング
- デザインレビュー会
- バグ修正優先度決定
このリストは、キーボード操作に最適化されています。
重要なポイントは以下の通りです。
- `
- `role=”listbox”`: スクリーンリーダーに対して、これがリストボックスであることを明示します。
- `tabindex=”0″`: タブキーを押すと、まずこの`
`要素自体にフォーカスが当たります。これがリストへの「入り口」です。
- 最初の`
- `には`tabindex=”0″`と`aria-selected=”true”`を付与します。
- それ以降の`
- `には`tabindex=”-1″`と`aria-selected=”false”`を付与します。これにより、タブキーで直接これらのアイテムにフォーカスが当たるのを防ぎます。
CSSの準備
フォーカス状態、選択状態を視覚的に明確にするためのCSSを準備します。
/ css/style.css (既存に追加または修正) /
body {
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
margin: 20px;
line-height: 1.6;
color: #333;
}
h1, p {
margin-bottom: 15px;
}
/ シンプルリストのスタイルはここでは割愛しますが、必要に応じて残してください /
/ ul.simple-list li:focus { … } /
/ キーボードナビゲーションリストのコンテナ /
ul.keyboard-nav-list {
list-style: none;
padding: 0;
border: 1px solid #ccc;
border-radius: 5px;
max-width: 400px;
margin-bottom: 30px;
}
/ キーボードナビゲーションリストの各アイテム /
ul.keyboard-nav-list li {
padding: 10px 15px;
border-bottom: 1px solid #eee;
cursor: pointer; / マウスユーザー向けにカーソルをポインタに /
transition: background-color 0.2s ease; / ホバー・フォーカス時のアニメーション /
}
ul.keyboard-nav-list li:last-child {
border-bottom: none;
}
/ リストコンテナ自体にフォーカスが当たった時のスタイル(ここではリスト内をハイライト) /
/ 注意: listboxロールの場合、外枠にフォーカスが当たっている状態を明示することが重要です。 /
ul.keyboard-nav-list:focus-within {
outline: 2px solid #5a9bff; / リスト全体へのアウトライン /
outline-offset: 2px;
}
/ アクティブなアイテム(tabindex=”0″が付与されているアイテム)のスタイル /
ul.keyboard-nav-list li[tabindex=”0″] {
outline: none; / 個々のアイテムのデフォルトフォーカスリングはJSで管理するため消す /
}
/ 選択状態(aria-selected=”true”)のアイテムのスタイル /
ul.keyboard-nav-list li[aria-selected=”true”] {
background-color: #d0edff; / 選択されているアイテムの背景色 /
font-weight: bold;
color: #0056b3;
}
/ ホバー時のスタイル /
ul.keyboard-nav-list li:hover {
background-color: #f0f8ff;
}
ここでは、`ul.keyboard-nav-list:focus-within`を使って、リスト全体にフォーカスが当たっていることを視覚的に示しています。個々の`li`のフォーカスはJavaScriptで管理し、`aria-selected=”true”`のスタイルを定義しています。
JavaScriptの実装(本丸!)
いよいよJavaScriptで、キーボード操作のロジックを実装します。
// js/script.js
document.addEventListener(‘DOMContentLoaded’, () => {
// キーボード操作対象のリスト要素を取得
const list = document.querySelector(‘.keyboard-nav-list’);
if (!list) {
console.warn(‘キーボードナビゲーションリストが見つかりません。’);
return;
}
// すべてのリストアイテムを取得
const listItems = Array.from(list.querySelectorAll(‘[role=”option”]’));
// 現在フォーカスが当たっている(tabindex=”0″が設定されている)アイテムのインデックスを保持
let currentFocusIndex = listItems.findIndex(item => item.tabIndex === 0);
if (currentFocusIndex === -1 && listItems.length > 0) {
// もし初期状態でtabindex=”0″のアイテムが見つからない場合は、最初のアイテムを初期フォーカスに設定
currentFocusIndex = 0;
listItems[currentFocusIndex].tabIndex = 0;
listItems[currentFocusIndex].setAttribute(‘aria-selected’, ‘true’);
}
/
- フォーカスを新しいアイテムに移動させる関数
- @param {number} newIndex – 新しいフォーカス先のアイテムのインデックス
/
function moveFocus(newIndex) {
// インデックスが範囲外にならないように調整
if (newIndex < 0) {
newIndex = listItems.length - 1; // 循環: 最初から最後へ
} else if (newIndex >= listItems.length) {
newIndex = 0; // 循環: 最後から最初へ
}
// 現在フォーカスが当たっているアイテムのtabindexを-1にし、aria-selectedをfalseにする
if (currentFocusIndex !== -1 && listItems[currentFocusIndex]) {
listItems[currentFocusIndex].tabIndex = -1;
listItems[currentFocusIndex].setAttribute(‘aria-selected’, ‘false’);
}
// 新しいフォーカス先のアイテムのtabindexを0にし、aria-selectedをtrueにする
const newItem = listItems[newIndex];
newItem.tabIndex = 0;
newItem.setAttribute(‘aria-selected’, ‘true’);
newItem.focus(); // 実際にフォーカスを移動させる
// スクロールしてアイテムが見えるようにする (オプション)
newItem.scrollIntoView({ behavior: ‘smooth’, block: ‘nearest’ });
// 現在のフォーカスインデックスを更新
currentFocusIndex = newIndex;
}
// リスト全体に対するキーダウンイベントリスナー
list.addEventListener(‘keydown’, (event) => {
// イベントの伝播を停止し、デフォルトのブラウザ動作を抑制
// これがないと、上下キーでページ全体がスクロールしてしまう
event.preventDefault();
switch (event.key) {
case ‘ArrowUp’: // 上矢印キー
moveFocus(currentFocusIndex – 1);
break;
case ‘ArrowDown’: // 下矢印キー
moveFocus(currentFocusIndex + 1);
break;
case ‘Home’: // Homeキー: 最初のアイテムに移動
moveFocus(0);
break;
case ‘End’: // Endキー: 最後のアイテムに移動
moveFocus(listItems.length – 1);
break;
case ‘Enter’: // Enterキー: 選択されたアイテムに対するアクションを実行
case ‘ ‘: // スペースキー: 選択されたアイテムに対するアクションを実行
// ここで選択されたアイテムに対するカスタムアクションを実行
if (listItems[currentFocusIndex]) {
console.log(`[Enter/Space] 選択されたアイテム: ${listItems[currentFocusIndex].textContent}`);
// 例: リンク遷移、モーダル表示、データ送信など
// alert(`”${listItems[currentFocusIndex].textContent}” が選択されました!`);
}
break;
default:
// その他のキーはデフォルトの動作を許可するか、無視する
// ここでevent.preventDefault(); をコメントアウトすると、
// 他のキー入力(例: テキスト入力)が許可される場合もある
break;
}
});
// マウスでのクリック操作にも対応
list.addEventListener(‘click’, (event) => {
const clickedItem = event.target.closest(‘[role=”option”]’);
if (clickedItem && clickedItem !== listItems[currentFocusIndex]) {
const newIndex = listItems.indexOf(clickedItem);
if (newIndex !== -1) {
moveFocus(newIndex);
}
}
// クリックした要素にフォーカスを移動させる(tabindex=”0″が付与されるため)
if (clickedItem) {
clickedItem.focus();
}
});
// リストコンテナがフォーカスを失ったときの処理
// ここでは特に何もせず、現在の選択状態を維持。
// 必要であれば、選択状態をリセットするなどの処理を追加できる。
list.addEventListener(‘blur’, () => {
// console.log(‘リストがフォーカスを失いました。’);
// 例: リスト外にフォーカスが移ったらaria-selectedをすべてfalseにする
// listItems.forEach(item => item.setAttribute(‘aria-selected’, ‘false’));
// currentFocusIndex = -1; // フォーカスインデックスをリセット
}, true); // キャプチャフェーズでイベントを捕捉
});
このJavaScriptコードのキモは、`moveFocus`関数と`keydown`イベントリスナーです。
1. `moveFocus(newIndex)`関数:
- 現在フォーカスされているアイテムの`tabindex`を`-1`に、`aria-selected`を`false`に設定解除します。
- 新しいフォーカス先のアイテムの`tabindex`を`0`に、`aria-selected`を`true`に設定します。
- `newItem.focus()`を呼び出し、実際にそのアイテムにプログラム的にフォーカスを移動させます。
- `scrollIntoView`で、リストが長い場合に選択アイテムが画面内に収まるように配慮しています。
2. `list.addEventListener(‘keydown’, …)`:
- リスト全体にキーダウンイベントリスナーを設定します。
- `event.preventDefault()`を呼び出すことで、矢印キーによるブラウザのデフォルトスクロール動作を抑制します。これは非常に重要です。
- `event.key`を使って押されたキーを判定し、`ArrowUp`, `ArrowDown`, `Home`, `End`キーに応じて`moveFocus`を呼び出します。
- `Enter`キーや`Space`キーが押された場合は、選択されたアイテムに対するアクションを実行するロジックを記述できます。これは、リストがメニューアイテムやボタンのような振る舞いをする場合に特に役立ちます。
3. クリック操作への対応:
- マウスユーザーのためにも、クリックでアイテムを選択・フォーカスできるように`click`イベントリスナーを追加しています。
この実装により、タブキーを一回押すだけでリスト全体に入り、その後は上下矢印キーでスムーズにアイテム間を移動し、Enterキーでアクションを実行できるようになります。これは、マウス操作と同等か、それ以上に効率的なナビゲーションを提供し、アクセシビリティを劇的に向上させます。
—
高度な考慮事項とベストプラクティス
上記の実装パターンは非常に強力ですが、さらに深い洞察とベストプラクティスを共有させてください。
1. `aria-activedescendant`の活用(さらに高度なパターン)
今回の実装では、実際に`