@ishibashi0112/spreadsheet-grid 0.41.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1349 @@
1
+ # SpreadsheetGrid 公開 API リファレンス
2
+
3
+ > SpreadsheetGrid(`@ishibashi0112/spreadsheet-grid`)の公開 API リファレンスです。npm パッケージに同梱され、
4
+ > 型定義(d.ts)の JSDoc にある各フィールドの説明は、本ファイルの表から生成しています(内容は同じです)。
5
+ >
6
+ > 保守者向け: 本ファイルの表(SpreadsheetGrid props / GridColumn props / 命令的 API / `detailRow` / `labelRow` /
7
+ > `scrollHint` / コンテキストメニュー / `classNames`)が公開型の説明の正本です。型を変えたら表を更新し、
8
+ > `pnpm run docs:jsdoc` で `packages/core/src/model/gridTypes.core.ts` の JSDoc を再生成してください
9
+ > (表と型の過不足や JSDoc のずれは `vp test` で検出します)。website の `content/docs/api/` は本ファイルの
10
+ > 複製で、手動で同期します。
11
+
12
+ 最終更新: 型定義の JSDoc 生成と npm パッケージへの同梱(api-docs)時点。
13
+
14
+ ## SpreadsheetGrid props (`SpreadsheetGridProps<T>`)
15
+
16
+ | Name | Type | Default | Description |
17
+ | --- | --- | --- | --- |
18
+ | `rows` | `readonly T[]` | — | clientSide モードの行データ。readonly 配列も受け付ける(グリッドは入力配列を破壊的に変更しない。編集結果は `onRowsChange` が新配列で返す)。`dataSource` を指定した場合は無視され serverSide モードになる(両者は排他)。 |
19
+ | `columns` | `readonly GridColumn<T>[]` | (required) | 列定義の配列。readonly 配列も受け付ける。 |
20
+ | `onRowsChange` | `(nextRows: T[]) => void` | — | 行が変化したとき呼ばれる(rows を controlled にする)。 |
21
+ | `dataSource` | `ServerSideDataSource<T>` | — | serverSide(SSRM)モードのデータ供給口。指定すると可視窓近傍のブロックだけを `getRows` で都度取得し、`rows` 系の clientSide パイプラインをバイパスする。`updateRows`(任意)を持たせるとセル編集の書き戻し(楽観更新つき)が有効になる(「セル編集の書き戻し」節)。 |
22
+ | `serverSideRefreshToken` | `number` | — | serverSide のソフトリフレッシュ用トークン。値を増やすと、クエリ(フィルター/ソート/グローバル)を変えずにキャッシュを破棄して現在の可視レンジをサーバから取り直す。スクロール位置は保持し、件数は到着ブロックの `totalRowCount` で追従する(clientSide では無視)。命令的に呼びたい場合は同挙動のハンドル `refreshServerSide()` を使う。 |
23
+ | `onServerSideLoadError` | `(error, params) => void` | — | serverSide の `getRows` が reject したときの通知(abort は正常キャンセルのため通知しない)。`params` は失敗した要求の view 空間レンジ `{ startIndex, endIndex }`。グリッド内蔵のエラーバー(再試行 UI)とは独立に呼ばれる(利用側トースト / ログ用)。インライン関数可(latest-ref 経由で読む)。 |
24
+ | `onServerSideWriteError` | `(error, params) => void` | — | serverSide の `dataSource.updateRows` が reject したときの通知。グリッド側は楽観更新をロールバック済みで、`params.updates` に失敗した行更新(`rowKey` / `changes` / `previousRow`)が入る(利用側トースト / リトライ導線用)。グリッド内蔵の保存失敗バーとは独立に呼ばれる。インライン関数可(latest-ref 経由で読む)。詳細は「セル編集の書き戻し」節。 |
25
+ | `onColumnsChange` | `(nextColumns: GridColumn<T>[]) => void` | — | 列が変化したとき呼ばれる。列メニューの固定切替はこれが指定されている場合のみ反映。 |
26
+ | `rowKeyGetter` | `(row: T, index: number) => GridRowKey` | index ベース | 安定した行キーを返す。 |
27
+ | `isRowExportable` | `(row: T, ctx: { viewRowIndex: number; rowKey: GridRowKey }) => boolean` | 全行 true | コピー(`Ctrl/Cmd+C` の TSV)/ `exportCsv` / `getExportData` の対象行フィルタ。`false` の行は出力から**行ごと**除く(行単位のみ。全体選択かの判定と貼り付けには影響しない)。`ctx.viewRowIndex` はフィルター / ソート適用後のビュー行 index(scope `'raw'` のみ rows 配列のソース index)、`ctx.rowKey` は `rowKeyGetter` の値。用途: プレースホルダ行など表示上の詰め物を出力から除く。 |
28
+ | `createRow` | `() => T` | — | 行追加時に使う新規行ファクトリ。 |
29
+ | `createOverflowColumn` | `(columnIndex: number) => GridColumn<T>` | — | 列追加時に使う列ファクトリ。 |
30
+ | `rowHeight` | `number` | density 依存(standard: `36`) | uniform 行の行高(px)。未指定時は density プリセット(compact: `28` / comfortable: `44`)から解決。明示指定が常に優先。 |
31
+ | `autoHeight` | `boolean` | `false` | auto-height 行(可変行高)を有効化する**大本のスイッチ**。これに加えて**少なくとも1列に `column.autoHeight: true`** が必要(その列が折り返して行高を駆動)。両方 true かつ**行数 ≤ 50,000**のとき有効(超過時は uniform `rowHeight` へフォールバック)。詳細は「auto-height 行」節。 |
32
+ | `estimateRowHeight` | `number` | `rowHeight` | 未測定行の推定行高(px)。 |
33
+ | `headerHeight` | `number` | density 依存(standard: `40`) | ヘッダー行の高さ(px)。未指定時は density プリセット(compact: `32` / comfortable: `48`)から解決。明示指定が常に優先。 |
34
+ | `density` | `'compact' \| 'standard' \| 'comfortable'` | `'standard'` | 密度プリセット。rowHeight / headerHeight の既定値と寸法トークン(セル横 padding / バー padding / アイコンボタン寸法 / セル文字の相対拡縮)を一括切替。`'standard'` は従来と同値。個別調整はトークン(`--ssg-cell-pad-x` 等)の上書きで可能。popover / menu 等のポータルは対象外。 |
35
+ | `theme` | `'light' \| 'dark' \| 'auto'` | `'light'` | カラーテーマ。`'dark'` でダークプリセット(`.ssg-theme-dark` のトークン一括上書き。Mantine dark 系パレット)をグリッド本体・全ポータル(popover / menu / panel)・ドラッグゴースト・ツールチップへ適用。`'auto'` は `prefers-color-scheme` へ追従(Mantine / HeroUI 等クラスベース dark 運用では、利用側カラースキームの解決値を `'light' \| 'dark'` で渡す使い方を推奨)。個別の色調整はトークン(`--ssg-*`)の上書きで可能。 |
36
+ | `rowHeaderWidth` | `number` | `56` | 行番号列の幅(px)。 |
37
+ | `height` | `number \| string` | `—` | グリッドの明示高さ。**値の種類で何の高さかが変わる**。① `%` を含む文字列(`'100%'` / `'50%'` / `'calc(100% - 40px)'`): トップバー・フィルターチップバー・ボトムバーを含む**グリッド全体**の高さ。`'100%'` でグリッドが親要素に収まり、バーを除いた残りがスクロール領域になる(ルートに `ssg-root--fill-height` が付く)。親要素が確定高さを持つ前提で、親が高さ `auto` だと全行分まで伸びて仮想化が効かない。② `number`(px)/ `%` を含まない文字列(`'400px'` / `'50vh'` / `'calc(100vh - 120px)'`): **スクロール領域だけ**の高さ(グリッド全体はバーの分だけ高くなる)。③ 未指定: スクロール領域は内容の高さで `maxHeight` によりクリップ。ルートへの `style={{ height }}` だけではスクロール領域は決まらないため、高さはこの prop で指定する。 |
38
+ | `maxHeight` | `number \| string` | `—`(既定 480px) | **スクロール領域**の高さ上限(バーは含まない。`height` の種類に関わらず同じ)。`height`・`maxHeight` が**共に未指定のときのみ**既定の 480px が効く(従来挙動)。数値の `height` と併用するとスクロール領域 = `min(height, maxHeight)`、`%` の `height` と併用すると `min(maxHeight, 親の高さ − バー)`(親が大きければグリッド全体はバー + `maxHeight` に縮む)。 |
39
+ | `readOnly` | `boolean` | `false` | グリッド全体の編集を無効化。 |
40
+ | `dimReadOnlyCells` | `boolean` | `false` | readonly セルの組み込み淡色表示(背景 + 文字色)を有効化。`false` でもセマンティッククラス `.ssg-body-cell--readonly` は常時付与され、利用側 CSS のフックに使える。 |
41
+ | `canEditCell` | `(rowIndex, colIndex, row, column) => boolean` | — | セル単位の編集可否ゲート。 |
42
+ | `enableUndoRedo` | `boolean` | `true` | グリッド編集(セル編集 / ペースト / `renderCell` の `setValue`)の取り消し/やり直し。`Ctrl/Cmd+Z` = undo、`Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` = redo(ハンドルの `undo()` / `redo()` でも可)。clientSide(`rows` + `onRowsChange`)専用で、serverSide(`dataSource`)/ `readOnly` / `onRowsChange` 未指定時は無効。履歴は「変更前 rows 配列」の参照スナップショット(未変更行は構造共有されるため低コスト)。**`onRowsChange` で受け取った配列は参照そのまま `rows` へ戻すのが前提**(map 等で作り直すと毎回「外部変更」と見なされ履歴が消える)。rows が grid 起点以外(親の直接 setState 等)で差し替わると履歴は自動破棄。エディタ内の文字入力の取り消しは input のネイティブ undo に委譲(グリッドの undo は**確定済みの編集**が対象)。 |
43
+ | `undoHistoryLimit` | `number` | `100` | 保持する undo ステップ数の上限。超過分は古い順に破棄。 |
44
+ | `enableClearOnDelete` | `boolean` | `true` | `Delete` / `Backspace` キーによる選択セル(なければアクティブセル)の値クリア。`false` でキーは何もしない(素通し)。ペースト・エディタでの上書き・undo/redo には影響しない(クリアのキーボード操作だけの opt-out)。 |
45
+ | `editorEnterMove` | `EditorEnterMove` | `'down'` | 組み込みエディタ(text / number / select / date)の `Enter` 確定後にアクティブセルをどこへ移すか(`'down'` \| `'up'` \| `'right'` \| `'left'` \| `'none'`。Excel の「Enter キーを押したら、セルを移動する(方向)」相当)。`'none'` は移動せずその場に留まる。`Tab` / `Shift+Tab`(右 / 左)と `Escape` には影響しない。custom エディタはキーバインドが consumer 責務のため対象外(`ctx.commit(value, direction)` の direction で指定)。 |
46
+ | `onUndoRedoStateChange` | `(state: { canUndo, canRedo }) => void` | — | undo / redo 可能状態が**変化したとき**に呼ばれる(ツールバーの undo/redo ボタンの disabled 表示などリアクティブな UI 用)。初回マウントでは発火せず、同値では再発火しない。毎レンダーのインライン関数でも問題ない。 |
47
+ | `enableRangeSelection` | `boolean` | `true` | 複数セル範囲選択。 |
48
+ | `enableRowSelection` | `boolean` | `false` | チェックボックス行選択の有効化(マスタースイッチ)。`true` で行ヘッダ(行NO)ガターが行選択のヒット領域になり、Excel 風のガター起点セル範囲選択は off(ボディ側セルのドラッグ範囲選択は不変)。判定は O(1)・全選択は除外集合でキーを列挙しない(1M 行でも一定コスト)。 |
49
+ | `rowSelectionMode` | `'single' \| 'multiple'` | `'multiple'` | 単一/複数の選択モード。single は常に 1 行。multiple はクリックでトグル、shift+クリック/ガタードラッグで範囲選択。 |
50
+ | `enableSelectAllRows` | `boolean` | `enableRowSelection && multiple` | ヘッダ左上コーナーの全選択チェック(tri-state: none/some/all)の有効化。 |
51
+ | `rowSelection` | `RowSelectionModel` | — | **controlled** の行選択記述子。`{ type:'include', rowKeys }`=これらを選択 / `{ type:'exclude', rowKeys }`=全選択のうち除外。全選択をキー列挙せず表現できる。指定時は controlled(内部 state を使わない)。 |
52
+ | `selectedRowKeys` | `GridRowKey[]` | — | controlled 簡易版(`{ type:'include', rowKeys }` の糖衣)。`rowSelection` と併用時は `rowSelection` を優先。全選択(exclude)は表現不可。 |
53
+ | `onRowSelectionChange` | `(model: RowSelectionModel) => void` | — | 行選択変化の通知(controlled/uncontrolled いずれでも発火)。 |
54
+ | `enableGlobalFilter` | `boolean` | `true` | グローバルフィルター**機能**の有効化。`false` で機能が無効になり、既定トップバーのフィルター入力欄も出ない(summary は `showTopBarSummary` に従う。トップバー自体を消すには `showTopBar=false`)。 |
55
+ | `enableColumnFilter` | `boolean` | `true` | 列ごとのフィルター。 |
56
+ | `renderFilterDateInput` | `(ctx: FilterDateInputContext) => ReactNode` | 内製の日付フィールド | dateSet フィルター条件の日付入力を利用側コンポーネント(Mantine `DatePickerInput` 等)へ差し替えるスロット。既定は内製フィールド(自由入力 + ドリルアップカレンダー。下記「dateSet の日付入力(既定 UI)」節)。詳細は「日付入力の差し替え(renderFilterDateInput)」節。 |
57
+ | `getFilterOptions` | `(params: GetFilterOptionsParams<T>) => Promise<{ options: GridSelectFilterOption[]; truncated?: boolean }>` | — | set / select / 複合(numberSet / textSet / dateSet)列の候補を**非同期に供給**する(DB の DISTINCT など)。popover を開くたびに `{ columnKey, column, columnFilters(自列を除く他列の有効フィルター), globalText, signal }` で呼ばれ、閉じる / 列切替で `signal` が abort される(ライブラリはキャッシュしない)。読み込み中 / 失敗(再試行)/ 打ち切り(`truncated`)の表示は popover が持つ。優先順位は `column.filterOptions`(静的)> `getFilterOptions` > rows 自動収集。非同期候補の列は反転(exclude)可。clientSide / serverSide 両対応。詳細は「ソートとフィルター」ガイド。 |
58
+ | `enableSorting` | `boolean` | `true` | ヘッダークリックでのソート。 |
59
+ | `manualFiltering` | `boolean` | `false` | 列 / グローバルフィルターの**絞り込みをグリッドで行わない**(手動フィルターモード)。フィルター UI(popover / チップバー / フィルター管理 / フィルター中の印)と状態(`GridState.filters` / `onStateChange`)は従来どおり動き、`rows` は渡した件数・順のまま表示される(絞り込みはサーバ側 WHERE 等の外部責務)。`rows` が 0 件でフィルターが載っているときは `noMatchingRowsText` を表示。serverSide(`dataSource`)では無視。詳細は「ソートとフィルター」ガイド。 |
60
+ | `manualSorting` | `boolean` | `false` | ソートの**並べ替えをグリッドで行わない**(手動ソートモード)。ソート UI と状態(`GridState.sort` / `onStateChange`)は従来どおり動き、`rows` は渡した順のまま。再マウントなしで切り替え可(`false` へ戻すと即座にクライアントソートが適用)。手動ソート中はラベル行の `sortMode` 連動 / 行ドラッグの無効化は起きない(並べ替えていない扱い)。serverSide では無視。 |
61
+ | `enableColumnResize` | `boolean` | `true` | 列幅の手動リサイズ可否のグリッド既定。各列 `resizable` 未指定時に継承(`column.resizable ?? enableColumnResize`)。 |
62
+ | `autoSizeColumns` | `'onMount' \| 'onDataChange' \| false` | `false` | データ投入時に全列幅を内容へ自動フィット。`'onMount'`=初回にデータが載った一度きり / `'onDataChange'`=`rows`(参照)が変わるたび(= データ差し替えのたび。手動リサイズは上書き) / `false`=無効。計測は列メニュー「すべての列の幅を自動調整」と同一エンジン(`suppressAutoSize` / `autoHeight` 列は除外)。フィルター / ソート / 列並べ替えでは再フィットしません。serverSide(`dataSource`)では無効。詳細は「flex と autoSize」節。 |
63
+ | `showCellOverflowTooltip` | `boolean` | `false` | セル内容が省略(…)される列で、ホバー時に全文ツールチップを表示。対象は既定テキストセルのみ(`renderCell` 列 / `autoHeight` 折り返し列は対象外)。表示はホバー時に `scrollWidth > clientWidth` を判定し、実際にクリップされているセルのみ。既存のカスタムツールチップ(`data-ssg-tooltip`)を共有。詳細は「ツールチップ」節。 |
64
+ | `showValidationMarks` | `boolean` | `true` | invalid マーク(背景 + 右上マーカー + ホバーツールチップ)の表示可否。`false` で非表示 + 可視セルの `validate` 評価スキップ。`getInvalidCells()` と `validationMode: 'reject'` の書き込み拒否には影響しない(独立経路)。「送信時にだけマークを出す」UX は利用側 state で本 prop を切り替えて実現(「バリデーション」節のレシピ参照)。 |
65
+ | `enableColumnMenu` | `boolean` | `true` | 列メニュー(⋮ + ヘッダー右クリック)。 |
66
+ | `enableRowHover` | `boolean` | `true` | 行ホバー時に行全体を薄くハイライト。 |
67
+ | `hoveredRowIndex` | `number \| null` | — | 行ホバーの controlled 値(ビュー行 index / `null` = ホバーなし)。指定時は内部 state を使わずこの値でハイライトし、pointer 由来の変化は `onHoveredRowChange` で通知のみ(optionally controlled)。`enableRowHover: false` のときは無視(ハイライトも通知もしない)。一時的な UI 状態のため `GridState` / ハンドルには載らない。 |
68
+ | `onHoveredRowChange` | `(viewRowIndex: number \| null, ctx: { source: 'pointer' }) => void` | — | 行ホバーが変わったときの通知(uncontrolled でも呼ばれる)。`viewRowIndex` はフィルター / ソート適用後のビュー行 index。同値では発火しない(pointerenter は同一行内のセル跨ぎでも来るため)。用途: 複数グリッド間のホバー同期など。 |
69
+ | `enableColumnHeaderHover` | `boolean` | `true` | 列ヘッダーのホバー時にヘッダーセルを薄くハイライト。 |
70
+ | `noMatchingRowsText` | `string` | `'一致する行がありません'` | フィルター結果 0 行時のオーバーレイ文言。 |
71
+ | `noRowsText` | `string` | `'表示する行がありません'` | rows が 0 件のときの文言。 |
72
+ | `showTopBar` | `boolean` | `true` | 上部バー(ツールバー)の表示有無。`false` で `renderTopBar` / `enableGlobalFilter` に関わらず一切描画しない(表示のマスタースイッチ。矛盾指定時は `renderTopBar` より優先)。 |
73
+ | `showTopBarSummary` | `boolean` | `true` | 既定トップバーの summary chips(件数/フィルター/ソート)の表示有無。`renderTopBar` 未指定時のみ有効。これと `showTopBarFilter` がともに非表示なら既定トップバーは描画されない(空バーを出さない)。 |
74
+ | `showTopBarCounts` | `boolean` | `true` | 既定トップバーの Rows / Columns 件数 chips の表示有無。`showTopBarSummary=true`(かつ `renderTopBar` 未指定)のときのみ有効。Filter / Sort chips は対象外。 |
75
+ | `showTopBarFilter` | `boolean` | `true` | 既定トップバーのグローバルフィルター入力欄の表示有無。`renderTopBar` 未指定時のみ有効。`enableGlobalFilter=false` のときは本値に関わらず非表示。 |
76
+ | `globalFilterPlaceholder` | `string` | `'グローバルフィルター'` | 既定トップバーのグローバルフィルター入力の placeholder。`renderTopBar` 未指定時のみ有効。 |
77
+ | `globalFilterIcon` | `ReactNode` | 組み込み検索アイコン | 既定トップバーのグローバルフィルター入力の左アイコン。`renderTopBar` 未指定時のみ有効。`undefined`=組み込みの検索(虫眼鏡)アイコン / `null`(など falsy)=アイコン無し / 任意 `ReactNode`=差し替え。クリアボタンは入力枠の内側右に `×` で表示され、入力が空のときは出ない。 |
78
+ | `showBottomBar` | `boolean` | `true` | 下部バー(ステータスバー)の表示有無。`false` で `renderBottomBar` に関わらず一切描画しない(表示のマスタースイッチ。矛盾指定時は `renderBottomBar` より優先)。 |
79
+ | `showBottomBarCounts` | `boolean` | `true` | 既定ボトムバーの Rows / Columns 件数 chips(左側)の表示有無。`renderBottomBar` 未指定時のみ有効。右側の Active / Selection / 選択統計 / Cols は対象外。 |
80
+ | `showFilterChipBar` | `boolean` | `false` | フィルターチップバー(適用中の列フィルターをトップバー直下にチップで常時表示)の表示有無(opt-in)。有効フィルター 0 件時はバーごと非表示(空バーは出さない)。`showTopBar` とは独立。チップ本体クリックで対象列へジャンプしてフィルター popover を開き、× で個別クリア、「すべてクリア」は列フィルターのみ対象(グローバルフィルターは対象外)。 |
81
+ | `renderTopBar` | `(ctx: SpreadsheetGridSlotContext<T>) => ReactNode` | 内蔵トップバー | 上部バーの差し替え。未指定時は内蔵トップバー(summary chips + フィルター入力。内訳は `showTopBarSummary` / `showTopBarFilter` で制御。フィルター入力は `enableGlobalFilter=true` が前提)。`showTopBar=false` 時は本指定に関わらず描画されない。 |
82
+ | `renderBottomBar` | `(ctx: SpreadsheetGridSlotContext<T>) => ReactNode` | 内蔵ボトムバー | 下部バーの差し替え。未指定時は内蔵ステータスバー。`showBottomBar=false` 時は本指定に関わらず描画されない。 |
83
+ | `className` | `string` | — | ルート要素の class。 |
84
+ | `style` | `CSSProperties` | — | ルート要素(`.ssg-root`)のインライン style。`classNames.root` の style とマージされ、こちらが後勝ち。 |
85
+ | `classNames` | `GridClassNames` | — | パーツ別の追加スロット。各値は `GridSlotProps`(`string \| { className?, style? }`)で、StyleX の `stylex.props(...)` の戻り値をそのまま渡せる。全 25 スロット配線済み(一覧と規則は「パーツ別スロット」節)。レンダー毎に新しいオブジェクトを渡してもよい(内容の署名で memo)。基底 class は未レイヤー・特異度 (0,1,0)のため同特異度のクラスは読み込み順で決まる(確実な上書きは連結セレクタか `style.layer.css`)。 |
86
+ | `getRowClassName` | `(row: T, rowIndex: number, ctx: RowStyleContext<T>) => GridSlotProps \| undefined` | — | 行ごとの追加 class(または `{ className, style }`)。行コンテナ + 行ヘッダー「#」セル + 各データセルに付与され、Tailwind / StyleX での行ハイライトに使える。`style` はインラインで付与され、座標 / 寸法はグリッドが後勝ち(返した style は内容比較で memo される)。第 3 引数 `ctx` は `{ row, rowIndex, sourceRowIndex, rowKey, isSelected }`(「補助型」節参照)。既存の 2 引数関数もそのまま動く(後方互換)。グループ行は対象外。 |
87
+ | `onStateChange` | `(state: GridState) => void` | — | 永続スライス(手動リサイズ幅 / フィルター / ソート)が**実際に変化したとき**に最新 `GridState` を渡して呼ばれる。保存タイミングの signal(例: localStorage 自動保存)。発火規約は「状態の保存 / 復元」節を参照。 |
88
+ | `onFiltersChange` | `(filters: GridFilterState) => void` | — | フィルター状態(`globalText` + `columnFilters`)が**実際に変化したとき**だけ、そのスライスの複製を渡して呼ばれる。`onStateChange` は列幅 / 列メタでも呼ばれるため、記述子から WHERE を組み立てるなどフィルターだけを追いたい用途向け。規約は `onStateChange` と同じ(初回非発火 / 同値非発火 / `applyState` でも発火)。 |
89
+ | `onSortChange` | `(sort: GridSortState) => void` | — | ソート状態が**実際に変化したとき**だけ、その複製を渡して呼ばれる(ORDER BY の組み立てなど)。規約は `onFiltersChange` と同じ。 |
90
+ | `onScroll` | `(params: GridScrollEventParams) => void` | — | スクロール位置の変化通知(rAF で 1 フレーム 1 回に間引き・縦横どちらの変化でも発火)。`params` は `{ top, left, source }`(px)。`source: 'api'` は `setScrollPosition` / `scrollTo*` 系由来、`'user'` はそれ以外。2 グリッドの双方向スクロール同期は `source === 'user'` のときだけ相手へ反映することでループを止められる。インライン関数可(latest-ref 経由)。 |
91
+ | `enableContextMenu` | `boolean` | `false` | コンテキストメニュー機能の有効化(マスタースイッチ)。他機能の `enable*` と同じく**既定 OFF**。`false` のあいだは `getContextMenuItems` を渡しても発火せず、右クリックはブラウザ標準メニューのまま。現状はまだ機能 / UI に改善余地があるため既定 OFF で提供する(利用側で明示 opt-in)。 |
92
+ | `getContextMenuItems` | `(params: GridContextMenuParams<T>) => GridContextMenuItem[]` | — | セル/行の**完全カスタム**コンテキストメニュー。右クリック時のみ呼ばれ、返した項目でメニューを描画する(ライブラリは固定の既定項目を持たない)。opt-in は `enableContextMenu={true}` かつ本コールバックの指定の両方。**未指定、または `[]` を返したときはブラウザ標準の右クリックメニューへフォールスルー**(空パネルは出さない)。SSRM 未ロード行では開かない。ヘッダー右クリックは列メニュー(`enableColumnMenu`)が担当し、本メニューはボディ(セル / 行NO ガター)専用。詳細は「コンテキストメニュー」節を参照。 |
93
+ | `onContextMenuOpen` | `(params: GridContextMenuParams<T>) => void` | — | コンテキストメニューが実際に開いた直後の通知(項目が 1 件以上あり表示された場合のみ)。
94
+ | `scrollHint` | `boolean \| ScrollHintOptions<T>` | —(無効) | **スクロール位置インジケーター**。スクロール中にスクロールバー脇へ行番号バブル(「行 N / 総行数」+ 任意の列値)と行目盛りルーラーを表示し、スクロールバー帯のホバーで「行 N へ」のジャンプ先プレビューを出す。`true` は全既定(`{ bubble: true, ruler: true, scrollbar: true, trigger: 'scroll', minRows: 0 }`)と同義。`minRows` で「表示行数がしきい値以上のときだけ有効」のデータ量ゲートも掛けられる。表示は総行数とスクロール位置のみで駆動されるため **clientSide / SSRM の全構成で動作**。オーバーレイは `pointer-events: none` で既存操作へ一切干渉しない。詳細は「スクロール位置インジケーター」節を参照。 |
95
+ | `detailRow` | `DetailRowOptions<T>` | —(無効) | **展開行(Master/Detail)**。マスター行の直下に、行順(view index)を変えずに全幅の帯を差し込み、その中(カード)へ `render` の返す任意の React 要素(自前のサブグリッド / フォーム / 集計パネル等)を描画する。指定時のみ有効で、未指定なら既存の描画・状態・イベント経路は一切変わらない。`{ render, height?, isExpandable?, showToggleColumn?, className? }`。clientSide / serverSide の両方で使える(serverSide の制約は節内)。詳細は「展開行(Master/Detail)」節を参照。 |
96
+ | `onExpandedDetailRowKeysChange` | `(keys: GridRowKey[]) => void` | — | 展開中の展開行のマスター行キー集合が**変化したとき**に呼ばれる(開閉の永続化・外部同期用)。初回マウントでは発火しない。インライン関数可(latest-ref 経由)。 |
97
+ | `labelRow` | `LabelRowOptions<T>` | —(無効) | **ラベル行(見出し / 区切り行)**。`rows` の中で `isLabelRow(row)` が true の行を「行数に数えない見出し」として、3 ペインを跨ぐ全幅の帯で描画する(中身は `render` で任意の React 要素に差し替え可。横スクロールしても左端に留まる)。編集 / 選択 / コピー / エクスポートの既定対象外で、行番号も消費しない。ソート / フィルターは既定でラベル行から次のラベル行までの区間(セクション)に閉じる(`sortMode`)。`sticky: true` で現在セクションのラベルを列ヘッダー直下に固定。`{ isLabelRow, getLabel, render?, height?, className?, sticky?, sortMode?, keepEmptySections?, exportText? }`。行グルーピング(`rowGroup`)とは別機能で併用不可。詳細は「ラベル行(見出し / 区切り行)」節。 |
98
+ | `enableRowDrag` | `boolean` | `false` | **行ドラッグ並び替え**。先頭にドラッグハンドル列(幅 28px・タイトル無しの合成列。左固定列があれば左固定側)を挿入し、ハンドル(⋮⋮)を掴んで行を上下へ動かせる。確定時は `onRowsChange` へ移動後の**新配列**を渡し(履歴ラッパ経由 = undo/redo 対象)、続けて `onRowMove` を呼ぶ。clientSide(`rows` + `onRowsChange`)専用で、`dataSource`(serverSide)/ 行グルーピング中 / `onRowsChange` 未指定ではハンドル列を出さない。ソート / フィルター適用中はハンドルを淡色 + 理由ツールチップにして操作を無効化する(列は残る)。詳細は「行ドラッグ並び替え」節。 |
99
+ | `isRowDraggable` | `(row: T, ctx: RowDragContext) => boolean` | 全行可 | 行ごとのドラッグ可否。`false` の行にはハンドルを描画しない。`ctx = { rowKey, sourceRowIndex }`。 |
100
+ | `onRowMove` | `(params: RowMoveParams<T>) => void` | — | 行移動の確定後(`onRowsChange` の直後)に呼ばれる。`params = { rowKey, fromIndex, toIndex, rows }`(index は元 `rows` 配列基準、`rows` は `onRowsChange` と同じ新配列参照)。ハンドルの `moveRow()` による移動でも呼ばれる。 |
101
+
102
+ ### バーの表示制御(top / bottom)
103
+
104
+ トップ / ボトムバーは次の優先順で解決される。
105
+
106
+ - **`showTopBar` / `showBottomBar`(マスタースイッチ)**: `false` ならそのバーは一切描画されない(`render*` / `enable*` より優先)。
107
+ - **`renderTopBar` / `renderBottomBar`(カスタム)**: 指定時はそのまま描画。トップバーの内訳 props(`showTopBarSummary` / `showTopBarFilter`)はカスタム側が中身を決めるため関与しない。
108
+ - **既定バー**: `render*` 未指定時のフォールバック。
109
+
110
+ 既定トップバーは **summary chips(左)** と **グローバルフィルター入力(右)** の 2 パートからなり、独立に出し分けできる。
111
+
112
+ | やりたいこと | 設定 |
113
+ | --- | --- |
114
+ | バーごと消す | `showTopBar={false}` |
115
+ | summary だけ(フィルター入力なし) | `showTopBarFilter={false}` |
116
+ | フィルター入力だけ(summary なし) | `showTopBarSummary={false}` |
117
+ | トップの Rows/Columns 件数だけ消す | `showTopBarCounts={false}` |
118
+ | ボトムの Rows/Columns 件数だけ消す | `showBottomBarCounts={false}` |
119
+ | 適用中の列フィルターをチップで常時表示 | `showFilterChipBar`(既定 OFF) |
120
+ | フィルター機能ごと無効 + summary は残す | `enableGlobalFilter={false}` |
121
+ | 完全に自前のバー | `renderTopBar={(ctx) => …}` |
122
+
123
+ `showTopBarSummary` と `showTopBarFilter`(実効は `showTopBarFilter && enableGlobalFilter`)がともに `false` の場合、既定トップバーは描画されない(空バーを出さない)。
124
+
125
+ ボトムバーは Rows / Columns 件数のみ `showBottomBarCounts` で出し分けできる(右側の Active / Selection / 選択統計 / Cols は対象外)。それ以外の内訳を変えたい場合は `renderBottomBar` を使う。
126
+
127
+ ### コンテキストメニュー(`enableContextMenu` / `getContextMenuItems`)
128
+
129
+ セル / 行の右クリックで開く**完全カスタム**メニュー。**既定は OFF**(`enableContextMenu={false}`)で、他機能の `enable*` と同じくマスタースイッチで有効化する(現状はまだ機能 / UI に改善余地があるため既定 OFF)。ライブラリは固定項目を一切持たず、`getContextMenuItems` が返した項目配列だけを描画する。用意されるのは「窓」(パネル外装 + 右クリック座標配置 + 開閉 / Escape / 外側クリック / スクロール close)だけで、中身(ラベル / アイコン / `onSelect`)はすべて利用側が渡す。列メニューと同じ `.ssg-menu-panel` / `.ssg-menu-item` 外装を再利用する。
130
+
131
+ **opt-in と標準メニューへのフォールスルー**
132
+
133
+ - `enableContextMenu` 未設定 / `false`(既定)→ ブラウザ標準の右クリックメニュー(`getContextMenuItems` を渡していても発火しない)。
134
+ - `enableContextMenu={true}` かつ `getContextMenuItems` が項目を返した → その項目が並んだメニューを右クリック座標に表示。
135
+ - `enableContextMenu={true}` でも `getContextMenuItems` 未指定 / `[]` を返した(対象で項目なし)→ ブラウザ標準メニュー(**空パネルは浮かせない**)。
136
+
137
+ **対象と挙動**
138
+
139
+ - **ヘッダー右クリックは対象外**(列メニュー `enableColumnMenu` が担当)。本メニューはボディの **データセル**と **行NO ガター**専用。
140
+ - **SSRM 未ロード行**(まだ取得できていない行)の上では開かない(標準メニューになる)。
141
+ - uncontrolled のみ。右クリックしても**セル選択は変化しない**(対象セル/行の情報は `params` で受け取る)。
142
+ - close 契機: 項目選択 / 外側クリック / Escape / スクロール。項目間のキーボード移動(矢印キー)は持たない。
143
+
144
+ **`GridContextMenuParams<T>`**(コールバック引数)
145
+
146
+ | フィールド | 型 | 説明 |
147
+ | --- | --- | --- |
148
+ | `target` | `GridContextMenuTarget<T>` | 右クリック対象。`{ type:'cell', rowIndex, colIndex, rowKey, row, column, value }` か `{ type:'rowHeader', rowIndex, rowKey, row }`(行NO ガター)。`rowIndex` はビュー行 index、`colIndex` は論理列 index(視覚順 左→中央→右 = `handle.selectCell` と同一空間)。 |
149
+ | `clientX` / `clientY` | `number` | 右クリックのビューポート座標(メニュー配置に使用済み。分岐の判断材料にも)。 |
150
+ | `selection` | `GridSelection` | 現在のセル範囲選択。チェックボックス行選択は `handle.getRowSelection()` で別途取得。 |
151
+ | `activeCell` | `CellCoord \| null` | 現在のアクティブセル。 |
152
+ | `isTargetSelected` | `boolean` | 対象(cell はそのセル / rowHeader はその行)が `selection` に含まれるか。「選択範囲への操作」か「単一対象への操作」かを分岐する簡便値。 |
153
+
154
+ **`GridContextMenuItem`**(判別共用体)
155
+
156
+ - **action(既定)**: `{ kind?: 'action'; id?; label: ReactNode; icon?: ReactNode; disabled?; danger?; onSelect: () => void }` — クリックで `onSelect` 実行後に自動で閉じる。`icon` 省略時も左 14px 枠が空スペーサになりラベル左端が揃う。`danger` は削除など危険操作の赤系強調(Mantine の `color="red"` 相当)。
157
+ - **label(見出し)**: `{ kind: 'label'; id?; label: ReactNode }` — 非インタラクティブなセクション見出し(Mantine の `Menu.Label` 相当)。項目群のグルーピング表示に使う。
158
+ - **separator**: `{ kind: 'separator'; id? }` — 区切り線(Mantine の `Menu.Divider` 相当)。
159
+ - **custom(レンダラ / エスケープハッチ)**: `{ kind: 'custom'; id?; render: (ctx: { close: () => void }) => ReactNode }` — パネル内に任意 JSX を差し込む。`close()` で任意タイミングに閉じられる。
160
+
161
+ `id` は React key 用(省略時は配列 index)。項目は非ジェネリック: 行データは `getContextMenuItems` 内で `params` を通じてクロージャに閉じ込める。
162
+
163
+ **例**
164
+
165
+ ```tsx
166
+ <SpreadsheetGrid
167
+ // …
168
+ enableContextMenu // 既定 false。機能を使うにはこのマスタースイッチが必要
169
+ getContextMenuItems={(params) => {
170
+ const items: GridContextMenuItem[] = [];
171
+ items.push({ kind: 'label', label: '操作' }); // セクション見出し
172
+ if (params.target.type === 'cell') {
173
+ const value = params.target.value; // narrowing はローカルへ退避してから onSelect で使う
174
+ items.push({
175
+ label: '値をコピー',
176
+ icon: '📋',
177
+ onSelect: () => navigator.clipboard?.writeText(String(value ?? '')),
178
+ });
179
+ }
180
+ items.push({ kind: 'separator' });
181
+ items.push({
182
+ label: 'この行を削除',
183
+ danger: true, // 赤系強調
184
+ onSelect: () => deleteRow(params.target.rowKey),
185
+ });
186
+ items.push({ kind: 'separator' });
187
+ items.push({
188
+ label: '選択範囲を CSV 出力',
189
+ disabled: !params.isTargetSelected,
190
+ onSelect: () => gridRef.current?.downloadCsv('selection.csv', { scope: 'selection' }),
191
+ });
192
+ items.push({
193
+ kind: 'custom',
194
+ render: ({ close }) => (
195
+ <div style={{ padding: '6px 8px' }}>
196
+ 行 {params.target.rowIndex}
197
+ <button type="button" onClick={close}>閉じる</button>
198
+ </div>
199
+ ),
200
+ });
201
+ return items; // [] を返すと標準メニューへフォールスルー
202
+ }}
203
+ onContextMenuOpen={(params) => console.log('opened at', params.target)}
204
+ />
205
+ ```
206
+
207
+ **レシピ: 右クリックからフィルター管理パネルを開く**
208
+
209
+ 適用中フィルターの一覧 / 編集 / クリアを行うフィルター管理パネル(列メニュー「フィルターを管理…」と同じもの)は、ハンドルの `openFilterManager()` で任意の場所から開ける。コンテキストメニューに載せる場合:
210
+
211
+ ```tsx
212
+ const gridRef = useRef<SpreadsheetGridHandle<Row>>(null);
213
+
214
+ <SpreadsheetGrid
215
+ ref={gridRef}
216
+ enableContextMenu
217
+ getContextMenuItems={() => [
218
+ {
219
+ label: 'フィルターを管理…',
220
+ onSelect: () => gridRef.current?.openFilterManager(),
221
+ },
222
+ ]}
223
+ />;
224
+ ```
225
+
226
+ ### スクロール位置インジケーター(`scrollHint`)
227
+
228
+ 大量行(特に 100 万行級)ではスクロールバー 1px の移動が数百〜数千行に相当し、移動中に「今どの行にいるか」を見失う。`scrollHint` は次の 3 つのオーバーレイでこれに答える(**既定は完全無効**。opt-in)。
229
+
230
+ - **行番号バブル(`bubble`)**: スクロール中、スクロールバー脇(サム位置)に「行 N / 総行数」を表示。`hintColumn` / `renderHint` で任意の列値を添えられる。
231
+ - **行目盛りルーラー(`ruler`)**: ボディ右端に行数スケール(切りのよい刻み。刻みが 1 万の倍数のときは「10万」等の圧縮表記へ全目盛り統一)。スクロール中とスクロールバー帯ホバー中のみフェード表示。
232
+ - **ジャンプ先プレビュー(`ruler` に含む)**: スクロールバー帯(右端)にポインタを置くと、スクロールバーのクリック / ドラッグと同じサム中心写像で「行 N へ」の点線 + ラベルを表示。
233
+ - **カスタムスクロールバー(`scrollbar`)**: ヘッダー下から始まる専用ガターに**常時表示**のトラック + **最小 30px サム**を自前描画(ドラッグ / クリックジャンプ / ホイール対応)。macOS のオーバーレイスクロールバーは自動で消え、大量行ではサムが極小になり「掴む場所」を見失うための置き換え。有効時はネイティブ縦スクロールバーを非表示化する(Chromium / WebKit。Firefox はネイティブ縦バーが残るがガター操作は有効)。
234
+
235
+ `ScrollHintOptions<T>`:
236
+
237
+ | 項目 | 型 | 既定 | 説明 |
238
+ | --- | --- | --- | --- |
239
+ | `bubble` | `boolean` | `true` | 行番号バブルの表示。 |
240
+ | `ruler` | `boolean` | `true` | ルーラー + ジャンプ先プレビューの表示。 |
241
+ | `scrollbar` | `boolean` | `true` | カスタム縦スクロールバー(専用ガター・常時表示)。`false` でネイティブバーのまま(バブル等は疑似サム位置に表示)。 |
242
+ | `trigger` | `'scroll' \| 'hover' \| 'always'` | `'scroll'` | バブル / ルーラーの表示トリガー(スクロールバー自体は常時表示)。`'scroll'` = スクロール中のみ(停止約 1 秒でフェードアウト)/ `'hover'` = グリッドホバー中 + スクロール中 / `'always'` = 常時。 |
243
+ | `minRows` | `number` | `0` | **データ量ゲート**。表示行数(フィルター / グルーピング適用後のビュー行数。SSRM はサーバー総行数)がこの値未満のあいだ、scrollHint 全体(カスタムスクロールバー含む)を自動 OFF にしてネイティブスクロールバー表示のままにする。`0` = 常時有効(従来挙動)。 |
244
+ | `hintColumn` | `string` | — | 行番号に添えて表示する列 key(= 行オブジェクトのフィールド名)。 |
245
+ | `renderHint` | `({ rowIndex, rowData }) => ReactNode` | — | 表示内容の完全カスタム(`hintColumn` より優先)。`null` / `undefined` を返すと行番号のみの既定表示。 |
246
+
247
+ ```tsx
248
+ // 簡易: 行番号 + 品番
249
+ <SpreadsheetGrid scrollHint={{ hintColumn: 'partNo' }} />
250
+
251
+ // 完全カスタム: 品番 — 品名
252
+ <SpreadsheetGrid
253
+ scrollHint={{
254
+ trigger: 'scroll',
255
+ renderHint: ({ rowData }) =>
256
+ rowData ? `${rowData.partNo} — ${rowData.partName}` : null,
257
+ }}
258
+ />
259
+ ```
260
+
261
+ **データ量ゲート(`minRows`)**: 小規模データではバブル / ルーラー / カスタムスクロールバーがノイズになるため、`minRows` を指定すると「表示行数がしきい値以上のときだけ出る」挙動にできる(例: `scrollHint={{ minRows: 100 }}`)。判定はバブルの「/ 総行数」と同じ行数(フィルター / グルーピング適用後のビュー行数。SSRM はサーバー総行数)で、フィルターで絞り込んでしきい値を割ればその間は自動 OFF になる。既定は `0`(常時有効)。注意: `scrollbar` 有効時はしきい値またぎでガター余白が付け外しされるため、行数が変動する画面では僅かなレイアウトシフトが起きる(気になる場合は `scrollbar: false` と併用する)。
262
+
263
+ **SSRM / グルーピングでの挙動**: バブル / プレビューの対象行が SSRM の未ロード行(またはグルーピングのグループ行)の場合、`rowData` は `undefined` になり、`hintColumn` 指定時は自動的に**行番号のみ**へフォールバックする(`renderHint` には `undefined` がそのまま渡るので利用側で分岐する)。総行数とスクロール位置だけで駆動されるため、インジケーター自体は SSRM を含む全構成で動作する。
264
+
265
+ **実装ノート**: 行番号は仮想化の縦ジオメトリ(1M 行の pixel scaling / auto-height の prefix-sum)と同一の写像で解決するため常に正確。バブル / ルーラー / プレビューのオーバーレイは `pointer-events: none` で、スクロール・セル操作へ一切干渉しない。カスタムスクロールバーのガターのみポインタ操作を受けるが、コンテンツのスクロール自体はネイティブのまま(ガターは `scrollTop` を書くだけの鏡映し)。全行が viewport に収まりスクロール不能のときは何も表示しない。配色はテーマトークン(`--ssg-pill-*` / `--ssg-scrollhint-*`)で light / dark 両対応。
266
+
267
+ ### キーボード操作
268
+
269
+ グリッド本体フォーカス中(編集中でない)の操作一覧。フィルター入力等のフォーム要素にフォーカス中は無効。
270
+
271
+ | キー | 操作 |
272
+ | --- | --- |
273
+ | 矢印(+ `Shift` で範囲拡張)/ `Tab` / `Shift+Tab` | アクティブセル移動(`labelRow` 有効時、↑ / ↓ はラベル行に止まらず読み飛ばす)。 |
274
+ | `Enter` / `F2` / 印字キー直打ち | 編集開始(印字キーはその 1 文字を初期値に)。編集可否は `readOnly` / 列 / `canEditCell` に従う。 |
275
+ | `Escape` | 選択解除。 |
276
+ | `Ctrl/Cmd+C` / ペースト(`Ctrl/Cmd+V`) | 選択範囲の TSV コピー(`isRowExportable` 指定時は `false` の行を除く)/ アクティブセル起点の貼り付け(readOnly では no-op)。 |
277
+ | `Ctrl/Cmd+A` | 全体選択(2 回目で解除)。 |
278
+ | `Delete` / `Backspace` | 選択セル(なければアクティブセル)の値クリア。編集不可セルは対象外。クリア値は「空文字のペースト」と同じ規則(`parseClipboardValue('')` 経由、未定義なら `''`)。変更が無ければ no-op(undo 履歴にも積まれない)。 |
279
+ | `Ctrl/Cmd+Z` / `Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` | undo / redo(詳細は命令的 API の「undo / redo」節)。 |
280
+
281
+ 編集エディタ内: `Enter` = 確定して下へ(移動先は `editorEnterMove` で変更可。既定 `'down'`)、`Tab` / `Shift+Tab` = 確定して右 / 左へ、`Escape` = キャンセル、フォーカスアウト = 確定。IME 変換中(`isComposing`)の `Enter` / `Escape` / `Tab` は IME の操作としてのみ扱われ、セル編集の確定 / キャンセルには使われない。
282
+
283
+ 確定後の移動先は「確定を反映した再レンダー後」の行数・列数でクランプされる。このため、`onRowsChange` で末尾に空行を追加する消費側(Excel 的な入力グリッドの定石パターン)では、最終行の `Enter` 確定で「増えた行」へそのまま移動できる。行が増えない場合は従来どおり最終行に留まる。
284
+
285
+ ## GridColumn props (`GridColumn<T>`)
286
+
287
+ | Name | Type | Default | Description |
288
+ | --- | --- | --- | --- |
289
+ | `key` | `string` | (required) | 列の一意キー。 |
290
+ | `title` | `string` | — | ヘッダーの表示ラベル。**未指定 / 空文字(`''`)のときは `key` を表示**(列メニュー / フィルターパネル / CSV ヘッダー等の列名表示も同じフォールバック)。ボタン専用列などで見出しを空にしたい場合は空白 1 文字(`' '`)等を指定する。 |
291
+ | `width` | `number` | (required) | 列幅(px)。 |
292
+ | `minWidth` | `number` | — | リサイズ時の下限幅。flex 配分時の下限クランプにも使用(flex 列で未指定なら内部既定 50px)。 |
293
+ | `maxWidth` | `number` | — | 上限幅。**未指定なら上限なし**(autoSize は内容にぴったり合わせ、手動リサイズも自由に広げられます。既定の上限は設けません)。指定すると autoSize / 手動リサイズ / flex 配分の上限クランプに使われます。 |
294
+ | `flex` | `number` | — | center 列(非 pinned)の伸縮比。余り幅(コンテナ幅 − 行ヘッダー − pinned 合計 − `width` 固定列の合計)を flex 比で配分し `minWidth`/`maxWidth` でクランプ。コンテナ追従でリアクティブに伸縮。手動リサイズで固定 px へ変化(`columns` 変化まで固定 → 以後 flex 復帰)。pinned 列では無視。詳細は下記「flex と autoSize」節。 |
295
+ | `resizable` | `boolean` | グリッドの `enableColumnResize` を継承 | この列の手動リサイズ可否。`false` でヘッダーのリサイズハンドルを非表示。リサイズハンドルの**ダブルクリック**でその列を内容幅へ autoSize(`false` 時はハンドルが無いため不可。列メニューからの autoSize は引き続き可能)。 |
296
+ | `suppressAutoSize` | `boolean` | `false` | `true` で autoSize の対象外(列メニュー / 境界ダブルクリック / すべての列の自動調整すべてでスキップ)。consumer 指定の `width` を維持(固定幅優先)。テキストで測れないカスタムUI列や固定で見せたい列向けの per-column opt-in。 |
297
+ | `estimateCellWidth` | `(row, column) => number` | — | autoSize の幅見積もり。指定列は「セル内容の content 幅(px・セルの padding/border を除く)」をこの関数から得て、**全行の最大 + セル枠**で確定します(テキスト/候補/実 DOM 計測を使わず、React mount もしません)。テキスト長が実描画幅と相関しない renderCell カスタムUI列(横並びバッジ等)向けの opt-in。返す値は `renderCell` の実描画幅と一致させること。 |
298
+ | `autoHeight` | `boolean` | — | この列が auto-height 行の高さを駆動(グリッドの `autoHeight` 有効時のみ)。**autoSize の対象外**(折り返し前提のため。下記「flex と autoSize」の制約を参照)。 |
299
+ | `wordBreak` | `'normal' \| 'break-all' \| 'keep-all' \| 'break-word' \| 'auto-phrase'` | — | 折り返し時(= `autoHeight` 列)の CSS `word-break`。`'auto-phrase'` は Chromium(Chrome / Edge)で BudouX による文節折り返し(Firefox / 一部 Safari 未対応)。**nowrap(非 `autoHeight`)列では折り返し自体が起きないため効果なし**。既定(未指定)はブラウザ標準=禁則つき文字折り返し。詳細は「日本語テキストの折り返し」節。 |
300
+ | `lineBreak` | `'auto' \| 'loose' \| 'normal' \| 'strict' \| 'anywhere'` | — | 折り返し時の CSS `line-break`(禁則処理の強さ)。`'strict'` で禁則を厳格化。`wordBreak` 同様、折り返す列でのみ効果あり。 |
301
+ | `visible` | `boolean` | — | 列の表示/非表示。 |
302
+ | `editable` | `boolean` | — | この列の編集を許可。 |
303
+ | `readOnly` | `boolean` | — | この列を読み取り専用にする。 |
304
+ | `pinned` | `'left' \| 'right'` | undefined = 中央スクロール | 列固定の方向。 |
305
+ | `rowGroup` | `boolean` | — | `true` でこの列を行グルーピングの対象にする(複数指定時は `columns` 配列の出現順が階層順)。有効時はグループ元列が表示から外れ、先頭に自動グループ列(ツリー表示)が注入される。**clientSide 限定**(serverSide では無視 + 開発時警告)。詳細は「行グルーピング + 集計」節。 |
306
+ | `aggFunc` | `'sum' \| 'min' \| 'max' \| 'avg' \| 'count' \| GridAggFunc<T>` | — | グルーピング時のこの列の集計。組み込みは値駆動の数値集計(`Number()` で有限になる値のみ対象・空値除外、`count` は配下 leaf 行数)。関数でカスタム集計可(返り値がグループ行に表示)。`rowGroup` 列がないときは無視。 |
307
+ | `getValue` | `(row: T) => unknown` | `row[key]` | 値アクセサ。 |
308
+ | `setValue` | `(row: T, value: unknown) => T` | — | 値ライター(新しい行を返す)。 |
309
+ | `renderCell` | `(ctx: CellRenderContext<T>) => ReactNode` | プレーン `<span>` | カスタムセル描画。 |
310
+ | `align` | `'left' \| 'center' \| 'right'` | `'left'` | セル内容の水平寄せ(UI 表示のみ・元の値は不変)。セル表示と編集 input に反映。 |
311
+ | `valueFormatter` | `(params: CellValueFormatterParams<T>) => string` | — | セル表示値の整形(UI 表示のみ)。`renderCell` 未指定の既定セルが返り値を表示。組み込み `numberFormatter` 等を渡せる。元の値/編集/コピー/ソート/フィルターには影響しない。 |
312
+ | `cellClassName` | `GridSlotProps \| ((ctx: CellStyleContext<T>) => GridSlotProps \| undefined)` | — | セルへ付与する追加 class(条件付きスタイル)。`GridSlotProps` = `string \| { className?, style? }` で、StyleX の `stylex.props(...)` をそのまま返せる(`style` はセルへインライン付与。座標 / 寸法はグリッドが後勝ち)。関数版は値 / 状態に応じて返せる。`ctx` には view の `rowIndex` に加え source 基準の `sourceRowIndex` / `rowKey` が入る(ソート / フィルター ON でも source 行基準のデータと突き合わせ可能。「補助型」節参照)。基底 `.ssg-body-cell` は未レイヤー・特異度 (0,1,0)。確実な上書きは `.ssg-body-cell.my-class` の連結を推奨。 |
313
+ | `renderHeader` | `(ctx: HeaderRenderContext<T>) => ReactNode` | — | カスタムヘッダー描画。 |
314
+ | `filterType` | `'text' \| 'textSet' \| 'number' \| 'numberSet' \| 'date' \| 'dateSet' \| 'select' \| 'set' \| 'custom' \| 'auto'` | — | フィルター UI の種別。`'auto'` は列の値から `numberSet` / `textSet` / `dateSet` を自動判定する opt-in(下記「filterType: 'auto'(自動判定)」節)。`'numberSet'` / `'textSet'` / `'dateSet'` は条件(演算子 + 値)と Set 一覧を 1 つの popover に縦に並べて **AND 結合**する複合フィルター(条件を適用すると Set 候補が連動して絞られる。候補外になった値の選択は破棄せず保持)。numberSet の演算子は 以上 / より大きい / 以下 / 未満 / に等しい / に等しくない / 範囲 / 空白 / 空白でない、textSet は を含む / に等しい / で始まる / で終わる / 空白 / 空白でない(判定は大文字小文字無視)。dateSet は 範囲 / 以降 / 以前 / に等しい / に等しくない / 空白 / 空白でない + 相対プリセット(今日 / 今月 / 過去 30 日。**相対のまま保存され評価のたびに解決**)で、Set 部分は年 / 月 / 日の 3 階層ツリー(親は 3 状態チェック)になる。 |
315
+ | `filterOptions` | `readonly GridSelectFilterOption[]` | rows から自動収集 | select / set / numberSet / textSet / dateSet の候補(readonly / `as const` 配列も可)。 |
316
+ | `dateFilterPresets` | `false \| DateFilterPresetOption[]` | ビルトイン 3 種 | dateSet の相対プリセットチップの構成。`false` / `[]` でチップ行を非表示(オプトアウト)。配列はビルトイン ID(`'today'` / `'thisMonth'` / `'last30days'`)の再利用とカスタム定義 `{ id, label, resolve }` を表示順のまま混在可。詳細は下記「dateSet の相対プリセット(dateFilterPresets)」節。 |
317
+ | `filterFn` | `(row: T, filterValue: unknown) => boolean` | — | カスタムフィルター述語。 |
318
+ | `editor` | `GridColumnEditor<T>` | text 相当 | セルエディタ種別(判別共用体)。`{ type: 'text' \| 'number' \| 'select' \| 'date' \| 'checkbox' \| 'custom', ... }`。詳細は「セルエディタ」節。 |
319
+ | `validate` | `(ctx: CellValidationContext<T>) => CellValidationResult` | — | セル値の検証。`true`=有効 / `false`=無効(既定メッセージ)/ `string`・`{ message }`=無効+メッセージ。**純粋・軽量であること**(描画中の可視セルごとに毎レンダー評価。`cellClassName` 関数と同コスト階級)。詳細は「バリデーション」節。 |
320
+ | `validationMode` | `'mark' \| 'reject'` | `'mark'` | 検証 NG 時の動作。`'mark'`=値は入るがセルに invalid 表示 / `'reject'`=書き込み自体を拒否。 |
321
+ | `parseClipboardValue` | `(raw: string, row: T) => unknown` | editor 既定パーサ | 「文字列 → セル値」のパーサ(貼り付け / クリア / エディタ commit で共通)。**明示指定が常に優先**。未指定で `editor` が number / date / checkbox のときは種別の既定パーサが自動供給されます(「セルエディタ」節の表参照)。 |
322
+ | `formatClipboardValue` | `(value: unknown, row: T) => string` | — | コピー時のフォーマッタ。 |
323
+
324
+ ### filterType: 'auto'(自動判定)
325
+
326
+ `filterType: 'auto'` を指定すると、列の値から実効種別(`numberSet` / `textSet` / `dateSet`)を自動判定します。**明示的な opt-in のみ**で、`filterType` 未指定の列は従来どおり「フィルターなし」です(既定は変わりません)。
327
+
328
+ - **判定タイミング**: その列の popover を**初回に開いた時点で 1 回だけ**判定し、以後その列では固定します(行の追加 / 編集で種別が揺れると、適用済みフィルター記述子と UI が食い違うため)。判定材料が無かった場合(まだ行が空 等)は確定させず、次回オープン時に再判定します。
329
+ - **優先順位**: ①適用済みフィルター記述子の `kind`(`applyState` 復元で先に値が載っているケース)→ ②前回の判定結果 → ③`editor` 種別のヒント(`{ type: 'number' }` → numberSet / `{ type: 'date' }` → dateSet。値を見ないため確実・高速)→ ④値のサンプリング。
330
+ - **サンプリング**: 先頭から**空白セルを除いた最大 1,000 件**(走査は最大 20,000 行で打ち切り)。**厳格判定**で、全サンプルが日付として解釈できれば `dateSet`、全て数値として解釈できれば `numberSet`、1 件でも外れれば `textSet`(安全側)。
331
+ - **判定基準は値の型ではなく「解釈できるか」**: DB 型が文字列でも `'1234'` は数値とみなし `numberSet` になります(フィルター判定側の数値化規則と一貫)。ただし**先頭ゼロの値(`'0001'` のような品番コード・郵便番号)は数値とみなしません**(Excel と同じ扱い)。`boolean` / オブジェクトも数値扱いしません。
332
+ - **serverSide**: クライアントが全行を持たないため値からの推定はしません。`editor` ヒントがあればそれで確定し、無ければ `'text'`(条件のみの部分一致フィルター)へフォールバックします。auto を使う場合は `editor` 種別の指定を推奨します。
333
+
334
+ ### dateSet の相対プリセット(dateFilterPresets)
335
+
336
+ `filterType: 'dateSet'` の popover に出る相対プリセットチップ(既定: 今日 / 今月 / 過去 30 日)は、列オプション `dateFilterPresets` で列ごとに構成できます。
337
+
338
+ ```tsx
339
+ // ① 非表示(オプトアウト): チップ行そのものを出さない
340
+ { key: 'updatedAt', filterType: 'dateSet', dateFilterPresets: false }
341
+
342
+ // ② カスタム構成: ビルトイン ID の再利用とカスタム定義を表示順のまま混在できる
343
+ {
344
+ key: 'updatedAt',
345
+ filterType: 'dateSet',
346
+ dateFilterPresets: [
347
+ 'today', // ビルトイン ID(ラベルは既定の「今日」)
348
+ {
349
+ id: 'thisWeek', // 保存されるのはこの id(相対のまま)
350
+ label: '今週',
351
+ resolve: (now) => { // 評価のたびに呼ばれ、絶対範囲へ解決される
352
+ const day = now.getDay();
353
+ const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - ((day + 6) % 7));
354
+ return { from: monday, to: now }; // 'YYYY-MM-DD' 文字列 or Date(両端含む)
355
+ },
356
+ },
357
+ ],
358
+ }
359
+ ```
360
+
361
+ - **意味論はビルトインと同じ「相対保存」**: フィルター値には `{ mode: 'preset', preset: id }` だけが保存され、評価(フィルター再計算)のたびに `resolve(now)` で絶対範囲へ解決されます。`getState()` の状態を翌日復元すると範囲が追従します。
362
+ - `resolve` の返り値は `{ from?, to? }`(両端含む)。**片側のみなら 以降 / 以前**として、`from > to` は自動で入れ替えて評価されます。両方省略は「条件なし」です。
363
+ - **列定義から消えた ID の保存値は「条件なし」として評価**されます(全行非表示になる事故を避ける安全側)。要約表示は 構成ラベル → ビルトイン既定ラベル → 生 ID の順でフォールバックします。
364
+ - **serverSide**: カスタム ID も `{ mode: 'preset', preset: id }` のまま dataSource へ渡ります。解釈(id → WHERE 句)はサーバ側の責務です(「サーバーサイド行モデル」節参照)。
365
+
366
+ ### dateSet の日付入力(既定 UI)
367
+
368
+ dateSet フィルター条件の日付入力は、v0.28 からネイティブ `<input type="date">` に代わり**内製の日付フィールド**になりました(ブラウザ依存の見た目・操作性の問題の解消。プレビュー合意のドリルアップ案)。
369
+
370
+ - **自由入力**: `2026/7/1` / `2026-07-01` など表記ゆれを受け付け、Enter / blur で `'YYYY-MM-DD'` へ正規化して確定します。解釈できない入力は赤枠(`aria-invalid`)のまま確定しません(適用済みの条件は保持)。
371
+ - **カレンダー**: フィールド右のボタンで開閉。タイトルクリックで 日 → 月一覧 → 年一覧 とドリルアップし、年 / 月を選ぶと下の段へ戻ります(12 年 / ページ)。月・年ビューのフッターには選択せずに 1 段戻れる「← 戻る」も出ます。今日は枠線・選択日は塗り表示。フッターに「今日」「クリア」。日を選ぶと即確定して閉じます。
372
+ - **Escape**: カレンダー表示中はカレンダーだけを閉じ、非表示なら popover を閉じます(従来挙動の踏襲)。
373
+ - ダークテーマ / compact 密度のトークンへ追従します。カレンダーは popover 内に描画されるため、外側クリック判定やはみ出しクランプ(FIT-1)に影響しません。
374
+ - セルエディタ `editor: { type: 'date' }` は従来どおりネイティブ input です(スコープ外)。
375
+
376
+ ### 日付入力の差し替え(renderFilterDateInput)
377
+
378
+ dateSet フィルター条件の日付入力(既定は上記の内製フィールド)を、利用側の UI ライブラリのピッカーへ差し替えるグリッドレベルのスロットです。対象は**フィルター popover の条件欄のみ**(セルエディタ `editor: { type: 'date' }` は対象外)。
379
+
380
+ ```ts
381
+ type FilterDateInputContext = {
382
+ value: string; // 'YYYY-MM-DD' か ''(未入力)
383
+ onChange: (value: string | Date | null) => void; // Date / null(クリア)/ 表記ゆれ文字列も可(内部で正規化)
384
+ ariaLabel: string; // '開始日' / '終了日' / '条件の日付'
385
+ slot: 'single' | 'from' | 'to'; // 範囲の from/to か単一値か
386
+ columnKey: string; // 列ごとの出し分けに
387
+ };
388
+ ```
389
+
390
+ **Mantine(@mantine/dates)の例**:
391
+
392
+ ```tsx
393
+ import { DatePickerInput } from '@mantine/dates';
394
+
395
+ <SpreadsheetGrid
396
+ renderFilterDateInput={({ value, onChange, ariaLabel }) => (
397
+ <DatePickerInput
398
+ aria-label={ariaLabel}
399
+ value={value || null} // v7 は Date 型のため new Date(`${value}T00:00:00`)
400
+ onChange={onChange} // string | Date | null をそのまま渡せる
401
+ valueFormat="YYYY/MM/DD"
402
+ size="xs"
403
+ clearable
404
+ popoverProps={{ withinPortal: false }} // ★ popover 内に描画(下記の外側クリック対策)
405
+ style={{ flex: 1, minWidth: 0 }}
406
+ />
407
+ )}
408
+ />
409
+ ```
410
+
411
+ **HeroUI の例**(バージョンにより prop 名は調整):
412
+
413
+ ```tsx
414
+ import { DatePicker } from '@heroui/react';
415
+ import { parseDate } from '@internationalized/date';
416
+
417
+ renderFilterDateInput={({ value, onChange, ariaLabel }) => (
418
+ <DatePicker
419
+ aria-label={ariaLabel}
420
+ value={value ? parseDate(value) : null}
421
+ onChange={(date) => onChange(date ? date.toString() : null)} // CalendarDate → 'YYYY-MM-DD'
422
+ size="sm"
423
+ />
424
+ )}
425
+ ```
426
+
427
+ **汎用(Tailwind 等で自作)の例**:
428
+
429
+ ```tsx
430
+ renderFilterDateInput={({ value, onChange, ariaLabel }) => (
431
+ <input
432
+ type="date"
433
+ aria-label={ariaLabel}
434
+ value={value}
435
+ onChange={(event) => onChange(event.target.value)}
436
+ className="my-date-input"
437
+ />
438
+ )}
439
+ ```
440
+
441
+ **外側クリック対策(重要)**: フィルター popover は「外側 pointerdown で閉じる」ため、ピッカーのカレンダーが body 直下ポータルに出ると、カレンダー操作で popover が閉じてしまいます。対策はどちらか:
442
+
443
+ 1. **ポータルを無効化して popover 内に描画**(推奨): Mantine は `popoverProps={{ withinPortal: false }}` 等。高さの変化は popover の実測リサイズ対応が自動処理します。
444
+ 2. **keep-open 属性でオプトアウト**: ポップアップ要素(またはその祖先)に `data-ssg-filter-keep-open` 属性を付与すると、その内側の pointerdown では popover を閉じません(多くのライブラリはポップアップへ `data-*` を渡せます)。
445
+
446
+ その他の注意:
447
+
448
+ - 値は常に `'YYYY-MM-DD'` / `''` に正規化されて draft に入ります(`Date` / `'2026/7/1'` 等も可。解釈できない値はクリア扱い)。
449
+ - 値を変更するとプリセットチップの選択は解除されます(ネイティブ UI と同じ規則)。
450
+ - Enter 適用 / Escape クローズのキーボード配線はネイティブ input 専用です。スロット側のキー操作はコンポーネントの責務になります(dateSet は即時適用のため、通常は配線不要)。
451
+
452
+ ### 値フォーマッタ(UI 表示)
453
+
454
+ `valueFormatter` はセルの**表示文字列だけ**を変えます(元の値・編集・コピー・ソート・フィルターは生値のまま)。組み込みファクタは `logic/valueFormatters.ts` に集約し、バレルから公開します。利用側も同じ契約(`CellValueFormatter<T>`)で自作でき、将来パターン(日付/%/通貨等)はファクタ追加 + バレル公開で拡張できます。
455
+
456
+ - `numberFormatter(options?)` — 数値を 3 桁区切りで整形。既定は**元の精度を保持**(小数桁を勝手に丸めない)。`minimumFractionDigits` / `maximumFractionDigits` で固定桁、`useGrouping: false` で区切り無効、`locale` 指定可。`null` / `undefined` / `''` は `emptyText`(既定 `''`)、数値化できない値は原値の文字列をそのまま表示。
457
+
458
+ 使用例:
459
+
460
+ ```ts
461
+ import { numberFormatter } from '@ishibashi0112/spreadsheet-grid';
462
+
463
+ const columns = [
464
+ { key: 'amount', title: '金額', width: 140, align: 'right', valueFormatter: numberFormatter() },
465
+ ];
466
+ ```
467
+
468
+ ### セルエディタ(editor)
469
+
470
+ `column.editor` で列のエディタ種別を指定します(未指定 = `text`)。判別共用体 `GridColumnEditor<T>` で、種別ごとの付随オプションは型で強制されます。編集可否は従来どおり `editable` / `readOnly` / `canEditCell` で判定されます(`editor` は種別のみ)。
471
+
472
+ | type | UI / 操作 | オプション | 既定パーサ(`parseClipboardValue` 未指定時) |
473
+ | --- | --- | --- | --- |
474
+ | `'text'` | 従来のテキスト input(既定) | — | パススルー(生文字列のまま) |
475
+ | `'number'` | `<input type="number">`(スピナー / ↑↓ステップ / 不正文字抑止) | `min` / `max` / `step` | `''`→`null` / 有限数値文字列→`number` / 非数値→生文字列のまま(mark が拾う) |
476
+ | `'select'` | 候補ドロップダウン(body 直下ポータル)。↑↓=ハイライト移動 / Enter=確定(下へ)/ Tab=確定(左右へ)/ クリック=確定 / 印字キー=label 前方一致のタイプアヘッド(約 700ms でリセット) | `options: GridSelectEditorOption[] \| (row) => GridSelectEditorOption[]`(静的 or 行依存。関数はレンダー中に呼ばれるため純粋であること) | パススルー(`option.value` は string。型変換したい場合は `parseClipboardValue` を併用) |
477
+ | `'date'` | ネイティブ `<input type="date">`。ドラフトは `'YYYY-MM-DD' \| ''` | — | `''`→`null` / 解釈可能→`'YYYY-MM-DD'` へ正規化 / 解釈不可→生文字列のまま |
478
+ | `'checkbox'` | **直接トグル**(編集セッションなし)。クリック / Space で即トグル。Enter / F2 / ダブルクリックではエディタが開かない | `checkedValue`(既定 `true`)/ `uncheckedValue`(既定 `false`)。checked 判定は `Object.is(value, checkedValue)` のみ | `''`→unchecked / checked・unchecked の文字列表現→対応値 / `'true'`・`'1'`→checked / その他→unchecked |
479
+ | `'custom'` | `render(ctx)` の返り値を編集オーバーレイ内に描画。**フォーカス管理・キーバインドは利用側の責務** | `render: (ctx: CellEditorContext<T>) => ReactNode` | パススルー |
480
+
481
+ 注意点:
482
+
483
+ - **既定パーサはエディタ commit / 貼り付け / Delete クリアで共通**に効きます(例: number 列は Delete クリアで `null` になる)。明示の `parseClipboardValue` が常に優先です。
484
+ - **select の blur は cancel**(値不変)です。text / number / date の blur = 確定と非対称ですが、select は「選択 = 即確定」でドラフト概念がないためです。
485
+ - **date の Tab はグリッド流(確定 + 移動)**です。ピッカー内のセグメント移動は ← → 矢印で行えます。
486
+ - **checkbox のダブルクリックは click 2 回**(トグル往復)として扱われます(Excel / AG Grid と同様)。`renderCell` 指定時はそちらが優先され、組み込みチェックボックスセルは描画されません。
487
+ - **custom の `ctx.commit(value)`** は、`value` が string なら列パーサ(`parseClipboardValue` ?? editor 既定)を通し、**非 string ならパースをバイパスしてドメイン値をそのまま書き込みます**。`ctx` には `row` / `rowIndex`(ビュー行)/ `sourceRowIndex`・`rowKey`(source 基準)/ `colIndex` / `column` / `value`(編集開始時の生値)/ `initialText`(印字キー開始時はそのキー)/ `align` / `commit` / `cancel` が入ります。`commit` の返り値(`EditorCommitResult`)で reject 列の検証結果を受け取れます(無視しても安全)。`commit(value, direction)` の第 2 引数(`'down' | 'up' | 'right' | 'left'`)で確定後のアクティブセル移動を指定できます(省略時は移動しない。`editorEnterMove` prop は custom エディタには効かないため、Enter で下へ進めたい場合は `'down'` を明示)。
488
+
489
+ ### バリデーション(validate / validationMode)
490
+
491
+ `column.validate` でセル値を検証します。返り値は `true`(有効)/ `false`(無効・既定メッセージ)/ `string` または `{ message }`(無効 + メッセージ)。
492
+
493
+ 動作モード(`validationMode`、既定 `'mark'`):
494
+
495
+ - **`'mark'`(既定)** — 値は書き込み、セルに invalid 表示(背景 + 右上マーカー)+ ホバーでメッセージのツールチップを出します。invalid 判定は**表示時導出**(state 非保持)のため、貼り付け・クリア・初期データ・undo/redo・外部からの `rows` 差し替え後も常に `rows` と整合します。
496
+ - **`'reject'`** — 検証 NG の書き込み自体を拒否します。経路ごとの挙動:
497
+ - エディタ確定(Enter / Tab): **確定拒否・編集継続**。エディタ枠が赤くなり、即時のエラーバブルでメッセージを表示します。blur(フォーカスが外れた)の場合は cancel(値不変でエディタを閉じる)へフォールバックします。
498
+ - 貼り付け: 検証 NG の**セルだけスキップ**します(他のセルは書き込まれる。readonly セルのスキップと同じ意味論)。
499
+ - Delete / Backspace クリア: クリア値が検証 NG ならスキップします(「必須列は Delete で空にできない」を表現できます)。
500
+ - `renderCell` の `setValue` / checkbox トグル: no-op になります。
501
+
502
+ 契約と注意:
503
+
504
+ - `validate` は**純粋・軽量**であること。mark 表示のため、描画中の可視セル(validate 指定列のみ)ごとに毎レンダー評価されます(`cellClassName` 関数と同じコスト階級)。
505
+ - 検証コンテキストは `{ value, row, column }` です(`row` は書き込み前の行。ビュー index はソート / フィルターで不安定なため渡しません)。
506
+ - 保存前の一括チェックはハンドルの `getInvalidCells()` を使います(「命令的 API」節参照)。
507
+ - invalid 表示の配色はトークン `--ssg-invalid` / `--ssg-invalid-bg` で調整できます(light / dark 両対応)。
508
+
509
+ #### 表示タイミングの制御(showValidationMarks)
510
+
511
+ `column.validate` は「ルール定義」、グリッド prop `showValidationMarks`(既定 `true`)は「マークをいつ見せるか」で、両者は独立です。既定はこれまでどおり常時リアルタイム表示。`false` にするとマークを出さず、可視セルごとの `validate` 評価もスキップします(評価結果はマーク表示にしか使わないため)。
512
+
513
+ - **宣言的・stateless**: 表示のオンオフは React state で prop を切り替えます(`showMarks()` のような命令的 API は持ちません)。invalid 判定自体が表示時導出(state 非保持)なので、マークを再表示した瞬間も常に現在の `rows` と整合します(undo / 外部差し替え後も同様)。
514
+ - **`getInvalidCells()` は表示状態と無関係**に常に全走査で動作します(マーク非表示中の送信前チェックに使えます)。
515
+ - **`validationMode: 'reject'` は影響を受けません**。reject は write 時のゲート(表示機能ではない)で、エディタ確定拒否時のエラーバブルも**マーク非表示中でも出します**。設計判断: エディタのエラーバブルは「いま行った操作への即時フィードバック」であり、「データ全体の不正状態の可視化」であるマークとは役割が違うため、表示制御の対象にしていません。
516
+
517
+ #### レシピ: 送信時にまとめて検証(マークは送信時だけ表示)
518
+
519
+ 入力中は何も出さず、送信時に一括検証して NG ならマーク + 通知、修正後の再送信成功でマークを消す——業務フォームの定番 UX です。利用側が持つ state は boolean 1 つだけです。
520
+
521
+ ```tsx
522
+ function OrderForm() {
523
+ const gridRef = useRef<SpreadsheetGridHandle<Row>>(null);
524
+ const [rows, setRows] = useState<Row[]>(initialRows);
525
+ const [showErrors, setShowErrors] = useState(false);
526
+
527
+ // ルールは列に定義したまま(完全な空行は検証対象外、の行相互参照ルールも ctx.row で書ける)
528
+ const columns: GridColumn<Row>[] = [
529
+ { key: 'itemCode', title: '品目コード', width: 200, editable: true,
530
+ validate: ({ value, row }) =>
531
+ isEmptyRow(row) || String(value ?? '').trim() !== '' || '品目コードが未入力の行があります' },
532
+ { key: 'qty', title: '数量', width: 120, editable: true,
533
+ validate: ({ value, row }) =>
534
+ isEmptyRow(row) || /^[1-9]\d*$/.test(String(value ?? '').trim()) || '数量は1以上の整数で入力してください' },
535
+ ];
536
+
537
+ const handleSubmit = () => {
538
+ const invalid = gridRef.current?.getInvalidCells() ?? [];
539
+ if (invalid.length > 0) {
540
+ setShowErrors(true); // ここで初めてマークを出す
541
+ // メッセージはセル粒度で返るため、重複排除して 1 通知にまとめる
542
+ notifyError([...new Set(invalid.map((c) => c.message))].join(' / '));
543
+ // 先頭のエラーセルへジャンプ。scrollToCell はビュー座標のため、手入力フォームの
544
+ // ようにソート / フィルター未適用なら sourceRowIndex をそのまま使える(ビュー順 =
545
+ // source 順)。ソートあり得る画面では rowKey からビュー index を自前解決すること。
546
+ const first = invalid[0];
547
+ gridRef.current?.scrollToCell(
548
+ first.sourceRowIndex,
549
+ columns.findIndex((c) => c.key === first.columnKey),
550
+ );
551
+ return;
552
+ }
553
+ setShowErrors(false); // 成功したらマークを消す
554
+ submit(rows);
555
+ };
556
+
557
+ return (
558
+ <>
559
+ <SpreadsheetGrid
560
+ ref={gridRef}
561
+ columns={columns}
562
+ rows={rows}
563
+ onRowsChange={setRows}
564
+ showValidationMarks={showErrors}
565
+ />
566
+ <button onClick={handleSubmit}>送信</button>
567
+ </>
568
+ );
569
+ }
570
+ ```
571
+
572
+ 注意: `getInvalidCells()` は clientSide 専用のため、本レシピも clientSide(rows 供給)前提です(serverSide は全行を保持しないため空配列 + `console.warn`)。
573
+
574
+ ### flex と autoSize(列幅の決め方)
575
+
576
+ 列幅を「グリッドに決めさせる」方法は 2 つあり、**決め方が異なる別概念**です。列ごとに使い分けでき、混在も可能です。どちらを使うべきか迷ったら下表で選びます。
577
+
578
+ | | flex(`column.flex`) | autoSize(列メニュー / 境界ダブルクリック) |
579
+ | --- | --- | --- |
580
+ | 何に合わせる | **コンテナの余り幅**(中身は見ない) | **セルの中身の長さ**(コンテナは見ない) |
581
+ | 反応性 | コンテナのリサイズに**追従してリアクティブに伸縮** | 実行時の内容で**固定 px を一度だけ算出**(以後自動追従しない) |
582
+ | 起動 | 列定義の `flex` を指定(= 宣言的) | 列メニュー「この列の幅を自動調整 / すべての列の幅を自動調整」、またはヘッダー境界(リサイズハンドル)の**ダブルクリック**でその列だけ(= 操作) |
583
+ | 適用範囲 | center 列(非 pinned)のみ | 任意の列 |
584
+ | 典型用途 | テーブルを横いっぱいに使う / 余白を特定列に吸わせる | 中身が切れないようにする |
585
+
586
+ - **flex** — `flex` を持つ center 列が「利用可能幅(コンテナ幅 − 行ヘッダー − pinned 合計 − `width` 固定列の合計)」を `flex` 比で分け合い、`minWidth`/`maxWidth` でクランプされます。固定列合計が利用可能幅を超えると flex 列は最小幅(`minWidth`、未指定時は内部既定 50px)まで潰れ、超過分は横スクロールになります。pinned 列では無視されます。
587
+ - **autoSize** — セルの中身に合わせて固定 px を一度だけ算出します(コンテナ幅は見ません)。算出時点の内容で幅が確定し、その後コンテナや内容が変わっても自動では追従しません。起動は列メニューのほか、ヘッダー境界(リサイズハンドル)の**ダブルクリック**でもその列を内容幅へ合わせられます(リサイズ可能な列のみ。AG Grid の境界ダブルクリック相当)。\
588
+ 計測は 2 段方式です。**Phase 1** で全表示行を canvas で概算して列ごとの最長候補を絞り、**Phase 2** で候補だけを grid root 配下の隠しセルで**実 DOM 実測**します。これにより**全行を見つつ**(画面外の最長値も反映)、**`valueFormatter` の整形結果・letter-spacing・padding まで実描画どおりに反映**され、はみ出しません。`suppressAutoSize: true` の列は計測対象から外れ `width` を維持します。\
589
+ renderCell で独自 DOM(バッジ等)を描く列は、テキストでは幅が出ないため `estimateCellWidth` を指定します。指定列は Phase 1/2 のテキスト計測を使わず、**`estimateCellWidth(row)` が返す content 幅の全行 running-max + セル枠**で確定します(consumer 申告を信頼。mount なし)。\
590
+ **制約 — `autoHeight: true` の列は autoSize の対象外です**(列メニュー / 境界ダブルクリック / すべての列の自動調整すべてでスキップし、`width` を維持)。autoHeight 列は「幅を固定して長文を**折り返す**」のが本来の挙動ですが、autoSize の計測は**単一行**で行うため、autoHeight 列を測ると折り返したい長文を1行幅にし、**極端に横長になる**ためです(列幅に既定の上限は無いため、長文ぶんだけ際限なく広がります)。長文列は autoHeight(折り返し)か、`maxWidth` 付きの固定幅(切り詰め)で運用してください。
591
+
592
+ flex 列を**手動リサイズ**すると、その列はドラッグした幅で**固定 px**に変わります(以後その列は flex 対象外)。固定は `columns` prop が変化する(pin 切替 / 表示切替 / 並べ替え / 親による差し替え)まで維持され、変化後は再び flex に復帰します(手動幅を恒久固定する仕様ではありません)。
593
+
594
+ ### autoSizeColumns(データ投入時の自動フィット)
595
+
596
+ グリッド prop `autoSizeColumns?: 'onMount' | 'onDataChange' | false`(既定 `false`)で、**データ投入時に全列幅を内容へ自動フィット**できます。フィットの計測は上記 autoSize(列メニュー「すべての列の幅を自動調整」)と**同一エンジン**で、`'onMount'` は初回にデータが載った一度きり、`'onDataChange'` は `rows`(参照)が変わるたび(= データ差し替えのたび)に走ります。フォーム送信結果などを丸ごと差し替えて毎回合わせ直す用途は `'onDataChange'` が該当します。
597
+
598
+ - **`GridColumn.suppressAutoSize` との関係**: 同一エンジンのため、`suppressAutoSize: true` の列(および `autoHeight: true` の列)は**この自動フィットの対象からも外れ**、`width` を維持します。「大半の列は内容へ合わせつつ、特定の列だけ固定幅で見せたい」場合は、その列に `suppressAutoSize: true` + `width` を付けてください(per-column の opt-out)。`estimateCellWidth` を指定した列も、通常の autoSize と同じ規則(申告 content 幅の全行 running-max)で見積もられます。
599
+ - **発火 signal は `rows`(データ)の変化のみ**です。フィルター / ソート、列の並べ替え / 表示切替 / 固定(= `columns` 変化)では再フィットしません(手動操作の直後に幅が飛ぶのを避けるため)。したがって手動リサイズした幅は、次のデータ投入(`'onDataChange'`)で上書きされます(合わせ直したくない列は上記 `suppressAutoSize` で外します)。
600
+ - フィット幅は**内部の列幅 state に反映され、`onColumnsChange` は呼びません**。`columns` を controlled で保持していても競合しません。
601
+ - **serverSide(`dataSource`)では無効**です(未ロード行を測れないため。clientSide 限定)。
602
+ - 計測が重い場合は本番でも既存の計測オーバーレイが出ます(小規模データでは体感差はありません)。
603
+
604
+ ```tsx
605
+ // 例: フォーム送信結果を丸ごと差し替え、そのたびに列幅を内容へ合わせ直す。
606
+ // consumer 側は autoSizeColumns を渡すだけ(トークンや effect は不要)。
607
+ <SpreadsheetGrid
608
+ rows={rows} // 送信のたびに新しい配列参照へ差し替える
609
+ columns={columns}
610
+ autoSizeColumns="onDataChange"
611
+ />
612
+ ```
613
+
614
+ ### auto-height 行(可変行高)
615
+
616
+ 行高を内容量に合わせて可変にする(長文を折り返して行を縦に伸ばす)機能です。**有効化には2つのスイッチが両方必要**です:
617
+
618
+ 1. **グリッド props `autoHeight={true}`**(大本のスイッチ・既定 `false`)。
619
+ 2. **少なくとも1列に `column.autoHeight: true`**。その列が折り返し(`white-space: normal`)、行高を駆動します。
620
+
621
+ 内部判定は「**グリッド `autoHeight` && `column.autoHeight === true`**」で、**両方 true** のセルだけが可変になります(片方だけでは効きません)。複数列に `autoHeight: true` を付けた行では、**最も背の高いセル**が行高になります。
622
+
623
+ - **行数 gate**: auto-height はビュー行数が **50,000 行以内**のときだけ有効です。超えると uniform 行高(`rowHeight`)へ自動フォールバックします(prefix-sum のコストとブラウザの要素高さ上限のため。開発時は console 警告あり)。
624
+ - **`estimateRowHeight`**: 仮想化で画面外の未測定行に使う推定行高です(既定 `rowHeight`)。行が画面に入ると実測値へ置き換わります(**上限ではありません**)。
625
+ - 行高は**実測**(描画セルの実際の高さ)で、上限/下限のクランプはありません。列幅を広げる等で内容が減れば行も縮みます。
626
+ - **`autoHeight` 列は autoSize の対象外**です(折り返し前提のため。上記「flex と autoSize」の制約を参照)。
627
+ - 折り返し位置の品質(日本語の文節折り返し等)は次節「日本語テキストの折り返し」を参照。
628
+
629
+ ```tsx
630
+ <SpreadsheetGrid
631
+ rows={rows}
632
+ columns={[
633
+ { key: 'id', title: 'ID', width: 80 },
634
+ // ↓ この列が折り返して行高を駆動する
635
+ { key: 'note', title: '備考', width: 320, autoHeight: true },
636
+ ]}
637
+ autoHeight // ← 大本のスイッチ(これが無いと列側 autoHeight は無視される)
638
+ />
639
+ ```
640
+
641
+ ### 日本語テキストの折り返し(word-break / BudouX)
642
+
643
+ `autoHeight: true` の列はセルが折り返され(行高可変)、この時点で**ブラウザ標準の禁則処理つき文字折り返し**が効きます(句読点・閉じ括弧を行頭に置かない等)。ここから先の「品質」は 2 段階です。
644
+
645
+ **① CSS のみ(ライブラリ改修ゼロ)** — 列の `wordBreak` / `lineBreak` で調整できます。特に **`wordBreak: 'auto-phrase'`** は Chromium(Chrome / Edge)で **BudouX による文節折り返し**(語の途中で割らない)を行います。社内向けなど Chromium 前提なら、これだけで文節折り返しが得られます(依存追加なし)。
646
+
647
+ ```tsx
648
+ // Chromium(Chrome / Edge): CSS だけで文節折り返し。
649
+ { key: 'desc', title: '説明', autoHeight: true, wordBreak: 'auto-phrase' }
650
+ ```
651
+
652
+ > `auto-phrase` は Firefox 未対応、Safari は一部フラグ付き(BudouX ではなく独自エンジン)です。禁則の厳格化は `lineBreak: 'strict'` を併用します。
653
+
654
+ **② クロスブラウザ(BudouX を利用側で使用)** — 全ブラウザで文節折り返ししたい場合は、[BudouX](https://github.com/google/budoux)(Google 製・辞書レス・約 15KB・クライアント完結。Chromium の `auto-phrase` の裏側エンジンでもあります)を**利用側の `renderCell`** で使います。**本ライブラリは BudouX を同梱しません**(seam 方針。`getExportData()` と同じく依存は利用側が持ちます)。BudouX が挿入した改行機会(ゼロ幅スペース)でのみ折り返すよう、`wordBreak: 'keep-all'` を併用するのがポイントです。
655
+
656
+ ```tsx
657
+ import { loadDefaultJapaneseParser } from 'budoux';
658
+ const parser = loadDefaultJapaneseParser();
659
+
660
+ // 列定義(利用側)
661
+ {
662
+ key: 'desc',
663
+ title: '説明',
664
+ autoHeight: true, // 折り返し = 行高可変
665
+ wordBreak: 'keep-all', // 文字間では切らない(ゼロ幅スペースでのみ折り返す)
666
+ renderCell: ({ value }) =>
667
+ parser.parse(String(value ?? '')).join('\u200b'), // 文節境界に ZWSP(値ごとに memo 推奨)
668
+ }
669
+ ```
670
+
671
+ 仮想化により描画されるのは可視セルのみ(数十件)なので、値ごとに memo すれば実コストは軽微です。
672
+
673
+ ### 行グルーピング + 集計(rowGroup / aggFunc)
674
+
675
+ `column.rowGroup: true` の列でビュー行をグルーピングします(複数列指定時は `columns` 配列の出現順が階層順)。
676
+
677
+ ```ts
678
+ const columns: GridColumn<Order>[] = [
679
+ { key: 'region', title: '地域', width: 100, rowGroup: true },
680
+ { key: 'rep', title: '担当', width: 100, rowGroup: true },
681
+ { key: 'product', title: '商品', width: 160 },
682
+ { key: 'qty', title: '数量', width: 90, align: 'right', aggFunc: 'sum' },
683
+ { key: 'amount', title: '金額', width: 120, align: 'right', aggFunc: 'sum' },
684
+ ];
685
+ ```
686
+
687
+ - **自動グループ列**: グルーピング有効時、先頭にツリー表示列(インデント + 開閉シェブロン + ラベル + 件数)が注入され、グループ元列は表示から自動的に外れます。自動グループ列は合成列で、列メニュー / ソート / 並べ替え DnD / autoSize / エクスポートの対象外です(手動リサイズ・列範囲選択は可能)。
688
+ - **集計**: `aggFunc` 指定列は、グループ行の同じ列位置に集計値を表示します。組み込み(`'sum' | 'min' | 'max' | 'avg' | 'count'`)は値駆動の数値集計で、`Number()` 変換で有限にならない値と空値(`null` / `undefined` / `''`)は対象外(`count` のみ配下 leaf 行数)。数値対象 0 件の sum / avg / min / max は空セルになります。カスタム関数(`GridAggFunc<T>` = `({ values, rows, column }) => unknown`)は返り値がそのまま表示されるため、整形済み文字列を返すこともできます。
689
+ - **集計値の整形**: 列に `valueFormatter` があれば集計値にも適用され、leaf セルと表示が揃います(`numberFormatter()` の 3 桁区切り等)。ただしグループ行に leaf 行は無いため **formatter の `row` は `undefined`** です。`row` を読む formatter を使う列では、`aggFunc` をカスタム関数にして整形済み文字列を返してください。
690
+ - **開閉**: シェブロン click / グループ行 double-click / グループ行上の `Enter`・`Space`。命令的 API(下記)からも操作できます。開閉状態は UI 状態で、undo/redo・`getState()` の対象外です。
691
+ - **並び / フィルター**: グループの並びは「ソート適用後の初出順」です(グループ元列をソートすればグループごと並び替わる)。フィルターは leaf 行に適用され、0 件になったグループは表示から消えます。空値は 1 つの「(空白)」グループへ集約されます。
692
+ - **leaf 限定の各機能**: グループ行は編集 / ペースト / クリア / コピー / 行選択 / エクスポートの対象外です(すべて leaf 行のみが対象)。件数表示(bar の Rows / 行選択件数)も leaf 基準です。
693
+ - **clientSide 限定**: serverSide(`dataSource`)では `rowGroup` は無視されます(開発時警告)。
694
+
695
+ グループ行の記述子は `GridGroupRow`(`groupKey` / `columnKey` / `value` / `label` / `level` / `leafCount` / `aggregates`)としてバレルから公開されます(`getGroupRows()` の返り値)。
696
+
697
+ ### 展開行(Master/Detail)(`detailRow`)
698
+
699
+ `detailRow` prop を渡すと、各行を「展開」してマスター行の直下に消費側 UI(カード)を差し込めます(AG Grid の Master/Detail 相当)。行グルーピングが「ライブラリがグループ行を作る」機能なのに対し、展開行は「行の下に何を出すかを利用側が全面的に決める」動線です。両者は独立で併用もできます。
700
+
701
+ ```tsx
702
+ <SpreadsheetGrid
703
+ rows={rows}
704
+ columns={columns}
705
+ rowKeyGetter={(row) => row.id}
706
+ detailRow={{
707
+ height: 220,
708
+ isExpandable: (row) => row.lines.length > 0,
709
+ render: ({ row, rowKey, collapse }) => (
710
+ <OrderLinesPanel order={row} onClose={collapse} />
711
+ ),
712
+ }}
713
+ onExpandedDetailRowKeysChange={(keys) => save(keys)}
714
+ />
715
+ ```
716
+
717
+ `DetailRowOptions<T>`:
718
+
719
+ | Name | Type | Default | Description |
720
+ | --- | --- | --- | --- |
721
+ | `render` | `(ctx: DetailRowRenderContext<T>) => ReactNode` | (required) | 展開行の中身。`ctx = { row, rowKey, rowIndex, sourceRowIndex, collapse }`(`rowIndex` はビュー行 index、`collapse()` はその展開行を閉じる)。帯の内側のカード要素(`.ssg-detail-card`、`data-ssg-detail` 属性つき)に描画される。 |
722
+ | `height` | `number` | `200` | 帯の高さ(px)。**固定高**で、中身が超えるとカード内でスクロールする(auto 高は非対応)。 |
723
+ | `isExpandable` | `(row: T, ctx: { rowKey; sourceRowIndex }) => boolean` | 全行展開可 | 行ごとの展開可否。`false` の行はトグルが描画されず、命令的 API / `ctx.detail.toggle()` からの展開も no-op。 |
724
+ | `showToggleColumn` | `boolean` | `true` | 専用トグル列(幅 28px・タイトル無し、行ヘッダーの右隣 = 先頭列。左固定列があるときは左固定側)を自動挿入する。`false` にすると列は挿入されず、任意の列の `renderCell` から `ctx.detail.toggle()` でトグルを自前配置する(下記)。 |
725
+ | `className` | `GridSlotProps` | — | カード要素へ追加する class(または `{ className, style }`。`classNames.detailCard` に加えて付与)。 |
726
+
727
+ - **表示**: 帯はマスター行の直下・グリッド全幅(3 ペインとも背景を描画)で、カードは中央ペインに `position: sticky` で置かれ、横スクロールしてもビューポート左端(左固定ペインの右隣)に留まります。幅は中央ペインの可視幅です。展開しても**行の順序・view index は変わらず**(第 3 の行種は作らない)、後続行が帯の高さぶん下がります。展開時にスクロール位置は動かしません。
728
+ - **状態**: 展開状態は `rowKeyGetter` の行キーで保持されるため、ソート / フィルター / 行の追加削除を跨いで同じ行に追従します(フィルターで除外中の行は帯が出ず、復帰すると再表示)。UI 状態で undo/redo・`getState()` の対象外。永続化は `onExpandedDetailRowKeysChange` + `setDetailRowExpanded()` で行います。
729
+ - **マウント**: カードは仮想化の描画窓に載っている間だけマウントされます(スクロールアウトでアンマウント、戻ると再マウント)。カード内で保持したい状態は消費側で `rowKey` をキーに持ってください。
730
+ - **イベント境界**: カード内のキーボード / クリップボード / 右クリック / ダブルクリック / ドラッグ開始はグリッド本体へ伝播しません(カード内の input で矢印キーを押してもアクティブセルは動かない)。カード内にフォーカスがある間は、グリッド側のフォーカス復帰(編集確定・popover close 後)がフォーカスを奪いません。**カード内に別の `SpreadsheetGrid` をネスト**しても、外側の自動高さ実測 / 列ヘッダー検索は内側のセルを対象にしません。
731
+ - **`renderCell` からの操作(`ctx.detail`)**: `detailRow` 有効時、`CellRenderContext` に `detail: { expanded, expandable, toggle(), setExpanded(bool) }` が入ります(無効時は `undefined`)。`showToggleColumn: false` と組み合わせて、商品名セルの横などにトグルを自前配置できます。
732
+ - **選択 / アクティブセル**: 帯はセルではないため選択・アクティブセルの対象外です。帯を跨ぐ範囲選択のハイライトは帯を避けて分割描画されます。
733
+ - **serverSide(`dataSource`)**: 使えますが、(1) クエリ(フィルター / ソート / グローバル)が変わると展開状態は**すべて閉じ**ます(結果セットが総入れ替えされ、未ロード行のキーを走査できないため)。(2) 未ロード行の帯は表示されません。
734
+ - **上限**: 帯は auto-height 行と同じ可変行高ジオメトリで描画するため、`rows × rowHeight + 展開中の帯の合計` が 15,000,000px(36px 行で約 41 万行)を超える構成では帯を描画しません(開発時警告。展開状態は保持され、行数を絞ると表示されます)。
735
+ - **行グルーピング併用**: 展開できるのは leaf 行のみ(グループ行は対象外)。
736
+
737
+ ### ラベル行(見出し / 区切り行)(`labelRow`)
738
+
739
+ `labelRow` prop を渡すと、`rows` の中の特定の行を「見出し(区切り)行」として扱えます。Excel のシートに置く節見出しの行と同じもので、データ行の間に置いた**その位置**に全幅の帯として描画され、行数には数えません。行の型 `T` も `rows` / `onRowsChange` の型も変わらないため、Excel / CSV 取り込みデータの形そのままで使えます。
740
+
741
+ **行グルーピングとの違い**
742
+
743
+ | | ラベル行(`labelRow`) | 行グルーピング(`rowGroup`) |
744
+ | --- | --- | --- |
745
+ | 何をするか | `rows` の中に**置いた位置**の見出し行を帯で描く | **列の値**で行をまとめ直し、グループ行を作る |
746
+ | 並び順 | `rows` の並びそのまま(ソートはセクション内に閉じる) | グループ単位に並び替わる |
747
+ | 集計 / ツリー / 開閉 | 無し | あり(`aggFunc` / 自動グループ列 / 開閉) |
748
+ | データ源 | 行データそのもの(`isLabelRow` で識別) | 列定義(`rowGroup: true`) |
749
+ | 併用 | **不可**(`rowGroup` 有効中はラベル行を表示しない。開発時警告) | |
750
+
751
+ ```tsx
752
+ type Row = { id: string; kind?: 'label'; code: string; name: string; qty: number };
753
+
754
+ <SpreadsheetGrid
755
+ rows={rows} // ラベル行はデータ行と同じ配列に混在させる
756
+ columns={columns}
757
+ rowKeyGetter={(row) => row.id}
758
+ labelRow={{
759
+ isLabelRow: (row) => row.kind === 'label',
760
+ getLabel: (row) => row.name,
761
+ sticky: true, // 縦スクロール中も現在セクションの見出しをヘッダー直下に固定
762
+ render: ({ label, sectionRowCount }) => (
763
+ <>
764
+ <strong>{label}</strong>
765
+ <span className="text-gray-500">{sectionRowCount} 件</span>
766
+ </>
767
+ ),
768
+ }}
769
+ />
770
+ ```
771
+
772
+ `LabelRowOptions<T>`:
773
+
774
+ | Name | Type | Default | Description |
775
+ | --- | --- | --- | --- |
776
+ | `isLabelRow` | `(row: T, sourceIndex: number) => boolean` | (required) | ラベル行の識別。`rows` / 本関数が変わったときに全行を 1 パス評価するため純粋・軽量であること。 |
777
+ | `getLabel` | `(row: T) => string` | (required) | 表示文字列。既定描画(`render` 未指定時)/ エクスポート / `aria-label` に使う。 |
778
+ | `render` | `(ctx: LabelRowRenderContext<T>) => ReactNode` | — | 中身の React 描画(装飾)。`ctx = { row, rowKey, rowIndex, sourceRowIndex, label, sectionRowCount }`(`sectionRowCount` はフィルター後のセクション内データ行数。serverSide では `undefined`)。 |
779
+ | `height` | `number \| ((row: T) => number)` | `rowHeight` | ラベル行の高さ(px)。行ごとに変えるときは関数。 |
780
+ | `className` | `GridSlotProps \| ((row: T) => GridSlotProps \| undefined)` | — | 行要素へ追加する class(または `{ className, style }`)。`classNames.labelRow` に加えて付与。 |
781
+ | `sticky` | `boolean` | `false` | 縦スクロール中、現在セクションのラベル行を列ヘッダー直下に固定する。次のラベル行が到達すると押し上げられて交代する(clientSide のみ)。 |
782
+ | `sortMode` | `'section' \| 'follow' \| 'hide'` | `'section'` | ソート / フィルター適用時の扱い(下記)。 |
783
+ | `keepEmptySections` | `boolean` | `false` | フィルターで中身が 0 件になったセクションのラベル行を残す。 |
784
+ | `exportText` | `(row: T) => string \| Array<string \| number \| null \| undefined>` | `getLabel` を先頭列へ | エクスポート(`includeLabelRows: true`)時の出力値。文字列は先頭列(他列は空)、配列は列順にそのまま。 |
785
+
786
+ - **セクション**: ラベル行から次のラベル行の直前までを 1 区間とみなします。最初のラベル行より前の行は「ラベル無しの区間」です。
787
+ - **ソート / フィルター(`sortMode`)**: `'section'`(既定)= 並べ替えは各セクションの中だけで行い、ラベル行の位置は動かない。フィルターで 0 件になったセクションはラベルごと消える(`keepEmptySections` で残せる)。`'follow'` = 全体を並べ替え、ラベル行は「元のセクションの先頭に来るデータ行」の直前に付いて移動する。`'hide'` = ソート / フィルター中はラベル行を出さない。ソートもフィルターも無いときは 3 モードとも `rows` の並びそのものです。
788
+ - **表示**: 帯は 3 ペインとも描き、中身(`getLabel` の文字列 / `render` の要素)は中央ペインに `position: sticky` で置かれ、横スクロールしてもビューポート左端(左固定ペインの右隣)に留まります(展開行カードと同じ機構)。行ヘッダー「#」は空欄で、データ行の行番号はラベル行を飛ばした通し番号になります。既定表示は左のアクセント線 + 太字(トークン `--ssg-label-row-bg` / `--ssg-label-row-text` / `--ssg-label-row-accent`)。
789
+ - **操作の対象外**: ラベル行はセルを持たないため、セル選択 / 編集 / コピー / クリア / チェックボックス行選択 / ホバー / コンテキストメニューの対象になりません。矢印キー(↑ / ↓)はラベル行に止まらず読み飛ばし、貼り付けはラベル行を飛ばして次のデータ行へ続けます(行は落ちません)。件数表示(bar の `Rows: X / Y`・行選択件数)はデータ行のみを数えます。範囲選択の塗りはラベル行の上も通過して描かれます(グループ行と同じ)。
790
+ - **行ドラッグ併用**: データ行はセクションを跨いで移動できます(移動先のセクションに所属が変わる)。ラベル行自体は掴めません。ソート / フィルター中に操作不可になる規則は従来どおりです。
791
+ - **展開行併用**: 可。ラベル行は展開できません(データ行のみ)。
792
+ - **serverSide(`dataSource`)**: サーバーが `getRows` の結果にラベル行を含めて返し(`totalRowCount` にも数える)、`isLabelRow` で識別します。セクションの並べ替え / `sectionRowCount` / 縦固定(`sticky`)/ ラベル行の `height` はサーバー行モデルでは適用されず、件数表示はサーバー総数のままです。
793
+ - **エクスポート**: 既定では出力に含みません(scope `'raw'` でもデータ行として出ません)。`exportCsv` / `getExportData` の `includeLabelRows: true` で 1 行として出力し、`getExportData` は `rowKinds` で行種を返します(「Excel / スプレッドシート エクスポート」節のレシピ参照)。コピー(`Ctrl/Cmd+C`)には含まれません。
794
+ - **上限**: `height` を指定した場合は可変行高ジオメトリで描画するため、論理全高が 15,000,000px を超える構成では上書きを諦めて `rowHeight` に戻ります。
795
+
796
+ ラベル行の記述子は `GridLabelRow<T>`(`{ kind: 'label', row, sourceIndex, label, sectionRowCount }`)としてバレルから公開されます(`LabelRowOptions<T>` / `LabelRowRenderContext<T>` / `LabelRowSortMode` も同様)。
797
+
798
+ ### 行ドラッグ並び替え(`enableRowDrag`)
799
+
800
+ `enableRowDrag` を付けると、先頭のハンドル列(⋮⋮)を掴んで行を上下へ動かせます(AG Grid の managed row dragging 相当)。並び替えの結果は通常の編集と同じく `onRowsChange` で新配列として返るため、消費側は `rows` を差し替えるだけです。
801
+
802
+ ```tsx
803
+ const [rows, setRows] = useState(initialRows);
804
+
805
+ <SpreadsheetGrid
806
+ rows={rows}
807
+ onRowsChange={setRows}
808
+ columns={columns}
809
+ rowKeyGetter={(row) => row.id}
810
+ enableRowDrag
811
+ isRowDraggable={(row) => !row.locked}
812
+ onRowMove={({ rowKey, fromIndex, toIndex, rows }) => saveOrder(rows)}
813
+ />
814
+ ```
815
+
816
+ - **操作**: ハンドルを押して上下へドラッグすると、挿入位置に水平のガイド線が出ます。ドロップで確定し、影響行が新しい位置へスライドします(`prefers-reduced-motion` では即時)。グリッドの枠外で離す / `Escape` でキャンセルします。上下端に近づくと自動スクロールします(仮想化された画面外の行へも運べます)。
817
+ - **ゴースト**: ドラッグ中はポインタ追従のピルに、先頭の(合成列でない)表示列の表示値(`valueFormatter` 適用後)を出します。空なら「行 N」。
818
+ - **データ契約**: 確定時は「1 要素を移動した新配列」(未変更行は参照共有)を `onRowsChange` に渡し、その直後に `onRowMove` を呼びます。掴んだ行の直上 / 直下(動かない位置)で離した場合はどちらも呼ばれません。履歴ラッパ経由のため `Ctrl/Cmd+Z` / `undo()` で戻せます。
819
+ - **有効条件**: clientSide(`rows` + `onRowsChange`)専用です。`dataSource`(serverSide)/ 行グルーピング中 / `onRowsChange` 未指定ではハンドル列を挿入しません。`readOnly` は関係しません(並び替えはセル編集ではないため)。
820
+ - **ソート / フィルター中**: 表示順と `rows` の順が一致しないため、ハンドルは淡色(`.ssg-row-drag-handle--disabled`)+ 理由のツールチップになり操作できません(列はそのまま残るのでレイアウトは跳ねません)。解除すると復帰します。
821
+ - **展開行との併用**: 展開中のマスター行は詳細パネルごと一緒に移動します。ドロップ位置の判定は詳細パネルの高さ込みで、パネルの上は「マスター行の下」として扱います。
822
+ - **ハンドル列**: 合成列のため、列メニュー / ソート / 列 DnD / autoSize / エクスポート / 並び替え管理パネルの対象外です。左固定列があるときは左固定側に、展開行トグル列よりさらに先頭に入ります。
823
+ - **命令的 API**: `moveRow(rowKey, toIndex)` は表示状態(ソート / フィルター)に関わらず元配列上で移動します(`onRowsChange` → `onRowMove` の順)。
824
+ - **将来拡張**: ドラッグ中に周囲の行がリアルタイムに退避する見せ方(`rowDragMotion: 'live'` 相当)は、スロット解決を共有したまま表示側だけ差し替えられる設計にしてあります(未実装)。
825
+ - **スタイル**: `.ssg-row-drag-handle`(+ `--disabled`)/ `.ssg-body-cell--row-drag-handle` / `.ssg-row-drop-indicator` / ドラッグ中の行 `.ssg-body-row[data-ssg-row-dragging]`。色はトークン(`--ssg-drop-indicator` / `--ssg-glyph-*` / `--ssg-ghost-*`)です。
826
+
827
+ ## 命令的 API(ref ハンドル / `SpreadsheetGridHandle<T>`)
828
+
829
+ 状態(列幅・可視・sort・filter 等)は controlled のまま、**prop では表現しづらい一発操作**だけを ref ハンドルで提供する。React 19 の **ref-as-prop**(`forwardRef` 不使用)で受け取る。
830
+
831
+ ```tsx
832
+ import { useRef } from 'react';
833
+ import {
834
+ SpreadsheetGrid,
835
+ type SpreadsheetGridHandle,
836
+ } from '@ishibashi0112/spreadsheet-grid';
837
+
838
+ const gridRef = useRef<SpreadsheetGridHandle<Row>>(null);
839
+ <SpreadsheetGrid<Row> ref={gridRef} columns={cols} rows={rows} />;
840
+ // gridRef.current?.scrollToRow(5000, { align: 'center' });
841
+ // const csv = gridRef.current?.exportCsv({ scope: 'selection' });
842
+ ```
843
+
844
+ `viewRowIndex` / `colIndex` は**ビュー座標**(フィルター/ソート適用後の表示 index。`colIndex` は固定列を含む視覚順 = 左→中央→右)。範囲外 index は内部でクランプ/無視する。
845
+
846
+ ### スクロール
847
+
848
+ | メソッド | 説明 |
849
+ | --- | --- |
850
+ | `scrollToRow(viewRowIndex, { align? })` | 指定行を可視域へ。`align`(既定 `'auto'`): `'auto'`(最小スクロール) / `'start'` / `'center'` / `'end'`。 |
851
+ | `scrollToCell(viewRowIndex, colIndex, { align? })` | 指定セルを縦横とも可視域へ。固定列(左右ピン)は常に可視のため横スクロールしない。 |
852
+ | `scrollToTop()` / `scrollToBottom()` | 先頭 / 末尾へ。 |
853
+ | `getVisibleRowRange()` | 現在描画中の行ウィンドウ `{ startIndex, endIndex }`(end 排他)。空は `null`。 |
854
+ | `getScrollPosition()` | 現在のスクロール位置 `{ top, left }`(px)。値はスクロールコンテナの生の `scrollTop` / `scrollLeft` で、`setScrollPosition` / `onScroll` と同一基準(往復で一貫)。未マウント時は `null`。 |
855
+ | `setScrollPosition({ top?, left? }, { behavior? })` | スクロール位置の設定(px)。省略側は現状維持・スクロール可能範囲へクランプ。`behavior` は `'auto'`(既定・即時)/ `'smooth'`。2 グリッドの双方向同期では `'auto'` を推奨(`'smooth'` は途中フレームの `onScroll` が `source:'user'` になり得る)。 |
856
+
857
+ スクロール変化の**通知**は props 側の `onScroll` で受け取る(`{ top, left, source }`・rAF で 1 フレーム 1 回に間引き)。`source: 'api'` は `setScrollPosition` / `scrollTo*` 系由来、`'user'` はそれ以外(ホイール / ドラッグ / キーボード等)。2 グリッドの双方向スクロール同期は「`onScroll` で `source === 'user'` のときだけ相手の `setScrollPosition` を呼ぶ」ことでループを止められる(proposals ⑧)。
858
+
859
+ ### 選択 / アクティブセル
860
+
861
+ | メソッド | 説明 |
862
+ | --- | --- |
863
+ | `getActiveCell()` | 現在のアクティブセル `{ row, col }`(なければ `null`)。 |
864
+ | `setActiveCell(cell \| null, { scrollIntoView? })` | アクティブセル設定(`null` で解除)。`scrollIntoView` で可視化も行う。 |
865
+ | `getSelection()` | 現在の選択状態(`GridSelection`)。 |
866
+ | `selectCell(viewRowIndex, colIndex, { scrollIntoView? })` | 単一セル選択(クリック相当)。 |
867
+ | `selectRange(range, { scrollIntoView? })` | セル範囲選択(ドラッグ相当)。アンカーは `range.start`。 |
868
+ | `clearSelection()` | 選択解除。 |
869
+ | `getSelectedRows()` | 選択に交差する行(distinct)を返す。serverSide はロード済み行のみ。 |
870
+
871
+ > 注: `getSelectedRows()` は**セル範囲選択**に交差する行です。下の**チェックボックス行選択**(`enableRowSelection`)とは別レイヤーで、そちらは `getSelectedRowKeys()` / `getSelectedRowData()` を使います。
872
+
873
+ ### 行選択(チェックボックス選択)
874
+
875
+ `enableRowSelection` を有効にしたチェックボックス行選択の状態を操作します(`getSelectedRows()`=セル範囲由来とは別物)。記述子は `RowSelectionModel = { type: 'include'; rowKeys } | { type: 'exclude'; rowKeys }`(exclude=全選択のうち除外。全選択をキー列挙せず表現)。
876
+
877
+ | メソッド | 説明 |
878
+ | --- | --- |
879
+ | `getRowSelection()` | 現在の行選択記述子(`RowSelectionModel`)。 |
880
+ | `setRowSelection(model)` | 行選択記述子を設定。controlled 時は `onRowSelectionChange` 経由で親へ委譲(内部 state は書かない)。 |
881
+ | `getSelectedRowKeys()` | 選択中の行キー配列。`include` はそのまま O(選択数)、`exclude` は現在の全行から除外を差し引いて列挙(O(行数))。serverSide はロード済みキーのみ。 |
882
+ | `getSelectedRowData()` | 選択中の行データ。行の探索が要るため O(行数)。キーで足りるなら `getSelectedRowKeys()` を推奨。serverSide はロード済み行のみ。 |
883
+ | `getSelectedRowCount()` | 選択件数。`exclude` は 総行数 − 除外数 で一定コスト。 |
884
+ | `isRowSelected(rowKey)` | 指定キーが選択中かを O(1) 判定。 |
885
+ | `selectAllRows()` | 全行を選択(exclude モード=キーを列挙しない)。 |
886
+ | `clearRowSelection()` | 行選択をすべて解除。 |
887
+
888
+ **操作(有効時)**: 行ヘッダ(行NO)ガター全体が選択のヒット領域。multiple はクリックでトグル・shift+クリック/ガタードラッグで範囲、single は常に 1 行。ヘッダ左上コーナーは tri-state の全選択チェック(`enableSelectAllRows`)。参照性能維持のため判定は Set の O(1)、全選択は除外集合でキーを materialize しません。
889
+
890
+ **controlled**: `rowSelection`(記述子)または `selectedRowKeys`(include 糖衣)を渡すと controlled。`onRowSelectionChange` で変化を受け、親が prop を更新して反映します。
891
+
892
+ ### 行グルーピング
893
+
894
+ 行グルーピング(`column.rowGroup`)有効時のグループ開閉を操作します。無効時はすべて no-op / 空配列です。
895
+
896
+ | メソッド | 説明 |
897
+ | --- | --- |
898
+ | `setGroupCollapsed(groupKey, collapsed)` | 指定グループを開閉する(`collapsed: true` = 折りたたみ)。`groupKey` は `getGroupRows()` の記述子から取得。同一イベント内の連続呼び出しも正しく積み重なる。 |
899
+ | `expandAllGroups()` / `collapseAllGroups()` | すべてのグループを展開 / 折りたたむ。 |
900
+ | `getGroupRows()` | 全グループ行の記述子(`GridGroupRow[]`)を DFS 順(表示順)で返す。開閉状態に関わらず全件。 |
901
+
902
+ ### 展開行(Master/Detail)
903
+
904
+ 展開行(`detailRow` prop)有効時の開閉を操作します。無効時はすべて no-op / 空配列です。
905
+
906
+ | メソッド | 説明 |
907
+ | --- | --- |
908
+ | `setDetailRowExpanded(rowKey, expanded)` | 指定行キー(`rowKeyGetter` の値)の展開行を開閉する。`isExpandable` が `false` の行は no-op。行がまだロードされていない / フィルターで除外中でもキーは保持され、表示可能になった時点で帯が出る。同一イベント内の連続呼び出しも正しく積み重なる。 |
909
+ | `getExpandedDetailRowKeys()` | 展開中の行キーを返す(`GridRowKey[]`)。 |
910
+ | `collapseAllDetailRows()` | すべての展開行を閉じる。「すべて開く」は提供しない(表示中の全行をまとめて開くと帯の合計高が大きくなりやすいため。必要なら `setDetailRowExpanded` を行ごとに呼ぶ)。 |
911
+
912
+ ### 行ドラッグ並び替え
913
+
914
+ | メソッド | 説明 |
915
+ | --- | --- |
916
+ | `moveRow(rowKey, toIndex)` | 指定行キーの行を元 `rows` 配列の `toIndex` へ移動する(clientSide + `onRowsChange` 指定時のみ。`enableRowDrag` / ソート / フィルターの状態には依存しない)。`onRowsChange`(履歴ラッパ経由 = undo 対象)→ `onRowMove` の順に呼ばれる。未知のキー / 同一位置 / 範囲外は no-op。serverSide では開発時警告 + no-op。 |
917
+
918
+ ### undo / redo(編集履歴)
919
+
920
+ グリッド編集(セル編集 / ペースト / `renderCell` の `setValue`)の取り消し/やり直しです。キーボード(`Ctrl/Cmd+Z` / `Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y`)と同じ操作をハンドルからも行えます。有効条件・制約は props の `enableUndoRedo` を参照(clientSide + `onRowsChange` + `readOnly=false` が前提)。
921
+
922
+ | メソッド | 説明 |
923
+ | --- | --- |
924
+ | `undo()` | 直近のグリッド編集を取り消す(`Ctrl/Cmd+Z` 相当)。無効条件下・履歴が空のときは no-op。 |
925
+ | `redo()` | undo で取り消した編集をやり直す(`Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` 相当)。undo 後に新しい編集が入った時点で redo 系譜は破棄される。 |
926
+ | `canUndo()` / `canRedo()` | undo / redo 可能かを返す(無効条件下では常に `false`)。 |
927
+ | `clearUndoHistory()` | 編集履歴を破棄する(rows は変更しない)。rows の外部差し替えはグリッド側でも自動検知して破棄するため、通常は呼ばなくてよい。 |
928
+
929
+ **挙動メモ**:
930
+
931
+ - 1 回の `onRowsChange`(= 1 回のセル確定 / 1 回のペースト / 1 回の Delete クリア)が 1 undo ステップ。複数セルへのペースト・範囲クリアも 1 ステップでまとめて戻る。
932
+ - undo は「変更前 rows 配列」をそのまま `onRowsChange` へ返す(セル値の逆適用ではなくスナップショット復元)。
933
+ - undo / redo は**編集時のアクティブセルとセレクションも復元**する(undo = 編集前の位置へ、redo = undo した時点の位置へ)。復元先のアクティブセルが画面外の場合は **`scrollToCell` の `'auto'` 相当(最小スクロール)で可視化まで追従**する(既に可視なら動かない)。
934
+ - ペーストの行自動拡張(`createRow`)も rows の一部なので undo で戻る。一方、列自動拡張(`createOverflowColumn` → `onColumnsChange`)は列が対象のため undo では戻らない。
935
+ - 編集エディタ内(`editingCell` 中)の `Ctrl+Z` はグリッドでは扱わず、input のネイティブ undo が効く。IME 変換中(`isComposing`)もグリッド側では発火しない。
936
+ - 可否の変化をリアクティブに受けたい場合は props の `onUndoRedoStateChange` を使う(`canUndo()` / `canRedo()` はポーリング用の命令的 API)。
937
+
938
+ ### バリデーション
939
+
940
+ | メソッド | 説明 |
941
+ | --- | --- |
942
+ | `getInvalidCells()` | `validate` 指定列 × 全ソース行をオンデマンドで全走査し、invalid セルの一覧(`GridInvalidCell[]` = `{ rowKey, sourceRowIndex, columnKey, message }`)を返す。保存前チェック用。invalid 表示は表示時導出のため状態を持たず、**呼ばれた時だけ計算**する(明示的な呼び出し = 明示的なコスト)。**`showValidationMarks` の表示状態と無関係に常に動作する**(マーク非表示中の送信前チェックに使える)。非表示列も対象(見えない列の不正値も検出)。clientSide 専用で、serverSide は全行を保持しないため空配列 + `console.warn`。 |
943
+
944
+ ### serverSide(SSRM)
945
+
946
+ | メソッド | 説明 |
947
+ | --- | --- |
948
+ | `refreshServerSide()` | serverSide(`dataSource`)のソフトリフレッシュ。クエリ(フィルター/ソート/グローバル)を変えずにキャッシュを破棄し、**スクロール位置を保ったまま現在の可視レンジを即時**(debounce なし)取り直す。件数は到着ブロックの `totalRowCount` で追従。宣言的に扱いたい場合は同挙動の `serverSideRefreshToken` prop もある(「serverSide モード」の節を参照)。clientSide(`rows`)では警告付き no-op。 |
949
+
950
+ ### CSV エクスポート
951
+
952
+ | メソッド | 説明 |
953
+ | --- | --- |
954
+ | `exportCsv(options?)` | CSV 文字列を返す(純粋・副作用なし)。 |
955
+ | `downloadCsv(filename?, options?)` | `exportCsv` の結果を `.csv` としてダウンロード(`filename` 既定 `'export.csv'`、`bom` 既定 `true`)。 |
956
+
957
+ `CsvExportOptions`: `scope`(下表)、`includeHeaders`(既定 `true`)、`delimiter`(既定 `','`。`'\t'` で TSV)、`bom`(`exportCsv` は既定 `false` / `downloadCsv` は既定 `true` = Excel 互換)、`includeLabelRows`(既定 `false`。ラベル行(`labelRow`)を `exportText`(未指定なら `getLabel` を先頭列・他列は空)の 1 行として出力する)。値整形はコピー(クリップボード)と同じ規則(`formatClipboardValue` があればそれ、無ければ `String(value ?? '')`)。RFC 4180 のクォート、行区切りは CRLF。props の `isRowExportable` 指定時は `false` の行が出力から行ごと除かれる(コピー / `getExportData` も同じ規則)。
958
+
959
+ **scope 対応表**(`exportCsv` / `downloadCsv` / `getExportData` 共通):
960
+
961
+ | scope | 意味 | スクロール位置 |
962
+ | --- | --- | --- |
963
+ | `'view'`(**既定**) | ビュー行全体(フィルター/ソート/列可視・固定順を反映) | 非依存 |
964
+ | `'raw'` | 全ソース行(`rows` 配列順)。**フィルターもソートも無視**(列は可視列・固定順に従う) | 非依存 |
965
+ | `'rendered'` | 仮想化ウィンドウ(いま**描画中**の行のみ・オーバースキャン込み) | **依存** |
966
+ | `'selection'` | 現在の選択範囲(セル/行/列)。選択なしは空 | — |
967
+ | `'all'` | **@deprecated** `'view'` のエイリアス(挙動同一) | 非依存 |
968
+ | `'visible'` | **@deprecated** `'rendered'` のエイリアス(挙動同一)。「フィルターで見えている行」では**ない**点に注意 | **依存** |
969
+
970
+ serverSide(SSRM)の注意: `'view'` は未ロード行をスキップ(= ロード済みビュー行のみ)。`'raw'` はソース行配列を持たないため `'view'` 相当へフォールバックし `console.warn` を出す。**全件エクスポートはサーバ側での実施を推奨**。
971
+
972
+ ### Excel / スプレッドシート エクスポート(getExportData)
973
+
974
+ | メソッド | 説明 |
975
+ | --- | --- |
976
+ | `getExportData(options?)` | 列メタ + 2 次元セルの、シリアライズ非依存な整形済みデータを返す(純粋・副作用なし)。 |
977
+
978
+ `GridExportOptions`: `scope`(既定 `'view'`。上記 **scope 対応表**と同一規則を共有)、`includeLabelRows`(既定 `false`。ラベル行を含める。含めたときは `rowKinds` で行種を判別できる)。
979
+
980
+ 戻り値 `GridExportData`:
981
+
982
+ ```ts
983
+ type GridExportData = {
984
+ columns: { key: string; title: string }[]; // 視覚順(selection では選択列のみ)
985
+ rows: { value: unknown; text: string }[][]; // scope の行レンジ(SSRM 未ロード行はスキップ)
986
+ rowKinds?: ('data' | 'label')[]; // includeLabelRows: true のときだけ(rows と同じ長さ)
987
+ };
988
+ ```
989
+
990
+ 各セルは生値 `value`(`getCellValue`)と文字列 `text`(CSV と同じ規則 = `formatClipboardValue ?? String(value ?? '')`)の双方を持つ。`value` があることで型付きセル(数値/日付のまま)+ Excel 側の数値書式へ流せる。`columns.key` はオブジェクト系ライブラリ向け、`title` はヘッダー表示向け。
991
+
992
+ **方針**: 本ライブラリは xlsx ライブラリを**同梱しない**(バンドル肥大・ライブラリ選定の押し付けを避ける)。グリッドは「現在の表」を整形済みデータで渡す**導線**に徹し、`.xlsx` / `.ods` 等の生成は consumer が任意のライブラリで行う。**マルチシートは consumer 側で本メソッドを scope 別 / グリッド別に呼び出して組み立てる**(グリッドは「1 表」を返すプリミティブ)。
993
+
994
+ #### レシピ: hucre(zero-dep・~14KB gzip・ESM/edge)
995
+
996
+ ```ts
997
+ import { writeXlsx } from 'hucre/xlsx';
998
+
999
+ const { columns, rows } = gridRef.current!.getExportData({ scope: 'view' });
1000
+ const buffer = await writeXlsx({
1001
+ sheets: [
1002
+ {
1003
+ name: 'Sheet1',
1004
+ columns: columns.map((c) => ({ header: c.title, key: c.key })),
1005
+ // 生値 value を型付きセルとして書く(数値/日付はそのまま)。
1006
+ data: rows.map((r) =>
1007
+ Object.fromEntries(r.map((cell, i) => [columns[i].key, cell.value])),
1008
+ ),
1009
+ },
1010
+ ],
1011
+ });
1012
+ // buffer(Uint8Array)を Blob 化してダウンロード。
1013
+ ```
1014
+
1015
+ #### レシピ: ExcelJS
1016
+
1017
+ ```ts
1018
+ import ExcelJS from 'exceljs';
1019
+
1020
+ const { columns, rows } = gridRef.current!.getExportData({ scope: 'view' });
1021
+ const wb = new ExcelJS.Workbook();
1022
+ const ws = wb.addWorksheet('Sheet1');
1023
+ ws.addRow(columns.map((c) => c.title)); // ヘッダー
1024
+ for (const r of rows) ws.addRow(r.map((cell) => cell.value)); // 型付きセル
1025
+ const buffer = await wb.xlsx.writeBuffer();
1026
+ ```
1027
+
1028
+ #### レシピ: ラベル行(見出し行)を結合セル + 太字で書く(ExcelJS)
1029
+
1030
+ `labelRow` を使うグリッドでは `includeLabelRows: true` で見出し行も出力し、`rowKinds` で行種を見て書式を当てます。
1031
+
1032
+ ```ts
1033
+ const { columns, rows, rowKinds } = gridRef.current!.getExportData({ includeLabelRows: true });
1034
+ const ws = wb.addWorksheet('Sheet1');
1035
+ ws.addRow(columns.map((c) => c.title));
1036
+ rows.forEach((r, i) => {
1037
+ const excelRow = ws.addRow(r.map((cell) => cell.value));
1038
+ if (rowKinds?.[i] === 'label') {
1039
+ ws.mergeCells(excelRow.number, 1, excelRow.number, columns.length); // 見出しは 1 行に結合
1040
+ excelRow.font = { bold: true };
1041
+ excelRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFEEF2F7' } };
1042
+ }
1043
+ });
1044
+ ```
1045
+
1046
+ #### レシピ: マルチシート
1047
+
1048
+ scope 違い(または複数グリッド)を複数シートに:
1049
+
1050
+ ```ts
1051
+ import { writeXlsx } from 'hucre/xlsx';
1052
+ import type { GridExportData } from '@ishibashi0112/spreadsheet-grid';
1053
+
1054
+ const toSheet = (name: string, d: GridExportData) => ({
1055
+ name,
1056
+ columns: d.columns.map((c) => ({ header: c.title, key: c.key })),
1057
+ data: d.rows.map((r) =>
1058
+ Object.fromEntries(r.map((cell, i) => [d.columns[i].key, cell.value])),
1059
+ ),
1060
+ });
1061
+
1062
+ const buffer = await writeXlsx({
1063
+ sheets: [
1064
+ toSheet('View', gridRef.current!.getExportData({ scope: 'view' })),
1065
+ toSheet('Selection', gridRef.current!.getExportData({ scope: 'selection' })),
1066
+ ],
1067
+ });
1068
+ ```
1069
+
1070
+ 1 グリッドをカテゴリ列で分割して N シートにする場合は、`getExportData({ scope: 'view' })` の戻りを「分割キー列の `value`」で group して各 group を `toSheet` 化する(列 index は `columns.findIndex((c) => c.key === '...')` で解決)。
1071
+
1072
+ ### UI パネル
1073
+
1074
+ | メソッド | 説明 |
1075
+ | --- | --- |
1076
+ | `openFilterManager()` | フィルター管理パネル(適用中の列フィルターの一覧 / 該当列へジャンプして編集 / 個別・全クリア / 追加)を開く。`enableColumnFilter=false` のときは何もしない。列メニューの「フィルターを管理…」/ 既定トップバーの **Filters chip クリック**(`enableColumnFilter=true` 時にクリック可能)と同じパネル。 |
1077
+ | `closeFilterManager()` | フィルター管理パネルを閉じる(開いていなければ何もしない)。 |
1078
+
1079
+ ### ツールチップ(TT-1)
1080
+
1081
+ グリッド内・ポータル内の操作ヒント / 切り詰めテキスト全文表示は、`title` 属性ではなく**カスタムツールチップ**(`.ssg-tooltip`・body 直下シングルトン)で表示される。表示対象は `data-ssg-tooltip="文言"` 属性で、window の pointerover / focusin 委譲で拾うため、**利用側が自前の要素(カスタムセル / renderHeader 等)へ同属性を付けても同じ見た目のツールチップが出る**。配色は `--ssg-tooltip-bg / --ssg-tooltip-text / --ssg-tooltip-shadow` トークンで調整可能。
1082
+
1083
+ **ボディセルの省略時ツールチップ**: グリッド prop `showCellOverflowTooltip`(既定 `false`)を `true` にすると、**既定テキストセルが省略(…)されているときだけ**ホバーで全文ツールチップが出る。実装は上記機構の派生で、セルへ `data-ssg-tooltip-overflow` マーカーを付け、表示可否は**ホバー時**に `scrollWidth > clientWidth` を判定(実際にクリップされているセルのみ表示)、文言はセルの表示テキスト(`textContent`)をそのまま使用する。`renderCell` 列(テキストとは限らない)と `autoHeight` 折り返し列(クリップされない)は対象外。特定列だけ全文表示したい / させたくない場合は、`renderCell` で自前要素へ `data-ssg-tooltip="…"`(常時)や `data-ssg-tooltip-overflow`(省略時)を付ける運用も可能。
1084
+
1085
+ ### 状態の保存 / 復元
1086
+
1087
+ | メソッド | 説明 |
1088
+ | --- | --- |
1089
+ | `getState()` | 永続化対象(手動リサイズ幅 / フィルター / ソート)のスナップショット `GridState` を返す(純粋・副作用なし)。新規オブジェクトなのでそのまま `JSON.stringify` して保存できる。 |
1090
+ | `applyState(state)` | `getState()` の値(または互換な部分形)を適用する。外部入力は内部で防御的に正規化され、幅 reset / フィルター一括 / ソート set の 3 dispatch(1 イベント = 1 再レンダー)で反映。clientSide / serverSide 双方に効く(SSRM は `filters`/`sort` 変化がクエリへ載り再取得)。 |
1091
+
1092
+ `GridState`: `{ version, columnWidths, filters, sort }`。`version` はマイグレーション用(現行 `1`)。対象は reducer 内の永続スライスのみで、列の可視/順序/ピン/flex は `columns` prop 側(consumer 所有)のため**含めない**。`activeCell` / `selection` などの一時 UI も含めない。`columnWidths` は手動リサイズした列のみを含む(flex 列はエントリを持たない規約)。`custom` フィルターの `value`(`unknown`)は深いコピーをしないため、シリアライズ可能性は consumer 責務。`applyState` は壊れた/部分的な入力にも耐える(非数値の幅・`kind` 無しの列フィルター・不正な `direction` は捨てる)が、列フィルター値の `kind` 中身までは検証しないため `getState` 出力の往復を前提とする。
1093
+
1094
+ ```ts
1095
+ // 保存(任意の永続先へ)。
1096
+ const state = gridRef.current?.getState();
1097
+ localStorage.setItem('grid-state', JSON.stringify(state));
1098
+
1099
+ // 復元。
1100
+ const saved = localStorage.getItem('grid-state');
1101
+ if (saved) gridRef.current?.applyState(JSON.parse(saved));
1102
+ ```
1103
+
1104
+ #### 変更通知 `onStateChange`
1105
+
1106
+ `onStateChange?: (state: GridState) => void`(prop)は、永続スライスが**実際に変化したとき**だけ最新 `GridState` を渡して呼ばれる。保存タイミングの signal として使える(`getState()` を別途叩く必要がない)。発火規約:
1107
+
1108
+ - **ドラッグ中は保留**: 列リサイズ / 範囲選択のドラッグ中は確定前のため発火しない。確定(ドラッグ終了)後に 1 回だけ評価する。これにより列リサイズの毎フレーム更新では発火せず、**確定幅で 1 回だけ**通知される。
1109
+ - **初回マウントでは発火しない**: 初期状態は通知対象外(復元は `applyState` 側の責務)。
1110
+ - **同値では発火しない**: 前回通知と構造等価(永続スライスが不変)なら発火しない。`activeCell` / `selection` などの一時 UI 変化では発火しない。
1111
+ - **`applyState` も「状態変化」として発火する**: 復元直後に同値を 1 回保存し直す可能性がある(冪等なので実害はない。避けたい場合は consumer 側で直前値と比較してスキップ)。
1112
+ - インライン関数を毎レンダー渡してよい(内部で latest-ref 経由で読むため、関数の参照変化では再評価しない)。
1113
+
1114
+ **スライス単位の通知 `onFiltersChange` / `onSortChange`**: `onStateChange` は列幅 / 列メタの変更でも呼ばれるため、フィルター / ソートだけを追いたい場合(記述子から WHERE / ORDER BY を組み立てる等)は `onFiltersChange?: (filters: GridFilterState) => void` / `onSortChange?: (sort: GridSortState) => void` を使う。それぞれ該当スライスが**構造的に変化したときだけ**複製を渡して呼ばれ、規約(初回非発火 / 同値非発火 / `applyState` でも発火)は `onStateChange` と同じ。ドラッグ中の保留は無い(フィルター / ソートはドラッグで変わらない)。`onStateChange` と併用でき、同じ変化では両方が呼ばれる。
1115
+
1116
+ ```ts
1117
+ // 自動保存(変化時)+ マウント時復元。
1118
+ const gridRef = useRef<SpreadsheetGridHandle<Row>>(null);
1119
+
1120
+ useEffect(() => {
1121
+ const saved = localStorage.getItem('grid-state');
1122
+ if (saved) gridRef.current?.applyState(JSON.parse(saved));
1123
+ }, []);
1124
+
1125
+ <SpreadsheetGrid
1126
+ ref={gridRef}
1127
+ columns={columns}
1128
+ rows={rows}
1129
+ onStateChange={(state) =>
1130
+ localStorage.setItem('grid-state', JSON.stringify(state))
1131
+ }
1132
+ />
1133
+ ```
1134
+
1135
+ ## serverSide モード(SSRM / DS-4 ②)
1136
+
1137
+ `dataSource` を渡すと serverSide モードになり、総行数ぶんの縦スクロール空間を保ったまま、可視窓に近いブロックだけを `getRows` で取得する(取得範囲を定数で縛りメモリを有界化)。未ロード行はスケルトン行として描画され、到着後に実データへ差し替わる。`rows`(clientSide)と `dataSource`(serverSide)は排他。セル編集は `dataSource.updateRows` を指定した場合のみ有効(楽観更新つきの書き戻し。「セル編集の書き戻し」節)。
1138
+
1139
+ ### query 配線(stage ②)
1140
+
1141
+ clientSide の操作状態(グローバルフィルター・列フィルター・ソート)を `ServerSideQuery` に組み立て、`getRows` の `params.query` として送出する(フィルター/ソートの実行はサーバへ委ねる)。
1142
+
1143
+ - **`ServerSideQuery`**: `{ globalText?: string; columnFilters?: Record<string, ColumnFilterValue>; sort?: GridSortState }`。全フィールドが空のときは `{}` を渡す。
1144
+ - **列フィルターの wire format**: `ColumnFilterValue` は `kind` を持つ discriminated union で、**そのまま**送出される(サーバはこの記述子を解釈して WHERE を組む)。`kind` 別の shape:
1145
+ - `{ kind: 'set'; mode?: 'include' | 'exclude'; values: string[] }` — `values` は常に小さい側のみ保持する(全候補が多いとき `mode: 'exclude'` で非選択側を送る)。サーバは `mode` に応じて IN / NOT IN を組む。
1146
+ - `{ kind: 'number'; raw: string; parsed }` — `parsed` が `comparison`(演算子 `>` `>=` `<` `<=` `=` `!=`)/ `range` / `blank` / `notBlank` / `null`(=`raw` で部分一致。旧 UI の互換値)。判定は常に `parsed` が正で、`raw` は人間可読の表示文字列(現行 UI は「10 以上」のような日本語。旧値は「>=10」等の式文字列)。比較 / 範囲では**空白セル(null / undefined / 空文字)は不一致**(空白の抽出は `blank` / `notBlank`)。
1147
+ - `{ kind: 'numberSet'; condition; set }` — 条件 AND 選択の複合(`filterType: 'numberSet'`)。`condition` は上記 `parsed` と同形(`null` = 条件なし)、`set` は `{ mode?: 'include' | 'exclude'; values: string[] }`(`null` = 全選択)。サーバは **condition AND set** で WHERE を組む(例: `qty >= 10 AND qty NOT IN (12)`)。両方 `null` の値は送出されない(クライアント側で clear へ正規化)。
1148
+ - `{ kind: 'textSet'; condition; set }` — テキスト版の複合(`filterType: 'textSet'`)。`condition` は `{ mode: 'contains' | 'equals' | 'startsWith' | 'endsWith'; value }` / `{ mode: 'blank' | 'notBlank' }` / `null`(判定は大文字小文字無視・`value` は trim 済み)。`set` と AND 結合の規約は numberSet と同一。
1149
+ - `{ kind: 'dateSet'; condition; set }` — 日付版の複合(`filterType: 'dateSet'`)。`condition` は `{ mode: 'range'; from; to }` / `{ mode: 'onOrAfter' | 'onOrBefore' | 'equals' | 'notEquals'; value }` / `{ mode: 'blank' | 'notBlank' }` / **`{ mode: 'preset'; preset: string }`**(ビルトインは `'today'` / `'thisMonth'` / `'last30days'`。列の `dateFilterPresets` で定義したカスタム ID もそのまま載る)/ `null`。日付は `'YYYY-MM-DD'`(ゼロ埋め ISO)。相対プリセットは**相対のまま送出される**ため、サーバ側も受信時点の「今日」を基準に解決すること(クライアントの clientSide 評価も同じ規約)。カスタム ID の解釈(id → WHERE 句)もサーバ側の責務(`resolve` はクライアント評価専用で送出されない)。`set.values` はセル生値ではなく**正規化済み日付キー**(`'YYYY-MM-DD'`。空白 = `''` / 日付として解釈できない値 = 生値)。
1150
+ - `{ kind: 'text'; value }` / `{ kind: 'date'; value }` / `{ kind: 'select'; value }`
1151
+ - `{ kind: 'custom'; value }` — `column.filterFn` 利用列の自由形値(サーバ解釈は利用側責務)。
1152
+ - アクティブなフィルターのみ送出される。キーは安定 queryKey のため昇順整列される。
1153
+ - **debounce**: query(filter/sort)の変更は約 300ms 静止後に一度だけ送出する(キーストロークごとの再フェッチを合体)。入力欄の表示自体は即時反映される。
1154
+ - **scroll-reset**: query が変わると結果セットが総入れ替えされるため、スクロールは先頭に戻る。
1155
+ - **enable\* フラグ**: serverSide でも `enableSorting` / `enableColumnFilter` / `enableGlobalFilter`(いずれも既定 true)が有効。サーバ非対応の操作を塞ぎたい場合に false にする。
1156
+
1157
+ ### set / select フィルターの候補(SSRM)
1158
+
1159
+ set / select / 複合(numberSet / textSet / dateSet)の候補集合はクライアントが供給する必要がある。**clientSide** は `rows` 全件から自動収集できるが、**serverSide** はクライアントが全件を持たないため自動収集できず候補が空になる(複合の条件欄は候補に依存しないため serverSide でも常に機能する)。
1160
+
1161
+ - **低カーディナリティ列**(状態・区分など): 列定義に `filterOptions` を静的指定する(serverSide でも set として機能する)。
1162
+ - **高カーディナリティ列**(品番・ID など): そもそも set 不適。`filterType: 'text'`(部分一致)や `number` 範囲を使う。
1163
+ - **サーバから非同期に供給**: グリッド prop `getFilterOptions`(async-options)を指定すると、popover を開くたびに `{ columnKey, column, columnFilters(他列), globalText, signal }` で呼ばれ、返した `{ options, truncated? }` が候補になる(読み込み中 / 失敗 + 再試行 / 打ち切り注記の表示付き。反転可)。高カーディナリティ列でも上限付き DISTINCT + `truncated: true` で set フィルターにできる。
1164
+ - `filterOptions` も `getFilterOptions` も無い set/select 列を serverSide で開くと、候補リストに「候補が未指定」である旨が表示される(バグではなく設定不足)。
1165
+
1166
+ ### dataSource とパラメータ
1167
+
1168
+ - **`initialRowCount`**: 初回 fetch 前から正しい総高さ/スクロールバーを出したい場合に渡す(未指定時は最初の `getRows` 結果が返るまで件数 0)。**mount 時に一度だけ読まれる**(下記 remount 契約を参照)。
1169
+ - **`blockSize`(既定 100)/ `maxCachedBlocks`(既定 64)**: 1 ブロックの行数とクライアント側 LRU 上限。超過分は画面外の古いブロックから退避する。
1170
+ - **`getRows(params)` 契約**: `params` は `{ startIndex, endIndex, query, signal }`。渡された `[startIndex, endIndex)`(view 空間・end 排他)を尊重し全件を返さないこと。`query` 適用後の**フィルター後総件数**を `result.totalRowCount` で返すこと(縦スクロール空間がこれに追従する)。`signal` が abort されたら速やかに reject すること。
1171
+ - **`result`**: `{ rows: T[]; totalRowCount: number }`。`rows` は要求レンジ内の存在ぶん(末端では要求幅より短くてよい)。
1172
+ - **`updateRows(params)`(任意)**: セル編集の書き戻し口。指定すると serverSide でもセル編集が有効になる(未指定なら編集 UI ごと無効)。詳細は「セル編集の書き戻し」節。
1173
+
1174
+ ### サーバ最新の取り直し(`refreshServerSide()` / `serverSideRefreshToken`)
1175
+
1176
+ サーバ側のデータが外部で更新された場合など、**クエリは変えずにサーバの最新を取り直したい**ときは、命令的ハンドルの **`refreshServerSide()`** を呼ぶ(batch 8 で追加)。宣言的に扱いたい場合(状態管理側で「更新すべき」を signal として持ち回る設計)は **`serverSideRefreshToken`**(`number`)を増やしても同じ挙動になる(どちらも内部の同一ソフトリフレッシュに委譲)。
1177
+
1178
+ - **`queryKey` 変化との違い**: フィルター/ソート/グローバルフィルターの変更は結果セットの総入れ替えなので**先頭へスクロールリセット**してキャッシュを全破棄し block 0 から取り直す。一方ソフトリフレッシュは**スクロール位置を保持**したままキャッシュを破棄し、**現在の可視レンジ**を即時(debounce なし)取り直す。
1179
+ - **件数**: リフレッシュ時に件数はリセットせず、到着ブロックの `totalRowCount` で更新する。外部更新で件数が増減していれば縦スクロール空間が追従する。可視レンジ未確立や件数 0(空結果)からのリフレッシュでは block 0 をブートストラップとして取り直す(空になったテーブルがサーバ側でデータを得た後も復帰できる)。
1180
+ - **挙動メモ**: 取り直し中は対象行が一瞬スケルトン表示になる(purge → 再取得)。`refreshToken` は単調増加で運用する(値が変わったときだけ取り直す)。初回 mount では発火しない。
1181
+ - **用途**: 外部での編集/追加削除(mutation)後の反映トリガーとして使う想定。グリッド上の**セル編集**は `dataSource.updateRows`(下の「セル編集の書き戻し」節)で直接書き戻せるため本機能は不要。**行の追加削除**は書き戻し API を持たないため、「サーバへ反映 → `refreshServerSide()`」の運用に寄せる(AG Grid も実質この形)。
1182
+
1183
+ ### getRows 失敗時のエラー表示とリトライ(batch 9)
1184
+
1185
+ `getRows` が reject する(abort 以外)と、失敗ブロックの行はスケルトンのまま残り、グリッド下部中央に**エラーバー**(「行の取得に失敗しました(N ブロック)」+ 再試行 / 閉じる)が表示される。
1186
+
1187
+ - **再試行**: 失敗中のブロック**だけ**を即時(debounce なし)取り直す。キャッシュ済みブロックには触れない(`refreshServerSide()` のような全破棄はしない)。再び失敗すればバーが再表示される。
1188
+ - **自然回復**: スクロールで失敗ブロックを再訪すると通常の可視レンジ要求として再 fetch され、成功すれば失敗は自動解除される(バーも消える)。
1189
+ - **閉じる(×)**: 同一の失敗状態の間だけ非表示になる。新しい失敗(失敗集合の変化)が起きると再表示される。クエリ変化 / `refreshServerSide()` / 全回復で失敗状態はリセットされる。
1190
+ - **abort の扱い**: スクロール通過・クエリ変化・unmount によるキャンセル(`signal` abort)は失敗として扱わない(バーも通知も出ない)。
1191
+ - **外部通知**: バーとは独立に、失敗ごとに `onServerSideLoadError(error, { startIndex, endIndex })` が呼ばれる(トースト / ログ用)。
1192
+
1193
+ ### セル編集の書き戻し(`updateRows`・楽観更新)
1194
+
1195
+ `dataSource.updateRows` を指定すると、serverSide でも**セル編集**(エディタ確定 / ペースト / Delete クリア / `renderCell` の `setValue` / checkbox トグル)が有効になり、編集はこの口を通してサーバへ書き戻される。**未指定なら serverSide の編集 UI は丸ごと無効**(セルは readOnly 表示になり、エディタも開かない — 書き戻し先が無い編集を受け付けないため)。
1196
+
1197
+ ```tsx
1198
+ const dataSource: ServerSideDataSource<Row> = {
1199
+ getRows: async ({ startIndex, endIndex, query, signal }) => { /* 取得 */ },
1200
+ updateRows: async ({ updates }) => {
1201
+ // updates: 行単位の更新記述子(1 ユーザー操作 = 1 呼び出し。ペーストの複数行も 1 回に集約)
1202
+ // { rowKey, rowIndex, row, previousRow, changes: [{ columnKey, previousValue, newValue }] }
1203
+ await api.patchRows(updates);
1204
+ // 返り値なし(または {})= グリッドの楽観値をそのまま確定。
1205
+ // サーバー側で値を正規化・計算した場合は updates と同順・同長で確定行を返すとキャッシュへマージされる:
1206
+ // return { rows: updates.map((u) => normalize(u.row)) };
1207
+ },
1208
+ };
1209
+ ```
1210
+
1211
+ - **楽観更新**: 編集はまず画面へ即時反映され(楽観値)、`updateRows` の resolve で確定する。確定までの間にブロックの再取得や LRU 退避が起きても楽観値の表示は消えない(確定前の編集はキャッシュとは別レイヤーで保持)。
1212
+ - **失敗時のロールバック**: `updateRows` が reject すると、該当セルは**編集前の値へ自動で戻り**、グリッド下部に保存失敗バー(「変更の保存に失敗しました(N 行)。値を元に戻しました」+ 閉じる)が表示される。値は復元済みのため再試行ボタンは無い(リトライ導線が必要なら `onServerSideWriteError` の `params.updates` から自前で再送する)。
1213
+ - **`onServerSideWriteError(error, { updates })`**: バーとは独立の外部通知(トースト / ログ / リトライ導線用)。`updates` は失敗した行更新(ロールバック済み)。
1214
+ - **同一行への連続編集**: 前の書き込みが確定する前に同じ行を再編集してよい(新しい編集が表示を支配し、古い書き込みの遅延決着が新しい値を巻き戻すことはない)。失敗時のロールバック先は常に「最後にサーバー確定した値」。
1215
+ - **バリデーションとの関係**: `validate` / `validationMode` は clientSide と同一規則で機能する(`reject` 列は確定拒否・ペースト/クリアのセル単位スキップ、`mark` 列は不正値も書き戻した上でマーク表示)。
1216
+ - **ペースト / Delete クリアの範囲**: 未ロード行(スケルトン)はスキップされる。ペーストの**行/列自動拡張は行わない**(`createRow` / `createOverflowColumn` は serverSide では不発。行追加は「サーバへ反映 → `refreshServerSide()`」運用)。
1217
+ - **undo/redo**: serverSide では無効のまま(適用先の全件 rows が無いため。取り消しはサーバ側の履歴で扱う)。
1218
+ - **`refreshServerSide()` / クエリ変更との関係**: 確定前(in-flight)の編集はリフレッシュ・フィルター/ソート変更で破棄され、以後の遅延決着も無視される(サーバ正本を取り直す操作が常に優先)。
1219
+ - **識別子**: `rowKey` は `rowKeyGetter` 由来(既定は view index のため、**書き戻しを使う場合は安定キーを返す `rowKeyGetter` の指定を推奨**)。`rowIndex` は view 空間(現在のフィルター/ソート適用後)の行位置で、`previousRow` にはサーバーが行を特定するための編集前スナップショットが入る。
1220
+
1221
+ ### clientSide ↔ serverSide の切替(remount 契約)
1222
+
1223
+ `initialRowCount` と内部の行数 state は mount 時に確定する。そのため **実行時にモードを切り替える場合は `key` を変えてグリッドを再マウントすること**(clientSide で mount 後に `dataSource` を後付けしても件数が初期化されない)。serverSide で直接 mount する通常利用ではこの限りではない。
1224
+
1225
+ ## テスト支援(`/testing` サブパス)
1226
+
1227
+ jsdom はレイアウトを計算しないため、素の jsdom では実グリッドの行・列が 1 本も描画されない(縦: スクロール要素の `clientHeight` / `clientWidth` が 0。横: 列仮想化(`@tanstack/virtual-core`)が **ResizeObserver の通知**から矩形を得るため、no-op スタブでは幅 0 のまま)。`@ishibashi0112/spreadsheet-grid/testing` の `installJsdomLayoutStubs()` がこれをまとめて解決する(proposals ③)。
1228
+
1229
+ ```ts
1230
+ // @vitest-environment jsdom
1231
+ import { installJsdomLayoutStubs } from '@ishibashi0112/spreadsheet-grid/testing';
1232
+
1233
+ beforeAll(() => {
1234
+ installJsdomLayoutStubs(); // 既定 1200×600。{ width, height } で変更可
1235
+ });
1236
+ ```
1237
+
1238
+ - インストール内容: `HTMLElement.prototype.clientHeight` / `clientWidth`(固定 getter)、`Element.prototype.getBoundingClientRect`(固定矩形)、`globalThis.ResizeObserver`(**observe 時に即時コールバック**するスタブ)、`Element.prototype.scrollTo`(無ければ no-op)。
1239
+ - 返り値はインストール前へ戻す restore 関数(`afterAll(restore)` 用・任意)。
1240
+ - React 非依存の小さなモジュールで、本体バンドルとは独立(`dist/testing.js`)。
1241
+ - これで `.ssg-body-row` / `.ssg-body-cell` の描画・付与クラス・セル文字列を DOM で検証できる(本体の `SpreadsheetGrid.jsdomLayoutStubs.integration.test.tsx` が動作保証)。
1242
+
1243
+ ## パーツ別スロット(`classNames` / `GridSlotProps`)
1244
+
1245
+ `className` 系の受け口(`classNames.*` / `cellClassName` / `getRowClassName` / `detailRow.className` / `labelRow.className`)はすべて **`GridSlotProps`** を受ける(バレルから `GridSlotProps` / `GridClassNames` を公開)。
1246
+
1247
+ ```ts
1248
+ type GridSlotProps = string | { className?: string; style?: CSSProperties };
1249
+ ```
1250
+
1251
+ - 文字列は従来どおり class として付与される。オブジェクト形は StyleX の `stylex.props(...)` の戻り値と同形で、そのまま渡せる(StyleX の動的スタイルは `style` 側の CSS 変数で届くため、この形が必要)。
1252
+ - `style` は当該パーツのルート要素へインラインで付与される。グリッドが位置決めに使う座標 / 寸法(`left` / `top` / `width` / `height` / `transform` 等)はグリッド側が後勝ちで上書きするため、レイアウトは壊せない。
1253
+ - インライン style は状態クラス(選択 / ホバー等の背景)にも勝つ。状態で切り替えたい装飾は className 側で行う。
1254
+ - `classNames` はレンダー毎に新しいオブジェクトを渡してもよい(内容の署名で memo され、行 / ヘッダーの再レンダーは増えない)。`getRowClassName` が返す style も内容比較で memo される。
1255
+ - `tooltip` / `dragGhost` は命令的 DOM(`document.body` 直下)のため、style の数値は単位なしのまま設定される(px が必要な値は `'12px'` のように文字列で)。
1256
+
1257
+ | スロット | 付与先 |
1258
+ | --- | --- |
1259
+ | `root` | ルート要素 `.ssg-root`(`className` / `style` prop と同じ要素) |
1260
+ | `toolbar` / `statusBar` | 既定トップバー `.ssg-bar--top` / 既定ボトムバー `.ssg-bar--bottom`(`renderTopBar` / `renderBottomBar` 指定時は対象外) |
1261
+ | `headerRow` / `headerCell` | ヘッダー行 / 列ヘッダーセル(コーナー・行ヘッダーセルは含まない) |
1262
+ | `rowHeaderCell` / `cornerCell` | 行ヘッダー「#」セルとコーナーセル(`rowHeaderCell` は両方、`cornerCell` はコーナーのみ) |
1263
+ | `bodyRow` / `bodyCell` | 本体行(データ / スケルトン / グループ行)/ データセル |
1264
+ | `groupRow` / `groupCell` | グループ行 / グループ行のセル(`bodyRow` / `bodyCell` に加えて付与) |
1265
+ | `detailBand` / `detailCard` | 展開行の帯 / カード(`detailRow.className` に加えて付与) |
1266
+ | `labelRow` / `labelRowContent` | ラベル行(見出し / 区切り行)の行要素 `.ssg-body-row[data-ssg-label-row]`(`bodyRow` / `labelRow.className` に加えて付与。縦固定の複製にも付く)/ 中身の器 `.ssg-label-row-content` |
1267
+ | `iconButton` | ヘッダーのアイコンボタン `.ssg-icon-btn` |
1268
+ | `popover` | ポータル系パネルの root(列メニュー / フィルター / コンテキストメニュー / select エディタ候補 / ツールパネル。`document.body` 直下) |
1269
+ | `menuItem` | メニュー項目 `.ssg-menu-item`(列メニュー / コンテキストメニュー) |
1270
+ | `tooltip` | カスタムツールチップ `.ssg-tooltip`(body 直下のシングルトン。複数グリッド同居時は最後に更新したグリッドの値) |
1271
+ | `dragGhost` | 列 / 行ドラッグのゴースト `[data-grid-drag-ghost]` |
1272
+ | `checkbox` | 行選択 / checkbox 列のチェックボックス glyph `.ssg-row-checkbox` |
1273
+ | `cellEditor` | セルエディタの枠 `.ssg-cell-editor` |
1274
+ | `emptyState` | 0 行時の空状態 `.ssg-empty-state` |
1275
+ | `filterChipBar` | フィルターチップバー `.ssg-filter-chip-bar` |
1276
+ | `errorBar` | SSRM のエラーバー `.ssg-ssrm-error-bar` |
1277
+ | `scrollHint` | スクロール位置インジケーター `.ssg-scroll-hint` |
1278
+ | `activeCellOverlay` / `selectionOverlay` | アクティブセル枠 / 範囲選択の塗り |
1279
+
1280
+ ### StyleX との併用
1281
+
1282
+ ```tsx
1283
+ import * as stylex from '@stylexjs/stylex';
1284
+ import { vars } from './tokens.stylex'; // stylex.defineVars({ accent: '#7c3aed', danger: '#dc2626' })
1285
+
1286
+ const s = stylex.create({
1287
+ grid: { '--ssg-accent': vars.accent, borderRadius: 8 }, // グリッドのトークンへ橋渡し
1288
+ cell: { fontVariantNumeric: 'tabular-nums' },
1289
+ negative: { color: vars.danger },
1290
+ width: (w: number) => ({ maxWidth: w }), // 動的スタイル(style 側で届く)
1291
+ });
1292
+
1293
+ <SpreadsheetGrid
1294
+ classNames={{ root: stylex.props(s.grid), bodyCell: stylex.props(s.cell) }}
1295
+ columns={[{ key: 'amount', title: '金額', cellClassName: (ctx) => stylex.props(typeof ctx.value === 'number' && ctx.value < 0 && s.negative, s.width(120)) }]}
1296
+ />
1297
+ ```
1298
+
1299
+ - StyleX は子孫セレクタを書けない(要素自身のクラスでしか装飾できない)ため、上記スロットに無い内部要素(ポップオーバー内のボタン / 入力欄等)はデザイントークン(`--ssg-*`)で調整する。
1300
+ - StyleX の atomic クラスとグリッドの基底クラスは同じ特異度 (0,1,0)。同じプロパティは後に読み込まれた方が勝つため、グリッド CSS を先に・StyleX の出力 CSS を後に読み込むか、`style.layer.css`(`@layer ssg-base`)を使う。StyleX 側で `useLayers` を有効にしている場合は、未レイヤーのグリッド CSS がレイヤー内に常に勝つため `style.layer.css` が必須。
1301
+
1302
+ ## スタイリング用の状態クラス(公開契約)
1303
+
1304
+ `cellClassName` / `getRowClassName` の返すクラスは、下記の内部付与クラスと連結セレクタで組み合わせて使える(例: `.ssg-body-cell.my-diff` で基底に勝たせ、`.ssg-body-cell.my-diff.ssg-body-cell--row-hovered` でホバー時色を切替)。以下は**公開契約**とし、変更時は breaking 扱いにする(proposals ⑥)。
1305
+
1306
+ | クラス | 付与先 / 条件 |
1307
+ | --- | --- |
1308
+ | `.ssg-root` | グリッドのルート要素(`className` prop の付与先)。 |
1309
+ | `.ssg-body-row` | 行コンテナ(`getRowClassName` の付与先のひとつ)。 |
1310
+ | `.ssg-body-cell` | データセルの基底(未レイヤー・特異度 (0,1,0))。 |
1311
+ | `.ssg-row-header-cell` | 行ヘッダー「#」セル。 |
1312
+ | `.ssg-body-cell--readonly` | 読み取り専用セル(範囲選択に入っていないとき。`dimReadOnlyCells` と独立して常時付与)。 |
1313
+ | `.ssg-body-cell--invalid` | validation mark 表示中のセル。 |
1314
+ | `.ssg-body-cell--row-hovered` | 行ホバー中のセル(`enableRowHover` 有効時)。 |
1315
+ | `.ssg-body-cell--autoheight` | auto-height 列のセル。 |
1316
+ | `.ssg-body-cell--align-center` / `.ssg-body-cell--align-right` | `column.align` の水平寄せ。 |
1317
+ | `.ssg-theme-dark` | `theme="dark"`(または `'auto'` のダーク解決)時に root と各ポータル(popover / menu / panel / ツールチップ)へ。 |
1318
+
1319
+ ※上記以外の `ssg-*` クラス(内部構造クラス)は非公開の実装詳細で、予告なく変わり得る。セレクタで依存しないこと。
1320
+
1321
+ ## 補助型(props で参照される shape)
1322
+
1323
+ - `GridRowKey = string | number`
1324
+ - `GridColumnPinned = 'left' | 'right'`
1325
+ - `GridSelectFilterOption = { label: string; value: string }`
1326
+ - `CellRenderContext<T> = { row, rowIndex, sourceRowIndex, rowKey, colIndex, value, column, isActive, isSelected, isEditing, readOnly, setValue, detail? }`
1327
+ - `detail?: CellDetailContext = { expanded, expandable, toggle, setExpanded }` は `detailRow` prop 有効時のみ定義(「展開行(Master/Detail)」節)。
1328
+ - `DetailRowOptions<T>` / `DetailRowRenderContext<T> = { row, rowKey, rowIndex, sourceRowIndex, collapse }` / `CellDetailContext`(展開行。バレルから公開)
1329
+ - `LabelRowOptions<T>` / `LabelRowRenderContext<T> = { row, rowKey, rowIndex, sourceRowIndex, label, sectionRowCount }` / `GridLabelRow<T> = { kind: 'label', row, sourceIndex, label, sectionRowCount }` / `LabelRowSortMode = 'section' | 'follow' | 'hide'`(ラベル行。バレルから公開。「ラベル行(見出し / 区切り行)」節)
1330
+ - `RowDragContext = { rowKey, sourceRowIndex }`(`isRowDraggable` の第 2 引数)/ `RowMoveParams<T> = { rowKey, fromIndex, toIndex, rows }`(`onRowMove` の引数。いずれもバレルから公開)
1331
+ - `CellStyleContext<T>` = 上記から `setValue` を除いた読み取り専用版(`cellClassName` 関数へ渡る)。バレル(`index.ts`)から公開(`import type { CellStyleContext } from '@ishibashi0112/spreadsheet-grid'`)
1332
+ - `rowIndex` は**ビュー行 index**(ソート / フィルター適用後の表示位置)、`sourceRowIndex` は**元 `rows` の index**、`rowKey` は行キー(`rowKeyGetter` 由来、既定は source index)。ソート / フィルター ON の画面で「エラー行 index の集合」など source 基準のデータと突き合わせるときは `sourceRowIndex` / `rowKey` を使う(`getInvalidCells()` の返す `sourceRowIndex` / `rowKey` と同一基準)。serverSide では view 順が正準のため `sourceRowIndex` は view index と同値。
1333
+ - `RowStyleContext<T> = { row, rowIndex, sourceRowIndex, rowKey, isSelected }`(`getRowClassName` の第 3 引数。バレルから公開)
1334
+ - `GridSlotProps = string | { className?: string; style?: CSSProperties }`(`classNames.*` / `cellClassName` / `getRowClassName` / `detailRow.className` の値。`GridClassNames` と共にバレルから公開)
1335
+ - `GridFrameworkTypes = { node: unknown; style: object }` / `ReactGridTypes = { node: ReactNode; style: CSSProperties }`(バレルから公開)。公開型の本体は React 非依存の core(描画ノード / style の型を束ね型 `F` で受ける)で、利用者が普段使う `GridColumn<T>` 等は core を `ReactGridTypes` で固定したエイリアス。型引数の数・名前は従来どおりで、通常は意識不要。
1336
+ - `rowIndex` / `sourceRowIndex` / `rowKey` の基準は `CellStyleContext` と同一。`isSelected` はチェックボックス行選択(`enableRowSelection`)の選択状態(範囲選択とは別)。グループ行(grouping 有効時)は専用描画のため `getRowClassName` の対象外。
1337
+ - `GridScrollPosition = { top: number; left: number }`(`getScrollPosition` の返り値 / `setScrollPosition` の基準)
1338
+ - `GridScrollEventParams = { top: number; left: number; source: 'user' | 'api' }`(`onScroll` の引数。`source` の意味は props 表の `onScroll` 行を参照)
1339
+ - `HeaderRenderContext<T> = { colIndex, width, column, filterValue?, isFiltered? }`
1340
+ - `SpreadsheetGridSlotContext<T> = { rows, filteredRows, columns, visibleColumns, globalFilterText, columnFilterValues, sortState, setGlobalFilterText, activeCell, selection, derivedSummary, globalFilterStatus, globalFilterProgress }`
1341
+ - `derivedSummary` は `SpreadsheetGridDerivedSummary`(行/列/フィルター/ソートの summary 文字列・選択統計などを内包)。helper を import せずトップ/ボトムバーで使える。
1342
+ - `globalFilterStatus: GlobalFilterStatus`(`'idle' | 'filtering' | 'ready'`)/ `globalFilterProgress: number`(0..1)。グローバルテキストフィルタは行数が大きい(しきい値 50,000 行・条件は `rows.length > 50000`)とき、入力を主スレッドを塞がず時間分割で適用する。適用中は `status='filtering'`・`progress` が進捗(0..1)になる。空/無効は `'idle'`、確定は `'ready'`(progress=1)。50,000 行以下は同期適用のため即 `'ready'`(`'filtering'` を経由しない)。**ローディング表示はグリッドが本体に重ねる組み込み overlay(autosize の計測中 overlay と同じ作法)で行うため、トップバーやカスタム UI 側で扱う必要は通常ない。** この 2 値は、入力の無効化や独自インジケータなどカスタム UI を出したい場合の参照用に公開している。serverSide では基本 `'idle'` / `'ready'`(取得中表示は行スケルトンが担当)。
1343
+
1344
+ ## ライブラリ化の宿題(現状把握)
1345
+
1346
+ - 〔解消〕**no-op props**: `enableClipboard` / `enableColumnResize` を型から削除(常時 ON 固定の挙動は不変)。将来「無効化」が必要になれば配線つきで非破壊追加する。
1347
+ - 〔追加済み〕**imperative API(ref ハンドル)**: `SpreadsheetGridProps.ref` で `SpreadsheetGridHandle<T>` を受け取り、スクロール / 選択操作 / CSV / エクスポートデータ(`getExportData`)/ 状態の保存・復元を命令的に呼べる(React 19 ref-as-prop、`forwardRef` 不使用)。詳細は「命令的 API」節。列状態のシリアライズ(`getState` / `applyState`)・変更通知 `onStateChange` prop は追加済み(対象は reducer 内の永続スライス = 手動リサイズ幅 / フィルター / ソート)。列の可視/順序/ピン/flex の状態化は `columns` prop 側で consumer 所有のため未対応(将来 columns 抽出/適用を入れるなら別途合意)。
1348
+ - 〔解消〕**公開バレル(`index.ts`)**: 入口を `index.ts` に集約し、`SpreadsheetGrid`(named)と公開型群(serverSide 型・`RowModel` 含む)を再エクスポート。`default export` は廃止。
1349
+ - 〔解消〕**テーマ/スタイリング API**: `className` / `style` / `classNames`(25 スロット。値は `GridSlotProps = string | { className, style }`)/ `cellClassName` / `getRowClassName` / CSS トークン(`--ssg-*`)を公開(「パーツ別スロット」節)。