【テクニカル・上級編】リストアイテムの選択状態管理 – HTML実践ガイド

リストの選択状態管理、その「当たり前」を疑う ― `aria-selected` とDOMの深淵

フロントエンドエンジニア諸君、リストの選択状態管理をどう実装しているだろうか?

「`ul` と `li` を並べて、クリックされたら `active` クラスを付与する」。もしあなたがまだその段階にいるのなら、一度立ち止まってほしい。モダンなWebアプリケーションにおいて、アクセシビリティ(A11y)とパフォーマンスは妥協してはならない両輪だ。

本稿では、`aria-selected` を軸とした、堅牢かつスケーラブルな選択リストの設計思想について、ブラウザエンジンの挙動を交えて深掘りしていく。

1. 「クラス切り替え」の呪縛を解く

多くの実装で見かける「CSSクラスの付け替え」による状態管理は、セマンティクス(意味論)において片手落ちだ。支援技術はクラス名など見ていない。彼らが見ているのは、DOMのアクセシビリティツリーである。

`aria-selected` を用いることで、状態は宣言的になり、ブラウザのアクセシビリティAPIと同期する。だが、ここで陥りやすい罠がある。「状態の同期」と「レンダリング」の分離だ。

頻繁に選択状態が変わるリストにおいて、DOM操作を直接行うのは、メモリリークとリフローの温床となる。我々が目指すべきは、状態(State)をソース・オブ・トゥルース(信頼できる唯一の情報源)とし、UIをその投影として扱うアプローチだ。

2. TypeScriptによる型安全な状態定義

状態管理を強固にするには、型定義から逃げないことだ。単一選択(Single Selection)か複数選択(Multi Selection)か。この境界を曖昧にするコードは、後々「選択解除のタイミングでイベントが発火しない」といった幽霊バグを招く。

/

  • 選択状態の定義。Discriminatorを用いた型安全な構成

/
type SelectionMode = ‘single’ | ‘multiple’;

interface ListState {
selectedIds: Set; // Setを使うことで、探索コストを O(1) に抑える
mode: SelectionMode;
}

// 状態変化を通知するカスタムイベント
class SelectionChangeEvent extends CustomEvent> {
constructor(detail: Set) {
super(‘selection-change’, { detail, bubbles: true });
}
}

ここで重要なのは、`selectedIds` に `Array` ではなく `Set` を採用している点だ。リストが数千件に及ぶ大規模データにおいて、要素の存在チェックを `Array.includes()` で行うのは非効率極まりない。`Set` を使えば、レンダリング負荷を最小限に抑えつつ、選択状態の計算コストを一定に保てる。

3. リフロー・リペイントを最小化するレンダリング戦略

DOMの更新は高コストだ。特にリストアイテムが多数ある場合、各アイテムで個別に `setAttribute` を呼ぶのは、メインスレッドを長時間ブロックする原因となる。

ここで推奨したいのは、「仮想的なデータ属性更新」と「データ属性セレクタによるスタイリング」の組み合わせだ。

/ CSSでaria-selectedを直接購読する /
[role=”listitem”][aria-selected=”true”] {
background-color: var(–color-primary-subtle);
border-left: 4px solid var(–color-primary);
}

JavaScript側では、`aria-selected` を直接更新するのではなく、親要素に `MutationObserver` を仕込むか、あるいは状態管理ライブラリからのリアクティブな更新をフックにするのが賢明だ。これにより、JSのロジックとCSSの描画命令を完全にデカップリングできる。

4. 非同期処理と競合の回避(レースコンディション)

Webアプリケーションのリアルな現場では、選択操作がAPI通信を伴うことが多い。

「ユーザーが高速でクリックし、直前のAPIレスポンスが後から到着して、選択状態が巻き戻る」――この現象を避けるために、AbortController の活用を強く推奨する。

let abortController: AbortController | null = null;

const handleSelect = async (id: string) => {
// 以前のリクエストをキャンセルして競合を防ぐ
abortController?.abort();
abortController = new AbortController();

try {
// 楽観的UI更新(Optimistic UI)を適用し、即座にDOMへ反映
updateDOMSelection(id, true);

await api.post(‘/select’, { id }, { signal: abortController.signal });
} catch (err) {
if (err.name !== ‘AbortError’) {
// エラー時はロールバック処理を記述
updateDOMSelection(id, false);
showToast(‘同期に失敗しました’);
}
}
};

5. 最後に:エンジニアとしての矜持

アクセシビリティは「オマケ」ではない。それは、あなたのコードがどれだけ堅牢で、ブラウザというプラットフォームを深く理解しているかの証明だ。

`aria-selected` を正しく扱うことは、単にスクリーンリーダー対応をするだけでなく、データ構造の最適化、描画パイプラインの理解、そして非同期通信の作法までを網羅する、非常に高度なフロントエンド・プラクティスである。

「動けばいい」というコードは誰でも書ける。だが、メモリ効率を考慮し、ブラウザの内部挙動を考慮し、数年後のメンテナーが感謝するようなコードを書くことこそが、スペシャリストの仕事だ。

あなたの書くリストが、今日もどこかのユーザーの操作を心地よく支えていることを願う。

コメント

タイトルとURLをコピーしました