@navikt/ds-react 8.8.0 → 8.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/cjs/data/drag-and-drop/drag-handler/DragAndDropDragHandler.d.ts +12 -0
  2. package/cjs/data/drag-and-drop/drag-handler/DragAndDropDragHandler.js +57 -0
  3. package/cjs/data/drag-and-drop/drag-handler/DragAndDropDragHandler.js.map +1 -0
  4. package/cjs/data/drag-and-drop/item/DragAndDropItem.d.ts +27 -0
  5. package/cjs/data/drag-and-drop/item/DragAndDropItem.js +52 -0
  6. package/cjs/data/drag-and-drop/item/DragAndDropItem.js.map +1 -0
  7. package/cjs/data/drag-and-drop/root/DragAndDrop.context.d.ts +16 -0
  8. package/cjs/data/drag-and-drop/root/DragAndDrop.context.js +10 -0
  9. package/cjs/data/drag-and-drop/root/DragAndDrop.context.js.map +1 -0
  10. package/cjs/data/drag-and-drop/root/{DataDragAndDropRoot.d.ts → DragAndDropRoot.d.ts} +11 -9
  11. package/cjs/data/drag-and-drop/root/DragAndDropRoot.js +187 -0
  12. package/cjs/data/drag-and-drop/root/DragAndDropRoot.js.map +1 -0
  13. package/cjs/data/drag-and-drop/types.d.ts +4 -0
  14. package/cjs/data/drag-and-drop/types.js +3 -0
  15. package/cjs/data/drag-and-drop/types.js.map +1 -0
  16. package/cjs/data/table/helpers/selection/getMultipleSelectProps.d.ts +14 -0
  17. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js +48 -0
  18. package/cjs/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -0
  19. package/cjs/data/table/helpers/selection/getSingleSelectProps.d.ts +10 -0
  20. package/cjs/data/table/helpers/selection/getSingleSelectProps.js +23 -0
  21. package/cjs/data/table/helpers/selection/getSingleSelectProps.js.map +1 -0
  22. package/cjs/data/table/helpers/selection/selection.types.d.ts +42 -0
  23. package/cjs/data/table/helpers/selection/selection.types.js +3 -0
  24. package/cjs/data/table/helpers/selection/selection.types.js.map +1 -0
  25. package/cjs/data/table/{root → hooks}/useTableKeyboardNav.js +1 -1
  26. package/cjs/data/table/hooks/useTableKeyboardNav.js.map +1 -0
  27. package/cjs/data/table/hooks/useTableSelection.d.ts +8 -0
  28. package/cjs/data/table/hooks/useTableSelection.js +49 -0
  29. package/cjs/data/table/hooks/useTableSelection.js.map +1 -0
  30. package/cjs/data/table/root/DataTableAuto.d.ts +4 -4
  31. package/cjs/data/table/root/DataTableAuto.js +24 -10
  32. package/cjs/data/table/root/DataTableAuto.js.map +1 -1
  33. package/cjs/data/table/root/DataTableRoot.d.ts +1 -1
  34. package/cjs/data/table/root/DataTableRoot.js +1 -1
  35. package/cjs/data/table/root/DataTableRoot.js.map +1 -1
  36. package/cjs/data/token-filter/AutoSuggest.js +37 -4
  37. package/cjs/data/token-filter/AutoSuggest.js.map +1 -1
  38. package/cjs/data/token-filter/TokenFilter.d.ts +1 -0
  39. package/cjs/data/token-filter/TokenFilter.js +1 -0
  40. package/cjs/data/token-filter/TokenFilter.js.map +1 -1
  41. package/cjs/data/toolbar/root/DataToolbarRoot.d.ts +6 -23
  42. package/cjs/data/toolbar/root/DataToolbarRoot.js +42 -7
  43. package/cjs/data/toolbar/root/DataToolbarRoot.js.map +1 -1
  44. package/cjs/date/Date.Input.js +8 -9
  45. package/cjs/date/Date.Input.js.map +1 -1
  46. package/cjs/date/datepicker/hooks/useDatepicker.js +4 -3
  47. package/cjs/date/datepicker/hooks/useDatepicker.js.map +1 -1
  48. package/cjs/date/monthpicker/hooks/useMonthPicker.js +3 -2
  49. package/cjs/date/monthpicker/hooks/useMonthPicker.js.map +1 -1
  50. package/cjs/internal-header/InternalHeaderButton.d.ts +5 -0
  51. package/cjs/internal-header/InternalHeaderButton.js +2 -2
  52. package/cjs/internal-header/InternalHeaderButton.js.map +1 -1
  53. package/cjs/utils/components/Listbox/group/ListboxGroup.js +2 -1
  54. package/cjs/utils/components/Listbox/group/ListboxGroup.js.map +1 -1
  55. package/esm/data/drag-and-drop/drag-handler/DragAndDropDragHandler.d.ts +12 -0
  56. package/esm/data/drag-and-drop/drag-handler/DragAndDropDragHandler.js +51 -0
  57. package/esm/data/drag-and-drop/drag-handler/DragAndDropDragHandler.js.map +1 -0
  58. package/esm/data/drag-and-drop/item/DragAndDropItem.d.ts +27 -0
  59. package/esm/data/drag-and-drop/item/DragAndDropItem.js +46 -0
  60. package/esm/data/drag-and-drop/item/DragAndDropItem.js.map +1 -0
  61. package/esm/data/drag-and-drop/root/DragAndDrop.context.d.ts +16 -0
  62. package/esm/data/drag-and-drop/root/DragAndDrop.context.js +6 -0
  63. package/esm/data/drag-and-drop/root/DragAndDrop.context.js.map +1 -0
  64. package/esm/data/drag-and-drop/root/{DataDragAndDropRoot.d.ts → DragAndDropRoot.d.ts} +11 -9
  65. package/esm/data/drag-and-drop/root/DragAndDropRoot.js +147 -0
  66. package/esm/data/drag-and-drop/root/DragAndDropRoot.js.map +1 -0
  67. package/esm/data/drag-and-drop/types.d.ts +4 -0
  68. package/esm/data/drag-and-drop/types.js +2 -0
  69. package/esm/data/drag-and-drop/types.js.map +1 -0
  70. package/esm/data/table/helpers/selection/getMultipleSelectProps.d.ts +14 -0
  71. package/esm/data/table/helpers/selection/getMultipleSelectProps.js +46 -0
  72. package/esm/data/table/helpers/selection/getMultipleSelectProps.js.map +1 -0
  73. package/esm/data/table/helpers/selection/getSingleSelectProps.d.ts +10 -0
  74. package/esm/data/table/helpers/selection/getSingleSelectProps.js +21 -0
  75. package/esm/data/table/helpers/selection/getSingleSelectProps.js.map +1 -0
  76. package/esm/data/table/helpers/selection/selection.types.d.ts +42 -0
  77. package/esm/data/table/helpers/selection/selection.types.js +2 -0
  78. package/esm/data/table/helpers/selection/selection.types.js.map +1 -0
  79. package/esm/data/table/{root → hooks}/useTableKeyboardNav.js +1 -1
  80. package/esm/data/table/hooks/useTableKeyboardNav.js.map +1 -0
  81. package/esm/data/table/hooks/useTableSelection.d.ts +8 -0
  82. package/esm/data/table/hooks/useTableSelection.js +47 -0
  83. package/esm/data/table/hooks/useTableSelection.js.map +1 -0
  84. package/esm/data/table/root/DataTableAuto.d.ts +4 -4
  85. package/esm/data/table/root/DataTableAuto.js +24 -10
  86. package/esm/data/table/root/DataTableAuto.js.map +1 -1
  87. package/esm/data/table/root/DataTableRoot.d.ts +1 -1
  88. package/esm/data/table/root/DataTableRoot.js +1 -1
  89. package/esm/data/table/root/DataTableRoot.js.map +1 -1
  90. package/esm/data/token-filter/AutoSuggest.js +38 -5
  91. package/esm/data/token-filter/AutoSuggest.js.map +1 -1
  92. package/esm/data/token-filter/TokenFilter.d.ts +1 -0
  93. package/esm/data/token-filter/TokenFilter.js +1 -0
  94. package/esm/data/token-filter/TokenFilter.js.map +1 -1
  95. package/esm/data/toolbar/root/DataToolbarRoot.d.ts +6 -23
  96. package/esm/data/toolbar/root/DataToolbarRoot.js +9 -7
  97. package/esm/data/toolbar/root/DataToolbarRoot.js.map +1 -1
  98. package/esm/date/Date.Input.js +9 -10
  99. package/esm/date/Date.Input.js.map +1 -1
  100. package/esm/date/datepicker/hooks/useDatepicker.js +4 -3
  101. package/esm/date/datepicker/hooks/useDatepicker.js.map +1 -1
  102. package/esm/date/monthpicker/hooks/useMonthPicker.js +3 -2
  103. package/esm/date/monthpicker/hooks/useMonthPicker.js.map +1 -1
  104. package/esm/internal-header/InternalHeaderButton.d.ts +5 -0
  105. package/esm/internal-header/InternalHeaderButton.js +2 -2
  106. package/esm/internal-header/InternalHeaderButton.js.map +1 -1
  107. package/esm/utils/components/Listbox/group/ListboxGroup.js +2 -1
  108. package/esm/utils/components/Listbox/group/ListboxGroup.js.map +1 -1
  109. package/package.json +4 -4
  110. package/src/data/drag-and-drop/drag-handler/DragAndDropDragHandler.tsx +90 -0
  111. package/src/data/drag-and-drop/item/DragAndDropItem.tsx +71 -0
  112. package/src/data/drag-and-drop/root/DragAndDrop.context.tsx +25 -0
  113. package/src/data/drag-and-drop/root/DragAndDropRoot.tsx +245 -0
  114. package/src/data/drag-and-drop/types.ts +4 -0
  115. package/src/data/table/helpers/selection/getMultipleSelectProps.ts +70 -0
  116. package/src/data/table/helpers/selection/getSingleSelectProps.ts +33 -0
  117. package/src/data/table/helpers/selection/selection.types.ts +56 -0
  118. package/src/data/table/hooks/__tests__/useTableSelection.test.ts +327 -0
  119. package/src/data/table/{root → hooks}/useTableKeyboardNav.ts +1 -1
  120. package/src/data/table/hooks/useTableSelection.ts +78 -0
  121. package/src/data/table/root/DataTableAuto.tsx +46 -23
  122. package/src/data/table/root/DataTableRoot.tsx +2 -2
  123. package/src/data/token-filter/AutoSuggest.tsx +65 -3
  124. package/src/data/token-filter/TokenFilter.tsx +1 -0
  125. package/src/data/toolbar/root/DataToolbarRoot.tsx +29 -32
  126. package/src/date/Date.Input.tsx +17 -16
  127. package/src/date/datepicker/hooks/useDatepicker.tsx +4 -5
  128. package/src/date/monthpicker/hooks/useMonthPicker.tsx +3 -4
  129. package/src/internal-header/InternalHeaderButton.tsx +18 -9
  130. package/src/utils/components/Listbox/group/ListboxGroup.tsx +9 -2
  131. package/cjs/data/action-bar/root/DataActionBarRoot.d.ts +0 -27
  132. package/cjs/data/action-bar/root/DataActionBarRoot.js +0 -49
  133. package/cjs/data/action-bar/root/DataActionBarRoot.js.map +0 -1
  134. package/cjs/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.d.ts +0 -21
  135. package/cjs/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.js +0 -24
  136. package/cjs/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.js.map +0 -1
  137. package/cjs/data/drag-and-drop/item/DataDragAndDropItem.d.ts +0 -27
  138. package/cjs/data/drag-and-drop/item/DataDragAndDropItem.js +0 -41
  139. package/cjs/data/drag-and-drop/item/DataDragAndDropItem.js.map +0 -1
  140. package/cjs/data/drag-and-drop/root/DataDragAndDrop.context.d.ts +0 -8
  141. package/cjs/data/drag-and-drop/root/DataDragAndDrop.context.js +0 -10
  142. package/cjs/data/drag-and-drop/root/DataDragAndDrop.context.js.map +0 -1
  143. package/cjs/data/drag-and-drop/root/DataDragAndDropRoot.js +0 -61
  144. package/cjs/data/drag-and-drop/root/DataDragAndDropRoot.js.map +0 -1
  145. package/cjs/data/table/root/useTableKeyboardNav.js.map +0 -1
  146. package/cjs/data/table/root/useTableSelection.d.ts +0 -55
  147. package/cjs/data/table/root/useTableSelection.js +0 -79
  148. package/cjs/data/table/root/useTableSelection.js.map +0 -1
  149. package/esm/data/action-bar/root/DataActionBarRoot.d.ts +0 -27
  150. package/esm/data/action-bar/root/DataActionBarRoot.js +0 -43
  151. package/esm/data/action-bar/root/DataActionBarRoot.js.map +0 -1
  152. package/esm/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.d.ts +0 -21
  153. package/esm/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.js +0 -18
  154. package/esm/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.js.map +0 -1
  155. package/esm/data/drag-and-drop/item/DataDragAndDropItem.d.ts +0 -27
  156. package/esm/data/drag-and-drop/item/DataDragAndDropItem.js +0 -35
  157. package/esm/data/drag-and-drop/item/DataDragAndDropItem.js.map +0 -1
  158. package/esm/data/drag-and-drop/root/DataDragAndDrop.context.d.ts +0 -8
  159. package/esm/data/drag-and-drop/root/DataDragAndDrop.context.js +0 -6
  160. package/esm/data/drag-and-drop/root/DataDragAndDrop.context.js.map +0 -1
  161. package/esm/data/drag-and-drop/root/DataDragAndDropRoot.js +0 -21
  162. package/esm/data/drag-and-drop/root/DataDragAndDropRoot.js.map +0 -1
  163. package/esm/data/table/root/useTableKeyboardNav.js.map +0 -1
  164. package/esm/data/table/root/useTableSelection.d.ts +0 -55
  165. package/esm/data/table/root/useTableSelection.js +0 -77
  166. package/esm/data/table/root/useTableSelection.js.map +0 -1
  167. package/src/data/action-bar/root/DataActionBarRoot.tsx +0 -59
  168. package/src/data/drag-and-drop/drag-handler/DataDragAndDropDragHandler.tsx +0 -63
  169. package/src/data/drag-and-drop/item/DataDragAndDropItem.tsx +0 -54
  170. package/src/data/drag-and-drop/root/DataDragAndDrop.context.tsx +0 -14
  171. package/src/data/drag-and-drop/root/DataDragAndDropRoot.tsx +0 -54
  172. package/src/data/table/root/useTableSelection.ts +0 -126
  173. /package/cjs/data/table/{root → hooks}/useTableKeyboardNav.d.ts +0 -0
  174. /package/esm/data/table/{root → hooks}/useTableKeyboardNav.d.ts +0 -0
