コンテンツにスキップ

データグリッド

hc-datagrid は業務画面のためのヘビーデューティなグリッドです: 多段スティッキーヘッダー固定列、行選択、キーボードによる セルナビゲーション、インライン編集を備えたセマンティックな <table>ページングされたデータのために作られています — htmx が 1 ページ(数十〜数百行)を読み込み、グリッドがそれを描画します。 クライアントサイドの仮想スクロール / ソート / フィルタエンジンでは 意図的にありません: ソート、フィルタ、永続化はサーバ(htmx)に とどまり、セルエディタは既存の HC フォームコントロールそのものです。

別名: データテーブル、一覧表、Excel ライクなグリッド。

プリミティブ状況
position: sticky(ヘッダー + 固定列)Baseline(すべてのエバーグリーンブラウザ)
:has()(任意、適応レイアウト用)Baseline 2023

スクロールビューポート内の標準の <table> です。グループ / サブ / リーフのヘッダーは colspan を持つ通常の <thead> 行。固定列は data-frozen を持ちます(最後の 1 つには data-frozen-edge — これが 固定線を落とします)。デモを横にスクロールしてみてください — チェックボックスと ID の列はピン留めされたままです:

IDGroup AGroup B
AlphaBetaGammaDeltaEpsilonZeta
1456 (wide content)789 (wide content)long value here456789789
2457 (wide content)790 (wide content)another value457790790

position: stickyどこに貼り付くかを知る必要があります: 各 ヘッダー段の top と各固定列の left です。これらは実際の描画サイズ を設定できるよう CSS 変数から来ます:

変数対象意味
--hc-datagrid-head-1-h.hc-datagridグループ(1 段目)ヘッダー行の高さ — 2 段目の top オフセット。
--hc-datagrid-head-2-h.hc-datagridサブ(2 段目)ヘッダー行の高さ — 3 段目のオフセットに加算。
--hc-datagrid-left各固定セルセルの left オフセット = その前にある固定列の合計幅。
--hc-datagrid-foot-1-h.hc-datagrid最後のフッター行の高さ — その上の行の bottom オフセット。
--hc-datagrid-rightdata-frozen-end セルセルの末尾側オフセット = その後ろにある末尾固定列の合計幅。

installDatagrid()これらを自動で計測・設定します(リサイズ時に 再計測も)。静的でスクリプト不要のグリッドでは自分で設定してください: 固定列に固定幅を与え、各固定セルの --hc-datagrid-left に累積幅を、 --hc-datagrid-head-1-h にグループ行の高さを設定します(上のデモの とおり)。

グリッドのスクロールコンテナは .hc-datagrid__scroll 1 つで、 ヘッダー・ボディ・フッターをすべて内包します。ヘッダーが動かないのは スクロール領域の外にあるからではなく、セルposition: sticky だからです。

これを知っておく意味が 2 つあります。

**sticky は行ではなくセルに付いています。**スクロール中に .hc-datagrid__head > tr を測ると動いて見えます。止まっているのは .hc-datagrid__headcell の方です。行を対象にしたテストは、ありもしない 不具合を報告します。

縦スクロールバーはヘッダーの横まで伸びます。スクロールポートが グリッドの上端から始まるため、ブラウザはその全高にバーを描きます — データ部の横だけではありません。バーをヘッダーより下から始めるには、 ヘッダーとボディを別々のスクロールコンテナに分ける、つまり テーブルを 2 つに割る必要があり、いま無償で得ている 3 つを失います。

  • 列幅の一致 — 1 つのテーブルならヘッダーとボディの列幅をブラウザが まとめて決めます。2 つに割ると、行の差し替え・リサイズ・列ドラッグの たびにスクリプトで幅を同期し続けることになります。
  • 横スクロールの同期 — ヘッダーをボディの scrollLeft に毎フレーム 追従させる必要があります。
  • 1 つのアクセシブルなグリッドrole="grid" の行・列の対応関係は 単一のテーブルから導出されます。分割すると、マークアップが自ずと 語っていたことを全セルの aria-colindex / aria-rowindex で明示し 直すことになります。

この取引が見合うことはまれです。バーを動かしたいのではなく目立たなく したいのであれば、細くするだけで足ります。Windows のクラシック スクロールバーをヘッダー横で重く見せている上下の矢印も消えます。

.hc-datagrid__scroll {
scrollbar-width: thin;
/* 任意: scrollbar-color: <つまみ> <軌道>; */
}

既定にしていないのは、スクロールバーの見た目がハウススタイルの判断で あることと、細いバーはタッチでつかみにくいためです。

data-frozen-enddata-frozen の鏡像です: 列はビューポートの 末尾側(LTR では右、RTL では左)に貼り付き、中間の列がその下を スクロールします — 定番の用途は、常に手が届く必要のある行アクション列 です。最初の末尾固定列の data-frozen-end-edge が鏡像のフリーズ ラインを落とします:

<th class="hc-datagrid__headcell" data-frozen-end data-frozen-end-edge
scope="col">Actions</th>
<td class="hc-datagrid__cell" data-frozen-end data-frozen-end-edge>
<button class="hc-button" data-size="sm" type="button">Edit</button>
</td>

ビヘイビアは各セルの --hc-datagrid-right(そのセルの後ろにある 末尾固定列の累積幅)を計測し、列リサイズや htmx スワップをまたいで 正しく保ちます。

<tfoot class="hc-datagrid__foot"> はスクロールビューポートの下端に 貼り付き、ヘッダー帯と同じ見た目になります。集計はサーバーが計算して 描画します — ページ単位かクエリ単位かはセルのテキストが語ります — CSS は貼り付けるだけで、クライアント側では何も合計しません:

<tfoot class="hc-datagrid__foot">
<tr>
<td class="hc-datagrid__cell" data-frozen>Total</td>
<td class="hc-datagrid__cell" data-numeric>1,680</td>
<td class="hc-datagrid__cell" data-frozen-end></td>
</tr>
</tfoot>

フッター行は上方向に積み重なります(例: 合計行の上に小計行)。 installDatagrid() が積み重ねオフセット用に --hc-datagrid-foot-1-h を計測します。フッターセルはボディセルと同様に data-frozen / data-frozen-end / data-numeric を取り、グリッドパターンのロールを 得ますが、キーボードナビゲーションには入りません — 集計は編集可能な ストップではないからです。集計が常にページと一致するよう、<tfoot> は 行と一緒に再描画してください(同じフラグメント、または OOB スワップ)。

