@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
package/src/tabs/Tabs.tsx CHANGED
@@ -1,48 +1,15 @@
1
- import * as RadixTabs from "@radix-ui/react-tabs";
2
1
  import cl from "clsx";
3
- import React, { HTMLAttributes, forwardRef } from "react";
4
- import { OverridableComponent } from "../util/types";
5
- import Tab, { TabProps } from "./Tab";
6
- import TabList, { TabListProps } from "./TabList";
7
- import TabPanel, { TabPanelProps } from "./TabPanel";
8
- import { TabsContext } from "./context";
9
-
10
- export interface TabsProps
11
- extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
12
- children: React.ReactNode;
13
- /**
14
- * Changes padding and font-size.
15
- * @default "medium"
16
- */
17
- size?: "medium" | "small";
18
- /**
19
- * onChange callback for selected Tab.
20
- */
21
- onChange?: (value: string) => void;
22
- /**
23
- * Controlled selected value.
24
- */
25
- value?: string;
26
- /**
27
- * If not controlled, a default-value needs to be set.
28
- */
29
- defaultValue?: string;
30
- /**
31
- * Automatically activates tab on focus/navigation.
32
- * @default false
33
- */
34
- selectionFollowsFocus?: boolean;
35
- /**
36
- * Loops back to start when navigating past last item.
37
- * @default false
38
- */
39
- loop?: boolean;
40
- /**
41
- * Icon position in Tab.
42
- * @default "left"
43
- */
44
- iconPosition?: "left" | "top";
45
- }
2
+ import React, { forwardRef } from "react";
3
+ import {
4
+ TabsDescendantsProvider,
5
+ TabsProvider,
6
+ useTabsDescendants,
7
+ } from "./Tabs.context";
8
+ import { TabsProps } from "./Tabs.types";
9
+ import Tab from "./parts/tab/Tab";
10
+ import TabList from "./parts/tablist/TabList";
11
+ import TabPanel from "./parts/tabpanel/TabPanel";
12
+ import { useTabs } from "./useTabs";
46
13
 
47
14
  interface TabsComponent
48
15
  extends React.ForwardRefExoticComponent<
@@ -52,19 +19,15 @@ interface TabsComponent
52
19
  * @see 🏷️ {@link TabProps}
53
20
  * @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
54
21
  */
55
- Tab: OverridableComponent<TabProps, HTMLButtonElement>;
22
+ Tab: typeof Tab;
56
23
  /**
57
24
  * @see 🏷️ {@link TabListProps}
58
25
  */
59
- List: React.ForwardRefExoticComponent<
60
- TabListProps & React.RefAttributes<HTMLDivElement>
61
- >;
26
+ List: typeof TabList;
62
27
  /**
63
28
  * @see 🏷️ {@link TabPanelProps}
64
29
  */
65
- Panel: React.ForwardRefExoticComponent<
66
- TabPanelProps & React.RefAttributes<HTMLDivElement>
67
- >;
30
+ Panel: typeof TabPanel;
68
31
  }
69
32
 
70
33
  /**
@@ -98,33 +61,48 @@ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
98
61
  {
99
62
  className,
100
63
  children,
101
- onChange,
102
64
  size = "medium",
65
+ defaultValue = "",
66
+ value,
67
+ onChange,
68
+ id,
103
69
  selectionFollowsFocus = false,
104
- loop = false,
70
+ loop = true,
105
71
  iconPosition = "left",
72
+ fill = false,
106
73
  ...rest
107
74
  },
108
75
  ref,
109
76
  ) => {
77
+ const descendants = useTabsDescendants();
78
+
79
+ const tabsContext = useTabs({ defaultValue, value, onChange, id });
80
+
81
+ /**
82
+ * TabsProvider handles memoization of context values, so we can safely skip it here.
83
+ */
84
+ const context = {
85
+ ...tabsContext,
86
+ selectionFollowsFocus,
87
+ loop,
88
+ size,
89
+ iconPosition,
90
+ fill,
91
+ };
92
+
110
93
  return (
111
- <RadixTabs.Root
112
- {...rest}
113
- ref={ref}
114
- className={cl("navds-tabs", className, `navds-tabs--${size}`)}
115
- activationMode={selectionFollowsFocus ? "automatic" : "manual"}
116
- onValueChange={onChange}
117
- >
118
- <TabsContext.Provider
119
- value={{
120
- size,
121
- loop,
122
- iconPosition,
123
- }}
124
- >
125
- {children}
126
- </TabsContext.Provider>
127
- </RadixTabs.Root>
94
+ <TabsDescendantsProvider value={descendants}>
95
+ <TabsProvider {...context}>
96
+ <div
97
+ ref={ref}
98
+ {...rest}
99
+ id={id}
100
+ className={cl("navds-tabs", className, `navds-tabs--${size}`)}
101
+ >
102
+ {children}
103
+ </div>
104
+ </TabsProvider>
105
+ </TabsDescendantsProvider>
128
106
  );
