【ITニュース解説】Make Any List Keyboard-Friendly in Minutes
2025年09月26日に「Dev.to」が公開したITニュース「Make Any List Keyboard-Friendly in Minutes」について初心者にもわかりやすく解説しています。
ITニュース概要
Angularアプリでキーボード操作可能なリストを簡単に作る方法を解説。Angular CDKのListKeyManagerを使えば、少ないコードで矢印キーでの移動やHome/Endキー対応など、高いアクセシビリティを実現できる。初心者でもユーザーに優しいUI構築が可能。
ITニュース解説
システムエンジニアを目指す初心者の皆さん、こんにちは。今回は、Webアプリケーションをより多くの人が使いやすくするための「アクセシビリティ」という考え方と、それを実現する具体的な技術について解説する。特に、キーボードだけでリスト操作ができるようにする方法に焦点を当てる。
Webアプリケーションは、マウスだけでなくキーボードでも操作できることが非常に重要である。なぜなら、マウスを使いにくい人や、視覚障がいなどでスクリーンリーダー(画面読み上げソフトウェア)を利用する人にとって、キーボード操作は必須だからである。もしアプリケーションがキーボードで操作できない部分があると、そうしたユーザーはその機能を使うことができず、大きな「アクセシビリティの壁」に直面してしまう。
ここでは、Angularというフレームワークで書かれた簡単なユーザーリストを例に考えてみよう。このリストは通常、マウスでクリックすれば項目を選択できるが、キーボードの矢印キーを押しても何も反応しない状態である。これはまさにアクセシビリティの課題を抱えている状態だ。タブキーでリストの最初の項目にフォーカスすることはできるが、そこから上下の矢印キーで別の項目に移動できないと、キーボードユーザーは困ってしまう。これを改善するのが今回の目標である。
まずは、このリストがどのような仕組みで動いているかを確認する。
リストのHTMLコードを見ると、全体を囲む div 要素に role="listbox" という属性が指定されている。これは、スクリーンリーダーなどの「支援技術」に対して、「これは選択可能なリストですよ」と伝えるための情報であり、アクセシビリティへの配慮の第一歩としては良いスタートである。この div には (click)="handleClick($event)" というクリックイベントも設定されており、リスト内のどこかがクリックされると handleClick 関数が呼び出される仕組みになっている。
リストの各項目は、@for というAngularの構文を使って繰り返し表示される div 要素である。これらには role="option" が指定されており、listbox の中の option であることを示している。また、tabindex という属性が動的に設定されている点も重要だ。tabindex="0" が設定された要素はタブキーでフォーカス可能になり、tabindex="-1" が設定された要素はタブキーではフォーカスできないが、プログラムからならフォーカスできる状態になる。このリストでは、現在選択されている項目、または何も選択されていない場合は最初の項目に tabindex="0" が設定され、他の項目には tabindex="-1" が設定される。これにより、タブキーで常に「アクティブな(選択されている、または最初の)項目」にフォーカスできる仕組みになっている。
次に、これらのHTML要素を操作するTypeScriptのコードを見てみよう。
handleClick(event: Event) 関数は、クリックされた要素がリストの何番目の項目かを判断し、そのインデックスを applyListState(index: number) 関数に渡している。
applyListState(index: number) 関数は、渡されたインデックスの項目を「アクティブ」にするための処理を行う。具体的には、内部的な「アクティブな項目」の状態を更新し、リスト内のすべての項目に対して tabindex を適切に設定し直す(アクティブな項目は 0、その他は -1)。そして、アクティブになった項目にプログラムからフォーカスを当て、さらに OptionDirective という別の部品のアクティブ状態も更新する。この一連の処理によって、クリックした項目が正しく選択され、見た目も更新されるわけだ。
OptionDirective は、リストの各項目に特定の振る舞いやスタイルを追加するための小さなプログラムである。このディレクティブは、内部のアクティブ状態に応じてCSSの active クラスを適用したり、親コンポーネントからフォーカスを受け取ったり、アクティブ状態を設定する setActive() 関数を提供したりする。
ここまでの説明でわかるように、このリストはマウスによるクリック操作には対応しているが、キーボードの矢印キーによるナビゲーションには一切対応していない。これを解決するために、Angular CDK(Component Dev Kit)という、Angularアプリケーションのコンポーネント開発を助けるツールキットの中にある ListKeyManager という強力なヘルパー機能を使用する。
ListKeyManager を使うには、まずAngular CDKをプロジェクトにインストールする必要がある。これは npm install @angular/cdk というコマンドで簡単にできる。
インストール後、この ListKeyManager をリストコンポーネントに組み込んでいく。
最初に、リスト全体を囲む div 要素に (keydown)="handleKeydown($event)" というイベントリスナーを追加する。これにより、リスト内でキーボードが押されたときに handleKeydown 関数が呼び出されるようになる。
次に、TypeScriptのコードで ListKeyManager のインスタンスを管理するためのプロパティ private keyManager?: ListKeyManager<OptionDirective>; を宣言する。OptionDirective は、ListKeyManager が管理する対象がこのディレクティブのインスタンスであることを示している。
setupKeyManager() という新しい関数を作成し、ここで ListKeyManager を初期化する。もし keyManager がまだ存在しなければ、new ListKeyManager(this.options()) として新しいインスタンスを作成する。this.options() はリストの各項目である OptionDirective のインスタンスのコレクションを ListKeyManager に渡す。初期化後、keyManager.setActiveItem(0) で最初の項目をアクティブにし、applyListState(0) でUIを更新する。
そして、先ほどHTMLに追加した handleKeydown(event: KeyboardEvent) 関数の中で、この setupKeyManager() を呼び出し、keyManager が確実に存在するようにする。その後が重要で、this.keyManager.onKeydown(event) を呼び出す。この一行が、キーボードイベント(特に矢印キーなど)を ListKeyManager に処理させる魔法の言葉である。ListKeyManager はイベントを解釈し、次にアクティブにすべき項目を内部的に判断する。処理後、keyManager.activeItemIndex から現在アクティブな項目のインデックスを取得し、再び applyListState を呼び出してUIを更新する。これで、矢印キーによるナビゲーションの基本的な仕組みが完成する。
ListKeyManager を利用する上で、リストの各項目を表す OptionDirective にも変更が必要となる。具体的には、ListKeyManagerOption と Highlightable という二つのインターフェースを実装する必要がある。これらのインターフェースは、ListKeyManager が項目を管理するために必要な特定のメソッド(関数)を提供するよう要求する。
追加するメソッドは以下の通りである。
getLabel(): その項目のラベル(表示テキスト)を返す。setActiveStyles(): 項目をアクティブ状態にするための処理を行う。setInactiveStyles(): 項目を非アクティブ状態にするための処理を行う。 これらを実装することで、ListKeyManagerは項目がどのように表示されるべきか、どのテキストを持つべきかを把握し、適切にアクティブ状態を切り替えることができるようになる。これにより、以前あったsetActive()関数は不要になるので削除する。 また、アクセシビリティをさらに高めるために、OptionDirectiveのホスト要素に[attr.aria-selected]="active() ? true : null"という属性を追加する。これは、項目が選択状態であることをスクリーンリーダーに伝えるための標準的な方法である。選択されていない場合はnullにすることで、属性自体が表示されないようにする。
OptionDirective の変更に伴い、コンポーネント側の applyListState() 関数も更新する必要がある。以前は o.setActive() を直接呼び出していたが、これからは ListKeyManager の規約に従い、o.setActiveStyles() と o.setInactiveStyles() を呼び出すように変更する。これにより、アクティブ状態の管理が ListKeyManager の流儀に統一され、より標準的で堅牢なコードになる。
ここまでで、キーボードの矢印キーでリスト項目をナビゲートできるようになるが、まだいくつか課題が残っている。
一つ目は、マウスで項目をクリックしたときに ListKeyManager の内部状態が更新されないため、キーボードとマウスの操作が同期しない問題である。これは handleClick() 関数の中で setupKeyManager() を呼び出し、さらに keyManager?.setActiveItem(i) でクリックされた項目を ListKeyManager に伝えることで解決する。
二つ目は、最初にタブキーでリストにフォーカスした際、最初の項目がフォーカスはされるものの、アクティブ状態として表示(CSSの active クラスや aria-selected 属性)されない問題である。これを解決するには、リストの option 要素に (focus)="handleFocus()" というイベントを追加し、handleFocus() 関数の中で setupKeyManager() を呼び出す。これにより、リストにフォーカスが当たった瞬間に ListKeyManager が初期化され、最初の項目が正しくアクティブ状態として表示されるようになる。
これらの修正を施すことで、キーボードとマウス、どちらの方法で操作してもリストの項目が常に正しくアクティブになり、ユーザーインターフェースが同期するようになる。
さらに、ListKeyManager には便利なボーナス機能がある。
リストの最後に到達したときに最初の項目に戻ったり(循環ナビゲーション)、Homeキーで最初の項目に、Endキーで最後の項目にジャンプしたりする機能は、ユーザーエクスペリエンスを大きく向上させる。これらの機能は、ListKeyManager を初期化する際に .withWrap() や .withHomeAndEnd() というメソッドをチェーンで追加するだけで簡単に実現できる。自分で複雑なキーイベント処理を書く必要は一切ない。
まとめると、Angular CDKの ListKeyManager を使うことで、これまでマウスでしか操作できなかったWebアプリケーションのリストを、キーボードでも快適に操作できる「アクセシブルなリストボックス」へと簡単にアップグレードできる。HTMLの role 属性でアクセシビリティのセマンティクスを確保し、ListKeyManager を活用することで、複雑なキーボードイベント処理のロジックを自分で書くことなく、クリーンでメンテナンスしやすいコードで、すべてのユーザーにとって使いやすいインターフェースを提供することが可能になる。このパターンは、複数の項目を選択できるリストや、一部の項目が無効化されているリストなど、より複雑なシナリオにも応用できる。