@jielga/tmdatagrid 2.1.4 → 2.3.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.
Files changed (57) hide show
  1. package/dist/index.d.ts +78 -5
  2. package/dist/index.js +2194 -1992
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +1 -1
  5. package/docs/cell-selection.md +1 -1
  6. package/docs/column-layout.md +13 -8
  7. package/docs/columns.md +59 -3
  8. package/docs/components.md +1 -0
  9. package/docs/export.md +2 -2
  10. package/docs/filtering.md +108 -15
  11. package/docs/getting-started.md +40 -0
  12. package/docs/grouping.md +9 -6
  13. package/docs/loading-and-empty.md +29 -9
  14. package/docs/localization.md +130 -3
  15. package/docs/menu.md +1 -1
  16. package/docs/pagination.md +26 -7
  17. package/docs/persistence.md +101 -2
  18. package/docs/row-details.md +41 -4
  19. package/docs/row-interaction.md +2 -2
  20. package/docs/row-selection.md +6 -7
  21. package/docs/row-styling.md +17 -0
  22. package/docs/scrolling.md +1 -1
  23. package/docs/server-side.md +3 -1
  24. package/docs/testing.md +18 -0
  25. package/docs/use-tm-data-grid.md +75 -36
  26. package/package.json +1 -1
  27. package/skills/appearance/SKILL.md +6 -4
  28. package/skills/appearance/references/labels.md +126 -0
  29. package/skills/cell-selection/SKILL.md +2 -2
  30. package/skills/columns/SKILL.md +44 -43
  31. package/skills/columns/references/columns-api.md +16 -1
  32. package/skills/data/SKILL.md +77 -50
  33. package/skills/editing/SKILL.md +1 -1
  34. package/skills/filtering/SKILL.md +94 -60
  35. package/skills/filtering/references/custom-filters.md +118 -0
  36. package/skills/getting-started/SKILL.md +37 -2
  37. package/skills/grouping/SKILL.md +16 -9
  38. package/skills/migrating-to-2/SKILL.md +1 -1
  39. package/skills/options/SKILL.md +206 -55
  40. package/skills/rows/SKILL.md +70 -39
  41. package/skills/server-side/SKILL.md +1 -1
  42. package/skills/testing/SKILL.md +12 -1
  43. package/skills/testing-components/SKILL.md +1 -1
  44. package/skills/testing-editing/SKILL.md +1 -1
  45. package/src/components/TMDataGridFilterPills.module.css +9 -7
  46. package/src/components/TMDataGridTable.module.css +15 -6
  47. package/src/components/TMDataGridTable.tsx +253 -41
  48. package/src/components/sticky.module.css +27 -7
  49. package/src/core/autosize.ts +19 -7
  50. package/src/core/cellRange.ts +11 -2
  51. package/src/core/colSpan.ts +78 -0
  52. package/src/core/columnUtils.ts +7 -0
  53. package/src/core/filterControls.ts +8 -0
  54. package/src/core/filterOperators.ts +29 -1
  55. package/src/core/persistence.ts +322 -26
  56. package/src/index.ts +5 -0
  57. package/src/useTMDataGrid.tsx +133 -14
@@ -2,19 +2,23 @@
2
2
  name: options
3
3
  description: >
4
4
  useTMDataGrid options, return value and state persistence. Covers passthrough
5
- of TanStack TableOptions, the default initialState (pagination, columnPinning,
6
- globalFilterFn), the meta object (loading, noResultsLabel, rowHeight,
7
- totalRowCount), the grid's own persist, enableColumnOrdering,
8
- enablePagination, selectionMode and showSelectedBackground options, the
9
- persist option with dataKey/settingsKey slice selection and storageMode, the
10
- returned api (table, ui, edit, features, labels, resetSettings, scrollToRow),
11
- ui panel and column drag actions, and reading grid state with useSelector.
12
- Load when configuring the hook, persisting column layout or filters, or
13
- reacting to grid state from a parent.
5
+ of TanStack TableOptions, every default the grid changes (globalFilterFn,
6
+ groupedColumnMode, autoResetExpanded, autoResetPageIndex,
7
+ defaultColumn.minSize), the default initialState, the meta object (loading,
8
+ noResultsLabel, rowHeight, totalRowCount), the grid's own options (persist,
9
+ labels, editing, filters, exportOptions, enableColumnOrdering,
10
+ enablePagination, resetPageOnQueryChange, enableRowNumbers, selectionMode,
11
+ showSelectedBackground, cellSelection, renderDetails, overscan,
12
+ quickSearchMode), persist with dataKey/settingsKey slices, storageMode and
13
+ urlKey deep links, the returned api (table, ui, edit, features, labels,
14
+ resetSettings, scrollToRow), ui actions, and reading grid state with
15
+ useSelector over table.store, since table.getState() does not exist in v9.
16
+ Load when configuring the hook, looking up an option's default, persisting
17
+ layout or filters, or reacting to grid state from a parent.
14
18
  metadata:
15
19
  type: core
16
20
  library: '@jielga/tmdatagrid'
17
- library_version: '2.1.4'
21
+ library_version: '2.3.0'
18
22
  sources:
19
23
  - 'Jielga/react:packages/tmdatagrid/docs/use-tm-data-grid.md'
20
24
  - 'Jielga/react:packages/tmdatagrid/docs/persistence.md'
@@ -28,7 +32,8 @@ Creates the table instance and the state used by the grid interface.
28
32
 
29
33
  ```tsx
30
34
  const grid = useTMDataGrid<TData>(options);
31
- // { table, ui, edit, features, labels, resetSettings, scrollToRow }
35
+ // { table, ui, edit, features, labels, filters, exportOptions, renderDetails,
36
+ // renderDetailsEstHeight, overscan, resetSettings, scrollToRow }
32
37
  ```
33
38
 
34
39
  The signature is
@@ -42,43 +47,77 @@ All TanStack `TableOptions` are supported and passed through unchanged, includin
42
47
  flags and `rowCount`. The `features` option is supplied internally and cannot be