@@ -0,0 +1,70 @@
1
+ import type { CheckboxProps } from "../../../../form/checkbox/types";
2
+ import type { SelectionT } from "./selection.types";
3
+
4
+ type GetMultipleSelectPropsArgs = {
5
+ selectedKeys: SelectionT;
6
+ setSelectedKeys: (keys: SelectionT) => void;
7
+ disabledKeys: (string | number)[];
8
+ allKeys: (string | number)[];
9
+ totalCount: number;
10
+ };
11
+
12
+ function getMultipleSelectProps({
13
+ selectedKeys,
14
+ setSelectedKeys,
15
+ disabledKeys,
16
+ allKeys,
17
+ totalCount,
18
+ }: GetMultipleSelectPropsArgs) {
19
+ const handleToggleAll = () => {
20
+ const allSelected =
21
+ selectedKeys === "all" ||
22
+ (Array.isArray(selectedKeys) && selectedKeys.length === totalCount);
23
+
24
+ setSelectedKeys(allSelected ? [] : allKeys);
25
+ };
26
+
27
+ const handleToggleRow = (key: string | number) => {
28
+ if (selectedKeys === "all") {
29
+ setSelectedKeys(allKeys.filter((id) => id !== key));
30
+ } else if (selectedKeys.includes(key)) {
31
+ setSelectedKeys(selectedKeys.filter((k) => k !== key));
32
+ } else {
33
+ setSelectedKeys([...selectedKeys, key]);
34
+ }
35
+ };
36
+
37
+ const isChecked = (key: string | number) =>
38
+ selectedKeys === "all" ||
39
+ (Array.isArray(selectedKeys) && selectedKeys.includes(key));
40
+
41
+ return {
42
+ getTheadCheckboxProps: (): CheckboxProps => {
43
+ const indeterminate =
44
+ Array.isArray(selectedKeys) &&
45
+ selectedKeys.length > 0 &&
46
+ selectedKeys.length < totalCount;
47
+
48
+ return {
49
+ children: "Select all rows",
50
+ onChange: handleToggleAll,
51
+ checked:
52
+ (selectedKeys === "all" ||
53
+ (Array.isArray(selectedKeys) && selectedKeys.length > 0)) &&
54
+ !indeterminate,
55
+ indeterminate,
56
+ disabled: disabledKeys.length === totalCount,
57
+ hideLabel: true,
58
+ };
59
+ },
60
+ getRowCheckboxProps: (key: string | number): CheckboxProps => ({
61
+ children: `Select row with id ${key}`,
62
+ onChange: () => handleToggleRow(key),
63
+ checked: isChecked(key),
64
+ disabled: disabledKeys.includes(key),
65
+ hideLabel: true,
66
+ }),
67
+ };
68
+ }
69
+
70
+ export { getMultipleSelectProps };
@@ -0,0 +1,33 @@
1
+ import type { RadioProps } from "../../../../form/radio/types";
2
+
3
+ type GetSingleSelectPropsArgs = {
4
+ selectedKeys: (string | number)[];
5
+ setSelectedKeys: (keys: (string | number)[]) => void;
6
+ disabledKeys: (string | number)[];
7
+ };
8
+
9
+ function getSingleSelectProps({
10
+ selectedKeys,
11
+ setSelectedKeys,
12
+ disabledKeys,
13
+ }: GetSingleSelectPropsArgs) {
14
+ const handleSelectionChange = (key: string | number) => {
15
+ if (selectedKeys.includes(key)) {
16
+ setSelectedKeys([]);
17
+ } else {
18
+ setSelectedKeys([key]);
19
+ }
20
+ };
21
+
22
+ return {
23
+ getRowRadioProps: (key: string | number): RadioProps => ({
24
+ children: `Select row with id ${key}`,
25
+ checked: selectedKeys.includes(key),
26
+ onChange: () => handleSelectionChange(key),
27
+ disabled: disabledKeys.includes(key),
28
+ value: key,
29
+ }),
30
+ };
31
+ }
32
+
33
+ export { getSingleSelectProps };
@@ -0,0 +1,56 @@
1
+ import type { CheckboxProps } from "../../../../form/checkbox/types";
2
+ import type { RadioProps } from "../../../../form/radio/types";
3
+
4
+ type SelectionT = (string | number)[] | "all";
5
+
6
+ type SelectionProps = {
7
+ /**
8
+ * Enables selection of rows.
9
+ *
10
+ * When set to "single", only one row can be selected at a time (renders radio buttons).
11
+ *
12
+ * When set to "multiple", multiple rows can be selected (renders checkboxes).
13
+ *
14
+ * @default "none"
15
+ */
16
+ selectionMode?: "none" | "single" | "multiple";
17
+ selectedKeys?: SelectionT;
18
+ defaultSelectedKeys?: SelectionT;
19
+ onSelectionChange?: (keys: SelectionT) => void;
20
+ disabledKeys?: (string | number)[];
21
+ };
22
+
23
+ type NoneSelection = {
24
+ selectionMode: "none";
25
+ allKeys: (string | number)[];
26
+ selectedKeys: (string | number)[];
27
+ disabledKeys: (string | number)[];
28
+ };
29
+
30
+ type SingleSelection = {
31
+ selectionMode: "single";
32
+ allKeys: (string | number)[];
33
+ selectedKeys: (string | number)[];
34
+ disabledKeys: (string | number)[];
35
+ getRowRadioProps: (key: string | number) => RadioProps;
36
+ };
37
+
38
+ type MultipleSelection = {
39
+ selectionMode: "multiple";
40
+ allKeys: (string | number)[];
41
+ selectedKeys: SelectionT;
42
+ disabledKeys: (string | number)[];
43
+ getTheadCheckboxProps: () => CheckboxProps;
44
+ getRowCheckboxProps: (key: string | number) => CheckboxProps;
45
+ };
46
+
47
+ type TableSelection = NoneSelection | SingleSelection | MultipleSelection;
48
+
49
+ export type {
50
+ MultipleSelection,
51
+ NoneSelection,
52
+ SelectionProps,
53
+ SelectionT,
54
+ SingleSelection,
55
+ TableSelection,
56
+ };
@@ -0,0 +1,327 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import { describe, expect, test, vi } from "vitest";
3
+ import type {
4
+ MultipleSelection,
5
+ SingleSelection,
6
+ } from "../../helpers/selection/selection.types";
7
+ import { useTableSelection } from "../useTableSelection";
8
+
9
+ type Item = { id: string; name: string };
10
+
11
+ const items: Item[] = [
12
+ { id: "a", name: "Alpha" },
13
+ { id: "b", name: "Beta" },
14
+ { id: "c", name: "Charlie" },
15
+ ];
16
+
17
+ const getRowId = (item: Item) => item.id;
18
+
19
+ function asSingle(
20
+ result: ReturnType<typeof renderHook>["result"],
21
+ ): SingleSelection {
22
+ return result.current as SingleSelection;
23
+ }
24
+
25
+ function asMultiple(
26
+ result: ReturnType<typeof renderHook>["result"],
27
+ ): MultipleSelection {
28
+ return result.current as MultipleSelection;
29
+ }
30
+
31
+ describe("useTableSelection", () => {
32
+ describe('selectionMode="none"', () => {
33
+ test("returns empty selectedKeys and no prop getters", () => {
34
+ const { result } = renderHook(() =>
35
+ useTableSelection({
36
+ selectionMode: "none",
37
+ data: items,
38
+ getRowId,
39
+ }),
40
+ );
41
+
42
+ expect(result.current.selectionMode).toBe("none");
43
+ expect(result.current.selectedKeys).toEqual([]);
44
+ expect(result.current.allKeys).toEqual(["a", "b", "c"]);
45
+ });
46
+ });
47
+
48
+ describe('selectionMode="single"', () => {
49
+ test("returns getRowRadioProps", () => {
50
+ const { result } = renderHook(() =>
51
+ useTableSelection({
52
+ selectionMode: "single",
53
+ data: items,
54
+ getRowId,
55
+ }),
56
+ );
57
+
58
+ expect(result.current.selectionMode).toBe("single");
59
+ expect(asSingle(result).getRowRadioProps).toBeDefined();
60
+ });
61
+
62
+ test("selecting a row via radio onChange", () => {
63
+ const onChange = vi.fn();
64
+ const { result } = renderHook(() =>
65
+ useTableSelection({
66
+ selectionMode: "single",
67
+ data: items,
68
+ getRowId,
69
+ onSelectionChange: onChange,
70
+ }),
71
+ );
72
+
73
+ const radioProps = asSingle(result).getRowRadioProps("a");
74
+ expect(radioProps.checked).toBe(false);
75
+
76
+ act(() => {
77
+ radioProps.onChange?.({} as React.ChangeEvent<HTMLInputElement>);
78
+ });
79
+
80
+ expect(asSingle(result).selectedKeys).toEqual(["a"]);
81
+ });
82
+
83
+ test("toggling the same row deselects it", () => {
84
+ const { result } = renderHook(() =>
85
+ useTableSelection({
86
+ selectionMode: "single",
87
+ data: items,
88
+ getRowId,
89
+ defaultSelectedKeys: ["a"],
90
+ }),
91
+ );
92
+
93
+ act(() => {
94
+ asSingle(result)
95
+ .getRowRadioProps("a")
96
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
97
+ });
98
+
99
+ expect(asSingle(result).selectedKeys).toEqual([]);
100
+ });
101
+
102
+ test("selecting a new row replaces the previous", () => {
103
+ const { result } = renderHook(() =>
104
+ useTableSelection({
105
+ selectionMode: "single",
106
+ data: items,
107
+ getRowId,
108
+ defaultSelectedKeys: ["a"],
109
+ }),
110
+ );
111
+
112
+ act(() => {
113
+ asSingle(result)
114
+ .getRowRadioProps("b")
115
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
116
+ });
117
+
118
+ expect(asSingle(result).selectedKeys).toEqual(["b"]);
119
+ });
120
+
121
+ test("disabled rows have disabled prop", () => {
122
+ const { result } = renderHook(() =>
123
+ useTableSelection({
124
+ selectionMode: "single",
125
+ data: items,
126
+ getRowId,
127
+ disabledKeys: ["b"],
128
+ }),
129
+ );
130
+
131
+ expect(asSingle(result).getRowRadioProps("a").disabled).toBe(false);
132
+ expect(asSingle(result).getRowRadioProps("b").disabled).toBe(true);
133
+ });
134
+
135
+ test("controlled selectedKeys", () => {
136
+ const { result, rerender } = renderHook(
137
+ ({ selectedKeys }) =>
138
+ useTableSelection({
139
+ selectionMode: "single",
140
+ data: items,
141
+ getRowId,
142
+ selectedKeys,
143
+ }),
144
+ { initialProps: { selectedKeys: ["a"] as (string | number)[] } },
145
+ );
146
+
147
+ expect(asSingle(result).selectedKeys).toEqual(["a"]);
148
+
149
+ rerender({ selectedKeys: ["b"] });
150
+ expect(asSingle(result).selectedKeys).toEqual(["b"]);
151
+ });
152
+ });
153
+
154
+ describe('selectionMode="multiple"', () => {
155
+ test("returns getTheadCheckboxProps and getRowCheckboxProps", () => {
156
+ const { result } = renderHook(() =>
157
+ useTableSelection({
158
+ selectionMode: "multiple",
159
+ data: items,
160
+ getRowId,
161
+ }),
162
+ );
163
+
164
+ expect(result.current.selectionMode).toBe("multiple");
165
+ expect(asMultiple(result).getTheadCheckboxProps).toBeDefined();
166
+ expect(asMultiple(result).getRowCheckboxProps).toBeDefined();
167
+ });
168
+
169
+ test("selecting individual rows", () => {
170
+ const { result } = renderHook(() =>
171
+ useTableSelection({
172
+ selectionMode: "multiple",
173
+ data: items,
174
+ getRowId,
175
+ }),
176
+ );
177
+
178
+ act(() => {
179
+ asMultiple(result)
180
+ .getRowCheckboxProps("a")
181
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
182
+ });
183
+
184
+ expect(asMultiple(result).selectedKeys).toEqual(["a"]);
185
+
186
+ act(() => {
187
+ asMultiple(result)
188
+ .getRowCheckboxProps("c")
189
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
190
+ });
191
+
192
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "c"]);
193
+ });
194
+
195
+ test("deselecting a row", () => {
196
+ const { result } = renderHook(() =>
197
+ useTableSelection({
198
+ selectionMode: "multiple",
199
+ data: items,
200
+ getRowId,
201
+ defaultSelectedKeys: ["a", "b"],
202
+ }),
203
+ );
204
+
205
+ act(() => {
206
+ asMultiple(result)
207
+ .getRowCheckboxProps("a")
208
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
209
+ });
210
+
211
+ expect(asMultiple(result).selectedKeys).toEqual(["b"]);
212
+ });
213
+
214
+ test("select all via thead checkbox", () => {
215
+ const { result } = renderHook(() =>
216
+ useTableSelection({
217
+ selectionMode: "multiple",
218
+ data: items,
219
+ getRowId,
220
+ }),
221
+ );
222
+
223
+ act(() => {
224
+ asMultiple(result)
225
+ .getTheadCheckboxProps()
226
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
227
+ });
228
+
229
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "b", "c"]);
230
+ });
231
+
232
+ test("deselect all when all are selected", () => {
233
+ const { result } = renderHook(() =>
234
+ useTableSelection({
235
+ selectionMode: "multiple",
236
+ data: items,
237
+ getRowId,
238
+ defaultSelectedKeys: ["a", "b", "c"],
239
+ }),
240
+ );
241
+
242
+ act(() => {
243
+ asMultiple(result)
244
+ .getTheadCheckboxProps()
245
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
246
+ });
247
+
248
+ expect(asMultiple(result).selectedKeys).toEqual([]);
249
+ });
250
+
251
+ test("thead checkbox shows indeterminate when partially selected", () => {
252
+ const { result } = renderHook(() =>
253
+ useTableSelection({
254
+ selectionMode: "multiple",
255
+ data: items,
256
+ getRowId,
257
+ defaultSelectedKeys: ["a"],
258
+ }),
259
+ );
260
+
261
+ const theadProps = asMultiple(result).getTheadCheckboxProps();
262
+ expect(theadProps.indeterminate).toBe(true);
263
+ expect(theadProps.checked).toBe(false);
264
+ });
265
+
266
+ test("thead checkbox shows checked when all selected", () => {
267
+ const { result } = renderHook(() =>
268
+ useTableSelection({
269
+ selectionMode: "multiple",
270
+ data: items,
271
+ getRowId,
272
+ defaultSelectedKeys: ["a", "b", "c"],
273
+ }),
274
+ );
275
+
276
+ const theadProps = asMultiple(result).getTheadCheckboxProps();
277
+ expect(theadProps.indeterminate).toBe(false);
278
+ expect(theadProps.checked).toBe(true);
279
+ });
280
+
281
+ test("deselecting one row when selectedKeys is 'all'", () => {
282
+ const { result } = renderHook(() =>
283
+ useTableSelection({
284
+ selectionMode: "multiple",
285
+ data: items,
286
+ getRowId,
287
+ defaultSelectedKeys: "all",
288
+ }),
289
+ );
290
+
291
+ act(() => {
292
+ asMultiple(result)
293
+ .getRowCheckboxProps("b")
294
+ .onChange?.({} as React.ChangeEvent<HTMLInputElement>);
295
+ });
296
+
297
+ expect(asMultiple(result).selectedKeys).toEqual(["a", "c"]);
298
+ });
299
+
300
+ test("disabled rows have disabled prop", () => {
301
+ const { result } = renderHook(() =>
302
+ useTableSelection({
303
+ selectionMode: "multiple",
304
+ data: items,
305
+ getRowId,
306
+ disabledKeys: ["b"],
307
+ }),
308
+ );
309
+
310
+ expect(asMultiple(result).getRowCheckboxProps("a").disabled).toBe(false);
311
+ expect(asMultiple(result).getRowCheckboxProps("b").disabled).toBe(true);
312
+ });
313
+
314
+ test("thead checkbox disabled when all rows disabled", () => {
315
+ const { result } = renderHook(() =>
316
+ useTableSelection({
317
+ selectionMode: "multiple",
318
+ data: items,
319
+ getRowId,
320
+ disabledKeys: ["a", "b", "c"],
321
+ }),
322
+ );
323
+
324
+ expect(asMultiple(result).getTheadCheckboxProps().disabled).toBe(true);
325
+ });
326
+ });
327
+ });
@@ -14,7 +14,7 @@ import {
14
14
  getNavigationAction,
15
15
  shouldBlockNavigation,
16
16
  } from "../helpers/table-keyboard";
