@navikt/ds-react 6.4.0 → 6.5.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 (203) hide show
  1. package/cjs/date/datepicker/parts/DayButton.js +1 -1
  2. package/cjs/date/datepicker/parts/DayButton.js.map +1 -1
  3. package/cjs/date/hooks/useDatepicker.js +4 -1
  4. package/cjs/date/hooks/useDatepicker.js.map +1 -1
  5. package/cjs/date/hooks/useMonthPicker.js +4 -1
  6. package/cjs/date/hooks/useMonthPicker.js.map +1 -1
  7. package/cjs/date/monthpicker/MonthButton.js +1 -1
  8. package/cjs/date/monthpicker/MonthButton.js.map +1 -1
  9. package/cjs/tabs/Tabs.context.d.ts +30 -0
  10. package/cjs/tabs/Tabs.context.js +14 -0
  11. package/cjs/tabs/Tabs.context.js.map +1 -0
  12. package/cjs/tabs/Tabs.d.ts +8 -43
  13. package/cjs/tabs/Tabs.js +19 -12
  14. package/cjs/tabs/Tabs.js.map +1 -1
  15. package/cjs/tabs/Tabs.types.d.ts +41 -0
  16. package/cjs/tabs/Tabs.types.js +3 -0
  17. package/cjs/tabs/Tabs.types.js.map +1 -0
  18. package/cjs/tabs/index.d.ts +5 -4
  19. package/cjs/tabs/index.js +6 -6
  20. package/cjs/tabs/index.js.map +1 -1
  21. package/cjs/tabs/parts/tab/Tab.d.ts +25 -0
  22. package/cjs/tabs/{Tab.js → parts/tab/Tab.js} +15 -14
  23. package/cjs/tabs/parts/tab/Tab.js.map +1 -0
  24. package/cjs/tabs/parts/tab/useTab.d.ts +20 -0
  25. package/cjs/tabs/parts/tab/useTab.js +29 -0
  26. package/cjs/tabs/parts/tab/useTab.js.map +1 -0
  27. package/cjs/tabs/parts/tablist/ScrollButtons.d.ts +8 -0
  28. package/cjs/tabs/parts/tablist/ScrollButtons.js +15 -0
  29. package/cjs/tabs/parts/tablist/ScrollButtons.js.map +1 -0
  30. package/{esm/tabs → cjs/tabs/parts/tablist}/TabList.d.ts +1 -1
  31. package/cjs/tabs/parts/tablist/TabList.js +61 -0
  32. package/cjs/tabs/parts/tablist/TabList.js.map +1 -0
  33. package/cjs/tabs/parts/tablist/useScrollButtons.d.ts +12 -0
  34. package/cjs/tabs/parts/tablist/useScrollButtons.js +61 -0
  35. package/cjs/tabs/parts/tablist/useScrollButtons.js.map +1 -0
  36. package/cjs/tabs/parts/tablist/useTabList.d.ts +7 -0
  37. package/cjs/tabs/parts/tablist/useTabList.js +66 -0
  38. package/cjs/tabs/parts/tablist/useTabList.js.map +1 -0
  39. package/cjs/tabs/parts/tabpanel/TabPanel.d.ts +25 -0
  40. package/cjs/tabs/{TabPanel.js → parts/tabpanel/TabPanel.js} +5 -3
  41. package/cjs/tabs/parts/tabpanel/TabPanel.js.map +1 -0
  42. package/cjs/tabs/parts/tabpanel/useTabPanel.d.ts +12 -0
  43. package/cjs/tabs/parts/tabpanel/useTabPanel.js +17 -0
  44. package/cjs/tabs/parts/tabpanel/useTabPanel.js.map +1 -0
  45. package/cjs/tabs/useTabs.d.ts +14 -0
  46. package/cjs/tabs/useTabs.js +43 -0
  47. package/cjs/tabs/useTabs.js.map +1 -0
  48. package/cjs/toggle-group/ToggleGroup.context.d.ts +31 -0
  49. package/cjs/toggle-group/ToggleGroup.context.js +16 -0
  50. package/cjs/toggle-group/ToggleGroup.context.js.map +1 -0
  51. package/cjs/toggle-group/ToggleGroup.d.ts +5 -36
  52. package/cjs/toggle-group/ToggleGroup.js +24 -24
  53. package/cjs/toggle-group/ToggleGroup.js.map +1 -1
  54. package/cjs/toggle-group/ToggleGroup.types.d.ts +38 -0
  55. package/cjs/toggle-group/ToggleGroup.types.js +3 -0
  56. package/cjs/toggle-group/ToggleGroup.types.js.map +1 -0
  57. package/cjs/toggle-group/index.d.ts +3 -2
  58. package/cjs/toggle-group/index.js +1 -1
  59. package/cjs/toggle-group/index.js.map +1 -1
  60. package/cjs/toggle-group/{ToggleItem.d.ts → parts/ToggleItem.d.ts} +2 -2
  61. package/cjs/toggle-group/{ToggleItem.js → parts/ToggleItem.js} +9 -8
  62. package/cjs/toggle-group/parts/ToggleItem.js.map +1 -0
  63. package/cjs/toggle-group/parts/useToggleItem.d.ts +20 -0
  64. package/cjs/toggle-group/parts/useToggleItem.js +76 -0
  65. package/cjs/toggle-group/parts/useToggleItem.js.map +1 -0
  66. package/cjs/toggle-group/useToggleGroup.d.ts +8 -0
  67. package/cjs/toggle-group/useToggleGroup.js +29 -0
  68. package/cjs/toggle-group/useToggleGroup.js.map +1 -0
  69. package/esm/date/datepicker/parts/DayButton.js +1 -1
  70. package/esm/date/datepicker/parts/DayButton.js.map +1 -1
  71. package/esm/date/hooks/useDatepicker.js +4 -1
  72. package/esm/date/hooks/useDatepicker.js.map +1 -1
  73. package/esm/date/hooks/useMonthPicker.js +4 -1
  74. package/esm/date/hooks/useMonthPicker.js.map +1 -1
  75. package/esm/date/monthpicker/MonthButton.js +1 -1
  76. package/esm/date/monthpicker/MonthButton.js.map +1 -1
  77. package/esm/tabs/Tabs.context.d.ts +30 -0
  78. package/esm/tabs/Tabs.context.js +10 -0
  79. package/esm/tabs/Tabs.context.js.map +1 -0
  80. package/esm/tabs/Tabs.d.ts +8 -43
  81. package/esm/tabs/Tabs.js +19 -12
  82. package/esm/tabs/Tabs.js.map +1 -1
  83. package/esm/tabs/Tabs.types.d.ts +41 -0
  84. package/esm/tabs/Tabs.types.js +2 -0
  85. package/esm/tabs/Tabs.types.js.map +1 -0
  86. package/esm/tabs/index.d.ts +5 -4
  87. package/esm/tabs/index.js +3 -3
  88. package/esm/tabs/index.js.map +1 -1
  89. package/esm/tabs/parts/tab/Tab.d.ts +25 -0
  90. package/esm/tabs/parts/tab/Tab.js +35 -0
  91. package/esm/tabs/parts/tab/Tab.js.map +1 -0
  92. package/esm/tabs/parts/tab/useTab.d.ts +20 -0
  93. package/esm/tabs/parts/tab/useTab.js +25 -0
  94. package/esm/tabs/parts/tab/useTab.js.map +1 -0
  95. package/esm/tabs/parts/tablist/ScrollButtons.d.ts +8 -0
  96. package/esm/tabs/parts/tablist/ScrollButtons.js +10 -0
  97. package/esm/tabs/parts/tablist/ScrollButtons.js.map +1 -0
  98. package/{cjs/tabs → esm/tabs/parts/tablist}/TabList.d.ts +1 -1
  99. package/esm/tabs/parts/tablist/TabList.js +32 -0
  100. package/esm/tabs/parts/tablist/TabList.js.map +1 -0
  101. package/esm/tabs/parts/tablist/useScrollButtons.d.ts +12 -0
  102. package/esm/tabs/parts/tablist/useScrollButtons.js +57 -0
  103. package/esm/tabs/parts/tablist/useScrollButtons.js.map +1 -0
  104. package/esm/tabs/parts/tablist/useTabList.d.ts +7 -0
  105. package/esm/tabs/parts/tablist/useTabList.js +62 -0
  106. package/esm/tabs/parts/tablist/useTabList.js.map +1 -0
  107. package/esm/tabs/parts/tabpanel/TabPanel.d.ts +25 -0
  108. package/esm/tabs/parts/tabpanel/TabPanel.js +22 -0
  109. package/esm/tabs/parts/tabpanel/TabPanel.js.map +1 -0
  110. package/esm/tabs/parts/tabpanel/useTabPanel.d.ts +12 -0
  111. package/esm/tabs/parts/tabpanel/useTabPanel.js +13 -0
  112. package/esm/tabs/parts/tabpanel/useTabPanel.js.map +1 -0
  113. package/esm/tabs/useTabs.d.ts +14 -0
  114. package/esm/tabs/useTabs.js +39 -0
  115. package/esm/tabs/useTabs.js.map +1 -0
  116. package/esm/toggle-group/ToggleGroup.context.d.ts +31 -0
  117. package/esm/toggle-group/ToggleGroup.context.js +12 -0
  118. package/esm/toggle-group/ToggleGroup.context.js.map +1 -0
  119. package/esm/toggle-group/ToggleGroup.d.ts +5 -36
  120. package/esm/toggle-group/ToggleGroup.js +24 -24
  121. package/esm/toggle-group/ToggleGroup.js.map +1 -1
  122. package/esm/toggle-group/ToggleGroup.types.d.ts +38 -0
  123. package/esm/toggle-group/ToggleGroup.types.js +2 -0
  124. package/esm/toggle-group/ToggleGroup.types.js.map +1 -0
  125. package/esm/toggle-group/index.d.ts +3 -2
  126. package/esm/toggle-group/index.js +1 -1
  127. package/esm/toggle-group/index.js.map +1 -1
  128. package/esm/toggle-group/{ToggleItem.d.ts → parts/ToggleItem.d.ts} +2 -2
  129. package/esm/toggle-group/parts/ToggleItem.js +25 -0
  130. package/esm/toggle-group/parts/ToggleItem.js.map +1 -0
  131. package/esm/toggle-group/parts/useToggleItem.d.ts +20 -0
  132. package/esm/toggle-group/parts/useToggleItem.js +72 -0
  133. package/esm/toggle-group/parts/useToggleItem.js.map +1 -0
  134. package/esm/toggle-group/useToggleGroup.d.ts +8 -0
  135. package/esm/toggle-group/useToggleGroup.js +25 -0
  136. package/esm/toggle-group/useToggleGroup.js.map +1 -0
  137. package/package.json +3 -5
  138. package/src/date/datepicker/datepicker.stories.tsx +39 -0
  139. package/src/date/datepicker/parts/DayButton.tsx +2 -0
  140. package/src/date/hooks/useDatepicker.tsx +5 -1
  141. package/src/date/hooks/useMonthPicker.tsx +5 -1
  142. package/src/date/monthpicker/MonthButton.tsx +1 -0
  143. package/src/date/monthpicker/monthpicker.stories.tsx +36 -19
  144. package/src/modal/modal.stories.tsx +2 -6
  145. package/src/tabs/Tabs.context.ts +24 -0
  146. package/src/tabs/Tabs.stories.tsx +233 -113
  147. package/src/tabs/Tabs.test.tsx +99 -37
  148. package/src/tabs/Tabs.tsx +48 -70
  149. package/src/tabs/Tabs.types.ts +43 -0
  150. package/src/tabs/index.ts +11 -4
  151. package/src/tabs/parts/tab/Tab.tsx +93 -0
  152. package/src/tabs/parts/tab/useTab.ts +52 -0
  153. package/src/tabs/parts/tablist/ScrollButtons.tsx +29 -0
  154. package/src/tabs/parts/tablist/TabList.tsx +56 -0
  155. package/src/tabs/parts/tablist/useScrollButtons.ts +69 -0
  156. package/src/tabs/parts/tablist/useTabList.ts +68 -0
  157. package/src/tabs/parts/tabpanel/TabPanel.tsx +50 -0
  158. package/src/tabs/parts/tabpanel/useTabPanel.ts +18 -0
  159. package/src/tabs/useTabs.ts +51 -0
  160. package/src/toggle-group/ToggleGroup.context.ts +31 -0
  161. package/src/toggle-group/ToggleGroup.stories.tsx +67 -6
  162. package/src/toggle-group/ToggleGroup.test.tsx +57 -16
  163. package/src/toggle-group/ToggleGroup.tsx +63 -90
  164. package/src/toggle-group/ToggleGroup.types.ts +40 -0
  165. package/src/toggle-group/index.ts +3 -2
  166. package/src/toggle-group/parts/ToggleItem.tsx +55 -0
  167. package/src/toggle-group/parts/useToggleItem.ts +104 -0
  168. package/src/toggle-group/useToggleGroup.ts +33 -0
  169. package/cjs/tabs/Tab.d.ts +0 -18
  170. package/cjs/tabs/Tab.js.map +0 -1
  171. package/cjs/tabs/TabList.js +0 -111
  172. package/cjs/tabs/TabList.js.map +0 -1
  173. package/cjs/tabs/TabPanel.d.ts +0 -13
  174. package/cjs/tabs/TabPanel.js.map +0 -1
  175. package/cjs/tabs/context.d.ts +0 -8
  176. package/cjs/tabs/context.js +0 -6
  177. package/cjs/tabs/context.js.map +0 -1
  178. package/cjs/toggle-group/ToggleItem.js.map +0 -1
  179. package/cjs/toggle-group/context.d.ts +0 -6
  180. package/cjs/toggle-group/context.js +0 -6
  181. package/cjs/toggle-group/context.js.map +0 -1
  182. package/esm/tabs/Tab.d.ts +0 -18
  183. package/esm/tabs/Tab.js +0 -34
  184. package/esm/tabs/Tab.js.map +0 -1
  185. package/esm/tabs/TabList.js +0 -82
  186. package/esm/tabs/TabList.js.map +0 -1
  187. package/esm/tabs/TabPanel.d.ts +0 -13
  188. package/esm/tabs/TabPanel.js +0 -20
  189. package/esm/tabs/TabPanel.js.map +0 -1
  190. package/esm/tabs/context.d.ts +0 -8
  191. package/esm/tabs/context.js +0 -3
  192. package/esm/tabs/context.js.map +0 -1
  193. package/esm/toggle-group/ToggleItem.js +0 -24
  194. package/esm/toggle-group/ToggleItem.js.map +0 -1
  195. package/esm/toggle-group/context.d.ts +0 -6
  196. package/esm/toggle-group/context.js +0 -3
  197. package/esm/toggle-group/context.js.map +0 -1
  198. package/src/tabs/Tab.tsx +0 -66
  199. package/src/tabs/TabList.tsx +0 -128
  200. package/src/tabs/TabPanel.tsx +0 -26
  201. package/src/tabs/context.ts +0 -9
  202. package/src/toggle-group/ToggleItem.tsx +0 -41
  203. package/src/toggle-group/context.ts +0 -9