43
48
  overridden.
44
49
 
45
- `persist`, `enableColumnOrdering`, `enablePagination`, `selectionMode` and
46
- `showSelectedBackground` are the grid's own options and are consumed by the hook
47
- rather than forwarded to TanStack.
50
+ The grid defines these options itself: `persist`, `labels`, `editing`,
51
+ `filters`, `exportOptions`, `enableColumnOrdering`, `enablePagination`,
52
+ `resetPageOnQueryChange`, `enableRowNumbers`, `selectionMode`,
53
+ `showSelectedBackground`, `defaultHighlightedRowId`, `onHighlightedRowChange`,
54
+ `cellSelection`, `onFocusedCellChange`, `renderDetails`,
55
+ `renderDetailsEstHeight`, `detailsColumnPosition`, `overscan`,
56
+ `quickSearchMode` and `enableMatchHighlighting`. The table below marks them
57
+ "Defined by the grid"; every other row is a TanStack option, listed where the
58
+ grid changes its default or where it decides something visible. A TanStack
59
+ default the grid changes reads, for example, `"remove"` (grid default;
60
+ TanStack: `"reorder"`). A TanStack option that is not listed passes through
61
+ with TanStack's own default.
48
62
 
49
63
  | Option | Type | Default | Description |
50
64
  | --- | --- | --- | --- |
51
- | `data` | `TData[]` | – | Row data. Keep the reference stable with `useMemo`. |
52
- | `columns` | `ColumnDef[]` | – | Created with `createTMDataGridColumnHelper`. |
53
- | `getRowId` | `(row, index) => string` | Row index | Used by row selection and virtualization. Required once `editing` is set. |
65
+ | `data` | `TData[]` | – | Row data. When the reference changes the table reprocesses the row models, so keep it stable with `useMemo`; a fresh `[]` for the empty case belongs in a module-scope constant. |
66
+ | `columns` | `ColumnDef[]` | – | Created with `createTMDataGridColumnHelper`. Define them outside the component. |
67
+ | `getRowId` | `(row, index, parent?) => string` | Row index | The id that row selection, expansion, row pinning and editing are keyed by. The index points at another record after a sort or a refetch, so return the entity id. Required once `editing` is set. |
68
+ | `defaultColumn` | `Partial<ColumnDef>` | `{ minSize: 80 }` (grid default; TanStack: `{}`) | Defaults merged under every column. The grid sets `filterFn: "tmDataGrid"` and clears `aggregationFn` and `aggregatedCell` on every column, so those three keys have no effect here; a value on the column wins. |
54
69
  | `enableRowSelection` | `boolean \| (row) => boolean` | `true` | `false` removes row selection and its checkbox column. |
55
- | `selectionMode` | `"checkbox" \| "row" \| "checkboxAndHighlight" \| "highlight"` | `"checkbox"` | What selecting looks like and what a row click does. Defined by the grid - see the `rows` skill. |
56
- | `showSelectedBackground` | `boolean` | Follows the mode | Background tint on selected rows: off for `"checkbox"`, on for `"row"`. Colour is `--dg-row-selected-bg`. Defined by the grid. |
57
- | `enableSorting` | `boolean` | `true` | Enables sorting for the table. |
58
- | `enableColumnFilters` | `boolean` | `true` | Enables filtering for the table. |
59
- | `enableHiding` | `boolean` | `true` | Enables column visibility for the table. |
70
+ | `enableMultiRowSelection` | `boolean \| (row) => boolean` | `true` | `false` removes the select-all checkbox and limits the selection to one row. |
71
+ | `selectionMode` | `"checkbox" \| "row" \| "checkboxAndHighlight" \| "highlight"` | `"checkbox"` | What selecting looks like and what a bare row click does. Defined by the grid, see the `rows` skill. |
72
+ | `showSelectedBackground` | `boolean` | On for `"row"`, off for `"checkbox"` | Determines whether selected rows take a background tint, coloured by `--dg-row-selected-bg`. Defined by the grid, see the `rows` skill. |
73
+ | `defaultHighlightedRowId` | `string \| null` | – | Row highlighted on mount, under a mode with a highlight. Read once, like `initialState`. Defined by the grid. |
74
+ | `onHighlightedRowChange` | `(rowId: string \| null) => void` | – | Follows the highlighted row, from clicks and from `ui.actions.setHighlightedRow`. Defined by the grid. |
75
+ | `enableSorting` | `boolean` | `true` | `false` removes the sort indicator, the sort menu items and click-to-sort. Also a column option. See the `columns` skill. |
76
+ | `enableMultiSort` | `boolean` | `true` for accessor columns | Shift+click on a header adds a sort instead of replacing it. Also a column option. See the `columns` skill. |
77
+ | `maxMultiSortColCount` | `number` | `Number.MAX_SAFE_INTEGER` | How many sorts are kept; past it the oldest is dropped. |
78
+ | `enableColumnFilters` | `boolean` | `true` | `false` removes the filter menu item, the `FilterButton`, the header filter row and the panel's column list, and disables the panel's "Add filter" button. Filters already in state still apply. See the `filtering` skill. |
79
+ | `enableGlobalFilter` | `boolean` | `true` | `false` removes `Search`. Also a column option, removing one column from the search. See the `filtering` skill. |
80
+ | `globalFilterFn` | filter fn \| name | `"tmDataGridFuzzy"`; `"includesString"` under `quickSearchMode: "contains"` (grid default; TanStack: `"auto"`) | How the quick search matches. An explicit value also switches the fuzzy rank ordering off. |
81
+ | `enableHiding` | `boolean` | `true` | `false` removes the Hide column item, the Manage columns submenu and the column chooser. Also a column option. |
60
82
  | `enableColumnPinning` | `boolean` | `true` | Enables pinning for the table. `false` removes the pin menu items; the generated lanes stay pinned. |
