@jielga/tmdatagrid 0.2.0 → 0.3.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 +397 -37
- package/dist/index.js +1550 -978
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/skills/columns/SKILL.md +1 -1
- package/skills/features/SKILL.md +1 -1
- package/skills/getting-started/SKILL.md +41 -6
- package/skills/options/SKILL.md +1 -1
- package/skills/server-side/SKILL.md +1 -1
- package/src/tmdatagrid/components/TMDataGrid.module.css +11 -1
- package/src/tmdatagrid/components/TMDataGrid.tsx +6 -0
- package/src/tmdatagrid/components/TMDataGridColumnsPanel.tsx +7 -1
- package/src/tmdatagrid/components/TMDataGridFilterPanel.module.css +15 -0
- package/src/tmdatagrid/components/TMDataGridFilterPanel.tsx +87 -10
- package/src/tmdatagrid/components/TMDataGridFilterPills.module.css +35 -0
- package/src/tmdatagrid/components/TMDataGridFilterPills.tsx +135 -0
- package/src/tmdatagrid/components/TMDataGridFooter.module.css +8 -0
- package/src/tmdatagrid/components/TMDataGridFooter.tsx +70 -42
- package/src/tmdatagrid/components/TMDataGridGroupColumn.module.css +48 -0
- package/src/tmdatagrid/components/TMDataGridGroupColumn.tsx +147 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.module.css +6 -0
- package/src/tmdatagrid/components/TMDataGridHeaderCell.tsx +77 -1
- package/src/tmdatagrid/components/TMDataGridSelectColumn.tsx +83 -6
- package/src/tmdatagrid/components/TMDataGridTable.module.css +38 -4
- package/src/tmdatagrid/components/TMDataGridTable.tsx +396 -32
- package/src/tmdatagrid/components/TMDataGridToolbar.tsx +3 -0
- package/src/tmdatagrid/components/icons.ts +5 -0
- package/src/tmdatagrid/core/capabilities.ts +81 -11
- package/src/tmdatagrid/core/filterOperators.ts +25 -0
- package/src/tmdatagrid/core/grouping.ts +21 -0
- package/src/tmdatagrid/core/persistence.ts +12 -0
- package/src/tmdatagrid/core/rowSelection.ts +202 -0
- package/src/tmdatagrid/index.ts +26 -2
- package/src/tmdatagrid/useTMDataGrid.tsx +240 -35
|
@@ -20,9 +20,23 @@ import type {
|
|
|
20
20
|
* what applies the per-column overrides.
|
|
21
21
|
*/
|
|
22
22
|
/**
|
|
23
|
-
* How rows are selected — see `
|
|
23
|
+
* How rows are selected — see `selectionMode` on {@link UseTMDataGridOptions}.
|
|
24
|
+
*
|
|
25
|
+
* One axis, because the two things a config could set independently — which
|
|
26
|
+
* chrome selects, and what a bare row click does — cannot both be free: a click
|
|
27
|
+
* can either toggle a multi-selection or move the highlight, never both. Folding
|
|
28
|
+
* them into one value makes that conflict unrepresentable rather than something
|
|
29
|
+
* to warn about.
|
|
24
30
|
*/
|
|
25
|
-
export type
|
|
31
|
+
export type TMDataGridSelectionMode =
|
|
32
|
+
/** Checkbox column, multi-select. Clicking a row elsewhere does nothing. */
|
|
33
|
+
| "checkbox"
|
|
34
|
+
/** No checkbox column; row click multi-selects, with Ctrl/Shift modifiers. */
|
|
35
|
+
| "row"
|
|
36
|
+
/** Checkbox column multi-selects, and a row click highlights one row. */
|
|
37
|
+
| "checkboxAndHighlight"
|
|
38
|
+
/** No selection at all — a row click only highlights. Master-detail. */
|
|
39
|
+
| "highlight";
|
|
26
40
|
|
|
27
41
|
export type TMDataGridFeatureFlags = {
|
|
28
42
|
sorting: boolean;
|
|
@@ -31,17 +45,49 @@ export type TMDataGridFeatureFlags = {
|
|
|
31
45
|
pinning: boolean;
|
|
32
46
|
resizing: boolean;
|
|
33
47
|
ordering: boolean;
|
|
48
|
+
/** Whether any row selection is possible. Off outright under `"highlight"`. */
|
|
34
49
|
rowSelection: boolean;
|
|
35
|
-
/**
|
|
36
|
-
|
|
50
|
+
/** Defaults to `"checkbox"`. */
|
|
51
|
+
selectionMode: TMDataGridSelectionMode;
|
|
52
|
+
/** Whether the generated checkbox column is prepended. */
|
|
53
|
+
selectColumn: boolean;
|
|
54
|
+
/** Whether a bare row click toggles the selection. Only under `"row"`. */
|
|
55
|
+
rowClickSelects: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Whether more than one row can be selected at once — TanStack's
|
|
58
|
+
* `enableMultiRowSelection`. Off, the grid drops the select-all header
|
|
59
|
+
* checkbox: TanStack's `toggleAllRowsSelected` only consults `getCanSelect`,
|
|
60
|
+
* so that control would select every row and walk straight past the limit.
|
|
61
|
+
*
|
|
62
|
+
* A per-row predicate counts as on, since some rows may still multi-select.
|
|
63
|
+
*/
|
|
64
|
+
multiRowSelection: boolean;
|
|
37
65
|
/** Whether a selected row takes the highlight colour. Follows the mode. */
|
|
38
|
-
|
|
66
|
+
showSelectedBackground: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The single highlighted row — clicking a row highlights it, for a detail
|
|
69
|
+
* panel to follow. State of its own, not a slice of `rowSelection`, which is
|
|
70
|
+
* what lets it coexist with a checkbox multi-selection.
|
|
71
|
+
*/
|
|
72
|
+
highlightRow: boolean;
|
|
39
73
|
/**
|
|
40
74
|
* The one default-off flag: pagination must be asked for, either with the
|
|
41
75
|
* grid's `enablePagination` or implicitly by declaring `manualPagination`.
|
|
42
76
|
* Off, the grid renders every filtered row and relies on virtualization.
|
|
43
77
|
*/
|
|
44
78
|
pagination: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Row grouping: the "Group by" items in the header menu and the generated
|
|
81
|
+
* tree column. On unless `enableGrouping: false`, since an ungrouped grid
|
|
82
|
+
* looks and behaves exactly as it did before — `grouping` starts empty, and
|
|
83
|
+
* an empty grouping state passes straight through the row model.
|
|
84
|
+
*
|
|
85
|
+
* The exception is `manualPagination`, where the client holds one page rather
|
|
86
|
+
* than the whole set: grouping that page would build groups out of an
|
|
87
|
+
* arbitrary slice and quietly show wrong counts. A server-side grid that does
|
|
88
|
+
* its own grouping can still say `enableGrouping: true` to override.
|
|
89
|
+
*/
|
|
90
|
+
grouping: boolean;
|
|
45
91
|
};
|
|
46
92
|
|
|
47
93
|
export function readFeatureFlags<TData extends RowData>(
|
|
@@ -54,12 +100,21 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
54
100
|
| "enableColumnResizing"
|
|
55
101
|
| "enableColumnOrdering"
|
|
56
102
|
| "enableRowSelection"
|
|
57
|
-
| "
|
|
58
|
-
| "
|
|
103
|
+
| "enableMultiRowSelection"
|
|
104
|
+
| "selectionMode"
|
|
105
|
+
| "showSelectedBackground"
|
|
59
106
|
| "enablePagination"
|
|
60
107
|
| "manualPagination"
|
|
108
|
+
| "enableGrouping"
|
|
61
109
|
>,
|
|
62
110
|
): TMDataGridFeatureFlags {
|
|
111
|
+
const selectionMode = options.selectionMode ?? "checkbox";
|
|
112
|
+
// `"highlight"` is the master-detail mode: no selection to speak of, so the
|
|
113
|
+
// checkbox column and the click-to-select behaviour both fall away. Otherwise
|
|
114
|
+
// `enableRowSelection` still has the final say, including its predicate form.
|
|
115
|
+
const rowSelection =
|
|
116
|
+
selectionMode !== "highlight" && options.enableRowSelection !== false;
|
|
117
|
+
|
|
63
118
|
return {
|
|
64
119
|
sorting: options.enableSorting !== false,
|
|
65
120
|
filtering: options.enableColumnFilters !== false,
|
|
@@ -67,14 +122,22 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
67
122
|
pinning: options.enableColumnPinning !== false,
|
|
68
123
|
resizing: options.enableColumnResizing !== false,
|
|
69
124
|
ordering: options.enableColumnOrdering !== false,
|
|
70
|
-
rowSelection
|
|
71
|
-
|
|
125
|
+
rowSelection,
|
|
126
|
+
selectionMode,
|
|
127
|
+
selectColumn:
|
|
128
|
+
rowSelection &&
|
|
129
|
+
(selectionMode === "checkbox" || selectionMode === "checkboxAndHighlight"),
|
|
130
|
+
rowClickSelects: rowSelection && selectionMode === "row",
|
|
131
|
+
multiRowSelection: options.enableMultiRowSelection !== false,
|
|
72
132
|
// In "row" mode the highlight *is* the feedback for a click, so it is on.
|
|
73
133
|
// With checkboxes the box already says it, so it is off unless asked for.
|
|
74
|
-
|
|
75
|
-
options.
|
|
134
|
+
showSelectedBackground:
|
|
135
|
+
options.showSelectedBackground ?? selectionMode === "row",
|
|
136
|
+
highlightRow:
|
|
137
|
+
selectionMode === "checkboxAndHighlight" || selectionMode === "highlight",
|
|
76
138
|
pagination:
|
|
77
139
|
options.enablePagination === true || options.manualPagination === true,
|
|
140
|
+
grouping: options.enableGrouping ?? options.manualPagination !== true,
|
|
78
141
|
};
|
|
79
142
|
}
|
|
80
143
|
|
|
@@ -89,6 +152,7 @@ export function readFeatureFlags<TData extends RowData>(
|
|
|
89
152
|
* | Pin to left / right | `enableColumnPinning` / `enablePinning` |
|
|
90
153
|
* | Resize dragging | `enableColumnResizing` / `enableResizing` |
|
|
91
154
|
* | Header dragging, Move left / right | `enableColumnOrdering` / `meta.enableOrdering` |
|
|
155
|
+
* | Group by / Ungroup | `enableGrouping` (table or column) |
|
|
92
156
|
*/
|
|
93
157
|
export type TMDataGridColumnCapabilities = {
|
|
94
158
|
canSort: boolean;
|
|
@@ -97,6 +161,7 @@ export type TMDataGridColumnCapabilities = {
|
|
|
97
161
|
canPin: boolean;
|
|
98
162
|
canResize: boolean;
|
|
99
163
|
canReorder: boolean;
|
|
164
|
+
canGroup: boolean;
|
|
100
165
|
};
|
|
101
166
|
|
|
102
167
|
export function getColumnCapabilities(
|
|
@@ -111,6 +176,9 @@ export function getColumnCapabilities(
|
|
|
111
176
|
canResize: features.resizing && column.getCanResize(),
|
|
112
177
|
// Ordering has no TanStack capability method — see isColumnReorderable.
|
|
113
178
|
canReorder: features.ordering && isColumnReorderable(column),
|
|
179
|
+
// `getCanGroup()` also insists on an accessor, which is what keeps the
|
|
180
|
+
// generated checkbox and tree columns from offering to group on themselves.
|
|
181
|
+
canGroup: features.grouping && column.getCanGroup(),
|
|
114
182
|
};
|
|
115
183
|
}
|
|
116
184
|
|
|
@@ -121,6 +189,7 @@ export type TMDataGridCapabilities = {
|
|
|
121
189
|
canHideAny: boolean;
|
|
122
190
|
canPinAny: boolean;
|
|
123
191
|
canReorderAny: boolean;
|
|
192
|
+
canGroupAny: boolean;
|
|
124
193
|
canSelectRows: boolean;
|
|
125
194
|
canPaginate: boolean;
|
|
126
195
|
};
|
|
@@ -139,6 +208,7 @@ export function getGridCapabilities(
|
|
|
139
208
|
canHideAny: any((c) => c.canHide),
|
|
140
209
|
canPinAny: any((c) => c.canPin),
|
|
141
210
|
canReorderAny: any((c) => c.canReorder),
|
|
211
|
+
canGroupAny: any((c) => c.canGroup),
|
|
142
212
|
canSelectRows: features.rowSelection,
|
|
143
213
|
canPaginate: features.pagination,
|
|
144
214
|
};
|
|
@@ -106,6 +106,31 @@ export function isFilterActive(value: unknown): boolean {
|
|
|
106
106
|
return !operatorNeedsValue(value.operator) || value.value.trim() !== "";
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
/**
|
|
110
|
+
* One-line description of a single filter, as shown on a filter pill.
|
|
111
|
+
*
|
|
112
|
+
* The type's default operator is left implicit — "First name: Sofia" reads the
|
|
113
|
+
* way a person would say it — while any other operator is spelled out, since
|
|
114
|
+
* that is the part a reader cannot guess.
|
|
115
|
+
*/
|
|
116
|
+
export function formatFilterLabel({
|
|
117
|
+
label,
|
|
118
|
+
type,
|
|
119
|
+
filter,
|
|
120
|
+
}: {
|
|
121
|
+
label: string;
|
|
122
|
+
type: TMDataGridColumnType;
|
|
123
|
+
filter: TMDataGridFilterValue;
|
|
124
|
+
}): string {
|
|
125
|
+
const { operator, value } = filter;
|
|
126
|
+
if (!operatorNeedsValue(operator)) {
|
|
127
|
+
return `${label} ${FILTER_OPERATOR_LABELS[operator]}`;
|
|
128
|
+
}
|
|
129
|
+
const text = value.trim();
|
|
130
|
+
if (operator === getDefaultOperator(type)) return `${label}: ${text}`;
|
|
131
|
+
return `${label} ${FILTER_OPERATOR_LABELS[operator]} ${text}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
109
134
|
export function matchesFilter(
|
|
110
135
|
cellValue: unknown,
|
|
111
136
|
{ operator, value }: TMDataGridFilterValue,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Row, RowData } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatures } from "../useTMDataGrid";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The data rows under a group row, at any depth.
|
|
6
|
+
*
|
|
7
|
+
* Not `row.getLeafRows()`, despite the name: that flattens the whole subtree
|
|
8
|
+
* and keeps the branches, so a group nested two deep reports its sub-groups
|
|
9
|
+
* alongside the records. Grouping `city` then `name` would have Stockholm
|
|
10
|
+
* counting four rows and three names as seven, and one tick on it selecting
|
|
11
|
+
* ids that hold no record.
|
|
12
|
+
*
|
|
13
|
+
* A row with no subRows is a data row, which also makes this the identity on an
|
|
14
|
+
* ungrouped grid.
|
|
15
|
+
*/
|
|
16
|
+
export function getGroupDataRows<TData extends RowData>(
|
|
17
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
18
|
+
): Array<Row<TMDataGridFeatures, TData>> {
|
|
19
|
+
if (row.subRows.length === 0) return [row];
|
|
20
|
+
return row.getLeafRows().filter((leaf) => leaf.subRows.length === 0);
|
|
21
|
+
}
|
|
@@ -28,6 +28,10 @@ export const DATA_STATE_SLICES = [
|
|
|
28
28
|
"globalFilter",
|
|
29
29
|
"sorting",
|
|
30
30
|
"pagination",
|
|
31
|
+
// Which groups are open. Keyed by group ids built from the values themselves
|
|
32
|
+
// (`department:Design`), so a restored entry for a group the data no longer
|
|
33
|
+
// produces is simply never looked up.
|
|
34
|
+
"expanded",
|
|
31
35
|
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
32
36
|
|
|
33
37
|
/**
|
|
@@ -39,6 +43,10 @@ export const SETTINGS_STATE_SLICES = [
|
|
|
39
43
|
"columnSizing",
|
|
40
44
|
"columnOrder",
|
|
41
45
|
"columnPinning",
|
|
46
|
+
// Which columns the rows are grouped by. A settings slice rather than a data
|
|
47
|
+
// one: it names columns, not values, so it survives the data changing under
|
|
48
|
+
// it the way the column layout does.
|
|
49
|
+
"grouping",
|
|
42
50
|
] as const satisfies ReadonlyArray<keyof GridState>;
|
|
43
51
|
|
|
44
52
|
export type TMDataGridDataSlice = (typeof DATA_STATE_SLICES)[number];
|
|
@@ -162,6 +170,9 @@ const SLICE_GUARDS: Record<
|
|
|
162
170
|
pageSize > 0
|
|
163
171
|
);
|
|
164
172
|
},
|
|
173
|
+
// `true` is a legal ExpandedState of its own, meaning every row is open.
|
|
174
|
+
expanded: (value) =>
|
|
175
|
+
value === true || isRecordOf(value, (entry) => typeof entry === "boolean"),
|
|
165
176
|
columnVisibility: (value) =>
|
|
166
177
|
isRecordOf(value, (entry) => typeof entry === "boolean"),
|
|
167
178
|
columnSizing: (value) => isRecordOf(value, isFiniteNumber),
|
|
@@ -169,6 +180,7 @@ const SLICE_GUARDS: Record<
|
|
|
169
180
|
columnPinning: (value) =>
|
|
170
181
|
isArrayOf(prop(value, "left"), isString) &&
|
|
171
182
|
isArrayOf(prop(value, "right"), isString),
|
|
183
|
+
grouping: (value) => isArrayOf(value, isString),
|
|
172
184
|
};
|
|
173
185
|
|
|
174
186
|
function isValidSlice(slice: keyof GridState, value: unknown): boolean {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import type { Row, RowData, RowSelectionState } from "@tanstack/react-table";
|
|
2
|
+
import type { TMDataGridFeatureFlags } from "./capabilities";
|
|
3
|
+
import { getGroupDataRows } from "./grouping";
|
|
4
|
+
import type { TMDataGridFeatures, TMDataGridTable } from "../useTMDataGrid";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rows in the order they are displayed, which is the order a shift-click range
|
|
8
|
+
* is measured over — so a range follows the active sort and skips filtered-out
|
|
9
|
+
* rows. Under pagination it is the current page, so a range cannot cross a page
|
|
10
|
+
* boundary.
|
|
11
|
+
*
|
|
12
|
+
* Once a column is grouped this includes the group rows, and only the leaves of
|
|
13
|
+
* groups that are open — and paging is suspended, so it is the whole tree. That
|
|
14
|
+
* is the right list for a range: it is what the user can see, and a range that
|
|
15
|
+
* swept collapsed rows would select things off screen. What a group row in the
|
|
16
|
+
* range contributes is decided by {@link getSelectableRowIds}.
|
|
17
|
+
*/
|
|
18
|
+
export function getDisplayedRows<TData extends RowData>(
|
|
19
|
+
table: TMDataGridTable<TData>,
|
|
20
|
+
features: TMDataGridFeatureFlags,
|
|
21
|
+
): Array<Row<TMDataGridFeatures, TData>> {
|
|
22
|
+
return isPagingActive(table, features)
|
|
23
|
+
? table.getPaginatedRowModel().rows
|
|
24
|
+
: table.getPrePaginatedRowModel().rows;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Whether the built-in pager is actually slicing anything.
|
|
29
|
+
*
|
|
30
|
+
* Grouping suspends it. A page can only count one kind of thing, and once the
|
|
31
|
+
* rows are a tree neither answer works: counting every row splits a group
|
|
32
|
+
* across a page boundary and strands the rest of the tree on pages the user has
|
|
33
|
+
* to go looking for, and counting only the top-level rows quietly redefines
|
|
34
|
+
* "rows per page" as groups per page, so a page of 25 can hold thousands of
|
|
35
|
+
* rows. Rather than pick, the grid renders the whole tree and leans on the
|
|
36
|
+
* virtualizer — which is its default mode anyway, pagination being opt-in.
|
|
37
|
+
*
|
|
38
|
+
* `TMDataGrid.Footer` reads this too, and greys the pager out rather than
|
|
39
|
+
* hiding it, so the pager going quiet is visible instead of mysterious.
|
|
40
|
+
*/
|
|
41
|
+
export function isPagingActive<TData extends RowData>(
|
|
42
|
+
table: TMDataGridTable<TData>,
|
|
43
|
+
features: TMDataGridFeatureFlags,
|
|
44
|
+
): boolean {
|
|
45
|
+
return features.pagination && table.store.state.grouping.length === 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Which modifiers were held. Named for what they mean rather than for the keys,
|
|
50
|
+
* because the keys differ per platform — `toggle` is Ctrl on Windows/Linux and
|
|
51
|
+
* Cmd on macOS.
|
|
52
|
+
*/
|
|
53
|
+
export type TMDataGridRowClickModifiers = {
|
|
54
|
+
toggle: boolean;
|
|
55
|
+
extend: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type ResolveRowSelectionClickArgs<TData extends RowData> = {
|
|
59
|
+
/** Displayed order — see {@link getDisplayedRows}. */
|
|
60
|
+
rows: ReadonlyArray<Row<TMDataGridFeatures, TData>>;
|
|
61
|
+
rowId: string;
|
|
62
|
+
/** The pivot a shift-click extends from. */
|
|
63
|
+
anchorRowId: string | null;
|
|
64
|
+
modifiers: TMDataGridRowClickModifiers;
|
|
65
|
+
selection: RowSelectionState;
|
|
66
|
+
/**
|
|
67
|
+
* Whether this gesture is allowed to clear rows it did not touch.
|
|
68
|
+
*
|
|
69
|
+
* `true` for a bare row click, where replacing is the whole point. `false` for
|
|
70
|
+
* a checkbox, which is only ever additive — ticking one box has never cleared
|
|
71
|
+
* the others, and shift-clicking one adds the range rather than becoming it.
|
|
72
|
+
*/
|
|
73
|
+
canReplaceSelection: boolean;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type ResolvedRowSelection = {
|
|
77
|
+
selection: RowSelectionState;
|
|
78
|
+
anchorRowId: string | null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The row ids one gesture on this row selects.
|
|
83
|
+
*
|
|
84
|
+
* A group row is never selected itself: it holds no record, and TanStack
|
|
85
|
+
* derives its checked state from its descendants. So ticking a group means
|
|
86
|
+
* ticking every selectable leaf under it, which is also what makes the group's
|
|
87
|
+
* own box light up.
|
|
88
|
+
*
|
|
89
|
+
* Leaves rather than direct children, so one click on a top-level group takes
|
|
90
|
+
* the whole subtree however deeply it nests.
|
|
91
|
+
*/
|
|
92
|
+
export function getSelectableRowIds<TData extends RowData>(
|
|
93
|
+
row: Row<TMDataGridFeatures, TData>,
|
|
94
|
+
): Array<string> {
|
|
95
|
+
return getGroupDataRows(row)
|
|
96
|
+
.filter((dataRow) => dataRow.getCanSelect())
|
|
97
|
+
.map((dataRow) => dataRow.id);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Turns one click into the next selection, following the convention every
|
|
102
|
+
* desktop list uses:
|
|
103
|
+
*
|
|
104
|
+
* | Gesture | Effect |
|
|
105
|
+
* | ------- | ------ |
|
|
106
|
+
* | Click | selection becomes only this row |
|
|
107
|
+
* | Ctrl/Cmd + click | toggle this row, leave the rest |
|
|
108
|
+
* | Shift + click | selection becomes the range anchor → this row |
|
|
109
|
+
* | Ctrl/Cmd + Shift + click | add that range to the selection |
|
|
110
|
+
*
|
|
111
|
+
* The anchor is a *pivot*, not "the last row touched": only a plain or
|
|
112
|
+
* Ctrl-click moves it, which is what lets a run of shift-clicks grow and shrink
|
|
113
|
+
* the same range from a fixed point.
|
|
114
|
+
*
|
|
115
|
+
* Selection state is rebuilt here and handed to `setRowSelection` wholesale
|
|
116
|
+
* rather than going through `row.toggleSelected()` per row, which would publish
|
|
117
|
+
* one state update each. That skips TanStack's sub-row cascade, so the cascade
|
|
118
|
+
* is done here instead: every row in the gesture is expanded through
|
|
119
|
+
* {@link getSelectableRowIds} before it reaches the map. Under grouping that is
|
|
120
|
+
* what makes one tick on a group select all of it.
|
|
121
|
+
*/
|
|
122
|
+
export function resolveRowSelectionClick<TData extends RowData>({
|
|
123
|
+
rows,
|
|
124
|
+
rowId,
|
|
125
|
+
anchorRowId,
|
|
126
|
+
modifiers,
|
|
127
|
+
selection,
|
|
128
|
+
canReplaceSelection,
|
|
129
|
+
}: ResolveRowSelectionClickArgs<TData>): ResolvedRowSelection {
|
|
130
|
+
const index = rows.findIndex((row) => row.id === rowId);
|
|
131
|
+
const clickedRow = rows[index];
|
|
132
|
+
// Clicked row is not in the displayed set — nothing sensible to resolve.
|
|
133
|
+
if (index === -1 || clickedRow === undefined) return { selection, anchorRowId };
|
|
134
|
+
|
|
135
|
+
const targets = getSelectableRowIds(clickedRow);
|
|
136
|
+
// Nothing selectable under this gesture: a row the predicate refuses, or a
|
|
137
|
+
// group whose every leaf refuses.
|
|
138
|
+
if (targets.length === 0) return { selection, anchorRowId };
|
|
139
|
+
|
|
140
|
+
// `enableMultiRowSelection: false`, or the per-row predicate saying no: one
|
|
141
|
+
// row at a time, so the modifiers have nothing to build on and every gesture
|
|
142
|
+
// collapses to a single id.
|
|
143
|
+
//
|
|
144
|
+
// This has to be enforced here. Rebuilding the map and handing it to
|
|
145
|
+
// `setRowSelection` skips `mutateRowIsSelected`, which is where TanStack
|
|
146
|
+
// clears the other rows — so without this, Ctrl and Shift would quietly
|
|
147
|
+
// produce the multi-selection the option ruled out.
|
|
148
|
+
if (!clickedRow.getCanMultiSelect()) {
|
|
149
|
+
// A group stands for several rows, which single-select cannot express. The
|
|
150
|
+
// checkbox column drops the box on those rows for the same reason.
|
|
151
|
+
const only = targets.length === 1 ? targets[0] : undefined;
|
|
152
|
+
if (only === undefined) return { selection, anchorRowId };
|
|
153
|
+
// A checkbox must still be able to untick the one selected row; a row click
|
|
154
|
+
// only ever sets, so re-clicking it cannot empty the selection.
|
|
155
|
+
const untick = !canReplaceSelection && selection[only] === true;
|
|
156
|
+
return {
|
|
157
|
+
selection: untick ? {} : { [only]: true },
|
|
158
|
+
anchorRowId: rowId,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const anchorIndex =
|
|
163
|
+
anchorRowId === null
|
|
164
|
+
? -1
|
|
165
|
+
: rows.findIndex((row) => row.id === anchorRowId);
|
|
166
|
+
|
|
167
|
+
// A range needs a pivot that is still on screen. When the anchor has been
|
|
168
|
+
// filtered or paged away, fall through and treat this as a fresh click.
|
|
169
|
+
if (modifiers.extend && anchorIndex !== -1) {
|
|
170
|
+
const from = Math.min(anchorIndex, index);
|
|
171
|
+
const to = Math.max(anchorIndex, index);
|
|
172
|
+
// Ctrl+Shift adds to what is already selected; Shift alone becomes the
|
|
173
|
+
// range — unless the gesture may not replace, in which case it also adds.
|
|
174
|
+
const next: RowSelectionState =
|
|
175
|
+
modifiers.toggle || !canReplaceSelection ? { ...selection } : {};
|
|
176
|
+
for (const row of rows.slice(from, to + 1)) {
|
|
177
|
+
// A group row inside the range brings its whole subtree, including leaves
|
|
178
|
+
// that are collapsed out of `rows` — sweeping past a closed group selects
|
|
179
|
+
// it entire, which is the only reading that matches what it displays.
|
|
180
|
+
for (const id of getSelectableRowIds(row)) next[id] = true;
|
|
181
|
+
}
|
|
182
|
+
// The pivot deliberately stays put.
|
|
183
|
+
return { selection: next, anchorRowId };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (modifiers.toggle || !canReplaceSelection) {
|
|
187
|
+
const next: RowSelectionState = { ...selection };
|
|
188
|
+
// Asked of the targets rather than of `rowId`: a group row carries no id in
|
|
189
|
+
// the map, so it counts as selected exactly when all of its leaves do —
|
|
190
|
+
// which is also when its checkbox shows a tick rather than a dash.
|
|
191
|
+
const allSelected = targets.every((id) => selection[id] === true);
|
|
192
|
+
for (const id of targets) {
|
|
193
|
+
if (allSelected) delete next[id];
|
|
194
|
+
else next[id] = true;
|
|
195
|
+
}
|
|
196
|
+
return { selection: next, anchorRowId: rowId };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const next: RowSelectionState = {};
|
|
200
|
+
for (const id of targets) next[id] = true;
|
|
201
|
+
return { selection: next, anchorRowId: rowId };
|
|
202
|
+
}
|
package/src/tmdatagrid/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ export {
|
|
|
5
5
|
type TMDataGridRowData,
|
|
6
6
|
} from "./TMDataGridContext";
|
|
7
7
|
export { SELECT_COLUMN_ID } from "./components/TMDataGridSelectColumn";
|
|
8
|
+
export {
|
|
9
|
+
formatGroupValue,
|
|
10
|
+
GROUP_COLUMN_ID,
|
|
11
|
+
} from "./components/TMDataGridGroupColumn";
|
|
8
12
|
export {
|
|
9
13
|
createTMDataGridColumnHelper,
|
|
10
14
|
openColumnFilter,
|
|
@@ -20,11 +24,16 @@ export {
|
|
|
20
24
|
useTMDataGrid,
|
|
21
25
|
type UseTMDataGridOptions,
|
|
22
26
|
} from "./useTMDataGrid";
|
|
27
|
+
export {
|
|
28
|
+
TMDataGridFilterPills,
|
|
29
|
+
type TMDataGridFilterPillsProps,
|
|
30
|
+
} from "./components/TMDataGridFilterPills";
|
|
23
31
|
export {
|
|
24
32
|
type TMDataGridColumnType,
|
|
25
33
|
type TMDataGridFilterOperator,
|
|
26
34
|
type TMDataGridFilterValue,
|
|
27
35
|
FILTER_OPERATOR_LABELS,
|
|
36
|
+
formatFilterLabel,
|
|
28
37
|
isFilterActive,
|
|
29
38
|
} from "./core/filterOperators";
|
|
30
39
|
export {
|
|
@@ -32,6 +41,7 @@ export {
|
|
|
32
41
|
getColumnType,
|
|
33
42
|
isColumnReorderable,
|
|
34
43
|
} from "./core/columnUtils";
|
|
44
|
+
export { getGroupDataRows } from "./core/grouping";
|
|
35
45
|
export {
|
|
36
46
|
getColumnRegion,
|
|
37
47
|
getStepTargetColumn,
|
|
@@ -49,8 +59,17 @@ export {
|
|
|
49
59
|
type TMDataGridCapabilities,
|
|
50
60
|
type TMDataGridColumnCapabilities,
|
|
51
61
|
type TMDataGridFeatureFlags,
|
|
52
|
-
type
|
|
62
|
+
type TMDataGridSelectionMode,
|
|
53
63
|
} from "./core/capabilities";
|
|
64
|
+
export {
|
|
65
|
+
getDisplayedRows,
|
|
66
|
+
getSelectableRowIds,
|
|
67
|
+
isPagingActive,
|
|
68
|
+
resolveRowSelectionClick,
|
|
69
|
+
type ResolvedRowSelection,
|
|
70
|
+
type ResolveRowSelectionClickArgs,
|
|
71
|
+
type TMDataGridRowClickModifiers,
|
|
72
|
+
} from "./core/rowSelection";
|
|
54
73
|
export {
|
|
55
74
|
DEFAULT_TMDATAGRID_SIZE,
|
|
56
75
|
SIZE_CONTROL_SIZE,
|
|
@@ -66,7 +85,12 @@ export {
|
|
|
66
85
|
type TMDataGridSettingsSlice,
|
|
67
86
|
type TMDataGridStorageMode,
|
|
68
87
|
} from "./core/persistence";
|
|
69
|
-
export type {
|
|
88
|
+
export type {
|
|
89
|
+
TMDataGridColumnLayout,
|
|
90
|
+
TMDataGridRowContextMenu,
|
|
91
|
+
TMDataGridRowContextMenuArgs,
|
|
92
|
+
TMDataGridTableProps,
|
|
93
|
+
} from "./components/TMDataGridTable";
|
|
70
94
|
export {
|
|
71
95
|
getTMDataGridPaginationApi,
|
|
72
96
|
type TMDataGridFooterProps,
|