@navikt/ds-react 8.10.1 → 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.
Files changed (114) hide show
  1. package/cjs/data/table/column-header/DataTableColumnHeader.d.ts +1 -1
  2. package/cjs/data/table/column-header/DataTableColumnHeader.js +16 -11
  3. package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  4. package/cjs/data/table/column-header/useTableColumnResize.d.ts +28 -4
  5. package/cjs/data/table/column-header/useTableColumnResize.js +144 -53
  6. package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
  7. package/cjs/data/table/helpers/collectTableRowEntries.d.ts +24 -0
  8. package/cjs/data/table/helpers/collectTableRowEntries.js +35 -0
  9. package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -0
  10. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
  11. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js +112 -0
  12. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
  13. package/cjs/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
  14. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
  15. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
  16. package/cjs/data/table/helpers/selection/selection.types.d.ts +1 -0
  17. package/cjs/data/table/helpers/table-keyboard.d.ts +1 -2
  18. package/cjs/data/table/helpers/table-keyboard.js +1 -5
  19. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  20. package/cjs/data/table/hooks/useTableExpansion.d.ts +7 -6
  21. package/cjs/data/table/hooks/useTableExpansion.js +42 -15
  22. package/cjs/data/table/hooks/useTableExpansion.js.map +1 -1
  23. package/cjs/data/table/hooks/useTableItems.d.ts +33 -0
  24. package/cjs/data/table/hooks/useTableItems.js +74 -0
  25. package/cjs/data/table/hooks/useTableItems.js.map +1 -0
  26. package/cjs/data/table/hooks/useTableKeyboardNav.js +3 -3
  27. package/cjs/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  28. package/cjs/data/table/hooks/useTableSelection.d.ts +3 -2
  29. package/cjs/data/table/hooks/useTableSelection.js +5 -4
  30. package/cjs/data/table/hooks/useTableSelection.js.map +1 -1
  31. package/cjs/data/table/root/DataTable.types.d.ts +5 -4
  32. package/cjs/data/table/root/DataTableAuto.d.ts +27 -1
  33. package/cjs/data/table/root/DataTableAuto.js +92 -50
  34. package/cjs/data/table/root/DataTableAuto.js.map +1 -1
  35. package/cjs/data/table/root/DataTableRoot.context.d.ts +5 -3
  36. package/cjs/data/table/root/DataTableRoot.context.js.map +1 -1
  37. package/cjs/data/table/root/DataTableRoot.js +6 -4
  38. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  39. package/cjs/data/table/tr/DataTableTr.js +30 -32
  40. package/cjs/data/table/tr/DataTableTr.js.map +1 -1
  41. package/cjs/form/checkbox/Checkbox.js +1 -0
  42. package/cjs/form/checkbox/Checkbox.js.map +1 -1
  43. package/cjs/form/radio/Radio.js +7 -1
  44. package/cjs/form/radio/Radio.js.map +1 -1
  45. package/cjs/modal/types.d.ts +8 -4
  46. package/esm/data/table/column-header/DataTableColumnHeader.d.ts +1 -1
  47. package/esm/data/table/column-header/DataTableColumnHeader.js +17 -12
  48. package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  49. package/esm/data/table/column-header/useTableColumnResize.d.ts +28 -4
  50. package/esm/data/table/column-header/useTableColumnResize.js +145 -54
  51. package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
  52. package/esm/data/table/helpers/collectTableRowEntries.d.ts +24 -0
  53. package/esm/data/table/helpers/collectTableRowEntries.js +33 -0
  54. package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -0
  55. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
  56. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js +109 -0
  57. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
  58. package/esm/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
  59. package/esm/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
  60. package/esm/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
  61. package/esm/data/table/helpers/selection/selection.types.d.ts +1 -0
  62. package/esm/data/table/helpers/table-keyboard.d.ts +1 -2
  63. package/esm/data/table/helpers/table-keyboard.js +1 -5
  64. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  65. package/esm/data/table/hooks/useTableExpansion.d.ts +7 -6
  66. package/esm/data/table/hooks/useTableExpansion.js +42 -16
  67. package/esm/data/table/hooks/useTableExpansion.js.map +1 -1
  68. package/esm/data/table/hooks/useTableItems.d.ts +33 -0
  69. package/esm/data/table/hooks/useTableItems.js +69 -0
  70. package/esm/data/table/hooks/useTableItems.js.map +1 -0
  71. package/esm/data/table/hooks/useTableKeyboardNav.js +3 -3
  72. package/esm/data/table/hooks/useTableKeyboardNav.js.map +1 -1
  73. package/esm/data/table/hooks/useTableSelection.d.ts +3 -2
  74. package/esm/data/table/hooks/useTableSelection.js +5 -4
  75. package/esm/data/table/hooks/useTableSelection.js.map +1 -1
  76. package/esm/data/table/root/DataTable.types.d.ts +5 -4
  77. package/esm/data/table/root/DataTableAuto.d.ts +27 -1
  78. package/esm/data/table/root/DataTableAuto.js +94 -52
  79. package/esm/data/table/root/DataTableAuto.js.map +1 -1
  80. package/esm/data/table/root/DataTableRoot.context.d.ts +5 -3
  81. package/esm/data/table/root/DataTableRoot.context.js.map +1 -1
  82. package/esm/data/table/root/DataTableRoot.js +7 -5
  83. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  84. package/esm/data/table/tr/DataTableTr.js +32 -34
  85. package/esm/data/table/tr/DataTableTr.js.map +1 -1
  86. package/esm/form/checkbox/Checkbox.js +1 -0
  87. package/esm/form/checkbox/Checkbox.js.map +1 -1
  88. package/esm/form/radio/Radio.js +7 -1
  89. package/esm/form/radio/Radio.js.map +1 -1
  90. package/esm/modal/types.d.ts +8 -4
  91. package/package.json +7 -7
  92. package/src/data/table/column-header/DataTableColumnHeader.tsx +26 -14
  93. package/src/data/table/column-header/useTableColumnResize.ts +209 -80
  94. package/src/data/table/helpers/collectTableRowEntries.ts +90 -0
  95. package/src/data/table/helpers/selection/SelectionSubtreeHelper.test.ts +66 -0
  96. package/src/data/table/helpers/selection/SelectionSubtreeHelper.ts +162 -0
  97. package/src/data/table/helpers/selection/getMultipleSelectProps.ts +57 -20
  98. package/src/data/table/helpers/selection/selection.types.ts +1 -0
  99. package/src/data/table/helpers/table-keyboard.ts +1 -6
  100. package/src/data/table/hooks/__tests__/useTableItems.test.ts +145 -0
  101. package/src/data/table/hooks/__tests__/useTableSelection.test.ts +132 -21
  102. package/src/data/table/hooks/useTableExpansion.tsx +68 -22
  103. package/src/data/table/hooks/useTableItems.ts +146 -0
  104. package/src/data/table/hooks/useTableKeyboardNav.ts +3 -3
  105. package/src/data/table/hooks/useTableSelection.ts +10 -6
  106. package/src/data/table/root/DataTable.types.ts +5 -4
  107. package/src/data/table/root/DataTableAuto.test.tsx +244 -0
  108. package/src/data/table/root/DataTableAuto.tsx +260 -141
  109. package/src/data/table/root/DataTableRoot.context.ts +4 -2
  110. package/src/data/table/root/DataTableRoot.tsx +22 -16
  111. package/src/data/table/tr/DataTableTr.tsx +48 -47
  112. package/src/form/checkbox/Checkbox.tsx +1 -0
  113. package/src/form/radio/Radio.tsx +7 -1
  114. package/src/modal/types.ts +8 -4