61
- | `enableColumnResizing` | `boolean` | `true` | Enables resizing for the table. |
62
- | `enableColumnOrdering` | `boolean` | `true` | Enables header dragging and the move menu items. Defined by the grid. |
63
- | `enablePagination` | `boolean` | `false` | Enables client-side paging and the `Footer` pager. Implied by `manualPagination`. Defined by the grid. |
64
- | `cellSelection` | `"none" \| "single" \| "range"` | `"none"` | Cell cursor; `"range"` adds a drag-selectable rectangle, Ctrl+C and CSV export. Defined by the grid. |
65
- | `onFocusedCellChange` | `(cell \| null) => void` | – | Fires whenever the focused cell moves. |
66
- | `overscan` | `number` | `6` | Rows the virtualizer keeps mounted above and below the viewport. Defined by the grid. |
67
- | `columnResizeMode` | `"onChange" \| "onEnd"` | `"onChange"` | Resize update strategy. |
68
- | `initialState` | `Partial<TableState>` | See below | The state the grid starts from, read once on mount. Merged over the grid defaults. |
69
- | `state` | `Partial<TableState>` | – | Controlled state. Each slice requires its `onXChange` - see below. |
70
- | `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration, see below. |
71
- | `persist` | `TMDataGridPersistence` | – | State persistence, see below. |
72
- | `editing` | `TMDataGridEditingOptions` | off | Turns editing on. Two axes: `mode` (`"cell" \| "cellConfirm" \| "row"`) picks what counts as a commit, `draft` picks whether it parks in the draft store; the object also holds `onCommit`, `onSaveDrafts`, `rowValidators`, `isRowEditable`, `newRowDefaults`, `newRowsSticky` (keeps committed new rows in the sticky entry block instead of sorting them in with the body), `onRowAdd` and `onRowDelete` - see the `editing` skill. |
73
- | `labels` | `TMDataGridLabelsOverride` | English | Overrides for the grid's strings and `aria-label`s. |
83
+ | `enableColumnResizing` | `boolean` | `true` | With `false` the divider is displayed but cannot be dragged, and double-click autosize and the Autosize menu item are removed. Also a column option, as `enableResizing`. |
84
+ | `enableColumnOrdering` | `boolean` | `true` | Enables header dragging and the move menu items. Defined by the grid, see the `columns` skill. |
85
+ | `enableGrouping` | `boolean` | `true`; `false` under `manualPagination` (grid default; TanStack: `true`) | `false` removes the Group by and Ungroup menu items and the tree lane. Also a column option. See the `grouping` skill. |
86
+ | `groupedColumnMode` | `"remove" \| "reorder" \| false` | `"remove"` (grid default; TanStack: `"reorder"`) | What happens to a column the rows are grouped by: `"remove"` takes it out of the grid, `"reorder"` moves it to the front, `false` leaves it in place. See the `grouping` skill. |
87
+ | `enablePagination` | `boolean` | `false` | Enables client-side paging and the `Footer` pager. Implied by `manualPagination`. Defined by the grid, see the `data` skill. |
88
+ | `resetPageOnQueryChange` | `boolean` | `true` | Returns to the first page when a column filter, the quick search, the sorting or the grouping changes through the table. A `data` change does not count. Defined by the grid, see the `data` skill. |
89
+ | `autoResetPageIndex` | `boolean` | `false` (grid default; TanStack: `!manualPagination`) | TanStack's return to the first page on every `data` change. Off, since `resetPageOnQueryChange` covers the query. |
90
+ | `autoResetExpanded` | `boolean` | `false` (grid default; TanStack: `!manualExpanding`) | TanStack's collapse of every expanded row on a `data` change. Off, so open details panels survive a refetch. |
91
+ | `manualFiltering`, `manualSorting`, `manualGrouping`, `manualPagination` | `boolean` | `false` | The server does that step and the grid shows `data` as given. `manualPagination` also implies `enablePagination`. See the `server-side` skill. |
92
+ | `rowCount` | `number` | – | The total row count under `manualPagination`, read by the pager. |
93
+ | `enableRowNumbers` | `boolean` | `false` | The row-number gutter, outermost left. Defined by the grid, see the `rows` skill. |
94
+ | `enableRowPinning` | `boolean \| (row) => boolean` | `false` (grid default; TanStack: `true`) | Rows can be pinned to sticky edge blocks with `row.pin()`. See the `rows` skill. |
95
+ | `quickSearchMode` | `"fuzzy" \| "contains"` | `"fuzzy"` | How `Search` matches. Fuzzy forgives typos and orders unsorted results by match quality; `"contains"` is plain substring matching. Defined by the grid, see the `filtering` skill. |
96
+ | `enableMatchHighlighting` | `boolean` | `false` | Cells mark the matched text while a contains-family filter or the quick search is active. Defined by the grid, see the `filtering` skill. |
97
+ | `renderDetails` | `({ row, table }) => ReactNode` | – | Panel rendered under an expanded row, spanning every column. Setting it turns row details on and adds the pinned chevron lane. Defined by the grid, see the `rows` skill. |
98
+ | `renderDetailsEstHeight` | `number` | `160` | What the virtualizer assumes for a panel it has not measured yet. Panels are measured once mounted, so an approximation is enough. Defined by the grid. |
99
+ | `detailsColumnPosition` | `"left" \| "right"` | `"left"` | Which edge the details lane is pinned to. `"right"` places it after every column, inside the edit lane. Defined by the grid, see the `rows` skill. |
100
+ | `cellSelection` | `"none" \| "single" \| "range"` | `"none"` | Cell cursor and, under `"range"`, a selectable rectangle with Ctrl+C and CSV export. Defined by the grid, see the `cell-selection` skill. |
101
+ | `onFocusedCellChange` | `(cell: TMDataGridCellPosition \| null) => void` | – | Called whenever the focused cell moves, by key, click or `setFocusedCell`. Defined by the grid. |
102
+ | `overscan` | `number` | `6` | Rows the virtualizer keeps mounted above and below the viewport. Raise it if fast scrolling flashes blank rows, lower it when rows are expensive to render. Defined by the grid. |
103
+ | `columnResizeMode` | `"onChange" \| "onEnd"` | `"onEnd"` | When a resize drag writes `columnSizing`. The grid paints a running drag itself either way; `"onChange"` also publishes a width on every pointer move, which re-renders the grid with each of them. |
104
+ | `initialState` | `Partial<TableState>` | – | Starting state, read once on mount. Merged over the grid defaults below. |
105
+ | `state` | `Partial<TableState>` | – | Controlled state. Each slice requires its `onXChange`. See below. |
106
+ | `atoms` | `Partial<Record<slice, Atom>>` | – | External atoms owning state slices. No callback required. Takes precedence over `state`. |
107
+ | `meta` | `TMDataGridTableMeta` | `{}` | Grid configuration. See below. |
108
+ | `filters` | `TMDataGridFiltersOptions` | `{ surface: "popup" }` | Where the filter controls go - a popup, a sidebar, the column headers, or nowhere. Defined by the grid, see the `filtering` skill. |
109
+ | `exportOptions` | `TMDataGridExportOptions` | `csvExcelFormat()`, `"export"`, headers on, `"visible"` columns | The export's `format`, `fileName`, `includeHeaders` and `columns`. Defined by the grid, see the `data` skill. |
110
+ | `editing` | `TMDataGridEditingOptions` | off | Turns editing on. `mode` picks what counts as a commit and `draft` whether edits collect in the draft store. Requires `getRowId`. Defined by the grid, see the `editing` skill. |
111
+ | `persist` | `TMDataGridPersistence` | – | State persistence. Defined by the grid, see below. |
112
+ | `labels` | `TMDataGridLabelsOverride` | English | Overrides for the grid's strings and `aria-label`s. Defined by the grid, see the `appearance` skill. |
74
113
 
75
114
  ### Default initial state
76
115
 
77
116
  | Slice | Default |
78
117
  | --- | --- |
79
- | `pagination` | `{ pageIndex: 0, pageSize: 25 }` - inert until pagination is enabled |
80
- | `columnPinning.start` | The checkbox column, followed by any columns you provide |
81
- | `globalFilterFn` | `"includesString"` |
118
+ | `pagination` | `{ pageIndex: 0, pageSize: 25 }`. Inert until pagination is enabled |
119
+ | `columnPinning.start` | The row number, checkbox, tree and details columns, followed by any columns you provide. The details column moves to `end` under `detailsColumnPosition: "right"` |
120
+ | `columnPinning.end` | Any columns you provide, then the details column under `detailsColumnPosition: "right"`, then the edit column |
82
121
 
83
122
  ### Controlled state
84
123
 
@@ -113,22 +152,25 @@ const grid = useTMDataGrid({
113
152
 
114
153
  | Field | Type | Default | Description |
115
154
  | --- | --- | --- | --- |
116
- | `loading` | `boolean` | – | Displays a loader instead of the empty state. |
117
- | `noResultsLabel` | `string` | `"No rows match your filters"` | Empty state text. |
155
+ | `loading` | `boolean` | `false` | While `true` and the view is empty, a loader replaces the empty state; `renderEmptyState` is not called. Rows on screen stay. See the `data` skill. |
156
+ | `noResultsLabel` | `string` | `labels.noResults` | The text shown when a filter or the search leaves no rows. |
118
157
  | `rowHeight` | `number` | From `size` | Row height in px. Overrides the size scale. |
119
- | `totalRowCount` | `number` | – | Unfiltered total used by `SummaryCount`. Required for server-side data. |
158
+ | `totalRowCount` | `number` | – | Unfiltered total used by `SummaryCount`. Without it a `manualFiltering` or `manualPagination` grid shows the shown count alone. |
120
159
 
121
160
  ## persist
122
161
 
123
162
  Restores table state on mount and writes it back on every change. State is split
124
- across two keys because the groups have different lifetimes: column configuration
125
- stays valid indefinitely, while filters and pagination go stale as the data
126
- changes. Separate keys let one group be cleared without touching the other.
163
+ across three keys because the groups have different lifetimes: column configuration
164
+ stays valid indefinitely, filters and pagination go stale as the data
165
+ changes, and a deep link has to travel with the page URL. Separate keys let one
166
+ group be cleared without touching the others.
127
167
 
128
168
  | Field | Type | Default | Description |
129
169
  | --- | --- | --- | --- |
130
170
  | `dataKey` | `TMDataGridPersistKey<TMDataGridDataSlice>`: `string \| [string, DataSlice[]]` | – | Storage key for the data group. |
131
171
  | `settingsKey` | `TMDataGridPersistKey<TMDataGridSettingsSlice>`: `string \| [string, SettingsSlice[]]` | – | Storage key for the settings group. |
172
+ | `urlKey` | `TMDataGridPersistKey<TMDataGridDataSlice>`: `string \| [string, DataSlice[]]` | – | Search param for data slices kept in the URL. See [Deep links](#deep-links). |
173
+ | `urlStorage` | `TMDataGridUrlStorage`: `{ get(name): string \| null; set(name, value: string \| null): void }` | `location.search` and `history.replaceState` | Reads and writes the URL param. |
132
174
  | `storageMode` | `TMDataGridStorageMode`: `"localStorage" \| "sessionStorage"` | `"localStorage"` | Storage area. `"sessionStorage"` is per tab. |
133
175
  | `serialize` | `(value) => string` | `JSON.stringify` | Serializes before storing. |
134
176
  | `deserialize` | `(value: string) => unknown` | `JSON.parse` | Parses a stored payload. |
@@ -164,6 +206,7 @@ const persist = {
164
206
  | Group | Available slices |
165
207
  | --- | --- |
166
208
  | `dataKey` | `columnFilters`, `globalFilter`, `sorting`, `pagination`, `expanded` |
209
+ | `urlKey` | The data slices. A slice listed here is taken out of `dataKey`. |
167
210
  | `settingsKey` | `columnVisibility`, `columnSizing`, `columnOrder`, `columnPinning`, `grouping` |
168
211
 
169
212
  `grouping` is a settings slice because it names columns rather than values, so it
@@ -188,38 +231,131 @@ dropped.
188
231
 
189
232
  Source: `packages/tmdatagrid/docs/persistence.md` (Behaviour).
190
233
 
234
+ ### Deep links
235
+
236
+ `urlKey` keeps data slices in one URL search param, so a link to the page opens
237
+ the grid on the same filters. The string is the param name; a bare key takes
238
+ every data slice, a tuple a subset, as with `dataKey`.
239
+
240
+ ```tsx
241
+ const persist = {
242
+ settingsKey: "employees.settings",
243
+ // Sorting is remembered per browser; the filters travel in the link.
244
+ dataKey: ["employees.data", ["sorting"]],
245
+ urlKey: ["employees", ["columnFilters", "globalFilter"]],
246
+ } satisfies TMDataGridPersistence;
247
+ ```
248
+
249
+ - A slice lives in one group. A slice listed under `urlKey` is taken out of
250
+ `dataKey`: a bare `dataKey` hands it over silently, while a `dataKey` tuple
251
+ that names the slice explicitly is reported with `console.error` on mount.
252
+ - The param carries only the slices whose value differs from the initial state
253
+ (`initialState` merged over the grid defaults), and only the column filters
254
+ that are active. When nothing differs, the param is removed. A link without the param, or without a slice, opens on the
255
+ initial state; a URL slice never falls back to storage.
256
+ - Writes use `history.replaceState`, never `pushState`. Only the grid's own
257
+ param changes; the path, the hash, other params and `history.state` are kept.
258
+ Two grids on one page use two keys.
259
+ - The URL payload has no version stamp, so a bookmarked link survives a
260
+ release. The shape guards and the realignment against existing columns apply
261
+ as for storage, and `serialize` / `deserialize` apply to the URL payload too.
262
+ - `meta.filter.url: false` on a column keeps its filter out of the param, on
263
+ read and on write. The column's filter is then not persisted anywhere, and a
264
+ filter on it in `initialState` still applies when a link is opened. The
265
+ flag has no effect unless `urlKey` carries `columnFilters`.
266
+ - `urlStorage` replaces the default read and write of the param, for a router
267
+ that owns the URL. `get` returns the param's string or `null`; `set` receives
268
+ the string, or `null` to remove the param. `set` is only called when the
269
+ value changed. Keep `persist` referentially stable here too: react-router
270
+ rebuilds `setSearchParams` on every URL change, so read it through a ref.
271
+
272
+ ```tsx
273
+ // react-router: let the router do the replace.
274
+ const [, setSearchParams] = useSearchParams();
275
+ const setSearchParamsRef = useRef(setSearchParams);
276
+ setSearchParamsRef.current = setSearchParams;
277
+
278
+ const persist = useMemo(
279
+ () =>
280
+ ({
281
+ urlKey: ["employees", ["columnFilters"]],
282
+ urlStorage: {
283
+ get: (name) => new URLSearchParams(window.location.search).get(name),
284
+ set: (name, value) =>
285
+ setSearchParamsRef.current(
286
+ (prev) => {
287
+ if (value === null) prev.delete(name);
288
+ else prev.set(name, value);
289
+ return prev;
290
+ },
291
+ { replace: true },
292
+ ),
293
+ },
294
+ }) satisfies TMDataGridPersistence,
295
+ [],
296
+ );
297
+ ```
298
+
299
+ Source: `packages/tmdatagrid/docs/persistence.md` (Deep links).
300
+
191
301
  ## Return value
192
302
 
193
303
  | Field | Type | Description |
194
304
  | --- | --- | --- |
195
305
  | `table` | `Table<TMDataGridFeatures, TData>` | The TanStack table instance. |
196
306
  | `ui` | `TMDataGridUiStore`: `Store<TMDataGridUiState, TMDataGridUiActions>` | State of the filter and column panels. |
307
+ | `filters` | `TMDataGridFiltersSettings` | The `filters` option with its defaults filled in. |
308
+ | `exportOptions` | `TMDataGridExportSettings` | The `exportOptions` option with its defaults filled in. |
197
309
  | `edit` | `TMDataGridEditApi` | The edit engine, inert until `editing` is set. See the `editing` skill. |
198
310
  | `features` | `TMDataGridFeatureFlags` | Table-level feature switches, re-read on each render. |
199
311
  | `labels` | `TMDataGridLabels` | The resolved label set, overrides merged over English. |
312
+ | `renderDetails` | `TMDataGridDetailsRenderer<TData> \| undefined` | The detail renderer, passed through for `TMDataGrid.Table` to call. |
313
+ | `renderDetailsEstHeight` | `number` | The estimate, resolved to its default when the option was not set. |
314
+ | `overscan` | `number` | The overscan, resolved to its default when the option was not set. |
200
315
  | `resetSettings` | `() => void` | Puts the settings state back to a clean first visit. |
201
316
  | `scrollToRow` | `({ rowId, align? }) => boolean` | Scrolls a row into view through the virtualizer. `false` when the row is not in the current view. |
202
317
 
203
- Both stores are subscribable, which is how a parent reacts to grid state without
204
- owning it:
318
+ `table` is `TMDataGridTable<TData>`, TanStack's core `Table` type. It has no
319
+ `getState()` (that method does not exist in TanStack Table v9) and `table.state`
320
+ is not part of the type; state lives in `table.store`.
321
+
322
+ ### Reading state
323
+
324
+ `table.store` and `ui` are TanStack stores. Read a slice with `useSelector`
325
+ from `@tanstack/react-store`, which subscribes the component to what the
326
+ selector returns:
205
327
 
206
328
  ```tsx
