@jielga/tmdatagrid 0.3.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 +947 -64
- package/dist/index.js +2474 -1423
- 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 +1 -1
- 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 +10 -0
- package/src/tmdatagrid/components/TMDataGrid.tsx +23 -1
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +5 -5
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.module.css +33 -0
- package/src/tmdatagrid/components/TMDataGridDetailsColumn.tsx +160 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +111 -33
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +5 -4
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +11 -19
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +23 -11
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +15 -3
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +181 -52
- package/src/tmdatagrid/components/TMDataGridSearch.tsx +122 -0
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +23 -13
- package/src/tmdatagrid/components/TMDataGridTable.module.css +127 -6
- package/src/tmdatagrid/components/TMDataGridTable.tsx +1003 -30
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +30 -7
- package/src/tmdatagrid/components/icons.ts +1 -0
- package/src/tmdatagrid/components/sticky.module.css +77 -16
- package/src/tmdatagrid/core/autosize.ts +125 -0
- package/src/tmdatagrid/core/capabilities.ts +41 -0
- 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 +176 -15
- package/src/tmdatagrid/core/labels.ts +211 -0
- package/src/tmdatagrid/core/labelsSv.ts +108 -0
- package/src/tmdatagrid/core/summary.ts +35 -0
- package/src/tmdatagrid/index.ts +70 -0
- package/src/tmdatagrid/useTMDataGrid.tsx +328 -12
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { aggregationFns, type RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridRowData } from "../TMDataGridContext";
|
|
3
|
+
import type { TMDataGridTable } from "../useTMDataGrid";
|
|
4
|
+
|
|
5
|
+
/** The registered aggregation names — the same set `aggregationFn` accepts. */
|
|
6
|
+
export type TMDataGridAggregationName = keyof typeof aggregationFns;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* One column aggregated over every filtered row — the number a summary-row
|
|
10
|
+
* `footer` wants:
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* columnHelper.accessor("salary", {
|
|
14
|
+
* footer: ({ table }) => sek(Number(aggregateColumn({ table, columnId: "salary" }))),
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Filtered rather than paginated rows, so the total covers everything the
|
|
19
|
+
* filters left — all pages — and follows the filters as they change. Reuses
|
|
20
|
+
* TanStack's registered aggregation functions; `fn` defaults to `"sum"`.
|
|
21
|
+
*/
|
|
22
|
+
export function aggregateColumn<TData extends RowData>({
|
|
23
|
+
table,
|
|
24
|
+
columnId,
|
|
25
|
+
fn = "sum",
|
|
26
|
+
}: {
|
|
27
|
+
table: TMDataGridTable<TData>;
|
|
28
|
+
columnId: string;
|
|
29
|
+
fn?: TMDataGridAggregationName;
|
|
30
|
+
}): unknown {
|
|
31
|
+
const erased = table as unknown as TMDataGridTable<TMDataGridRowData>;
|
|
32
|
+
const rows = erased.getFilteredRowModel().rows;
|
|
33
|
+
// Leaf and child rows are the same set here: the filtered model is flat.
|
|
34
|
+
return aggregationFns[fn](columnId, rows, rows);
|
|
35
|
+
}
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
export { TMDataGrid, type TMDataGridProps } from "./components/TMDataGrid";
|
|
2
2
|
export {
|
|
3
|
+
useCellControlTabIndex,
|
|
3
4
|
useTMDataGridContext,
|
|
4
5
|
type TMDataGridContextValue,
|
|
5
6
|
type TMDataGridRowData,
|
|
6
7
|
} from "./TMDataGridContext";
|
|
7
8
|
export { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
|
|
9
|
+
export { DETAILS_COLUMN_ID } from "./components/TMDataGridDetailsColumn";
|
|
8
10
|
export {
|
|
9
11
|
formatGroupValue,
|
|
10
12
|
GROUP_COLUMN_ID,
|
|
@@ -15,6 +17,8 @@ export {
|
|
|
15
17
|
tmDataGridFeatures,
|
|
16
18
|
type TMDataGridApi,
|
|
17
19
|
type TMDataGridColumnMeta,
|
|
20
|
+
type TMDataGridDetailsArgs,
|
|
21
|
+
type TMDataGridDetailsRenderer,
|
|
18
22
|
type TMDataGridFeatures,
|
|
19
23
|
type TMDataGridTable,
|
|
20
24
|
type TMDataGridTableMeta,
|
|
@@ -28,20 +32,84 @@ export {
|
|
|
28
32
|
TMDataGridFilterPills,
|
|
29
33
|
type TMDataGridFilterPillsProps,
|
|
30
34
|
} from "./components/TMDataGridFilterPills";
|
|
35
|
+
export {
|
|
36
|
+
TMDataGridSearch,
|
|
37
|
+
type TMDataGridSearchProps,
|
|
38
|
+
} from "./components/TMDataGridSearch";
|
|
31
39
|
export {
|
|
32
40
|
type TMDataGridColumnType,
|
|
33
41
|
type TMDataGridFilterOperator,
|
|
34
42
|
type TMDataGridFilterValue,
|
|
35
43
|
FILTER_OPERATOR_LABELS,
|
|
36
44
|
formatFilterLabel,
|
|
45
|
+
getDefaultOperator,
|
|
46
|
+
getOperatorsForType,
|
|
37
47
|
isFilterActive,
|
|
48
|
+
operatorNeedsValue,
|
|
49
|
+
operatorTakesArrayValue,
|
|
38
50
|
} from "./core/filterOperators";
|
|
51
|
+
export {
|
|
52
|
+
optionsToComboboxData,
|
|
53
|
+
resolveColumnOptions,
|
|
54
|
+
type TMDataGridOption,
|
|
55
|
+
type TMDataGridOptionsArgs,
|
|
56
|
+
type TMDataGridOptionsSource,
|
|
57
|
+
} from "./core/columnOptions";
|
|
58
|
+
export {
|
|
59
|
+
mergeLabels,
|
|
60
|
+
TMDATAGRID_LABELS_EN,
|
|
61
|
+
type TMDataGridLabels,
|
|
62
|
+
type TMDataGridLabelsOverride,
|
|
63
|
+
} from "./core/labels";
|
|
64
|
+
export { TMDATAGRID_LABELS_SV } from "./core/labelsSv";
|
|
39
65
|
export {
|
|
40
66
|
getColumnLabel,
|
|
41
67
|
getColumnType,
|
|
42
68
|
isColumnReorderable,
|
|
69
|
+
isControlColumn,
|
|
43
70
|
} from "./core/columnUtils";
|
|
44
71
|
export { getGroupDataRows } from "./core/grouping";
|
|
72
|
+
export {
|
|
73
|
+
aggregateColumn,
|
|
74
|
+
type TMDataGridAggregationName,
|
|
75
|
+
} from "./core/summary";
|
|
76
|
+
export { autosizeColumn, measureColumnContentWidth } from "./core/autosize";
|
|
77
|
+
export {
|
|
78
|
+
isSameCell,
|
|
79
|
+
resolveCellMove,
|
|
80
|
+
type ResolveCellMoveArgs,
|
|
81
|
+
type TMDataGridCellCoords,
|
|
82
|
+
type TMDataGridCellPosition,
|
|
83
|
+
} from "./core/cellNavigation";
|
|
84
|
+
export {
|
|
85
|
+
boundsCellCount,
|
|
86
|
+
boundsEdges,
|
|
87
|
+
isWithinBounds,
|
|
88
|
+
resolveRangeBounds,
|
|
89
|
+
type ResolveRangeBoundsArgs,
|
|
90
|
+
type TMDataGridCellRange,
|
|
91
|
+
type TMDataGridRangeBounds,
|
|
92
|
+
} from "./core/cellRange";
|
|
93
|
+
export {
|
|
94
|
+
buildCellMatrix,
|
|
95
|
+
buildGridCellMatrix,
|
|
96
|
+
DEFAULT_CELL_EXPORT_OPTIONS,
|
|
97
|
+
downloadTextFile,
|
|
98
|
+
exportGridToCsv,
|
|
99
|
+
formatExportValue,
|
|
100
|
+
toClipboardText,
|
|
101
|
+
toExcelCsv,
|
|
102
|
+
writeClipboardText,
|
|
103
|
+
type BuildCellMatrixArgs,
|
|
104
|
+
type TMDataGridCellExportOptions,
|
|
105
|
+
type TMDataGridCellMatrix,
|
|
106
|
+
} from "./core/cellExport";
|
|
107
|
+
export {
|
|
108
|
+
areAllRowsExpanded,
|
|
109
|
+
resolveExpandAll,
|
|
110
|
+
type TMDataGridExpandAllArgs,
|
|
111
|
+
type TMDataGridExpandTarget,
|
|
112
|
+
} from "./core/expanding";
|
|
45
113
|
export {
|
|
46
114
|
getColumnRegion,
|
|
47
115
|
getStepTargetColumn,
|
|
@@ -56,6 +124,7 @@ export {
|
|
|
56
124
|
getColumnCapabilities,
|
|
57
125
|
getGridCapabilities,
|
|
58
126
|
readFeatureFlags,
|
|
127
|
+
type TMDataGridCellSelectionMode,
|
|
59
128
|
type TMDataGridCapabilities,
|
|
60
129
|
type TMDataGridColumnCapabilities,
|
|
61
130
|
type TMDataGridFeatureFlags,
|
|
@@ -86,6 +155,7 @@ export {
|
|
|
86
155
|
type TMDataGridStorageMode,
|
|
87
156
|
} from "./core/persistence";
|
|
88
157
|
export type {
|
|
158
|
+
TMDataGridCellNav,
|
|
89
159
|
TMDataGridColumnLayout,
|
|
90
160
|
TMDataGridRowContextMenu,
|
|
91
161
|
TMDataGridRowContextMenuArgs,
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
filterFns,
|
|
23
23
|
globalFilteringFeature,
|
|
24
24
|
metaHelper,
|
|
25
|
+
type Row,
|
|
25
26
|
type RowData,
|
|
26
27
|
rowExpandingFeature,
|
|
27
28
|
rowPaginationFeature,
|
|
@@ -35,12 +36,18 @@ import {
|
|
|
35
36
|
useTable,
|
|
36
37
|
} from "@tanstack/react-table";
|
|
37
38
|
import type { Store } from "@tanstack/store";
|
|
38
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
39
|
+
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
40
|
+
import type { TMDataGridOptionsSource } from "./core/columnOptions";
|
|
39
41
|
import {
|
|
40
42
|
type TMDataGridColumnType,
|
|
41
43
|
tmDataGridFilterFn,
|
|
42
44
|
getDefaultOperator,
|
|
43
45
|
} from "./core/filterOperators";
|
|
46
|
+
import {
|
|
47
|
+
mergeLabels,
|
|
48
|
+
type TMDataGridLabels,
|
|
49
|
+
type TMDataGridLabelsOverride,
|
|
50
|
+
} from "./core/labels";
|
|
44
51
|
import {
|
|
45
52
|
hasPersistenceKeys,
|
|
46
53
|
readPersistedState,
|
|
@@ -49,9 +56,15 @@ import {
|
|
|
49
56
|
} from "./core/persistence";
|
|
50
57
|
import {
|
|
51
58
|
readFeatureFlags,
|
|
59
|
+
type TMDataGridCellSelectionMode,
|
|
52
60
|
type TMDataGridFeatureFlags,
|
|
53
61
|
type TMDataGridSelectionMode,
|
|
54
62
|
} from "./core/capabilities";
|
|
63
|
+
import {
|
|
64
|
+
isSameCell,
|
|
65
|
+
type TMDataGridCellPosition,
|
|
66
|
+
} from "./core/cellNavigation";
|
|
67
|
+
import type { TMDataGridCellRange } from "./core/cellRange";
|
|
55
68
|
import {
|
|
56
69
|
createSelectColumn,
|
|
57
70
|
SELECT_COLUMN_ID,
|
|
@@ -60,6 +73,10 @@ import {
|
|
|
60
73
|
createGroupColumn,
|
|
61
74
|
GROUP_COLUMN_ID,
|
|
62
75
|
} from "./components/TMDataGridGroupColumn";
|
|
76
|
+
import {
|
|
77
|
+
createDetailsColumn,
|
|
78
|
+
DETAILS_COLUMN_ID,
|
|
79
|
+
} from "./components/TMDataGridDetailsColumn";
|
|
63
80
|
|
|
64
81
|
/**
|
|
65
82
|
* How long the grid waits after the last state change before writing to
|
|
@@ -72,11 +89,28 @@ const PERSIST_DEBOUNCE_MS = 200;
|
|
|
72
89
|
export type TMDataGridColumnMeta = {
|
|
73
90
|
/** Name shown in menus and the column manager. Falls back to a string header. */
|
|
74
91
|
label?: string;
|
|
75
|
-
/**
|
|
92
|
+
/**
|
|
93
|
+
* Drives which filter operators are offered and, once editing is on, which
|
|
94
|
+
* editor the cell opens. Defaults to `"string"`.
|
|
95
|
+
*/
|
|
76
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;
|
|
77
105
|
/** Share of the leftover width this column claims. Defaults to `1`. */
|
|
78
106
|
flex?: number;
|
|
79
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;
|
|
80
114
|
/**
|
|
81
115
|
* `false` keeps the column where it is: no header dragging, no move items.
|
|
82
116
|
* Column ordering is the one feature TanStack defines no column option for,
|
|
@@ -151,6 +185,26 @@ export function createTMDataGridColumnHelper<TData extends RowData>() {
|
|
|
151
185
|
return createColumnHelper<TMDataGridFeatures, TData>();
|
|
152
186
|
}
|
|
153
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
|
+
|
|
154
208
|
/**
|
|
155
209
|
* Chrome state that is *not* table state: which panels are open, and which
|
|
156
210
|
* column opened the filter panel. Kept in a TanStack Store so consumers can
|
|
@@ -180,6 +234,25 @@ export type TMDataGridUiState = {
|
|
|
180
234
|
highlightedRowId: string | null;
|
|
181
235
|
/** The pivot a shift-click extends from. See resolveRowSelectionClick. */
|
|
182
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;
|
|
183
256
|
};
|
|
184
257
|
|
|
185
258
|
export type TMDataGridUiActions = {
|
|
@@ -195,6 +268,19 @@ export type TMDataGridUiActions = {
|
|
|
195
268
|
*/
|
|
196
269
|
setHighlightedRow: (rowId: string | null) => void;
|
|
197
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;
|
|
198
284
|
};
|
|
199
285
|
|
|
200
286
|
export type TMDataGridUiStore = Store<TMDataGridUiState, TMDataGridUiActions>;
|
|
@@ -205,6 +291,14 @@ export type TMDataGridApi<TData extends RowData> = {
|
|
|
205
291
|
ui: TMDataGridUiStore;
|
|
206
292
|
/** Table-level feature switches, re-read from options on every render. */
|
|
207
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;
|
|
208
302
|
};
|
|
209
303
|
|
|
210
304
|
export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
@@ -219,6 +313,20 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
219
313
|
* dependency of the subscription that writes back.
|
|
220
314
|
*/
|
|
221
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;
|
|
222
330
|
/**
|
|
223
331
|
* Header drag-and-drop and the move items in the column menu. Defaults to
|
|
224
332
|
* `true`.
|
|
@@ -301,6 +409,113 @@ export type UseTMDataGridOptions<TData extends RowData> = Omit<
|
|
|
301
409
|
* Fires for `ui.actions.setHighlightedRow` too, not only for clicks.
|
|
302
410
|
*/
|
|
303
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;
|
|
304
519
|
};
|
|
305
520
|
|
|
306
521
|
type TMDataGridColumnDef<TData extends RowData> = ColumnDef<
|
|
@@ -355,12 +570,18 @@ function withTMDataGridDefaults<TData extends RowData>(
|
|
|
355
570
|
export function useTMDataGrid<TData extends RowData>({
|
|
356
571
|
persist,
|
|
357
572
|
// Not TanStack options, so they are kept out of what `useTable` receives.
|
|
573
|
+
labels: labelsOverride,
|
|
358
574
|
enableColumnOrdering,
|
|
359
575
|
enablePagination,
|
|
360
576
|
selectionMode,
|
|
361
577
|
showSelectedBackground,
|
|
362
578
|
defaultHighlightedRowId,
|
|
363
579
|
onHighlightedRowChange,
|
|
580
|
+
cellSelection,
|
|
581
|
+
onFocusedCellChange,
|
|
582
|
+
renderDetails,
|
|
583
|
+
renderDetailsEstHeight = DEFAULT_DETAILS_EST_HEIGHT,
|
|
584
|
+
overscan = DEFAULT_OVERSCAN,
|
|
364
585
|
...options
|
|
365
586
|
}: UseTMDataGridOptions<TData>): TMDataGridApi<TData> {
|
|
366
587
|
// Derived up here, rather than just before the return, because the rest of the
|
|
@@ -375,27 +596,60 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
375
596
|
enablePagination,
|
|
376
597
|
selectionMode,
|
|
377
598
|
showSelectedBackground,
|
|
599
|
+
cellSelection,
|
|
378
600
|
});
|
|
379
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
|
+
|
|
380
606
|
const pinningEnabled = options.enableColumnPinning !== false;
|
|
381
607
|
const selectColumnEnabled = features.selectColumn;
|
|
382
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;
|
|
383
619
|
|
|
384
620
|
const columns = useMemo(() => {
|
|
385
621
|
const base = withTMDataGridDefaults<TData>(
|
|
386
622
|
options.columns as ReadonlyArray<TMDataGridColumnDef<TData>>,
|
|
387
623
|
);
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
//
|
|
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.
|
|
393
634
|
return [
|
|
394
|
-
...(selectColumnEnabled
|
|
395
|
-
|
|
635
|
+
...(selectColumnEnabled
|
|
636
|
+
? [createSelectColumn<TData>(selectColumnLabel)]
|
|
637
|
+
: []),
|
|
638
|
+
...(groupColumnEnabled ? [createGroupColumn<TData>(groupColumnLabel)] : []),
|
|
639
|
+
...(detailsColumnEnabled
|
|
640
|
+
? [createDetailsColumn<TData>(detailsColumnLabel)]
|
|
641
|
+
: []),
|
|
396
642
|
...base,
|
|
397
643
|
];
|
|
398
|
-
}, [
|
|
644
|
+
}, [
|
|
645
|
+
options.columns,
|
|
646
|
+
selectColumnEnabled,
|
|
647
|
+
detailsColumnEnabled,
|
|
648
|
+
groupColumnEnabled,
|
|
649
|
+
selectColumnLabel,
|
|
650
|
+
groupColumnLabel,
|
|
651
|
+
detailsColumnLabel,
|
|
652
|
+
]);
|
|
399
653
|
|
|
400
654
|
// Read once on mount: `initialState` is only consumed on the first render,
|
|
401
655
|
// and re-reading later would fight the user's live edits.
|
|
@@ -417,6 +671,14 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
417
671
|
// `"reorder"` to keep the column and have it moved to the front instead.
|
|
418
672
|
groupedColumnMode: "remove",
|
|
419
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
|
+
: {}),
|
|
420
682
|
features: tmDataGridFeatures,
|
|
421
683
|
columns: columns as TableOptions<TMDataGridFeatures, TData>["columns"],
|
|
422
684
|
initialState: {
|
|
@@ -437,11 +699,17 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
437
699
|
left: [
|
|
438
700
|
...(selectColumnEnabled && pinningEnabled ? [SELECT_COLUMN_ID] : []),
|
|
439
701
|
...(groupColumnEnabled && pinningEnabled ? [GROUP_COLUMN_ID] : []),
|
|
702
|
+
...(detailsColumnEnabled && pinningEnabled ? [DETAILS_COLUMN_ID] : []),
|
|
440
703
|
...(
|
|
441
704
|
persistedState.columnPinning?.left ??
|
|
442
705
|
options.initialState?.columnPinning?.left ??
|
|
443
706
|
[]
|
|
444
|
-
).filter(
|
|
707
|
+
).filter(
|
|
708
|
+
(id) =>
|
|
709
|
+
id !== SELECT_COLUMN_ID &&
|
|
710
|
+
id !== DETAILS_COLUMN_ID &&
|
|
711
|
+
id !== GROUP_COLUMN_ID,
|
|
712
|
+
),
|
|
445
713
|
],
|
|
446
714
|
right:
|
|
447
715
|
persistedState.columnPinning?.right ??
|
|
@@ -557,6 +825,8 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
557
825
|
// default rather than a value that would fight later clicks.
|
|
558
826
|
highlightedRowId: defaultHighlightedRowId ?? null,
|
|
559
827
|
selectionAnchorRowId: null,
|
|
828
|
+
focusedCell: null,
|
|
829
|
+
cellRange: null,
|
|
560
830
|
},
|
|
561
831
|
({ setState }) => ({
|
|
562
832
|
openFilterPanel: (columnId = null) =>
|
|
@@ -583,6 +853,25 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
583
853
|
setState((prev) => ({ ...prev, highlightedRowId: rowId })),
|
|
584
854
|
setSelectionAnchor: (rowId) =>
|
|
585
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
|
+
),
|
|
586
875
|
}),
|
|
587
876
|
);
|
|
588
877
|
|
|
@@ -608,7 +897,34 @@ export function useTMDataGrid<TData extends RowData>({
|
|
|
608
897
|
return () => subscription.unsubscribe();
|
|
609
898
|
}, [ui]);
|
|
610
899
|
|
|
611
|
-
|
|
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]);
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
table,
|
|
921
|
+
ui,
|
|
922
|
+
features,
|
|
923
|
+
labels,
|
|
924
|
+
renderDetails,
|
|
925
|
+
renderDetailsEstHeight,
|
|
926
|
+
overscan,
|
|
927
|
+
};
|
|
612
928
|
}
|
|
613
929
|
|
|
614
930
|
/**
|