ヘッダー名が列のデータよりずっと長いときは、列を広げる代わりにラベルを 回転させます。ヘッダーセルに data-orientation="vertical" を足すと、 ラベルは上から下に読まれ(CJK は正立、ラテン文字は回転)、列はデータの 幅のまま狭く保たれます。リーフのヘッダー行(高さに制約のない一番下 のスティッキー行)で使ってください:

ProductVery long header name 2Discontinued flagReorder level
Chai18no10
Chang19no25

2 つの向きがあり、どちらも純 CSS です — ビヘイビア不要:

data-orientationwriting-mode読む向き向いている用途
verticalvertical-rl上 → 下(CJK は正立、ラテン文字は回転)安全なデフォルト。CJK 混在 / 日本語主体のヘッダー
sidewayssideways-lr下 → 上(行全体を回転)ラテン文字 / 「軸ラベル」スタイル

vertical-rl が最も広くサポートされています。sideways-* は新しめ です(Chromium / Firefox。対象の Safari を確認してください)。完全に 制御したいときは、セルまたはグリッドの --hc-datagrid-head-writing-mode を自分で設定します(例: sideways-rlvertical-lr)。

RegionUnits soldReturnsBackorders
North41283
South37650

その上のグループ / サブヘッダーは水平のままにしてください。スティッキー の積み重ねを単純に保つため、回転させるのはリーフ行だけにします。

ヘッダーに data-resizable + data-col を、その列のボディセルに同じ data-col を付けると、列がリサイズ可能になります。installDatagrid() がヘッダーの右端にグリップを追加します: ドラッグするか、フォーカス して矢印キーを使います(Shift で大きなステップ)。その列だけが 固定幅になり(省略記号でクリップ)、他の列はコンテンツベースの幅を保ち ます。

<thead class="hc-datagrid__head">
<tr>
<th class="hc-datagrid__headcell" data-resizable data-col="name" scope="col">Name</th>
<th class="hc-datagrid__headcell" scope="col">Fixed</th>
</tr>
</thead>
<tbody class="hc-datagrid__body">
<tr class="hc-datagrid__row">
<td class="hc-datagrid__cell" data-col="name">Chai…</td>
<td class="hc-datagrid__cell">x</td>
</tr>
</tbody>

変更のたびにグリッドは hc:datagridcolumnresize (detail: { col, width })をディスパッチします — さらにディスパッチの に、確定した幅を input[data-hc-datagrid-width="<col>"] (グリッドに最も近い <form> 内、なければドキュメント全体)へミラー するため、イベント起動の htmx リクエストは新しい値をシリアライズ します。 datagrid-prefs レシピ がこれをユーザーごとの永続化にします。サーバーは記憶した幅を インライン幅 + data-resized として描画し返します。 グリップをダブルクリック(またはフォーカスして Enter) すると、列を最も広い描画済みセルに自動フィットします — 確定した 幅は同じイベント/ミラーのパイプラインを流れます。グリップは キーボード操作可能な role="separator" で、aria-valuenow を 持ちます。JS が生成する要素で、クラスは hc-datagrid__resizer — 他のパートクラス同様にスタイルを上書きできます。

ヘッダーに data-sortable(と data-col キー)を付けます。ビヘイビア がそれをフォーカス可能にし、クリック / Enter / Spacearia-sortnone → ascending → descending → none とトグルし( / / のインジケーターつき)、 hc:datagridsort をディスパッチします — グリッドはサーバ ページングなので、サーバがソートしてページを返します。

<th class="hc-datagrid__headcell" data-sortable data-col="price" scope="col">
Price
</th>
grid.addEventListener('hc:datagridsort', (e) => {
// e.detail = {
// col: 'price', direction: 'asc' | 'desc' | null,
// sorts: [{ col, direction }, …] // 順序つきのソートセット全体
// }
});

素のアクティベーションは単一列です(他の列はクリアされます)。 Shift+クリック / Shift+Enter は代わりに その列をソートセットに追加します。2 列以上がソートされると各 ヘッダーは data-sort-index="1…n" を持ち、インジケーターが序数を 表示します(↑1↓2)。detail.sorts が順序つきセットの全体で、 ワイヤの慣例形式は ?sort=name,-price(先頭の - が降順)です:

grid.addEventListener('hc:datagridsort', (e) => {
const sort = e.detail.sorts
.map((s) => (s.direction === 'desc' ? `-${s.col}` : s.col))
.join(',');
// → "name,-price"
});

クライアントページソート(オプトイン): data-sortable="client" はサーバーを待たず、描画済みのページ行を DOM 内でソートします — 両方の値が数値としてパースできれば数値比較(セルテキストより data-value を優先)、そうでなければロケール文字列比較です。小さく 全件ロード済みのテーブル向けに深化プランが明示的に許容したモードです。 htmx スワップはサーバーの順序を復元します — それが正しい挙動です。 指示イベントは引き続きオブザーバー向けに発火します。素の data-sortable はサーバー指示のままです。

ソートはフォームのフィールドに入れ、フォームに運ばせます。

<form id="filters" data-hx-get="/orders" data-hx-target="#rows"
data-hx-trigger="submit, hc:datagridsort from:body">
<input type="hidden" name="sort" data-hc-datagrid-sort>
…フィルタのコントロール…
</form>

installDatagrid()hc:datagridsort を発火する前に、グリッドの 最も近い <form>(なければドキュメント)内のすべての input[data-hc-datagrid-sort]順序つきのソートセット全体を書き 込みます — 列幅の環境設定 と同じ仕組みです。イベント起点のリクエストは必ず最新値を載せます。

data-hx-vals ではなくフォームに入れることで、2 つが自然に手に入ります。

  • フィルタの Apply でソートが維持されます — 絞り込みは同じフォームを 再送信するので、順序が黙ってリセットされることがなくなります。
  • 保存ビューがソートを含みますsaved-views はフォームのフィールドを保存するからです。並び順を忘れるビューは、 ビューとして半分しか機能していません。

