@jielga/tmdatagrid 0.2.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1337 -94
- package/dist/index.js +2812 -1189
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +61 -11
- package/skills/getting-started/SKILL.md +41 -6
- package/skills/options/SKILL.md +12 -2
- package/skills/server-side/SKILL.md +1 -1
- package/src/tmdatagrid/TMDataGridContext.ts +26 -0
- package/src/tmdatagrid/components/TMDataGrid.module.css +21 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +29 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +12 -6
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +192 -37
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +136 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +63 -43
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +159 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +18 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +249 -44
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +103 -16
- package/src/tmdatagrid/components/TMDataGridTable.module.css +160 -5
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1380 -43
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +33 -7
- package/src/tmdatagrid/components/icons.ts +6 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +122 -11
- package/src/tmdatagrid/core/cellExport.ts +321 -0
- package/src/tmdatagrid/core/cellNavigation.ts +103 -0
- package/src/tmdatagrid/core/cellRange.ts +118 -0
- package/src/tmdatagrid/core/columnOptions.ts +123 -0
- package/src/tmdatagrid/core/columnUtils.ts +18 -0
- package/src/tmdatagrid/core/expanding.ts +93 -0
- package/src/tmdatagrid/core/filterOperators.ts +198 -12
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/persistence.ts +12 -0
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +96 -2
- package/src/tmdatagrid/useTMDataGrid.tsx +558 -37
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { useCreateStore } from "@tanstack/react-store";
|
|
2
2
|
import {
|
|
3
|
+
aggregationFns,
|
|
3
4
|
type ColumnDef,
|
|
4
5
|
columnFacetingFeature,
|
|
5
6
|
columnFilteringFeature,
|
|
7
|
+
columnGroupingFeature,
|
|
6
8
|
columnOrderingFeature,
|
|
7
9
|
columnPinningFeature,
|
|
8
10
|
columnResizingFeature,
|
|
9
11
|
columnSizingFeature,
|
|
10
12
|
columnVisibilityFeature,
|
|
11
13
|
createColumnHelper,
|
|
14
|
+
createExpandedRowModel,
|
|
12
15
|
createFacetedMinMaxValues,
|
|
13
16
|
createFacetedRowModel,
|
|
14
17
|
createFacetedUniqueValues,
|
|
15
18
|
createFilteredRowModel,
|
|
19
|
+
createGroupedRowModel,
|
|
16
20
|
createPaginatedRowModel,
|
|
17
21
|
createSortedRowModel,
|
|
18
22
|
filterFns,
|
|
19
23
|
globalFilteringFeature,
|
|
20
24
|
metaHelper,
|
|
25
|
+
type Row,
|
|
21
26
|
type RowData,
|
|
27
|
+
rowExpandingFeature,
|
|
22
28
|
rowPaginationFeature,
|
|
23
29
|
rowSelectionFeature,
|
|
24
30
|
rowSortingFeature,
|
|
@@ -30,12 +36,18 @@ import {
|
|
|
30
36
|
useTable,
|
|
31
37
|
} from "@tanstack/react-table";
|
|
32
38
|
import type { Store } from "@tanstack/store";
|
|
33
|
-
import { useEffect, useMemo, useState } from "react";
|
|
39
|
+
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
40
|
+
import type { TMDataGridOptionsSource } from "./core/columnOptions";
|
|
34
41
|
import {
|
|
35
42
|
type TMDataGridColumnType,
|
|
36
43
|
tmDataGridFilterFn,
|
|
37
44
|
getDefaultOperator,
|
|
38
45
|
} from "./core/filterOperators";
|
|
46
|
+
import {
|
|
47
|
+
mergeLabels,
|
|
48
|
+
type TMDataGridLabels,
|
|
49
|
+
type TMDataGridLabelsOverride,
|
|
50
|
+
} from "./core/labels";
|
|
39
51
|
import {
|
|
40
52
|
hasPersistenceKeys,
|
|
41
53
|
readPersistedState,
|
|
@@ -44,13 +56,27 @@ import {
|
|
|
44
56
|
} from "./core/persistence";
|
|
45
57
|
import {
|
|
46
58
|
readFeatureFlags,
|
|
59
|
+
type TMDataGridCellSelectionMode,
|
|
47
60
|
type TMDataGridFeatureFlags,
|
|
48
|
-
type
|
|
61
|
+
type TMDataGridSelectionMode,
|
|
49
62
|
} from "./core/capabilities";
|
|
63
|
+
import {
|
|
64
|
+
isSameCell,
|
|
65
|
+
type TMDataGridCellPosition,
|
|
66
|
+
} from "./core/cellNavigation";
|
|
67
|
+
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
50
68
|
import {
|
|
51
69
|
createSelectColumn,
|
|
52
70
|
SELECT_COLUMN_ID,
|
|
53
71
|
} from "./components/TMDataGridSelectColumn";
|
|
72
|
+
import {
|
|
73
|
+
createGroupColumn,
|
|
74
|
+
GROUP_COLUMN_ID,
|
|
75
|
+
} from "./components/TMDataGridGroupColumn";
|
|
76
|
+
import {
|
|
77
|
+
createDetailsColumn,
|
|
78
|
+
DETAILS_COLUMN_ID,
|
|
79
|
+
} from "./components/TMDataGridDetailsColumn";
|
|
54
80
|
|
|
55
81
|
/**
|
|
56
82
|
* How long the grid waits after the last state change before writing to
|
|
@@ -63,11 +89,28 @@ const PERSIST_DEBOUNCE_MS = 200;
|
|
|
63
89
|
export type TMDataGridColumnMeta = {
|
|
64
90
|
/** Name shown in menus and the column manager. Falls back to a string header. */
|
|
65
91
|
label?: string;
|
|
66
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Drives which filter operators are offered and, once editing is on, which
|
|
94
|
+
* editor the cell opens. Defaults to `"string"`.
|
|
95
|
+
*/
|
|
67
96
|
type?: TMDataGridColumnType;
|
|
97
|
+
/**
|
|
98
|
+
* The choices of a `select` / `multiSelect` column — one declaration feeding
|
|
99
|
+
* the filter panel's value control and the cell editor alike. A static
|
|
100
|
+
* array, `"faceted"` (the distinct values present in the data), or a
|
|
101
|
+
* function of the table, column and — for editors — the row. See
|
|
102
|
+
* {@link TMDataGridOptionsSource}.
|
|
103
|
+
*/
|
|
104
|
+
options?: TMDataGridOptionsSource;
|
|
68
105
|
/** Share of the leftover width this column claims. Defaults to `1`. */
|
|
69
106
|
flex?: number;
|
|
70
107
|
align?: "left" | "right" | "center";
|
|
108
|
+
/**
|
|
109
|
+
* Size the column to its widest mounted content once, after the first rows
|
|
110
|
+
* render — unless a persisted or user-set width already covers it. The same
|
|
111
|
+
* measurement as double-clicking the resize divider; see `autosizeColumn`.
|
|
112
|
+
*/
|
|
113
|
+
autoSize?: boolean;
|
|
71
114
|
/**
|
|
72
115
|
* `false` keeps the column where it is: no header dragging, no move items.
|
|
73
116
|
* Column ordering is the one feature TanStack defines no column option for,
|
|
@@ -105,9 +148,16 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
105
148
|
columnSizingFeature,
|
|
106
149
|
columnResizingFeature,
|
|
107
150
|
columnFacetingFeature,
|
|
151
|
+
columnGroupingFeature,
|
|
152
|
+
// Registered for grouping's sake rather than for tree data: the grouped row
|
|
153
|
+
// model builds the parent rows, and this is what flattens the expanded ones
|
|
154
|
+
// back into the flat list the body virtualizes.
|
|
155
|
+
rowExpandingFeature,
|
|
108
156
|
|
|
109
157
|
filteredRowModel: createFilteredRowModel(),
|
|
158
|
+
groupedRowModel: createGroupedRowModel(),
|
|
110
159
|
sortedRowModel: createSortedRowModel(),
|
|
160
|
+
expandedRowModel: createExpandedRowModel(),
|
|
111
161
|
paginatedRowModel: createPaginatedRowModel(),
|
|
112
162
|
facetedRowModel: createFacetedRowModel(),
|
|
113
163
|
facetedMinMaxValues: createFacetedMinMaxValues(),
|
|
@@ -115,6 +165,10 @@ export const tmDataGridFeatures = tableFeatures({
|
|
|
115
165
|
|
|
116
166
|
filterFns: { ...filterFns, tmDataGrid: tmDataGridFilterFn },
|
|
117
167
|
sortFns,
|
|
168
|
+
// The names `columnDef.aggregationFn` accepts. Only consulted for columns
|
|
169
|
+
// that ask for one — a column with no `aggregationFn` reports `undefined` on
|
|
170
|
+
// a group row, which is what keeps a plain "group by" free of aggregates.
|
|
171
|
+
aggregationFns,
|
|
118
172
|
|
|
119
173
|
tableMeta: metaHelper<TMDataGridTableMeta>(),
|
|
120
174
|
columnMeta: metaHelper<TMDataGridColumnMeta>(),
|
|
@@ -131,6 +185,26 @@ export function createTMDataGridColumnHelper<TData extends RowData>() {
|
|
|
131
185
|
return createColumnHelper<TMDataGridFeatures, TData>();
|
|
132
186
|
}
|
|
133
187
|
|
|
188
|
+
/** What the `renderDetails` render prop is handed for an expanded row. */
|
|
189
|
+
export type TMDataGridDetailsArgs<TData extends RowData> = {
|
|
190
|
+
row: Row<TMDataGridFeatures, TData>;
|
|
191
|
+
table: TMDataGridTable<TData>;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Builds the panel shown underneath an expanded row. See `renderDetails`. */
|
|
195
|
+
export type TMDataGridDetailsRenderer<TData extends RowData> = (
|
|
196
|
+
args: TMDataGridDetailsArgs<TData>,
|
|
197
|
+
) => ReactNode;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What the virtualizer assumes a detail panel it has not measured yet is worth.
|
|
201
|
+
* Only a seed — every mounted row is measured, so the real height replaces it.
|
|
202
|
+
*/
|
|
203
|
+
const DEFAULT_DETAILS_EST_HEIGHT = 160;
|
|
204
|
+
|
|
205
|
+
/** Rows kept mounted on each side of the viewport. See `overscan`. */
|
|
206
|
+
const DEFAULT_OVERSCAN = 6;
|
|
207
|
+
|
|
134
208
|
/**
|
|
135
209
|
* Chrome state that is *not* table state: which panels are open, and which
|
|
136
210
|
* column opened the filter panel. Kept in a TanStack Store so consumers can
|
|
@@ -146,6 +220,39 @@ export type TMDataGridUiState = {
|
|
|
146
220
|
* `dataTransfer`, which browsers keep unreadable until the drop.
|
|
147
221
|
*/
|
|
148
222
|
draggedColumnId: string | null;
|
|
223
|
+
/**
|
|
224
|
+
* The single highlighted row — the one a detail panel would be showing. Its
|
|
225
|
+
* own concept, not a slice of `rowSelection`: under
|
|
226
|
+
* `selectionMode: "checkboxAndHighlight"` the two coexist, and TanStack's one
|
|
227
|
+
* selection map cannot hold both.
|
|
228
|
+
*
|
|
229
|
+
* Not pruned when the row is filtered out, paged away or dropped from `data`,
|
|
230
|
+
* matching how TanStack treats `rowSelection` — nothing there resets it
|
|
231
|
+
* either. The row simply renders unhighlighted, and highlights again if it
|
|
232
|
+
* comes back.
|
|
233
|
+
*/
|
|
234
|
+
highlightedRowId: string | null;
|
|
235
|
+
/** The pivot a shift-click extends from. See resolveRowSelectionClick. */
|
|
236
|
+
selectionAnchorRowId: string | null;
|
|
237
|
+
/**
|
|
238
|
+
* The cell the keyboard is on, under `enableCellSelection`. `null` until the
|
|
239
|
+
* grid is first entered — and again whenever a consumer clears it.
|
|
240
|
+
*
|
|
241
|
+
* The state is the source of truth and DOM focus follows it, not the other
|
|
242
|
+
* way around: under virtualization the cell it names is often not mounted,
|
|
243
|
+
* which is exactly what a coordinate has to survive. Held as ids for the
|
|
244
|
+
* same reason — see {@link TMDataGridCellPosition}.
|
|
245
|
+
*/
|
|
246
|
+
focusedCell: TMDataGridCellPosition | null;
|
|
247
|
+
/**
|
|
248
|
+
* The selected rectangle, under `cellSelection: "range"`. Held as the two
|
|
249
|
+
* cells that span it — see {@link TMDataGridCellRange}.
|
|
250
|
+
*
|
|
251
|
+
* Always covers the focused cell: every gesture that moves the focus either
|
|
252
|
+
* extends the rectangle or collapses it onto the new cell, so the two never
|
|
253
|
+
* describe different places.
|
|
254
|
+
*/
|
|
255
|
+
cellRange: TMDataGridCellRange | null;
|
|
149
256
|
};
|
|
150
257
|
|
|
151
258
|
export type TMDataGridUiActions = {
|
|
@@ -155,6 +262,25 @@ export type TMDataGridUiActions = {
|
|
|
155
262
|
toggleColumnsPanel: () => void;
|
|
156
263
|
startColumnDrag: (columnId: string) => void;
|
|
157
264
|
endColumnDrag: () => void;
|
|
265
|
+
/**
|
|
266
|
+
* Moves the active row, or clears it with `null` — which is how a consumer
|
|
267
|
+
* closing its detail panel puts the grid back in step.
|
|
268
|
+
*/
|
|
269
|
+
setHighlightedRow: (rowId: string | null) => void;
|
|
270
|
+
setSelectionAnchor: (rowId: string | null) => void;
|
|
271
|
+
/**
|
|
272
|
+
* Moves the focused cell, or clears it with `null`. DOM focus follows while
|
|
273
|
+
* the grid holds it — so this both moves the keyboard and, when the row is
|
|
274
|
+
* off screen, scrolls it into view. Called for every arrow key, and available
|
|
275
|
+
* for a consumer putting the keyboard somewhere itself.
|
|
276
|
+
*/
|
|
277
|
+
setFocusedCell: (cell: TMDataGridCellPosition | null) => void;
|
|
278
|
+
/**
|
|
279
|
+
* Sets the selected rectangle, or clears it with `null`. Does not move the
|
|
280
|
+
* focused cell — the two are set together by the gestures that change both,
|
|
281
|
+
* which is what keeps "extend the selection" and "move the cursor" separable.
|
|
282
|
+
*/
|
|
283
|
+
setCellRange: (range: TMDataGridCellRange | null) => void;
|
|
158
284
|
};
|
|
159
285
|
|
|
160
286
|
export type TMDataGridUiStore = Store<TMDataGridUiState, TMDataGridUiActions>;
|
|
@@ -165,6 +291,14 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
165
291
|
ui: TMDataGridUiStore;
|
|
166
292
|
/** Table-level feature switches, re-read from options on every render. */
|
|
167
293
|
features: TMDataGridFeatureFlags;
|
|
294
|
+
/** Every string the chrome renders, `labels` merged over the English defaults. */
|
|
295
|
+
labels: TMDataGridLabels;
|
|
296
|
+
/** The detail renderer, when row details are on. See `renderDetails`. */
|
|
297
|
+
renderDetails?: TMDataGridDetailsRenderer<TData>;
|
|
298
|
+
/** Detail estimate in px: the option, or {@link DEFAULT_DETAILS_EST_HEIGHT}. */
|
|
299
|
+
renderDetailsEstHeight: number;
|
|
300
|
+
/** Virtualizer overscan: the option, or {@link DEFAULT_OVERSCAN}. */
|
|
301
|
+
overscan: number;
|
|
168
302
|
};
|
|
169
303
|
|
|
170
304
|
export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
@@ -179,6 +313,20 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
179
313
|
* dependency of the subscription that writes back.
|
|
180
314
|
*/
|
|
181
315
|
persist?: TMDataGridPersistence;
|
|
316
|
+
/**
|
|
317
|
+
* Overrides for the grid's strings — menu items, panels, the pager, and
|
|
318
|
+
* every `aria-label`. Any subset, merged over the English defaults; a full
|
|
319
|
+
* Swedish dictionary ships as `TMDATAGRID_LABELS_SV`.
|
|
320
|
+
*
|
|
321
|
+
* ```tsx
|
|
322
|
+
* useTMDataGrid({ data, columns, labels: TMDATAGRID_LABELS_SV });
|
|
323
|
+
* useTMDataGrid({ data, columns, labels: { noResults: "Inga rader" } });
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* Keep the object referentially stable (module scope or `useMemo`) — the
|
|
327
|
+
* chrome re-renders when its identity changes.
|
|
328
|
+
*/
|
|
329
|
+
labels?: TMDataGridLabelsOverride;
|
|
182
330
|
/**
|
|
183
331
|
* Header drag-and-drop and the move items in the column menu. Defaults to
|
|
184
332
|
* `true`.
|
|
@@ -201,18 +349,29 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
201
349
|
/**
|
|
202
350
|
* How rows are selected. Defaults to `"checkbox"`.
|
|
203
351
|
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
352
|
+
* | Mode | Checkbox column | Row click |
|
|
353
|
+
* | ---- | --------------- | --------- |
|
|
354
|
+
* | `"checkbox"` | yes, multi-select | nothing |
|
|
355
|
+
* | `"row"` | no | multi-selects, Ctrl/Shift modifiers |
|
|
356
|
+
* | `"checkboxAndHighlight"` | yes, multi-select | highlights one row |
|
|
357
|
+
* | `"highlight"` | no | highlights one row, no selection at all |
|
|
208
358
|
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
359
|
+
* One option rather than two, so the combination that cannot work — a click
|
|
360
|
+
* that both toggles a multi-selection and moves the highlight — is not
|
|
361
|
+
* expressible.
|
|
362
|
+
*
|
|
363
|
+
* The first two write to TanStack's `rowSelection`. The highlight is separate
|
|
364
|
+
* state, which is what lets `"checkboxAndHighlight"` run both at once: tick
|
|
365
|
+
* rows for a bulk action, click one to open its detail panel. See
|
|
366
|
+
* `defaultHighlightedRowId` / `onHighlightedRowChange`.
|
|
367
|
+
*
|
|
368
|
+
* `enableRowSelection` still gates the selection half, predicate form
|
|
369
|
+
* included; under `"highlight"` there is nothing for it to gate.
|
|
211
370
|
*/
|
|
212
|
-
|
|
371
|
+
selectionMode?: TMDataGridSelectionMode;
|
|
213
372
|
/**
|
|
214
373
|
* Give selected rows the highlight background. Defaults to `true` under
|
|
215
|
-
* `
|
|
374
|
+
* `selectionMode: "row"`, where the highlight is the only feedback a click
|
|
216
375
|
* gives, and `false` under `"checkbox"`, where the box already shows it.
|
|
217
376
|
*
|
|
218
377
|
* The colour is the `--dg-row-selected-bg` CSS variable, so it can be changed
|
|
@@ -225,7 +384,138 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
225
384
|
* />
|
|
226
385
|
* ```
|
|
227
386
|
*/
|
|
228
|
-
|
|
387
|
+
showSelectedBackground?: boolean;
|
|
388
|
+
/**
|
|
389
|
+
* The row that starts out highlighted, under a `selectionMode` that has a
|
|
390
|
+
* highlight. Read once on mount, like `initialState`.
|
|
391
|
+
*
|
|
392
|
+
* The grid never persists the highlighted row. Pair this with
|
|
393
|
+
* {@link onHighlightedRowChange} and keep it wherever it belongs — for a
|
|
394
|
+
* detail panel that is usually the route, which gets you a shareable link and
|
|
395
|
+
* a working back button as well as surviving a reload:
|
|
396
|
+
*
|
|
397
|
+
* ```tsx
|
|
398
|
+
* const { rowId } = useParams();
|
|
399
|
+
* useTMDataGrid({
|
|
400
|
+
* selectionMode: "highlight",
|
|
401
|
+
* defaultHighlightedRowId: rowId,
|
|
402
|
+
* onHighlightedRowChange: (id) => navigate(id ? `/employees/${id}` : "/employees"),
|
|
403
|
+
* });
|
|
404
|
+
* ```
|
|
405
|
+
*/
|
|
406
|
+
defaultHighlightedRowId?: string | null;
|
|
407
|
+
/**
|
|
408
|
+
* Called with the newly highlighted row id, or `null` when it is cleared.
|
|
409
|
+
* Fires for `ui.actions.setHighlightedRow` too, not only for clicks.
|
|
410
|
+
*/
|
|
411
|
+
onHighlightedRowChange?: (rowId: string | null) => void;
|
|
412
|
+
/**
|
|
413
|
+
* How cells are selected. Defaults to `"none"`.
|
|
414
|
+
*
|
|
415
|
+
* | Mode | What it gives |
|
|
416
|
+
* | ---- | ------------- |
|
|
417
|
+
* | `"none"` | nothing; the body's tab stop stays on the row |
|
|
418
|
+
* | `"single"` | one focused cell, moved with the arrow keys |
|
|
419
|
+
* | `"range"` | as `"single"`, plus a rectangle of cells |
|
|
420
|
+
*
|
|
421
|
+
* Under either live mode:
|
|
422
|
+
*
|
|
423
|
+
* | Key | Moves to |
|
|
424
|
+
* | --- | --- |
|
|
425
|
+
* | Arrows | the neighbouring cell, clamped at the edges |
|
|
426
|
+
* | PageUp / PageDown | one viewport of rows |
|
|
427
|
+
* | Home / End | first / last cell of the row |
|
|
428
|
+
* | Ctrl+Home / Ctrl+End | first / last cell of the grid |
|
|
429
|
+
* | Enter or F2 | into the cell — the checkbox, link or button it holds |
|
|
430
|
+
* | Escape | back out to the cell |
|
|
431
|
+
* | Space | selects the row, as it does in row-selection mode |
|
|
432
|
+
*
|
|
433
|
+
* And under `"range"`, additionally:
|
|
434
|
+
*
|
|
435
|
+
* | Gesture | Does |
|
|
436
|
+
* | ------- | ---- |
|
|
437
|
+
* | Drag across cells | selects the rectangle they span |
|
|
438
|
+
* | Shift+click, Shift+arrows | extends the rectangle from its anchor |
|
|
439
|
+
* | Ctrl+C | copies it as tab-separated text — paste lands in Excel's cells |
|
|
440
|
+
* | Right-click inside it | offers the CSV export, headers optional |
|
|
441
|
+
*
|
|
442
|
+
* Off, nothing about the body changes. On, three things do: the body's tab
|
|
443
|
+
* stop moves from the row to a cell, so the whole grid is one Tab stop; the
|
|
444
|
+
* grid reports itself as a `grid` of `gridcell`s rather than a `table` of
|
|
445
|
+
* `cell`s, which is what tells a screen reader the arrow keys are live; and
|
|
446
|
+
* the cells take `data-focused` / `data-selected` for the ring and the tint.
|
|
447
|
+
*
|
|
448
|
+
* The state is `ui.state.focusedCell` and `ui.state.cellRange`, and moving it
|
|
449
|
+
* is `ui.actions.setFocusedCell` / `setCellRange` — so a consumer can put the
|
|
450
|
+
* keyboard on a cell, or follow it:
|
|
451
|
+
*
|
|
452
|
+
* ```tsx
|
|
453
|
+
* const grid = useTMDataGrid({
|
|
454
|
+
* data,
|
|
455
|
+
* columns,
|
|
456
|
+
* cellSelection: "range",
|
|
457
|
+
* onFocusedCellChange: (cell) => setStatus(cell?.columnId ?? ""),
|
|
458
|
+
* });
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* Ids rather than a row/column index pair, so sorting, filtering and column
|
|
462
|
+
* reordering move the selection with the cells instead of leaving it over
|
|
463
|
+
* whatever slid into those positions.
|
|
464
|
+
*/
|
|
465
|
+
cellSelection?: TMDataGridCellSelectionMode;
|
|
466
|
+
/**
|
|
467
|
+
* Called with the newly focused cell, or `null` when it is cleared. Fires for
|
|
468
|
+
* `ui.actions.setFocusedCell` too, not only for keys and clicks.
|
|
469
|
+
*/
|
|
470
|
+
onFocusedCellChange?: (cell: TMDataGridCellPosition | null) => void;
|
|
471
|
+
/**
|
|
472
|
+
* Renders a panel underneath an expanded row, spanning every column. Setting
|
|
473
|
+
* it is what turns row details on.
|
|
474
|
+
*
|
|
475
|
+
* Which rows are open is TanStack's own `expanded` state, so opening one is
|
|
476
|
+
* `row.toggleExpanded()` from wherever suits — a chevron in a cell, a button
|
|
477
|
+
* in the context menu, a double-click:
|
|
478
|
+
*
|
|
479
|
+
* ```tsx
|
|
480
|
+
* const grid = useTMDataGrid({
|
|
481
|
+
* data,
|
|
482
|
+
* columns,
|
|
483
|
+
* renderDetails: ({ row }) => <EmployeeCard employee={row.original} />,
|
|
484
|
+
* });
|
|
485
|
+
* ```
|
|
486
|
+
*
|
|
487
|
+
* The panel is as tall as what it renders — see {@link renderDetailsEstHeight}
|
|
488
|
+
* for what the virtualizer assumes before it has measured one.
|
|
489
|
+
*
|
|
490
|
+
* Group rows are left out: expanding one opens its children, and a panel there
|
|
491
|
+
* would be about an arbitrary one of them. The same reason they sit out
|
|
492
|
+
* `onRowClick`.
|
|
493
|
+
*
|
|
494
|
+
* Row details and `selectionMode: "highlight"` are two answers to the same
|
|
495
|
+
* question. Details keep the record in place and in context, which suits a few
|
|
496
|
+
* fields or an action strip; a highlight-driven side panel has room for more
|
|
497
|
+
* and survives scrolling. Nothing stops a grid from doing both.
|
|
498
|
+
*/
|
|
499
|
+
renderDetails?: TMDataGridDetailsRenderer<TData>;
|
|
500
|
+
/**
|
|
501
|
+
* What the virtualizer assumes an unmeasured detail panel is worth, in px.
|
|
502
|
+
* Defaults to 160.
|
|
503
|
+
*
|
|
504
|
+
* An estimate, not a height: every mounted row is measured, so the real one
|
|
505
|
+
* takes over as soon as the panel is on screen. It keeps the scrollbar honest
|
|
506
|
+
* for panels that open off screen — restored `expanded` state, say — and being
|
|
507
|
+
* roughly right is enough.
|
|
508
|
+
*/
|
|
509
|
+
renderDetailsEstHeight?: number;
|
|
510
|
+
/**
|
|
511
|
+
* Rows the virtualizer keeps mounted above and below the viewport. Defaults
|
|
512
|
+
* to 6.
|
|
513
|
+
*
|
|
514
|
+
* Raise it to trade memory for a scroll that stays painted — fast wheel or
|
|
515
|
+
* touch flings can outrun the virtualizer and flash blank rows, and a larger
|
|
516
|
+
* buffer covers the gap. Lower it when rows are expensive to render.
|
|
517
|
+
*/
|
|
518
|
+
overscan?: number;
|
|
229
519
|
};
|
|
230
520
|
|
|
231
521
|
type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
@@ -235,8 +525,17 @@ type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
|
235
525
|
>;
|
|
236
526
|
|
|
237
527
|
/**
|
|
238
|
-
* Point every column at the operator-dispatching filter function
|
|
239
|
-
* column opted into its
|
|
528
|
+
* Point every column at the operator-dispatching filter function, and take
|
|
529
|
+
* grouping's aggregation defaults back off, unless the column opted into its
|
|
530
|
+
* own. Anything the consumer set wins — it is spread over these.
|
|
531
|
+
*
|
|
532
|
+
* The aggregation pair needs explaining. TanStack's grouping feature hands
|
|
533
|
+
* every column `aggregationFn: "auto"` and an `aggregatedCell` that stringifies
|
|
534
|
+
* whatever comes out, so merely registering the feature would have every
|
|
535
|
+
* numeric column silently sum itself and every date column show a range the
|
|
536
|
+
* moment anything is grouped. That is a summary table, and "group by" is not a
|
|
537
|
+
* request for one. Cleared here, so a grouped grid is a tree until a column
|
|
538
|
+
* says otherwise — `aggregationFn: "sum"` on the column that wants it.
|
|
240
539
|
*/
|
|
241
540
|
function withTMDataGridDefaults<TData extends RowData>(
|
|
242
541
|
columns: ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
@@ -248,7 +547,12 @@ function withTMDataGridDefaults<TData extends RowData>(
|
|
|
248
547
|
columns: withTMDataGridDefaults<TData>(column.columns),
|
|
249
548
|
};
|
|
250
549
|
}
|
|
251
|
-
return {
|
|
550
|
+
return {
|
|
551
|
+
filterFn: "tmDataGrid",
|
|
552
|
+
aggregationFn: undefined,
|
|
553
|
+
aggregatedCell: undefined,
|
|
554
|
+
...column,
|
|
555
|
+
} as TMDataGridColumnDef<TData>;
|
|
252
556
|
});
|
|
253
557
|
}
|
|
254
558
|
|
|
@@ -266,50 +570,146 @@ function withTMDataGridDefaults<TData extends RowData>(
|
|
|
266
570
|
export function useTMDataGrid<TData extends RowData>({
|
|
267
571
|
persist,
|
|
268
572
|
// Not TanStack options, so they are kept out of what `useTable` receives.
|
|
573
|
+
labels: labelsOverride,
|
|
269
574
|
enableColumnOrdering,
|
|
270
575
|
enablePagination,
|
|
271
|
-
|
|
272
|
-
|
|
576
|
+
selectionMode,
|
|
577
|
+
showSelectedBackground,
|
|
578
|
+
defaultHighlightedRowId,
|
|
579
|
+
onHighlightedRowChange,
|
|
580
|
+
cellSelection,
|
|
581
|
+
onFocusedCellChange,
|
|
582
|
+
renderDetails,
|
|
583
|
+
renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
|
|
584
|
+
overscan = DEFAULT_OVERSCAN,
|
|
273
585
|
...options
|
|
274
586
|
}: UseTMDataGridOptions<TData>): TMDataGridApi<TData> {
|
|
275
|
-
|
|
587
|
+
// Derived up here, rather than just before the return, because the rest of the
|
|
588
|
+
// hook needs `selectColumn` — one place decides what each mode means.
|
|
589
|
+
//
|
|
590
|
+
// Deliberately not memoized on `table`: the flags must re-derive whenever the
|
|
591
|
+
// caller passes different options, and `table` keeps the same identity when
|
|
592
|
+
// they do. See readFeatureFlags.
|
|
593
|
+
const features = readFeatureFlags({
|
|
594
|
+
...options,
|
|
595
|
+
enableColumnOrdering,
|
|
596
|
+
enablePagination,
|
|
597
|
+
selectionMode,
|
|
598
|
+
showSelectedBackground,
|
|
599
|
+
cellSelection,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// Resolved on the override's identity, so a module-scope dictionary costs one
|
|
603
|
+
// merge for the lifetime of the grid.
|
|
604
|
+
const labels = useMemo(() => mergeLabels(labelsOverride), [labelsOverride]);
|
|
605
|
+
|
|
276
606
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
607
|
+
const selectColumnEnabled = features.selectColumn;
|
|
608
|
+
const groupColumnEnabled = features.grouping;
|
|
609
|
+
// The lane that opens the panels. Nothing to switch on: a grid with no
|
|
610
|
+
// `renderDetails` has nothing for it to open.
|
|
611
|
+
const detailsColumnEnabled = renderDetails !== undefined;
|
|
612
|
+
|
|
613
|
+
// The generated lanes bake `meta.label` into their definitions, so the memo
|
|
614
|
+
// depends on the three strings rather than on the labels object — a fresh
|
|
615
|
+
// inline `labels` must not rebuild the table's columns.
|
|
616
|
+
const selectColumnLabel = labels.selectColumnLabel;
|
|
617
|
+
const groupColumnLabel = labels.groupColumnLabel;
|
|
618
|
+
const detailsColumnLabel = labels.detailsColumnLabel;
|
|
280
619
|
|
|
281
620
|
const columns = useMemo(() => {
|
|
282
621
|
const base = withTMDataGridDefaults<TData>(
|
|
283
622
|
options.columns as ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
284
623
|
);
|
|
285
|
-
|
|
286
|
-
|
|
624
|
+
// Every generated column is present whenever its feature is on, and the
|
|
625
|
+
// tree column hides itself while nothing is grouped. Adding it to the array
|
|
626
|
+
// only once a column is grouped would make the column list depend on table
|
|
627
|
+
// state, which is the one thing that cannot be a `useMemo` dependency here
|
|
628
|
+
// — the table is built from these columns.
|
|
629
|
+
//
|
|
630
|
+
// The order is the order they are pinned in, and it follows what each one
|
|
631
|
+
// is about: tick a row, find it in the tree the user grouped it into, then
|
|
632
|
+
// open it. The details chevron sits last because it acts on the record the
|
|
633
|
+
// lanes to its left have narrowed down to.
|
|
634
|
+
return [
|
|
635
|
+
...(selectColumnEnabled
|
|
636
|
+
? [createSelectColumn<TData>(selectColumnLabel)]
|
|
637
|
+
: []),
|
|
638
|
+
...(groupColumnEnabled ? [createGroupColumn<TData>(groupColumnLabel)] : []),
|
|
639
|
+
...(detailsColumnEnabled
|
|
640
|
+
? [createDetailsColumn<TData>(detailsColumnLabel)]
|
|
641
|
+
: []),
|
|
642
|
+
...base,
|
|
643
|
+
];
|
|
644
|
+
}, [
|
|
645
|
+
options.columns,
|
|
646
|
+
selectColumnEnabled,
|
|
647
|
+
detailsColumnEnabled,
|
|
648
|
+
groupColumnEnabled,
|
|
649
|
+
selectColumnLabel,
|
|
650
|
+
groupColumnLabel,
|
|
651
|
+
detailsColumnLabel,
|
|
652
|
+
]);
|
|
287
653
|
|
|
288
654
|
// Read once on mount: `initialState` is only consumed on the first render,
|
|
289
655
|
// and re-reading later would fight the user's live edits.
|
|
290
656
|
const [persistedState] = useState(() => readPersistedState(persist));
|
|
291
657
|
|
|
658
|
+
// Restored grouping decides whether the tree column starts out visible, so a
|
|
659
|
+
// reload comes back to the tree the user left rather than to a hidden lane.
|
|
660
|
+
const initialGrouping =
|
|
661
|
+
persistedState.grouping ?? options.initialState?.grouping ?? [];
|
|
662
|
+
|
|
292
663
|
const table = useTable({
|
|
293
664
|
columnResizeMode: "onChange",
|
|
294
665
|
enableSorting: true,
|
|
295
666
|
enableColumnResizing: true,
|
|
296
667
|
globalFilterFn: "includesString",
|
|
668
|
+
// Grouping by a column takes it out of the grid, the way AG Grid does it:
|
|
669
|
+
// its values have moved into the tree column, so leaving it in place would
|
|
670
|
+
// show every row the same value it was grouped under. Overridable — pass
|
|
671
|
+
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
672
|
+
groupedColumnMode: "remove",
|
|
297
673
|
...options,
|
|
674
|
+
// Row details ride on `expanded`, the same state the tree uses — but a data
|
|
675
|
+
// row answers `getCanExpand()` false, since TanStack's fallback is
|
|
676
|
+
// `subRows.length > 0`. `() => true` is the right answer for a group row
|
|
677
|
+
// too, so nothing here has to tell the two apart, and a consumer passing
|
|
678
|
+
// their own predicate still wins.
|
|
679
|
+
...(renderDetails !== undefined
|
|
680
|
+
? { getRowCanExpand: options.getRowCanExpand ?? (() => true) }
|
|
681
|
+
: {}),
|
|
298
682
|
features: tmDataGridFeatures,
|
|
299
683
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
300
684
|
initialState: {
|
|
301
685
|
...options.initialState,
|
|
302
686
|
...persistedState,
|
|
687
|
+
columnVisibility: {
|
|
688
|
+
...options.initialState?.columnVisibility,
|
|
689
|
+
...persistedState.columnVisibility,
|
|
690
|
+
// Last word, because the tree column's visibility is not a user setting
|
|
691
|
+
// — it tracks the grouping state. See the effect below.
|
|
692
|
+
...(groupColumnEnabled
|
|
693
|
+
? { [GROUP_COLUMN_ID]: initialGrouping.length > 0 }
|
|
694
|
+
: {}),
|
|
695
|
+
},
|
|
303
696
|
columnPinning: {
|
|
304
|
-
// The
|
|
305
|
-
// top of anything restored from storage.
|
|
697
|
+
// The generated columns are structurally pinned, so they are re-applied
|
|
698
|
+
// on top of anything restored from storage.
|
|
306
699
|
left: [
|
|
307
700
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
701
|
+
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
702
|
+
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
308
703
|
...(
|
|
309
704
|
persistedState.columnPinning?.left ??
|
|
310
705
|
options.initialState?.columnPinning?.left ??
|
|
311
706
|
[]
|
|
312
|
-
).filter(
|
|
707
|
+
).filter(
|
|
708
|
+
(id) =>
|
|
709
|
+
id !== SELECT_COLUMN_ID &&
|
|
710
|
+
id !== DETAILS_COLUMN_ID &&
|
|
711
|
+
id !== GROUP_COLUMN_ID,
|
|
712
|
+
),
|
|
313
713
|
],
|
|
314
714
|
right:
|
|
315
715
|
persistedState.columnPinning?.right ??
|
|
@@ -325,6 +725,60 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
325
725
|
},
|
|
326
726
|
});
|
|
327
727
|
|
|
728
|
+
// Two things have to happen whenever `grouping` changes.
|
|
729
|
+
//
|
|
730
|
+
// One: the tree column appears with the first grouped column and goes away
|
|
731
|
+
// with the last, so an ungrouped grid looks exactly as it did before grouping
|
|
732
|
+
// existed. Driven from a subscription rather than by rebuilding the column
|
|
733
|
+
// array, because the array is what the table is built from — deriving it from
|
|
734
|
+
// table state would close the loop. Visibility is the one column property
|
|
735
|
+
// that can be changed after the fact without touching the definitions.
|
|
736
|
+
//
|
|
737
|
+
// Two, and this one is a workaround. In table-core 9.0.0-beta.21 the
|
|
738
|
+
// per-region column APIs do not list `grouping` among their memo
|
|
739
|
+
// dependencies, even though they all derive from `getAllLeafColumns()`, which
|
|
740
|
+
// does:
|
|
741
|
+
//
|
|
742
|
+
// | API | Declares |
|
|
743
|
+
// | --- | --- |
|
|
744
|
+
// | `getLeft/Center/RightVisibleLeafColumns` | columns, columnPinning, columnVisibility, columnOrder |
|
|
745
|
+
// | `getLeft/Center/RightHeaderGroups` | columnPinning, columnOrder |
|
|
746
|
+
// | `row.getLeft/Center/RightVisibleCells` | columnPinning, columnVisibility |
|
|
747
|
+
//
|
|
748
|
+
// So grouping a *second* column leaves every one of them returning the
|
|
749
|
+
// previous list: the column TanStack removed keeps its header and its grid
|
|
750
|
+
// track, and the row cells no longer line up with them. The first grouping
|
|
751
|
+
// appears to work only because the visibility write above happens to touch a
|
|
752
|
+
// dependency they share.
|
|
753
|
+
//
|
|
754
|
+
// Re-publishing `columnVisibility` and `columnOrder` — same contents, new
|
|
755
|
+
// identity — invalidates all three families. `columnOrder` is the only
|
|
756
|
+
// dependency the header groups declare, and `columnVisibility` the only one
|
|
757
|
+
// the cells do, so both are needed. Remove this once the deps are fixed
|
|
758
|
+
// upstream; the test that fails without it groups two columns and asserts the
|
|
759
|
+
// second one leaves the grid.
|
|
760
|
+
//
|
|
761
|
+
// Writing back into the store from its own subscriber is safe: the guard is
|
|
762
|
+
// on `grouping`'s identity, and neither write touches it, so the callback
|
|
763
|
+
// these writes trigger short-circuits.
|
|
764
|
+
useEffect(() => {
|
|
765
|
+
if (!groupColumnEnabled) return;
|
|
766
|
+
let previousGrouping = table.store.state.grouping;
|
|
767
|
+
|
|
768
|
+
const subscription = table.store.subscribe((state) => {
|
|
769
|
+
if (state.grouping === previousGrouping) return;
|
|
770
|
+
previousGrouping = state.grouping;
|
|
771
|
+
|
|
772
|
+
table.setColumnVisibility((old) => ({
|
|
773
|
+
...old,
|
|
774
|
+
[GROUP_COLUMN_ID]: state.grouping.length > 0,
|
|
775
|
+
}));
|
|
776
|
+
table.setColumnOrder((old) => [...old]);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
return () => subscription.unsubscribe();
|
|
780
|
+
}, [groupColumnEnabled, table]);
|
|
781
|
+
|
|
328
782
|
// Mirror every state change back to storage. Subscribing (rather than writing
|
|
329
783
|
// from an effect on a state snapshot) means nothing is missed, including
|
|
330
784
|
// changes made straight through the table API by the consumer.
|
|
@@ -367,6 +821,12 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
367
821
|
columnsPanelOpen: false,
|
|
368
822
|
filterPanelColumnId: null,
|
|
369
823
|
draggedColumnId: null,
|
|
824
|
+
// `useCreateStore` builds the store once per mount, so this is a genuine
|
|
825
|
+
// default rather than a value that would fight later clicks.
|
|
826
|
+
highlightedRowId: defaultHighlightedRowId ?? null,
|
|
827
|
+
selectionAnchorRowId: null,
|
|
828
|
+
focusedCell: null,
|
|
829
|
+
cellRange: null,
|
|
370
830
|
},
|
|
371
831
|
({ setState }) => ({
|
|
372
832
|
openFilterPanel: (columnId = null) =>
|
|
@@ -389,21 +849,82 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
389
849
|
setState((prev) => ({ ...prev, draggedColumnId: columnId })),
|
|
390
850
|
endColumnDrag: () =>
|
|
391
851
|
setState((prev) => ({ ...prev, draggedColumnId: null })),
|
|
852
|
+
setHighlightedRow: (rowId) =>
|
|
853
|
+
setState((prev) => ({ ...prev, highlightedRowId: rowId })),
|
|
854
|
+
setSelectionAnchor: (rowId) =>
|
|
855
|
+
setState((prev) => ({ ...prev, selectionAnchorRowId: rowId })),
|
|
856
|
+
setFocusedCell: (cell) =>
|
|
857
|
+
setState((prev) =>
|
|
858
|
+
// Same cell, same object: the body re-renders on every scroll frame
|
|
859
|
+
// and mouse events land on the cell that already has the focus, so a
|
|
860
|
+
// fresh identity here would republish the store for nothing.
|
|
861
|
+
isSameCell(prev.focusedCell, cell)
|
|
862
|
+
? prev
|
|
863
|
+
: { ...prev, focusedCell: cell },
|
|
864
|
+
),
|
|
865
|
+
setCellRange: (range) =>
|
|
866
|
+
setState((prev) =>
|
|
867
|
+
// Same guard as above, and it earns more here: a drag publishes a new
|
|
868
|
+
// corner on every mouse move, and most of those land on the cell the
|
|
869
|
+
// range already ends at.
|
|
870
|
+
isSameCell(prev.cellRange?.anchor ?? null, range?.anchor ?? null) &&
|
|
871
|
+
isSameCell(prev.cellRange?.focus ?? null, range?.focus ?? null)
|
|
872
|
+
? prev
|
|
873
|
+
: { ...prev, cellRange: range },
|
|
874
|
+
),
|
|
392
875
|
}),
|
|
393
876
|
);
|
|
394
877
|
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
878
|
+
// `onHighlightedRowChange` is fired from a subscription rather than from the store
|
|
879
|
+
// action, so it covers every route to a new active row — a row click, and a
|
|
880
|
+
// consumer calling `setHighlightedRow` itself. Held in a ref because the store is
|
|
881
|
+
// built once and its actions would otherwise close over the first render's
|
|
882
|
+
// callback.
|
|
883
|
+
const onHighlightedRowChangeRef = useRef(onHighlightedRowChange);
|
|
884
|
+
useEffect(() => {
|
|
885
|
+
onHighlightedRowChangeRef.current = onHighlightedRowChange;
|
|
886
|
+
}, [onHighlightedRowChange]);
|
|
887
|
+
|
|
888
|
+
useEffect(() => {
|
|
889
|
+
// Seeded from the current value so mounting with a `defaultHighlightedRowId`
|
|
890
|
+
// does not report a change that nothing made.
|
|
891
|
+
let previous = ui.state.highlightedRowId;
|
|
892
|
+
const subscription = ui.subscribe((state) => {
|
|
893
|
+
if (state.highlightedRowId === previous) return;
|
|
894
|
+
previous = state.highlightedRowId;
|
|
895
|
+
onHighlightedRowChangeRef.current?.(state.highlightedRowId);
|
|
896
|
+
});
|
|
897
|
+
return () => subscription.unsubscribe();
|
|
898
|
+
}, [ui]);
|
|
899
|
+
|
|
900
|
+
// Same shape for the focused cell, and for the same reason: arrow keys, a
|
|
901
|
+
// click and `setFocusedCell` all have to report through one place.
|
|
902
|
+
const onFocusedCellChangeRef = useRef(onFocusedCellChange);
|
|
903
|
+
useEffect(() => {
|
|
904
|
+
onFocusedCellChangeRef.current = onFocusedCellChange;
|
|
905
|
+
}, [onFocusedCellChange]);
|
|
906
|
+
|
|
907
|
+
useEffect(() => {
|
|
908
|
+
let previous = ui.state.focusedCell;
|
|
909
|
+
const subscription = ui.subscribe((state) => {
|
|
910
|
+
// Identity is enough: `setFocusedCell` keeps the previous object when the
|
|
911
|
+
// cell has not changed, so a new one always means a new cell.
|
|
912
|
+
if (state.focusedCell === previous) return;
|
|
913
|
+
previous = state.focusedCell;
|
|
914
|
+
onFocusedCellChangeRef.current?.(state.focusedCell);
|
|
915
|
+
});
|
|
916
|
+
return () => subscription.unsubscribe();
|
|
917
|
+
}, [ui]);
|
|
405
918
|
|
|
406
|
-
return {
|
|
919
|
+
return {
|
|
920
|
+
table,
|
|
921
|
+
ui,
|
|
922
|
+
features,
|
|
923
|
+
labels,
|
|
924
|
+
renderDetails,
|
|
925
|
+
renderDetailsEstHeight,
|
|
926
|
+
overscan,
|
|
927
|
+
};
|
|
407
928
|
}
|
|
408
929
|
|
|
409
930
|
/**
|