@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.
- package/package.json +1 -1
- package/skills/appearance/SKILL.md +322 -0
- package/skills/cell-selection/SKILL.md +240 -0
- package/skills/columns/SKILL.md +260 -85
- package/skills/data/SKILL.md +289 -0
- package/skills/editing/SKILL.md +492 -0
- package/skills/editing/references/editing-api.md +124 -0
- package/skills/editing/references/editors-and-validation.md +198 -0
- package/skills/filtering/SKILL.md +344 -0
- package/skills/getting-started/SKILL.md +33 -12
- package/skills/grouping/SKILL.md +264 -0
- package/skills/options/SKILL.md +24 -13
- package/skills/rows/SKILL.md +369 -0
- package/skills/rows/references/rows-api.md +117 -0
- package/skills/server-side/SKILL.md +1 -1
- package/skills/testing/SKILL.md +1 -1
- package/skills/features/SKILL.md +0 -352
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jielga/tmdatagrid",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A React data grid built on TanStack Table v9 and Mantine - always virtualized, with resizable, reorderable, sortable, filterable, hideable and pinnable columns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: appearance
|
|
3
|
+
description: >
|
|
4
|
+
Theme, size, compose and translate a TMDataGrid. Covers the size scale (xs to
|
|
5
|
+
xl) and what it drives, every --dg-* CSS variable for metrics, colours and the
|
|
6
|
+
stacking ladder, the two stylesheets styles.css and styles.layer.css and why
|
|
7
|
+
only one may be imported, the bounded-height layout rule with minHeight 0,
|
|
8
|
+
toolbar composition through children and TMDataGrid.Spacer, writing a toolbar
|
|
9
|
+
button with useTMDataGridContext, hiding it the way the built-ins do with
|
|
10
|
+
getGridCapabilities and getColumnCapabilities, why capabilities take a
|
|
11
|
+
features argument under the React Compiler, and localization through the
|
|
12
|
+
labels option, TMDATAGRID_LABELS_EN, TMDATAGRID_LABELS_SV, mergeLabels and
|
|
13
|
+
grid.labels. Load when styling or theming the grid, choosing a density,
|
|
14
|
+
building a toolbar, adding a button beside the built-in ones, or translating
|
|
15
|
+
the interface.
|
|
16
|
+
metadata:
|
|
17
|
+
type: core
|
|
18
|
+
library: '@jielga/tmdatagrid'
|
|
19
|
+
library_version: '1.0.2'
|
|
20
|
+
sources:
|
|
21
|
+
- 'Jielga/TMDataGrid:src/docs/styling.md'
|
|
22
|
+
- 'Jielga/TMDataGrid:src/docs/toolbar.md'
|
|
23
|
+
- 'Jielga/TMDataGrid:src/docs/localization.md'
|
|
24
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/capabilities.ts'
|
|
25
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/labels.ts'
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# TMDataGrid - Appearance, toolbar and labels
|
|
29
|
+
|
|
30
|
+
Everything set on the grid element or composed around it: the size scale, the
|
|
31
|
+
CSS variables, the toolbar, and the strings.
|
|
32
|
+
|
|
33
|
+
## Size
|
|
34
|
+
|
|
35
|
+
`size` drives row height, header height, font size and cell padding together,
|
|
36
|
+
and selects the size of every Mantine control the grid renders.
|
|
37
|
+
|
|
38
|
+
| `size` | Row height | Header height | Font size | Cell padding |
|
|
39
|
+
| --- | --- | --- | --- | --- |
|
|
40
|
+
| `xs` | 34px | 32px | `xs` | 6px |
|
|
41
|
+
| `sm` | 42px | 38px | `sm` | 8px |
|
|
42
|
+
| `md` (default) | 52px | 44px | `sm` | 10px |
|
|
43
|
+
| `lg` | 62px | 52px | `md` | 14px |
|
|
44
|
+
| `xl` | 72px | 60px | `lg` | 18px |
|
|
45
|
+
|
|
46
|
+
Row height is also required by the virtualizer **as a number**, so it cannot be
|
|
47
|
+
defined in CSS alone. For a height outside the scale set `meta.rowHeight`, not
|
|
48
|
+
the variable. `SIZE_ROW_HEIGHT` is the exported source of these values.
|
|
49
|
+
|
|
50
|
+
## CSS variables
|
|
51
|
+
|
|
52
|
+
`style` accepts custom properties and `className` reaches the same element from
|
|
53
|
+
a stylesheet. Both are per instance - a grid is themed without a provider.
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
<TMDataGrid
|
|
57
|
+
{...grid}
|
|
58
|
+
size="sm"
|
|
59
|
+
style={{ "--dg-row-selected-bg": "var(--mantine-color-blue-0)" }}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Metrics:** `--dg-row-height`, `--dg-header-height`, `--dg-summary-height`,
|
|
64
|
+
`--dg-entry-height`, `--dg-font-size`, `--dg-padding`. All default from `size`.
|
|
65
|
+
The generated lanes are exempt from padding: they are fixed 36px tracks that
|
|
66
|
+
centre their control.
|
|
67
|
+
|
|
68
|
+
**Colours:** `--row-bg` (one row's own background - set this, never
|
|
69
|
+
`background`), `--dg-row-selected-bg`, `--dg-row-highlight-bg`,
|
|
70
|
+
`--dg-row-striped-bg`, `--dg-row-group-bg`, `--dg-match-highlight-bg`,
|
|
71
|
+
`--dg-header-shadow-color`.
|
|
72
|
+
|
|
73
|
+
**Layout internals:** `--dg-sticky-edge-range` (`20px`), the `--dg-edge-*`
|
|
74
|
+
markers the grid sets on cell-range borders, and the `--dg-z-*` stacking ladder -
|
|
75
|
+
change those only to slot something of your own between two layers.
|
|
76
|
+
|
|
77
|
+
One stylesheet import, once, anywhere:
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
import "@jielga/tmdatagrid/styles.css";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`@jielga/tmdatagrid/styles.layer.css` is the same stylesheet wrapped in a
|
|
84
|
+
`@layer`, for an application that orders its own layers. Import **one** of the
|
|
85
|
+
two, never both.
|
|
86
|
+
|
|
87
|
+
## Layout
|
|
88
|
+
|
|
89
|
+
The grid fills the box you give it and scrolls inside it. It does not size
|
|
90
|
+
itself to its content - a virtualized grid has no content height to measure.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
|
|
94
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }}>
|
|
95
|
+
<TMDataGrid.Table />
|
|
96
|
+
</TMDataGrid>
|
|
97
|
+
</div>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`minHeight: 0` is the part everyone forgets: a flex item's default
|
|
101
|
+
`min-height: auto` refuses to shrink below its content, so without it the grid
|
|
102
|
+
grows past the viewport instead of scrolling.
|
|
103
|
+
|
|
104
|
+
## Toolbar
|
|
105
|
+
|
|
106
|
+
The toolbar is a flex row and nothing more. No slots API, no `actions` prop -
|
|
107
|
+
your buttons sit beside the built-in ones because they are all just children,
|
|
108
|
+
and `TMDataGrid.Spacer` pushes what follows to the right. That is the whole
|
|
109
|
+
layout system.
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
<TMDataGrid.Toolbar>
|
|
113
|
+
<TMDataGrid.SummaryCount />
|
|
114
|
+
<TMDataGrid.Search />
|
|
115
|
+
<TMDataGrid.Spacer />
|
|
116
|
+
<TMDataGrid.LoadingIndicator />
|
|
117
|
+
<ExportButton />
|
|
118
|
+
<TMDataGrid.FilterButton />
|
|
119
|
+
<TMDataGrid.ColumnsButton />
|
|
120
|
+
</TMDataGrid.Toolbar>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Each built-in renders nothing when its feature is off, so a read-only grid needs
|
|
124
|
+
no conditionals: `FilterButton` under `enableColumnFilters: false` is simply
|
|
125
|
+
absent.
|
|
126
|
+
|
|
127
|
+
A button of your own reads the grid from context - `{ table, ui, features,
|
|
128
|
+
labels, controlSize, resetSettings }`:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import {
|
|
132
|
+
exportGridToCsv,
|
|
133
|
+
getGridCapabilities,
|
|
134
|
+
useTMDataGridContext,
|
|
135
|
+
} from "@jielga/tmdatagrid";
|
|
136
|
+
|
|
137
|
+
function ExportButton() {
|
|
138
|
+
const { table, features, controlSize } = useTMDataGridContext();
|
|
139
|
+
const { canFilterAny } = getGridCapabilities(table, features);
|
|
140
|
+
|
|
141
|
+
if (!canFilterAny) return null;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Button size={controlSize} onClick={() => exportGridToCsv({ table })}>
|
|
145
|
+
Export
|
|
146
|
+
</Button>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`getGridCapabilities(table, features)` answers `canSortAny`, `canFilterAny`,
|
|
152
|
+
`canHideAny`, `canPinAny`, `canReorderAny`, `canGroupAny`, `canSelectRows`,
|
|
153
|
+
`canPaginate` and `canSearch`. `getColumnCapabilities(column, features)` answers
|
|
154
|
+
the same for one column as `canSort`, `canFilter`, `canHide`, `canPin`,
|
|
155
|
+
`canResize`, `canReorder` and `canGroup`.
|
|
156
|
+
|
|
157
|
+
### Why `features` is a second argument
|
|
158
|
+
|
|
159
|
+
`features` comes back from `useTMDataGrid` and is re-derived from the options
|
|
160
|
+
object on every render. It is required **in addition to** TanStack's `getCanX()`
|
|
161
|
+
methods because it is what makes the result reactive: `column.getCanSort()` is a
|
|
162
|
+
method call on a column object whose identity survives an options change, so
|
|
163
|
+
under the React Compiler that call is memoized and a grid whose `enableSorting`
|
|
164
|
+
flipped to `false` would carry on rendering sort indicators. `features` supplies
|
|
165
|
+
a value that changes; `getCanX()` still decides the outcome.
|
|
166
|
+
|
|
167
|
+
The same rule applies anywhere in your app: read state through
|
|
168
|
+
`useSelector(table.store, …)` and options through `features`, rather than
|
|
169
|
+
calling methods on a long-lived object.
|
|
170
|
+
|
|
171
|
+
## Labels
|
|
172
|
+
|
|
173
|
+
Every string the grid renders, and every `aria-label`, comes from one labels
|
|
174
|
+
object. English by default, and `labels` takes **any subset**, merged over the
|
|
175
|
+
defaults.
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
import { TMDATAGRID_LABELS_SV } from "@jielga/tmdatagrid";
|
|
179
|
+
|
|
180
|
+
const grid = useTMDataGrid({ data, columns, labels: TMDATAGRID_LABELS_SV });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Labels that carry a value are functions, so a language can put the value where
|
|
184
|
+
its grammar wants it:
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
const labels = {
|
|
188
|
+
groupBy: (column) => `Gruppera på ${column}`,
|
|
189
|
+
pageRange: ({ from, to, total }) => `${from}–${to} av ${total}`,
|
|
190
|
+
} satisfies TMDataGridLabelsOverride;
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
`TMDataGridLabels` is the full dictionary type, which is what makes a new
|
|
194
|
+
translation a typed exercise rather than a guess. The resolved dictionary comes
|
|
195
|
+
back as `grid.labels` and from `useTMDataGridContext().labels`, so a component
|
|
196
|
+
of your own uses the same strings as the built-in chrome.
|
|
197
|
+
|
|
198
|
+
## Common mistakes
|
|
199
|
+
|
|
200
|
+
### CRITICAL Importing both stylesheets
|
|
201
|
+
|
|
202
|
+
`styles.css` and `styles.layer.css` are the same rules, one wrapped in a
|
|
203
|
+
`@layer`. Importing both means the unlayered copy wins every cascade contest,
|
|
204
|
+
so an application that ordered its layers to put the grid underneath its own
|
|
205
|
+
overrides silently gets the opposite.
|
|
206
|
+
|
|
207
|
+
Source: `src/docs/styling.md` (The stylesheet).
|
|
208
|
+
|
|
209
|
+
### CRITICAL A grid with no bounded height
|
|
210
|
+
|
|
211
|
+
The grid does not size itself to its content, so inside a flex parent without
|
|
212
|
+
`minHeight: 0` it grows past the viewport and the page scrolls instead of the
|
|
213
|
+
body. Rows render, so nothing looks broken until the list is long.
|
|
214
|
+
|
|
215
|
+
Wrong:
|
|
216
|
+
|
|
217
|
+
```tsx
|
|
218
|
+
<TMDataGrid {...grid} style={{ flex: 1 }} />
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Correct:
|
|
222
|
+
|
|
223
|
+
```tsx
|
|
224
|
+
<TMDataGrid {...grid} style={{ flex: 1, minHeight: 0 }} />
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Source: `src/docs/styling.md` (Layout).
|
|
228
|
+
|
|
229
|
+
### HIGH Setting `--dg-row-height` to change density
|
|
230
|
+
|
|
231
|
+
The virtualizer needs the row height as a number, and takes it from
|
|
232
|
+
`meta.rowHeight` or `size` - not from the variable. Setting the variable alone
|
|
233
|
+
leaves the measurement and the render disagreeing, so rows overlap or gaps open
|
|
234
|
+
as you scroll.
|
|
235
|
+
|
|
236
|
+
Correct:
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
useTMDataGrid({ data, columns, meta: { rowHeight: 64 } });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Source: `src/docs/styling.md` (The size scale).
|
|
243
|
+
|
|
244
|
+
### HIGH A capability check without `features`
|
|
245
|
+
|
|
246
|
+
`getGridCapabilities` and `getColumnCapabilities` both take `features` as a
|
|
247
|
+
second argument because it is the value that changes. Calling
|
|
248
|
+
`column.getCanSort()` directly in a custom header or toolbar button memoizes
|
|
249
|
+
against the column identity under the React Compiler, and the control keeps
|
|
250
|
+
rendering after its option was switched off.
|
|
251
|
+
|
|
252
|
+
Wrong:
|
|
253
|
+
|
|
254
|
+
```tsx
|
|
255
|
+
if (!column.getCanSort()) return null;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Correct:
|
|
259
|
+
|
|
260
|
+
```tsx
|
|
261
|
+
const { canSort } = getColumnCapabilities(column, features);
|
|
262
|
+
if (!canSort) return null;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Source: `src/docs/toolbar.md` (Why `features` is a second argument).
|
|
266
|
+
|
|
267
|
+
### MEDIUM An inline `labels` object
|
|
268
|
+
|
|
269
|
+
The chrome re-renders when the labels object changes identity, and an inline
|
|
270
|
+
literal is a new object every render. Keep it at module scope, or `useMemo` it.
|
|
271
|
+
|
|
272
|
+
Wrong:
|
|
273
|
+
|
|
274
|
+
```tsx
|
|
275
|
+
useTMDataGrid({ data, columns, labels: { noResults: "Inga träffar" } });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Correct:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
const labels = { noResults: "Inga träffar" } satisfies TMDataGridLabelsOverride;
|
|
282
|
+
|
|
283
|
+
useTMDataGrid({ data, columns, labels });
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Source: `src/docs/localization.md` (Keep the object stable).
|
|
287
|
+
|
|
288
|
+
### MEDIUM Conditionally rendering built-in toolbar parts
|
|
289
|
+
|
|
290
|
+
Each built-in already renders nothing when its feature is off. Wrapping them in
|
|
291
|
+
your own checks duplicates the capability logic, and the two drift apart the
|
|
292
|
+
first time an option changes.
|
|
293
|
+
|
|
294
|
+
Source: `src/docs/toolbar.md` (The built-in parts).
|
|
295
|
+
|
|
296
|
+
## Reference
|
|
297
|
+
|
|
298
|
+
| Name | Kind | Type | Default | What it does |
|
|
299
|
+
| --- | --- | --- | --- | --- |
|
|
300
|
+
| `size` | Prop | `MantineSize` | `"md"` | The whole density scale. |
|
|
301
|
+
| `className` · `style` · `id` | Props | – | – | Set on the root element. `style` takes the `--dg-*` variables. |
|
|
302
|
+
| `meta.rowHeight` | Option | `number` | From `size` | A row height outside the scale. |
|
|
303
|
+
| `SIZE_ROW_HEIGHT` | Export | `Record<MantineSize, number>` | – | The row heights the scale table lists. |
|
|
304
|
+
| `SIZE_CONTROL_SIZE` | Export | `Record<MantineSize, MantineSize>` | – | Which control size each grid size uses. |
|
|
305
|
+
| `DEFAULT_TMDATAGRID_SIZE` | Export | `"md"` | – | The default size. |
|
|
306
|
+
| `TMDataGrid.Toolbar` · `Spacer` | Components | `children` | – | The flex row, and the push-right. |
|
|
307
|
+
| `useTMDataGridContext` | Hook | `() => TMDataGridContextValue` | – | `{ table, ui, features, labels, controlSize, resetSettings }`. |
|
|
308
|
+
| `getGridCapabilities` | Export | `(table, features) => TMDataGridCapabilities` | – | What this grid can do, reactively. |
|
|
309
|
+
| `getColumnCapabilities` | Export | `(column, features) => TMDataGridColumnCapabilities` | – | The same for one column. |
|
|
310
|
+
| `readFeatureFlags` | Export | `(options) => TMDataGridFeatureFlags` | – | Derives the flags from an options object. |
|
|
311
|
+
| `labels` | Option | `TMDataGridLabelsOverride` | English | Any subset, merged over the defaults. Keep it stable. |
|
|
312
|
+
| `grid.labels` | Hook return | `TMDataGridLabels` | – | The resolved dictionary. |
|
|
313
|
+
| `TMDATAGRID_LABELS_EN` · `TMDATAGRID_LABELS_SV` | Exports | `TMDataGridLabels` | – | The English base, and a complete Swedish dictionary. |
|
|
314
|
+
| `TMDataGridLabels` · `TMDataGridLabelsOverride` | Exports | types | – | The full dictionary, and a partial one. |
|
|
315
|
+
| `mergeLabels` | Export | `(base, override) => TMDataGridLabels` | – | The merge, for composing dictionaries. |
|
|
316
|
+
| `meta.noResultsLabel` | Option | `string` | `labels.noResults` | Per-instance override of the filtered-empty message. |
|
|
317
|
+
|
|
318
|
+
The CSS layer name `tmdatagrid` in `styles.layer.css` is public API and never
|
|
319
|
+
changes.
|
|
320
|
+
|
|
321
|
+
See also: the `rows` skill for `--row-bg` and per-row styling, and
|
|
322
|
+
`getting-started` for the component catalog.
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cell-selection
|
|
3
|
+
description: >
|
|
4
|
+
Cell cursor, ranges, clipboard and CSV export in TMDataGrid. Covers the
|
|
5
|
+
cellSelection option and its none / single / range modes, the keyboard map
|
|
6
|
+
(arrows, Shift+arrows, PageUp/PageDown, Home/End, Enter, F2, Escape, Space,
|
|
7
|
+
Ctrl+C), the one-tab-stop rule and useCellControlTabIndex for controls inside
|
|
8
|
+
cells, the role flip from table/cell to grid/gridcell, ui.state.focusedCell
|
|
9
|
+
and ui.state.cellRange keyed by id, onFocusedCellChange, the copy and export
|
|
10
|
+
context menu, the cellExport options and their Nordic Excel defaults, and
|
|
11
|
+
exportGridToCsv over every filtered row. Load when adding keyboard cell
|
|
12
|
+
navigation, selecting blocks of cells, copying to a spreadsheet, exporting
|
|
13
|
+
CSV, or when Tab walks through controls inside the grid body.
|
|
14
|
+
metadata:
|
|
15
|
+
type: core
|
|
16
|
+
library: '@jielga/tmdatagrid'
|
|
17
|
+
library_version: '1.0.2'
|
|
18
|
+
sources:
|
|
19
|
+
- 'Jielga/TMDataGrid:src/docs/cell-selection.md'
|
|
20
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/cellNavigation.ts'
|
|
21
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/cellRange.ts'
|
|
22
|
+
- 'Jielga/TMDataGrid:src/tmdatagrid/core/cellExport.ts'
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# TMDataGrid - Cell selection
|
|
26
|
+
|
|
27
|
+
A cell cursor the arrow keys move, a rectangle of cells that can be dragged out,
|
|
28
|
+
and a Ctrl+C that pastes into Excel as cells rather than as one string. Off by
|
|
29
|
+
default.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
const grid = useTMDataGrid({ data, columns, cellSelection: "range" });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Mode | What it gives |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `"none"` (default) | Nothing |
|
|
38
|
+
| `"single"` | One focused cell, moved with the arrow keys |
|
|
39
|
+
| `"range"` | As `"single"`, plus a rectangle, Ctrl+C and the export menu |
|
|
40
|
+
|
|
41
|
+
Setting `editMode` defaults `cellSelection` to `"single"`, since editing
|
|
42
|
+
navigates by cursor. An explicit `cellSelection` always wins.
|
|
43
|
+
|
|
44
|
+
Turning it on changes three things about the body:
|
|
45
|
+
|
|
46
|
+
- The tab stop moves from the row to a cell, so the whole grid is **one** Tab
|
|
47
|
+
stop and the arrow keys walk it.
|
|
48
|
+
- The grid reports itself as a `grid` of `gridcell`s rather than a `table` of
|
|
49
|
+
`cell`s, which is what tells a screen reader those keys are live.
|
|
50
|
+
- The focused cell takes `data-focused`, selected ones `data-selected` and
|
|
51
|
+
`data-edge-*`.
|
|
52
|
+
|
|
53
|
+
## Keys
|
|
54
|
+
|
|
55
|
+
| Key | Does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| Arrows | Moves one cell, clamped at the edges, no wrapping |
|
|
58
|
+
| Shift+arrows | Extends the rectangle from its anchor (`"range"`) |
|
|
59
|
+
| PageUp / PageDown | Moves one viewport of rows |
|
|
60
|
+
| Home / End | First / last cell of the row |
|
|
61
|
+
| Ctrl+Home / Ctrl+End | First / last cell of the grid |
|
|
62
|
+
| Enter or F2 | Steps into the cell - its checkbox, link or button |
|
|
63
|
+
| Escape | Steps back out, or drops the rectangle to the focused cell |
|
|
64
|
+
| Space | Selects the row, as a row click does under `selectionMode: "row"` |
|
|
65
|
+
| Ctrl+C | Copies the selection as tab-separated text |
|
|
66
|
+
|
|
67
|
+
Enter and F2 are the pair a cell editor takes over. Until then they focus the
|
|
68
|
+
first control in the cell, and the arrow keys go quiet while focus is in there -
|
|
69
|
+
a cell's contents own their own keys.
|
|
70
|
+
|
|
71
|
+
## One tab stop
|
|
72
|
+
|
|
73
|
+
Tab from a cell leaves the grid. What makes that true is that controls inside
|
|
74
|
+
body cells - the checkbox, the tree chevron, the details chevron - take
|
|
75
|
+
`tabindex="-1"` while cell selection is on. Left tabbable, Tab would walk through
|
|
76
|
+
one per mounted row, and how many that is depends on the scroll position.
|
|
77
|
+
|
|
78
|
+
They stay reachable: Enter or F2 steps in, Escape steps out, and Space ticks the
|
|
79
|
+
row from any of its cells. Header controls are untouched - the header row is not
|
|
80
|
+
part of cell navigation.
|
|
81
|
+
|
|
82
|
+
A custom cell with a control in it wants the same treatment:
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
import { useCellControlTabIndex } from "@jielga/tmdatagrid";
|
|
86
|
+
|
|
87
|
+
const OpenButton = ({ row }) => (
|
|
88
|
+
<Button tabIndex={useCellControlTabIndex()} onClick={() => open(row.id)}>
|
|
89
|
+
Open
|
|
90
|
+
</Button>
|
|
91
|
+
);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The hook returns `-1` while cell selection is on and `0` otherwise, so the same
|
|
95
|
+
cell works either way.
|
|
96
|
+
|
|
97
|
+
## Where the selection lives
|
|
98
|
+
|
|
99
|
+
`ui.state.focusedCell` is a `{ rowId, columnId }` pair, and `ui.state.cellRange`
|
|
100
|
+
is two of them, the anchor and the moving corner. **Ids rather than indices**, so
|
|
101
|
+
sorting, filtering and column reordering carry the selection with the cells
|
|
102
|
+
instead of leaving it over whatever slid into those positions. A range whose
|
|
103
|
+
corner is filtered away paints nothing, and comes back when the filter lifts.
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
const focusedCell = useSelector(grid.ui, (state) => state.focusedCell);
|
|
107
|
+
|
|
108
|
+
grid.ui.actions.setFocusedCell({ rowId: "42", columnId: "salary" });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
One rectangle at a time; Ctrl+drag for a second, disjoint block is not
|
|
112
|
+
supported.
|
|
113
|
+
|
|
114
|
+
The generated lanes (checkbox, tree, details) are part of the selection - they
|
|
115
|
+
take the tint and the outline so the block stays a rectangle - but are never
|
|
116
|
+
*exported*, since they hold controls rather than values. A block covering
|
|
117
|
+
nothing else has nothing to copy, and the Copy and Export items say so by being
|
|
118
|
+
disabled.
|
|
119
|
+
|
|
120
|
+
## Copy and export
|
|
121
|
+
|
|
122
|
+
Ctrl+C puts the block on the clipboard as tab-separated text with CRLF between
|
|
123
|
+
rows, the format Excel, Sheets and Numbers all produce themselves. Values only:
|
|
124
|
+
Excel's own copy carries no header row either.
|
|
125
|
+
|
|
126
|
+
Right-clicking inside the selection opens Copy, "Export as CSV for Excel" and an
|
|
127
|
+
"Include headers" toggle. A right-click outside it moves the selection there
|
|
128
|
+
first, the way a spreadsheet does. Your own `rowContextMenu` items are appended
|
|
129
|
+
below a divider, so nothing is lost by turning cell selection on.
|
|
130
|
+
|
|
131
|
+
The CSV is written for a Nordic Excel: a `sep=;` first line, a UTF-8 BOM, CRLF
|
|
132
|
+
endings, semicolons between fields and a comma as the decimal mark. That
|
|
133
|
+
combination is what makes the file open straight into columns with å ä ö intact.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
<TMDataGrid.Table
|
|
137
|
+
cellExport={{ separator: ",", decimalComma: false, fileName: "employees" }}
|
|
138
|
+
/>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
What gets written is the cell's **value**, not what it renders. Dates come out
|
|
142
|
+
in the `sv-SE` form (`2026-07-31`), which Excel reads as a date.
|
|
143
|
+
|
|
144
|
+
`exportGridToCsv({ table, options })` takes **every filtered row** instead of the
|
|
145
|
+
selected block, with the same options and defaults. There is no built-in button
|
|
146
|
+
for it:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<Button onClick={() => exportGridToCsv({ table: grid.table })}>Export</Button>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Common mistakes
|
|
153
|
+
|
|
154
|
+
### CRITICAL A custom cell control that breaks the single tab stop
|
|
155
|
+
|
|
156
|
+
A `<Button>` or `<Checkbox>` rendered in a body cell keeps its default tab index,
|
|
157
|
+
so Tab walks through one per mounted row. How many that is depends on the scroll
|
|
158
|
+
position, which makes the bug look intermittent.
|
|
159
|
+
|
|
160
|
+
Wrong:
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
const OpenButton = ({ row }) => <Button onClick={() => open(row.id)}>Open</Button>;
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Correct:
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
const OpenButton = ({ row }) => (
|
|
170
|
+
<Button tabIndex={useCellControlTabIndex()} onClick={() => open(row.id)}>
|
|
171
|
+
Open
|
|
172
|
+
</Button>
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Source: `src/docs/cell-selection.md` (One tab stop).
|
|
177
|
+
|
|
178
|
+
### HIGH Selectors written for `table` / `cell` roles
|
|
179
|
+
|
|
180
|
+
The grid reports `grid` and `gridcell` once cell selection is on, so a test or a
|
|
181
|
+
query written against `getByRole("cell")` stops resolving the moment the option
|
|
182
|
+
is set - including when `editMode` turns it on implicitly.
|
|
183
|
+
|
|
184
|
+
Source: `src/docs/cell-selection.md`, and the `testing` skill.
|
|
185
|
+
|
|
186
|
+
### HIGH Reading the selection as row and column indices
|
|
187
|
+
|
|
188
|
+
`focusedCell` and `cellRange` hold `{ rowId, columnId }`. Code that converts them
|
|
189
|
+
to indices to look values up gets the wrong cell after any sort, filter or column
|
|
190
|
+
move - which is exactly what ids were chosen to avoid.
|
|
191
|
+
|
|
192
|
+
Correct:
|
|
193
|
+
|
|
194
|
+
```tsx
|
|
195
|
+
const row = grid.table.getRow(focusedCell.rowId);
|
|
196
|
+
const value = row.getValue(focusedCell.columnId);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Source: `src/docs/cell-selection.md` (Where the selection lives).
|
|
200
|
+
|
|
201
|
+
### MEDIUM Expecting the export to match what the cells show
|
|
202
|
+
|
|
203
|
+
A cell renders React - often a badge, a link or a formatted string - and the
|
|
204
|
+
export writes the underlying value. A currency cell showing `32 000 kr` exports
|
|
205
|
+
`32000`. Format in the data, or post-process the matrix from `buildCellMatrix`,
|
|
206
|
+
if the file must match the screen.
|
|
207
|
+
|
|
208
|
+
Source: `src/docs/cell-selection.md` (The CSV).
|
|
209
|
+
|
|
210
|
+
### MEDIUM Expecting headers in the clipboard
|
|
211
|
+
|
|
212
|
+
Ctrl+C copies values only, matching Excel's own copy. Headers are an option of
|
|
213
|
+
the export menu and of `cellExport`, not of the clipboard path.
|
|
214
|
+
|
|
215
|
+
Source: `src/docs/cell-selection.md` (Copy and export).
|
|
216
|
+
|
|
217
|
+
## Reference
|
|
218
|
+
|
|
219
|
+
| Name | Kind | Type | Default | What it does |
|
|
220
|
+
| --- | --- | --- | --- | --- |
|
|
221
|
+
| `cellSelection` | Option | `"none" \| "single" \| "range"` | `"none"`, or `"single"` under `editMode` | Turns the cursor, and the rectangle, on. |
|
|
222
|
+
| `onFocusedCellChange` | Callback | `(cell \| null) => void` | – | Follows the cursor. |
|
|
223
|
+
| `cellExport` | Table prop | `TMDataGridCellExportOptions` | Nordic Excel | Separator, decimal mark, headers, file name. |
|
|
224
|
+
| `ui.state.focusedCell` | UI state | `{ rowId, columnId } \| null` | `null` | The cursor. |
|
|
225
|
+
| `ui.state.cellRange` | UI state | `{ anchor, focus } \| null` | `null` | The rectangle's two corners. |
|
|
226
|
+
| `ui.actions.setFocusedCell` · `setCellRange` | UI actions | – | – | Move either from your own code. |
|
|
227
|
+
| `useCellControlTabIndex` | Hook | `() => 0 \| -1` | – | The tab index a control inside a body cell wants. |
|
|
228
|
+
| `exportGridToCsv` | Export | `({ table, options? }) => void` | – | Downloads every filtered row as CSV. |
|
|
229
|
+
| `buildGridCellMatrix` · `buildCellMatrix` | Exports | – | – | The value matrix behind the file, for post-processing. |
|
|
230
|
+
| `toClipboardText` · `toExcelCsv` · `writeClipboardText` · `downloadTextFile` | Exports | – | – | The pieces behind Ctrl+C and the file. |
|
|
231
|
+
| `formatExportValue` | Export | `(value, options) => string` | – | One value, formatted as the export would. |
|
|
232
|
+
| `DEFAULT_CELL_EXPORT_OPTIONS` | Export | object | – | The Nordic Excel defaults, to spread over. |
|
|
233
|
+
| `isSameCell` · `resolveCellMove` | Exports | – | – | The cursor arithmetic, for a custom navigator. |
|
|
234
|
+
| `resolveRangeBounds` · `isWithinBounds` · `boundsEdges` · `boundsCellCount` | Exports | – | – | The rectangle arithmetic. |
|
|
235
|
+
| `data-focused` | Data attribute | – | – | On the focused cell. |
|
|
236
|
+
| `data-edge-top` · `-bottom` · `-left` · `-right` | Data attributes | – | – | On cells at the rectangle's border. |
|
|
237
|
+
|
|
238
|
+
See also: the `editing` skill, which turns this on implicitly and takes over
|
|
239
|
+
Enter and F2, and the `rows` skill for `rowContextMenu`, whose items are
|
|
240
|
+
appended below the copy and export ones.
|