role=table を保ったまま HTML table を行列仮想化する: colgroup固定幅 + スペーサーセル + 可視スライス内rowSpan再計算
React
フロントエンド
CSS
アクセシビリティ
知識
判断
課題: 表の意味的ロール(table/row/columnheader等)やそれに依存する既存の自動テスト・アクセシビリティ挙動を壊せない制約下で、軸の値が数百〜数千件になり本体セルのDOM描画が重い(行×列で組み合わせ爆発する)場合の仮想化手法。
不採用にした代替: 各セルをCSS Gridの絶対位置指定(grid-row/grid-column)で可視範囲だけ描画する一般的な仮想化手法は、table要素そのものを離れる(divベースのグリッドになる)ため、tableロールが失われ、ロールベースの自動テスト(例: getByRole("table"))やスクリーンリーダーの表読み上げが壊れる。ロール保持が不要なら、列幅もauto/max-contentにできるなど自由度が高いこちらの方がシンプルで素直に選ぶべき。
採用した判断: table要素・thead/tbody・colSpan/rowSpan構造を維持したまま、以下の組み合わせで仮想化する。
- 繰り返し発生するデータ列はcolgroupに固定ピクセル幅のcolを宣言し、table-layout:fixedと組み合わせる。列を描画省略しても列幅はcolgroup宣言値から決まるため、横スクロール幅やスクロールバー位置がずれない。
- 可視範囲外の行・列は、colSpanと固定heightを持つ1つのスペーサーセル/スペーサー行に置き換えて、省略した面積を代替する(CSS Gridのようにトラック単位で明示配置する手段がtableには無いため)。
- 複数行にまたがるrowSpan見出しセル(グループ先頭行だけが見出しを持ち、以降の行が結合される構造)は、元データ全体でのグループ境界ではなく、現在描画している可視スライス(配列)を実質的な1つの小さなテーブルとみなして再計算する。可視スライスの先頭から連続して同じグループ値を持つ行数を数え、それをrowSpanにする。これにより、グループの本当の先頭行がスクロールで未描画になっていても、可視スライスの先頭行が代理の先頭として見出しを引き継ぎ、見出しが消えたりズレたりしない。
- 見出し行・見出し列(軸ラベル)自体は行数+列数のオーダーで組み合わせ爆発しないため、仮想化せず常時描画してよい。仮想化が必要なのは行×列で爆発する本体セル部分だけで、見出しまで仮想化すると複雑さが増す割に効果が薄い。
適用条件: 表のロール・既存テスト・アクセシビリティ要件がtable構造に依存している場合。依存がなければCSS Grid方式の仮想化を優先する。
検証方法: 行数×列数が大きいデータで、(a) 初期描画時の本体セルDOM数が理論上の総数より十分少ないこと、(b) 特定の行・列にしか現れない値を持つセルが初期描画時には存在しないこと、(c) スクロール後にそのセルが出現すること、を自動テストで確認する。rowSpan再計算は、可視スライスの先頭がグループの途中になる位置までスクロールしてから見出しが正しく表示される(消えない)ことも別途確認する。