207
329
  import { useSelector } from "@tanstack/react-store";
208
330
 
209
331
  const selectedCount = useSelector(
210
332
  grid.table.store,
211
- (state) => Object.keys(state.rowSelection).length,
333
+ () => grid.table.getSelectedRowModel().rows.length,
212
334
  );
213
-
335
+ const isGrouped = useSelector(grid.table.store, (state) =>
336
+ state.grouping.includes("department"),
337
+ );
338
+ const pageIndex = useSelector(grid.table.store, (state) => state.pagination.pageIndex);
214
339
  const filterPanelOpen = useSelector(grid.ui, (state) => state.filterPanelOpen);
215
340
  ```
216
341
 
342
+ `grid.table.store.state` is a plain read and does not subscribe the component.
343
+ A bare read is stale in any component that does not call `useTMDataGrid`
344
+ itself, and under the React Compiler also in the one that does; `useSelector`
345
+ subscribes either way. A table or column getter such as
346
+ `table.getSelectedRowModel()` or `column.getIsGrouped()` is a plain read as
347
+ well; call it inside a selector, as above. `getTMDataGridPaginationApi(table)`
348
+ is a snapshot too; wrap it in `useSelector` or use the `state` the Footer's
349
+ `renderPagination` slot receives.
350
+
217
351
  ### ui actions
218
352
 
219
353
  | Action | Signature |
220
354
  | --- | --- |
221
355
  | `openFilterPanel` | `(columnId?: string \| null) => void` |
222
356
  | `closeFilterPanel` | `() => void` |
357
+ | `openExportPicker` | `(request: TMDataGridExportPickerRequest) => void` |
358
+ | `closeExportPicker` | `() => void` |
223
359
  | `focusPanelFilter` | `(columnId: string \| null) => void` |
224
360
  | `focusHeaderFilter` | `(columnId: string \| null) => void` |
225
361
  | `startColumnDrag` | `(columnId: string) => void` |
@@ -232,29 +368,41 @@ const filterPanelOpen = useSelector(grid.ui, (state) => state.filterPanelOpen);
232
368
  The last two move the cell cursor and the selected rectangle under
233
369
  `cellSelection`; DOM focus follows `focusedCell` and scrolls its row into view.
234
370
 
235
- `startColumnDrag` / `endColumnDrag` are called by the header cells while a column is being dragged, and
236
- `ui.draggedColumnId` holds the column being moved - browsers keep `dataTransfer`
237
- unreadable until the drop.
371
+ `startColumnDrag` / `endColumnDrag` are called by the header cells while a
372
+ column is being dragged, and `ui.state.draggedColumnId` holds the column being
373
+ moved - browsers keep `dataTransfer` unreadable until the drop.
238
374
 
239
375
  `openColumnFilter(grid, columnId)` combines the two steps the column menu uses:
240
- add an empty filter row for the column if none exists, then open the panel.
376
+ add an empty filter row for the column if none exists, then open the panel on
377
+ it, or focus the column's header control under `filters.inHeader`.
241
378
 
242
379
  ## Common mistakes
243
380
 
244
381
  ### Reading store state during render
245
382
 
246
- `grid.table.store.state` is a plain read - it does not subscribe the component,
247
- so the value is correct on first render and then never updates.
383
+ `grid.table.store.state` is a plain read - it does not subscribe the component.
384
+ A bare read is stale in any component that does not call `useTMDataGrid`
385
+ itself, and under the React Compiler also in the one that does; `useSelector`
386
+ subscribes either way. The same holds for every table and column getter called
387
+ outside a selector.
248
388
 
249
389
  ```tsx
