@jielga/tmdatagrid 2.0.0-beta.2 → 2.0.0-beta.21

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 (150) hide show
  1. package/README.md +5 -212
  2. package/dist/index.d.ts +1664 -632
  3. package/dist/index.js +5226 -3223
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +1 -1
  6. package/docs/anatomy.md +102 -0
  7. package/docs/cell-selection.md +154 -0
  8. package/docs/column-layout.md +204 -0
  9. package/docs/columns.md +262 -0
  10. package/docs/components.md +304 -0
  11. package/docs/editing.md +603 -0
  12. package/docs/editors.md +250 -0
  13. package/docs/export.md +326 -0
  14. package/docs/filtering.md +358 -0
  15. package/docs/getting-started.md +123 -0
  16. package/docs/grouping.md +165 -0
  17. package/docs/loading-and-empty.md +92 -0
  18. package/docs/localization.md +79 -0
  19. package/docs/menu.md +143 -0
  20. package/docs/pagination.md +144 -0
  21. package/docs/persistence.md +111 -0
  22. package/docs/portfolio-rebalancer.md +94 -0
  23. package/docs/query-builder.md +175 -0
  24. package/docs/quick-search.md +83 -0
  25. package/docs/row-details.md +113 -0
  26. package/docs/row-interaction.md +148 -0
  27. package/docs/row-pinning.md +132 -0
  28. package/docs/row-selection.md +134 -0
  29. package/docs/row-styling.md +133 -0
  30. package/docs/scrolling.md +111 -0
  31. package/docs/server-query.md +246 -0
  32. package/docs/server-side.md +206 -0
  33. package/docs/sorting.md +101 -0
  34. package/docs/styling.md +126 -0
  35. package/docs/summary-row.md +76 -0
  36. package/docs/testing.md +309 -0
  37. package/docs/toolbar.md +161 -0
  38. package/docs/use-tm-data-grid.md +361 -0
  39. package/package.json +21 -45
  40. package/skills/appearance/SKILL.md +70 -17
  41. package/skills/cell-selection/SKILL.md +70 -76
  42. package/skills/columns/SKILL.md +131 -32
  43. package/skills/data/SKILL.md +100 -23
  44. package/skills/editing/SKILL.md +217 -96
  45. package/skills/editing/references/common-mistakes.md +111 -24
  46. package/skills/editing/references/editing-api.md +63 -39
  47. package/skills/editing/references/editors-and-validation.md +77 -19
  48. package/skills/filtering/SKILL.md +148 -40
  49. package/skills/getting-started/SKILL.md +18 -16
  50. package/skills/grouping/SKILL.md +32 -15
  51. package/skills/options/SKILL.md +39 -9
  52. package/skills/rows/SKILL.md +22 -18
  53. package/skills/server-side/SKILL.md +170 -17
  54. package/skills/testing/SKILL.md +10 -7
  55. package/src/{tmdatagrid/TMDataGridContext.ts → TMDataGridContext.ts} +7 -19
  56. package/src/{tmdatagrid/components → components}/TMDataGrid.module.css +7 -1
  57. package/src/{tmdatagrid/components → components}/TMDataGrid.tsx +39 -23
  58. package/src/{tmdatagrid/components → components}/TMDataGridCellEditor.tsx +106 -38
  59. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.module.css +5 -1
  60. package/src/{tmdatagrid/components → components}/TMDataGridColumnsPanel.tsx +47 -55
  61. package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.tsx +4 -4
  62. package/src/components/TMDataGridDraftActions.tsx +307 -0
  63. package/src/{tmdatagrid/components → components}/TMDataGridEditColumn.tsx +58 -50
  64. package/src/{tmdatagrid/components → components}/TMDataGridEntryRows.tsx +150 -115
  65. package/src/components/TMDataGridExportPicker.module.css +77 -0
  66. package/src/components/TMDataGridExportPicker.tsx +234 -0
  67. package/src/components/TMDataGridFilterPanel.module.css +54 -0
  68. package/src/components/TMDataGridFilterPanel.tsx +348 -0
  69. package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.tsx +7 -5
  70. package/src/components/TMDataGridFilterSurface.module.css +54 -0
  71. package/src/components/TMDataGridFilterSurface.tsx +167 -0
  72. package/src/{tmdatagrid/components → components}/TMDataGridFooter.tsx +53 -13
  73. package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.tsx +4 -3
  74. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.module.css +10 -0
  75. package/src/{tmdatagrid/components → components}/TMDataGridHeaderCell.tsx +100 -28
  76. package/src/components/TMDataGridHeaderFilterRow.module.css +51 -0
  77. package/src/components/TMDataGridHeaderFilterRow.tsx +301 -0
  78. package/src/components/TMDataGridMenu.tsx +354 -0
  79. package/src/{tmdatagrid/components → components}/TMDataGridSelectColumn.tsx +12 -7
  80. package/src/{tmdatagrid/components → components}/TMDataGridTable.module.css +90 -67
  81. package/src/{tmdatagrid/components → components}/TMDataGridTable.tsx +678 -156
  82. package/src/components/TMDataGridToolbar.module.css +21 -0
  83. package/src/components/TMDataGridToolbar.tsx +181 -0
  84. package/src/{tmdatagrid/components → components}/editors/TMDataGridBooleanEditor.tsx +3 -3
  85. package/src/{tmdatagrid/components → components}/editors/TMDataGridDateEditor.tsx +3 -3
  86. package/src/{tmdatagrid/components → components}/editors/TMDataGridMultiSelectEditor.tsx +3 -3
  87. package/src/components/editors/TMDataGridNumberEditor.tsx +70 -0
  88. package/src/{tmdatagrid/components → components}/editors/TMDataGridSelectEditor.tsx +3 -3
  89. package/src/{tmdatagrid/components → components}/editors/TMDataGridStringEditor.tsx +3 -3
  90. package/src/{tmdatagrid/components → components}/editors/editorShared.ts +17 -31
  91. package/src/{tmdatagrid/components → components}/filters/DgAutocompleteFilter.tsx +5 -4
  92. package/src/{tmdatagrid/components → components}/filters/DgDateRangeFilter.tsx +22 -5
  93. package/src/{tmdatagrid/components → components}/filters/DgRangeSliderFilter.tsx +5 -1
  94. package/src/{tmdatagrid/components → components}/filters/DgTriStateFilter.tsx +5 -1
  95. package/src/{tmdatagrid/components → components}/filters/TMDataGridFilterValueInput.tsx +44 -28
  96. package/src/components/filters/controlLayout.ts +32 -0
  97. package/src/components/filters/filterControlFor.ts +65 -0
  98. package/src/{tmdatagrid/components → components}/icons.ts +1 -0
  99. package/src/{tmdatagrid/components → components}/sticky.module.css +44 -0
  100. package/src/components/useHideableColumns.ts +52 -0
  101. package/src/{tmdatagrid/core → core}/autosize.ts +5 -2
  102. package/src/{tmdatagrid/core → core}/capabilities.ts +14 -6
  103. package/src/{tmdatagrid/core → core}/columnOptions.ts +46 -0
  104. package/src/{tmdatagrid/core → core}/columnOrdering.ts +33 -14
  105. package/src/{tmdatagrid/core → core}/columnUtils.ts +44 -5
  106. package/src/core/controlledState.ts +179 -0
  107. package/src/core/controlledStateSync.ts +108 -0
  108. package/src/core/deletedRows.ts +34 -0
  109. package/src/core/dom.ts +74 -0
  110. package/src/core/editEngine.ts +2476 -0
  111. package/src/{tmdatagrid/core → core}/editorFocus.ts +8 -4
  112. package/src/core/export.ts +843 -0
  113. package/src/{tmdatagrid/core → core}/filterControls.ts +38 -1
  114. package/src/{tmdatagrid/core → core}/filterOperators.ts +64 -1
  115. package/src/core/filterSurface.ts +99 -0
  116. package/src/{tmdatagrid/core → core}/labels.ts +66 -8
  117. package/src/{tmdatagrid/core → core}/labelsSv.ts +26 -3
  118. package/src/core/pageReset.ts +120 -0
  119. package/src/{tmdatagrid/core → core}/persistence.ts +22 -5
  120. package/src/core/resizePreview.ts +141 -0
  121. package/src/core/summary.ts +59 -0
  122. package/src/core/useSettledTableState.ts +36 -0
  123. package/src/{tmdatagrid/index.ts → index.ts} +75 -12
  124. package/src/{tmdatagrid/useTMDataGrid.tsx → useTMDataGrid.tsx} +734 -135
  125. package/src/useTMDataGridExport.ts +78 -0
  126. package/src/tmdatagrid/components/TMDataGridEditActions.tsx +0 -162
  127. package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +0 -35
  128. package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +0 -354
  129. package/src/tmdatagrid/components/TMDataGridToolbar.module.css +0 -12
  130. package/src/tmdatagrid/components/TMDataGridToolbar.tsx +0 -162
  131. package/src/tmdatagrid/components/editors/TMDataGridNumberEditor.tsx +0 -40
  132. package/src/tmdatagrid/core/cellExport.ts +0 -320
  133. package/src/tmdatagrid/core/editEngine.ts +0 -1006
  134. package/src/tmdatagrid/core/summary.ts +0 -35
  135. /package/src/{tmdatagrid/components → components}/TMDataGridDetailsColumn.module.css +0 -0
  136. /package/src/{tmdatagrid/components → components}/TMDataGridFilterPills.module.css +0 -0
  137. /package/src/{tmdatagrid/components → components}/TMDataGridFooter.module.css +0 -0
  138. /package/src/{tmdatagrid/components → components}/TMDataGridGroupColumn.module.css +0 -0
  139. /package/src/{tmdatagrid/components → components}/TMDataGridRowNumberColumn.tsx +0 -0
  140. /package/src/{tmdatagrid/components → components}/TMDataGridSearch.tsx +0 -0
  141. /package/src/{tmdatagrid/core → core}/cellNavigation.ts +0 -0
  142. /package/src/{tmdatagrid/core → core}/cellRange.ts +0 -0
  143. /package/src/{tmdatagrid/core → core}/draftCellContext.ts +0 -0
  144. /package/src/{tmdatagrid/core → core}/expanding.ts +0 -0
  145. /package/src/{tmdatagrid/core → core}/grouping.ts +0 -0
  146. /package/src/{tmdatagrid/core → core}/matchHighlight.ts +0 -0
  147. /package/src/{tmdatagrid/core → core}/quickSearch.ts +0 -0
  148. /package/src/{tmdatagrid/core → core}/rowPinning.ts +0 -0
  149. /package/src/{tmdatagrid/core → core}/rowSelection.ts +0 -0
  150. /package/src/{tmdatagrid/core → core}/sizes.ts +0 -0