@@ -0,0 +1,51 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useId } from "../util";
3
+ import { useControllableState } from "../util/hooks/useControllableState";
4
+ import { TabsProps } from "./Tabs.types";
5
+
6
+ export function useTabs({
7
+ onChange,
8
+ value,
9
+ defaultValue = "",
10
+ id,
11
+ }: Pick<TabsProps, "onChange" | "value" | "defaultValue" | "id">) {
12
+ const [focusedValue, setFocusedValue] = useState(defaultValue);
13
+
14
+ const [selectedValue, setSelectedValue] = useControllableState({
15
+ defaultValue,
16
+ value,
17
+ onChange,
18
+ });
19
+
20
+ /**
21
+ * Sync focused `value` with controlled `selectedValue`
22
+ */
23
+ useEffect(() => {
24
+ if (value != null) {
25
+ setFocusedValue(value);
26
+ }
27
+ }, [value]);
28
+
29
+ /**
30
+ * Scope ids for better tracking
31
+ */
32
+ const uuid = useId();
33
+
34
+ return {
35
+ id: `tabs-${id ?? uuid}`,
36
+ selectedValue,
37
+ setSelectedValue,
38
+ focusedValue,
39
+ setFocusedValue,
40
+ makeTabId,
41
+ makeTabPanelId,
42
+ };
43
+ }
44
+
45
+ function makeTabId(id: string, value: string) {
46
+ return `${id}--tab-${value}`;
47
+ }
48
+
49
+ function makeTabPanelId(id: string, value: string) {
50
+ return `${id}--tabpanel-${value}`;
51
+ }
@@ -0,0 +1,31 @@
1
+ import { createContext as ReactCreateContext } from "react";
2
+ import { createContext } from "../util/create-context";
3
+ import { createDescendantContext } from "../util/hooks/descendants/useDescendant";
4
+ import { ToggleGroupProps } from "./ToggleGroup.types";
5
+ import { useToggleGroup } from "./useToggleGroup";
6
+
7
+ interface ToggleContextProps {
8
+ size: "medium" | "small";
9
+ }
10
+
11
+ export const ToggleGroupContext = ReactCreateContext<ToggleContextProps | null>(
12
+ null,
13
+ );
14
+
15
+ export const [
16
+ ToggleGroupDescendantsProvider,
17
+ useToggleGroupDescendantsContext,
18
+ useToggleGroupDescendants,
19
+ useToggleGroupDescendant,
20
+ ] = createDescendantContext<HTMLButtonElement, { value: string }>();
21
+
22
+ type ToggleGroupProviderProps = ReturnType<typeof useToggleGroup> &
23
+ Pick<ToggleGroupProps, "size">;
24
+
25
+ /* State context */
26
+ export const [ToggleGroupProvider, useToggleGroupContext] =
27
+ createContext<ToggleGroupProviderProps>({
28
+ name: "ToggleGroupContext",
29
+ hookName: "useToggleGroupContext",
30
+ providerName: "ToggleGroupProvider",
31
+ });
@@ -5,6 +5,7 @@ import {
5
5
  EnvelopeOpenIcon,
6
6
  InboxUpIcon,
7
7
  } from "@navikt/aksel-icons";