インジケーターがスワップ後も残るよう、各ヘッダーは現在の aria-sort(複数列ソート時は data-sort-index も)つきで描画して ください。

フィールドを足したくない場合、detail.sorts は従来どおり data-hx-vals から使えます。ただし単一列の sort + dir の組では なく、そこから同じ sort=name,-price の文字列を組み立ててください。 そうしないと複数列ソートが往復できません。

セルはデフォルトで数字を等幅数字 (font-variant-numeric: tabular-nums)でレンダリングするため、桁の 並びは列方向に垂直に揃います。このプロパティは数字グリフにしか影響せず、 テキストセルは不変です。右揃えは列ごとの意味論のままです: セル / ヘッダーセルの data-numeric が末尾揃えにします (text-align: end の論理値 — RTL では自動で反転)。data-sortable とも合成できます — 数値列は最もソートされる列であり、ソート インジケータは末尾揃えのラベルに追従するだけです。

<th class="hc-datagrid__headcell" data-numeric data-sortable data-col="amount">Amount</th>
<td class="hc-datagrid__cell" data-numeric>1,234.50</td>

数値セルのインライン編集でも末尾揃えは保たれます — マウントされたエディタがセルの text-align を再継承するので、編集 モードに入っても値がジャンプしません。

条件付き書式はサーバーのルールと CSS の塗りです: サーバーが条件を 評価し、その結果data-tone="info | success | warning | error" としてセル・行・レコード <tbody> に描画します。スタイルシートは共有の ステータスカラーでティントします(ダークテーマ対応、固定列でも安全な グラデーション):

<td class="hc-datagrid__cell" data-numeric data-tone="error">-12%</td>
<tr class="hc-datagrid__row" data-tone="warning"></tr>

ティント色は datagrid.tone-* トークン(セマンティックステータス ランプを参照)から来ます — 独自の濃さにするには --hc-datagrid-tone-<tone>-bg/-fg を上書きします。色だけに頼らないで ください: 値、またはアイコン/テキストのマーカーをセルに残します (forced colors ではティントは点線アウトラインになります)。

import { installDatagrid } from '@hypermedia-components/core';
installDatagrid(); // or the auto-init bundle: @hypermedia-components/core/behaviors

サーバ描画のテーブルをインタラクティブなグリッド (WAI-ARIA grid パターン) にアップグレードします: role="grid" とボディセルへのロービング tabindex を適用し、スティッキーオフセットを計測し、選択を配線します。 ネットワーク取得は決して行いません — ページングと永続化は htmx / サーバのままです。冪等で、アンインストーラを返し、htmx でスワップされた グリッドと行は MutationObserver で拾います。

キー動作
矢印キーアクティブセルを移動
Home / End行の最初 / 最後のセル
Ctrl + Home / End最初の行の最初のセル / 最後の行の最後のセル
Page Up / Downビューポート分の行を移動
Spaceアクティブ行の選択をトグル
Shift + 矢印 / Shift + クリックアクティブセルからセル範囲を拡張
Ctrl/Cmd + C範囲(なければアクティブセル)を TSV としてコピー
Ctrl/Cmd + Aページ内のすべての行を選択
Escapeセル範囲をクリア

グリッドは単一のタブストップです。セル内のウィジェットは個別のタブ ストップになりません。Space は行のチェックボックスと aria-selected をトグルします。ヘッダーの全選択チェックボックスはすべての行をトグル します(選択が部分的なときは不確定状態)。選択の変更はグリッド上で hc:datagridselectionchange(detail: { selected, total })を発行 します。

Shift+矢印(または Shift+クリック)はアクティブセルから矩形の セル範囲を拡張します。範囲内のセルは data-in-range を持ち、 選択ティントで塗られます。Ctrl/Cmd+C は範囲を TSV(セルはタブ、 行は改行区切り)としてクリップボードに書き込みます — そのまま スプレッドシートに貼り付けられます。複数スロットにまたがるセルは、 矩形内で最初に覆われたスロットに一度だけテキストを出力します。

書き込みの前に、グリッドはキャンセル可能な hc:datagridcopy (detail: { text, rows, cols })をディスパッチします。 preventDefault() でコピーを引き取り、より豊かなペイロードを自前で クリップボードに置けます。範囲は純粋に表示上の状態です — 行の htmx スワップでクリアされ、サーバーには何も送信されません。

状態は属性で表現され、コンポーネントがスタイルします:

属性対象効果
aria-selected="true".hc-datagrid__row選択行の背景。
data-active.hc-datagrid__cellアクティブセルのフォーカスリング(キーボードビヘイビアが設定)。
data-highlight.hc-datagrid__cell列 / セルのハイライト帯。
data-in-range.hc-datagrid__cellセル範囲選択のティント(キーボードビヘイビアが設定)。
data-toneセル / 行 / レコード条件付き書式のティント — info / success / warning / error(サーバーが描画)。
data-editing.hc-datagrid__cell編集モード — エディタがセルを満たすようパディングが落ちる。
data-pending.hc-datagrid__cell行再描画を待つ楽観的コミット(グリッドの data-hc-datagrid-pending でオプトイン)。
data-invalid.hc-datagrid__cellサーバーが拒否した値 — エラーリング + ティント(422 再描画が描画)。
data-attention行 / セル / レコード / ヘッダーセルこの行(または列)がユーザーの対応を要する — error(直す必要がある)/ warning(判断が要る)。どのティントにも塗りつぶされない端のバー。
data-alt.hc-datagrid__rowゼブラ縞(グリッドがオプトインしたときビヘイビアが付与)。

これらの状態は同じセルに同時に載ります。失敗行が再試行のために選択 されつつホバーされる、書式付きの値が選択行の中にある、といった具合 です。何も失われないよう、2 つのチャネルに分けて描画します。

背景チャネルはティント 1 つだけで、1 つの状態しか持てません。 以下は梯子で、後のものが勝ちます:

data-tone:hoverdata-pendingdata-highlightdata-in-rangearia-selected:targetdata-invalid

選択が条件付き書式に勝つのは、選択こそがユーザーが今まさに操作して いる状態であり、書式付きの値はティント越しでも読めるからです。 data-invalid は最上位ですが、覆うのは該当セル 1 つだけです — 行の残りは選択ティントを保つので、選択行の中の拒否セルは両方として 読めます。