17
- import { useGridCache } from "../hooks/useGridCache";
17
+ import { useGridCache } from "./useGridCache";
18
18
 
19
19
  type UseTableKeyboardNavOptions = {
20
20
  enabled: boolean;
@@ -0,0 +1,78 @@
1
+ import { useMemo } from "react";
2
+ import { useControllableState } from "../../../utils/hooks";
3
+ import { getMultipleSelectProps } from "../helpers/selection/getMultipleSelectProps";
4
+ import { getSingleSelectProps } from "../helpers/selection/getSingleSelectProps";
5
+ import type {
6
+ SelectionProps,
7
+ SelectionT,
8
+ TableSelection,
9
+ } from "../helpers/selection/selection.types";
10
+
11
+ type UseTableSelectionArgs<T> = SelectionProps & {
12
+ data: T[];
13
+ getRowId: (rowData: T, index: number) => string | number;
14
+ };
15
+
16
+ function useTableSelection<T>({
17
+ selectionMode = "none",
18
+ defaultSelectedKeys,
19
+ selectedKeys: selectedKeysProp,
20
+ onSelectionChange,
21
+ disabledKeys = [],
22
+ data,
23
+ getRowId,
24
+ }: UseTableSelectionArgs<T>): TableSelection {
25
+ const allKeys = useMemo(
26
+ () => data.map((item, index) => getRowId(item, index)),
27
+ [data, getRowId],
28
+ );
29
+
30
+ const [selectedKeys, setSelectedKeys] = useControllableState<SelectionT>({
31
+ value: selectionMode !== "none" ? selectedKeysProp : undefined,
32
+ defaultValue: defaultSelectedKeys ?? [],
33
+ onChange: onSelectionChange,
34
+ });
35
+
36
+ if (selectionMode === "none") {
37
+ return { selectionMode, allKeys, selectedKeys: [], disabledKeys };
38
+ }
39
+
40
+ if (selectionMode === "single") {
41
+ const arrayKeys = Array.isArray(selectedKeys) ? selectedKeys : [];
42
+ const { getRowRadioProps } = getSingleSelectProps({
43
+ selectedKeys: arrayKeys,
44
+ setSelectedKeys,
45
+ disabledKeys,
46
+ });
47
+
48
+ return {
49
+ selectionMode,
50
+ allKeys,
51
+ selectedKeys: arrayKeys,
52
+ disabledKeys,
53
+ getRowRadioProps,
54
+ };
55
+ }
56
+
57
+ const { getTheadCheckboxProps, getRowCheckboxProps } = getMultipleSelectProps(
58
+ {
59
+ selectedKeys,
60
+ setSelectedKeys,
61
+ disabledKeys,
62
+ allKeys,
63
+ totalCount: data.length,
64
+ },
65
+ );
66
+
67
+ return {
68
+ selectionMode,
69
+ allKeys,
70
+ selectedKeys,
71
+ disabledKeys,
72
+ getTheadCheckboxProps,
73
+ getRowCheckboxProps,
74
+ };
75
+ }
76
+
77
+ export { useTableSelection };
78
+ export type { SelectionProps, SelectionT };
@@ -1,7 +1,13 @@
1
+ /** biome-ignore-all lint/correctness/useHookAtTopLevel: False positive because of the way forwardRef() is added */
1
2
  import React, { forwardRef, useState } from "react";
2
3
  import { Checkbox } from "../../../form/checkbox";
3
4
  import { cl } from "../../../utils/helpers";
4
5
  import { useMergeRefs } from "../../../utils/hooks";
6
+ import { useTableKeyboardNav } from "../hooks/useTableKeyboardNav";
7
+ import {
8
+ type SelectionProps,
9
+ useTableSelection,
10
+ } from "../hooks/useTableSelection";
5
11
  import { DataTableTbody } from "../tbody/DataTableTbody";
6
12
  import { DataTableTd } from "../td/DataTableTd";
7
13
  import { DataTableTh } from "../th/DataTableTh";
@@ -9,8 +15,6 @@ import { DataTableThead } from "../thead/DataTableThead";
9
15
  import { DataTableTr } from "../tr/DataTableTr";
10
16
  import type { ColumnDefinitions } from "./DataTable.types";
11
17
  import { DataTableContextProvider } from "./DataTableRoot.context";
12
- import { useTableKeyboardNav } from "./useTableKeyboardNav";
13
- import { type SelectionProps, useTableSelection } from "./useTableSelection";
14
18
 
15
19
  interface DataTableProps<T>
16
20
  extends React.HTMLAttributes<HTMLTableElement>, SelectionProps {
@@ -62,7 +66,8 @@ interface DataTableProps<T>
62
66
  *
63
67
  */
64
68
  columnDefinitions: ColumnDefinitions<T>;
65
- data: (T & { id: string | number })[];
69
+ data: T[];
70
+ getRowId?: (rowData: T, index: number) => string | number;
66
71
  }
67
72
 
68
73
  function DataTableAutoInner<T>(
@@ -81,6 +86,7 @@ function DataTableAutoInner<T>(
81
86
  disabledKeys = [],
82
87
  data,
83
88
  columnDefinitions,
89
+ getRowId,
84
90
  ...rest
85
91
  }: DataTableProps<T>,
86
92
  forwardedRef: React.ForwardedRef<HTMLTableElement>,
@@ -93,15 +99,19 @@ function DataTableAutoInner<T>(
93
99
  shouldBlockNavigation,
94
100
  });
95
101
 
96
- const { getTheadCheckboxProps, selectionMode, getRowCheckboxProps } =
97
- useTableSelection({
98
- selectionMode: selectionModeProp,
99
- selectedKeys,
100
- defaultSelectedKeys,
101
- onSelectionChange,
102
- disabledKeys,
103
- data,
104
- });
102
+ const resolvedGetRowId =
103
+ getRowId ??
104
+ (((_row: T, index: number) => index) as (rowData: T) => string | number);
105
+
106
+ const selection = useTableSelection({
107
+ selectionMode: selectionModeProp,
108
+ selectedKeys,
109
+ defaultSelectedKeys,
110
+ onSelectionChange,
111
+ disabledKeys,
112
+ data,
113
+ getRowId: resolvedGetRowId,
114
+ });
105
115
 
106
116
  return (
107
117
  <DataTableContextProvider layout={layout} withKeyboardNav={withKeyboardNav}>
@@ -119,14 +129,20 @@ function DataTableAutoInner<T>(
119
129
  >
120
130
  <DataTableThead>
121
131
  <DataTableTr>
122
- {getTheadCheckboxProps && (
123
- <DataTableTd align="center" width="60px">
124
- <Checkbox {...getTheadCheckboxProps()} />
125
- </DataTableTd>
132
+ {selection.selectionMode === "multiple" && (
133
+ /* TODO: Overflow/focus is clipped. Alignment is off */
134
+ /* TODO: Should not be resizable */
135
+ <DataTableTh textAlign="center" width="60px">
136
+ <Checkbox {...selection.getTheadCheckboxProps()} />
137
+ </DataTableTh>
138
+ )}
139
+ {selection.selectionMode === "single" && (
140
+ <DataTableTd align="center" width="60px" />
126
141
  )}
127
142
  {columnDefinitions.map((colDef, colDefIndex) => {
128
143
  return (
129
144
  <DataTableTh
145
+ /* TODO: Make these user-changable */
130
146
  maxWidth="400px"
131
147
  minWidth="100px"
132
148
  defaultWidth="100%"
@@ -141,15 +157,22 @@ function DataTableAutoInner<T>(
141
157
  </DataTableThead>
142
158
  <DataTableTbody>
143
159
  {data.map((rowData, rowIndex) => {
160
+ const rowId = selection.allKeys[rowIndex];
144
161
  return (
145
- <DataTableTr
146
- key={
147
- rowIndex /* TODO: Should be more flexible to allow user to define the key? */
148
- }
149
- >
150
- {selectionMode !== "none" && getRowCheckboxProps && (
162
+ <DataTableTr key={rowId}>
163
+ {selection.selectionMode === "multiple" && (
164
+ <DataTableTd align="center" width="60px">
165
+ <Checkbox {...selection.getRowCheckboxProps(rowId)} />
166
+ </DataTableTd>
167
+ )}
168
+ {selection.selectionMode === "single" && (
151
169
  <DataTableTd align="center" width="60px">
152
- <Checkbox {...getRowCheckboxProps(rowData.id)} />
170
+ {/**
171
+ * TODO: This should be a radio, but our current Radio implementation has some issues:
172
+ * - Checked cant be controlled outside of radiogroup
173
+ * - Cant hide label
174
+ * */}
175
+ <Checkbox {...selection.getRowRadioProps(rowId)} />
153
176
  </DataTableTd>
154
177
  )}
155
178
  {columnDefinitions.map((colDef, colDefIndex) => {