@ishibashi0112/spreadsheet-grid 0.14.0 → 0.16.0

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.
package/README.md CHANGED
@@ -17,6 +17,8 @@ A high-performance, virtualized spreadsheet / data grid for **React 19**, writte
17
17
  - Three-pane pinned columns (left / center / right) via sticky positioning.
18
18
  - Sorting, per-column filters (`text` / `number` / `date` / `select` / `set` / `custom`), and a global filter.
19
19
  - In-cell editing and clipboard copy / paste, with range selection, keyboard navigation, and Delete / Backspace to clear selected cells. Cell editing is IME-aware (composing Enter never commits the cell).
20
+ - Built-in cell editor types via `column.editor` — `text` (default), `number` (`min` / `max` / `step`), `select` (dropdown with keyboard navigation & type-ahead; static or per-row options), `date` (native date picker), `checkbox` (direct toggle by click / Space, no edit session, custom checked/unchecked value mapping), and `custom` (render your own editor with `ctx.commit` / `ctx.cancel`; committing a non-string value bypasses parsing). Built-in editors auto-supply a default string→value parser (an explicit `parseClipboardValue` always wins) so paste and clear stay type-consistent.
21
+ - Cell validation via `column.validate` — default `'mark'` mode accepts the value but flags the cell (background + corner marker + hover tooltip message, derived at render time so it always matches `rows`, including after undo or external replacement); opt-in `'reject'` mode (`validationMode: 'reject'`) refuses invalid writes: the editor keeps editing with an inline error bubble, paste / clear skip the offending cells. `getInvalidCells()` on the imperative handle scans all rows for a pre-save check.
20
22
  - Undo / redo for grid edits (cell edits, paste, clear) — `Ctrl/Cmd+Z`, `Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y`, plus `undo()` / `redo()` / `canUndo()` / `canRedo()` on the imperative handle and an `onUndoRedoStateChange` callback for toolbars. Restores the edited cell's active cell & selection and scrolls it back into view. History is snapshot-based with structural sharing, capped by `undoHistoryLimit` (default 100), and clears automatically when `rows` is replaced externally (client-side row model only).
21
23
  - Optional auto-height rows for wrapped, variable-height content.
22
24
  - Auto-fit column widths to content on data load — `autoSizeColumns="onMount"` (once, on first data) or `"onDataChange"` (every time `rows` changes, e.g. after a form submit). Same engine as the column menu's "Autosize All Columns"; opt individual columns out with `suppressAutoSize`.