アテンションチャネルは背景に一切触れません。拒否セルのリングと 隅のフラグ、data-attention の端バー、保存中のスピナーがこれです。 下にどのティントが塗られていても残ります。

一括失敗のレポートが機能するのはこの仕組みのおかげです。失敗行には data-tone="error" ではなく data-attention="error" を付けます:

<tr class="hc-datagrid__row" id="row-101" data-attention="error"
aria-selected="true"></tr>

行は選択ティント(これから再送信する再試行対象が見える)と、エラー バーの両方を保ちます。data-toneが何を意味するか、 data-attentionが何を必要としているかに使ってください。

.hc-datagrid__headcelldata-attention は該当を示します。 30 列あるグリッドでセルが画面外にスクロールされていても、原因を 見つけられるようになります。

severity は行が何を必要としているかで選びます。いつ判明したかでは ありません。そうしないと、状況が何も変わっていない行がアクション実行 前は warning、実行後は error になってしまいます。

  • error — 先に進むには何かを直す必要がある: 必須値の未入力、 不正な入力、状態違い(「出荷済み」)、権限なし。必須入力チェックは プリフライトを含め、どこに現れても error です。
  • warning判断が要る。値自体は正しい: 未来日の出荷日、 規定を超える割引など。

グリッドに data-hc-zebra を付けてオプトインします:

<div class="hc-datagrid" data-hc-zebra></div>

installDatagrid() が再構築のたびに data-alt を交互の行へ付与します。 :nth-child() ではこれを正しく表現できません:

  • 折りたたまれたグループで隠れた行も数えてしまうため、グループを 閉じた瞬間に縞がずれます。
  • レコード単位で交互にできません。3 行にまたがる .hc-datagrid__record は 1 ブロックとして縞を付けないと、レコードが 1 つのまとまりとして読めなくなります。

どちらも rebuild() が既に知っていることなので、そこで付与します — 表示中の行に対して、レコード 1 つにつき 1 段階です。

縞は梯子の最下段なので、ホバー・選択・アテンションバーは縞付きの行の 上でも見えたままです。固定列も縞を保ちます(ティントは不透明な frozen-bg の上に描かれます)。

オプトインしなければビヘイビアは data-alt に触れないので、サーバーが 直接描画すれば JavaScript なしで動きます — グループもレコードも ないフラットなグリッドにはそれが適切です。

installDatagridActions() はグリッドの選択を、一括アクションの コントロールを収めたバーにミラーリングします。バーは data-hc-datagrid-actions="<selector>" で対象グリッドを宣言します。 [data-hc-datagrid-count] の子が翻訳済みの件数を表示し、何も選択され ていない間バーは hidden です:

<form method="post" action="/products/bulk">
<div class="hc-toolbar" role="toolbar" aria-label="Bulk actions"
data-hc-datagrid-actions="#grid" hidden>
<span data-hc-datagrid-count></span>
<button class="hc-button" type="submit" name="action" value="archive"
data-hx-post="/products/bulk"
data-hx-target="#rows" data-hx-swap="innerHTML"
data-hx-disabled-elt="this">Archive</button>
</div>
<div class="hc-datagrid" id="grid"></div>
</form>

バーはグリッドの hc:datagridselectionchange イベントをリッスンします — 初期状態はインストール時に選択属性から読まれ、tbody 内で行がスワップ されるたびにグリッドが再発行するため(全選択チェックボックスも再同期)、 行を再描画する一括アクションは追加の配線なしにバーをクリアします。

件数メッセージは i18n キー datagrid.selected(デフォルト {selected} selected{total} パラメータも利用可能)で、件数要素には デフォルトで role="status" が付くため、変更は丁寧に読み上げられます。 完全なワイヤ契約 — 行チェックボックス id のネイティブなフォーム シリアライズ、サーバレスポンスの形、no-JS の経路 — は datagrid-bulk-actions レシピを 参照してください。

グループ化は描画の選択です: サーバーが見出し行 — hc-datagrid__row hc-datagrid__grouprow に、グループラベルと サーバーが描画すると決めた集計を収めた colspan セルを 1 つ — を行間に差し込み、ビヘイビアがクリック / Enter / Space でグループの行をトグルします。クライアント側では何も グループ化も合計もしません。折りたたみは純粋な表示状態です(行は すでにページ上にあります):

<tbody class="hc-datagrid__body">
<tr class="hc-datagrid__row hc-datagrid__grouprow" data-group-level="1">
<td class="hc-datagrid__cell" colspan="3">Fruit — Σ 30</td>
</tr>
<tr class="hc-datagrid__row"></tr>
<tr class="hc-datagrid__row"></tr>
</tbody>
  • 見出しのセルaria-expanded を持ちます(gridcell で有効。 ビヘイビアのデフォルトは "true")— "false" で描画すると 折りたたみ状態で始まります。キャレット( / )が追随します。
  • data-group-level="1…3" でネストします: 折りたたみは次の同位以上の 見出しまでをすべて隠し、再展開しても折りたたまれたサブグループは 折りたたまれたままです。レベル 2・3 はインデントされます。
  • グループ見出しは通常の行としてキーボードナビゲーションに参加します (スパンする 1 セルが 1 ストップ)が、選択ユニットではありません — 全選択とアクションバーの件数はデータ行だけを数え、折りたたみが 選択を変えることはありません。
  • トグルは hc:datagridgrouptoggle { row, expanded } を 発行します。
  • ソートやページングとの組み合わせはサーバーの関心事です: グループ 見出しはフラグメント内のただの行なので、再描画がアトミックに 置き換えます。(グループレイアウトはフラット行のグリッド向けです — 複数行レコードでは代わりにレコードごとの tbody を使ってください。)

階層を遅延ハイパーメディアとして: すべての行が aria-level を持ち、 展開可能な行は aria-expanded と先頭セルの data-hc-datagrid-tree トグルを持ちます。子は 1 レベル深い兄弟行です — サーバー描画済み、 または htmx で一度だけロード(ワイヤ契約は datagrid-tree レシピ)。 ツリートグルが存在するとビヘイビアはテーブルを role="treegrid" に格上げします — 行レベルの aria-level / aria-expanded が有効に なるロールです。トグルのクリックか先頭セルの Enter で開閉。 折りたたみはロード済みサブツリーを隠し(隠れた行はキーボード ナビゲーションから抜けます)、再展開は折りたたまれた子を尊重し、 トグルは hc:datagridtreetoggle { row, expanded } を発行します。 遅延行の初回展開は data-loaded を付け、aria-busy を立て、htmx の ために hc:datagridtreeload をディスパッチします。レベル 2〜4 は 先頭セルをインデントします — ステップは --hc-datagrid-indent で 変更できます。