@@ -17,7 +17,13 @@ const items: Item[] = [
17
17
  { id: "c", name: "Charlie" },
18
18
  ];
19
19
 
20
- const allRowKeys = items.map((item) => item.id);
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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
- allRowKeys,
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,17 +1,18 @@
1
1
  import React, { useCallback } from "react";
2
2
  import { createStrictContext } from "../../../utils/helpers";
3
3
  import { useControllableState } from "../../../utils/hooks";
4
+ import { useTableItemsContext } from "./useTableItems";
4
5
 
5
6
  type DataTableExpansionContextT = {
6
- expandedIds: (string | number)[];
7
7
  isExpanded: (id: string | number) => boolean;
8
+ isDetailsPanelExpandable: (id: string | number) => boolean;
8
9
  toggleExpansion: (id: string | number) => void;
9
10
  toggleAll: () => void;
10
11
  isAllExpanded: boolean;
11
12
  getDetailsPanelContent?: (row: unknown) => React.ReactNode;
12
13
  getDetailsPanelHeight?: (row: unknown) => number | "auto";
13
- showExpandAll?: boolean;
14
- enableExpansion: boolean;
14
+ showExpandAll: boolean;
15
+ enableDetailsPanel: boolean;
15
16
  };
16
17
 
17
18
  const {
@@ -27,57 +28,98 @@ type TableExpansionOptions<T> = {
27
28
  detailsPanelRowIds?: (string | number)[];
28
29
  defaultDetailsPanelRowIds?: (string | number)[];
29
30
  onDetailsPanelChange?: (ids: (string | number)[]) => void;
30
- allRowKeys: (string | number)[];
31
31
  getDetailsPanelContent?: (row: T) => React.ReactNode;
32
+ isDetailsPanelExpandable?: (rowData: T) => boolean;
32
33
  getDetailsPanelHeight?: (row: T) => number | "auto";
33
34
  showExpandAll?: boolean;
34
35
  };
35
36
 
37
+ function getDataTableExpansionId(tableId: string, rowId: string | number) {
38
+ return `${tableId}-expansion-${rowId}`;
39
+ }
40
+
36
41
  function DataTableExpansionProvider<T>({
37
42
  children,
38
43
  detailsPanelRowIds,
39
44
  defaultDetailsPanelRowIds = [],
40
45
  onDetailsPanelChange,
41
- allRowKeys,
42
46
  getDetailsPanelContent,
47
+ isDetailsPanelExpandable,
43
48
  getDetailsPanelHeight,
44
49
  showExpandAll = false,
45
50
  }: TableExpansionOptions<T> & { children: React.ReactNode }) {
46
51
  const [expandedIds, setExpandedIds] = useControllableState({
47
52
  value: detailsPanelRowIds,
48
53
  defaultValue: defaultDetailsPanelRowIds,
54
+ onChange: onDetailsPanelChange,
49
55
  });
50
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
+
64
+ const expandableIds = React.useMemo(() => {
65
+ if (!getDetailsPanelContent) {
66
+ return new Set<string | number>();
67
+ }
68
+
69
+ const ids = new Set<string | number>();
70
+
71
+ for (const [rowData, { id, level }] of itemDetails.entries()) {
72
+ /* We only allow Master - Details pattern on top level rows */
73
+ if (level > 0) {
74
+ continue;
75
+ }
76
+
77
+ if (!isDetailsPanelExpandable || isDetailsPanelExpandable(rowData)) {
78
+ ids.add(id);
79
+ }
80
+ }
81
+
82
+ return ids;
83
+ }, [getDetailsPanelContent, isDetailsPanelExpandable, itemDetails]);
84
+
85
+ const isDetailsPanelExpandableById = useCallback(
86
+ (id: string | number) => expandableIds.has(id),
87
+ [expandableIds],
88
+ );
89
+
51
90
  const isExpanded = useCallback(
52
- (id: string | number) => expandedIds.includes(id),
53
- [expandedIds],
91
+ (id: string | number) =>
92
+ isDetailsPanelExpandableById(id) && expandedIds.includes(id),
93
+ [expandedIds, isDetailsPanelExpandableById],
54
94
  );
55
95
 
56
96
  const toggleExpansion = useCallback(
57
97
  (id: string | number) => {
58
- const next = expandedIds.includes(id)
59
- ? expandedIds.filter((eid) => eid !== id)
60
- : [...expandedIds, id];
61
- setExpandedIds(next);
62
- onDetailsPanelChange?.(next);
98
+ if (!isDetailsPanelExpandableById(id)) {
99
+ return;
100
+ }
101
+
102
+ setExpandedIds((currentExpandedIds) =>
103
+ currentExpandedIds.includes(id)
104
+ ? currentExpandedIds.filter((expandedId) => expandedId !== id)
105
+ : [...currentExpandedIds, id],
106
+ );
63
107
  },
64
- [expandedIds, setExpandedIds, onDetailsPanelChange],
108
+ [isDetailsPanelExpandableById, setExpandedIds],
65
109
  );
66
110
 
67
111
  const isAllExpanded =
68
- allRowKeys.length > 0 &&
69
- allRowKeys.every((key) => expandedIds.includes(key));
112
+ expandableIds.size > 0 &&
113
+ Array.from(expandableIds).every((key) => expandedIds.includes(key));
70
114
 
71
115
  const toggleAll = useCallback(() => {
72
- const next = isAllExpanded ? [] : [...allRowKeys];
73
- setExpandedIds(next);
74
- onDetailsPanelChange?.(next);
75
- }, [isAllExpanded, allRowKeys, setExpandedIds, onDetailsPanelChange]);
116
+ setExpandedIds(isAllExpanded ? [] : Array.from(expandableIds));
117
+ }, [expandableIds, isAllExpanded, setExpandedIds]);
76
118
 