@@ -0,0 +1,175 @@
1
+ # A query builder form
2
+
3
+ A search form holds dates, a title filter and a list of extra conditions. The
4
+ conditions are rows, so the condition builder is a grid, wrapped as a form
5
+ field: rows come in through `value`, and approved changes go back out through
6
+ `onChange`.
7
+
8
+ ```demo
9
+ file: recipes/QueryBuilderForm.tsx
10
+ hint: Add a condition and pick Status - the value editor becomes a select. Delete every condition, or repeat one, and Search says why it will not.
11
+ height: 560
12
+ ```
13
+
14
+ The form holds the array; the grid holds the row being edited. The grid never
15
+ mutates `data`, so the form's state is only ever what the user has approved.
16
+
17
+ ## Wrapping the grid as a field
18
+
19
+ The grid is wrapped once, as an ordinary controlled component:
20
+
21
+ ```tsx
22
+ function ConditionsGrid({ value, onChange }: {
23
+ value: Array<QueryCondition>;
24
+ onChange: (next: Array<QueryCondition>) => void;
25
+ }) {
26
+ const grid = useTMDataGrid({
27
+ data: value,
28
+ columns,
29
+ getRowId: (row) => String(row.id),
30
+ editing: {
31
+ mode: "row",
32
+ rowValidators,
33
+ onCommit: ({ rowId, value: row }) =>
34
+ onChange(value.map((c) => (String(c.id) === rowId ? row : c))),
35
+ onRowAdd: ({ value: row }) =>
36
+ onChange([...value, { ...row, id: Math.min(0, ...value.map((c) => c.id)) - 1 }]),
37
+ onRowDelete: ({ rowId }) =>
38
+ onChange(value.filter((c) => String(c.id) !== rowId)),
39
+ newRowDefaults: () => ({ id: 0, field: "title", operator: "contains", value: "" }),
40
+ },
41
+ });
42
+ return (
43
+ <TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
44
+ <TMDataGrid.Table<QueryCondition> />
45
+ </TMDataGrid>
46
+ );
47
+ }
48
+ ```
49
+
50
+ And handed to the form as a field:
51
+
52
+ ```tsx
53
+ <form.Field
54
+ name="conditions"
55
+ validators={{
56
+ onChange: ({ value }) =>
57
+ value.length === 0 ? "Add at least one condition" : undefined,
58
+ }}
59
+ >
60
+ {(field) => (
61
+ <ConditionsGrid value={field.state.value} onChange={field.handleChange} />
62
+ )}
63
+ </form.Field>
64
+ ```
65
+
66
+ `field.handleChange` is the `onChange`: it writes the array into the form and
67
+ runs the field's validators. `onChange` carries the whole next array rather than
68
+ a diff.
69
+
70
+ - **Map by row id, never by index.** A sort or a delete moves the index; the
71
+ draft is keyed by the id.
72
+ - **`data` identity stays stable.** `field.state.value` changes identity only
73
+ when a row is written, which is what `data` requires.
74
+ See [useTMDataGrid](/docs/use-tm-data-grid).
75
+ - **New rows count down from `-1`.** `Math.min(0, ...ids) - 1`, so an emptied
76
+ grid starts at `-1` rather than `-Infinity` and existing negative ids keep
77
+ descending. The grid's `tempId` never leaves the grid: the id you assign in
78
+ `editing.onRowAdd` is the one the form sees.
79
+
80
+ ## Where validation belongs
81
+
82
+ A rule that can be decided from one row belongs to the grid. A rule that needs
83
+ the other rows, or the collection as a whole, belongs to the form.
84
+
85
+ | Rule | Owner | Written as |
86
+ | --- | --- | --- |
87
+ | "The condition needs a value" | Grid | `editing.rowValidators.onSubmit` |
88
+ | "A date, for a date field" | Grid | `meta.edit.editor` per column |
89
+ | "At least one condition" | Form | the `conditions` field's `onChange` validator |
90
+ | "No two conditions repeat a field and operator" | Form | the same place |
91
+
92
+ A row's form is seeded with that row's values only, so it cannot see the array,
93
+ and the grid's `edit.store` publishes field names but not values, so the form
94
+ cannot see a draft.
95
+
96
+ ## Editors that follow the row
97
+
98
+ In a `[field, operator, value]` builder the value cell means something
99
+ different on every row. `meta.edit.editor` receives the row's live form, so one
100
+ component can switch on the *draft* field rather than the committed one, and
101
+ picking a new field resets its dependents:
102
+
103
+ ```tsx
104
+ const FieldEditor: TMDataGridEditorComponent = ({ field, form, size }) => (
105
+ <Select
106
+ size={size}
107
+ data={[...FIELDS]}
108
+ value={String(field.state.value)}
109
+ onChange={(next) => {
110
+ if (next === null) return;
111
+ field.handleChange(next);
112
+ form.setFieldValue("operator", OPERATORS[next as QueryField][0]);
113
+ form.setFieldValue("value", "");
114
+ }}
115
+ />
116
+ );
117
+ ```
118
+
119
+ The operator and value editors read the sibling with TanStack Store's
120
+ [`useSelector`](https://tanstack.com/store/latest/docs/framework/react/reference):
121
+ `useSelector(form.store, (state) => state.values.field)`, so switching Field
122
+ mid-edit switches them immediately. See
123
+ [Editors and validation](/docs/editors) for the editor's full API.
124
+
125
+ ## Which mode
126
+
127
+ `editing.mode: "row"`. A condition is approved as a unit: the pencil opens the
128
+ whole row, Save commits it, and `editing.onCommit` hands the form one finished
129
+ condition. The form's rules run at that point.
130
+
131
+ **Not `draft: true`.** A draft store holds every commit inside the grid and
132
+ calls nothing until `edit.saveDrafts()`, so the form's array goes stale the
133
+ moment typing starts: "at least one condition" counts rows the user may have
134
+ marked for deletion, a duplicate sitting in a draft passes unseen, and the
135
+ form's submit saves an array missing every pending edit.
136
+
137
+ To use the store anyway, invert where the save happens: `grid.edit.saveDrafts()`
138
+ becomes the only way rows reach the form, and the form's submit is gated on the
139
+ grid holding no draft -
140
+ `useSelector(grid.edit.store, (s) => s.openRowIds.length === 0)`.
141
+
142
+ ## Submitting
143
+
144
+ The form submits only while the grid holds no draft.
145
+
146
+ ```tsx
147
+ const hasOpenDraft = useSelector(grid.edit.store, (s) => s.openRowIds.length > 0);
148
+
149
+ <Button type="submit" disabled={!canSubmit || hasOpenDraft}>
150
+ {hasOpenDraft ? "Finish the open condition" : "Search"}
151
+ </Button>
152
+ ```
153
+
154
+ The alternative is to flush instead of block. `await grid.edit.commitAll()`
155
+ before `form.handleSubmit()` submits every open row through the normal
156
+ `editing.onCommit` path, in every mode; with `draft: true` follow it with
157
+ `grid.edit.saveDrafts()`. Rows that fail their own validation
158
+ stay open, so the submit fails.
159
+
160
+ Two constraints apply to a native `<form>` around a grid:
161
+
162
+ - The grid's buttons cannot submit it. Mantine buttons default to
163
+ `type="button"`, so only your own `type="submit"` button submits.
164
+ - Enter inside a cell editor commits the cell, not the form. The editor
165
+ prevents the default, so implicit form submission never fires.
166
+
167
+ ## Reference
168
+
169
+ The pieces this recipe composes:
170
+
171
+ | Piece | Documented on |
172
+ | --- | --- |
173
+ | `data`, `getRowId`, `editing.mode`, `editing.onCommit`, `editing.onRowAdd`, `editing.onRowDelete`, `editing.newRowDefaults`, `edit.store` | [Editing](/docs/editing) |
174
+ | `editing.rowValidators`, `meta.edit.editor`, the editor contract | [Editors and validation](/docs/editors) |
175
+ | `useForm`, `form.Field`, field validators | TanStack Form's own docs |
@@ -0,0 +1,83 @@
1
+ # Quick search
2
+
3
+ One box over every column. `TMDataGrid.Search` is a debounced input writing
4
+ TanStack's `globalFilter` state. There is no option to turn it on: render the
5
+ component, or do not.
6
+
7
+ ```tsx
8
+ <TMDataGrid.Toolbar>
9
+ <TMDataGrid.Search />
10
+ </TMDataGrid.Toolbar>
11
+ ```
12
+
13
+ ```demo
14
+ file: data/QuickSearch.tsx
15
+ hint: Try “Stckholm”. Fuzzy finds it; contains does not.
16
+ extraSources: data/employeeColumns.tsx
17
+ ```
18
+
19
+ ## Fuzzy by default
20
+
21
+ Typos and skipped characters still match, and while the search is the only thing
22
+ narrowing the grid (no sort, no grouping) the rows are ordered by **match
23
+ quality**, best first.
24
+
25
+ That ordering is derived and never written into `sorting`: no column takes
26
+ `aria-sort`, nothing is written to the persisted slices, and the next sort click
27
+ replaces it.
28
+
29
+ ```tsx
30
+ const grid = useTMDataGrid({ data, columns, quickSearchMode: "contains" });
31
+ ```
32
+
33
+ `"contains"` restores plain substring matching. An explicit `globalFilterFn`
34
+ overrides both, and switches the rank ordering off with it.
35
+
36
+ `fuzzyGlobalFilterFn` is exported for building your own input over the same
37
+ matching.
38
+
39
+ ## Match highlighting
40
+
41
+ Opt in with `enableMatchHighlighting: true` and cells mark the matched part of
42
+ their text, while the quick search is active or a `contains` / `starts with` /
43
+ `ends with` column filter is.
44
+
45
+ ```tsx
46
+ const grid = useTMDataGrid({ data, columns, enableMatchHighlighting: true });
47
+ ```
48
+
49
+ What is marked is the **contiguous, case-insensitive** occurrence of the search
50
+ term, so a fuzzy typo-match with no contiguous occurrence is not highlighted.
51
+ Equality operators highlight nothing.
52
+
53
+ **Default-rendered cells only.** A column with its own `cell` renderer is
54
+ excluded: the grid reproduces the default value-to-string render with the marks
55
+ added, and does not modify a custom renderer's output.
56
+
57
+ The mark colour is `--dg-match-highlight-bg`, a yellow that follows the Mantine
58
+ colour scheme.
59
+
60
+ ## Opting columns out
61
+
62
+ `enableGlobalFilter: false` on a column takes it out of the search; on the table
63
+ it removes the input entirely. The generated lanes are already excluded.
64
+
65
+ A search input of your own can skip the component and call
66
+ `table.setGlobalFilter` directly. The state is TanStack's own, so a
67
+ [`manualFiltering`](/docs/server-side) grid forwards it to the server, and it is
68
+ one of the persisted `data` slices.
69
+
70
+ ## Reference
71
+
72
+ | Name | Kind | Type | Default | What it does |
73
+ | --- | --- | --- | --- | --- |
74
+ | `TMDataGrid.Search` | Component | – | – | The debounced quick-search input. |
75
+ | `placeholder` | Search prop | `string` | `labels.searchPlaceholder` | Input placeholder. |
76
+ | `debounce` | Search prop | `number` | `250` | Pause before the filter applies, in ms. `0` filters per keystroke. |
77
+ | `w` | Search prop | `number \| string` | `220` | Input width. |
78
+ | `quickSearchMode` | Option | `"fuzzy" \| "contains"` | `"fuzzy"` | How the search matches. |
79
+ | `enableMatchHighlighting` | Option | `boolean` | `false` | Mark the matched text in default-rendered cells. |
80
+ | `enableGlobalFilter` | Table option | `boolean` | `true` | Also a column option. `false` removes the input, or one column's participation. |
81
+ | `globalFilterFn` | Table option | filter fn | `fuzzy` | Overrides the matching, and the rank ordering with it. |
82
+ | `fuzzyGlobalFilterFn` | Export | filter fn | – | The default matcher, for a custom input. |
83
+ | `--dg-match-highlight-bg` | CSS variable | colour | Themed yellow | The mark colour. |
@@ -0,0 +1,113 @@
1
+ # Row details
2
+
3
+ A panel that opens underneath a row, spanning every column, holding whatever
4
+ does not fit in the cells - a summary card, an action strip, a nested table.
5
+
6
+ Setting `renderDetails` turns the lane on. There is no separate flag.
7
+
8
+ ```tsx
9
+ const grid = useTMDataGrid({
10
+ data,
11
+ columns,
12
+ renderDetails: ({ row }) => <EmployeeCard employee={row.original} />,
13
+ });
14
+ ```
15
+
16
+ ```demo
17
+ file: rows/DetailsPanel.tsx
18
+ hint: Expand a few rows - the panels differ in height, and each one is measured.
19
+ height: 460
20
+ ```
21
+
22
+ The panel is as tall as whatever it renders. The grid measures each one, so
23
+ they need not be uniform. `renderDetailsEstHeight` (default `160`) is what the
24
+ virtualizer assumes for a panel it has not measured yet, which keeps the
25
+ scrollbar accurate for rows that open off screen.
26
+
27
+ ## The details lane
28
+
29
+ Setting `renderDetails` prepends a generated chevron column,
30
+ `DETAILS_COLUMN_ID` (`"__details__"`), pinned to the left after the checkbox
31
+ and tree columns - `[checkbox, tree, details, …]`.
32
+
33
+ It is a system lane: as wide as the chevron it holds, with no resize handle and
34
+ no column menu, and it cannot be hidden, moved, resized or unpinned. Its header
35
+ is a control rather than a title, expanding and collapsing every panel at once.
36
+
37
+ Group rows get no chevron: they expand into their children from the tree lane.
38
+
39
+ ## Opening a row from elsewhere
40
+
41
+ Which rows are open is TanStack's own `expanded` state, so anything can open
42
+ one - a context menu item, a double-click, a button in your own cell:
43
+
44
+ ```tsx
45
+ <Menu.Item onClick={() => row.toggleExpanded()}>Show details</Menu.Item>
46
+ ```
47
+
48
+ Anything reading `row.getIsExpanded()` inside a cell has to subscribe to the
49
+ store with TanStack Store's
50
+ [`useSelector`](https://tanstack.com/store/latest/docs/framework/react/reference), or the
51
+ React Compiler caches the call along with the `row` identity and the control
52
+ never updates:
53
+
54
+ ```tsx
55
+ const expanded = useSelector(row.table.store, () => row.getIsExpanded());
56
+ ```
57
+
58
+ Because it is the standard state, `table.toggleAllRowsExpanded()`,
59
+ `initialState.expanded` and persistence all work with it. It is one of the
60
+ `data` slices, so open panels survive a reload on a
61
+ [persisted](/docs/use-tm-data-grid#persist) grid.
62
+
63
+ > TanStack resets `expanded` whenever the `data` array changes, which would
64
+ > close every panel on each draft commit. The grid therefore sets
65
+ > `autoResetExpanded: false`; pass `autoResetExpanded: true` to have a new
66
+ > `data` array close the panels.
67
+
68
+ ## The two kinds of expanding
69
+
70
+ TanStack keeps one `expanded` state, and the grid uses it for two unrelated
71
+ things: opening a group row into its children, and opening a data row into its
72
+ panel. The controls are kept separate. The details header only opens and closes
73
+ panels, and "Expand all groups" in the tree menu only unfolds the tree.
74
+
75
+ `table.toggleAllRowsExpanded()` is the one that does not distinguish them: it
76
+ writes the state's whole-table form, which is every group and every panel at
77
+ once. `resolveExpandAll` and `areAllRowsExpanded` are exported for anything
78
+ building its own control:
79
+
80
+ ```tsx
81
+ table.setExpanded(
82
+ resolveExpandAll({
83
+ rows: table.getPrePaginatedRowModel().flatRows,
84
+ expanded: table.store.state.expanded,
85
+ target: "details",
86
+ expand: true,
87
+ }),
88
+ );
89
+ ```
90
+
91
+ ## Panel behaviour
92
+
93
+ The panel is a cell spanning the row, inside the row element. It takes the row's
94
+ background, moves with it and is measured with it, but it is **not a row**:
95
+ `aria-rowcount` still counts records, and a click or right-click inside it does
96
+ not select or highlight the row underneath. It carries
97
+ `data-dg-part="details"` with the row's `data-row-id`. See
98
+ [Testing](/docs/testing).
99
+
100
+ Group rows have no panel. Expanding one opens its children.
101
+
102
+ ## Reference
103
+
104
+ | Name | Kind | Type | Default | What it does |
105
+ | --- | --- | --- | --- | --- |
106
+ | `renderDetails` | Option | `({ row, table }) => ReactNode` | – | Contents of the panel. Setting it adds the lane. |
107
+ | `renderDetailsEstHeight` | Option | `number` | `160` | Height the virtualizer assumes for an unmeasured panel. |
108
+ | `initialState.expanded` | Table option | `ExpandedState` | `{}` | Rows open at mount. A data slice, so it persists. |
109
+ | `autoResetExpanded` | Table option | `boolean` | `false` | `true` closes the panels when the `data` array changes. Off by default, so a draft commit keeps them open. |
110
+ | `DETAILS_COLUMN_ID` | Export | `"__details__"` | – | Id of the generated chevron column. |
111
+ | `resolveExpandAll` | Export | `(args) => ExpandedState` | – | Expand or collapse every group, or every panel, but not both. |
112
+ | `areAllRowsExpanded` | Export | `(args) => boolean` | – | Whether every row of one target is open. |
113
+ | `data-dg-part="details"` | Data attribute | – | – | The panel element, carrying the row's `data-row-id`. |
@@ -0,0 +1,148 @@
1
+ # Clicks and context menus
2
+
3
+ Handling clicks in the body: a row click, a cell click, a double-click, and a
4
+ right-click for a menu.
5
+
6
+ Every handler here **composes** with what the click already does. Setting
7
+ `onRowClick` does not replace selection or the highlight; both still happen,
8
+ and your handler runs as well.
9
+
10
+ ```tsx
11
+ <TMDataGrid.Table<Employee> onRowClick={(row) => open(row.original.id)} />
12
+ ```
13
+
14
+ Pass the row type, as above, and `row.original` is typed.
15
+
16
+ ```demo
17
+ file: rows/ClickAndContextMenu.tsx
18
+ hint: Click, double-click and right-click anywhere in the body.
19
+ ```
20
+
21
+ ## The click handlers
22
+
23
+ | Prop | Argument | Notes |
24
+ | --- | --- | --- |
25
+ | `onRowClick` | `row` | Rows show a pointer cursor when set. |
26
+ | `onCellClick` | `{ cell, row, column, event }` | The cell cursor still moves. |
27
+ | `onCellDoubleClick` | same | A double-click that opens an editor still does. |
28
+ | `onCellContextMenu` | same | `renderRowContextMenu` and the cell-selection menu still open. |
29
+
30
+ None of the four fires on a group row: a group row is built on its first
31
+ child's record rather than on one of its own.
32
+
33
+ ## Context menus
34
+
35
+ Right-clicking a row opens a Mantine `Menu` at the pointer. The grid renders the
36
+ `Menu` and its `Menu.Dropdown`, opens it at the cursor, and closes it on Escape,
37
+ on an outside click, on a body scroll, and after an item is picked. The render
38
+ prop supplies only the contents, so anything valid in a dropdown works:
39
+ `Menu.Item`, `Menu.Label`, `Menu.Divider`, `Menu.Sub`, or your own components.
40
+
41
+ ```tsx
42
+ <TMDataGrid.Table<Employee>
43
+ renderRowContextMenu={({ row, cell }) => (
44
+ <>
45
+ <Menu.Label>{row.original.firstName}</Menu.Label>
46
+ <Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
47
+ <Menu.Item
48
+ onClick={() =>
49
+ navigator.clipboard.writeText(String(cell?.getValue() ?? ""))
50
+ }
51
+ >
52
+ Copy cell value
53
+ </Menu.Item>
54
+ <Menu.Divider />
55
+ <Menu.Item color="red" onClick={() => remove(row.original.id)}>
56
+ Delete
57
+ </Menu.Item>
58
+ </>
59
+ )}
60
+ />
61
+ ```
62
+
63
+ | Argument | Type | Description |
64
+ | --- | --- | --- |
65
+ | `table` | `Table<TMDataGridFeatures, TData>` | For actions that read wider state, such as `getSelectedRowModel()`. |
66
+ | `row` | `Row<TMDataGridFeatures, TData>` | The right-clicked row. |
67
+ | `cell` | `Cell<…> \| null` | The cell under the pointer, so a per-cell action such as "copy value" is possible. `null` only if a custom cell renderer stopped the event. |
68
+ | `close` | `() => void` | Closes the menu. `Menu.Item` already closes on click, so this is for content that is not a menu item. |
69
+
70
+ The render prop is called **during render**, and only for the row whose menu is
71
+ open. Keep it a pure function of its arguments and do the work in the item
72
+ handlers.
73
+
74
+ Return `null` to leave a row without a menu. The browser's own context menu
75
+ stays suppressed over the grid either way:
76
+
77
+ ```tsx
78
+ renderRowContextMenu={({ row }) => (row.original.locked ? null : <Menu.Item>Edit</Menu.Item>)}
79
+ ```
80
+
81
+ ### The right-clicked row
82
+
83
+ A right-click does not select or highlight the row. The grid marks it with
84
+ `data-context-menu` while its menu is open, which gives it the hover background.
85
+ An action that should apply to a multi-selection can read the selection off
86
+ `table` and fall back to the clicked row:
87
+
88
+ ```tsx
89
+ renderRowContextMenu={({ table, row }) => {
90
+ const selected = table.getSelectedRowModel().rows;
91
+ const targets = selected.some((r) => r.id === row.id) ? selected : [row];
92
+ return <Menu.Item onClick={() => archive(targets)}>Archive {targets.length}</Menu.Item>;
93
+ }}
94
+ ```
95
+
96
+ ### Taking the whole menu
97
+
98
+ Under `cellSelection: "range"` a right-click inside the selection opens the
99
+ copy and export items too. By default they appear above a divider and yours
100
+ below, which is what happens when the render prop does not use `internalItems`.
101
+ See [Cell selection](/docs/cell-selection#copy-and-export).
102
+
103
+ `internalItems` is those built-in items. **Using it takes over the
104
+ composition**: the menu becomes exactly what you return, in the order you
105
+ return it.
106
+
107
+ ```tsx
108
+ renderRowContextMenu={({ row, internalItems }) => (
109
+ <>
110
+ <Menu.Item onClick={() => open(row.id)}>Open</Menu.Item>
111
+ <Menu.Divider />
112
+ {internalItems}
113
+ </>
114
+ )}
115
+ ```
116
+
117
+ Accept `internalItems` without rendering it to drop the built-in items
118
+ entirely.
119
+
120
+ ### Menu options
121
+
122
+ `rowContextMenuProps` is passed to the Mantine `Menu` unchanged, apart from its
123
+ open state:
124
+
125
+ ```tsx
126
+ <TMDataGrid.Table<Employee>
127
+ renderRowContextMenu={items}
128
+ rowContextMenuProps={{ width: 260, shadow: "lg", position: "right-start" }}
129
+ />
130
+ ```
131
+
132
+ On touch devices a long press (500 ms) opens the same menu. Mantine sets
133
+ `user-select: none` on the element it attaches a context menu to, so body cell
134
+ text is not selectable with the mouse in a grid that has one.
135
+
136
+ ## Reference
137
+
138
+ | Name | Kind | Type | Default | What it does |
139
+ | --- | --- | --- | --- | --- |
140
+ | `onRowClick` | Table prop | `(row) => void` | – | Row click. Adds a pointer cursor. |
141
+ | `onCellClick` | Table prop | `(args) => void` | – | Cell click. Receives `{ cell, row, column, event }`. |
142
+ | `onCellDoubleClick` | Table prop | `(args) => void` | – | Cell double-click. |
143
+ | `onCellContextMenu` | Table prop | `(args) => void` | – | Cell right-click. |
144
+ | `renderRowContextMenu` | Table prop | `({ table, row, cell, close, internalItems }) => ReactNode` | – | Contents of the row's context menu. `null` for no menu. |
145
+ | `renderColumnMenuItems` | Table prop | `({ column, table, internalItems }) => ReactNode[]` | – | Contents of a column's menu. An empty list removes the button. |
146
+ | `rowContextMenuProps` | Table prop | `MenuProps` | – | Passed to the Mantine `Menu`. |
147
+ | `TMDataGridCellEventArgs` | Export | type | – | The argument the three cell handlers receive. |
148
+ | `data-context-menu` | Data attribute | – | – | On the row whose menu is open. |
@@ -0,0 +1,132 @@
1
+ # Row pinning and numbering
2
+
3
+ Two independent features: rows stuck to the top or bottom of the body so they
4
+ stay in view, and a gutter numbering the rows.
5
+
6
+ ## Pinning rows
7
+
8
+ Opt in with `enableRowPinning: true`, or a per-row predicate.
9
+
10
+ ```tsx
11
+ const grid = useTMDataGrid({ data, columns, enableRowPinning: true });
12
+ ```
13
+
14
+ Pinned rows leave the scrolling order and render in sticky blocks: top-pinned
15
+ rows under the header (and under the entry block while one is open),
16
+ bottom-pinned rows above the summary row.
17
+
18
+ There is no built-in pin gesture and no pin icon. The grid provides `row.pin()`
19
+ on every row, callable from anywhere a row is rendered. Two places are
20
+ convenient for it.
21
+
22
+ A **lane of your own**, a display column whose cell is a pin button:
23
+
24
+ ```tsx
25
+ function PinToggle({ row }: { row: Row<TMDataGridFeatures, Employee> }) {
26
+ // Subscribed rather than called in the component body: the React Compiler
27
+ // caches a bare call along with the stable `row` identity.
28
+ const pinned = useSelector(row.table.store, () => row.getIsPinned());
29
+ return (
30
+ <ActionIcon
31
+ variant={pinned === false ? "subtle" : "light"}
32
+ color={pinned === false ? "gray" : "blue"}
33
+ aria-label={pinned === false ? "Pin to top" : "Unpin"}
34
+ // The row underneath may select or highlight on click.
35
+ onClick={(event) => {
36
+ event.stopPropagation();
37
+ row.pin(pinned === false ? "top" : false);
38
+ }}
39
+ >
40
+ {pinned === false ? <IconPin size={16} /> : <IconPinFilled size={16} />}
41
+ </ActionIcon>
42
+ );
43
+ }
44
+
45
+ const pinColumn = columnHelper.display({
46
+ id: "pin",
47
+ header: "",
48
+ // Columns are fluid - `minmax(minSize, flex fr)` - so a control lane states
49
+ // one width three times to opt out. See [Sizing](/docs/column-layout#sizing).
50
+ size: 44,
51
+ minSize: 44,
52
+ maxSize: 44,
53
+ meta: { label: "Pin", align: "center" },
54
+ enableResizing: false,
55
+ enableSorting: false,
56
+ cell: ({ row }) => <PinToggle row={row} />,
57
+ });
58
+ ```
59
+
60
+ Or the **row context menu**, which reaches a row at either edge as well as one
61
+ in the body:
62
+
63
+ ```tsx
64
+ <TMDataGrid.Table<Employee>
65
+ renderRowContextMenu={({ row }) => (
66
+ <>
67
+ {row.getIsPinned() !== "top" && (
68
+ <Menu.Item onClick={() => row.pin("top")}>Pin to top</Menu.Item>
69
+ )}
70
+ {row.getIsPinned() !== false && (
71
+ <Menu.Item onClick={() => row.pin(false)}>Unpin</Menu.Item>
72
+ )}
73
+ </>
74
+ )}
75
+ />
76
+ ```
77
+
78
+ Include a way to unpin.
79
+
80
+ ```demo
81
+ file: rows/PinningAndNumbers.tsx
82
+ hint: Click a pin, or right-click a row for either edge, then scroll.
83
+ ```
84
+
85
+ `row.pin("top" | "bottom" | false)`, `row.getIsPinned()` and `row.getCanPin()`
86
+ are TanStack's own APIs; the state is `rowPinning: { top: string[], bottom:
87
+ string[] }`, settable wholesale with `table.setRowPinning()` or seeded through
88
+ `initialState`.
89
+
90
+ ### Pinned row behaviour
91
+
92
+ Selection, editing, details, the context menu and per-row styling all behave as
93
+ they do in the body. Pinned rows are excluded only from the features that
94
+ depend on scroll *order* - striping and the cell range - and the row-number
95
+ gutter leaves them unnumbered.
96
+
97
+ Also note:
98
+
99
+ - A pinned row stays at its edge even when a filter or the pager would have
100
+ dropped it from the body.
101
+ - A pinned id whose row leaves `data` (a delete, a server-side page swap) is not
102
+ shown. It stays in state and the row returns to its edge if its data comes
103
+ back.
104
+ - **Group rows never pin.** A leaf whose group is collapsed stays hidden while
105
+ pinned.
106
+ - `rowPinning` is **not** persisted. Row ids are data, and the settings group
107
+ holds layout only.
108
+
109
+ ## Numbering rows
110
+
111
+ `enableRowNumbers: true` adds a gutter, outermost left, before every other
112
+ lane.
113
+
114
+ ```tsx
115
+ const grid = useTMDataGrid({ data, columns, enableRowNumbers: true });
116
+ ```
117
+
118
+ It numbers **the current view**: sorted, filtered, and continuing across pages
119
+ rather than restarting on each one. The number is a position, not an identifier
120
+ for the record; for that, add a column of your own over the record's id. Group
121
+ rows and pinned rows take no number.
122
+
123
+ ## Reference
124
+
125
+ | Name | Kind | Type | Default | What it does |
126
+ | --- | --- | --- | --- | --- |
127
+ | `enableRowPinning` | Table option | `boolean \| (row) => boolean` | `false` | Whether rows can be pinned. |
128
+ | `initialState.rowPinning` | Table option | `{ top: string[], bottom: string[] }` | empty | Rows pinned at mount. Not persisted. |
129
+ | `enableRowNumbers` | Option | `boolean` | `false` | Adds the row-number gutter. |
130
+ | `row.pin` | Row method | `("top" \| "bottom" \| false) => void` | – | Pins or unpins one row. |
131
+ | `row.getIsPinned` | Row method | `() => "top" \| "bottom" \| false` | – | Where a row is pinned. |
132
+ | `ROW_NUMBER_COLUMN_ID` | Export | `"__rowNumber__"` | – | Id of the generated number gutter. |