密度の高い業務画面では、1 レコードを複数行にまたいで表示することが よくあります(例: 1 行目にコード / 商品、2 行目に数量 / 単価、3 行目に 利益)。各レコードをサブ行の <tbody class="hc-datagrid__record"> として モデル化し(<table> は複数の <tbody> 要素を持てます)、先頭列 (No. / 選択)を rowspan でまたがせます。ヘッダーは通常の多段 <thead> で、サブ行ごとに 1 つのヘッダー行を持ちます。

No.CodeProduct
QtyUnit price
1D0006Better Roast Ham
12 boxes$14,000
2D0004Tasty Base
47 boxes$17,250

installDatagrid() は各レコード <tbody>単一の選択単位として 扱います: レコードのチェックボックス(または Space)はそのすべての サブ行を選択し(各行に aria-selected<tbody>data-selected)、 全選択と hc:datagridselectionchange はレコード単位で数え、アクティブ セルを含むレコードには data-current が付きます(先頭の rowspan セルがアクセント表示)。レコード間は太めの境界線で区切られ、レコード内 のサブ行は薄い線で分けられます。キーボードナビゲーションは視覚上の 位置で移動します: ↑/↓ はサブ行やレコードをまたいでも同じ視覚列に とどまり(rowspan/colspan は解決されるため、↓ の後の ↑ は開始セル に戻ります)、先頭の rowspan セルのようなまたがるセルは、それがまた ぐどのサブ行からも ←/→ で到達できる単一のストップです。単一行の グリッド(1 つの <tbody class="hc-datagrid__body">)は変わりません。

レコードに開閉トグルを与えて、任意の HTML の詳細パネル — ネストしたグリッド、フォーム、チャート — を開きます。レコードの先頭 セルに [data-hc-datagrid-toggle] ボタンを置き、レコード <tbody> の 最後の行として .hc-datagrid__detail-row(1 つの colspan セルを持つ <tr>)を追加します。+/− ボタンをクリック(またはそのセルで Enter) でトグルします:

DetailCategoryDescription
BeveragesSoft drinks, coffees, teas

Detail panel — any HTML here (a nested table, form, chart, …).

installDatagrid() はレコードの data-expanded をトグルし、詳細行を 表示 / 非表示にし、aria-expanded / aria-controls を同期し、 hc:datagridexpand / hc:datagridcollapse(detail: { record })を ディスパッチします。<tbody>data-expanded を置けばレコードは 開いた状態で始まります。詳細パネル内のネストした hc-datagrid は 独立にアップグレードされ操作されます — 外側のグリッドはそこからバブル するイベントを無視します。

htmx での遅延読み込み — 詳細セルに data-lazy を足します。初回の 展開でビヘイビアはセル上で hc:datagriddetailload を発火し、 ビジースピナーを表示します(aria-busy="true")。そのイベントに htmx を配線してコンテンツを取得すれば、コンテンツがスワップされ次第スピナー は消えます。再展開では再読み込みしません。

<tr class="hc-datagrid__detail-row">
<td class="hc-datagrid__detail" colspan="3" data-lazy
data-hx-get="/categories/1/products"
data-hx-trigger="hc:datagriddetailload"
data-hx-target="this" data-hx-swap="innerHTML">
<!-- filled on first expand -->
</td>
</tr>

切り詰めとオーバーフローツールチップ

Section titled “切り詰めとオーバーフローツールチップ”

値が列に対して広すぎるときは、省略記号つきの 1 行にクリップし、ホバー / フォーカスで全文を表示します。値を .hc-datagrid__truncate で包み、 --hc-datagrid-truncate-max かインラインの max-inline-size で固定幅 (列のコンテンツ幅)を与えます:

<td class="hc-datagrid__cell">
<span class="hc-datagrid__truncate" style="max-inline-size: 12rem">
Data 1 xxxxxxxxxxxxxx
</span>
</td>

切り詰めを機能させるのはこの固定幅です: 列の max-content に上限を かけるため、テーブルが単純に広がって収めてしまうことがなくなります。 installDatagrid() はこれらの要素を監視し、テキストが実際にクリップ されているときだけ(scrollWidth > clientWidth)、ホバー キーボードフォーカスで、単一の共有されたスタイル済みツールチップに全文 を表示します — セルごとにツールチップを持たないので、数百セルの グリッドにもスケールします。ツールチップは JS が生成する hc-datagrid__tooltip クラスの要素で、--hc-tooltip-* トークンを 再利用します。

自分自身のメッセージを持つセル — サーバーが描画した data-invalid や、hc-tooltip を指す aria-describedby を持つセル — はオーバーフローツールチップを 抑制します。1 つのホバーに 2 つの意味が乗るのは不具合であり、エラーが 勝ちます。クリップされたテキストも読ませたい場合は列を広げてください。

業務のグリッドは口頭で語られます。「137行目が落ちたやつ」。レコード ID はシステムには正しく、その文には合いません。そこでグリッドは 位置も持てるようにします。

番号を振るのはサーバー(結果セット上の位置)で、ビヘイビアは ARIA の番号を導出します。ARIA が数えるのはヘッダー行を含む DOM の行です。

<div class="hc-datagrid" data-row-total="5000">
<tr class="hc-datagrid__row" id="row-4901" data-row-no="137">
<td class="hc-datagrid__cell" data-numeric>137</td>
<th class="hc-datagrid__cell" scope="row"><a href="/orders/4901">SO-4901</a></th>

installDatagrid() が table に aria-rowcount を、ヘッダー行と番号付き の各行に aria-rowindex を書きます。ヘッダー分のオフセットもこちらで 足します。ここを間違えるのはスクリーンリーダー無しでは誰も気付かない ずれなので、サーバーには要求しません。