77
119
  return (
78
120
  <DataTableExpansionContextProvider
79
- expandedIds={expandedIds}
80
121
  isExpanded={isExpanded}
122
+ isDetailsPanelExpandable={isDetailsPanelExpandableById}
81
123
  toggleExpansion={toggleExpansion}
82
124
  toggleAll={toggleAll}
83
125
  isAllExpanded={isAllExpanded}
@@ -90,11 +132,15 @@ function DataTableExpansionProvider<T>({
90
132
  getDetailsPanelHeight as ((row: unknown) => number | "auto") | undefined
91
133
  }
92
134
  showExpandAll={showExpandAll}
93
- enableExpansion={!!getDetailsPanelContent}
135
+ enableDetailsPanel={!!getDetailsPanelContent}
94
136
  >
95
137
  {children}
96
138
  </DataTableExpansionContextProvider>
97
139
  );
98
140
  }
99
141
 
100
- export { DataTableExpansionProvider, useDataTableExpansion };
142
+ export {
143
+ DataTableExpansionProvider,
144
+ getDataTableExpansionId,
145
+ useDataTableExpansion,
146
+ };
@@ -0,0 +1,146 @@
1
+ import { useCallback, useMemo } from "react";
2
+ import { createStrictContext } from "../../../utils/helpers";
3
+ import { useControllableState } from "../../../utils/hooks";
4
+ import {
5
+ type ItemDetail,
6
+ type TableRowEntryId,
7
+ collectTableRowEntries,
8
+ } from "../helpers/collectTableRowEntries";
9
+
10
+ type UseTableItemsArgs<T> = {
11
+ items: T[];
12
+ getRowId?: (rowData: T, index: number) => string | number;
13
+ /**
14
+ * Master - Detail pattern props
15
+ */
16
+ /* expandedDetailsPanelIds?: (string | number)[];
17
+ defaultExpandedDetailsPanelIds?: (string | number)[];
18
+ isDetailsPanelExpandable?: (rowData: T) => boolean;
19
+ onDetailsPanelChange?: (ids: (string | number)[]) => void;
20
+
21
+ getDetailsPanelHeight?: (row: T) => number | "auto";
22
+ getDetailsPanelContent?: (row: T) => React.ReactNode; */
23
+ /**
24
+ * Expanded/Nested rows pattern props
25
+ */
26
+ getSubRows?: (rowData: T) => T[];
27
+ expandedSubRowIds?: (string | number)[];
28
+ defaultExpandedSubRowIds?: (string | number)[];
29
+ isSubRowExpandable?: (rowData: T) => boolean;
30
+ onExpandedSubRowIdsChange?: (ids: (string | number)[]) => void;
31
+ };
32
+
33
+ type useTableItemsReturn<T> = {
34
+ items: T[];
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[]>;
40
+ onExpandedSubRowIdsChange: (id: string | number) => void;
41
+ isSubRowExpanded: (id: string | number) => boolean;
42
+ };
43
+
44
+ function useTableItems<T>(args: UseTableItemsArgs<T>): useTableItemsReturn<T> {
45
+ const {
46
+ items,
47
+ expandedSubRowIds,
48
+ defaultExpandedSubRowIds,
49
+ getSubRows,
50
+ getRowId,
51
+ onExpandedSubRowIdsChange,
52
+ isSubRowExpandable,
53
+ } = args;
54
+
55
+ const [nestedSubRowsExpandedIds, setNestedSubRowsExpandedIds] =
56
+ useControllableState({
57
+ value: expandedSubRowIds,
58
+ defaultValue: defaultExpandedSubRowIds ?? [],
59
+ onChange: onExpandedSubRowIdsChange,
60
+ });
61
+
62
+ const expandedIdsSet = useMemo(
63
+ () => new Set(nestedSubRowsExpandedIds),
64
+ [nestedSubRowsExpandedIds],
65
+ );
66
+
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
+ });
76
+
77
+ const localVisibleItems: T[] = [];
78
+ const localVisibleRowIds: TableRowEntryId[] = [];
79
+
80
+ const addVisibleRows = (rowData: T): TableRowEntryId[] => {
81
+ const details = rowEntriesMap.get(rowData);
82
+
83
+ if (!details) {
84
+ return [];
85
+ }
86
+
87
+ localVisibleItems.push(rowData);
88
+ localVisibleRowIds.push(details.id);
89
+
90
+ const visibleDescendantRowIds: TableRowEntryId[] = [];
91
+
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
+ }
105
+
106
+ return {
107
+ visibleItems: localVisibleItems,
108
+ visibleRowIds: localVisibleRowIds,
109
+ childRowIdsById: _childRowIdsById,
110
+ itemDetails: rowEntriesMap,
111
+ };
112
+ }, [getSubRows, items, getRowId, isSubRowExpandable, expandedIdsSet]);
113
+
114
+ const handleExpandedSubRowIdChange = useCallback(
115
+ (id: string | number) => {
116
+ setNestedSubRowsExpandedIds((prev) =>
117
+ prev.includes(id)
118
+ ? prev.filter((expandedId) => expandedId !== id)
119
+ : [...prev, id],
120
+ );
121
+ },
122
+ [setNestedSubRowsExpandedIds],
123
+ );
124
+
125
+ return {
126
+ items: visibleItems,
127
+ itemDetails,
128
+ visibleRowIds,
129
+ childRowIdsById,
130
+ onExpandedSubRowIdsChange: handleExpandedSubRowIdChange,
131
+ isSubRowExpanded: (id: string | number) => expandedIdsSet.has(id),
132
+ };
133
+ }
134
+
135
+ const { Provider: TableItemsProvider, useContext: useTableItemsContext } =
136
+ /* TODO: Can we type this better? */
137
+ createStrictContext<
138
+ Omit<useTableItemsReturn<any>, "visibleRowIds" | "childRowIdsById">
139
+ >({
140
+ name: "TableItemsContext",
141
+ errorMessage:
142
+ "useTableItemsContext must be used within a TableItemsProvider",
143
+ });
144
+
145
+ export { useTableItems, TableItemsProvider, useTableItemsContext };
146
+ export type { ItemDetail };
@@ -114,12 +114,12 @@ function useTableKeyboardNav({
114
114
  return;
115
115
  }
116
116
 
117
- if (shouldBlockNavigation(event)) {
117
+ const action = getNavigationAction(event);
118
+ if (!action) {
118
119
  return;
119
120
  }
120
121
 
121
- const action = getNavigationAction(event);
122
- if (!action) {
122
+ if (shouldBlockNavigation(event)) {
123
123
  return;
124
124
  }
125
125
 
@@ -10,8 +10,10 @@ import type {
10
10
  } from "../helpers/selection/selection.types";
11
11
 
12
12
  type UseTableSelectionArgs = SelectionProps & {
13
- /* This is needed for multiple selection to know which keys to select when "select all" is used */
14
- allRowKeys: (string | number)[];
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
- allRowKeys,
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: allRowKeys.length !== 0,
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
- allRowKeys,
91
+ visibleRowIds,
92
+ childRowIdsById,
89
93
  }),
90
94
  },
91
- renderSelection: allRowKeys.length !== 0,
95
+ renderSelection: visibleRowIds.length !== 0,
92
96
  };
93
97
  }
94
98
 
@@ -12,11 +12,12 @@ type ColumnDefinition<T> = {
12
12
  minWidth?: number | string;
13
13
  maxWidth?: number | string;
14
14
  /**
15
- * Currently only handles cell alignment.
16
- * TODO: Should this include centering?
17
- * type "icon" or something to avoid ellipsis on actions, tags etc
15
+ * Text alignment for cells in this column.
16
+ *
17
+ *
18
+ * @default "left"
18
19
  */
19
- type?: "string" | "number";
20
+ align?: "left" | "right" | "center";
20
21
  /**
21
22
  * Assigned to the cell's `th` element instead of `td` if true.
22
23
  *