129
107
  },
130
108
  ) as TabsComponent;
@@ -0,0 +1,43 @@
1
+ import { HTMLAttributes } from "react";
2
+
3
+ export interface TabsProps
4
+ extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
5
+ children: React.ReactNode;
6
+ /**
7
+ * Changes padding and font-size.
8
+ * @default "medium"
9
+ */
10
+ size?: "medium" | "small";
11
+ /**
12
+ * onChange callback for selected Tab.
13
+ */
14
+ onChange?: (value: string) => void;
15
+ /**
16
+ * Controlled selected value.
17
+ */
18
+ value?: string;
19
+ /**
20
+ * If not controlled, a default-value needs to be set.
21
+ */
22
+ defaultValue?: string;
23
+ /**
24
+ * Automatically activates tab on focus/navigation.
25
+ * @default false
26
+ */
27
+ selectionFollowsFocus?: boolean;
28
+ /**
29
+ * Loops back to start when navigating past last item.
30
+ * @default true
31
+ */
32
+ loop?: boolean;
33
+ /**
34
+ * Icon position in Tab.
35
+ * @default "left"
36
+ */
37
+ iconPosition?: "left" | "top";
38
+ /**
39
+ * Stretches each tab to fill avaliable space in container.
40
+ * @default false
41
+ */
42
+ fill?: boolean;
43
+ }
package/src/tabs/index.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  "use client";
2
- export { default as TabsTab, type TabProps } from "./Tab";
3
- export { default as TabsList, type TabListProps } from "./TabList";
4
- export { default as TabsPanel, type TabPanelProps } from "./TabPanel";
5
- export { default as Tabs, type TabsProps } from "./Tabs";
2
+ export { default as Tabs } from "./Tabs";
3
+ export { type TabsProps } from "./Tabs.types";
4
+ export { default as TabsTab, type TabProps } from "./parts/tab/Tab";
5
+ export {
6
+ default as TabsList,
7
+ type TabListProps,
8
+ } from "./parts/tablist/TabList";
9
+ export {
10
+ default as TabsPanel,
11
+ type TabPanelProps,
12
+ } from "./parts/tabpanel/TabPanel";
@@ -0,0 +1,93 @@
1
+ import cl from "clsx";
2
+ import React, { forwardRef } from "react";
3
+ import { BodyShort } from "../../../typography";
4
+ import { OverridableComponent } from "../../../util";
5
+ import { useTabsContext } from "../../Tabs.context";
6
+ import { useTab } from "./useTab";
7
+
8
+ export interface TabProps
9
+ extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
10
+ /**
11
+ * Tab label.
12
+ */
13
+ label?: React.ReactNode;
14
+ /**
15
+ * Tab Icon.
16
+ */
17
+ icon?: React.ReactNode;
18
+ /**
19
+ * Value for state-handling.
20
+ */
21
+ value: string;
22
+ /**
23
+ * Overrides auto-generated id.
24
+ *
25
+ * **Warning**: Tab generates an id if not provided. If you need to override it,
26
+ * make sure to also include the correct `aria-controls` id for the TabPanel it controls.
27
+ */
28
+ id?: string;
29
+ }
30
+
31
+ export const Tab: OverridableComponent<TabProps, HTMLButtonElement> =
32
+ forwardRef(
33
+ (
34
+ {
35
+ className,
36
+ as: Component = "button",
37
+ label,
38
+ icon,
39
+ value,
40
+ onClick,
41
+ onFocus,
42
+ disabled,
43
+ id,
44
+ ...rest
45
+ },
46
+ ref: React.ForwardedRef<HTMLButtonElement>,
47
+ ) => {
48
+ const tabCtx = useTab({ value, onClick, onFocus, disabled }, ref);
49
+ const ctx = useTabsContext();
50
+
51
+ if (!label && !icon) {
52
+ console.error("<Tabs.Tab/> needs label and/or icon");
53
+ return null;
54
+ }
55
+
56
+ return (
57
+ <Component
58
+ ref={tabCtx.ref}
59
+ {...rest}
60
+ className={cl(
61
+ "navds-tabs__tab",
62
+ `navds-tabs__tab--${ctx?.size ?? "medium"}`,
63
+ `navds-tabs__tab-icon--${ctx?.iconPosition}`,
64
+ className,
65
+ {
66
+ "navds-tabs__tab--icon-only": icon && !label,
67
+ "navds-tabs__tab--fill": ctx.fill,
68
+ },
69
+ )}
70
+ role="tab"
71
+ type="button"
72
+ aria-selected={tabCtx.isSelected}
73
+ data-state={tabCtx.isSelected ? "active" : "inactive"}
74
+ tabIndex={tabCtx.isFocused ? 0 : -1}
75
+ aria-controls={rest["aria-controls"] ?? tabCtx.controlsId}
76
+ id={id ?? tabCtx.id}
77
+ onFocus={tabCtx.onFocus}
78
+ onClick={tabCtx.onClick}
79
+ >
80
+ <BodyShort
81
+ as="span"
82
+ className="navds-tabs__tab-inner"
83
+ size={ctx?.size}
84
+ >
85
+ <span aria-hidden={!!label}>{icon}</span>
86
+ <span>{label}</span>
87
+ </BodyShort>
88
+ </Component>
89
+ );
90
+ },
91
+ );
92
+
93
+ export default Tab;
@@ -0,0 +1,52 @@
1
+ import { composeEventHandlers } from "../../../util/composeEventHandlers";
2
+ import { mergeRefs } from "../../../util/hooks/useMergeRefs";
3
+ import { useTabsContext, useTabsDescendant } from "../../Tabs.context";
4
+
5
+ export interface UseTabProps {
6
+ /**
7
+ * If `true`, the `Tab` won't be toggleable
8
+ * @default false
9
+ */
10
+ disabled?: boolean;
11
+ onClick?: React.MouseEventHandler;
12
+ onFocus?: React.FocusEventHandler;
13
+ value: string;
14
+ }
15
+
16
+ export function useTab<P extends UseTabProps>(
17
+ { value, disabled = false, onFocus: _onFocus, onClick }: P,
18
+ ref: React.ForwardedRef<HTMLButtonElement>,
19
+ ) {
20
+ const {
21
+ id,
22
+ setSelectedValue,
23
+ selectionFollowsFocus,
24
+ focusedValue,
25
+ setFocusedValue,
26
+ selectedValue,
27
+ makeTabId,
28
+ makeTabPanelId,
29
+ } = useTabsContext();
30
+
31
+ const { register } = useTabsDescendant({
32
+ disabled,
33
+ value,
34
+ });
35
+
36
+ const isSelected = value === selectedValue;
37
+
38
+ const onFocus = () => {
39
+ setFocusedValue(value);
40
+ selectionFollowsFocus && setSelectedValue(value);
41
+ };
42
+
43
+ return {
44
+ ref: mergeRefs([register, ref]),
45
+ isSelected,
46
+ isFocused: focusedValue === value,
47
+ id: makeTabId(id, value),
48
+ controlsId: makeTabPanelId(id, value),
49
+ onClick: composeEventHandlers(onClick, () => setSelectedValue(value)),
50
+ onFocus: disabled ? undefined : composeEventHandlers(_onFocus, onFocus),
51
+ };
52
+ }
@@ -0,0 +1,29 @@
1
+ import cl from "clsx";
2
+ import React from "react";
3
+ import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
4
+
5
+ interface ScrollButtonProps {
6
+ hidden: boolean;
7
+ onClick: () => void;
8
+ dir: "left" | "right";
9
+ }
10
+
11
+ function ScrollButton({ hidden, onClick, dir }: ScrollButtonProps) {
12
+ return (
13
+ <div
14
+ className={cl("navds-tabs__scroll-button", {
15
+ "navds-tabs__scroll-button--hidden": hidden,
16
+ })}
17
+ onClick={onClick}
18
+ aria-hidden
19
+ >
20
+ {dir === "left" ? (
21
+ <ChevronLeftIcon title="scroll tilbake" />
22
+ ) : (
23
+ <ChevronRightIcon title="scroll neste" />
24
+ )}
25
+ </div>
26
+ );
27
+ }
28
+
29
+ export default ScrollButton;
@@ -0,0 +1,56 @@
1
+ /* eslint-disable jsx-a11y/interactive-supports-focus */
2
+ import cl from "clsx";
3
+ import React, { forwardRef, useRef } from "react";
4
+ import { composeEventHandlers } from "../../../util/composeEventHandlers";
5
+ import { useMergeRefs } from "../../../util/hooks/useMergeRefs";
6
+ import ScrollButton from "./ScrollButtons";
7
+ import { useScrollButtons } from "./useScrollButtons";
8
+ import { useTabList } from "./useTabList";
9
+
10
+ export interface TabListProps extends React.HTMLAttributes<HTMLDivElement> {
11
+ /**
12
+ * <Tabs.Tab /> elements.
13
+ */
14
+ children: React.ReactNode;
15
+ }
16
+
17
+ export const TabList = forwardRef<HTMLDivElement, TabListProps>(
18
+ ({ className, onKeyDown, ...rest }, ref) => {
19
+ const { onKeyDown: _onKeyDown } = useTabList();
20
+
21
+ const listRef = useRef<HTMLDivElement>(null);
22
+ const mergedRef = useMergeRefs(listRef, ref);
23
+
24
+ const scrollCtx = useScrollButtons(listRef);
25
+
26
+ return (
27
+ <div className="navds-tabs__tablist-wrapper">
28
+ {scrollCtx.show && (
29
+ <ScrollButton
30
+ dir="left"
31
+ hidden={!scrollCtx.start}
32
+ onClick={scrollCtx.scrollLeft}
33
+ />
34
+ )}
35
+ <div
36
+ ref={mergedRef}
37
+ {...rest}
38
+ onScroll={scrollCtx.update}
39
+ className={cl("navds-tabs__tablist", className)}
40
+ role="tablist"
41
+ aria-orientation="horizontal"
42
+ onKeyDown={composeEventHandlers(onKeyDown, _onKeyDown)}
43
+ />
44
+ {scrollCtx.show && (
45
+ <ScrollButton
46
+ dir="right"
47
+ hidden={!scrollCtx.end}
48
+ onClick={scrollCtx.scrollRight}
49
+ />
50
+ )}
51
+ </div>
52
+ );
53
+ },
54
+ );
55
+
56
+ export default TabList;
@@ -0,0 +1,69 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { debounce } from "../../../util";
3
+
4
+ export function useScrollButtons(listRef: React.RefObject<HTMLDivElement>) {
5
+ const [displayScroll, setDisplayScroll] = useState({
6
+ start: false,
7
+ end: false,
8
+ });
9
+
10
+ const updateScrollButtonState = useMemo(
11
+ () =>
12
+ debounce(() => {
13
+ if (!listRef?.current) return;
14
+ const { scrollWidth, clientWidth } = listRef.current;
15
+ const scrollLeft = listRef.current.scrollLeft;
16
+ // use 1 for the potential rounding error with browser zooms.
17
+ const showStartScroll = scrollLeft > 1;
18
+ const showEndScroll = scrollLeft < scrollWidth - clientWidth - 1;
19
+
20
+ setDisplayScroll((oldDisplayScroll) =>
21
+ showStartScroll === oldDisplayScroll.start &&
22
+ showEndScroll === oldDisplayScroll.end
23
+ ? oldDisplayScroll
24
+ : { start: showStartScroll, end: showEndScroll },
25
+ );
26
+ }),
27
+ [listRef],
28
+ );
29
+
30
+ useEffect(() => {
31
+ const handleResize = () => updateScrollButtonState();
32
+ const win = listRef.current?.ownerDocument ?? document ?? window;
33
+ win.addEventListener("resize", handleResize);
34
+
35
+ let resizeObserver;
36
+
37
+ if (typeof ResizeObserver !== "undefined") {
38
+ resizeObserver = new ResizeObserver(handleResize);
39
+ resizeObserver.observe(listRef.current);
40
+ }
41
+
42
+ return () => {
43
+ win.removeEventListener("resize", handleResize);
44
+ resizeObserver && resizeObserver.disconnect();
45
+ updateScrollButtonState.clear();
46
+ };
47
+ }, [listRef, updateScrollButtonState]);
48
+
49
+ useEffect(() => {
50
+ updateScrollButtonState();
51
+ });
52
+
53
+ return {
54
+ update: updateScrollButtonState,
55
+ start: displayScroll.start,
56
+ end: displayScroll.end,
57
+ show: displayScroll.end || displayScroll.start,
58
+ scrollLeft: () => {
59
+ if (listRef.current) {
60
+ listRef.current.scrollLeft -= 100;
61
+ }
62
+ },
63
+ scrollRight: () => {
64
+ if (listRef.current) {
65
+ listRef.current.scrollLeft += 100;
66
+ }
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,68 @@
1
+ import { useCallback } from "react";
2
+ import { useTabsContext, useTabsDescendantsContext } from "../../Tabs.context";
3
+
4
+ /**
5
+ * TabList hook to manage multiple tab buttons,
6
+ * and ensures only one tab is selected at a time.
7
+ */
8
+ export function useTabList() {
9
+ const { focusedValue, loop, selectedValue, setFocusedValue } =
10
+ useTabsContext();
11
+
12
+ const descendants = useTabsDescendantsContext();
13
+
14
+ /**
15
+ * Implements rowing-tabindex for horizontal tabs
16
+ */
17
+ const onKeyDown = useCallback(
18
+ (event: React.KeyboardEvent) => {
19
+ /**
20
+ * Tabs.Tab is registered with its prop 'value'.
21
+ * We can then use it to find the current focuses descendant
22
+ */
23
+ const idx = descendants
24
+ .values()
25
+ .findIndex((x) => x.value === focusedValue);
26
+
27
+ const nextTab = () => {
28
+ const next = descendants.nextEnabled(idx, loop);
29
+ next && next.node?.focus();
30
+ };
31
+ const prevTab = () => {
32
+ const prev = descendants.prevEnabled(idx, loop);
33
+ prev && prev.node?.focus();
34
+ };
35
+ const firstTab = () => {
36
+ const first = descendants.firstEnabled();
37
+ first && first.node?.focus();
38
+ };
39
+ const lastTab = () => {
40
+ const last = descendants.lastEnabled();
41
+ last && last.node?.focus();
42
+ };
43
+
44
+ const keyMap: Record<string, React.KeyboardEventHandler> = {
45
+ ArrowLeft: prevTab,
46
+ ArrowRight: nextTab,
47
+ Home: firstTab,
48
+ End: lastTab,
49
+ };
50
+
51
+ const action = keyMap[event.key];
52
+
53
+ if (action) {
54
+ event.preventDefault();
55
+ action(event);
56
+ } else if (event.key === "Tab") {
57
+ /**
58
+ * Imperative focus during keydown is risky so we prevent React's batching updates
59
+ * to avoid potential bugs. See: https://github.com/facebook/react/issues/20332
60
+ */
61
+ selectedValue && setTimeout(() => setFocusedValue(selectedValue));
62
+ }
63
+ },
64
+ [descendants, focusedValue, loop, selectedValue, setFocusedValue],
65
+ );
66
+
67
+ return { onKeyDown };
68
+ }
@@ -0,0 +1,50 @@
1
+ import cl from "clsx";
2
+ import React, { forwardRef } from "react";
3
+ import { useTabPanel } from "./useTabPanel";
4
+
5
+ export interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ /**
7
+ * Tab panel content.
8
+ */
9
+ children: React.ReactNode;
10
+ /**
11
+ * Value for state-handling.
12
+ */
13
+ value: string;
14
+ /**
15
+ * If true, will only render children when selected.
16
+ * @default true
17
+ */
18
+ lazy?: boolean;
19
+ /**
20
+ * Overrides auto-generated id.
21
+ *
22
+ * **Warning**: TabPanel generates an id if not provided. If you need to override it,
23
+ * make sure to also include the correct `aria-labelledby` id for the Tab that labels it.
24
+ */
25
+ id?: string;
26
+ }
27
+
28
+ const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(
29
+ ({ className, value, children, lazy = true, id, ...rest }, ref) => {
30
+ const ctx = useTabPanel({ value });
31
+
32
+ return (
33
+ <div
34
+ ref={ref}
35
+ {...rest}
36
+ className={cl("navds-tabs__tabpanel", className)}
37
+ role="tabpanel"
38
+ tabIndex={0}
39
+ aria-labelledby={rest["aria-labelledby"] ?? ctx.labelledbyId}
40
+ id={id ?? ctx.id}
41
+ hidden={ctx.hidden}
42
+ data-state={!ctx.hidden ? "active" : "inactive"}
43
+ >
44
+ {lazy && ctx.hidden ? null : children}
45
+ </div>
46
+ );
47
+ },
48
+ );
49
+
50
+ export default TabPanel;
@@ -0,0 +1,18 @@
1
+ import { useTabsContext } from "../../Tabs.context";
2
+
3
+ type TabPanelProps = {
4
+ value: string;
5
+ };
6
+
7
+ /**
8
+ * Tabs hook for managing the visible/hidden state of Tabs.Panel
9
+ */
10
+ export function useTabPanel({ value }: TabPanelProps) {
11
+ const { id, selectedValue, makeTabId, makeTabPanelId } = useTabsContext();
12
+
13
+ return {
14
+ labelledbyId: makeTabId(id, value),
15
+ hidden: selectedValue !== value,
16
+ id: makeTabPanelId(id, value),
17
+ };
18
+ }