@jielga/tmdatagrid 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,369 @@
1
+ ---
2
+ name: rows
3
+ description: >
4
+ Everything a TMDataGrid row does beyond holding cells. Covers selectionMode
5
+ and its four modes (checkbox, row, checkboxAndHighlight, highlight), the
6
+ highlight as state of its own with defaultHighlightedRowId and
7
+ onHighlightedRowChange, showSelectedBackground and --dg-row-selected-bg,
8
+ reading a selection through the table store, onRowClick / onCellClick /
9
+ onCellDoubleClick / onCellContextMenu, the rowContextMenu render prop and
10
+ rowContextMenuProps, per-row styling with rowStyle, rowClassName, striped and
11
+ the --row-bg rule, the row details panel through renderDetails and
12
+ DETAILS_COLUMN_ID, row pinning with enableRowPinning and row.pin, and the
13
+ row-number gutter through enableRowNumbers. Load when selecting rows, reacting
14
+ to a click, opening a detail panel, colouring rows by their data, pinning rows
15
+ to an edge, or numbering them.
16
+ metadata:
17
+ type: core
18
+ library: '@jielga/tmdatagrid'
19
+ library_version: '1.0.2'
20
+ sources:
21
+ - 'Jielga/TMDataGrid:src/docs/row-selection.md'
22
+ - 'Jielga/TMDataGrid:src/docs/row-interaction.md'
23
+ - 'Jielga/TMDataGrid:src/docs/row-styling.md'
24
+ - 'Jielga/TMDataGrid:src/docs/row-details.md'
25
+ - 'Jielga/TMDataGrid:src/docs/row-pinning.md'
26
+ - 'Jielga/TMDataGrid:src/tmdatagrid/core/rowSelection.ts'
27
+ ---
28
+
29
+ # TMDataGrid - Rows
30
+
31
+ Selecting rows, reacting to clicks, colouring them, opening a panel under one,
32
+ and pinning or numbering them. Column-side concerns are the `columns` skill;
33
+ cell cursors and ranges are `cell-selection`.
34
+
35
+ ## Selection
36
+
37
+ `selectionMode` sets what selecting looks like **and** what a bare row click
38
+ does. One option rather than two, because a click can either toggle a
39
+ multi-selection or move a highlight, never both.
40
+
41
+ | Mode | Checkbox column | Row click |
42
+ | --- | --- | --- |
43
+ | `"checkbox"` (default) | yes, multi-select | nothing |
44
+ | `"row"` | no | selects, with the usual modifiers |
45
+ | `"checkboxAndHighlight"` | yes, multi-select | highlights one row |
46
+ | `"highlight"` | no | highlights one row, no selection at all |
47
+
48
+ ```tsx
49
+ const grid = useTMDataGrid({ data, columns, selectionMode: "row" });
50
+ ```
51
+
52
+ The first two write to TanStack's `rowSelection`, so everything downstream - the
53
+ toolbar count, `getSelectedRowModel()`, persistence - is unaffected by the
54
+ choice. Under `"row"` the click follows desktop-list conventions: a plain click
55
+ makes the selection this row, Ctrl/Cmd toggles it and leaves the rest, Shift
56
+ selects the range from the anchor, Ctrl+Shift adds that range. Rows are
57
+ focusable in this mode, and Space or Enter toggles the focused row.
58
+
59
+ `enableRowSelection: false` removes the selection half whatever the mode.
60
+
61
+ ### The highlight is not a selection
62
+
63
+ The highlight is state of its own, not a slice of `rowSelection` - which is what
64
+ lets `"checkboxAndHighlight"` run both at once: tick rows for a bulk action,
65
+ click one to open its record beside the grid.
66
+
67
+ ```tsx
68
+ const grid = useTMDataGrid({
69
+ data,
70
+ columns,
71
+ selectionMode: "highlight",
72
+ defaultHighlightedRowId: routeId,
73
+ onHighlightedRowChange: (rowId) =>
74
+ navigate({ to: "/employees/$id", params: { id: rowId ?? "" } }),
75
+ });
76
+ ```
77
+
78
+ ### Reading a selection
79
+
80
+ Read it through the table store, never by calling the method bare. `table` keeps
81
+ one identity across renders, so the React Compiler caches a bare
82
+ `getSelectedRowModel()` and the reader stops updating.
83
+
84
+ ```tsx
85
+ import { useSelector } from "@tanstack/react-store";
86
+
87
+ const selected = useSelector(grid.table.store, () =>
88
+ grid.table.getSelectedRowModel().rows,
89
+ );
90
+ ```
91
+
92
+ ### The background
93
+
94
+ `showSelectedBackground` follows the mode: on for `"row"`, where the background
95
+ is the only feedback a click gives, off for `"checkbox"`, where the box already
96
+ says so. Set it to override either way. The colour is `--dg-row-selected-bg`,
97
+ changed on the grid element rather than through the flag:
98
+
99
+ ```tsx
100
+ <TMDataGrid
101
+ {...grid}
102
+ style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }}
103
+ />
104
+ ```
105
+
106
+ A group row's checkbox selects every record under it at any depth, shows a tick
107
+ once all are selected and a dash while only some are, and writes only the
108
+ records to `rowSelection`.
109
+
110
+ ## Clicks and context menus
111
+
112
+ Every handler **composes** with what the click already does. `onRowClick` does
113
+ not replace selection or the highlight; yours runs in addition.
114
+
115
+ | Handler | Argument | Notes |
116
+ | --- | --- | --- |
117
+ | `onRowClick` | `row` | Rows show a pointer cursor when set |
118
+ | `onCellClick` | `{ cell, row, column, event }` | The cell cursor still moves |
119
+ | `onCellDoubleClick` | same | A double-click that opens an editor still does |
120
+ | `onCellContextMenu` | same | `rowContextMenu` still opens |
121
+
122
+ ```tsx
123
+ <TMDataGrid.Table<Employee> onRowClick={(row) => open(row.original.id)} />
124
+ ```
125
+
126
+ Pass the row type, as above, and `row.original` is typed. Group rows sit out all
127
+ four: TanStack builds a group row on top of its first child's record, so a
128
+ handler would receive a real-looking row that is the wrong one.
129
+
130
+ `rowContextMenu` is a render prop saying what goes inside the menu. The grid
131
+ owns the Mantine `Menu`, opening it at the pointer and closing it on Escape, an
132
+ outside click, a body scroll and after an item is picked.
133
+
134
+ ```tsx
135
+ <TMDataGrid.Table<Employee>
136
+ rowContextMenu={({ table, row, cell }) => (
137
+ <>
138
+ <Menu.Label>{row.original.firstName}</Menu.Label>
139
+ <Menu.Item onClick={() => open(row.original.id)}>Open</Menu.Item>
140
+ <Menu.Item
141
+ onClick={() =>
142
+ navigator.clipboard.writeText(String(cell?.getValue() ?? ""))
143
+ }
144
+ >
145
+ Copy cell value
146
+ </Menu.Item>
147
+ </>
148
+ )}
149
+ rowContextMenuProps={{ width: 260, position: "right-start" }}
150
+ />
151
+ ```
152
+
153
+ It is called **during render**, only for the row whose menu is open, so keep it
154
+ a pure function of its arguments and do the work in the item handlers. Return
155
+ `null` to leave a row without a menu. Right-clicking does not select: it marks
156
+ the row with `data-context-menu` while its menu is open, so an action meant for
157
+ a multi-selection reads `table` and falls back to the clicked row.
158
+
159
+ ## Row styling
160
+
161
+ ```tsx
162
+ <TMDataGrid.Table<Employee>
163
+ striped
164
+ rowStyle={(row) =>
165
+ row.original.status === "Terminated"
166
+ ? { "--row-bg": "var(--mantine-color-red-0)" }
167
+ : undefined
168
+ }
169
+ rowClassName={(row) => (row.original.overdue ? classes.overdue : undefined)}
170
+ />
171
+ ```
172
+
173
+ A row is a strip of cells, some sticky in pinned lanes, and hover, selection,
174
+ the highlight, the cell range and striping all paint on top of it. `--row-bg`
175
+ feeds the variable those layers compose against; `background` wins over all of
176
+ them and kills them. `striped` follows position in the view, so it survives
177
+ sorting and filtering rather than sticking to records, and pinned rows sit it
178
+ out.
179
+
180
+ Rows carry `data-selected`, `data-selected-bg`, `data-highlighted`,
181
+ `data-grouped`, `data-depth`, `data-context-menu` and `data-row-id`, so a
182
+ stylesheet can reach any of it without a callback.
183
+
184
+ ## The details panel
185
+
186
+ Setting `renderDetails` turns the lane on. There is no flag.
187
+
188
+ ```tsx
189
+ const grid = useTMDataGrid({
190
+ data,
191
+ columns,
192
+ renderDetails: ({ row }) => <EmployeeCard employee={row.original} />,
193
+ });
194
+ ```
195
+
196
+ Each panel is measured, so heights need not be uniform;
197
+ `renderDetailsEstHeight` (default `160`) is only what the virtualizer assumes
198
+ for one it has not seen. A generated chevron column, `DETAILS_COLUMN_ID`, is
199
+ prepended and pinned left after the checkbox and tree lanes; it cannot be
200
+ hidden, moved, resized or unpinned, and its header opens and closes every panel.
201
+
202
+ Which rows are open is TanStack's own `expanded` state, so anything can open
203
+ one - `row.toggleExpanded()` from a menu item, `initialState.expanded`,
204
+ `table.toggleAllRowsExpanded()` - and it persists as a `data` slice. The panel
205
+ is a cell spanning the row, not a row: `aria-rowcount` still counts records, and
206
+ a click inside it stops there rather than selecting the row underneath. Group
207
+ rows have no panel; expanding one opens its children.
208
+
209
+ ## Pinning and numbering
210
+
211
+ Two independent lanes at the edges.
212
+
213
+ ```tsx
214
+ const grid = useTMDataGrid({
215
+ data,
216
+ columns,
217
+ enableRowPinning: true,
218
+ enableRowNumbers: true,
219
+ });
220
+ ```
221
+
222
+ There is no built-in pin gesture - pin from wherever suits, most naturally the
223
+ row context menu, through TanStack's own `row.pin("top" | "bottom" | false)`,
224
+ `row.getIsPinned()` and `row.getCanPin()`. Pinned rows leave the scrolling order
225
+ and render in sticky blocks: top under the header, bottom above the summary row.
226
+
227
+ Pinned rows are still body rows - selection, editing, details, the context menu
228
+ and per-row styling all behave normally. What they sit out are the statements
229
+ about scrolling order: striping, the cell range, and the number gutter. A pinned
230
+ row stays at its edge even when a filter or the pager would have dropped it, and
231
+ **group rows never pin**.
232
+
233
+ `enableRowNumbers` adds a gutter outermost left that numbers the current view -
234
+ sorted, filtered, continuing across pages. It answers "where am I in what I am
235
+ looking at", not "which record is this"; a stable identifier is a column of your
236
+ own over the record's id.
237
+
238
+ ## Common mistakes
239
+
240
+ ### CRITICAL Setting `background` in `rowStyle`
241
+
242
+ A coloured row stops responding to hover, selection, the highlight and the cell
243
+ range, because `background` paints over every layer composed on top of the row.
244
+ Nothing errors; the row simply goes inert.
245
+
246
+ Wrong:
247
+
248
+ ```tsx
249
+ rowStyle={() => ({ background: "pink" })}
250
+ ```
251
+
252
+ Correct:
253
+
254
+ ```tsx
255
+ rowStyle={() => ({ "--row-bg": "pink" })}
256
+ ```
257
+
258
+ Source: `src/docs/row-styling.md` (Set `--row-bg`, not `background`).
259
+
260
+ ### CRITICAL Reading the selection without subscribing
261
+
262
+ `table` keeps one identity for the life of the grid, so the React Compiler
263
+ memoizes a bare `getSelectedRowModel()` against it. The count renders once and
264
+ then never changes, which reads as a broken toolbar rather than a missing
265
+ subscription.
266
+
267
+ Wrong:
268
+
269
+ ```tsx
270
+ const selected = grid.table.getSelectedRowModel().rows;
271
+ ```
272
+
273
+ Correct:
274
+
275
+ ```tsx
276
+ const selected = useSelector(grid.table.store, () =>
277
+ grid.table.getSelectedRowModel().rows,
278
+ );
279
+ ```
280
+
281
+ Source: `src/docs/row-selection.md` (Acting on a selection).
282
+
283
+ ### HIGH Looking for the highlight in `rowSelection`
284
+
285
+ The highlight is separate state, which is what lets
286
+ `"checkboxAndHighlight"` run both at once. It never appears in `rowSelection`,
287
+ `getSelectedRowModel()` or the persisted selection slice.
288
+
289
+ Wrong:
290
+
291
+ ```tsx
292
+ // Always empty under selectionMode: "highlight".
293
+ const current = Object.keys(grid.table.store.state.rowSelection)[0];
294
+ ```
295
+
296
+ Correct:
297
+
298
+ ```tsx
299
+ const current = useSelector(grid.ui, (state) => state.highlightedRowId);
300
+ ```
301
+
302
+ Source: `src/docs/row-selection.md` (The highlight is not a selection).
303
+
304
+ ### HIGH Expecting a click handler to replace the built-in behaviour
305
+
306
+ `onRowClick` composes. Under `selectionMode: "row"` a click both selects and
307
+ runs the handler, so a handler that navigates away will also have changed the
308
+ selection on the way out.
309
+
310
+ Correct, when the click should only navigate:
311
+
312
+ ```tsx
313
+ // "highlight" moves a marker and selects nothing.
314
+ useTMDataGrid({ data, columns, selectionMode: "highlight" });
315
+ ```
316
+
317
+ Source: `src/docs/row-interaction.md`.
318
+
319
+ ### HIGH Reading `row.getIsExpanded()` in a cell without subscribing
320
+
321
+ Same mechanism as the selection above: the compiler caches the call against the
322
+ `row` identity, so a custom expand control renders its initial state forever.
323
+
324
+ Correct:
325
+
326
+ ```tsx
327
+ const expanded = useSelector(row.table.store, () => row.getIsExpanded());
328
+ ```
329
+
330
+ Source: `src/docs/row-details.md` (Opening a row from elsewhere).
331
+
332
+ ### MEDIUM Assuming group rows behave like data rows
333
+
334
+ Group rows are built on their first child's record. They sit out `onRowClick`,
335
+ `onCellClick`, `onCellDoubleClick` and `onCellContextMenu`, they never pin, they
336
+ take no row number, and they have no details panel. A handler written as though
337
+ every row reaches it will silently skip them.
338
+
339
+ Source: `src/docs/row-interaction.md`, `src/docs/row-pinning.md`.
340
+
341
+ ### MEDIUM Open panels closing when `data` is replaced
342
+
343
+ TanStack resets `expanded` when the row structure changes, so a refetch collapses
344
+ every open panel.
345
+
346
+ Correct:
347
+
348
+ ```tsx
349
+ useTMDataGrid({ data, columns, renderDetails, autoResetExpanded: false });
350
+ ```
351
+
352
+ Source: `src/docs/row-details.md`.
353
+
354
+ ### MEDIUM Expecting pinned rows to persist
355
+
356
+ `rowPinning` is deliberately left out of `settingsKey`: row ids are data, and a
357
+ layout store outlives any one data set. A pinned id whose row leaves `data` is
358
+ simply not shown, and returns to its edge if the data comes back.
359
+
360
+ Source: `src/docs/row-pinning.md`.
361
+
362
+ ## References
363
+
364
+ - [Rows API](references/rows-api.md) - every option, table prop, callback,
365
+ export, CSS variable and data attribute these five topics own.
366
+
367
+ See also: the `grouping` skill for group rows and the tree lane, the
368
+ `cell-selection` skill for the cell cursor and ranges, and the `appearance`
369
+ skill for the CSS variables named here.
@@ -0,0 +1,117 @@
1
+ # Rows API
2
+
3
+ Reference for the `rows` skill.
4
+
5
+ ## Selection
6
+
7
+ | Name | Kind | Type | Default | What it does |
8
+ | --- | --- | --- | --- | --- |
9
+ | `selectionMode` | Option | `"checkbox" \| "row" \| "checkboxAndHighlight" \| "highlight"` | `"checkbox"` | What selecting looks like and what a row click does. |
10
+ | `enableRowSelection` | Table option | `boolean \| ((row) => boolean)` | `true` | `false` removes the checkbox column and row-click selection. |
11
+ | `enableMultiRowSelection` | Table option | `boolean` | `true` | `false` limits the selection to one row and drops group checkboxes. |
12
+ | `showSelectedBackground` | Option | `boolean` | Follows the mode | Whether selected rows take a background tint. |
13
+ | `defaultHighlightedRowId` | Option | `string \| null` | `null` | Row highlighted at mount. |
14
+ | `onHighlightedRowChange` | Callback | `(rowId: string \| null) => void` | – | Fires when the highlight moves. |
15
+ | `SELECT_COLUMN_ID` | Export | `"__select__"` | – | Id of the generated checkbox column. |
16
+ | `getSelectableRowIds` | Export | `(table) => string[]` | – | Ids the header checkbox would select. |
17
+ | `resolveRowSelectionClick` | Export | `(args) => ResolvedRowSelection` | – | The desktop-list click rules, for a custom surface. |
18
+ | `getDisplayedRows` | Export | `(table) => Row[]` | – | The rows currently on screen, paging respected. |
19
+ | `isPagingActive` | Export | `(table) => boolean` | – | Whether a pager is in play. |
20
+
21
+ The highlight lives on the ui store, not in `rowSelection`:
22
+ `useSelector(grid.ui, (state) => state.highlightedRowId)`.
23
+
24
+ ## Clicks and context menus
25
+
26
+ All are props of `TMDataGrid.Table`, not hook options.
27
+
28
+ | Name | Type | What it does |
29
+ | --- | --- | --- |
30
+ | `onRowClick` | `(row) => void` | Row click. Adds a pointer cursor. |
31
+ | `onCellClick` | `(args) => void` | Cell click. `args` is `TMDataGridCellEventArgs`. |
32
+ | `onCellDoubleClick` | `(args) => void` | Cell double-click. |
33
+ | `onCellContextMenu` | `(args) => void` | Cell right-click. |
34
+ | `rowContextMenu` | `({ table, row, cell, close }) => ReactNode` | Contents of the row's context menu. `null` for no menu. |
35
+ | `rowContextMenuProps` | `MenuProps` | Passed to the Mantine `Menu` untouched apart from its open state. |
36
+
37
+ `TMDataGridCellEventArgs` is `{ cell, row, column, event }`. The context-menu
38
+ render prop's `cell` is `null` only when a custom cell renderer stopped the
39
+ event. One `Menu` serves the whole body: a closed Mantine `Popover` still runs
40
+ its hooks on every render, and the virtualized body re-renders every scroll
41
+ frame.
42
+
43
+ On touch devices a long press (500 ms) opens the same menu. Mantine sets
44
+ `user-select: none` on the element it hangs a context menu off, so body cell
45
+ text stops being mouse-selectable in a grid that has one.
46
+
47
+ ## Styling
48
+
49
+ | Name | Kind | Type | Default | What it does |
50
+ | --- | --- | --- | --- | --- |
51
+ | `rowStyle` | Table prop | `TMDataGridRowStyle \| ((row) => TMDataGridRowStyle)` | – | Inline style for a body row. |
52
+ | `rowClassName` | Table prop | `string \| ((row) => string \| undefined)` | – | Class added after the grid's own. |
53
+ | `striped` | Table prop | `boolean` | `false` | Every second row takes `--dg-row-striped-bg`. |
54
+ | `TMDataGridRowStyle` | Export | type | – | `CSSProperties` or an object of `--*` custom properties. |
55
+ | `--row-bg` | CSS variable | colour | – | The row's own background, composed under hover, selection and range. |
56
+ | `--dg-row-striped-bg` | CSS variable | colour | Themed | The stripe colour. |
57
+ | `--dg-row-selected-bg` | CSS variable | colour | `--mantine-primary-color-light` | Selected row background. |
58
+ | `--dg-row-highlight-bg` | CSS variable | colour | Themed | Highlighted row background. |
59
+ | `--dg-row-height` | CSS variable | length | From `size` | Row height. `meta.rowHeight` is the supported way to change it. |
60
+
61
+ Row data attributes:
62
+
63
+ | Attribute | On |
64
+ | --- | --- |
65
+ | `data-selected` | Selected rows |
66
+ | `data-selected-bg` | Selected rows that also take the background |
67
+ | `data-highlighted` | The highlighted row |
68
+ | `data-grouped` | Group rows |
69
+ | `data-depth` | Every row - the nesting level |
70
+ | `data-context-menu` | The row whose context menu is open |
71
+ | `data-deleted` | Rows marked for deletion under batch editing |
72
+ | `data-row-id` | Every row - its id |
73
+
74
+ ## Details
75
+
76
+ | Name | Kind | Type | Default | What it does |
77
+ | --- | --- | --- | --- | --- |
78
+ | `renderDetails` | Option | `({ row, table }) => ReactNode` | – | Contents of the panel. Setting it adds the lane. |
79
+ | `renderDetailsEstHeight` | Option | `number` | `160` | Height the virtualizer assumes for an unmeasured panel. |
80
+ | `initialState.expanded` | Table option | `ExpandedState` | `{}` | Rows open at mount. A `data` slice, so it persists. |
81
+ | `autoResetExpanded` | Table option | `boolean` | `true` | `false` keeps panels open when `data` changes. |
82
+ | `DETAILS_COLUMN_ID` | Export | `"__details__"` | – | Id of the generated chevron column. |
83
+ | `resolveExpandAll` | Export | `(args) => ExpandedState` | – | Expand or collapse every group, or every panel, but not both. |
84
+ | `areAllRowsExpanded` | Export | `(args) => boolean` | – | Whether every row of one target is open. |
85
+ | `data-dg-part="details"` | Data attribute | – | – | The panel element, carrying the row's `data-row-id`. |
86
+
87
+ One `expanded` state opens two unrelated things - a group row into its children,
88
+ a data row into its panel. The controls stay apart: the details header only
89
+ opens panels, "Expand all groups" only unfolds the tree.
90
+ `table.toggleAllRowsExpanded()` is the one that does not distinguish them, which
91
+ is what `resolveExpandAll({ rows, expanded, target, expand })` exists to avoid:
92
+
93
+ ```tsx
94
+ table.setExpanded(
95
+ resolveExpandAll({
96
+ rows: table.getPrePaginatedRowModel().flatRows,
97
+ expanded: table.store.state.expanded,
98
+ target: "details",
99
+ expand: true,
100
+ }),
101
+ );
102
+ ```
103
+
104
+ ## Pinning and numbering
105
+
106
+ | Name | Kind | Type | Default | What it does |
107
+ | --- | --- | --- | --- | --- |
108
+ | `enableRowPinning` | Table option | `boolean \| ((row) => boolean)` | `false` | Whether rows can be pinned. |
109
+ | `initialState.rowPinning` | Table option | `{ top: string[], bottom: string[] }` | empty | Rows pinned at mount. Not persisted. |
110
+ | `enableRowNumbers` | Option | `boolean` | `false` | Adds the row-number gutter, outermost left. |
111
+ | `row.pin` | Row method | `("top" \| "bottom" \| false) => void` | – | Pins or unpins one row. |
112
+ | `row.getIsPinned` | Row method | `() => "top" \| "bottom" \| false` | – | Where a row is pinned. |
113
+ | `row.getCanPin` | Row method | `() => boolean` | – | Whether this row may pin. |
114
+ | `ROW_NUMBER_COLUMN_ID` | Export | `"__rowNumber__"` | – | Id of the generated number gutter. |
115
+
116
+ Lane order, left to right: row number, checkbox, tree, details, your columns,
117
+ edit.
@@ -10,7 +10,7 @@ description: >
10
10
  metadata:
11
11
  type: core
12
12
  library: '@jielga/tmdatagrid'
13
- library_version: '1.0.1'
13
+ library_version: '1.0.2'
14
14
  sources:
15
15
  - 'Jielga/TMDataGrid:src/docs/server-side.md'
16
16
  - 'Jielga/TMDataGrid:src/tmdatagrid/useTMDataGrid.tsx'
@@ -11,7 +11,7 @@ description: >
11
11
  metadata:
12
12
  type: core
13
13
  library: '@jielga/tmdatagrid'
14
- library_version: '1.0.1'
14
+ library_version: '1.0.2'
15
15
  sources:
16
16
  - 'Jielga/TMDataGrid:src/docs/testing.md'
17
17
  - 'Jielga/TMDataGrid:src/tmdatagrid/components/TMDataGrid.tsx'