8
+ import { VStack } from "../layout/stack";
8
9
  import ToggleGroup from "./ToggleGroup";
9
10
 
10
11
  const meta: Meta<typeof ToggleGroup> = {
@@ -22,7 +23,11 @@ const meta: Meta<typeof ToggleGroup> = {
22
23
  control: { type: "radio" },
23
24
  },
24
25
  },
26
+ parameters: {
27
+ chromatic: { disable: true },
28
+ },
25
29
  };
30
+
26
31
  export default meta;
27
32
 
28
33
  const Items = (icon?: boolean, both?: boolean) => (
@@ -71,6 +76,7 @@ export const Default = (props) => {
71
76
  </ToggleGroup>
72
77
  );
73
78
  };
79
+
74
80
  Default.args = {
75
81
  icon: true,
76
82
  text: true,
@@ -81,7 +87,7 @@ export const Compositions = () => {
81
87
  const [activeValue, setActiveValue] = useState("ulest");
82
88
 
83
89
  return (
84
- <div className="colgap">
90
+ <VStack gap="6">
85
91
  <ToggleGroup value={activeValue} onChange={setActiveValue}>
86
92
  {Items()}
87
93
  </ToggleGroup>
@@ -91,7 +97,10 @@ export const Compositions = () => {
91
97
  <ToggleGroup value={activeValue} onChange={setActiveValue}>
92
98
  {Items(true)}
93
99
  </ToggleGroup>
94
- </div>
100
+ <ToggleGroup fill value={activeValue} onChange={setActiveValue}>
101
+ {Items(true)}
102
+ </ToggleGroup>
103
+ </VStack>
95
104
  );
96
105
  };
97
106
 
@@ -99,7 +108,7 @@ export const Variants = () => {
99
108
  const [activeValue, setActiveValue] = useState("ulest");
100
109
 
101
110
  return (
102
- <div className="colgap">
111
+ <VStack gap="6">
103
112
  <ToggleGroup
104
113
  variant="action"
105
114
  value={activeValue}
@@ -114,7 +123,7 @@ export const Variants = () => {
114
123
  >
115
124
  {Items(true, true)}
116
125
  </ToggleGroup>
117
- </div>
126
+ </VStack>
118
127
  );
119
128
  };
120
129
 
@@ -122,7 +131,7 @@ export const Small = () => {
122
131
  const [activeValue, setActiveValue] = useState("ulest");
123
132
 
124
133
  return (
125
- <div className="colgap">
134
+ <VStack gap="6">
126
135
  <ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
127
136
  {Items()}
128
137
  </ToggleGroup>
@@ -132,6 +141,58 @@ export const Small = () => {
132
141
  <ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
133
142
  {Items(true)}
134
143
  </ToggleGroup>
135
- </div>
144
+ </VStack>
136
145
  );
137
146
  };
147
+
148
+ export const Chromatic = {
149
+ render: () => (
150
+ <VStack gap="6">
151
+ <div>
152
+ <h2>Text</h2>
153
+ <ToggleGroup value="ulest" onChange={console.log}>
154
+ {Items()}
155
+ </ToggleGroup>
156
+ </div>
157
+ <div>
158
+ <h2>Icon</h2>
159
+ <ToggleGroup value="ulest" onChange={console.log}>
160
+ {Items(true)}
161
+ </ToggleGroup>
162
+ </div>
163
+ <div>
164
+ <h2>Text + icon</h2>
165
+ <ToggleGroup value="ulest" onChange={console.log}>
166
+ {Items(true, true)}
167
+ </ToggleGroup>
168
+ </div>
169
+ <div style={{ minWidth: 600 }}>
170
+ <h2>Fill</h2>
171
+ <ToggleGroup value="ulest" onChange={console.log} fill>
172
+ {Items(true, true)}
173
+ </ToggleGroup>
174
+ </div>
175
+ <div>
176
+ <h2>Small</h2>
177
+ <ToggleGroup value="ulest" onChange={console.log} size="small">
178
+ {Items(true, true)}
179
+ </ToggleGroup>
180
+ </div>
181
+ <div>
182
+ <h2>Small + fill</h2>
183
+ <ToggleGroup value="ulest" onChange={console.log} size="small" fill>
184
+ {Items(true, true)}
185
+ </ToggleGroup>
186
+ </div>
187
+ <div>
188
+ <h2>Neutral</h2>
189
+ <ToggleGroup value="ulest" onChange={console.log} variant="neutral">
190
+ {Items(true, true)}
191
+ </ToggleGroup>
192
+ </div>
193
+ </VStack>
194
+ ),
195
+ parameters: {
196
+ chromatic: { disable: false },
197
+ },
198
+ };
@@ -1,4 +1,5 @@
1
- import { fireEvent, render, screen } from "@testing-library/react";
1
+ import { act, fireEvent, render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
2
3
  import React from "react";
3
4
  import { describe, expect, test } from "vitest";
4
5
  import { ToggleGroup } from "./ToggleGroup";
@@ -20,36 +21,76 @@ const TestToggleGroup = ({ value, onChange, defaultValue }: any) => (
20
21
  describe("ToggleGroup", () => {
21
22
  test("sets default value correctly", () => {
22
23
  render(<TestToggleGroup defaultValue="toggle2" />);
23
- const toggle = screen.getByTestId("toggle2");
24
+ const toggle2 = screen.getByTestId("toggle2");
24
25
 
25
- expect(toggle).toHaveAttribute("aria-checked", "true");
26
+ expect(toggle2).toHaveAttribute("aria-checked", "true");
26
27
  });
27
28
 
28
29
  test("sets correct attributes on active toggle", () => {
29
30
  render(<TestToggleGroup defaultValue="toggle2" />);
30
- const toggle = screen.getByTestId("toggle2");
31
+ const toggle2 = screen.getByTestId("toggle2");
31
32
 
32
- expect(toggle).toHaveAttribute("aria-checked", "true");
33
- expect(toggle).toHaveAttribute("role", "radio");
34
- expect(toggle).toHaveAttribute("type", "button");
35
- expect(toggle).toHaveAttribute("tabindex", "-1");
33
+ expect(toggle2).toHaveAttribute("aria-checked", "true");
34
+ expect(toggle2).toHaveAttribute("role", "radio");
35
+ expect(toggle2).toHaveAttribute("type", "button");
36
+ expect(toggle2).toHaveAttribute("tabindex", "0");
36
37
  });
37
38
 
38
39
  test("sets correct attributes on idle toggle", () => {
39
40
  render(<TestToggleGroup defaultValue="toggle1" />);
40
- const toggle = screen.getByTestId("toggle2");
41
+ const toggle2 = screen.getByTestId("toggle2");
41
42
 
42
- expect(toggle).toHaveAttribute("aria-checked", "false");
43
- expect(toggle).toHaveAttribute("role", "radio");
44
- expect(toggle).toHaveAttribute("type", "button");
45
- expect(toggle).toHaveAttribute("tabindex", "-1");
43
+ expect(toggle2).toHaveAttribute("aria-checked", "false");
44
+ expect(toggle2).toHaveAttribute("role", "radio");
45
+ expect(toggle2).toHaveAttribute("type", "button");
46
+ expect(toggle2).toHaveAttribute("tabindex", "-1");
46
47
  });
47
48
 
48
49
  test("sets tabindex to 0 when focused", () => {
49
50
  render(<TestToggleGroup defaultValue="toggle2" />);
50
- const toggle = screen.getByTestId("toggle2");
51
+ const toggle2 = screen.getByTestId("toggle2");
51
52
 
52
- fireEvent.focus(toggle);
53
- expect(toggle).toHaveAttribute("tabindex", "0");
53
+ fireEvent.focus(toggle2);
54
+ expect(toggle2).toHaveAttribute("tabindex", "0");
55
+ });
56
+
57
+ test("roving tabindex keydown moves focus", () => {
58
+ render(<TestToggleGroup defaultValue="toggle1" />);
59
+ const toggle1 = screen.getByTestId("toggle1");
60
+
61
+ expect(toggle1).toHaveAttribute("tabindex", "0");
62
+ fireEvent.keyDown(toggle1, { key: "ArrowRight" });
63
+
64
+ expect(toggle1).toHaveAttribute("tabindex", "-1");
65
+ expect(screen.getByTestId("toggle2")).toHaveAttribute("tabindex", "0");
66
+ expect(screen.getByTestId("toggle2")).toHaveAttribute(
67
+ "aria-checked",
68
+ "false",
69
+ );
70
+ });
71
+
72
+ test("Space selects focused toggle-item", async () => {
73
+ render(<TestToggleGroup defaultValue="toggle1" />);
74
+ const toggle1 = screen.getByTestId("toggle1");
75
+
76
+ expect(toggle1).toHaveAttribute("tabindex", "0");
77
+ fireEvent.keyDown(toggle1, { key: "ArrowRight" });
78
+
79
+ expect(toggle1).toHaveAttribute("tabindex", "-1");
80
+ expect(screen.getByTestId("toggle2")).toHaveAttribute("tabindex", "0");
81
+ expect(screen.getByTestId("toggle2")).toHaveAttribute(
82
+ "aria-checked",
83
+ "false",
84
+ );
85
+
86
+ // eslint-disable-next-line testing-library/no-unnecessary-act
87
+ await act(async () => {
88
+ await userEvent.keyboard(" ");
89
+ });
90
+
91
+ expect(screen.getByTestId("toggle2")).toHaveAttribute(
92
+ "aria-checked",
93
+ "true",
94
+ );
54
95
  });
55
96
  });
@@ -1,55 +1,24 @@
1
- import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
2
1
  import cl from "clsx";
3
- import React, { HTMLAttributes, forwardRef, useState } from "react";
2
+ import React, { forwardRef } from "react";
4
3
  import { Label } from "../typography";
5
- import { useId } from "../util/hooks";
6
- import ToggleItem, { ToggleGroupItemProps } from "./ToggleItem";
7
- import { ToggleGroupContext } from "./context";
8
-
9
- export interface ToggleGroupProps
10
- extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
11
- /**
12
- * Toggles.Item elements
13
- */
14
- children: React.ReactNode;
15
- /**
16
- * Changes padding and font-size
17
- * @default "medium"
18
- */
19
- size?: "medium" | "small";
20
- /**
21
- * Controlled selected value
22
- */
23
- value?: string;
24
- /**
25
- * If not controlled, a default-value needs to be set
26
- */
27
- defaultValue?: string;
28
- /**
29
- * Callback for selected toggle
30
- */
31
- onChange: (value: string) => void;
32
- /**
33
- * Label describing ToggleGroup
34
- */
35
- label?: React.ReactNode;
36
- /**
37
- * Changes design and interaction-visuals
38
- * @default "action"
39
- */
40
- variant?: "action" | "neutral";
41
- }
4
+ import { useId } from "../util";
5
+ import {
6
+ ToggleGroupDescendantsProvider,
7
+ ToggleGroupProvider,
8
+ useToggleGroupDescendants,
9
+ } from "./ToggleGroup.context";
10
+ import { ToggleGroupProps } from "./ToggleGroup.types";
11
+ import ToggleItem from "./parts/ToggleItem";
12
+ import { useToggleGroup } from "./useToggleGroup";
42
13
 
43
14
  interface ToggleGroupComponent
44
15
  extends React.ForwardRefExoticComponent<
45
16
  ToggleGroupProps & React.RefAttributes<HTMLDivElement>
46
17
  > {
47
18
  /**
48
- * @see 🏷️ {@link ToggleGroupItemProps}
19
+ * @see 🏷️ {@link ToggleItem}
49
20
  */
50
- Item: React.ForwardRefExoticComponent<
51
- ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>
52
- >;
21
+ Item: typeof ToggleItem;
53
22
  }
54
23
 
55
24
  /**
@@ -77,70 +46,74 @@ export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
77
46
  label,
78
47
  value,
79
48
  defaultValue,
80
- "aria-describedby": desc,
49
+ "aria-describedby": userDescribedby,
81
50
  variant = "action",
51
+ fill = false,
82
52
  ...rest
83
53
  },
84
54
  ref,
85
55
  ) => {
86
- const [groupValue, setGroupValue] = useState(defaultValue);
87
- const labelId = useId();
56
+ const descendants = useToggleGroupDescendants();
57
+
58
+ const toggleGroupContext = useToggleGroup({
59
+ defaultValue,
60
+ value,
61
+ onChange,
62
+ });
88
63
 
89
- const handleValueChange = (v: string) => {
90
- if (v !== "") {
91
- setGroupValue(v);
92
- onChange?.(v);
93
- }
64
+ /**
65
+ * ToggleGroupProvider handles memoization.
66
+ */
67
+ const context = {
68
+ ...toggleGroupContext,
69
+ size,
94
70
  };
95
71
 
72
+ const labelId = useId();
73
+
96
74
  if (!value && !defaultValue) {
97
- console.error("ToggleGroup without value/defaultvalue is not allowed");
75
+ console.error("ToggleGroup without value or defaultvalue is not allowed");
98
76
  }
99
77
 
100
- const describeBy = cl({
101
- [desc ?? ""]: !!desc,
102
- [labelId ?? ""]: !!label,
103
- });
104
-
105
78
  if (!value && !defaultValue) {
106
79
  console.error("ToggleGroup needs either a value or defaultValue");
107
80
  }
108
81
 
109
82
  return (
110
- <ToggleGroupContext.Provider
111
- value={{
112
- size,
113
- }}
114
- >
115
- <div className={cl("navds-toggle-group__wrapper", className)}>
116
- {label && (
117
- <Label
118
- size={size}
119
- className="navds-toggle-group__label"
120
- id={labelId}
121
- >
122
- {label}
123
- </Label>
124
- )}
125
- <RadixToggleGroup.Root
126
- {...rest}
127
- onValueChange={handleValueChange}
128
- value={value ?? groupValue}
129
- defaultValue={defaultValue}
130
- ref={ref}
131
- className={cl(
132
- "navds-toggle-group",
133
- `navds-toggle-group--${size}`,
134
- `navds-toggle-group--${variant}`,
135
- )}
136
- {...(describeBy && { "aria-describedby": describeBy })}
137
- role="radiogroup"
138
- type="single"
83
+ <ToggleGroupDescendantsProvider value={descendants}>
84
+ <ToggleGroupProvider {...context}>
85
+ <div
86
+ className={cl("navds-toggle-group__wrapper", className, {
87
+ "navds-toggle-group__wrapper--fill": fill,
88
+ })}
139
89
  >
140
- {children}
141
- </RadixToggleGroup.Root>
142
- </div>
143
- </ToggleGroupContext.Provider>
90
+ {label && (
91
+ <Label
92
+ size={size}
93
+ className="navds-toggle-group__label"
94
+ id={labelId}
95
+ >
96
+ {label}
97
+ </Label>
98
+ )}
99
+ <div
100
+ {...rest}
101
+ ref={ref}
102
+ className={cl(
103
+ "navds-toggle-group",
104
+ `navds-toggle-group--${size}`,
105
+ `navds-toggle-group--${variant}`,
106
+ )}
107
+ aria-describedby={
108
+ cl(userDescribedby, !!label && labelId) || undefined
109
+ }
110
+ role="radiogroup"
111
+ >
112
+ {children}
113
+ </div>
114
+ </div>
115
+ </ToggleGroupProvider>
116
+ </ToggleGroupDescendantsProvider>
144
117
  );
145
118
  },
146
119
  ) as ToggleGroupComponent;
@@ -0,0 +1,40 @@
1
+ import { HTMLAttributes } from "react";
2
+
3
+ export interface ToggleGroupProps
4
+ extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
5
+ /**
6
+ * Toggles.Item elements.
7
+ */
8
+ children: React.ReactNode;
9
+ /**
10
+ * Changes padding and font-size.
11
+ * @default "medium"
12
+ */
13
+ size?: "medium" | "small";
14
+ /**
15
+ * Controlled selected value.
16
+ */
17
+ value?: string;
18
+ /**
19
+ * If not controlled, a default-value needs to be set.
20
+ */
21
+ defaultValue?: string;
22
+ /**
23
+ * Callback for selected toggle.
24
+ */
25
+ onChange: (value: string) => void;
26
+ /**
27
+ * Label describing ToggleGroup.
28
+ */
29
+ label?: React.ReactNode;
30
+ /**
31
+ * Changes design and interaction-visuals.
32
+ * @default "action"
33
+ */
34
+ variant?: "action" | "neutral";
35
+ /**
36
+ * Stretch each button to fill avaliable space in container.
37
+ * @default false
38
+ */
39
+ fill?: boolean;
40
+ }
@@ -1,6 +1,7 @@
1
1
  "use client";
2
- export { default as ToggleGroup, type ToggleGroupProps } from "./ToggleGroup";
2
+ export { default as ToggleGroup } from "./ToggleGroup";
3
+ export { type ToggleGroupProps } from "./ToggleGroup.types";
3
4
  export {
4
5
  default as ToggleGroupItem,
5
6
  type ToggleGroupItemProps,
6
- } from "./ToggleItem";
7
+ } from "./parts/ToggleItem";
@@ -0,0 +1,55 @@
1
+ import cl from "clsx";
2
+ import React, { forwardRef } from "react";
3
+ import { BodyShort } from "../../typography/BodyShort";
4
+ import { useToggleGroupContext } from "../ToggleGroup.context";
5
+ import { useToggleItem } from "./useToggleItem";
6
+
7
+ export interface ToggleGroupItemProps
8
+ extends React.HTMLAttributes<HTMLButtonElement> {
9
+ /**
10
+ * Content.
11
+ */
12
+ children: React.ReactNode;
13
+ /**
14
+ * Value for state-handling.
15
+ */
16
+ value: string;
17
+ }
18
+
19
+ const ToggleItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
20
+ (
21
+ { className, children, value, onClick, onFocus, onKeyDown, ...rest },
22
+ forwardedRef,
23
+ ) => {
24
+ const itemCtx = useToggleItem(
25
+ { value, onClick, onFocus, disabled: false, onKeyDown },
26
+ forwardedRef,
27
+ );
28
+ const ctx = useToggleGroupContext();
29
+
30
+ return (
31
+ <button
32
+ {...rest}
33
+ ref={itemCtx.ref}
34
+ className={cl("navds-toggle-group__button", className)}
35
+ type="button"
36
+ role="radio"
37
+ aria-checked={itemCtx.isSelected}
38
+ tabIndex={itemCtx.isFocused ? 0 : -1}
39
+ onClick={itemCtx.onClick}
40
+ onFocus={itemCtx.onFocus}
41
+ onKeyDown={itemCtx.onKeyDown}
42
+ >
43
+ <BodyShort
44
+ as="span"
45
+ className="navds-toggle-group__button-inner"
46
+ size={ctx?.size}
47
+ >
48
+ {children}
49
+ </BodyShort>
50
+ </button>
51
+ );
52
+ },
53
+ );
54
+
55
+ export default ToggleItem;