@@ -114,6 +116,52 @@ export function Example() {
114
116
 
115
117
  `rows` and `onRowsChange` make the grid a controlled component. A column needs at least `key` and `width`.
116
118
 
119
+ ## Cell editors & validation
120
+
121
+ Pick an editor per column with `column.editor`, and validate values with `column.validate`:
122
+
123
+ ```tsx
124
+ const columns: GridColumn<Row>[] = [
125
+ {
126
+ key: 'name', title: 'Name', width: 200,
127
+ // 'reject' refuses invalid writes: the editor keeps editing with an error
128
+ // bubble; paste / Delete skip the offending cells.
129
+ validate: ({ value }) => (String(value ?? '').length > 0 ? true : 'Name is required'),
130
+ validationMode: 'reject',
131
+ },
132
+ {
133
+ key: 'qty', title: 'Qty', width: 120, align: 'right',
134
+ editor: { type: 'number', min: 0, step: 1 },
135
+ // Default 'mark' mode: invalid values are accepted but flagged on the cell
136
+ // (background + corner marker + tooltip message on hover).
137
+ validate: ({ value }) =>
138
+ typeof value === 'number' && value >= 0 ? true : 'Enter a number ≥ 0',
139
+ },
140
+ {
141
+ key: 'status', title: 'Status', width: 140,
142
+ editor: { type: 'select', options: [
143
+ { value: 'active', label: 'Active' },
144
+ { value: 'hold', label: 'On hold' },
145
+ ] },
146
+ },
147
+ { key: 'shippedAt', title: 'Shipped', width: 140, editor: { type: 'date' } },
148
+ // Direct toggle by click / Space — no edit session. Values default to true / false;
149
+ // map them with checkedValue / uncheckedValue (e.g. '1' / '0').
150
+ { key: 'done', title: 'Done', width: 90, align: 'center', editor: { type: 'checkbox' } },
151
+ // Bring your own editor: render anything, call ctx.commit(value) / ctx.cancel().
152
+ // Committing a non-string value bypasses parsing and writes the domain value as-is.
153
+ { key: 'amount', title: 'Amount', width: 140,
154
+ editor: { type: 'custom', render: (ctx) => <MyAmountEditor ctx={ctx} /> } },
155
+ ]
156
+ ```
157
+
158
+ Before saving, collect every invalid cell (client-side row model):
159
+
160
+ ```ts
161
+ const invalid = gridRef.current?.getInvalidCells()
162
+ // -> [{ rowKey, sourceRowIndex, columnKey, message }, ...]
163
+ ```
164
+
117
165
  ## Sizing
118
166
 
119
167
  By default the grid caps its height at `480px` (`max-height`) and scrolls when the content is taller. Pass `height` to take explicit control — use `height="100%"` to follow the parent's height, or a pixel value:
@@ -250,6 +298,8 @@ The full prop and type reference lives in [`src/components/spreadsheet-grid/API_
250
298
  - `position: sticky` による 3 ペイン固定列(左 / 中央 / 右)。
251
299
  - ソート、列ごとのフィルター(`text` / `number` / `date` / `select` / `set` / `custom`)、グローバルフィルター。
252
300
  - セル内編集とクリップボードのコピー/貼り付け、範囲選択、キーボード操作、Delete / Backspace による選択セルのクリア。セル編集は IME 対応(変換確定の Enter でセルが確定されない)。
301
+ - `column.editor` による組み込みエディタ種別 — `text`(既定)/ `number`(`min` / `max` / `step`)/ `select`(キーボード操作・タイプアヘッド付きドロップダウン。候補は静的配列 or 行依存関数)/ `date`(ネイティブ日付ピッカー)/ `checkbox`(クリック / Space の直接トグル。編集セッションなし、checked/unchecked の値マッピング可)/ `custom`(`ctx.commit` / `ctx.cancel` で自作エディタを差し込み。非 string の commit はパースをバイパス)。組み込みエディタは「文字列 → 値」の既定パーサを自動供給し(明示の `parseClipboardValue` が常に優先)、貼り付け・クリアでも型が揃います。
302
+ - `column.validate` によるセル検証 — 既定の `'mark'` モードは値を受け入れつつセルへ invalid 表示(背景 + 右上マーカー + ホバーでメッセージ。表示時導出のため undo や外部差し替え後も常に `rows` と整合)。`validationMode: 'reject'` で不正な書き込み自体を拒否(エディタはエラーバブル表示で編集継続、貼り付け / クリアは該当セルのみスキップ)。ハンドルの `getInvalidCells()` で保存前の全行チェックができます。
253
303
  - グリッド編集の undo / redo(セル編集・貼り付け・クリア)— `Ctrl/Cmd+Z`、`Ctrl/Cmd+Shift+Z` / `Ctrl/Cmd+Y` に加え、ハンドルの `undo()` / `redo()` / `canUndo()` / `canRedo()` とツールバー向けの `onUndoRedoStateChange` コールバック。編集時のアクティブセル・選択範囲まで復元し、画面外なら可視位置へスクロールで追従。履歴は構造共有のスナップショット方式で `undoHistoryLimit`(既定 100)まで保持し、`rows` が外部から差し替えられたときは自動破棄(クライアントサイド行モデル専用)。
254
304
  - 折り返し・可変行高に対応する auto-height 行(任意)。
255
305
  - データ投入時に列幅を内容へ自動フィット — `autoSizeColumns="onMount"`(初回にデータが載った一度きり)/ `"onDataChange"`(`rows` が変わるたび。フォーム送信結果の差し替え等)。列メニュー「すべての列の幅を自動調整」と同一エンジンで、列個別の除外は `suppressAutoSize`。
@@ -347,6 +397,52 @@ export function Example() {
347
397
 
348
398
  `rows` と `onRowsChange` でグリッドは controlled になります。列には最低限 `key` と `width` が必要です。
349
399
 
400
+ ### セルエディタとバリデーション
401
+
402
+ 列ごとに `column.editor` でエディタ種別を選び、`column.validate` で値を検証できます:
403
+
404
+ ```tsx
405
+ const columns: GridColumn<Row>[] = [
406
+ {
407
+ key: 'name', title: '品名', width: 200,
408
+ // 'reject' は不正な書き込み自体を拒否: エディタはエラーバブル表示で編集継続、
409
+ // 貼り付け / Delete は該当セルのみスキップされます。
410
+ validate: ({ value }) => (String(value ?? '').length > 0 ? true : '品名は必須です'),
411
+ validationMode: 'reject',
412
+ },
413
+ {
414
+ key: 'qty', title: '数量', width: 120, align: 'right',
415
+ editor: { type: 'number', min: 0, step: 1 },
416
+ // 既定の 'mark' モード: 不正値も一旦入り、セルに invalid 表示
417
+ // (背景 + 右上マーカー + ホバーでメッセージ)が付きます。
418
+ validate: ({ value }) =>
419
+ typeof value === 'number' && value >= 0 ? true : '0 以上の数値を入力してください',
420
+ },
421
+ {
422
+ key: 'status', title: '状態', width: 140,
423
+ editor: { type: 'select', options: [
424
+ { value: '有効', label: '有効' },
425
+ { value: '保留', label: '保留' },
426
+ ] },
427
+ },
428
+ { key: 'shippedAt', title: '出荷日', width: 140, editor: { type: 'date' } },
429
+ // クリック / Space の直接トグル(編集セッションなし)。値は既定 true / false、
430
+ // checkedValue / uncheckedValue でマッピング可(例: '1' / '0')。
431
+ { key: 'done', title: '完了', width: 90, align: 'center', editor: { type: 'checkbox' } },
432
+ // 自作エディタ: 任意の UI を描画し、ctx.commit(value) / ctx.cancel() を呼びます。
433
+ // 非 string の commit はパースをバイパスしてドメイン値をそのまま書き込みます。
434
+ { key: 'amount', title: '金額', width: 140,
435
+ editor: { type: 'custom', render: (ctx) => <MyAmountEditor ctx={ctx} /> } },
436
+ ]
437
+ ```
438
+
439
+ 保存前に invalid セルを一括取得できます(クライアントサイド行モデル):
440
+
441
+ ```ts
442
+ const invalid = gridRef.current?.getInvalidCells()
443
+ // -> [{ rowKey, sourceRowIndex, columnKey, message }, ...]
444
+ ```
445
+
350
446
  ### サイズ(高さ)
351
447
 
352
448
  既定ではグリッドの高さは `480px`(`max-height`)で頭打ちになり、中身がそれより高いとスクロールします。`height` を渡すと高さを明示制御できます。`height="100%"` で親要素の高さに追従、`number` で px 指定です:
@@ -1,19 +1,30 @@
1
- export type EditorCommitDirection = 'down' | 'right' | 'left';
1
+ import type { EditorCommitDirection, EditorCommitResult, GridColumn, GridColumnEditor } from './model/gridTypes';
2
+ export type { EditorCommitDirection };
2
3
  type CellEditorRect = {
3
4
  left: number;
4
5
  top: number;
5
6
  width: number;
6
7
  height: number;
7
8
  };
8
- type CellEditorLayerProps = {
9
+ export type CellEditorSession<T> = {
10
+ row: T;
11
+ rowIndex: number;
12
+ colIndex: number;
13
+ column: GridColumn<T>;
14
+ value: unknown;
15
+ };
16
+ type CellEditorLayerProps<T> = {
9
17
  rect: CellEditorRect | null;
10
18
  headerHeight: number;
11
19
  leadingWidth: number;
12
20
  baseOffset?: number;
13
21
  initialValue: string;
14
- onCommit: (value: string, direction?: EditorCommitDirection) => void;
22
+ editor?: GridColumnEditor<T>;
23
+ editorSession?: CellEditorSession<T> | null;
24
+ themeClassName?: string;
25
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
15
26
  onCancel: () => void;
16
27
  align?: 'left' | 'center' | 'right';
17
28
  };
18
- export declare function CellEditorLayer({ rect, headerHeight, leadingWidth, baseOffset, initialValue, onCommit, onCancel, align, }: CellEditorLayerProps): import("react").JSX.Element | null;
29
+ export declare function CellEditorLayer<T>({ rect, headerHeight, leadingWidth, baseOffset, initialValue, editor, editorSession, themeClassName, onCommit, onCancel, align, }: CellEditorLayerProps<T>): import("react").JSX.Element | null;
19
30
  export default CellEditorLayer;
@@ -1,3 +1,3 @@
1
1
  import './styles.css';
2
2
  import type { SpreadsheetGridProps } from './model/gridTypes';
3
- export declare function SpreadsheetGrid<T extends object>({ rows, dataSource, serverSideRefreshToken, columns, onRowsChange, onColumnsChange, rowKeyGetter, createRow, createOverflowColumn, rowHeight: rowHeightProp, autoHeight, estimateRowHeight, headerHeight: headerHeightProp, density, theme, rowHeaderWidth, height, maxHeight, readOnly, dimReadOnlyCells, canEditCell, enableUndoRedo, undoHistoryLimit, onUndoRedoStateChange, enableRangeSelection, enableRowSelection, rowSelectionMode, enableSelectAllRows: enableSelectAllRowsProp, rowSelection: rowSelectionProp, selectedRowKeys: selectedRowKeysProp, onRowSelectionChange, enableGlobalFilter, enableColumnFilter, enableSorting, enableColumnResize, autoSizeColumns, showCellOverflowTooltip, enableRowHover, enableColumnHeaderHover, enableColumnMenu, noMatchingRowsText, noRowsText, showTopBar, showBottomBar, showTopBarSummary, showTopBarFilter, globalFilterPlaceholder, globalFilterIcon, showTopBarCounts, showBottomBarCounts, showFilterChipBar, renderTopBar, renderBottomBar, className, classNames, getRowClassName, enableContextMenu, getContextMenuItems, onContextMenuOpen, ref, onStateChange, }: SpreadsheetGridProps<T>): import("react").JSX.Element;
3
+ export declare function SpreadsheetGrid<T extends object>({ rows, dataSource, serverSideRefreshToken, onServerSideLoadError, columns, onRowsChange, onColumnsChange, rowKeyGetter, createRow, createOverflowColumn, rowHeight: rowHeightProp, autoHeight, estimateRowHeight, headerHeight: headerHeightProp, density, theme, rowHeaderWidth, height, maxHeight, readOnly, dimReadOnlyCells, canEditCell, enableUndoRedo, undoHistoryLimit, onUndoRedoStateChange, enableRangeSelection, enableRowSelection, rowSelectionMode, enableSelectAllRows: enableSelectAllRowsProp, rowSelection: rowSelectionProp, selectedRowKeys: selectedRowKeysProp, onRowSelectionChange, enableGlobalFilter, enableColumnFilter, enableSorting, enableColumnResize, autoSizeColumns, showCellOverflowTooltip, enableRowHover, enableColumnHeaderHover, enableColumnMenu, noMatchingRowsText, noRowsText, showTopBar, showBottomBar, showTopBarSummary, showTopBarFilter, globalFilterPlaceholder, globalFilterIcon, showTopBarCounts, showBottomBarCounts, showFilterChipBar, renderTopBar, renderBottomBar, className, classNames, getRowClassName, enableContextMenu, getContextMenuItems, onContextMenuOpen, ref, onStateChange, }: SpreadsheetGridProps<T>): import("react").JSX.Element;
@@ -0,0 +1,4 @@
1
+ export declare function CellEditorErrorBubble({ message }: {
2
+ message: string;
3
+ }): import("react").JSX.Element;
4
+ export default CellEditorErrorBubble;
@@ -0,0 +1,7 @@
1
+ type CheckboxCellProps = {
2
+ checked: boolean;
3
+ readOnly: boolean;
4
+ onToggle: () => void;
5
+ };
6
+ export declare function CheckboxCell({ checked, readOnly, onToggle }: CheckboxCellProps): import("react").JSX.Element;
7
+ export default CheckboxCell;
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { CellEditorContext } from '../model/gridTypes';
3
+ type CustomCellEditorProps<T> = {
4
+ render: (ctx: CellEditorContext<T>) => ReactNode;
5
+ context: CellEditorContext<T>;
6
+ };
7
+ export declare function CustomCellEditor<T>({ render, context, }: CustomCellEditorProps<T>): import("react").JSX.Element;
8
+ export default CustomCellEditor;
@@ -0,0 +1,9 @@
1
+ import type { EditorCommitDirection, EditorCommitResult } from '../model/gridTypes';
2
+ type DateCellEditorProps = {
3
+ initialValue: string;
4
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
5
+ onCancel: () => void;
6
+ align?: 'left' | 'center' | 'right';
7
+ };
8
+ export declare function DateCellEditor({ initialValue, onCommit, onCancel, align, }: DateCellEditorProps): import("react").JSX.Element;
9
+ export default DateCellEditor;
@@ -0,0 +1,12 @@
1
+ import type { EditorCommitDirection, EditorCommitResult } from '../model/gridTypes';
2
+ type NumberCellEditorProps = {
3
+ initialValue: string;
4
+ min?: number;
5
+ max?: number;
6
+ step?: number;
7
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
8
+ onCancel: () => void;
9
+ align?: 'left' | 'center' | 'right';
10
+ };
11
+ export declare function NumberCellEditor({ initialValue, min, max, step, onCommit, onCancel, align, }: NumberCellEditorProps): import("react").JSX.Element;
12
+ export default NumberCellEditor;
@@ -0,0 +1,11 @@
1
+ import type { EditorCommitDirection, EditorCommitResult, GridSelectEditorOption } from '../model/gridTypes';
2
+ type SelectCellEditorProps = {
3
+ options: GridSelectEditorOption[];
4
+ value: unknown;
5
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
6
+ onCancel: () => void;
7
+ align?: 'left' | 'center' | 'right';
8
+ themeClassName?: string;
9
+ };
10
+ export declare function SelectCellEditor({ options, value, onCommit, onCancel, align, themeClassName, }: SelectCellEditorProps): import("react").JSX.Element;
11
+ export default SelectCellEditor;
@@ -0,0 +1,9 @@
1
+ import type { EditorCommitDirection, EditorCommitResult } from '../model/gridTypes';
2
+ type TextCellEditorProps = {
3
+ initialValue: string;
4
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
5
+ onCancel: () => void;
6
+ align?: 'left' | 'center' | 'right';
7
+ };
8
+ export declare function TextCellEditor({ initialValue, onCommit, onCancel, align, }: TextCellEditorProps): import("react").JSX.Element;
9
+ export default TextCellEditor;
@@ -0,0 +1,10 @@
1
+ import type { KeyboardEvent } from 'react';
2
+ import type { EditorCommitDirection, EditorCommitResult } from '../model/gridTypes';
3
+ type CreateEditorKeyDownHandlerArgs = {
4
+ value: unknown;
5
+ onCommit: (value: unknown, direction?: EditorCommitDirection) => EditorCommitResult | void;
6
+ onCancel: () => void;
7
+ onRejected?: (message: string) => void;
8
+ };
9
+ export declare const createEditorKeyDownHandler: ({ value, onCommit, onCancel, onRejected }: CreateEditorKeyDownHandlerArgs) => (event: KeyboardEvent<HTMLInputElement>) => void;
10
+ export {};
@@ -1,7 +1,6 @@
1
1
  import { type Dispatch, type RefObject } from 'react';
2
2
  import { type GridUiAction } from '../model/gridActions';
3
- import type { CellCoord, GridColumn, GridUiState, RowModel } from '../model/gridTypes';
4
- import type { EditorCommitDirection } from '../CellEditorLayer';
3
+ import type { CellCoord, EditorCommitDirection, EditorCommitResult, GridColumn, GridUiState, RowModel } from '../model/gridTypes';
5
4
  type UseGridEditControllerArgs<T extends object> = {
6
5
  uiState: GridUiState;
7
6
  rows: T[];
@@ -17,7 +16,7 @@ type UseGridEditControllerArgs<T extends object> = {
17
16
  export declare const useGridEditController: <T extends object>({ uiState, rows, visibleColumns, rowModel, setEditorInitialValue, onRowsChange, dispatch, getMovedCell, gridRootRef, editorActionGuardRef, }: UseGridEditControllerArgs<T>) => {
18
17
  activateSingleCell: (cell: CellCoord) => void;
19
18
  startEditWithValue: (cell: CellCoord, initialValue: string) => void;
20
- commitEdit: (committedValue: string, direction?: EditorCommitDirection) => void;
19
+ commitEdit: (committedValue: unknown, direction?: EditorCommitDirection) => EditorCommitResult;
21
20
  cancelEdit: () => void;
22
21
  };
23
22
  export default useGridEditController;
@@ -16,8 +16,9 @@ type UseGridKeyboardInteractionsArgs<T> = {
16
16
  onUndo: () => void;
17
17
  onRedo: () => void;
18
18
  onClearSelection: () => void;
19
+ onToggleCheckboxCell: (cell: CellCoord) => void;
19
20
  };
20
- export declare const useGridKeyboardInteractions: <T>({ uiState, rowModel, visibleColumns, readOnly, canEditCell, setEditorInitialValue, dispatch, handleCopy, handleCellDoubleClick, isWholeGridSelected, selectEntireGrid, onUndo, onRedo, onClearSelection, }: UseGridKeyboardInteractionsArgs<T>) => {
21
+ export declare const useGridKeyboardInteractions: <T>({ uiState, rowModel, visibleColumns, readOnly, canEditCell, setEditorInitialValue, dispatch, handleCopy, handleCellDoubleClick, isWholeGridSelected, selectEntireGrid, onUndo, onRedo, onClearSelection, onToggleCheckboxCell, }: UseGridKeyboardInteractionsArgs<T>) => {
21
22
  getMovedCell: (baseCell: CellCoord, deltaRow: number, deltaCol: number) => CellCoord;
22
23
  handleKeyDown: (event: KeyboardEvent<HTMLDivElement>) => Promise<void>;
23
24
  };
@@ -1,16 +1,23 @@
1
- import type { GridRowKey, RowModel, ServerSideDataSource, ServerSideQuery } from '../model/gridTypes';
1
+ import type { GridRowKey, RowModel, ServerSideDataSource, ServerSideLoadErrorParams, ServerSideQuery } from '../model/gridTypes';
2
2
  export type UseServerSideRowModelParams<T> = {
3
3
  dataSource?: ServerSideDataSource<T>;
4
4
  rowKeyGetter: (row: T, index: number) => GridRowKey;
5
5
  query: ServerSideQuery;
6
6
  queryKey: string;
7
7
  refreshToken?: number;
8
+ onLoadError?: (error: unknown, params: ServerSideLoadErrorParams) => void;
8
9
  debounceMs?: number;
9
10
  };
11
+ export type ServerSideLoadErrorState = {
12
+ failedBlockCount: number;
13
+ };
10
14
  export type UseServerSideRowModelResult<T> = {
11
15
  rowModel: RowModel<T>;
12
16
  rowCount: number;
13
17
  isRowLoaded: (viewIndex: number) => boolean;
14
18
  requestRange: (startIndex: number, endIndex: number) => void;
19
+ refresh: () => void;
20
+ loadError: ServerSideLoadErrorState | null;
21
+ retryFailedBlocks: () => void;
15
22
  };
16
23
  export declare function useServerSideRowModel<T>(params: UseServerSideRowModelParams<T>): UseServerSideRowModelResult<T>;