@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.
- package/cjs/date/datepicker/parts/DayButton.js +1 -1
- package/cjs/date/datepicker/parts/DayButton.js.map +1 -1
- package/cjs/date/hooks/useDatepicker.js +4 -1
- package/cjs/date/hooks/useDatepicker.js.map +1 -1
- package/cjs/date/hooks/useMonthPicker.js +4 -1
- package/cjs/date/hooks/useMonthPicker.js.map +1 -1
- package/cjs/date/monthpicker/MonthButton.js +1 -1
- package/cjs/date/monthpicker/MonthButton.js.map +1 -1
- package/cjs/tabs/Tabs.context.d.ts +30 -0
- package/cjs/tabs/Tabs.context.js +14 -0
- package/cjs/tabs/Tabs.context.js.map +1 -0
- package/cjs/tabs/Tabs.d.ts +8 -43
- package/cjs/tabs/Tabs.js +19 -12
- package/cjs/tabs/Tabs.js.map +1 -1
- package/cjs/tabs/Tabs.types.d.ts +41 -0
- package/cjs/tabs/Tabs.types.js +3 -0
- package/cjs/tabs/Tabs.types.js.map +1 -0
- package/cjs/tabs/index.d.ts +5 -4
- package/cjs/tabs/index.js +6 -6
- package/cjs/tabs/index.js.map +1 -1
- package/cjs/tabs/parts/tab/Tab.d.ts +25 -0
- package/cjs/tabs/{Tab.js → parts/tab/Tab.js} +15 -14
- package/cjs/tabs/parts/tab/Tab.js.map +1 -0
- package/cjs/tabs/parts/tab/useTab.d.ts +20 -0
- package/cjs/tabs/parts/tab/useTab.js +29 -0
- package/cjs/tabs/parts/tab/useTab.js.map +1 -0
- package/cjs/tabs/parts/tablist/ScrollButtons.d.ts +8 -0
- package/cjs/tabs/parts/tablist/ScrollButtons.js +15 -0
- package/cjs/tabs/parts/tablist/ScrollButtons.js.map +1 -0
- package/{esm/tabs → cjs/tabs/parts/tablist}/TabList.d.ts +1 -1
- package/cjs/tabs/parts/tablist/TabList.js +61 -0
- package/cjs/tabs/parts/tablist/TabList.js.map +1 -0
- package/cjs/tabs/parts/tablist/useScrollButtons.d.ts +12 -0
- package/cjs/tabs/parts/tablist/useScrollButtons.js +61 -0
- package/cjs/tabs/parts/tablist/useScrollButtons.js.map +1 -0
- package/cjs/tabs/parts/tablist/useTabList.d.ts +7 -0
- package/cjs/tabs/parts/tablist/useTabList.js +66 -0
- package/cjs/tabs/parts/tablist/useTabList.js.map +1 -0
- package/cjs/tabs/parts/tabpanel/TabPanel.d.ts +25 -0
- package/cjs/tabs/{TabPanel.js → parts/tabpanel/TabPanel.js} +5 -3
- package/cjs/tabs/parts/tabpanel/TabPanel.js.map +1 -0
- package/cjs/tabs/parts/tabpanel/useTabPanel.d.ts +12 -0
- package/cjs/tabs/parts/tabpanel/useTabPanel.js +17 -0
- package/cjs/tabs/parts/tabpanel/useTabPanel.js.map +1 -0
- package/cjs/tabs/useTabs.d.ts +14 -0
- package/cjs/tabs/useTabs.js +43 -0
- package/cjs/tabs/useTabs.js.map +1 -0
- package/cjs/toggle-group/ToggleGroup.context.d.ts +31 -0
- package/cjs/toggle-group/ToggleGroup.context.js +16 -0
- package/cjs/toggle-group/ToggleGroup.context.js.map +1 -0
- package/cjs/toggle-group/ToggleGroup.d.ts +5 -36
- package/cjs/toggle-group/ToggleGroup.js +24 -24
- package/cjs/toggle-group/ToggleGroup.js.map +1 -1
- package/cjs/toggle-group/ToggleGroup.types.d.ts +38 -0
- package/cjs/toggle-group/ToggleGroup.types.js +3 -0
- package/cjs/toggle-group/ToggleGroup.types.js.map +1 -0
- package/cjs/toggle-group/index.d.ts +3 -2
- package/cjs/toggle-group/index.js +1 -1
- package/cjs/toggle-group/index.js.map +1 -1
- package/cjs/toggle-group/{ToggleItem.d.ts → parts/ToggleItem.d.ts} +2 -2
- package/cjs/toggle-group/{ToggleItem.js → parts/ToggleItem.js} +9 -8
- package/cjs/toggle-group/parts/ToggleItem.js.map +1 -0
- package/cjs/toggle-group/parts/useToggleItem.d.ts +20 -0
- package/cjs/toggle-group/parts/useToggleItem.js +76 -0
- package/cjs/toggle-group/parts/useToggleItem.js.map +1 -0
- package/cjs/toggle-group/useToggleGroup.d.ts +8 -0
- package/cjs/toggle-group/useToggleGroup.js +29 -0
- package/cjs/toggle-group/useToggleGroup.js.map +1 -0
- package/esm/date/datepicker/parts/DayButton.js +1 -1
- package/esm/date/datepicker/parts/DayButton.js.map +1 -1
- package/esm/date/hooks/useDatepicker.js +4 -1
- package/esm/date/hooks/useDatepicker.js.map +1 -1
- package/esm/date/hooks/useMonthPicker.js +4 -1
- package/esm/date/hooks/useMonthPicker.js.map +1 -1
- package/esm/date/monthpicker/MonthButton.js +1 -1
- package/esm/date/monthpicker/MonthButton.js.map +1 -1
- package/esm/tabs/Tabs.context.d.ts +30 -0
- package/esm/tabs/Tabs.context.js +10 -0
- package/esm/tabs/Tabs.context.js.map +1 -0
- package/esm/tabs/Tabs.d.ts +8 -43
- package/esm/tabs/Tabs.js +19 -12
- package/esm/tabs/Tabs.js.map +1 -1
- package/esm/tabs/Tabs.types.d.ts +41 -0
- package/esm/tabs/Tabs.types.js +2 -0
- package/esm/tabs/Tabs.types.js.map +1 -0
- package/esm/tabs/index.d.ts +5 -4
- package/esm/tabs/index.js +3 -3
- package/esm/tabs/index.js.map +1 -1
- package/esm/tabs/parts/tab/Tab.d.ts +25 -0
- package/esm/tabs/parts/tab/Tab.js +35 -0
- package/esm/tabs/parts/tab/Tab.js.map +1 -0
- package/esm/tabs/parts/tab/useTab.d.ts +20 -0
- package/esm/tabs/parts/tab/useTab.js +25 -0
- package/esm/tabs/parts/tab/useTab.js.map +1 -0
- package/esm/tabs/parts/tablist/ScrollButtons.d.ts +8 -0
- package/esm/tabs/parts/tablist/ScrollButtons.js +10 -0
- package/esm/tabs/parts/tablist/ScrollButtons.js.map +1 -0
- package/{cjs/tabs → esm/tabs/parts/tablist}/TabList.d.ts +1 -1
- package/esm/tabs/parts/tablist/TabList.js +32 -0
- package/esm/tabs/parts/tablist/TabList.js.map +1 -0
- package/esm/tabs/parts/tablist/useScrollButtons.d.ts +12 -0
- package/esm/tabs/parts/tablist/useScrollButtons.js +57 -0
- package/esm/tabs/parts/tablist/useScrollButtons.js.map +1 -0
- package/esm/tabs/parts/tablist/useTabList.d.ts +7 -0
- package/esm/tabs/parts/tablist/useTabList.js +62 -0
- package/esm/tabs/parts/tablist/useTabList.js.map +1 -0
- package/esm/tabs/parts/tabpanel/TabPanel.d.ts +25 -0
- package/esm/tabs/parts/tabpanel/TabPanel.js +22 -0
- package/esm/tabs/parts/tabpanel/TabPanel.js.map +1 -0
- package/esm/tabs/parts/tabpanel/useTabPanel.d.ts +12 -0
- package/esm/tabs/parts/tabpanel/useTabPanel.js +13 -0
- package/esm/tabs/parts/tabpanel/useTabPanel.js.map +1 -0
- package/esm/tabs/useTabs.d.ts +14 -0
- package/esm/tabs/useTabs.js +39 -0
- package/esm/tabs/useTabs.js.map +1 -0
- package/esm/toggle-group/ToggleGroup.context.d.ts +31 -0
- package/esm/toggle-group/ToggleGroup.context.js +12 -0
- package/esm/toggle-group/ToggleGroup.context.js.map +1 -0
- package/esm/toggle-group/ToggleGroup.d.ts +5 -36
- package/esm/toggle-group/ToggleGroup.js +24 -24
- package/esm/toggle-group/ToggleGroup.js.map +1 -1
- package/esm/toggle-group/ToggleGroup.types.d.ts +38 -0
- package/esm/toggle-group/ToggleGroup.types.js +2 -0
- package/esm/toggle-group/ToggleGroup.types.js.map +1 -0
- package/esm/toggle-group/index.d.ts +3 -2
- package/esm/toggle-group/index.js +1 -1
- package/esm/toggle-group/index.js.map +1 -1
- package/esm/toggle-group/{ToggleItem.d.ts → parts/ToggleItem.d.ts} +2 -2
- package/esm/toggle-group/parts/ToggleItem.js +25 -0
- package/esm/toggle-group/parts/ToggleItem.js.map +1 -0
- package/esm/toggle-group/parts/useToggleItem.d.ts +20 -0
- package/esm/toggle-group/parts/useToggleItem.js +72 -0
- package/esm/toggle-group/parts/useToggleItem.js.map +1 -0
- package/esm/toggle-group/useToggleGroup.d.ts +8 -0
- package/esm/toggle-group/useToggleGroup.js +25 -0
- package/esm/toggle-group/useToggleGroup.js.map +1 -0
- package/package.json +3 -5
- package/src/date/datepicker/datepicker.stories.tsx +39 -0
- package/src/date/datepicker/parts/DayButton.tsx +2 -0
- package/src/date/hooks/useDatepicker.tsx +5 -1
- package/src/date/hooks/useMonthPicker.tsx +5 -1
- package/src/date/monthpicker/MonthButton.tsx +1 -0
- package/src/date/monthpicker/monthpicker.stories.tsx +36 -19
- package/src/modal/modal.stories.tsx +2 -6
- package/src/tabs/Tabs.context.ts +24 -0
- package/src/tabs/Tabs.stories.tsx +233 -113
- package/src/tabs/Tabs.test.tsx +99 -37
- package/src/tabs/Tabs.tsx +48 -70
- package/src/tabs/Tabs.types.ts +43 -0
- package/src/tabs/index.ts +11 -4
- package/src/tabs/parts/tab/Tab.tsx +93 -0
- package/src/tabs/parts/tab/useTab.ts +52 -0
- package/src/tabs/parts/tablist/ScrollButtons.tsx +29 -0
- package/src/tabs/parts/tablist/TabList.tsx +56 -0
- package/src/tabs/parts/tablist/useScrollButtons.ts +69 -0
- package/src/tabs/parts/tablist/useTabList.ts +68 -0
- package/src/tabs/parts/tabpanel/TabPanel.tsx +50 -0
- package/src/tabs/parts/tabpanel/useTabPanel.ts +18 -0
- package/src/tabs/useTabs.ts +51 -0
- package/src/toggle-group/ToggleGroup.context.ts +31 -0
- package/src/toggle-group/ToggleGroup.stories.tsx +67 -6
- package/src/toggle-group/ToggleGroup.test.tsx +57 -16
- package/src/toggle-group/ToggleGroup.tsx +63 -90
- package/src/toggle-group/ToggleGroup.types.ts +40 -0
- package/src/toggle-group/index.ts +3 -2
- package/src/toggle-group/parts/ToggleItem.tsx +55 -0
- package/src/toggle-group/parts/useToggleItem.ts +104 -0
- package/src/toggle-group/useToggleGroup.ts +33 -0
- package/cjs/tabs/Tab.d.ts +0 -18
- package/cjs/tabs/Tab.js.map +0 -1
- package/cjs/tabs/TabList.js +0 -111
- package/cjs/tabs/TabList.js.map +0 -1
- package/cjs/tabs/TabPanel.d.ts +0 -13
- package/cjs/tabs/TabPanel.js.map +0 -1
- package/cjs/tabs/context.d.ts +0 -8
- package/cjs/tabs/context.js +0 -6
- package/cjs/tabs/context.js.map +0 -1
- package/cjs/toggle-group/ToggleItem.js.map +0 -1
- package/cjs/toggle-group/context.d.ts +0 -6
- package/cjs/toggle-group/context.js +0 -6
- package/cjs/toggle-group/context.js.map +0 -1
- package/esm/tabs/Tab.d.ts +0 -18
- package/esm/tabs/Tab.js +0 -34
- package/esm/tabs/Tab.js.map +0 -1
- package/esm/tabs/TabList.js +0 -82
- package/esm/tabs/TabList.js.map +0 -1
- package/esm/tabs/TabPanel.d.ts +0 -13
- package/esm/tabs/TabPanel.js +0 -20
- package/esm/tabs/TabPanel.js.map +0 -1
- package/esm/tabs/context.d.ts +0 -8
- package/esm/tabs/context.js +0 -3
- package/esm/tabs/context.js.map +0 -1
- package/esm/toggle-group/ToggleItem.js +0 -24
- package/esm/toggle-group/ToggleItem.js.map +0 -1
- package/esm/toggle-group/context.d.ts +0 -6
- package/esm/toggle-group/context.js +0 -3
- package/esm/toggle-group/context.js.map +0 -1
- package/src/tabs/Tab.tsx +0 -66
- package/src/tabs/TabList.tsx +0 -128
- package/src/tabs/TabPanel.tsx +0 -26
- package/src/tabs/context.ts +0 -9
- package/src/toggle-group/ToggleItem.tsx +0 -41
- 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, {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
22
|
+
Tab: typeof Tab;
|
|
56
23
|
/**
|
|
57
24
|
* @see 🏷️ {@link TabListProps}
|
|
58
25
|
*/
|
|
59
|
-
List:
|
|
60
|
-
TabListProps & React.RefAttributes<HTMLDivElement>
|
|
61
|
-
>;
|
|
26
|
+
List: typeof TabList;
|
|
62
27
|
/**
|
|
63
28
|
* @see 🏷️ {@link TabPanelProps}
|
|
64
29
|
*/
|
|
65
|
-
Panel:
|
|
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 =
|
|
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
|
-
<
|
|
112
|
-
{...
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
3
|
-
export {
|
|
4
|
-
export { default as
|
|
5
|
-
export {
|
|
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
|
+
}
|