@navikt/ds-react 8.10.2 → 8.10.4

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 (200) hide show
  1. package/cjs/action-menu/ActionMenu.js +1 -1
  2. package/cjs/action-menu/ActionMenu.js.map +1 -1
  3. package/cjs/data/stories/Data.test-data.d.ts +24 -0
  4. package/cjs/data/stories/Data.test-data.js +1616 -0
  5. package/cjs/data/stories/Data.test-data.js.map +1 -0
  6. package/cjs/data/table/column-header/DataTableColumnHeader.d.ts +4 -1
  7. package/cjs/data/table/column-header/DataTableColumnHeader.js +4 -2
  8. package/cjs/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  9. package/cjs/data/table/column-header/useTableColumnResize.d.ts +39 -14
  10. package/cjs/data/table/column-header/useTableColumnResize.js +37 -39
  11. package/cjs/data/table/column-header/useTableColumnResize.js.map +1 -1
  12. package/cjs/data/table/details-panel-row/DataTableDetailsPanelRow.d.ts +6 -0
  13. package/cjs/data/table/details-panel-row/DataTableDetailsPanelRow.js +32 -0
  14. package/cjs/data/table/details-panel-row/DataTableDetailsPanelRow.js.map +1 -0
  15. package/cjs/data/table/helpers/collectTableRowEntries.d.ts +13 -5
  16. package/cjs/data/table/helpers/collectTableRowEntries.js +26 -19
  17. package/cjs/data/table/helpers/collectTableRowEntries.js.map +1 -1
  18. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
  19. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js +112 -0
  20. package/cjs/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
  21. package/cjs/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
  22. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
  23. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
  24. package/cjs/data/table/helpers/selection/selection.types.d.ts +1 -0
  25. package/cjs/data/table/helpers/table-keyboard.d.ts +1 -2
  26. package/cjs/data/table/helpers/table-keyboard.js +1 -2
  27. package/cjs/data/table/helpers/table-keyboard.js.map +1 -1
  28. package/cjs/data/table/hooks/useColumnOptions.js +18 -5
  29. package/cjs/data/table/hooks/useColumnOptions.js.map +1 -1
  30. package/cjs/data/table/hooks/useTableDetailsPanel.d.ts +62 -0
  31. package/cjs/data/table/hooks/{useTableExpansion.js → useTableDetailsPanel.js} +26 -19
  32. package/cjs/data/table/hooks/useTableDetailsPanel.js.map +1 -0
  33. package/cjs/data/table/hooks/useTableItems.d.ts +18 -17
  34. package/cjs/data/table/hooks/useTableItems.js +27 -15
  35. package/cjs/data/table/hooks/useTableItems.js.map +1 -1
  36. package/cjs/data/table/hooks/useTableSelection.d.ts +6 -3
  37. package/cjs/data/table/hooks/useTableSelection.js +10 -4
  38. package/cjs/data/table/hooks/useTableSelection.js.map +1 -1
  39. package/cjs/data/table/index.d.ts +1 -2
  40. package/cjs/data/table/index.js +22 -12
  41. package/cjs/data/table/index.js.map +1 -1
  42. package/cjs/data/table/root/DataTable.types.d.ts +12 -10
  43. package/cjs/data/table/root/DataTableRoot.context.d.ts +5 -1
  44. package/cjs/data/table/root/DataTableRoot.context.js.map +1 -1
  45. package/cjs/data/table/root/DataTableRoot.d.ts +79 -115
  46. package/cjs/data/table/root/DataTableRoot.js +167 -39
  47. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  48. package/cjs/data/table/root/DataTableRoot.legacy.d.ts +177 -0
  49. package/cjs/data/table/root/DataTableRoot.legacy.js +104 -0
  50. package/cjs/data/table/root/DataTableRoot.legacy.js.map +1 -0
  51. package/cjs/data/table/sub-row-toggle/DataTableSubRowToggle.d.ts +6 -0
  52. package/cjs/data/table/sub-row-toggle/DataTableSubRowToggle.js +21 -0
  53. package/cjs/data/table/sub-row-toggle/DataTableSubRowToggle.js.map +1 -0
  54. package/cjs/data/table/tr/DataTableTr.js +11 -11
  55. package/cjs/data/table/tr/DataTableTr.js.map +1 -1
  56. package/cjs/form/checkbox/Checkbox.js +1 -0
  57. package/cjs/form/checkbox/Checkbox.js.map +1 -1
  58. package/cjs/form/radio/Radio.js +7 -1
  59. package/cjs/form/radio/Radio.js.map +1 -1
  60. package/cjs/modal/types.d.ts +8 -4
  61. package/cjs/utils/components/dismissablelayer/DismissableLayer.js +1 -1
  62. package/cjs/utils/components/dismissablelayer/DismissableLayer.js.map +1 -1
  63. package/cjs/utils/components/floating/Floating.d.ts +16 -1
  64. package/cjs/utils/components/floating/Floating.js +50 -13
  65. package/cjs/utils/components/floating/Floating.js.map +1 -1
  66. package/cjs/utils/components/floating-menu/Menu.js +1 -1
  67. package/cjs/utils/components/floating-menu/Menu.js.map +1 -1
  68. package/cjs/utils/helpers/create-strict-context.js +1 -1
  69. package/cjs/utils/helpers/create-strict-context.js.map +1 -1
  70. package/cjs/utils/hooks/useControllableState.d.ts +5 -5
  71. package/cjs/utils/hooks/useControllableState.js.map +1 -1
  72. package/cjs/utils/hooks/useValueAsRef.js +1 -1
  73. package/cjs/utils/hooks/useValueAsRef.js.map +1 -1
  74. package/cjs/utils-external/hooks/useId.js +1 -1
  75. package/cjs/utils-external/hooks/useId.js.map +1 -1
  76. package/esm/action-menu/ActionMenu.js +1 -1
  77. package/esm/action-menu/ActionMenu.js.map +1 -1
  78. package/esm/data/stories/Data.test-data.d.ts +24 -0
  79. package/esm/data/stories/Data.test-data.js +1607 -0
  80. package/esm/data/stories/Data.test-data.js.map +1 -0
  81. package/esm/data/table/column-header/DataTableColumnHeader.d.ts +4 -1
  82. package/esm/data/table/column-header/DataTableColumnHeader.js +4 -2
  83. package/esm/data/table/column-header/DataTableColumnHeader.js.map +1 -1
  84. package/esm/data/table/column-header/useTableColumnResize.d.ts +39 -14
  85. package/esm/data/table/column-header/useTableColumnResize.js +38 -40
  86. package/esm/data/table/column-header/useTableColumnResize.js.map +1 -1
  87. package/esm/data/table/details-panel-row/DataTableDetailsPanelRow.d.ts +6 -0
  88. package/esm/data/table/details-panel-row/DataTableDetailsPanelRow.js +27 -0
  89. package/esm/data/table/details-panel-row/DataTableDetailsPanelRow.js.map +1 -0
  90. package/esm/data/table/helpers/collectTableRowEntries.d.ts +13 -5
  91. package/esm/data/table/helpers/collectTableRowEntries.js +26 -19
  92. package/esm/data/table/helpers/collectTableRowEntries.js.map +1 -1
  93. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.d.ts +46 -0
  94. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js +109 -0
  95. package/esm/data/table/helpers/selection/SelectionSubtreeHelper.js.map +1 -0
  96. package/esm/data/table/helpers/selection/getMultipleSelectProps.d.ts +3 -2
  97. package/esm/data/table/helpers/selection/getMultipleSelectProps.js +43 -19
  98. package/esm/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -1
  99. package/esm/data/table/helpers/selection/selection.types.d.ts +1 -0
  100. package/esm/data/table/helpers/table-keyboard.d.ts +1 -2
  101. package/esm/data/table/helpers/table-keyboard.js +1 -2
  102. package/esm/data/table/helpers/table-keyboard.js.map +1 -1
  103. package/esm/data/table/hooks/useColumnOptions.js +18 -5
  104. package/esm/data/table/hooks/useColumnOptions.js.map +1 -1
  105. package/esm/data/table/hooks/useTableDetailsPanel.d.ts +62 -0
  106. package/esm/data/table/hooks/useTableDetailsPanel.js +58 -0
  107. package/esm/data/table/hooks/useTableDetailsPanel.js.map +1 -0
  108. package/esm/data/table/hooks/useTableItems.d.ts +18 -17
  109. package/esm/data/table/hooks/useTableItems.js +27 -15
  110. package/esm/data/table/hooks/useTableItems.js.map +1 -1
  111. package/esm/data/table/hooks/useTableSelection.d.ts +6 -3
  112. package/esm/data/table/hooks/useTableSelection.js +10 -4
  113. package/esm/data/table/hooks/useTableSelection.js.map +1 -1
  114. package/esm/data/table/index.d.ts +1 -2
  115. package/esm/data/table/index.js +21 -1
  116. package/esm/data/table/index.js.map +1 -1
  117. package/esm/data/table/root/DataTable.types.d.ts +12 -10
  118. package/esm/data/table/root/DataTableRoot.context.d.ts +5 -1
  119. package/esm/data/table/root/DataTableRoot.context.js.map +1 -1
  120. package/esm/data/table/root/DataTableRoot.d.ts +79 -115
  121. package/esm/data/table/root/DataTableRoot.js +174 -37
  122. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  123. package/esm/data/table/root/DataTableRoot.legacy.d.ts +177 -0
  124. package/esm/data/table/root/DataTableRoot.legacy.js +59 -0
  125. package/esm/data/table/root/DataTableRoot.legacy.js.map +1 -0
  126. package/esm/data/table/sub-row-toggle/DataTableSubRowToggle.d.ts +6 -0
  127. package/esm/data/table/sub-row-toggle/DataTableSubRowToggle.js +16 -0
  128. package/esm/data/table/sub-row-toggle/DataTableSubRowToggle.js.map +1 -0
  129. package/esm/data/table/tr/DataTableTr.js +11 -11
  130. package/esm/data/table/tr/DataTableTr.js.map +1 -1
  131. package/esm/form/checkbox/Checkbox.js +1 -0
  132. package/esm/form/checkbox/Checkbox.js.map +1 -1
  133. package/esm/form/radio/Radio.js +7 -1
  134. package/esm/form/radio/Radio.js.map +1 -1
  135. package/esm/modal/types.d.ts +8 -4
  136. package/esm/utils/components/dismissablelayer/DismissableLayer.js +1 -1
  137. package/esm/utils/components/dismissablelayer/DismissableLayer.js.map +1 -1
  138. package/esm/utils/components/floating/Floating.d.ts +16 -1
  139. package/esm/utils/components/floating/Floating.js +48 -13
  140. package/esm/utils/components/floating/Floating.js.map +1 -1
  141. package/esm/utils/components/floating-menu/Menu.js +2 -2
  142. package/esm/utils/components/floating-menu/Menu.js.map +1 -1
  143. package/esm/utils/helpers/create-strict-context.js +1 -1
  144. package/esm/utils/helpers/create-strict-context.js.map +1 -1
  145. package/esm/utils/hooks/useControllableState.d.ts +5 -5
  146. package/esm/utils/hooks/useControllableState.js.map +1 -1
  147. package/esm/utils/hooks/useValueAsRef.js +1 -1
  148. package/esm/utils/hooks/useValueAsRef.js.map +1 -1
  149. package/esm/utils-external/hooks/useId.js +1 -1
  150. package/esm/utils-external/hooks/useId.js.map +1 -1
  151. package/package.json +3 -3
  152. package/src/action-menu/ActionMenu.tsx +1 -1
  153. package/src/data/stories/Data.test-data.tsx +1703 -0
  154. package/src/data/table/column-header/DataTableColumnHeader.tsx +11 -7
  155. package/src/data/table/column-header/useTableColumnResize.ts +95 -54
  156. package/src/data/table/details-panel-row/DataTableDetailsPanelRow.tsx +53 -0
  157. package/src/data/table/helpers/collectTableRowEntries.ts +55 -31
  158. package/src/data/table/helpers/selection/SelectionSubtreeHelper.test.ts +66 -0
  159. package/src/data/table/helpers/selection/SelectionSubtreeHelper.ts +162 -0
  160. package/src/data/table/helpers/selection/getMultipleSelectProps.ts +57 -20
  161. package/src/data/table/helpers/selection/selection.types.ts +1 -0
  162. package/src/data/table/helpers/table-keyboard.ts +1 -2
  163. package/src/data/table/hooks/__tests__/useTableItems.test.ts +27 -6
  164. package/src/data/table/hooks/__tests__/useTableSelection.test.ts +182 -58
  165. package/src/data/table/hooks/useColumnOptions.ts +19 -5
  166. package/src/data/table/hooks/useTableDetailsPanel.tsx +182 -0
  167. package/src/data/table/hooks/useTableItems.ts +74 -60
  168. package/src/data/table/hooks/useTableSelection.ts +27 -12
  169. package/src/data/table/index.tsx +5 -3
  170. package/src/data/table/root/DataTable.types.ts +25 -10
  171. package/src/data/table/root/DataTableRoot.context.ts +5 -1
  172. package/src/data/table/root/DataTableRoot.legacy.tsx +297 -0
  173. package/src/data/table/root/DataTableRoot.tsx +483 -219
  174. package/src/data/table/sub-row-toggle/DataTableSubRowToggle.tsx +39 -0
  175. package/src/data/table/tr/DataTableTr.tsx +14 -13
  176. package/src/form/checkbox/Checkbox.tsx +1 -0
  177. package/src/form/radio/Radio.tsx +7 -1
  178. package/src/modal/types.ts +8 -4
  179. package/src/utils/components/dismissablelayer/DismissableLayer.tsx +1 -1
  180. package/src/utils/components/floating/Floating.tsx +56 -13
  181. package/src/utils/components/floating-menu/Menu.tsx +4 -1
  182. package/src/utils/helpers/create-strict-context.tsx +1 -1
  183. package/src/utils/hooks/useControllableState.ts +11 -8
  184. package/src/utils/hooks/useValueAsRef.ts +1 -1
  185. package/src/utils-external/hooks/useId.ts +1 -1
  186. package/cjs/data/table/hooks/useTableExpansion.d.ts +0 -29
  187. package/cjs/data/table/hooks/useTableExpansion.js.map +0 -1
  188. package/cjs/data/table/root/DataTableAuto.d.ts +0 -174
  189. package/cjs/data/table/root/DataTableAuto.js +0 -206
  190. package/cjs/data/table/root/DataTableAuto.js.map +0 -1
  191. package/esm/data/table/hooks/useTableExpansion.d.ts +0 -29
  192. package/esm/data/table/hooks/useTableExpansion.js +0 -51
  193. package/esm/data/table/hooks/useTableExpansion.js.map +0 -1
  194. package/esm/data/table/root/DataTableAuto.d.ts +0 -174
  195. package/esm/data/table/root/DataTableAuto.js +0 -170
  196. package/esm/data/table/root/DataTableAuto.js.map +0 -1
  197. package/src/data/table/hooks/__tests__/useTableExpansion.test.tsx +0 -115
  198. package/src/data/table/hooks/useTableExpansion.tsx +0 -141
  199. package/src/data/table/root/DataTableAuto.test.tsx +0 -118
  200. package/src/data/table/root/DataTableAuto.tsx +0 -603