250
390
  // Wrong - never re-renders when the selection changes.
251
391
  const count = Object.keys(grid.table.store.state.rowSelection).length;
392
+ // Wrong - a getter outside a selector is a snapshot.
393
+ const grouped = grid.table.getColumn("department")!.getIsGrouped();
394
+ // Wrong - `getState()` does not exist in TanStack Table v9.
395
+ const sorting = grid.table.getState().sorting;
252
396
 
253
397
  // Right.
254
398
  const count = useSelector(
255
399
  grid.table.store,
256
400
  (state) => Object.keys(state.rowSelection).length,
257
401
  );
402
+ const grouped = useSelector(grid.table.store, (state) =>
403
+ state.grouping.includes("department"),
404
+ );
405
+ const sorting = useSelector(grid.table.store, (state) => state.sorting);
258
406
  ```
259
407
 
260
408
  ### persist object built during render
@@ -273,4 +421,7 @@ Include a tenant or user identifier in the key.
273
421
 
274
422
  A new array identity on each render re-runs the row model. `data={rows ?? []}`
275
423
  allocates a fresh `[]` every render when `rows` is undefined - hoist the empty
276
- array to a module-scope constant or `useMemo` the expression.
424
+ array to a module-scope constant or `useMemo` the expression. Under `editing`
425
+ the cost is higher: every new `data` identity drops the open drafts of the rows
426
+ it no longer contains, so a transient `[]` while refetching discards every
427
+ draft.
@@ -8,16 +8,16 @@ description: >
8
8
  reading a selection through the table store, onRowClick / onCellClick /
9
9
  onCellDoubleClick / onCellContextMenu, the renderRowContextMenu slot with its
10
10
  internalItems handback, renderColumnMenuItems and rowContextMenuProps, per-row
11
- styling with rowStyle, rowClassName, striped and the --row-bg rule, the row
12
- details panel through renderDetails, detailsColumnPosition and
13
- DETAILS_COLUMN_ID, row pinning with enableRowPinning and row.pin, and the
14
- row-number gutter through enableRowNumbers. Load when selecting rows, reacting
15
- to a click, opening a detail panel, colouring rows by their data, pinning rows
16
- to an edge, or numbering them.
11
+ styling with rowStyle, rowClassName, striped, highlightOnHover and the
12
+ --row-bg rule, the row details panel through renderDetails,
13
+ detailsColumnPosition and DETAILS_COLUMN_ID, row pinning with enableRowPinning
14
+ and row.pin, and the row-number gutter through enableRowNumbers. Load when
15
+ selecting rows, reacting to a click, opening a detail panel, colouring rows by
16
+ their data, pinning rows to an edge, or numbering them.
17
17
  metadata:
18
18
  type: core
19
19
  library: '@jielga/tmdatagrid'
20
- library_version: '2.1.4'
20
+ library_version: '2.3.0'
21
21
  sources:
22
22
  - 'Jielga/react:packages/tmdatagrid/docs/row-selection.md'
23
23
  - 'Jielga/react:packages/tmdatagrid/docs/row-interaction.md'
@@ -37,8 +37,8 @@ cell cursors and ranges are `cell-selection`.
37
37
  ## Selection
38
38
 
39
39
  `selectionMode` sets what selecting looks like **and** what a bare row click
40
- does. It is one option rather than two because a click can toggle a
41
- multi-selection or move a highlight, but not both.
40
+ does. A bare click either toggles the selection or moves the highlight, never
41
+ both.
42
42
 
43
43
  | Mode | Checkbox column | Row click |
44
44
  | --- | --- | --- |
@@ -51,8 +51,8 @@ multi-selection or move a highlight, but not both.
51
51
  const grid = useTMDataGrid({ data, columns, selectionMode: "row" });
52
52
  ```
