@navikt/ds-react 8.10.2 → 8.10.3
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/cjs/data/table/column-header/DataTableColumnHeader.js +3 -1
- package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
- package/cjs/data/table/column-header/useTableColumnResize.d.ts +24 -2
- package/cjs/data/table/column-header/useTableColumnResize.js +31 -15
- package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
- package/cjs/data/table/helpers/collectTableRowEntries.d.ts +10 -2
- package/cjs/data/table/helpers/collectTableRowEntries.js +25 -17
- package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -1
- package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
- package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js +112 -0
- package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
- package/cjs/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
- package/cjs/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
- package/cjs/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
- package/cjs/data/table/helpers/selection/selection.types.d.ts +1 -0
- package/cjs/data/table/helpers/table-keyboard.d.ts +1 -2
- package/cjs/data/table/helpers/table-keyboard.js +1 -2
- package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
- package/cjs/data/table/hooks/useTableExpansion.d.ts +1 -3
- package/cjs/data/table/hooks/useTableExpansion.js +7 -1
- package/cjs/data/table/hooks/useTableExpansion.js.map +1 -1
- package/cjs/data/table/hooks/useTableItems.d.ts +7 -3
- package/cjs/data/table/hooks/useTableItems.js +18 -7
- package/cjs/data/table/hooks/useTableItems.js.map +1 -1
- package/cjs/data/table/hooks/useTableSelection.d.ts +3 -2
- package/cjs/data/table/hooks/useTableSelection.js +5 -4
- package/cjs/data/table/hooks/useTableSelection.js.map +1 -1
- package/cjs/data/table/root/DataTable.types.d.ts +5 -4
- package/cjs/data/table/root/DataTableAuto.d.ts +9 -1
- package/cjs/data/table/root/DataTableAuto.js +50 -50
- package/cjs/data/table/root/DataTableAuto.js.map +1 -1
- package/cjs/data/table/root/DataTableRoot.js +2 -3
- package/cjs/data/table/root/DataTableRoot.js.map +1 -1
- package/cjs/form/checkbox/Checkbox.js +1 -0
- package/cjs/form/checkbox/Checkbox.js.map +1 -1
- package/cjs/form/radio/Radio.js +7 -1
- package/cjs/form/radio/Radio.js.map +1 -1
- package/cjs/modal/types.d.ts +8 -4
- package/esm/data/table/column-header/DataTableColumnHeader.js +3 -1
- package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
- package/esm/data/table/column-header/useTableColumnResize.d.ts +24 -2
- package/esm/data/table/column-header/useTableColumnResize.js +32 -16
- package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
- package/esm/data/table/helpers/collectTableRowEntries.d.ts +10 -2
- package/esm/data/table/helpers/collectTableRowEntries.js +25 -17
- package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -1
- package/esm/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
- package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js +109 -0
- package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
- package/esm/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
- package/esm/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
- package/esm/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
- package/esm/data/table/helpers/selection/selection.types.d.ts +1 -0
- package/esm/data/table/helpers/table-keyboard.d.ts +1 -2
- package/esm/data/table/helpers/table-keyboard.js +1 -2
- package/esm/data/table/helpers/table-keyboard.js.map +1 -1
- package/esm/data/table/hooks/useTableExpansion.d.ts +1 -3
- package/esm/data/table/hooks/useTableExpansion.js +7 -1
- package/esm/data/table/hooks/useTableExpansion.js.map +1 -1
- package/esm/data/table/hooks/useTableItems.d.ts +7 -3
- package/esm/data/table/hooks/useTableItems.js +18 -7
- package/esm/data/table/hooks/useTableItems.js.map +1 -1
- package/esm/data/table/hooks/useTableSelection.d.ts +3 -2
- package/esm/data/table/hooks/useTableSelection.js +5 -4
- package/esm/data/table/hooks/useTableSelection.js.map +1 -1
- package/esm/data/table/root/DataTable.types.d.ts +5 -4
- package/esm/data/table/root/DataTableAuto.d.ts +9 -1
- package/esm/data/table/root/DataTableAuto.js +51 -51
- package/esm/data/table/root/DataTableAuto.js.map +1 -1
- package/esm/data/table/root/DataTableRoot.js +3 -4
- package/esm/data/table/root/DataTableRoot.js.map +1 -1
- package/esm/form/checkbox/Checkbox.js +1 -0
- package/esm/form/checkbox/Checkbox.js.map +1 -1
- package/esm/form/radio/Radio.js +7 -1
- package/esm/form/radio/Radio.js.map +1 -1
- package/esm/modal/types.d.ts +8 -4
- package/package.json +3 -3
- package/src/data/table/column-header/DataTableColumnHeader.tsx +5 -1
- package/src/data/table/column-header/useTableColumnResize.ts +73 -17
- package/src/data/table/helpers/collectTableRowEntries.ts +57 -25
- package/src/data/table/helpers/selection/SelectionSubtreeHelper.test.ts +66 -0
- package/src/data/table/helpers/selection/SelectionSubtreeHelper.ts +162 -0
- package/src/data/table/helpers/selection/getMultipleSelectProps.ts +57 -20
- package/src/data/table/helpers/selection/selection.types.ts +1 -0
- package/src/data/table/helpers/table-keyboard.ts +1 -2
- package/src/data/table/hooks/__tests__/useTableItems.test.ts +14 -0
- package/src/data/table/hooks/__tests__/useTableSelection.test.ts +132 -21
- package/src/data/table/hooks/useTableExpansion.tsx +8 -3
- package/src/data/table/hooks/useTableItems.ts +50 -27
- package/src/data/table/hooks/useTableSelection.ts +10 -6
- package/src/data/table/root/DataTable.types.ts +5 -4
- package/src/data/table/root/DataTableAuto.test.tsx +128 -2
- package/src/data/table/root/DataTableAuto.tsx +144 -135
- package/src/data/table/root/DataTableRoot.tsx +6 -7
- package/src/form/checkbox/Checkbox.tsx +1 -0
- package/src/form/radio/Radio.tsx +7 -1
- package/src/modal/types.ts +8 -4
- package/src/data/table/hooks/__tests__/useTableExpansion.test.tsx +0 -115
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { CheckboxInputProps } from "../../../../form/checkbox/checkbox-input/CheckboxInput";
|
|
2
|
+
import { SelectionSubtreeHelper } from "./SelectionSubtreeHelper";
|
|
2
3
|
|
|
3
4
|
type GetMultipleSelectPropsArgs = {
|
|
4
5
|
selectedKeysSet: Set<string | number>;
|
|
5
6
|
selectedKeys: (string | number)[];
|
|
6
7
|
setSelectedKeys: (keys: (string | number)[]) => void;
|
|
7
8
|
disabledKeysSet: Set<string | number>;
|
|
8
|
-
|
|
9
|
+
visibleRowIds: (string | number)[];
|
|
10
|
+
childRowIdsById?: Map<string | number, (string | number)[]>;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
13
|
function getMultipleSelectProps({
|
|
@@ -13,34 +15,56 @@ function getMultipleSelectProps({
|
|
|
13
15
|
selectedKeys,
|
|
14
16
|
setSelectedKeys,
|
|
15
17
|
disabledKeysSet,
|
|
16
|
-
|
|
18
|
+
visibleRowIds,
|
|
19
|
+
childRowIdsById,
|
|
17
20
|
}: GetMultipleSelectPropsArgs) {
|
|
18
|
-
const
|
|
19
|
-
|
|
21
|
+
const subtreeHelper = new SelectionSubtreeHelper({
|
|
22
|
+
childRowIdsById,
|
|
23
|
+
disabledKeysSet,
|
|
24
|
+
selectedKeysSet,
|
|
25
|
+
});
|
|
20
26
|
|
|
21
|
-
|
|
27
|
+
// Header selection traverses the visible roots and skips already visited
|
|
28
|
+
// descendants, so expanded trees stay linear in the number of rows.
|
|
29
|
+
const headerSelectableKeys = subtreeHelper.getSelectableKeys(visibleRowIds);
|
|
30
|
+
const headerSelectableKeysSet = new Set(headerSelectableKeys);
|
|
31
|
+
|
|
32
|
+
const selectedSelectableCount = headerSelectableKeys.filter((k) =>
|
|
22
33
|
selectedKeysSet.has(k),
|
|
23
34
|
).length;
|
|
24
35
|
|
|
25
36
|
const allSelectableSelected =
|
|
26
|
-
|
|
27
|
-
selectedSelectableCount ===
|
|
37
|
+
headerSelectableKeys.length > 0 &&
|
|
38
|
+
selectedSelectableCount === headerSelectableKeys.length;
|
|
28
39
|
|
|
29
40
|
const indeterminate =
|
|
30
41
|
selectedSelectableCount > 0 &&
|
|
31
|
-
selectedSelectableCount <
|
|
42
|
+
selectedSelectableCount < headerSelectableKeys.length;
|
|
32
43
|
|
|
33
44
|
const selectedKeysNotInView = selectedKeys.filter(
|
|
34
|
-
(k) => !
|
|
45
|
+
(k) => !headerSelectableKeysSet.has(k),
|
|
35
46
|
);
|
|
36
47
|
const disabledSelected = selectedKeys.filter((k) => disabledKeysSet.has(k));
|
|
37
|
-
const preservedKeys = [
|
|
48
|
+
const preservedKeys = [
|
|
49
|
+
...new Set([...selectedKeysNotInView, ...disabledSelected]),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const isGroupFullySelected = (key: string | number) => {
|
|
53
|
+
const groupStats = subtreeHelper.getSelectionStats(key);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
groupStats.selectableCount > 0 &&
|
|
57
|
+
groupStats.selectedCount === groupStats.selectableCount
|
|
58
|
+
);
|
|
59
|
+
};
|
|
38
60
|
|
|
39
61
|
const handleToggleAll = () => {
|
|
40
62
|
if (allSelectableSelected) {
|
|
41
63
|
setSelectedKeys(preservedKeys);
|
|
42
64
|
} else {
|
|
43
|
-
setSelectedKeys([
|
|
65
|
+
setSelectedKeys([
|
|
66
|
+
...new Set([...preservedKeys, ...headerSelectableKeys]),
|
|
67
|
+
]);
|
|
44
68
|
}
|
|
45
69
|
};
|
|
46
70
|
|
|
@@ -48,10 +72,16 @@ function getMultipleSelectProps({
|
|
|
48
72
|
if (disabledKeysSet.has(key)) {
|
|
49
73
|
return;
|
|
50
74
|
}
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
|
|
76
|
+
const groupKeys = subtreeHelper.getSelectableKeys([key]);
|
|
77
|
+
|
|
78
|
+
if (isGroupFullySelected(key)) {
|
|
79
|
+
const groupKeysSet = new Set(groupKeys);
|
|
80
|
+
setSelectedKeys(
|
|
81
|
+
selectedKeys.filter((selectedKey) => !groupKeysSet.has(selectedKey)),
|
|
82
|
+
);
|
|
53
83
|
} else {
|
|
54
|
-
setSelectedKeys([...selectedKeys,
|
|
84
|
+
setSelectedKeys([...new Set([...selectedKeys, ...groupKeys])]);
|
|
55
85
|
}
|
|
56
86
|
};
|
|
57
87
|
|
|
@@ -60,13 +90,20 @@ function getMultipleSelectProps({
|
|
|
60
90
|
onChange: handleToggleAll,
|
|
61
91
|
checked: allSelectableSelected,
|
|
62
92
|
indeterminate,
|
|
63
|
-
disabled:
|
|
64
|
-
}),
|
|
65
|
-
getRowCheckboxProps: (key: string | number): CheckboxInputProps => ({
|
|
66
|
-
onChange: () => handleToggleRow(key),
|
|
67
|
-
checked: selectedKeysSet.has(key),
|
|
68
|
-
disabled: disabledKeysSet.has(key),
|
|
93
|
+
disabled: headerSelectableKeys.length === 0,
|
|
69
94
|
}),
|
|
95
|
+
getRowCheckboxProps: (key: string | number): CheckboxInputProps => {
|
|
96
|
+
const groupStats = subtreeHelper.getSelectionStats(key);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
onChange: () => handleToggleRow(key),
|
|
100
|
+
checked: isGroupFullySelected(key),
|
|
101
|
+
indeterminate:
|
|
102
|
+
groupStats.selectedCount > 0 &&
|
|
103
|
+
groupStats.selectedCount < groupStats.selectableCount,
|
|
104
|
+
disabled: disabledKeysSet.has(key),
|
|
105
|
+
};
|
|
106
|
+
},
|
|
70
107
|
toggleSelection: handleToggleRow,
|
|
71
108
|
};
|
|
72
109
|
}
|
|
@@ -10,6 +10,7 @@ type SelectionProps = {
|
|
|
10
10
|
* When set to "single", only one row can be selected at a time (renders radio buttons).
|
|
11
11
|
*
|
|
12
12
|
* When set to "multiple", multiple rows can be selected (renders checkboxes).
|
|
13
|
+
* Nested rows use cascading selection, so selecting a parent toggles its descendants too.
|
|
13
14
|
*
|
|
14
15
|
* @default "none"
|
|
15
16
|
*/
|
|
@@ -16,8 +16,7 @@ type NavigationAction =
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Maps keyboard events to navigation actions.
|
|
19
|
-
* Supports arrow keys, Home/End (row navigation), Ctrl/Cmd+Home/End (table navigation)
|
|
20
|
-
* and PageUp/PageDown (multi-row navigation).
|
|
19
|
+
* Supports arrow keys, Home/End (row navigation), Ctrl/Cmd+Home/End (table navigation).
|
|
21
20
|
*/
|
|
22
21
|
function getNavigationAction(event: KeyboardEvent): NavigationAction | null {
|
|
23
22
|
const key = event.key;
|
|
@@ -86,6 +86,20 @@ describe("useTableItems", () => {
|
|
|
86
86
|
expect(getVisibleIds(result.current.items)).toEqual(["a", "a1", "a2", "b"]);
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
test("collects direct child row ids even when nested rows are collapsed", () => {
|
|
90
|
+
const { result } = renderHook(() =>
|
|
91
|
+
useTableItems({
|
|
92
|
+
items: nestedRows,
|
|
93
|
+
getRowId: (row) => row.id,
|
|
94
|
+
getSubRows,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.current.childRowIdsById.get("a")).toEqual(["a1", "a2"]);
|
|
99
|
+
expect(result.current.childRowIdsById.get("a2")).toEqual(["a2a"]);
|
|
100
|
+
expect(result.current.childRowIdsById.get("b")).toEqual(["b1"]);
|
|
101
|
+
});
|
|
102
|
+
|
|
89
103
|
test("uses the same fallback root id to reveal child rows when getRowId is omitted", () => {
|
|
90
104
|
const { result } = renderHook(() =>
|
|
91
105
|
useTableItems({
|
|
@@ -17,7 +17,13 @@ const items: Item[] = [
|
|
|
17
17
|
{ id: "c", name: "Charlie" },
|
|
18
18
|
];
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const visibleRowIds = items.map((item) => item.id);
|
|
21
|
+
const childRowIdsById = new Map<string | number, (string | number)[]>([
|
|
22
|
+
["a", ["a1", "a2"]],
|
|
23
|
+
["a1", []],
|
|
24
|
+
["a2", ["a2a"]],
|
|
25
|
+
["a2a", []],
|
|
26
|
+
]);
|
|
21
27
|
|
|
22
28
|
function asSingle(result: {
|
|
23
29
|
current: UseTableSelectionReturn;
|
|
@@ -37,7 +43,7 @@ describe("useTableSelection", () => {
|
|
|
37
43
|
const { result } = renderHook(() =>
|
|
38
44
|
useTableSelection({
|
|
39
45
|
selectionMode: "none",
|
|
40
|
-
|
|
46
|
+
visibleRowIds,
|
|
41
47
|
}),
|
|
42
48
|
);
|
|
43
49
|
|
|
@@ -51,7 +57,7 @@ describe("useTableSelection", () => {
|
|
|
51
57
|
const { result } = renderHook(() =>
|
|
52
58
|
useTableSelection({
|
|
53
59
|
selectionMode: "single",
|
|
54
|
-
|
|
60
|
+
visibleRowIds,
|
|
55
61
|
}),
|
|
56
62
|
);
|
|
57
63
|
|
|
@@ -64,7 +70,7 @@ describe("useTableSelection", () => {
|
|
|
64
70
|
const { result } = renderHook(() =>
|
|
65
71
|
useTableSelection({
|
|
66
72
|
selectionMode: "single",
|
|
67
|
-
|
|
73
|
+
visibleRowIds,
|
|
68
74
|
onSelectionChange: onChange,
|
|
69
75
|
}),
|
|
70
76
|
);
|
|
@@ -83,7 +89,7 @@ describe("useTableSelection", () => {
|
|
|
83
89
|
const { result } = renderHook(() =>
|
|
84
90
|
useTableSelection({
|
|
85
91
|
selectionMode: "single",
|
|
86
|
-
|
|
92
|
+
visibleRowIds,
|
|
87
93
|
defaultSelectedKeys: ["a"],
|
|
88
94
|
}),
|
|
89
95
|
);
|
|
@@ -101,7 +107,7 @@ describe("useTableSelection", () => {
|
|
|
101
107
|
const { result } = renderHook(() =>
|
|
102
108
|
useTableSelection({
|
|
103
109
|
selectionMode: "single",
|
|
104
|
-
|
|
110
|
+
visibleRowIds,
|
|
105
111
|
defaultSelectedKeys: ["a"],
|
|
106
112
|
}),
|
|
107
113
|
);
|
|
@@ -119,7 +125,7 @@ describe("useTableSelection", () => {
|
|
|
119
125
|
const { result } = renderHook(() =>
|
|
120
126
|
useTableSelection({
|
|
121
127
|
selectionMode: "single",
|
|
122
|
-
|
|
128
|
+
visibleRowIds,
|
|
123
129
|
disabledSelectionKeys: ["b"],
|
|
124
130
|
}),
|
|
125
131
|
);
|
|
@@ -133,7 +139,7 @@ describe("useTableSelection", () => {
|
|
|
133
139
|
({ selectedKeys }) =>
|
|
134
140
|
useTableSelection({
|
|
135
141
|
selectionMode: "single",
|
|
136
|
-
|
|
142
|
+
visibleRowIds,
|
|
137
143
|
selectedKeys,
|
|
138
144
|
}),
|
|
139
145
|
{ initialProps: { selectedKeys: ["a"] as (string | number)[] } },
|
|
@@ -151,7 +157,7 @@ describe("useTableSelection", () => {
|
|
|
151
157
|
const { result } = renderHook(() =>
|
|
152
158
|
useTableSelection({
|
|
153
159
|
selectionMode: "multiple",
|
|
154
|
-
|
|
160
|
+
visibleRowIds,
|
|
155
161
|
}),
|
|
156
162
|
);
|
|
157
163
|
|
|
@@ -164,7 +170,7 @@ describe("useTableSelection", () => {
|
|
|
164
170
|
const { result } = renderHook(() =>
|
|
165
171
|
useTableSelection({
|
|
166
172
|
selectionMode: "multiple",
|
|
167
|
-
|
|
173
|
+
visibleRowIds,
|
|
168
174
|
}),
|
|
169
175
|
);
|
|
170
176
|
|
|
@@ -189,7 +195,7 @@ describe("useTableSelection", () => {
|
|
|
189
195
|
const { result } = renderHook(() =>
|
|
190
196
|
useTableSelection({
|
|
191
197
|
selectionMode: "multiple",
|
|
192
|
-
|
|
198
|
+
visibleRowIds,
|
|
193
199
|
defaultSelectedKeys: ["a", "b"],
|
|
194
200
|
}),
|
|
195
201
|
);
|
|
@@ -207,7 +213,7 @@ describe("useTableSelection", () => {
|
|
|
207
213
|
const { result } = renderHook(() =>
|
|
208
214
|
useTableSelection({
|
|
209
215
|
selectionMode: "multiple",
|
|
210
|
-
|
|
216
|
+
visibleRowIds,
|
|
211
217
|
}),
|
|
212
218
|
);
|
|
213
219
|
|
|
@@ -220,11 +226,29 @@ describe("useTableSelection", () => {
|
|
|
220
226
|
expect(asMultiple(result).selectedKeys).toEqual(["a", "b", "c"]);
|
|
221
227
|
});
|
|
222
228
|
|
|
229
|
+
test("select all via thead includes hidden descendants for visible parents", () => {
|
|
230
|
+
const { result } = renderHook(() =>
|
|
231
|
+
useTableSelection({
|
|
232
|
+
selectionMode: "multiple",
|
|
233
|
+
visibleRowIds: ["a"],
|
|
234
|
+
childRowIdsById,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
act(() => {
|
|
239
|
+
asMultiple(result)
|
|
240
|
+
.getTheadCheckboxProps()
|
|
241
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
|
|
245
|
+
});
|
|
246
|
+
|
|
223
247
|
test("deselect all when all are selected", () => {
|
|
224
248
|
const { result } = renderHook(() =>
|
|
225
249
|
useTableSelection({
|
|
226
250
|
selectionMode: "multiple",
|
|
227
|
-
|
|
251
|
+
visibleRowIds,
|
|
228
252
|
defaultSelectedKeys: ["a", "b", "c"],
|
|
229
253
|
}),
|
|
230
254
|
);
|
|
@@ -238,11 +262,30 @@ describe("useTableSelection", () => {
|
|
|
238
262
|
expect(asMultiple(result).selectedKeys).toEqual([]);
|
|
239
263
|
});
|
|
240
264
|
|
|
265
|
+
test("deselect all clears hidden descendants for visible parents but preserves unrelated keys", () => {
|
|
266
|
+
const { result } = renderHook(() =>
|
|
267
|
+
useTableSelection({
|
|
268
|
+
selectionMode: "multiple",
|
|
269
|
+
visibleRowIds: ["a"],
|
|
270
|
+
childRowIdsById,
|
|
271
|
+
defaultSelectedKeys: ["a", "a1", "a2", "a2a", "external"],
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
act(() => {
|
|
276
|
+
asMultiple(result)
|
|
277
|
+
.getTheadCheckboxProps()
|
|
278
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
expect(asMultiple(result).selectedKeys).toEqual(["external"]);
|
|
282
|
+
});
|
|
283
|
+
|
|
241
284
|
test("select all skips disabled keys", () => {
|
|
242
285
|
const { result } = renderHook(() =>
|
|
243
286
|
useTableSelection({
|
|
244
287
|
selectionMode: "multiple",
|
|
245
|
-
|
|
288
|
+
visibleRowIds,
|
|
246
289
|
disabledSelectionKeys: ["b"],
|
|
247
290
|
}),
|
|
248
291
|
);
|
|
@@ -260,7 +303,7 @@ describe("useTableSelection", () => {
|
|
|
260
303
|
const { result } = renderHook(() =>
|
|
261
304
|
useTableSelection({
|
|
262
305
|
selectionMode: "multiple",
|
|
263
|
-
|
|
306
|
+
visibleRowIds,
|
|
264
307
|
defaultSelectedKeys: ["a", "b", "c"],
|
|
265
308
|
disabledSelectionKeys: ["b"],
|
|
266
309
|
}),
|
|
@@ -279,7 +322,7 @@ describe("useTableSelection", () => {
|
|
|
279
322
|
const { result } = renderHook(() =>
|
|
280
323
|
useTableSelection({
|
|
281
324
|
selectionMode: "multiple",
|
|
282
|
-
|
|
325
|
+
visibleRowIds,
|
|
283
326
|
defaultSelectedKeys: ["a"],
|
|
284
327
|
}),
|
|
285
328
|
);
|
|
@@ -293,7 +336,7 @@ describe("useTableSelection", () => {
|
|
|
293
336
|
const { result } = renderHook(() =>
|
|
294
337
|
useTableSelection({
|
|
295
338
|
selectionMode: "multiple",
|
|
296
|
-
|
|
339
|
+
visibleRowIds,
|
|
297
340
|
defaultSelectedKeys: ["a", "b", "c"],
|
|
298
341
|
}),
|
|
299
342
|
);
|
|
@@ -307,7 +350,7 @@ describe("useTableSelection", () => {
|
|
|
307
350
|
const { result } = renderHook(() =>
|
|
308
351
|
useTableSelection({
|
|
309
352
|
selectionMode: "multiple",
|
|
310
|
-
|
|
353
|
+
visibleRowIds,
|
|
311
354
|
defaultSelectedKeys: ["a", "c"],
|
|
312
355
|
disabledSelectionKeys: ["b"],
|
|
313
356
|
}),
|
|
@@ -322,7 +365,7 @@ describe("useTableSelection", () => {
|
|
|
322
365
|
const { result } = renderHook(() =>
|
|
323
366
|
useTableSelection({
|
|
324
367
|
selectionMode: "multiple",
|
|
325
|
-
|
|
368
|
+
visibleRowIds,
|
|
326
369
|
defaultSelectedKeys: ["a", "b", "c"],
|
|
327
370
|
}),
|
|
328
371
|
);
|
|
@@ -340,7 +383,7 @@ describe("useTableSelection", () => {
|
|
|
340
383
|
const { result } = renderHook(() =>
|
|
341
384
|
useTableSelection({
|
|
342
385
|
selectionMode: "multiple",
|
|
343
|
-
|
|
386
|
+
visibleRowIds,
|
|
344
387
|
disabledSelectionKeys: ["b"],
|
|
345
388
|
}),
|
|
346
389
|
);
|
|
@@ -353,12 +396,80 @@ describe("useTableSelection", () => {
|
|
|
353
396
|
const { result } = renderHook(() =>
|
|
354
397
|
useTableSelection({
|
|
355
398
|
selectionMode: "multiple",
|
|
356
|
-
|
|
399
|
+
visibleRowIds,
|
|
357
400
|
disabledSelectionKeys: ["a", "b", "c"],
|
|
358
401
|
}),
|
|
359
402
|
);
|
|
360
403
|
|
|
361
404
|
expect(asMultiple(result).getTheadCheckboxProps().disabled).toBe(true);
|
|
362
405
|
});
|
|
406
|
+
|
|
407
|
+
test("parent rows show indeterminate when visible descendants are partially selected", () => {
|
|
408
|
+
const { result } = renderHook(() =>
|
|
409
|
+
useTableSelection({
|
|
410
|
+
selectionMode: "multiple",
|
|
411
|
+
visibleRowIds: ["a", "a1", "a2"],
|
|
412
|
+
childRowIdsById,
|
|
413
|
+
defaultSelectedKeys: ["a1"],
|
|
414
|
+
}),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const parentProps = asMultiple(result).getRowCheckboxProps("a");
|
|
418
|
+
|
|
419
|
+
expect(parentProps.checked).toBe(false);
|
|
420
|
+
expect(parentProps.indeterminate).toBe(true);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
test("toggling a parent row selects and deselects its descendants", () => {
|
|
424
|
+
const { result } = renderHook(() =>
|
|
425
|
+
useTableSelection({
|
|
426
|
+
selectionMode: "multiple",
|
|
427
|
+
visibleRowIds: ["a", "a1", "a2"],
|
|
428
|
+
childRowIdsById,
|
|
429
|
+
}),
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
act(() => {
|
|
433
|
+
asMultiple(result)
|
|
434
|
+
.getRowCheckboxProps("a")
|
|
435
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
|
|
439
|
+
|
|
440
|
+
act(() => {
|
|
441
|
+
asMultiple(result)
|
|
442
|
+
.getRowCheckboxProps("a")
|
|
443
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
expect(asMultiple(result).selectedKeys).toEqual([]);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("toggling a collapsed parent selects and deselects hidden descendants", () => {
|
|
450
|
+
const { result } = renderHook(() =>
|
|
451
|
+
useTableSelection({
|
|
452
|
+
selectionMode: "multiple",
|
|
453
|
+
visibleRowIds: ["a"],
|
|
454
|
+
childRowIdsById,
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
act(() => {
|
|
459
|
+
asMultiple(result)
|
|
460
|
+
.getRowCheckboxProps("a")
|
|
461
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
|
|
465
|
+
|
|
466
|
+
act(() => {
|
|
467
|
+
asMultiple(result)
|
|
468
|
+
.getRowCheckboxProps("a")
|
|
469
|
+
.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
expect(asMultiple(result).selectedKeys).toEqual([]);
|
|
473
|
+
});
|
|
363
474
|
});
|
|
364
475
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useCallback } from "react";
|
|
2
2
|
import { createStrictContext } from "../../../utils/helpers";
|
|
3
3
|
import { useControllableState } from "../../../utils/hooks";
|
|
4
|
-
import
|
|
4
|
+
import { useTableItemsContext } from "./useTableItems";
|
|
5
5
|
|
|
6
6
|
type DataTableExpansionContextT = {
|
|
7
7
|
isExpanded: (id: string | number) => boolean;
|
|
@@ -28,7 +28,6 @@ type TableExpansionOptions<T> = {
|
|
|
28
28
|
detailsPanelRowIds?: (string | number)[];
|
|
29
29
|
defaultDetailsPanelRowIds?: (string | number)[];
|
|
30
30
|
onDetailsPanelChange?: (ids: (string | number)[]) => void;
|
|
31
|
-
itemDetails: Map<T, ItemDetail<T>>;
|
|
32
31
|
getDetailsPanelContent?: (row: T) => React.ReactNode;
|
|
33
32
|
isDetailsPanelExpandable?: (rowData: T) => boolean;
|
|
34
33
|
getDetailsPanelHeight?: (row: T) => number | "auto";
|
|
@@ -44,7 +43,6 @@ function DataTableExpansionProvider<T>({
|
|
|
44
43
|
detailsPanelRowIds,
|
|
45
44
|
defaultDetailsPanelRowIds = [],
|
|
46
45
|
onDetailsPanelChange,
|
|
47
|
-
itemDetails,
|
|
48
46
|
getDetailsPanelContent,
|
|
49
47
|
isDetailsPanelExpandable,
|
|
50
48
|
getDetailsPanelHeight,
|
|
@@ -56,6 +54,13 @@ function DataTableExpansionProvider<T>({
|
|
|
56
54
|
onChange: onDetailsPanelChange,
|
|
57
55
|
});
|
|
58
56
|
|
|
57
|
+
/* TODO: False is just fallback until auto and root is merged */
|
|
58
|
+
const tableItemsContext = useTableItemsContext(false);
|
|
59
|
+
|
|
60
|
+
const { itemDetails } = tableItemsContext ?? {
|
|
61
|
+
itemDetails: new Map(),
|
|
62
|
+
};
|
|
63
|
+
|
|
59
64
|
const expandableIds = React.useMemo(() => {
|
|
60
65
|
if (!getDetailsPanelContent) {
|
|
61
66
|
return new Set<string | number>();
|
|
@@ -3,6 +3,7 @@ import { createStrictContext } from "../../../utils/helpers";
|
|
|
3
3
|
import { useControllableState } from "../../../utils/hooks";
|
|
4
4
|
import {
|
|
5
5
|
type ItemDetail,
|
|
6
|
+
type TableRowEntryId,
|
|
6
7
|
collectTableRowEntries,
|
|
7
8
|
} from "../helpers/collectTableRowEntries";
|
|
8
9
|
|
|
@@ -32,6 +33,10 @@ type UseTableItemsArgs<T> = {
|
|
|
32
33
|
type useTableItemsReturn<T> = {
|
|
33
34
|
items: T[];
|
|
34
35
|
itemDetails: Map<T, ItemDetail<T>>;
|
|
36
|
+
/** Row ids for the rows currently rendered in the table body. */
|
|
37
|
+
visibleRowIds: TableRowEntryId[];
|
|
38
|
+
/** Direct child ids for each row, used to traverse selection groups lazily. */
|
|
39
|
+
childRowIdsById: Map<TableRowEntryId, TableRowEntryId[]>;
|
|
35
40
|
onExpandedSubRowIdsChange: (id: string | number) => void;
|
|
36
41
|
isSubRowExpanded: (id: string | number) => boolean;
|
|
37
42
|
};
|
|
@@ -59,38 +64,52 @@ function useTableItems<T>(args: UseTableItemsArgs<T>): useTableItemsReturn<T> {
|
|
|
59
64
|
[nestedSubRowsExpandedIds],
|
|
60
65
|
);
|
|
61
66
|
|
|
62
|
-
const { itemDetails, visibleItems } =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
const { itemDetails, visibleItems, visibleRowIds, childRowIdsById } =
|
|
68
|
+
useMemo(() => {
|
|
69
|
+
const { itemDetails: rowEntriesMap, childRowIdsById: _childRowIdsById } =
|
|
70
|
+
collectTableRowEntries({
|
|
71
|
+
items,
|
|
72
|
+
getRowId,
|
|
73
|
+
getSubRows,
|
|
74
|
+
isSubRowExpandable,
|
|
75
|
+
});
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
localVisibleItems.push(rowData);
|
|
77
|
+
const localVisibleItems: T[] = [];
|
|
78
|
+
const localVisibleRowIds: TableRowEntryId[] = [];
|
|
73
79
|
|
|
74
|
-
const
|
|
80
|
+
const addVisibleRows = (rowData: T): TableRowEntryId[] => {
|
|
81
|
+
const details = rowEntriesMap.get(rowData);
|
|
75
82
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
if (!details) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
79
86
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
localVisibleItems.push(rowData);
|
|
88
|
+
localVisibleRowIds.push(details.id);
|
|
89
|
+
|
|
90
|
+
const visibleDescendantRowIds: TableRowEntryId[] = [];
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
92
|
+
if (expandedIdsSet.has(details.id)) {
|
|
93
|
+
for (const childRow of details.children) {
|
|
94
|
+
const childVisibleRowIds = addVisibleRows(childRow);
|
|
95
|
+
visibleDescendantRowIds.push(...childVisibleRowIds);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return [details.id, ...visibleDescendantRowIds];
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
for (const rowData of items) {
|
|
103
|
+
addVisibleRows(rowData);
|
|
104
|
+
}
|
|
88
105
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
return {
|
|
107
|
+
visibleItems: localVisibleItems,
|
|
108
|
+
visibleRowIds: localVisibleRowIds,
|
|
109
|
+
childRowIdsById: _childRowIdsById,
|
|
110
|
+
itemDetails: rowEntriesMap,
|
|
111
|
+
};
|
|
112
|
+
}, [getSubRows, items, getRowId, isSubRowExpandable, expandedIdsSet]);
|
|
94
113
|
|
|
95
114
|
const handleExpandedSubRowIdChange = useCallback(
|
|
96
115
|
(id: string | number) => {
|
|
@@ -106,6 +125,8 @@ function useTableItems<T>(args: UseTableItemsArgs<T>): useTableItemsReturn<T> {
|
|
|
106
125
|
return {
|
|
107
126
|
items: visibleItems,
|
|
108
127
|
itemDetails,
|
|
128
|
+
visibleRowIds,
|
|
129
|
+
childRowIdsById,
|
|
109
130
|
onExpandedSubRowIdsChange: handleExpandedSubRowIdChange,
|
|
110
131
|
isSubRowExpanded: (id: string | number) => expandedIdsSet.has(id),
|
|
111
132
|
};
|
|
@@ -113,7 +134,9 @@ function useTableItems<T>(args: UseTableItemsArgs<T>): useTableItemsReturn<T> {
|
|
|
113
134
|
|
|
114
135
|
const { Provider: TableItemsProvider, useContext: useTableItemsContext } =
|
|
115
136
|
/* TODO: Can we type this better? */
|
|
116
|
-
createStrictContext<
|
|
137
|
+
createStrictContext<
|
|
138
|
+
Omit<useTableItemsReturn<any>, "visibleRowIds" | "childRowIdsById">
|
|
139
|
+
>({
|
|
117
140
|
name: "TableItemsContext",
|
|
118
141
|
errorMessage:
|
|
119
142
|
"useTableItemsContext must be used within a TableItemsProvider",
|
|
@@ -10,8 +10,10 @@ import type {
|
|
|
10
10
|
} from "../helpers/selection/selection.types";
|
|
11
11
|
|
|
12
12
|
type UseTableSelectionArgs = SelectionProps & {
|
|
13
|
-
/*
|
|
14
|
-
|
|
13
|
+
/* Visible rows manage the header checkbox state and render selection cells. */
|
|
14
|
+
visibleRowIds: (string | number)[];
|
|
15
|
+
/* Direct child ids let selection walk nested rows lazily. */
|
|
16
|
+
childRowIdsById?: Map<string | number, (string | number)[]>;
|
|
15
17
|
};
|
|
16
18
|
|
|
17
19
|
type UseTableSelectionReturn = {
|
|
@@ -25,7 +27,8 @@ function useTableSelection({
|
|
|
25
27
|
selectedKeys: selectedKeysProp,
|
|
26
28
|
onSelectionChange,
|
|
27
29
|
disabledSelectionKeys = [],
|
|
28
|
-
|
|
30
|
+
visibleRowIds = [],
|
|
31
|
+
childRowIdsById,
|
|
29
32
|
}: UseTableSelectionArgs): UseTableSelectionReturn {
|
|
30
33
|
const radioGroupName = useId();
|
|
31
34
|
|
|
@@ -72,7 +75,7 @@ function useTableSelection({
|
|
|
72
75
|
name: radioGroupName,
|
|
73
76
|
}),
|
|
74
77
|
},
|
|
75
|
-
renderSelection:
|
|
78
|
+
renderSelection: visibleRowIds.length !== 0,
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
|
|
@@ -85,10 +88,11 @@ function useTableSelection({
|
|
|
85
88
|
selectedKeys,
|
|
86
89
|
setSelectedKeys,
|
|
87
90
|
disabledKeysSet,
|
|
88
|
-
|
|
91
|
+
visibleRowIds,
|
|
92
|
+
childRowIdsById,
|
|
89
93
|
}),
|
|
90
94
|
},
|
|
91
|
-
renderSelection:
|
|
95
|
+
renderSelection: visibleRowIds.length !== 0,
|
|
92
96
|
};
|
|
93
97
|
}
|
|
94
98
|
|