@@ -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;
@@ -36,8 +42,8 @@ describe("useTableSelection", () => {
36
42
  test("returns empty selectedKeys and no prop getters", () => {
37
43
  const { result } = renderHook(() =>
38
44
  useTableSelection({
39
- selectionMode: "none",
40
- allRowKeys,
45
+ selection: { selectionMode: "none" },
46
+ visibleRowIds,
41
47
  }),
42
48
  );
43
49
 
@@ -50,8 +56,8 @@ describe("useTableSelection", () => {
50
56
  test("returns getRowRadioProps", () => {
51
57
  const { result } = renderHook(() =>
52
58
  useTableSelection({
53
- selectionMode: "single",
54
- allRowKeys,
59
+ selection: { selectionMode: "single" },
60
+ visibleRowIds,
55
61
  }),
56
62
  );
57
63
 
@@ -63,9 +69,8 @@ describe("useTableSelection", () => {
63
69
  const onChange = vi.fn();
64
70
  const { result } = renderHook(() =>
65
71
  useTableSelection({
66
- selectionMode: "single",
67
- allRowKeys,
68
- onSelectionChange: onChange,
72
+ selection: { selectionMode: "single", onSelectionChange: onChange },
73
+ visibleRowIds,
69
74
  }),
70
75
  );
71
76
 
@@ -82,9 +87,8 @@ describe("useTableSelection", () => {
82
87
  test("toggling the same row keeps it selected", () => {
83
88
  const { result } = renderHook(() =>
84
89
  useTableSelection({
85
- selectionMode: "single",
86
- allRowKeys,
87
- defaultSelectedKeys: ["a"],
90
+ selection: { selectionMode: "single", defaultSelectedKeys: ["a"] },
91
+ visibleRowIds,
88
92
  }),
89
93
  );
90
94
 
@@ -100,9 +104,8 @@ describe("useTableSelection", () => {
100
104
  test("selecting a new row replaces the previous", () => {
101
105
  const { result } = renderHook(() =>
102
106
  useTableSelection({
103
- selectionMode: "single",
104
- allRowKeys,
105
- defaultSelectedKeys: ["a"],
107
+ selection: { selectionMode: "single", defaultSelectedKeys: ["a"] },
108
+ visibleRowIds,
106
109
  }),
107
110
  );
108
111
 
@@ -118,9 +121,8 @@ describe("useTableSelection", () => {
118
121
  test("disabled rows have disabled prop", () => {
119
122
  const { result } = renderHook(() =>
120
123
  useTableSelection({
121
- selectionMode: "single",
122
- allRowKeys,
123
- disabledSelectionKeys: ["b"],
124
+ selection: { selectionMode: "single", disabledSelectionKeys: ["b"] },
125
+ visibleRowIds,
124
126
  }),
125
127
  );
126
128
 
@@ -132,9 +134,8 @@ describe("useTableSelection", () => {
132
134
  const { result, rerender } = renderHook(
133
135
  ({ selectedKeys }) =>
134
136
  useTableSelection({
135
- selectionMode: "single",
136
- allRowKeys,
137
- selectedKeys,
137
+ selection: { selectionMode: "single", selectedKeys },
138
+ visibleRowIds,
138
139
  }),
139
140
  { initialProps: { selectedKeys: ["a"] as (string | number)[] } },
140
141
  );
@@ -150,8 +151,8 @@ describe("useTableSelection", () => {
150
151
  test("returns getTheadCheckboxProps and getRowCheckboxProps", () => {
151
152
  const { result } = renderHook(() =>
152
153
  useTableSelection({
153
- selectionMode: "multiple",
154
- allRowKeys,
154
+ selection: { selectionMode: "multiple" },
155
+ visibleRowIds,
155
156
  }),
156
157
  );
157
158
 
@@ -163,8 +164,8 @@ describe("useTableSelection", () => {
163
164
  test("selecting individual rows", () => {
164
165
  const { result } = renderHook(() =>
165
166
  useTableSelection({
166
- selectionMode: "multiple",
167
- allRowKeys,
167
+ selection: { selectionMode: "multiple" },
168
+ visibleRowIds,
168
169
  }),
169
170
  );
170
171
 
@@ -188,9 +189,11 @@ describe("useTableSelection", () => {
188
189
  test("deselecting a row", () => {
189
190
  const { result } = renderHook(() =>
190
191
  useTableSelection({
191
- selectionMode: "multiple",
192
- allRowKeys,
193
- defaultSelectedKeys: ["a", "b"],
192
+ selection: {
193
+ selectionMode: "multiple",
194
+ defaultSelectedKeys: ["a", "b"],
195
+ },
196
+ visibleRowIds,
194
197
  }),
195
198
  );
196
199
 
@@ -206,8 +209,8 @@ describe("useTableSelection", () => {
206
209
  test("select all via thead checkbox", () => {
207
210
  const { result } = renderHook(() =>
208
211
  useTableSelection({
209
- selectionMode: "multiple",
210
- allRowKeys,
212
+ selection: { selectionMode: "multiple" },
213
+ visibleRowIds,
211
214
  }),
212
215
  );
213
216
 
@@ -220,12 +223,32 @@ describe("useTableSelection", () => {
220
223
  expect(asMultiple(result).selectedKeys).toEqual(["a", "b", "c"]);
221
224
  });
222
225
 
226
+ test("select all via thead includes hidden descendants for visible parents", () => {
227
+ const { result } = renderHook(() =>
228
+ useTableSelection({
229
+ selection: { selectionMode: "multiple" },
230
+ visibleRowIds: ["a"],
231
+ childRowIdsById,
232
+ }),
233
+ );
234
+
235
+ act(() => {
236
+ asMultiple(result)
237
+ .getTheadCheckboxProps()
238
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
239
+ });
240
+
241
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
242
+ });
243
+
223
244
  test("deselect all when all are selected", () => {
224
245
  const { result } = renderHook(() =>
225
246
  useTableSelection({
226
- selectionMode: "multiple",
227
- allRowKeys,
228
- defaultSelectedKeys: ["a", "b", "c"],
247
+ selection: {
248
+ selectionMode: "multiple",
249
+ defaultSelectedKeys: ["a", "b", "c"],
250
+ },
251
+ visibleRowIds,
229
252
  }),
230
253
  );
231
254
 
@@ -238,12 +261,35 @@ describe("useTableSelection", () => {
238
261
  expect(asMultiple(result).selectedKeys).toEqual([]);
239
262
  });
240
263
 
264
+ test("deselect all clears hidden descendants for visible parents but preserves unrelated keys", () => {
265
+ const { result } = renderHook(() =>
266
+ useTableSelection({
267
+ selection: {
268
+ selectionMode: "multiple",
269
+ defaultSelectedKeys: ["a", "a1", "a2", "a2a", "external"],
270
+ },
271
+ visibleRowIds: ["a"],
272
+ childRowIdsById,
273
+ }),
274
+ );
275
+
276
+ act(() => {
277
+ asMultiple(result)
278
+ .getTheadCheckboxProps()
279
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
280
+ });
281
+
282
+ expect(asMultiple(result).selectedKeys).toEqual(["external"]);
283
+ });
284
+
241
285
  test("select all skips disabled keys", () => {
242
286
  const { result } = renderHook(() =>
243
287
  useTableSelection({
244
- selectionMode: "multiple",
245
- allRowKeys,
246
- disabledSelectionKeys: ["b"],
288
+ selection: {
289
+ selectionMode: "multiple",
290
+ disabledSelectionKeys: ["b"],
291
+ },
292
+ visibleRowIds,
247
293
  }),
248
294
  );
249
295
 
@@ -259,10 +305,12 @@ describe("useTableSelection", () => {
259
305
  test("deselect all preserves disabled-but-selected rows", () => {
260
306
  const { result } = renderHook(() =>
261
307
  useTableSelection({
262
- selectionMode: "multiple",
263
- allRowKeys,
264
- defaultSelectedKeys: ["a", "b", "c"],
265
- disabledSelectionKeys: ["b"],
308
+ selection: {
309
+ selectionMode: "multiple",
310
+ defaultSelectedKeys: ["a", "b", "c"],
311
+ disabledSelectionKeys: ["b"],
312
+ },
313
+ visibleRowIds,
266
314
  }),
267
315
  );
268
316
 
@@ -278,9 +326,8 @@ describe("useTableSelection", () => {
278
326
  test("thead checkbox shows indeterminate when partially selected", () => {
279
327
  const { result } = renderHook(() =>
280
328
  useTableSelection({
281
- selectionMode: "multiple",
282
- allRowKeys,
283
- defaultSelectedKeys: ["a"],
329
+ selection: { selectionMode: "multiple", defaultSelectedKeys: ["a"] },
330
+ visibleRowIds,
284
331
  }),
285
332
  );
286
333
 
@@ -292,9 +339,11 @@ describe("useTableSelection", () => {
292
339
  test("thead checkbox shows checked when all selected", () => {
293
340
  const { result } = renderHook(() =>
294
341
  useTableSelection({
295
- selectionMode: "multiple",
296
- allRowKeys,
297
- defaultSelectedKeys: ["a", "b", "c"],
342
+ selection: {
343
+ selectionMode: "multiple",
344
+ defaultSelectedKeys: ["a", "b", "c"],
345
+ },
346
+ visibleRowIds,
298
347
  }),
299
348
  );
300
349
 
@@ -306,10 +355,12 @@ describe("useTableSelection", () => {
306
355
  test("thead checkbox shows checked when all selectable rows are selected", () => {
307
356
  const { result } = renderHook(() =>
308
357
  useTableSelection({
309
- selectionMode: "multiple",
310
- allRowKeys,
311
- defaultSelectedKeys: ["a", "c"],
312
- disabledSelectionKeys: ["b"],
358
+ selection: {
359
+ selectionMode: "multiple",
360
+ defaultSelectedKeys: ["a", "c"],
361
+ disabledSelectionKeys: ["b"],
362
+ },
363
+ visibleRowIds,
313
364
  }),
314
365
  );
315
366
 
@@ -321,9 +372,11 @@ describe("useTableSelection", () => {
321
372
  test("deselecting one row when all rows are selected", () => {
322
373
  const { result } = renderHook(() =>
323
374
  useTableSelection({
324
- selectionMode: "multiple",
325
- allRowKeys,
326
- defaultSelectedKeys: ["a", "b", "c"],
375
+ selection: {
376
+ selectionMode: "multiple",
377
+ defaultSelectedKeys: ["a", "b", "c"],
378
+ },
379
+ visibleRowIds,
327
380
  }),
328
381
  );
329
382
 
@@ -339,9 +392,11 @@ describe("useTableSelection", () => {
339
392
  test("disabled rows have disabled prop", () => {
340
393
  const { result } = renderHook(() =>
341
394
  useTableSelection({
342
- selectionMode: "multiple",
343
- allRowKeys,
344
- disabledSelectionKeys: ["b"],
395
+ selection: {
396
+ selectionMode: "multiple",
397
+ disabledSelectionKeys: ["b"],
398
+ },
399
+ visibleRowIds,
345
400
  }),
346
401
  );
347
402
 
@@ -352,13 +407,82 @@ describe("useTableSelection", () => {
352
407
  test("thead checkbox disabled when all rows disabled", () => {
353
408
  const { result } = renderHook(() =>
354
409
  useTableSelection({
355
- selectionMode: "multiple",
356
- allRowKeys,
357
- disabledSelectionKeys: ["a", "b", "c"],
410
+ selection: {
411
+ selectionMode: "multiple",
412
+ disabledSelectionKeys: ["a", "b", "c"],
413
+ },
414
+ visibleRowIds,
358
415
  }),
359
416
  );
360
417
 
361
418
  expect(asMultiple(result).getTheadCheckboxProps().disabled).toBe(true);
362
419
  });
420
+
421
+ test("parent rows show indeterminate when visible descendants are partially selected", () => {
422
+ const { result } = renderHook(() =>
423
+ useTableSelection({
424
+ selection: { selectionMode: "multiple", defaultSelectedKeys: ["a1"] },
425
+ visibleRowIds: ["a", "a1", "a2"],
426
+ childRowIdsById,
427
+ }),
428
+ );
429
+
430
+ const parentProps = asMultiple(result).getRowCheckboxProps("a");
431
+
432
+ expect(parentProps.checked).toBe(false);
433
+ expect(parentProps.indeterminate).toBe(true);
434
+ });
435
+
436
+ test("toggling a parent row selects and deselects its descendants", () => {
437
+ const { result } = renderHook(() =>
438
+ useTableSelection({
439
+ selection: { selectionMode: "multiple" },
440
+ visibleRowIds: ["a", "a1", "a2"],
441
+ childRowIdsById,
442
+ }),
443
+ );
444
+
445
+ act(() => {
446
+ asMultiple(result)
447
+ .getRowCheckboxProps("a")
448
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
449
+ });
450
+
451
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
452
+
453
+ act(() => {
454
+ asMultiple(result)
455
+ .getRowCheckboxProps("a")
456
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
457
+ });
458
+
459
+ expect(asMultiple(result).selectedKeys).toEqual([]);
460
+ });
461
+
462
+ test("toggling a collapsed parent selects and deselects hidden descendants", () => {
463
+ const { result } = renderHook(() =>
464
+ useTableSelection({
465
+ selection: { selectionMode: "multiple" },
466
+ visibleRowIds: ["a"],
467
+ childRowIdsById,
468
+ }),
469
+ );
470
+
471
+ act(() => {
472
+ asMultiple(result)
473
+ .getRowCheckboxProps("a")
474
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
475
+ });
476
+
477
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "a1", "a2", "a2a"]);
478
+
479
+ act(() => {
480
+ asMultiple(result)
481
+ .getRowCheckboxProps("a")
482
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
483
+ });
484
+
485
+ expect(asMultiple(result).selectedKeys).toEqual([]);
486
+ });
363
487
  });
364
488
  });
@@ -1,3 +1,4 @@
1
+ import { useMemo } from "react";
1
2
  import type {
2
3
  ColumnDefinition,
3
4
  ColumnDefinitions,
@@ -28,19 +29,32 @@ function useColumnOptions<T>(
28
29
 
29
30
  const hasSelection = selectionMode !== "none";
30
31
 
31
- return {
32
- stickySelection: selectionMode !== "none" && stickyColumns?.first === "1",
33
- columns: columnDefinitions.map((colDef, index) => {
32
+ const columns = useMemo(() => {
33
+ return columnDefinitions.map((colDef, index) => {
34
34
  const isFirstSticky =
35
35
  stickyColumns?.first === "1" && index === 0 && !hasSelection;
36
36
  const isLastSticky =
37
37
  stickyColumns?.last === "1" && index === columnDefinitions.length - 1;
38
38
 
39
39
  return {
40
- isSticky: isFirstSticky ? "start" : isLastSticky ? "end" : false,
40
+ isSticky: isFirstSticky
41
+ ? ("start" as const)
42
+ : isLastSticky
43
+ ? ("end" as const)
44
+ : (false as const),
41
45
  colDef,
42
46
  };
43
- }),
47
+ });
48
+ }, [
49
+ columnDefinitions,
50
+ hasSelection,
51
+ stickyColumns?.first,
52
+ stickyColumns?.last,
53
+ ]);
54
+
55
+ return {
56
+ stickySelection: selectionMode !== "none" && stickyColumns?.first === "1",
57
+ columns,
44
58
  };
45
59
  }
46
60
 
@@ -0,0 +1,182 @@
1
+ import React, { useCallback } from "react";
2
+ import { createStrictContext } from "../../../utils/helpers";
3
+ import { useControllableState } from "../../../utils/hooks";
4
+ import { useTableItemsContext } from "./useTableItems";
5
+
6
+ type DetailsPanelProps<T> = {
7
+ /**
8
+ * Renders a details panel below the row when expanded.
9
+ * When provided, an expand toggle column is added automatically.
10
+ */
11
+ getContent?: (rowData: T) => React.ReactNode;
12
+ /**
13
+ * Determines whether a row can be expanded to show details panel content.
14
+ * @default () => true
15
+ */
16
+ isRowExpandable?: (rowData: T) => boolean;
17
+ /**
18
+ * Controlled list of expanded row IDs.
19
+ * Use with `onDetailsPanelChange` for controlled usage, or `defaultDetailsPanelRowIds` for uncontrolled.
20
+ */
21
+ expandedRowIds?: (string | number)[];
22
+ /**
23
+ * Initial list of expanded row IDs for uncontrolled usage.
24
+ * @default []
25
+ */
26
+ defaultExpandedRowIds?: (string | number)[];
27
+ /**
28
+ * Called when the list of expanded row IDs changes.
29
+ *
30
+ *
31
+ * TODO:
32
+ * - Docs: This pattern is called "Master / Detail" in general terms
33
+ */
34
+ onExpandedRowIdsChange?: (ids: (string | number)[]) => void;
35
+ /**
36
+ * Returns the height (in px) or `"auto"` for a row's details panel.
37
+ * When a number is returned, the panel scrolls within that fixed height.
38
+ * @default "auto"
39
+ */
40
+ getHeight?: (rowData: T) => number | "auto";
41
+ /**
42
+ * Shows an expand-all toggle button in the expand column header.
43
+ * @default false
44
+ */
45
+ showExpandAll?: boolean;
46
+ };
47
+
48
+ type DataTableDetailsPanelContextT = {
49
+ isExpanded: (id: string | number) => boolean;
50
+ isDetailsPanelExpandable: (id: string | number) => boolean;
51
+ toggleExpansion: (id: string | number) => void;
52
+ toggleAll: () => void;
53
+ isAllExpanded: boolean;
54
+ getDetailsPanelContent?: (row: unknown) => React.ReactNode;
55
+ getDetailsPanelHeight?: (row: unknown) => number | "auto";
56
+ showExpandAll: boolean;
57
+ enableDetailsPanel: boolean;
58
+ };
59
+
60
+ const {
61
+ Provider: DataTableDetailsPanelContextProvider,
62
+ useContext: useDataTableDetailsPanel,
63
+ } = createStrictContext<DataTableDetailsPanelContextT>({
64
+ name: "DataTableDetailsPanelContext",
65
+ errorMessage:
66
+ "useDataTableDetailsPanel must be used within a DataTableDetailsPanelProvider.",
67
+ });
68
+
69
+ function DataTableDetailsPanelProvider<T>({
70
+ children,
71
+ detailsPanel = {},
72
+ }: { detailsPanel?: DetailsPanelProps<T> } & { children: React.ReactNode }) {
73
+ const {
74
+ expandedRowIds,
75
+ defaultExpandedRowIds = [],
76
+ onExpandedRowIdsChange,
77
+ getContent,
78
+ isRowExpandable,
79
+ getHeight,
80
+ showExpandAll = false,
81
+ } = detailsPanel;
82
+
83
+ const [expandedIds, setExpandedIds] = useControllableState({
84
+ value: expandedRowIds,
85
+ defaultValue: defaultExpandedRowIds,
86
+ onChange: onExpandedRowIdsChange,
87
+ });
88
+
89
+ /* TODO: False is just fallback until auto and root is merged */
90
+ const tableItemsContext = useTableItemsContext(false);
91
+
92
+ const { itemDetails } = tableItemsContext ?? {
93
+ itemDetails: new Map(),
94
+ };
95
+
96
+ const expandableIds = React.useMemo(() => {
97
+ if (!getContent) {
98
+ return new Set<string | number>();
99
+ }
100
+
101
+ const ids = new Set<string | number>();
102
+
103
+ for (const [rowData, { id, level }] of itemDetails.entries()) {
104
+ /* We only allow Master - Details pattern on top level rows */
105
+ if (level > 0) {
106
+ continue;
107
+ }
108
+
109
+ if (!isRowExpandable || isRowExpandable(rowData)) {
110
+ ids.add(id);
111
+ }
112
+ }
113
+
114
+ return ids;
115
+ }, [getContent, isRowExpandable, itemDetails]);
116
+
117
+ const isDetailsPanelExpandableById = useCallback(
118
+ (id: string | number) => expandableIds.has(id),
119
+ [expandableIds],
120
+ );
121
+
122
+ const isExpanded = useCallback(
123
+ (id: string | number) =>
124
+ isDetailsPanelExpandableById(id) && expandedIds.includes(id),
125
+ [expandedIds, isDetailsPanelExpandableById],
126
+ );
127
+
128
+ const toggleExpansion = useCallback(
129
+ (id: string | number) => {
130
+ if (!isDetailsPanelExpandableById(id)) {
131
+ return;
132
+ }
133
+
134
+ setExpandedIds((currentExpandedIds) =>
135
+ currentExpandedIds.includes(id)
136
+ ? currentExpandedIds.filter((expandedId) => expandedId !== id)
137
+ : [...currentExpandedIds, id],
138
+ );
139
+ },
140
+ [isDetailsPanelExpandableById, setExpandedIds],
141
+ );
142
+
143
+ const isAllExpanded =
144
+ expandableIds.size > 0 &&
145
+ Array.from(expandableIds).every((key) => expandedIds.includes(key));
146
+
147
+ const toggleAll = useCallback(() => {
148
+ setExpandedIds(isAllExpanded ? [] : Array.from(expandableIds));
149
+ }, [expandableIds, isAllExpanded, setExpandedIds]);
150
+
151
+ return (
152
+ <DataTableDetailsPanelContextProvider
153
+ isExpanded={isExpanded}
154
+ isDetailsPanelExpandable={isDetailsPanelExpandableById}
155
+ toggleExpansion={toggleExpansion}
156
+ toggleAll={toggleAll}
157
+ isAllExpanded={isAllExpanded}
158
+ getDetailsPanelContent={
159
+ getContent as ((row: unknown) => React.ReactNode) | undefined
160
+ }
161
+ getDetailsPanelHeight={
162
+ getHeight as ((row: unknown) => number | "auto") | undefined
163
+ }
164
+ showExpandAll={showExpandAll}
165
+ enableDetailsPanel={!!getContent}
166
+ >
167
+ {children}
168
+ </DataTableDetailsPanelContextProvider>
169
+ );
170
+ }
171
+
172
+ function getDataTableDetailsPanelId(tableId: string, rowId: string | number) {
173
+ return `${tableId}-expansion-${rowId}`;
174
+ }
175
+
176
+ export {
177
+ DataTableDetailsPanelProvider,
178
+ getDataTableDetailsPanelId,
179
+ useDataTableDetailsPanel,
180
+ };
181
+
182
+ export type { DetailsPanelProps };