@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jielga/tmdatagrid",
3
- "version": "1.0.1",
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.