53
53
 
54
- The first two write to TanStack's `rowSelection`, so the toolbar count,
55
- `getSelectedRowModel()` and persistence all behave the same either way. Under
54
+ The first two write to TanStack's `rowSelection`, so `getSelectedRowModel()`
55
+ and `onRowSelectionChange` behave the same either way. Under
56
56
  `"row"` the click follows desktop-list conventions: a plain click
57
57
  makes the selection this row, Ctrl/Cmd toggles it and leaves the rest, Shift
58
58
  selects the range from the anchor, Ctrl+Shift adds that range. Rows are
@@ -61,7 +61,7 @@ focusable in this mode, and Space or Enter toggles the focused row.
61
61
  `enableRowSelection: false` removes the checkbox column and row-click selection
62
62
  in any mode.
63
63
 
64
- ### The highlight is not a selection
64
+ ### The highlighted row
65
65
 
66
66
  The highlight is state of its own, not a slice of `rowSelection`. That is what
67
67
  lets `"checkboxAndHighlight"` run both at once: tick rows for a bulk action,
@@ -81,8 +81,9 @@ const grid = useTMDataGrid({
81
81
  ### Reading a selection
82
82
 
83
83
  Read it through the table store rather than by calling the method directly.
84
- `table` keeps one identity across renders, so the React Compiler caches a bare
85
- `getSelectedRowModel()` and the component stops updating.
84
+ A bare read is stale in any component that does not call `useTMDataGrid`
85
+ itself, and under the React Compiler also in the one that does; `useSelector`
86
+ subscribes either way.
86
87
 
87
88
  ```tsx
88
89
  import { useSelector } from "@tanstack/react-store";
@@ -160,9 +161,10 @@ the row with `data-context-menu` while its menu is open, so an action meant for
160
161
  a multi-selection reads `table` and falls back to the clicked row.
161
162
 
162
163
  Under `cellSelection: "range"` the grid has its own items for this menu: copy,
163
- export and include headers. `internalItems` is those items, and **using it takes
164
- over the composition**: place it and the menu is exactly what you returned; omit
165
- it and the grid keeps its own items above a divider and yours below.
164
+ export and include headers. `internalItems` holds those items, and **using it
165
+ takes over the composition**. Place it, and the menu is exactly what you
166
+ returned. Omit it, and the grid renders its items above a divider and yours
167
+ below.
166
168
 
167
169
  ```tsx
168
170
  renderRowContextMenu={({ row, internalItems }) => (
@@ -211,8 +213,15 @@ them and they stop showing. `striped` follows position in the view, so it
211
213
  survives sorting and filtering rather than sticking to records, and pinned rows
212
214
  are not striped.
213
215
 
216
+ The hover tint follows the gestures a row answers: a row takes it under
217
+ `selectionMode: "row"` or a highlight mode, `onRowClick` or
218
+ `renderRowContextMenu`, and a group row never does. A grid with none of those
219
+ shows no hover tint. `highlightOnHover` on the Table overrides that: `true`
220
+ tints every row, `false` none.
221
+
214
222
  Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
215
- `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
223
+ `data-grouped`, `data-depth`, `data-context-menu`, `data-highlight-on-hover`
224
+ and `data-row-id`, so a
216
225
  stylesheet can target any of it without a callback. The boolean attributes are
217
226
  present, with the value `"true"`, only while they apply - `data-grouped` only
218
227
  on group rows - so `[data-grouped]` and `[data-grouped="true"]` are
@@ -232,20 +241,31 @@ const grid = useTMDataGrid({
232
241
  });
233
242
  ```
234
243
 
235
- Each panel is measured, so heights need not be uniform.
236
- `renderDetailsEstHeight` (default `160`) is what the virtualizer assumes for one
237
- it has not measured yet. A generated chevron column, `DETAILS_COLUMN_ID`, is
238
- pinned left after the checkbox and tree lanes, or right, inside the edit lane,
239
- under `detailsColumnPosition: "right"`; it cannot be hidden, moved, resized or
240
- unpinned, and its header opens and closes every panel.
244
+ The panel opens below the row's cells, which stay where they are; there is no
245
+ mode in which a panel replaces the row. Each panel is measured, so heights need
246
+ not be uniform. `renderDetailsEstHeight` (default `160`) is what the virtualizer
247
+ assumes for one it has not measured yet; set it to the typical panel height.
248
+ `DETAILS_COLUMN_ID` is a generated chevron lane. It is pinned left, after the
249
+ checkbox and tree lanes; under `detailsColumnPosition: "right"` it is pinned
250
+ right, inside the edit lane. It cannot be hidden, moved, resized or unpinned.
251
+ Its header opens and closes every panel.
241
252
 
242
253
  Which rows are open is TanStack's own `expanded` state, so anything can open
243
254
  one - `row.toggleExpanded()` from a menu item, `initialState.expanded`,
244
255
  `table.toggleAllRowsExpanded()` - and it persists as a `data` slice. The panel
245
256
  is a cell spanning the row, not a row: `aria-rowcount` still counts records, and
246
- a click inside it does not select the row underneath. Group rows have no panel;
257
+ a click inside it does not select the row underneath. It has no padding: content
258
+ fills the row edge to edge, and content that should line up with the cell text
259
+ pads itself with `padding: var(--dg-padding)`. Group rows have no panel;
247
260
  expanding one opens its children.
248
261
 
262
+ Rows are virtualized. A row that leaves the viewport and the `overscan`
263
+ unmounts, and its panel unmounts with it, so state held inside the panel is
264
+ lost; keep such state outside the grid, keyed by `row.id`. `renderDetails` runs
265
+ on every render of the body, so give a heavy panel a `memo` component and pass
266
+ it `row.original`, not the row. Key a stateful child by `row.id`; that is stable
267
+ only when `getRowId` returns the entity id.
268
+
249
269
  ## Pinning and numbering
250
270
 
251
271
  Two independent lanes at the edges.
@@ -264,7 +284,7 @@ There is no built-in pin gesture and no pin icon. Build one from TanStack's own
264
284
  either as a display column whose cell is a pin button or in the row context
265
285
  menu. Read the pinned state through a subscription
266
286
  (`useSelector(row.table.store, () => row.getIsPinned())`) rather than calling it
267
- in a component body: the `row` identity survives a pin, so the React Compiler
287
+ in a component body. The `row` identity survives a pin, so the React Compiler
268
288
  would cache the call and the icon would never change. Whichever control you
269
289
  build, include unpin in it. Pinned rows leave the scrolling order and render in
270
290
  sticky blocks: top under the header, bottom above the summary row.
@@ -300,13 +320,13 @@ Correct:
300
320
  rowStyle={() => ({ "--row-bg": "pink" })}
301
321
  ```
302
322
 
303
- Source: `packages/tmdatagrid/docs/row-styling.md` (Set `--row-bg`, not `background`).
323
+ Source: `packages/tmdatagrid/docs/row-styling.md` (Set the row background).
304
324
 
305
325
  ### CRITICAL Reading the selection without subscribing
306
326
 
307
- `table` keeps one identity for the life of the grid, so the React Compiler
308
- memoizes a bare `getSelectedRowModel()` against it. The count renders once and
309
- then never changes.
327
+ A bare read is stale in any component that does not call `useTMDataGrid`
328
+ itself, and under the React Compiler also in the one that does; `useSelector`
329
+ subscribes either way. A stale count renders once and then never changes.
310
330
 
311
331
  Wrong:
312
332
 
@@ -327,8 +347,8 @@ Source: `packages/tmdatagrid/docs/row-selection.md` (Acting on a selection).
327
347
  ### HIGH Looking for the highlight in `rowSelection`
328
348
 
329
349
  The highlight is separate state, which is what lets `"checkboxAndHighlight"`
330
- run both at once. It never appears in `rowSelection`, `getSelectedRowModel()` or
331
- the persisted selection slice.
350
+ run both at once. It never appears in `rowSelection` or
351
+ `getSelectedRowModel()`.
332
352
 
333
353
  Wrong:
334
354
 
@@ -343,7 +363,7 @@ Correct:
343
363
  const current = useSelector(grid.ui, (state) => state.highlightedRowId);
344
364
  ```
345
365
 
346
- Source: `packages/tmdatagrid/docs/row-selection.md` (The highlight is not a selection).
366
+ Source: `packages/tmdatagrid/docs/row-selection.md` (The highlighted row).
347
367
 
348
368
  ### HIGH Expecting a click handler to replace the built-in behaviour
349
369
 
@@ -382,18 +402,29 @@ written as though every row reaches it silently skips them.
382
402
 
383
403
  Source: `packages/tmdatagrid/docs/row-interaction.md`, `packages/tmdatagrid/docs/row-pinning.md`.
384
404
 
385
- ### MEDIUM Open panels closing when `data` is replaced
405
+ ### MEDIUM Keeping state inside a details panel
386
406
 
387
- TanStack resets `expanded` when the row structure changes, so a refetch collapses
388
- every open panel.
407
+ A panel unmounts with its row when the row leaves the virtual window, so a form
408
+ or an accordion inside `renderDetails` loses its state on a long scroll and
409
+ comes back reset.
389
410
 
390
- Correct:
411
+ Correct: hold the state outside the grid, keyed by `row.id`, and pass it in.
391
412
 
392
413
  ```tsx
393
- useTMDataGrid({ data, columns, renderDetails, autoResetExpanded: false });
414
+ const [drafts, setDrafts] = useState<Record<string, Draft>>({});
415
+ useTMDataGrid({
416
+ data,
417
+ columns,
418
+ renderDetails: ({ row }) => (
419
+ <DetailsForm draft={drafts[row.id]} onChange={(d) => setDrafts((all) => ({ ...all, [row.id]: d }))} />
420
+ ),
421
+ });
394
422
  ```
395
423
 
396
- Source: `packages/tmdatagrid/docs/row-details.md`.
424
+ `autoResetExpanded` is already `false` by default, so a refetch does not close
425
+ the panels; pass `autoResetExpanded: true` to get TanStack's collapse back.
426
+
427
+ Source: `packages/tmdatagrid/docs/row-details.md` (Panel behaviour).
397
428
 
398
429
  ### MEDIUM Expecting pinned rows to persist
399
430
 
@@ -15,7 +15,7 @@ description: >
15
15
  metadata:
16
16
  type: core
17
17
  library: '@jielga/tmdatagrid'
18
- library_version: '2.1.4'
18
+ library_version: '2.3.0'
19
19
  sources:
20
20
  - 'Jielga/react:packages/tmdatagrid/docs/server-side.md'
21
21
  - 'Jielga/react:packages/tmdatagrid/docs/server-query.md'