番号を正直に保つ規則が 2 つあります。

  • **序数は所在、ID が同一性。**ソートや条件が変われば序数は動くので、 保存されるもの(一括エラーのレポート、共有リンク)が名指しするのは ID で、序数は表示するだけです — SO-4901 (row 137)
  • **数えるのは結果セットで、ページではありません。**2 ページ目の 12 行目 は 52 行目です。aria-rowcount / aria-rowindex が無いページ送り グリッドは、4 ページ目で「40 行中 3 行目」と読み上げます。単純に嘘です。

data-row-total を省略すると不明の意味になり、table には aria-rowcount="-1" が入ります。まだ読み込み途中の 無限グリッド にとってはそれが正直な答えです。data-row-no の無い行(グループ見出し、 クライアントが挿入したツリーの子)は、持っていない位置を与えられるより 番号なしのままにします。

行へのリンク(#row-101 — 一括エラーレポートの項目や、ページへの ディープリンク)は、ネイティブに行をスクロール表示します。ブラウザの 履歴 — つまり戻る — がそのまま働くのが利点です。着地を実用的に するのは次の 2 点です:

  • installDatagrid()アクティブセルをその行の先頭セルに移し、 フォーカスします。キーボードとスクリーンリーダーの利用者も視線と 同じ場所に到達し、そのまま矢印キーで続けられます。ロード時と hashchange で動作し、このグリッドに存在しないハッシュは無視され ます。
  • ハッシュはセルを指すこともできます(#cell-101-ship-date)。 幅の広いグリッドでは行への着地だけでは足りません — 30 列のどれが 拒否されたのかを探す必要があり、その列は画面外にスクロールされて いるかもしれません。セルへのリンクなら両軸をスクロールして着地 します(スティッキーヘッダーと固定列の両方を避けます)。どの列が 原因かサーバーが分かっている場合は、セルにリンクしてください。
  • 行は :target で強調され(点滅ではなく持続)、計測済みヘッダー高から 導いた scroll-margin-block-start を持つため、スティッキーヘッダーの 下に潜り込みません。

そのために行には安定した id が必要です(id="row-<id>" をサーバーで 描画)。別ページの行へは実 URL(/items?focus=101#row-101)で到達 し、その行を含むページをサーバーが描画します。

業務グリッドは、ユーザーがセルに触れる前に 3 つの問いに答える必要が あります — 編集できるか値が必須かロックされているかgridcell はこの 3 つの語彙をすべてサポートしており、ビヘイビアは すでに書かれているものから導出するので、追加で書くことはありません。

状態読み上げ導出元
編集可・必須aria-required="true"列のエディタテンプレートのコントロールが持つ required
編集可・任意どちらの属性もなしdata-editable + 対応するエディタテンプレート
ロックaria-readonly="true"data-editable がないこと

3 つの規則がこれを正直に保ちます。

  • **サーバーが描画した値が常に勝ちます。**行に依存する必須性(「受注が オープンな間だけ必須」)はサーバーのルールです — セルに aria-required を描画すれば、ビヘイビアは手を触れません。
  • 全体が読み取り専用のグリッドは 1 度だけ宣言します(グリッド要素 に)。全セルで繰り返しません。
  • **編集可否はセル単位なので、行の状態はそのまま扱えます。**未出荷は 編集可・出荷済みはロック、というのは単にサーバーがロック行に data-editable を描画しないだけです。行のスワップですべてが再導出 されるので、読み上げはデータに追随します。

編集可能セルには既定でホバー / フォーカス時のアフォーダンス (カーソルと控えめな内側の枠)が付きます — ユーザーがそのセルを見て いるまさにそのときに現れ、平常時は静かです。200 行のグリッドではこれが 効きます。常時のマークが必要なら、グリッド単位でオプトインし、 例外の側をマークします。

<!-- ほぼ読み取り専用のグリッド → 編集できる方をマーク -->
<div class="hc-datagrid" data-hc-editable-hint="editable">
<!-- ほぼ編集可能なグリッド → できない方を沈める -->
<div class="hc-datagrid" data-hc-editable-hint="readonly">

必須は aria-required="true" が付いている場所に * で示されます。 列全体が必須なら列ヘッダーに(1 度だけ、印刷にも残ります)、行に よって変わるならセルに付けてください。どちらにするかはサーバーが 決めます — ルールを知っているのはサーバーだけだからです。色だけに 頼ることはありません: マーカーはテキストであり、状態はいずれにせよ アクセシビリティツリーに入ります。

「必須なのに空」は列の性質ではなく行のエラーです。これには 編集フィードバックのサーバー描画の data-invalid / data-tone の語彙を使ってください。

編集は、専用のエディタエンジンではなく既存の HC フォームコントロールを 再利用します。編集可能セルdata-editable と、列を名指す data-col(コード化された値なら data-value も)を持ちます。列の エディタはコントロールを収めた <template data-datagrid-editor data-col="…"> です。installDatagrid() はアクティベーション時にそれをセルへクローンし、セルの現在値でシード し、フォーカスします:

<div class="hc-datagrid">
<template data-datagrid-editor data-col="qty">
<input class="hc-input" type="text" aria-label="Quantity">
</template>
<template data-datagrid-editor data-col="code">
<!-- a searchable select, reusing hc-combobox -->
<div class="hc-combobox">
<input class="hc-combobox__input hc-input" role="combobox"
aria-controls="code-list" aria-haspopup="listbox" autocomplete="off">
<ul class="hc-combobox__listbox" id="code-list" role="listbox" popover>
<li class="hc-combobox__option" role="option" data-value="001">Code A</li>
<li class="hc-combobox__option" role="option" data-value="002">Code B</li>
</ul>
</div>
</template>
<div class="hc-datagrid__scroll">
<table class="hc-datagrid__table">
<!-- … -->
<td class="hc-datagrid__cell" data-editable data-col="qty">3</td>
<td class="hc-datagrid__cell" data-editable data-col="code" data-value="001">Code A</td>
</table>
</div>
</div>

エディタの種類は、すでに使っているコントロールに対応づけます: テキスト → hc-input、日付 → hc-input[type=date]、セレクト → hc-select、 検索可能セレクト → hc-combobox。(コンボボックスのリストボックスは popover を使うため、ドロップダウンはグリッドのスクロールクリッピング から逃れられます。)

アクティベーション: アクティブな編集可能セルで Enter / F2 / ダブルクリック、または単に入力を始める(最初の 1 文字がエディタを シードします。Excel スタイル)。IME 経由の入力にも対応しています — 変換中のキーストロークはエディタをシードなしで開き、変換セッションを フォーカスされた入力欄に引き渡すため、日本語入力がセルに飲み込まれる ことはありません。コミット: Enter、セル外への フォーカス移動、またはコンボボックスならオプションの選択。 キャンセル: Escape(元の値を復元)。

エディタを開いたまま行が差し替わった場合 — SSE 更新、他ユーザーの 変更、ページャの再読み込み — 開いていた編集は破棄されます。その行自体が なくなっているからです。グリッドは編集状態を解除するので、新しい行で キーボード操作は動き続けます。黙って失われるのが許容できない場合は、 リモートの行更新を edit-conflict 契約 と組み合わせ、コミットが上書きではなく拒否されるようにしてください。

バリデーション: エディタコントロールのネイティブ制約がそのまま API です — テンプレートの入力に requiredpatternmin / maxmaxlength を与えると、無効な値はコミットをブロックします: エディタは ネイティブメッセージ(reportValidity())を表示したまま開き続け、 何も書き戻されず、イベントも発火しません。Escape は引き続き キャンセルします。サーバー側の拒否は inline-edit レシピの 422 契約のままです。

コミット時に値は書き戻され(data-value + セルの表示テキスト)、 グリッドは hc:datagrideditdetail: { cell, col, value, label, oldValue } つきでディスパッチ します。htmx で永続化してください — 例えば行またはグリッドに:

<tbody class="hc-datagrid__body"
data-hx-trigger="hc:datagridedit"
data-hx-patch="/rows" data-hx-include="closest tr">
</tbody>

コミットは楽観的です — サーバーが応答する前にセルは新しい値を 表示します。フィードバックのループは 3 つの追加ピースで閉じます (edit-feedback プラン):

  • data-pending — グリッドラッパーに data-hc-datagrid-pending があるとき、値が変わったコミットは編集セルに data-pending + aria-busy="true"(ビジーティント + スピナー)を付け、サーバーの 行再描画が置き換えるまで保ちます。意図的なオプトインです: 再描画契約が前提で、永続化の配線なしでは何もクリアしません。

  • data-invalid — サーバーの 422 再描画が拒否されたセルに 付けます(aria-invalid="true" と、エラー行のメッセージを指す aria-describedby も描画してください)。セルはサーバーの値を 表示し、エラー行が送信された値を保存します。Enter で再編集 します — エディタは自動では再オープンされません。

  • マーカーは列幅を変えません。テーブルは max-content でサイズが 決まり、セルは折り返さないため、セルへのあらゆるインライン追加 — スピナー、バッジ、「詳細」リンク — はその列を広げてレイアウトを ずらします(実測: 小さなリンク 1 つで 76px → 121px)。data-resized の列では逆にクリップされて消えます。したがってグリッド自身の マーカーはすべて絶対配置です: 保存中スピナー、拒否セルの隅の フラグ、セル単位の必須 * は、いずれもセルのパディング内に置かれ、 レイアウトコストはゼロです。独自のマーカーも同じ規則に従い、説明用の リンクはデータセルの中ではなくレポートか専用列に置いてください。

  • .hc-datagrid__error-row — メッセージスロット: 行の直下に サーバーが描画する <tr> で、colspan.hc-datagrid__error セルを 1 つ持ちます。フォーカスを奪わず 読み上げるため、role="alert" は内側の要素に付けてください。 詳細行と同様、キーボードナビゲーションには入りません。

  • data-attention="warning" — 4 つ目の結果: 値としては受理可能 だが異常な場合(未来日の出荷日、規定を超える割引など)、サーバーは 200 を返し、セルには提案値を、その下に data-tone="warning" の メッセージ行(確認 / 取り消し)を描画します。まだ何もコミットされて いません。これに data-pending を使わないでください。あの状態は 「サーバー待ち」を意味してスピナーを描きますが、ここで待っているのは ユーザーです。

422 / 409 の完全なワイヤ契約は datagrid-edit-errors / datagrid-edit-conflict レシピが担います。前者は確認付き警告の分岐(確認トークンを値に束縛 しなければならない理由を含む)も担います。

すべてのイベントはバブリングする CustomEvent なので、グリッド(または 祖先、htmx の data-hx-trigger)へのリスナー 1 つですべてを捕捉できます。 hc:datagriddetailload を除き、いずれも .hc-datagrid ルートで ディスパッチされます:

イベントディスパッチ先detail
hc:datagridsort.hc-datagrid{ col, direction, sorts } — direction は 'asc' / 'desc' / nullsorts は順序つきソートセットの全体。
hc:datagridcolumnresize.hc-datagrid{ col, width } — 新しい幅(px)。
hc:datagridcopy.hc-datagrid{ text, rows, cols } — キャンセル可能。preventDefault() でクリップボード書き込みを引き取れます。
hc:datagridselectionchange.hc-datagrid{ selected, total } — レコード / 行の単位で数えます。
hc:datagridedit編集されたセル(行 / レコード / グリッドへバブリング){ cell, col, value, label, oldValue } — コミット時のみ、かつ値が変わったときのみ。
hc:datagridgrouptoggle.hc-datagrid{ row, expanded } — トグルされた .hc-datagrid__grouprow
hc:datagridtreetoggle.hc-datagrid{ row, expanded } — トグルされたツリー行。
hc:datagridtreeload遅延ツリーの .hc-datagrid__row{ row } — 初回の展開のみ(htmx が data-hx-trigger でロード)。
hc:datagridexpand.hc-datagrid{ record } — 展開されたレコード <tbody>
hc:datagridcollapse.hc-datagrid{ record } — 折りたたまれたレコード <tbody>
hc:datagriddetailload[data-lazy].hc-datagrid__detail セル{ record } — 初回の展開のみ。

著者が書く data-* の一覧です — 機能ごとに、スタイルシートだけで動くか installDatagrid() が必要かを示します:

機能属性(対象)必要なもの
固定列data-frozendata-frozen-edge(ヘッダー / ボディのセル)CSS — ビヘイビアは --hc-datagrid-left / ヘッダー高さのオフセットを自動化するだけです。
末尾側の固定列data-frozen-enddata-frozen-end-edge(ヘッダー / ボディ / フッターのセル)CSS — ビヘイビアは --hc-datagrid-right のオフセットを自動化するだけです。
集計フッター<tfoot class="hc-datagrid__foot">(サーバーが描画)CSS — ビヘイビアは --hc-datagrid-foot-1-h の計測とロール付与のみ。
縦書きヘッダーdata-orientationvertical / sideways(ヘッダーセル)CSS
列 / セルのハイライトdata-highlight(セル)CSS
数値列data-numeric(ヘッダー / ボディのセル)CSS
行の序数data-row-no(行)、data-row-total(グリッド)installDatagrid()aria-rowindex / aria-rowcount を導出(ヘッダー分のオフセット込み)。
条件付き書式data-toneinfo / success / warning / error(セル / 行 / レコード)CSS
ゼブラ縞data-hc-zebra(グリッド) — またはサーバーが描画する data-alt(行)動的なケースでは installDatagrid()。サーバーが data-alt を付けるなら CSS だけ
列のリサイズdata-resizable + data-col(ヘッダー)· 同じ data-col(ボディセル)installDatagrid()
ソート可能な列data-sortable + data-col(ヘッダー)— 値 "client" は描画済みページを DOM 内でソートinstallDatagrid()
ソート → ワイヤグリッドのフォーム内の <input data-hc-datagrid-sort>hc:datagridsort の発火前に順序つき sort=name,-price を受け取るinstallDatagrid()
グループ化された行.hc-datagrid__grouprow + data-group-level(見出し行)· aria-expanded(見出しセル。"false" で折りたたみ開始)installDatagrid()
ツリー行aria-level(全行)· aria-expanded + data-hc-datagrid-tree トグル(展開可能な行)· data-lazy + htmx ワイヤリング(遅延行)installDatagrid()
展開可能な行詳細data-hc-datagrid-toggle(ボタン)· data-expanded(レコード <tbody>、開いた状態で開始)· data-lazy(詳細セル)installDatagrid()
インライン編集data-editable + data-col(+ data-value)(セル)· data-datagrid-editor + data-col(<template>)installDatagrid()
編集可否の状態導出される aria-required / aria-readonly(セルまたはグリッド。サーバー描画の値が優先)· data-hc-editable-hinteditable / readonly(グリッド)installDatagrid()
選択アクションバーdata-hc-datagrid-actions="<selector>"(バー)· data-hc-datagrid-count(件数要素)installDatagridActions()

ビヘイビアは CSS のために状態属性も書き込みます — セルに data-activedata-in-rangedata-editingdata-resized、 レコードに data-selecteddata-currentdata-expanded (状態を参照)。

  • <thead> / <tbody>、ヘッダーセルの scope、行選択チェックボックス の aria-label を備えた本物の <table> です — スクリプトが一切 なくても意味を持ちます。
  • installDatagrid()WAI-ARIA grid パターン に従う完全なキーボードセルナビゲーションを追加します(上の ビヘイビアを参照)。
  • スクロールビューポートはフォーカス可能なコントロール(チェック ボックス)を含むため、キーボードで到達可能です。

component トークン(component.tokens.json)。共有のセマンティック カラーを参照するため、グリッドはアクティブなライト / ダークとカラー テーマに自動で追随します。

トークンパス用途
datagrid.bg / fg / borderグリッドの面、テキスト、外枠。
datagrid.head-bg / head-fgヘッダー帯の色。
datagrid.frozen-bg固定(スティッキー)列の背景。
datagrid.row-hover-bgホバー行の色味。
datagrid.row-alt-bgdata-alt 行のゼブラストライプの色味。
datagrid.selected-bg / highlight-bg選択行 / ハイライト行の色味。
datagrid.current-bg / current-fgアクティブレコードの先頭セルのアクセント。
datagrid.tone-<tone>-bg / -fg条件付き書式のティント(info / success / warning / error)。セマンティックステータスカラーを参照。
datagrid.attention-error-bg / attention-warning-bgdata-attention エッジバーのレベル別の色。
datagrid.subrow-borderレコード内サブ行の間の線。
datagrid.cell-padding-x / cell-padding-yセルのパディング。
datagrid.head-1-h / head-2-h非リーフのヘッダー段の固定高さ(下の段がスティッキーの top をオフセットできるように)。
生成される CSS 変数を表示

上の datagrid.* トークンから生成されます — 好きなスコープでどれでも 上書きしてカスタムな見た目にできます(管理画面でよく見る青いヘッダーや 色つきの列は、ただの上書きです):

--hc-datagrid-bg | -fg | -border
--hc-datagrid-head-bg | -head-fg | -frozen-bg
--hc-datagrid-row-hover-bg | -row-alt-bg | -selected-bg | -highlight-bg
--hc-datagrid-current-bg | -current-fg | -subrow-border
--hc-datagrid-cell-padding-x | -cell-padding-y
--hc-datagrid-head-1-h | -head-2-h
--hc-datagrid-tone-info-bg | … (per-tone -bg / -fg: info, success, warning, error)
--hc-datagrid-attention-error-bg | -attention-warning-bg

いくつかのノブはトークン非対応です — 直接設定してください:

--hc-datagrid-max-height (default 70vh)
--hc-datagrid-truncate-max (default 16rem — width of a .hc-datagrid__truncate cell)
--hc-datagrid-freeze-shadow (frozen-column edge shadow; its direction flips per edge)
--hc-datagrid-freeze-end-shadow (the frozen-end mirror of the freeze shadow)
--hc-datagrid-foot-shadow (upward shadow cast by the sticky footer)
--hc-datagrid-attention-color (the data-attention accent — set per level from the -attention-*-bg tokens)
--hc-datagrid-attention-bar (the inset edge-bar shadow painted on data-attention rows)

レシピでの利用: データグリッド一括操作 · データグリッドの一括操作エラー · データグリッドの列選択 · データグリッドの編集エラー · データグリッドの列フィルタ · データグリッドの無限スクロール · データグリッドのページネーション · データグリッドの列プリファレンス · データグリッドの並び替え · データグリッドのツリー · 行から詳細へ · SSE ライブ更新