@khanacademy/wonder-blocks-tabs 0.4.2 → 0.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/CHANGELOG.md +30 -0
- package/dist/components/navigation-tabs-dropdown.d.ts +119 -0
- package/dist/components/navigation-tabs.d.ts +58 -1
- package/dist/components/responsive-navigation-tabs.d.ts +138 -0
- package/dist/components/responsive-tabs.d.ts +110 -0
- package/dist/components/tab.d.ts +4 -0
- package/dist/components/tabs-dropdown.d.ts +94 -0
- package/dist/components/tabs.d.ts +13 -19
- package/dist/components/types.d.ts +18 -0
- package/dist/es/index.js +27 -10
- package/dist/hooks/use-responsive-layout.d.ts +50 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +31 -7
- package/package.json +8 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-tabs
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 27a30ad: Adds TabsDropdown component
|
|
8
|
+
- a633e23: Adds ResponsiveNavigationTabs component
|
|
9
|
+
- be1c83f: ResponsiveTabs and TabsDropdown: Add support for tab item aria-label
|
|
10
|
+
- cae9768: Adds ResponsiveTabs component
|
|
11
|
+
- 61d61a9: Adds support for icons in ResponsiveTabs, Tabs, and TabsDropdown
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- e0c42a6: Adds internal NavigationTabsDropdown component for the upcoming ResponsiveNavigationTabs component
|
|
16
|
+
- 6928835: Update component docs for ResponsiveTabs and ResponsiveNavigationTabs
|
|
17
|
+
- 75b5418: TabsDropdown: Improve a11y
|
|
18
|
+
- Updated dependencies [a633e23]
|
|
19
|
+
- Updated dependencies [be1c83f]
|
|
20
|
+
- Updated dependencies [61d61a9]
|
|
21
|
+
- @khanacademy/wonder-blocks-link@10.1.4
|
|
22
|
+
- @khanacademy/wonder-blocks-dropdown@10.7.0
|
|
23
|
+
- @khanacademy/wonder-blocks-button@11.3.3
|
|
24
|
+
|
|
25
|
+
## 0.4.3
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [8bb7ada]
|
|
30
|
+
- @khanacademy/wonder-blocks-tokens@15.0.0
|
|
31
|
+
- @khanacademy/wonder-blocks-typography@4.2.29
|
|
32
|
+
|
|
3
33
|
## 0.4.2
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
type NavigationTabDropdownItem = {
|
|
4
|
+
/**
|
|
5
|
+
* The label of the navigation tab
|
|
6
|
+
*/
|
|
7
|
+
label: string;
|
|
8
|
+
/**
|
|
9
|
+
* The id of the navigation tab
|
|
10
|
+
*/
|
|
11
|
+
id: string;
|
|
12
|
+
/**
|
|
13
|
+
* The URL to navigate to
|
|
14
|
+
*/
|
|
15
|
+
href: string;
|
|
16
|
+
/**
|
|
17
|
+
* Optional test ID for e2e testing of the menu item.
|
|
18
|
+
*/
|
|
19
|
+
testId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional aria-label for the navigation tab.
|
|
22
|
+
*/
|
|
23
|
+
"aria-label"?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Optional icon to display in the navigation tab. Should be a PhosphorIcon or Icon component.
|
|
26
|
+
*/
|
|
27
|
+
icon?: React.ReactElement;
|
|
28
|
+
};
|
|
29
|
+
export type NavigationTabsDropdownProps = {
|
|
30
|
+
/**
|
|
31
|
+
* The navigation tabs to render in the dropdown.
|
|
32
|
+
*/
|
|
33
|
+
tabs: Array<NavigationTabDropdownItem>;
|
|
34
|
+
/**
|
|
35
|
+
* The id of the tab that is selected (current page).
|
|
36
|
+
*
|
|
37
|
+
* If the selectedTabId is not valid, the `labels.defaultOpenerLabel` will
|
|
38
|
+
* be used to label the dropdown opener.
|
|
39
|
+
*/
|
|
40
|
+
selectedTabId: string;
|
|
41
|
+
/**
|
|
42
|
+
* Called when a navigation tab is selected.
|
|
43
|
+
*/
|
|
44
|
+
onTabSelected?: (id: string) => unknown;
|
|
45
|
+
/**
|
|
46
|
+
* A unique id for the component. If not provided, a unique base id will be
|
|
47
|
+
* generated automatically.
|
|
48
|
+
*
|
|
49
|
+
* Here is how the id is used for the different elements in the component:
|
|
50
|
+
* - The root will have an id of `${id}`
|
|
51
|
+
* - The opener will have an id of `${id}-opener`
|
|
52
|
+
*/
|
|
53
|
+
id?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Optional test ID for e2e testing.
|
|
56
|
+
*
|
|
57
|
+
* Here is how the testId is used for the different elements in the component:
|
|
58
|
+
* - The root will have a testId of `${testId}`
|
|
59
|
+
* - The opener will have a testId of `${testId}-opener`
|
|
60
|
+
*/
|
|
61
|
+
testId?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Accessible label for the navigation element.
|
|
64
|
+
*
|
|
65
|
+
* It is important to provide a unique aria-label if there are multiple
|
|
66
|
+
* navigation elements on the page.
|
|
67
|
+
*
|
|
68
|
+
* If there is a visual label for the navigation tabs already, use
|
|
69
|
+
* `aria-labelledby` instead.
|
|
70
|
+
*/
|
|
71
|
+
"aria-label"?: string;
|
|
72
|
+
/**
|
|
73
|
+
* If there is a visual label for the navigation tabs already, set
|
|
74
|
+
* `aria-labelledby` to the `id` of the element that labels the navigation
|
|
75
|
+
* tabs.
|
|
76
|
+
*/
|
|
77
|
+
"aria-labelledby"?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Labels for the dropdown.
|
|
80
|
+
*/
|
|
81
|
+
labels?: {
|
|
82
|
+
/**
|
|
83
|
+
* The label used for the opener when there is no selected tab. Defaults
|
|
84
|
+
* to an untranslated "Tabs" string.
|
|
85
|
+
*/
|
|
86
|
+
defaultOpenerLabel?: string;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Can be used to override the opened state for the dropdown.
|
|
90
|
+
*/
|
|
91
|
+
opened?: boolean;
|
|
92
|
+
/**
|
|
93
|
+
* The HTML tag to use for the root element. Defaults to "nav".
|
|
94
|
+
*/
|
|
95
|
+
tag?: keyof JSX.IntrinsicElements;
|
|
96
|
+
/**
|
|
97
|
+
* Custom styles for the elements in NavigationTabsDropdown.
|
|
98
|
+
* - `root`: Styles the root element.
|
|
99
|
+
* - `actionMenu`: Styles the ActionMenu.
|
|
100
|
+
* - `opener`: Styles the opener button.
|
|
101
|
+
*/
|
|
102
|
+
styles?: {
|
|
103
|
+
root?: StyleType;
|
|
104
|
+
actionMenu?: StyleType;
|
|
105
|
+
opener?: StyleType;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Whether to show a divider under the tabs. Defaults to `false`.
|
|
109
|
+
*/
|
|
110
|
+
showDivider?: boolean;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* The NavigationTabsDropdown component is used to represent navigation tabs
|
|
114
|
+
* in an ActionMenu when there is not enough horizontal space to render the
|
|
115
|
+
* tabs as a horizontal layout. Unlike TabsDropdown, this component uses links
|
|
116
|
+
* for navigation instead of managing tab panels.
|
|
117
|
+
*/
|
|
118
|
+
export declare const NavigationTabsDropdown: React.ForwardRefExoticComponent<NavigationTabsDropdownProps & React.RefAttributes<HTMLElement>>;
|
|
119
|
+
export {};
|
|
@@ -1,5 +1,58 @@
|
|
|
1
|
-
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
1
|
+
import { AriaProps, StyleType } from "@khanacademy/wonder-blocks-core";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
export type NavigationTabsProps = AriaProps & {
|
|
4
|
+
/**
|
|
5
|
+
* The NavigationTabItem components to render.
|
|
6
|
+
*/
|
|
7
|
+
children: React.ReactElement | Array<React.ReactElement>;
|
|
8
|
+
/**
|
|
9
|
+
* An id for the navigation element.
|
|
10
|
+
*/
|
|
11
|
+
id?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional test ID for e2e testing.
|
|
14
|
+
*/
|
|
15
|
+
testId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Accessible label for the navigation element.
|
|
18
|
+
*
|
|
19
|
+
* It is important to provide a unique aria-label if there are multiple
|
|
20
|
+
* navigation elements on the page.
|
|
21
|
+
*
|
|
22
|
+
* If there is a visual label for the navigation tabs already, use
|
|
23
|
+
* `aria-labelledby` instead.
|
|
24
|
+
*/
|
|
25
|
+
"aria-label"?: string;
|
|
26
|
+
/**
|
|
27
|
+
* If there is a visual label for the navigation tabs already, set
|
|
28
|
+
* `aria-labelledby` to the `id` of the element that labels the navigation
|
|
29
|
+
* tabs.
|
|
30
|
+
*/
|
|
31
|
+
"aria-labelledby"?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Custom styles for the elements in NavigationTabs.
|
|
34
|
+
* - `root`: Styles the root `nav` element.
|
|
35
|
+
* - `list`: Styles the underlying `ul` element that wraps the
|
|
36
|
+
* `NavigationTabItem` components
|
|
37
|
+
*/
|
|
38
|
+
styles?: {
|
|
39
|
+
root?: StyleType;
|
|
40
|
+
list?: StyleType;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Whether to include animation in the `NavigationTabs`. This should be false
|
|
44
|
+
* if the user has `prefers-reduced-motion` opted in. Defaults to `false`.
|
|
45
|
+
*/
|
|
46
|
+
animated?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The HTML tag to render. Defaults to `nav`.
|
|
49
|
+
*/
|
|
50
|
+
tag?: keyof JSX.IntrinsicElements;
|
|
51
|
+
/**
|
|
52
|
+
* Whether to show a divider under the tabs. Defaults to `false`.
|
|
53
|
+
*/
|
|
54
|
+
showDivider?: boolean;
|
|
55
|
+
};
|
|
3
56
|
/**
|
|
4
57
|
* The `NavigationTabs` component is a tabbed interface for link navigation.
|
|
5
58
|
* The tabs are links and keyboard users can change tabs using tab.
|
|
@@ -73,4 +126,8 @@ export declare const NavigationTabs: React.ForwardRefExoticComponent<Readonly<im
|
|
|
73
126
|
* The HTML tag to render. Defaults to `nav`.
|
|
74
127
|
*/
|
|
75
128
|
tag?: keyof JSX.IntrinsicElements;
|
|
129
|
+
/**
|
|
130
|
+
* Whether to show a divider under the tabs. Defaults to `false`.
|
|
131
|
+
*/
|
|
132
|
+
showDivider?: boolean;
|
|
76
133
|
} & React.RefAttributes<HTMLElement>>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import { type NavigationTabsDropdownProps } from "./navigation-tabs-dropdown";
|
|
4
|
+
import { type NavigationTabsProps } from "./navigation-tabs";
|
|
5
|
+
export type ResponsiveNavigationTabItem = {
|
|
6
|
+
/**
|
|
7
|
+
* The label of the navigation tab.
|
|
8
|
+
*/
|
|
9
|
+
label: string;
|
|
10
|
+
/**
|
|
11
|
+
* The id of the navigation tab.
|
|
12
|
+
*/
|
|
13
|
+
id: string;
|
|
14
|
+
/**
|
|
15
|
+
* The URL to navigate to.
|
|
16
|
+
*/
|
|
17
|
+
href: string;
|
|
18
|
+
/**
|
|
19
|
+
* Optional icon to display in the navigation tab. Should be a PhosphorIcon
|
|
20
|
+
* or Icon component.
|
|
21
|
+
*/
|
|
22
|
+
icon?: React.ReactElement;
|
|
23
|
+
/**
|
|
24
|
+
* Optional aria-label for the navigation tab. This will be used as the
|
|
25
|
+
* accessible label for the tab link or menu item.
|
|
26
|
+
*/
|
|
27
|
+
"aria-label"?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional test ID for e2e testing of the tab link or menu item.
|
|
30
|
+
*/
|
|
31
|
+
testId?: string;
|
|
32
|
+
};
|
|
33
|
+
type Props = {
|
|
34
|
+
/**
|
|
35
|
+
* A unique id for the component.
|
|
36
|
+
*
|
|
37
|
+
* Here is how the id is used for the different elements in the component:
|
|
38
|
+
* - The root will have an id of `${id}`
|
|
39
|
+
*
|
|
40
|
+
* To set the id of the navigation tabs or dropdown, set the `id` prop in
|
|
41
|
+
* the props: `tabsProps` or `dropdownProps`.
|
|
42
|
+
*/
|
|
43
|
+
id?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional test ID for e2e testing.
|
|
46
|
+
*
|
|
47
|
+
* Here is how the test id is used for the different elements in the component:
|
|
48
|
+
* - The root will have a testId of `${testId}`
|
|
49
|
+
*
|
|
50
|
+
* To set the test id of the navigation tabs or dropdown, set the `testId`
|
|
51
|
+
* prop in the props: `tabsProps` or `dropdownProps`.
|
|
52
|
+
*/
|
|
53
|
+
testId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The navigation tabs to render.
|
|
56
|
+
*/
|
|
57
|
+
tabs: ResponsiveNavigationTabItem[];
|
|
58
|
+
/**
|
|
59
|
+
* The id of the tab that is selected (current page).
|
|
60
|
+
*/
|
|
61
|
+
selectedTabId: string;
|
|
62
|
+
/**
|
|
63
|
+
* Called when a navigation tab is selected.
|
|
64
|
+
*/
|
|
65
|
+
onTabSelected?: (id: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Called when the layout changes between NavigationTabs and
|
|
68
|
+
* NavigationTabsDropdown.
|
|
69
|
+
*/
|
|
70
|
+
onLayoutChange?: (layout: "tabs" | "dropdown") => void;
|
|
71
|
+
/**
|
|
72
|
+
* Additional props to pass to the NavigationTabs component when it is used.
|
|
73
|
+
*
|
|
74
|
+
* Note: This prop doesn't include the props that are available on the
|
|
75
|
+
* ResponsiveNavigationTabs component already.
|
|
76
|
+
*/
|
|
77
|
+
tabsProps?: Omit<NavigationTabsProps, "children" | "aria-label" | "aria-labelledby" | "tag">;
|
|
78
|
+
/**
|
|
79
|
+
* Additional props to pass to the NavigationTabsDropdown component when it
|
|
80
|
+
* is used.
|
|
81
|
+
*
|
|
82
|
+
* Note: This prop doesn't include the props that are available on the
|
|
83
|
+
* ResponsiveNavigationTabs component already.
|
|
84
|
+
*/
|
|
85
|
+
dropdownProps?: Omit<NavigationTabsDropdownProps, "tabs" | "selectedTabId" | "onTabSelected" | "aria-label" | "aria-labelledby" | "tag">;
|
|
86
|
+
/**
|
|
87
|
+
* Custom styles for the ResponsiveNavigationTabs component.
|
|
88
|
+
* - `root`: Styles the root container element.
|
|
89
|
+
*
|
|
90
|
+
* To customize the styles of the navigation tabs or dropdown, set the
|
|
91
|
+
* `styles` prop on the `tabsProps` or `dropdownProps` props. See the
|
|
92
|
+
* `NavigationTabs` and `NavigationTabsDropdown` docs for more details.
|
|
93
|
+
*/
|
|
94
|
+
styles?: {
|
|
95
|
+
root?: StyleType;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Accessible label for the navigation element.
|
|
99
|
+
*
|
|
100
|
+
* It is important to provide a unique aria-label if there are multiple
|
|
101
|
+
* navigation elements on the page.
|
|
102
|
+
*
|
|
103
|
+
* If there is a visual label for the navigation tabs already, use
|
|
104
|
+
* `aria-labelledby` instead.
|
|
105
|
+
*/
|
|
106
|
+
"aria-label"?: string;
|
|
107
|
+
/**
|
|
108
|
+
* If there is a visual label for the navigation tabs already, set
|
|
109
|
+
* `aria-labelledby` to the `id` of the element that labels the navigation
|
|
110
|
+
* tabs.
|
|
111
|
+
*/
|
|
112
|
+
"aria-labelledby"?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The HTML tag to use. Defaults to `nav` in both layouts.
|
|
115
|
+
*/
|
|
116
|
+
tag?: keyof JSX.IntrinsicElements;
|
|
117
|
+
/**
|
|
118
|
+
* Whether to show a divider under the tabs. Defaults to `false`.
|
|
119
|
+
*/
|
|
120
|
+
showDivider?: boolean;
|
|
121
|
+
};
|
|
122
|
+
/**
|
|
123
|
+
* Renders NavigationTabs when there is enough horizontal space to display the
|
|
124
|
+
* tabs. When there is not enough space, it renders NavigationTabsDropdown. If
|
|
125
|
+
* the tabs are not links, use ResponsiveTabs instead, which implements different
|
|
126
|
+
* semantics and keyboard interactions.
|
|
127
|
+
*
|
|
128
|
+
* Prefer using ResponsiveNavigationTabs instead of NavigationTabs. For cases
|
|
129
|
+
* where the tabs should always be in a horizontal layout, use the NavigationTabs
|
|
130
|
+
* component directly.
|
|
131
|
+
*
|
|
132
|
+
* Note: This component switches layouts depending on factors like the container
|
|
133
|
+
* width, the number of tabs, the length of tab labels, zoom level, etc. Once the
|
|
134
|
+
* horizontal NavigationTabs need to start scrolling horizontally, the component
|
|
135
|
+
* will switch to the dropdown.
|
|
136
|
+
*/
|
|
137
|
+
export declare const ResponsiveNavigationTabs: (props: Props) => React.JSX.Element;
|
|
138
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import { TabsProps } from "./tabs";
|
|
4
|
+
import { TabsDropdownProps } from "./tabs-dropdown";
|
|
5
|
+
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
6
|
+
export type ResponsiveTabItem = {
|
|
7
|
+
/**
|
|
8
|
+
* The label of the tab.
|
|
9
|
+
*/
|
|
10
|
+
label: string;
|
|
11
|
+
/**
|
|
12
|
+
* The id of the tab.
|
|
13
|
+
*/
|
|
14
|
+
id: string;
|
|
15
|
+
/**
|
|
16
|
+
* The panel of the tab.
|
|
17
|
+
*/
|
|
18
|
+
panel: React.ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Optional test ID for e2e testing of the tab.
|
|
21
|
+
*/
|
|
22
|
+
testId?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Optional aria-label for the tab.
|
|
25
|
+
*/
|
|
26
|
+
"aria-label"?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Optional icon to display in the tab. Should be a PhosphorIcon or Icon
|
|
29
|
+
* component.
|
|
30
|
+
*/
|
|
31
|
+
icon?: React.ReactElement;
|
|
32
|
+
};
|
|
33
|
+
type Props = AriaLabelOrAriaLabelledby & {
|
|
34
|
+
/**
|
|
35
|
+
* A unique id for the component.
|
|
36
|
+
*
|
|
37
|
+
* Here is how the id is used for the different elements in the component:
|
|
38
|
+
* - The root will have an id of `${id}`
|
|
39
|
+
*
|
|
40
|
+
* To set the id of the tabs or dropdown, set the `id` prop in the props:
|
|
41
|
+
* `tabsProps` or `dropdownProps`.
|
|
42
|
+
*/
|
|
43
|
+
id?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Optional test ID for e2e testing.
|
|
46
|
+
*
|
|
47
|
+
* Here is how the test id is used for the different elements in the component:
|
|
48
|
+
* - The root will have a testId of `${testId}`
|
|
49
|
+
*
|
|
50
|
+
* To set the test id of the tabs or dropdown, set the `testId` prop in the props:
|
|
51
|
+
* `tabsProps` or `dropdownProps`.
|
|
52
|
+
*/
|
|
53
|
+
testId?: string;
|
|
54
|
+
/**
|
|
55
|
+
* The tabs to render.
|
|
56
|
+
*/
|
|
57
|
+
tabs: ResponsiveTabItem[];
|
|
58
|
+
/**
|
|
59
|
+
* The id of the tab that is selected.
|
|
60
|
+
*/
|
|
61
|
+
selectedTabId: string;
|
|
62
|
+
/**
|
|
63
|
+
* Called when a tab is selected.
|
|
64
|
+
*/
|
|
65
|
+
onTabSelected: (id: string) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Called when the layout changes.
|
|
68
|
+
*/
|
|
69
|
+
onLayoutChange?: (layout: "tabs" | "dropdown") => void;
|
|
70
|
+
/**
|
|
71
|
+
* Additional props to pass to the Tabs component when it is used.
|
|
72
|
+
*
|
|
73
|
+
* Note: This prop doesn't include the props that are available on the
|
|
74
|
+
* ResponsiveTabs component already.
|
|
75
|
+
*/
|
|
76
|
+
tabsProps?: Omit<TabsProps, "tabs" | "selectedTabId" | "onTabSelected" | "aria-label" | "aria-labelledby">;
|
|
77
|
+
/**
|
|
78
|
+
* Additional props to pass to the TabsDropdown component when it is used.
|
|
79
|
+
*
|
|
80
|
+
* Note: This prop doesn't include the props that are available on the
|
|
81
|
+
* ResponsiveTabs component already.
|
|
82
|
+
*/
|
|
83
|
+
dropdownProps?: Omit<TabsDropdownProps, "tabs" | "selectedTabId" | "onTabSelected" | "aria-label" | "aria-labelledby">;
|
|
84
|
+
/**
|
|
85
|
+
* Custom styles for the ResponsiveTabs component.
|
|
86
|
+
* - `root`: Styles the root `div` element.
|
|
87
|
+
*
|
|
88
|
+
* To customize the styles of the tabs or dropdown, set the `styles` prop on
|
|
89
|
+
* the `tabsProps` or `dropdownProps` props. See the `Tabs` and `TabsDropdown`
|
|
90
|
+
* docs for more details.
|
|
91
|
+
*/
|
|
92
|
+
styles?: {
|
|
93
|
+
root?: StyleType;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Renders the Tabs component when there is enough space to display the tabs as
|
|
98
|
+
* a horizontal layout. When there is not enough space, it renders the
|
|
99
|
+
* tabs as a dropdown. If the tabs are links, use ResponsiveNavigationTabs instead.
|
|
100
|
+
*
|
|
101
|
+
* Prefer using ResponsiveTabs instead of Tabs. For cases where the tabs should
|
|
102
|
+
* always be in a horizontal layout, use the Tabs component directly.
|
|
103
|
+
*
|
|
104
|
+
* Note: This component switches layouts depending on factors like the container
|
|
105
|
+
* width, the number of tabs, the length of tab labels, zoom level, etc. Once the
|
|
106
|
+
* horizontal Tabs need to start scrolling horizontally, the component will
|
|
107
|
+
* switch to the dropdown.
|
|
108
|
+
*/
|
|
109
|
+
export declare const ResponsiveTabs: (props: Props) => React.JSX.Element;
|
|
110
|
+
export {};
|
package/dist/components/tab.d.ts
CHANGED
|
@@ -39,5 +39,9 @@ export declare const Tab: React.ForwardRefExoticComponent<Readonly<import("@khan
|
|
|
39
39
|
* Custom styles for the `Tab` component.
|
|
40
40
|
*/
|
|
41
41
|
style?: StyleType;
|
|
42
|
+
/**
|
|
43
|
+
* Optional icon to display before the tab label.
|
|
44
|
+
*/
|
|
45
|
+
icon?: React.ReactElement;
|
|
42
46
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
43
47
|
export declare const styles: import("aphrodite").StyleDeclaration;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
4
|
+
type TabDropdownItem = {
|
|
5
|
+
/**
|
|
6
|
+
* The label of the tab
|
|
7
|
+
*/
|
|
8
|
+
label: string;
|
|
9
|
+
/**
|
|
10
|
+
* The id of the tab
|
|
11
|
+
*/
|
|
12
|
+
id: string;
|
|
13
|
+
/**
|
|
14
|
+
* The contents for the tab
|
|
15
|
+
*/
|
|
16
|
+
panel: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Optional test ID for e2e testing of the menu item.
|
|
19
|
+
*/
|
|
20
|
+
testId?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional aria-label for the tab.
|
|
23
|
+
*/
|
|
24
|
+
"aria-label"?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional icon to display in the tab. Should be a PhosphorIcon or Icon component.
|
|
27
|
+
*/
|
|
28
|
+
icon?: React.ReactElement;
|
|
29
|
+
};
|
|
30
|
+
export type TabsDropdownProps = AriaLabelOrAriaLabelledby & {
|
|
31
|
+
/**
|
|
32
|
+
* A unique id for the component. If not provided, a unique base id will be
|
|
33
|
+
* generated automatically.
|
|
34
|
+
*
|
|
35
|
+
* Here is how the id is used for the different elements in the component:
|
|
36
|
+
* - The root will have an id of `${id}`
|
|
37
|
+
* - The opener will have an id of `${id}-opener`
|
|
38
|
+
* - The panel will have an id of `${id}-panel`
|
|
39
|
+
*/
|
|
40
|
+
id?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional test ID for e2e testing.
|
|
43
|
+
*
|
|
44
|
+
* Here is how the testId is used for the different elements in the component:
|
|
45
|
+
* - The root will have a testId of `${testId}`
|
|
46
|
+
* - The opener will have a testId of `${testId}-opener`
|
|
47
|
+
* - The panel will have a testId of `${testId}-panel`
|
|
48
|
+
*/
|
|
49
|
+
testId?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The tabs to render in the dropdown.
|
|
52
|
+
*/
|
|
53
|
+
tabs: Array<TabDropdownItem>;
|
|
54
|
+
/**
|
|
55
|
+
* The id of the tab that is selected.
|
|
56
|
+
*
|
|
57
|
+
* If the selectedTabId is not valid, the `labels.defaultOpenerLabel` will
|
|
58
|
+
* be used to label the dropdown opener.
|
|
59
|
+
*/
|
|
60
|
+
selectedTabId: string;
|
|
61
|
+
/**
|
|
62
|
+
* Called when a tab is selected.
|
|
63
|
+
*/
|
|
64
|
+
onTabSelected: (id: string) => unknown;
|
|
65
|
+
/**
|
|
66
|
+
* Labels for the dropdown.
|
|
67
|
+
*/
|
|
68
|
+
labels?: {
|
|
69
|
+
defaultOpenerLabel?: string;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Can be used to override the opened state for the dropdown
|
|
73
|
+
*/
|
|
74
|
+
opened?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Styling for the tabs dropdown.
|
|
77
|
+
*/
|
|
78
|
+
styles?: {
|
|
79
|
+
root?: StyleType;
|
|
80
|
+
actionMenu?: StyleType;
|
|
81
|
+
opener?: StyleType;
|
|
82
|
+
tabPanel?: StyleType;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The TabsDropdown component is used to represent tabs in an ActionMenu when
|
|
87
|
+
* there is not enough horizontal space to render the tabs as a horizontal layout.
|
|
88
|
+
*
|
|
89
|
+
* Note: This component is meant to be used internally to address responsiveness
|
|
90
|
+
* in the ResponsiveTabs component. Please reach out to the WB team if there is
|
|
91
|
+
* a need to use this component directly.
|
|
92
|
+
*/
|
|
93
|
+
export declare const TabsDropdown: React.ForwardRefExoticComponent<TabsDropdownProps & React.RefAttributes<HTMLDivElement>>;
|
|
94
|
+
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { AriaProps, PropsFor, StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
3
|
import { Tab } from "./tab";
|
|
4
|
+
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
4
5
|
export type TabRenderProps = Omit<PropsFor<typeof Tab>, "children">;
|
|
5
6
|
export type TabItem = AriaProps & {
|
|
6
7
|
/**
|
|
@@ -35,26 +36,12 @@ export type TabItem = AriaProps & {
|
|
|
35
36
|
* - The associated tab panel will have a testId formatted as `${testId}-panel`
|
|
36
37
|
*/
|
|
37
38
|
testId?: string;
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* Type to help ensure aria-label or aria-labelledby is set.
|
|
41
|
-
*/
|
|
42
|
-
type AriaLabelOrAriaLabelledby = {
|
|
43
39
|
/**
|
|
44
|
-
*
|
|
45
|
-
* label describing the tabs.
|
|
40
|
+
* Optional icon to display in the tab. Should be a PhosphorIcon or Icon component.
|
|
46
41
|
*/
|
|
47
|
-
|
|
48
|
-
"aria-labelledby"?: never;
|
|
49
|
-
} | {
|
|
50
|
-
/**
|
|
51
|
-
* If the tabs have a visible label, set aria-labelledby to a value
|
|
52
|
-
* that refers to the labelling element.
|
|
53
|
-
*/
|
|
54
|
-
"aria-labelledby": string;
|
|
55
|
-
"aria-label"?: never;
|
|
42
|
+
icon?: React.ReactElement;
|
|
56
43
|
};
|
|
57
|
-
type
|
|
44
|
+
export type TabsProps = {
|
|
58
45
|
/**
|
|
59
46
|
* A unique id to use as the base of the ids for the elements within the
|
|
60
47
|
* component. If the `id` prop is not provided, a base unique id will be
|
|
@@ -138,12 +125,19 @@ type Props = {
|
|
|
138
125
|
* Defaults to `false`.
|
|
139
126
|
*/
|
|
140
127
|
mountAllPanels?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Optional ref to the scrollable wrapper element.
|
|
130
|
+
* This is useful for components that need to detect horizontal overflow.
|
|
131
|
+
*/
|
|
132
|
+
scrollableElementRef?: React.RefObject<HTMLDivElement>;
|
|
141
133
|
} & AriaLabelOrAriaLabelledby;
|
|
142
134
|
/**
|
|
143
135
|
* A component that uses a tabbed interface to control a specific view. The
|
|
144
136
|
* tabs have `role=”tab”` and keyboard users can change tabs using arrow keys.
|
|
145
137
|
* For a tabbed interface where the tabs are links, see the NavigationTabs
|
|
146
138
|
* component.
|
|
139
|
+
*
|
|
140
|
+
* For responsive cases where the tabs should switch to a dropdown when there is
|
|
141
|
+
* not enough horizontal space, use the `ResponsiveTabs` component.
|
|
147
142
|
*/
|
|
148
|
-
export declare const Tabs: React.ForwardRefExoticComponent<
|
|
149
|
-
export {};
|
|
143
|
+
export declare const Tabs: React.ForwardRefExoticComponent<TabsProps & React.RefAttributes<HTMLDivElement>>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type to help ensure aria-label or aria-labelledby is set.
|
|
3
|
+
*/
|
|
4
|
+
export type AriaLabelOrAriaLabelledby = {
|
|
5
|
+
/**
|
|
6
|
+
* If there is no visible label for the tabs, set aria-label to a
|
|
7
|
+
* label describing the tabs.
|
|
8
|
+
*/
|
|
9
|
+
"aria-label": string;
|
|
10
|
+
"aria-labelledby"?: never;
|
|
11
|
+
} | {
|
|
12
|
+
/**
|
|
13
|
+
* If the tabs have a visible label, set aria-labelledby to a value
|
|
14
|
+
* that refers to the labelling element.
|
|
15
|
+
*/
|
|
16
|
+
"aria-labelledby": string;
|
|
17
|
+
"aria-label"?: never;
|
|
18
|
+
};
|
package/dist/es/index.js
CHANGED
|
@@ -1,26 +1,43 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { useOnMountEffect, addStyle, keys } from '@khanacademy/wonder-blocks-core';
|
|
2
|
+
import { useOnMountEffect, addStyle, View, keys } from '@khanacademy/wonder-blocks-core';
|
|
3
3
|
import { border, semanticColor, sizing, breakpoint } from '@khanacademy/wonder-blocks-tokens';
|
|
4
4
|
import { StyleSheet } from 'aphrodite';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
-
import {
|
|
6
|
+
import { createElement } from 'react';
|
|
7
|
+
import { styles as styles$b } from '@khanacademy/wonder-blocks-typography';
|
|
8
|
+
import { ActionMenu, ActionItem } from '@khanacademy/wonder-blocks-dropdown';
|
|
9
|
+
import Button from '@khanacademy/wonder-blocks-button';
|
|
10
|
+
import caretDown from '@phosphor-icons/core/bold/caret-down-bold.svg';
|
|
11
|
+
import checkCircleIcon from '@phosphor-icons/core/fill/check-circle-fill.svg';
|
|
12
|
+
import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
|
|
13
|
+
import Link from '@khanacademy/wonder-blocks-link';
|
|
7
14
|
|
|
8
|
-
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React.useRef(false);const[underlineStyle,setUnderlineStyle]=React.useState({left:0,width:0});const updateUnderlineStyle=React.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$
|
|
15
|
+
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React.useRef(false);const[underlineStyle,setUnderlineStyle]=React.useState({left:0,width:0});const updateUnderlineStyle=React.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$a.currentUnderline,...positioningStyle,...animated?styles$a.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$a={currentUnderline:{position:"absolute",bottom:0,left:0,height:border.width.thick,backgroundColor:semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}};
|
|
9
16
|
|
|
10
|
-
const StyledUl=addStyle("ul");const StyledDiv$3=addStyle("div");const NavigationTabs=React.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React.useMemo(()=>addStyle(tag),[tag]);const listRef=React.useRef(null);const isTabActive=React.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$
|
|
17
|
+
const StyledUl=addStyle("ul");const StyledDiv$3=addStyle("div");const NavigationTabs=React.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",showDivider=false,...otherProps}=props;const StyledTag=React.useMemo(()=>addStyle(tag),[tag]);const listRef=React.useRef(null);const isTabActive=React.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$9.nav,showDivider&&styles$9.divider,stylesProp?.root],...otherProps,children:jsxs(StyledDiv$3,{style:styles$9.contents,children:[jsx(StyledUl,{style:[styles$9.list,stylesProp?.list],ref:listRef,children:children}),jsx("div",{...indicatorProps})]})})});const styles$9=StyleSheet.create({nav:{overflowX:"auto"},contents:{position:"relative"},list:{paddingInline:sizing.size_040,paddingBlock:sizing.size_0,margin:sizing.size_0,display:"flex",gap:sizing.size_160,flexWrap:"nowrap"},divider:{borderBlockEnd:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`}});
|
|
11
18
|
|
|
12
|
-
const StyledLi=addStyle("li");const NavigationTabItem=React.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[styles$
|
|
19
|
+
const StyledLi=addStyle("li");const NavigationTabItem=React.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[styles$b.Body,styles$8.link,current&&styles$8.currentLink],"aria-current":current?"page":undefined};if(typeof children==="function"){return children(linkProps)}return React.cloneElement(children,linkProps)}return jsx(StyledLi,{id:id,"data-testid":testId,style:[styles$8.root,current&&styles$8.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$8=StyleSheet.create({root:{listStyle:"none",display:"inline-flex",[":has(a:hover)"]:{boxShadow:`inset 0 calc(${sizing.size_020}*-1) 0 0 ${semanticColor.core.border.instructive.default}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${sizing.size_060}*-1) 0 0 ${semanticColor.core.border.instructive.strong}`},paddingBlockStart:sizing.size_080,paddingBlockEnd:sizing.size_180,[breakpoint.mediaQuery.mdOrLarger]:{paddingBlockStart:sizing.size_200,paddingBlockEnd:sizing.size_240}},current:{[":has(a:hover)"]:{boxShadow:"none"},[":has(a:active):not([aria-disabled=true])"]:{boxShadow:"none"}},currentLink:{color:semanticColor.link.rest,[":active:not([aria-disabled=true])"]:{color:semanticColor.link.rest}},link:{display:"flex",margin:0,color:semanticColor.core.foreground.neutral.subtle,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.link.hover,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.link.press},":focus-visible":{color:semanticColor.link.rest,border:"none",outline:"none",boxShadow:`0 0 0 ${sizing.size_020} ${semanticColor.focus.inner}, 0 0 0 ${sizing.size_040} ${semanticColor.focus.outer}`,borderRadius:border.radius.radius_0}}});
|
|
13
20
|
|
|
14
21
|
const focus={":focus-visible":{boxShadow:`0 0 0 ${border.width.medium} ${semanticColor.focus.inner}`,outline:`${border.width.medium} solid ${semanticColor.focus.outer}`,outlineOffset:border.width.medium}};var focusStyles=Object.freeze({__proto__:null,focus:focus});const pressColor=`color-mix(in srgb, ${semanticColor.core.border.neutral.default} 55%, ${semanticColor.core.border.knockout.default})`;const inverse={":not([aria-disabled=true])":{borderColor:semanticColor.core.border.knockout.default,color:semanticColor.core.foreground.knockout.default},":hover:not([aria-disabled=true])":{color:semanticColor.core.foreground.knockout.default,borderColor:semanticColor.core.border.knockout.default},...focus,":active:not([aria-disabled=true])":{borderRadius:border.radius.radius_080,borderColor:pressColor,background:`color-mix(in srgb, ${semanticColor.core.background.base.default} 5%, transparent)`}};Object.freeze({__proto__:null,inverse:inverse});
|
|
15
22
|
|
|
16
23
|
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
17
24
|
|
|
18
|
-
const StyledDiv$2=addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React.useState(null);const updateHasFocusableElement=React.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$
|
|
25
|
+
const StyledDiv$2=addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React.useState(null);const updateHasFocusableElement=React.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$7.tabPanel,style],children:children})};const styles$7=StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
|
|
19
26
|
|
|
20
|
-
const StyledButton=addStyle("button");const Tab=React.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return
|
|
27
|
+
const StyledButton=addStyle("button");const Tab=React.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,icon,...otherProps}=props;return jsxs(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[styles$b.Body,styles$6.tab,selected&&styles$6.selectedTab,style],children:[icon&&jsx(View,{children:React.cloneElement(icon,{size:icon.props.size??"medium"})}),children]})});const bottomSpacing=sizing.size_140;const styles$6=StyleSheet.create({tab:{display:"flex",alignItems:"center",textWrap:"nowrap",backgroundColor:"transparent",border:"none",margin:0,padding:0,cursor:"pointer",marginBlockStart:sizing.size_080,marginBlockEnd:bottomSpacing,gap:sizing.size_080,position:"relative",color:semanticColor.core.foreground.neutral.subtle,...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:semanticColor.link.hover,[":after"]:{height:border.width.thin,backgroundColor:semanticColor.link.hover}},[":active:not([aria-selected='true'])"]:{color:semanticColor.link.press,[":after"]:{height:border.width.thick,backgroundColor:semanticColor.link.press}}},selectedTab:{color:semanticColor.link.rest}});
|
|
21
28
|
|
|
22
|
-
const StyledDiv$1=addStyle("div");const Tablist=React.forwardRef(function Tablist(props,ref){const{id,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur,testId,style}=props;return jsx(StyledDiv$1,{id:id,role:"tablist",style:[styles$
|
|
29
|
+
const StyledDiv$1=addStyle("div");const Tablist=React.forwardRef(function Tablist(props,ref){const{id,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur,testId,style}=props;return jsx(StyledDiv$1,{id:id,role:"tablist",style:[styles$5.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$5=StyleSheet.create({tablist:{display:"flex",gap:sizing.size_240,borderBottom:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`,paddingInline:sizing.size_040}});
|
|
23
30
|
|
|
24
|
-
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=addStyle("div");const Tabs=React.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false}=props;const focusedTabId=React.useRef(selectedTabId);const tabRefs=React.useRef({});const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React.useRef(null);const isTabActive=React.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&keys.right:case!isRtl&&keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&keys.left:case!isRtl&&keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case keys.enter:case keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsx(React.Fragment,{})}return jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles.tabs,stylesProp?.root],children:[jsxs(StyledDiv,{style:styles.tablistWrapper,children:[jsx(Tablist,{"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:handleTablistBlur,id:tablistId,testId:testId&&`${testId}-tablist`,ref:tablistRef,style:stylesProp?.tablist,children:tabs.map(tab=>{const{id,label,panel:_,testId:tabTestId,...otherProps}=tab;const tabProps={...otherProps,key:id,id:getTabId(id),testId:tabTestId&&getTabId(tabTestId),selected:id===selectedTabId,"aria-controls":getTabPanelId(id),onClick:()=>{onTabSelected(id);},onKeyDown:handleKeyDown,ref:element=>{tabRefs.current[tab.id]=element;},style:stylesProp?.tab};if(typeof label==="function"){return label(tabProps)}return jsx(Tab,{...tabProps,children:label})})}),jsx("div",{...indicatorProps})]}),tabs.map(tab=>{return jsx(TabPanel,{id:getTabPanelId(tab.id),"aria-labelledby":getTabId(tab.id),active:selectedTabId===tab.id,testId:tab.testId&&getTabPanelId(tab.testId),style:stylesProp?.tabPanel,children:(mountAllPanels||visitedTabsRef.current.has(tab.id))&&tab.panel},tab.id)})]})});const styles=StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
|
|
31
|
+
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=addStyle("div");const Tabs=React.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false,scrollableElementRef}=props;const focusedTabId=React.useRef(selectedTabId);const tabRefs=React.useRef({});const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React.useRef(null);const isTabActive=React.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&keys.right:case!isRtl&&keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&keys.left:case!isRtl&&keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case keys.enter:case keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsx(React.Fragment,{})}return jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles$4.tabs,stylesProp?.root],children:[jsxs(StyledDiv,{ref:scrollableElementRef,style:styles$4.tablistWrapper,children:[jsx(Tablist,{"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:handleTablistBlur,id:tablistId,testId:testId&&`${testId}-tablist`,ref:tablistRef,style:stylesProp?.tablist,children:tabs.map(tab=>{const{id,label,panel:_,testId:tabTestId,icon,...otherProps}=tab;const tabProps={...otherProps,key:id,id:getTabId(id),testId:tabTestId&&getTabId(tabTestId),selected:id===selectedTabId,icon,"aria-controls":getTabPanelId(id),onClick:()=>{onTabSelected(id);},onKeyDown:handleKeyDown,ref:element=>{tabRefs.current[tab.id]=element;},style:stylesProp?.tab};if(typeof label==="function"){return label(tabProps)}return jsx(Tab,{...tabProps,children:label})})}),jsx("div",{...indicatorProps})]}),tabs.map(tab=>{return jsx(TabPanel,{id:getTabPanelId(tab.id),"aria-labelledby":getTabId(tab.id),active:selectedTabId===tab.id,testId:tab.testId&&getTabPanelId(tab.testId),style:stylesProp?.tabPanel,children:(mountAllPanels||visitedTabsRef.current.has(tab.id))&&tab.panel},tab.id)})]})});const styles$4=StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
|
|
25
32
|
|
|
26
|
-
|
|
33
|
+
const defaultLabels$1={defaultOpenerLabel:"Tabs"};const TabsDropdown=React.forwardRef((props,ref)=>{const{tabs,selectedTabId,onTabSelected,labels:labelsProp,opened,id:idProp,testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,styles:stylesProp}=props;const labels=React.useMemo(()=>{return {...defaultLabels$1,...labelsProp}},[labelsProp]);const generatedUniqueId=React.useId();const uniqueId=idProp??generatedUniqueId;const openerId=`${uniqueId}-opener`;const panelId=`${uniqueId}-panel`;const selectedTabItem=React.useMemo(()=>{return tabs.find(tab=>tab.id===selectedTabId)},[tabs,selectedTabId]);const processedTabs=React.useMemo(()=>{return tabs.map(tab=>({...tab,leftAccessory:tab.icon?React.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:()=>{onTabSelected(tab.id);}}))},[tabs,onTabSelected]);if(tabs.length===0){return jsx(React.Fragment,{})}const menuText=selectedTabItem?.label||labels.defaultOpenerLabel;return jsxs(View,{ref:ref,id:uniqueId,testId:testId,style:stylesProp?.root,role:"region","aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,children:[jsx(ActionMenu,{opened:opened,id:openerId,menuText:menuText,opener:()=>jsx(Button,{testId:testId?`${testId}-opener`:undefined,kind:"tertiary",endIcon:caretDown,style:[styles$3.opener,stylesProp?.opener],labelStyle:styles$3.labelStyle,"aria-label":selectedTabItem?.["aria-label"],startIcon:selectedTabItem?.icon,children:menuText}),style:[styles$3.actionMenu,stylesProp?.actionMenu],children:processedTabs.map(tab=>{return jsx(ActionItem,{label:tab.label,"aria-label":tab["aria-label"],onClick:tab.handleClick,active:tab.id===selectedTabId,testId:tab.testId,rightAccessory:tab.id===selectedTabId?jsx(PhosphorIcon,{icon:checkCircleIcon,size:"medium","aria-hidden":"true"}):undefined,leftAccessory:tab.leftAccessory},tab.id)})}),jsx(View,{id:panelId,role:"group","aria-labelledby":openerId,testId:testId?`${testId}-panel`:undefined,style:stylesProp?.tabPanel,children:selectedTabItem?.panel})]})});const styles$3=StyleSheet.create({actionMenu:{width:"100%",alignItems:"flex-start",borderBlockEnd:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`},opener:{position:"relative",height:"unset",paddingBlockStart:sizing.size_120,paddingBlockEnd:sizing.size_140,paddingInline:sizing.size_180,width:"100%",justifyContent:"space-between",gap:sizing.size_020},labelStyle:{flexGrow:1,maxWidth:"100%",textAlign:"start"}});
|
|
34
|
+
|
|
35
|
+
function useResponsiveLayout(options){const{tabs,elementWithOverflowRef,containerRef,onLayoutChange}=options;const[showDropdown,setShowDropdown]=React.useState(false);const tabsWidthRef=React.useRef(null);const tabsSignature=React.useMemo(()=>tabs.map(t=>`${t.id}:${t.label}-${t.icon?"with-icon":"without-icon"}`).join("|"),[tabs]);const checkOverflow=React.useCallback(()=>{const container=containerRef.current;if(!container){return}if(!showDropdown&&elementWithOverflowRef.current){const scrollableWrapper=elementWithOverflowRef.current;if(scrollableWrapper){const hasOverflow=scrollableWrapper.scrollWidth>scrollableWrapper.clientWidth;if(hasOverflow){tabsWidthRef.current=scrollableWrapper.scrollWidth;setShowDropdown(true);}}}else if(showDropdown&&tabsWidthRef.current){const containerWidth=container.clientWidth;if(containerWidth>=tabsWidthRef.current){setShowDropdown(false);}}},[showDropdown,elementWithOverflowRef,containerRef]);React.useEffect(()=>{if(showDropdown){tabsWidthRef.current=null;setShowDropdown(false);}else {checkOverflow();}},[tabsSignature]);React.useEffect(()=>{const container=containerRef.current;if(!container||!window.ResizeObserver){return}const resizeObserver=new ResizeObserver(()=>{checkOverflow();});resizeObserver.observe(container);return ()=>{resizeObserver.disconnect();}},[checkOverflow,containerRef]);React.useEffect(()=>{onLayoutChange?.(showDropdown?"dropdown":"tabs");},[showDropdown,onLayoutChange]);return {showDropdown}}
|
|
36
|
+
|
|
37
|
+
const ResponsiveTabs=props=>{const{tabs,selectedTabId,onTabSelected,onLayoutChange,id,testId,tabsProps,dropdownProps,styles:stylesProp,...ariaProps}=props;const tabsRef=React.useRef(null);const scrollableTabsRef=React.useRef(null);const containerRef=React.useRef(null);const{showDropdown}=useResponsiveLayout({tabs,elementWithOverflowRef:scrollableTabsRef,containerRef,onLayoutChange});return jsx(View,{ref:containerRef,style:[styles$2.container,stylesProp?.root],id:id,testId:testId,children:showDropdown?createElement(TabsDropdown,{...dropdownProps,...ariaProps,key:"dropdown",tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...dropdownProps?.styles,root:[styles$2.fadeIn,dropdownProps?.styles?.root]}}):createElement(Tabs,{...tabsProps,...ariaProps,key:"tabs",ref:tabsRef,scrollableElementRef:scrollableTabsRef,tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...tabsProps?.styles,root:[styles$2.fadeIn,tabsProps?.styles?.root]}})})};const fadeInKeyframes$1={from:{opacity:0},to:{opacity:1}};const styles$2=StyleSheet.create({fadeIn:{animationName:fadeInKeyframes$1,animationDuration:"150ms",animationTimingFunction:"ease-in-out"},container:{width:"100%",minHeight:"auto"}});
|
|
38
|
+
|
|
39
|
+
const defaultLabels={defaultOpenerLabel:"Tabs"};const NavigationTabsDropdown=React.forwardRef((props,ref)=>{const{tabs,selectedTabId,onTabSelected,id:idProp,testId,labels:labelsProp,opened,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag="nav",styles:stylesProp,showDivider=false}=props;const StyledTag=React.useMemo(()=>addStyle(tag),[tag]);const labels=React.useMemo(()=>{return {...defaultLabels,...labelsProp}},[labelsProp]);const selectedTabItem=React.useMemo(()=>{return tabs.find(tab=>tab.id===selectedTabId)},[tabs,selectedTabId]);const processedTabs=React.useMemo(()=>{return tabs.map(tab=>({...tab,leftAccessory:tab.icon?React.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:onTabSelected?()=>onTabSelected(tab.id):undefined}))},[tabs,onTabSelected]);const generatedUniqueId=React.useId();const uniqueId=idProp??generatedUniqueId;const openerId=`${uniqueId}-opener`;if(tabs.length===0){return jsx(React.Fragment,{})}const menuText=selectedTabItem?.label||labels.defaultOpenerLabel;return jsx(StyledTag,{ref:ref,id:uniqueId,"data-testid":testId,style:[showDivider&&styles$1.divider,stylesProp?.root],"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,children:jsx(ActionMenu,{opened:opened,id:openerId,menuText:menuText,opener:()=>jsx(Button,{testId:testId?`${testId}-opener`:undefined,kind:"tertiary",endIcon:caretDown,style:[styles$1.opener,stylesProp?.opener],labelStyle:styles$1.labelStyle,"aria-label":selectedTabItem?.["aria-label"],startIcon:selectedTabItem?.icon,children:menuText}),style:[styles$1.actionMenu,stylesProp?.actionMenu],children:processedTabs.map(tab=>{return jsx(ActionItem,{label:tab.label,href:tab.href,"aria-label":tab["aria-label"],active:tab.id===selectedTabId,testId:tab.testId,leftAccessory:tab.leftAccessory,onClick:tab.handleClick,rightAccessory:tab.id===selectedTabId?jsx(PhosphorIcon,{icon:checkCircleIcon,size:"medium","aria-hidden":"true"}):undefined},tab.id)})})})});const styles$1=StyleSheet.create({actionMenu:{width:"100%",alignItems:"flex-start"},divider:{borderBlockEnd:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`},opener:{position:"relative",height:"unset",paddingBlockStart:sizing.size_120,paddingBlockEnd:sizing.size_140,paddingInline:sizing.size_180,width:"100%",justifyContent:"space-between",gap:sizing.size_020},labelStyle:{flexGrow:1,maxWidth:"100%",textAlign:"start"}});
|
|
40
|
+
|
|
41
|
+
const ResponsiveNavigationTabs=props=>{const{tabs,selectedTabId,onTabSelected,onLayoutChange,id,testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag,tabsProps,dropdownProps,styles:stylesProp,showDivider=false}=props;const navigationTabsRef=React.useRef(null);const containerRef=React.useRef(null);const{showDropdown}=useResponsiveLayout({tabs,elementWithOverflowRef:navigationTabsRef,containerRef,onLayoutChange});const processedTabs=React.useMemo(()=>{return tabs.map(tab=>({...tab,startIcon:tab.icon?React.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:onTabSelected?()=>onTabSelected(tab.id):undefined}))},[tabs,onTabSelected]);return jsx(View,{ref:containerRef,style:[styles.container,stylesProp?.root],id:id,testId:testId,children:showDropdown?createElement(NavigationTabsDropdown,{...dropdownProps,key:"dropdown",tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag:tag,styles:{...dropdownProps?.styles,root:[styles.fadeIn,dropdownProps?.styles?.root]},showDivider:showDivider}):createElement(NavigationTabs,{...tabsProps,key:"tabs",ref:navigationTabsRef,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag:tag,styles:{...tabsProps?.styles,root:[styles.fadeIn,tabsProps?.styles?.root]},showDivider:showDivider},processedTabs.map(tab=>jsx(NavigationTabItem,{current:tab.id===selectedTabId,children:jsx(Link,{href:tab.href,onClick:tab.handleClick,startIcon:tab.startIcon,"aria-label":tab["aria-label"],testId:tab.testId,children:tab.label})},tab.id)))})};const fadeInKeyframes={from:{opacity:0},to:{opacity:1}};const styles=StyleSheet.create({fadeIn:{animationName:fadeInKeyframes,animationDuration:"150ms",animationTimingFunction:"ease-in-out"},container:{width:"100%",minHeight:"auto"}});
|
|
42
|
+
|
|
43
|
+
export { NavigationTabItem, NavigationTabs, ResponsiveNavigationTabs, ResponsiveTabs, Tab, Tabs };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type TabItem = {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: React.ReactElement;
|
|
6
|
+
};
|
|
7
|
+
type UseResponsiveLayoutOptions = {
|
|
8
|
+
/**
|
|
9
|
+
* The tabs to display. Used to create a signature for detecting changes.
|
|
10
|
+
*/
|
|
11
|
+
tabs: TabItem[];
|
|
12
|
+
/**
|
|
13
|
+
* Reference to the element with horizontal scroll (element with `overflow-x: auto` set).
|
|
14
|
+
* This ref is used to check for horizontal overflow to determine if the
|
|
15
|
+
* layout should be switched.
|
|
16
|
+
*/
|
|
17
|
+
elementWithOverflowRef: React.RefObject<HTMLElement>;
|
|
18
|
+
/**
|
|
19
|
+
* Reference to the container element that wraps both layouts.
|
|
20
|
+
*
|
|
21
|
+
* Used for determining if there is enough space in the container to
|
|
22
|
+
* display the tabs in a horizontal layout. Also used to observe resize
|
|
23
|
+
* events.
|
|
24
|
+
*/
|
|
25
|
+
containerRef: React.RefObject<HTMLDivElement>;
|
|
26
|
+
/**
|
|
27
|
+
* Optional callback that is called when the layout changes between
|
|
28
|
+
* tabs and dropdown layouts.
|
|
29
|
+
*/
|
|
30
|
+
onLayoutChange?: (layout: "tabs" | "dropdown") => void;
|
|
31
|
+
};
|
|
32
|
+
type UseResponsiveLayoutResult = {
|
|
33
|
+
/**
|
|
34
|
+
* Whether to show the dropdown layout instead of the horizontal layout.
|
|
35
|
+
*/
|
|
36
|
+
showDropdown: boolean;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Custom hook that manages the responsive layout logic for switching between
|
|
40
|
+
* a horizontal tabs layout and a dropdown layout based on available space.
|
|
41
|
+
*
|
|
42
|
+
* Changes that can trigger a layout change:
|
|
43
|
+
* - The number of tabs
|
|
44
|
+
* - The length of the tab labels
|
|
45
|
+
* - The width of the container
|
|
46
|
+
* - The zoom level
|
|
47
|
+
* - The presence of icons in the tabs
|
|
48
|
+
*/
|
|
49
|
+
export declare function useResponsiveLayout(options: UseResponsiveLayoutOptions): UseResponsiveLayoutResult;
|
|
50
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,7 @@ export { NavigationTabItem } from "./components/navigation-tab-item";
|
|
|
3
3
|
export { Tabs } from "./components/tabs";
|
|
4
4
|
export type { TabItem, TabRenderProps } from "./components/tabs";
|
|
5
5
|
export { Tab } from "./components/tab";
|
|
6
|
+
export { ResponsiveTabs } from "./components/responsive-tabs";
|
|
7
|
+
export type { ResponsiveTabItem } from "./components/responsive-tabs";
|
|
8
|
+
export { ResponsiveNavigationTabs } from "./components/responsive-navigation-tabs";
|
|
9
|
+
export type { ResponsiveNavigationTabItem } from "./components/responsive-navigation-tabs";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,14 @@ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
|
|
|
8
8
|
var aphrodite = require('aphrodite');
|
|
9
9
|
var React = require('react');
|
|
10
10
|
var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
|
|
11
|
+
var wonderBlocksDropdown = require('@khanacademy/wonder-blocks-dropdown');
|
|
12
|
+
var Button = require('@khanacademy/wonder-blocks-button');
|
|
13
|
+
var caretDown = require('@phosphor-icons/core/bold/caret-down-bold.svg');
|
|
14
|
+
var checkCircleIcon = require('@phosphor-icons/core/fill/check-circle-fill.svg');
|
|
15
|
+
var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
|
|
16
|
+
var Link = require('@khanacademy/wonder-blocks-link');
|
|
17
|
+
|
|
18
|
+
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
11
19
|
|
|
12
20
|
function _interopNamespace(e) {
|
|
13
21
|
if (e && e.__esModule) return e;
|
|
@@ -28,26 +36,42 @@ function _interopNamespace(e) {
|
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
39
|
+
var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
|
|
40
|
+
var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
|
|
41
|
+
var checkCircleIcon__default = /*#__PURE__*/_interopDefaultLegacy(checkCircleIcon);
|
|
42
|
+
var Link__default = /*#__PURE__*/_interopDefaultLegacy(Link);
|
|
31
43
|
|
|
32
|
-
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React__namespace.useRef(false);const[underlineStyle,setUnderlineStyle]=React__namespace.useState({left:0,width:0});const updateUnderlineStyle=React__namespace.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);wonderBlocksCore.useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$
|
|
44
|
+
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React__namespace.useRef(false);const[underlineStyle,setUnderlineStyle]=React__namespace.useState({left:0,width:0});const updateUnderlineStyle=React__namespace.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);wonderBlocksCore.useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$a.currentUnderline,...positioningStyle,...animated?styles$a.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$a={currentUnderline:{position:"absolute",bottom:0,left:0,height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}};
|
|
33
45
|
|
|
34
|
-
const StyledUl=wonderBlocksCore.addStyle("ul");const StyledDiv$3=wonderBlocksCore.addStyle("div");const NavigationTabs=React__namespace.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React__namespace.useMemo(()=>wonderBlocksCore.addStyle(tag),[tag]);const listRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsxRuntime.jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$
|
|
46
|
+
const StyledUl=wonderBlocksCore.addStyle("ul");const StyledDiv$3=wonderBlocksCore.addStyle("div");const NavigationTabs=React__namespace.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",showDivider=false,...otherProps}=props;const StyledTag=React__namespace.useMemo(()=>wonderBlocksCore.addStyle(tag),[tag]);const listRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsxRuntime.jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$9.nav,showDivider&&styles$9.divider,stylesProp?.root],...otherProps,children:jsxRuntime.jsxs(StyledDiv$3,{style:styles$9.contents,children:[jsxRuntime.jsx(StyledUl,{style:[styles$9.list,stylesProp?.list],ref:listRef,children:children}),jsxRuntime.jsx("div",{...indicatorProps})]})})});const styles$9=aphrodite.StyleSheet.create({nav:{overflowX:"auto"},contents:{position:"relative"},list:{paddingInline:wonderBlocksTokens.sizing.size_040,paddingBlock:wonderBlocksTokens.sizing.size_0,margin:wonderBlocksTokens.sizing.size_0,display:"flex",gap:wonderBlocksTokens.sizing.size_160,flexWrap:"nowrap"},divider:{borderBlockEnd:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`}});
|
|
35
47
|
|
|
36
|
-
const StyledLi=wonderBlocksCore.addStyle("li");const NavigationTabItem=React__namespace.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[wonderBlocksTypography.styles.Body,styles$
|
|
48
|
+
const StyledLi=wonderBlocksCore.addStyle("li");const NavigationTabItem=React__namespace.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[wonderBlocksTypography.styles.Body,styles$8.link,current&&styles$8.currentLink],"aria-current":current?"page":undefined};if(typeof children==="function"){return children(linkProps)}return React__namespace.cloneElement(children,linkProps)}return jsxRuntime.jsx(StyledLi,{id:id,"data-testid":testId,style:[styles$8.root,current&&styles$8.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$8=aphrodite.StyleSheet.create({root:{listStyle:"none",display:"inline-flex",[":has(a:hover)"]:{boxShadow:`inset 0 calc(${wonderBlocksTokens.sizing.size_020}*-1) 0 0 ${wonderBlocksTokens.semanticColor.core.border.instructive.default}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${wonderBlocksTokens.sizing.size_060}*-1) 0 0 ${wonderBlocksTokens.semanticColor.core.border.instructive.strong}`},paddingBlockStart:wonderBlocksTokens.sizing.size_080,paddingBlockEnd:wonderBlocksTokens.sizing.size_180,[wonderBlocksTokens.breakpoint.mediaQuery.mdOrLarger]:{paddingBlockStart:wonderBlocksTokens.sizing.size_200,paddingBlockEnd:wonderBlocksTokens.sizing.size_240}},current:{[":has(a:hover)"]:{boxShadow:"none"},[":has(a:active):not([aria-disabled=true])"]:{boxShadow:"none"}},currentLink:{color:wonderBlocksTokens.semanticColor.link.rest,[":active:not([aria-disabled=true])"]:{color:wonderBlocksTokens.semanticColor.link.rest}},link:{display:"flex",margin:0,color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.link.hover,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.link.press},":focus-visible":{color:wonderBlocksTokens.semanticColor.link.rest,border:"none",outline:"none",boxShadow:`0 0 0 ${wonderBlocksTokens.sizing.size_020} ${wonderBlocksTokens.semanticColor.focus.inner}, 0 0 0 ${wonderBlocksTokens.sizing.size_040} ${wonderBlocksTokens.semanticColor.focus.outer}`,borderRadius:wonderBlocksTokens.border.radius.radius_0}}});
|
|
37
49
|
|
|
38
50
|
const focus={":focus-visible":{boxShadow:`0 0 0 ${wonderBlocksTokens.border.width.medium} ${wonderBlocksTokens.semanticColor.focus.inner}`,outline:`${wonderBlocksTokens.border.width.medium} solid ${wonderBlocksTokens.semanticColor.focus.outer}`,outlineOffset:wonderBlocksTokens.border.width.medium}};var focusStyles=Object.freeze({__proto__:null,focus:focus});const pressColor=`color-mix(in srgb, ${wonderBlocksTokens.semanticColor.core.border.neutral.default} 55%, ${wonderBlocksTokens.semanticColor.core.border.knockout.default})`;const inverse={":not([aria-disabled=true])":{borderColor:wonderBlocksTokens.semanticColor.core.border.knockout.default,color:wonderBlocksTokens.semanticColor.core.foreground.knockout.default},":hover:not([aria-disabled=true])":{color:wonderBlocksTokens.semanticColor.core.foreground.knockout.default,borderColor:wonderBlocksTokens.semanticColor.core.border.knockout.default},...focus,":active:not([aria-disabled=true])":{borderRadius:wonderBlocksTokens.border.radius.radius_080,borderColor:pressColor,background:`color-mix(in srgb, ${wonderBlocksTokens.semanticColor.core.background.base.default} 5%, transparent)`}};Object.freeze({__proto__:null,inverse:inverse});
|
|
39
51
|
|
|
40
52
|
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
41
53
|
|
|
42
|
-
const StyledDiv$2=wonderBlocksCore.addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React__namespace.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React__namespace.useState(null);const updateHasFocusableElement=React__namespace.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React__namespace.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsxRuntime.jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$
|
|
54
|
+
const StyledDiv$2=wonderBlocksCore.addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React__namespace.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React__namespace.useState(null);const updateHasFocusableElement=React__namespace.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React__namespace.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsxRuntime.jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$7.tabPanel,style],children:children})};const styles$7=aphrodite.StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
|
|
55
|
+
|
|
56
|
+
const StyledButton=wonderBlocksCore.addStyle("button");const Tab=React__namespace.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,icon,...otherProps}=props;return jsxRuntime.jsxs(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[wonderBlocksTypography.styles.Body,styles$6.tab,selected&&styles$6.selectedTab,style],children:[icon&&jsxRuntime.jsx(wonderBlocksCore.View,{children:React__namespace.cloneElement(icon,{size:icon.props.size??"medium"})}),children]})});const bottomSpacing=wonderBlocksTokens.sizing.size_140;const styles$6=aphrodite.StyleSheet.create({tab:{display:"flex",alignItems:"center",textWrap:"nowrap",backgroundColor:"transparent",border:"none",margin:0,padding:0,cursor:"pointer",marginBlockStart:wonderBlocksTokens.sizing.size_080,marginBlockEnd:bottomSpacing,gap:wonderBlocksTokens.sizing.size_080,position:"relative",color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.hover,[":after"]:{height:wonderBlocksTokens.border.width.thin,backgroundColor:wonderBlocksTokens.semanticColor.link.hover}},[":active:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.press,[":after"]:{height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.link.press}}},selectedTab:{color:wonderBlocksTokens.semanticColor.link.rest}});
|
|
57
|
+
|
|
58
|
+
const StyledDiv$1=wonderBlocksCore.addStyle("div");const Tablist=React__namespace.forwardRef(function Tablist(props,ref){const{id,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur,testId,style}=props;return jsxRuntime.jsx(StyledDiv$1,{id:id,role:"tablist",style:[styles$5.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$5=aphrodite.StyleSheet.create({tablist:{display:"flex",gap:wonderBlocksTokens.sizing.size_240,borderBottom:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,paddingInline:wonderBlocksTokens.sizing.size_040}});
|
|
59
|
+
|
|
60
|
+
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=wonderBlocksCore.addStyle("div");const Tabs=React__namespace.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false,scrollableElementRef}=props;const focusedTabId=React__namespace.useRef(selectedTabId);const tabRefs=React__namespace.useRef({});const generatedUniqueId=React__namespace.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React__namespace.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React__namespace.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React__namespace.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React__namespace.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React__namespace.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&wonderBlocksCore.keys.right:case!isRtl&&wonderBlocksCore.keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&wonderBlocksCore.keys.left:case!isRtl&&wonderBlocksCore.keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case wonderBlocksCore.keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case wonderBlocksCore.keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case wonderBlocksCore.keys.enter:case wonderBlocksCore.keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React__namespace.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsxRuntime.jsx(React__namespace.Fragment,{})}return jsxRuntime.jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles$4.tabs,stylesProp?.root],children:[jsxRuntime.jsxs(StyledDiv,{ref:scrollableElementRef,style:styles$4.tablistWrapper,children:[jsxRuntime.jsx(Tablist,{"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:handleTablistBlur,id:tablistId,testId:testId&&`${testId}-tablist`,ref:tablistRef,style:stylesProp?.tablist,children:tabs.map(tab=>{const{id,label,panel:_,testId:tabTestId,icon,...otherProps}=tab;const tabProps={...otherProps,key:id,id:getTabId(id),testId:tabTestId&&getTabId(tabTestId),selected:id===selectedTabId,icon,"aria-controls":getTabPanelId(id),onClick:()=>{onTabSelected(id);},onKeyDown:handleKeyDown,ref:element=>{tabRefs.current[tab.id]=element;},style:stylesProp?.tab};if(typeof label==="function"){return label(tabProps)}return jsxRuntime.jsx(Tab,{...tabProps,children:label})})}),jsxRuntime.jsx("div",{...indicatorProps})]}),tabs.map(tab=>{return jsxRuntime.jsx(TabPanel,{id:getTabPanelId(tab.id),"aria-labelledby":getTabId(tab.id),active:selectedTabId===tab.id,testId:tab.testId&&getTabPanelId(tab.testId),style:stylesProp?.tabPanel,children:(mountAllPanels||visitedTabsRef.current.has(tab.id))&&tab.panel},tab.id)})]})});const styles$4=aphrodite.StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
|
|
61
|
+
|
|
62
|
+
const defaultLabels$1={defaultOpenerLabel:"Tabs"};const TabsDropdown=React__namespace.forwardRef((props,ref)=>{const{tabs,selectedTabId,onTabSelected,labels:labelsProp,opened,id:idProp,testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,styles:stylesProp}=props;const labels=React__namespace.useMemo(()=>{return {...defaultLabels$1,...labelsProp}},[labelsProp]);const generatedUniqueId=React__namespace.useId();const uniqueId=idProp??generatedUniqueId;const openerId=`${uniqueId}-opener`;const panelId=`${uniqueId}-panel`;const selectedTabItem=React__namespace.useMemo(()=>{return tabs.find(tab=>tab.id===selectedTabId)},[tabs,selectedTabId]);const processedTabs=React__namespace.useMemo(()=>{return tabs.map(tab=>({...tab,leftAccessory:tab.icon?React__namespace.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:()=>{onTabSelected(tab.id);}}))},[tabs,onTabSelected]);if(tabs.length===0){return jsxRuntime.jsx(React__namespace.Fragment,{})}const menuText=selectedTabItem?.label||labels.defaultOpenerLabel;return jsxRuntime.jsxs(wonderBlocksCore.View,{ref:ref,id:uniqueId,testId:testId,style:stylesProp?.root,role:"region","aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,children:[jsxRuntime.jsx(wonderBlocksDropdown.ActionMenu,{opened:opened,id:openerId,menuText:menuText,opener:()=>jsxRuntime.jsx(Button__default["default"],{testId:testId?`${testId}-opener`:undefined,kind:"tertiary",endIcon:caretDown__default["default"],style:[styles$3.opener,stylesProp?.opener],labelStyle:styles$3.labelStyle,"aria-label":selectedTabItem?.["aria-label"],startIcon:selectedTabItem?.icon,children:menuText}),style:[styles$3.actionMenu,stylesProp?.actionMenu],children:processedTabs.map(tab=>{return jsxRuntime.jsx(wonderBlocksDropdown.ActionItem,{label:tab.label,"aria-label":tab["aria-label"],onClick:tab.handleClick,active:tab.id===selectedTabId,testId:tab.testId,rightAccessory:tab.id===selectedTabId?jsxRuntime.jsx(wonderBlocksIcon.PhosphorIcon,{icon:checkCircleIcon__default["default"],size:"medium","aria-hidden":"true"}):undefined,leftAccessory:tab.leftAccessory},tab.id)})}),jsxRuntime.jsx(wonderBlocksCore.View,{id:panelId,role:"group","aria-labelledby":openerId,testId:testId?`${testId}-panel`:undefined,style:stylesProp?.tabPanel,children:selectedTabItem?.panel})]})});const styles$3=aphrodite.StyleSheet.create({actionMenu:{width:"100%",alignItems:"flex-start",borderBlockEnd:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`},opener:{position:"relative",height:"unset",paddingBlockStart:wonderBlocksTokens.sizing.size_120,paddingBlockEnd:wonderBlocksTokens.sizing.size_140,paddingInline:wonderBlocksTokens.sizing.size_180,width:"100%",justifyContent:"space-between",gap:wonderBlocksTokens.sizing.size_020},labelStyle:{flexGrow:1,maxWidth:"100%",textAlign:"start"}});
|
|
63
|
+
|
|
64
|
+
function useResponsiveLayout(options){const{tabs,elementWithOverflowRef,containerRef,onLayoutChange}=options;const[showDropdown,setShowDropdown]=React__namespace.useState(false);const tabsWidthRef=React__namespace.useRef(null);const tabsSignature=React__namespace.useMemo(()=>tabs.map(t=>`${t.id}:${t.label}-${t.icon?"with-icon":"without-icon"}`).join("|"),[tabs]);const checkOverflow=React__namespace.useCallback(()=>{const container=containerRef.current;if(!container){return}if(!showDropdown&&elementWithOverflowRef.current){const scrollableWrapper=elementWithOverflowRef.current;if(scrollableWrapper){const hasOverflow=scrollableWrapper.scrollWidth>scrollableWrapper.clientWidth;if(hasOverflow){tabsWidthRef.current=scrollableWrapper.scrollWidth;setShowDropdown(true);}}}else if(showDropdown&&tabsWidthRef.current){const containerWidth=container.clientWidth;if(containerWidth>=tabsWidthRef.current){setShowDropdown(false);}}},[showDropdown,elementWithOverflowRef,containerRef]);React__namespace.useEffect(()=>{if(showDropdown){tabsWidthRef.current=null;setShowDropdown(false);}else {checkOverflow();}},[tabsSignature]);React__namespace.useEffect(()=>{const container=containerRef.current;if(!container||!window.ResizeObserver){return}const resizeObserver=new ResizeObserver(()=>{checkOverflow();});resizeObserver.observe(container);return ()=>{resizeObserver.disconnect();}},[checkOverflow,containerRef]);React__namespace.useEffect(()=>{onLayoutChange?.(showDropdown?"dropdown":"tabs");},[showDropdown,onLayoutChange]);return {showDropdown}}
|
|
43
65
|
|
|
44
|
-
const
|
|
66
|
+
const ResponsiveTabs=props=>{const{tabs,selectedTabId,onTabSelected,onLayoutChange,id,testId,tabsProps,dropdownProps,styles:stylesProp,...ariaProps}=props;const tabsRef=React__namespace.useRef(null);const scrollableTabsRef=React__namespace.useRef(null);const containerRef=React__namespace.useRef(null);const{showDropdown}=useResponsiveLayout({tabs,elementWithOverflowRef:scrollableTabsRef,containerRef,onLayoutChange});return jsxRuntime.jsx(wonderBlocksCore.View,{ref:containerRef,style:[styles$2.container,stylesProp?.root],id:id,testId:testId,children:showDropdown?React.createElement(TabsDropdown,{...dropdownProps,...ariaProps,key:"dropdown",tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...dropdownProps?.styles,root:[styles$2.fadeIn,dropdownProps?.styles?.root]}}):React.createElement(Tabs,{...tabsProps,...ariaProps,key:"tabs",ref:tabsRef,scrollableElementRef:scrollableTabsRef,tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...tabsProps?.styles,root:[styles$2.fadeIn,tabsProps?.styles?.root]}})})};const fadeInKeyframes$1={from:{opacity:0},to:{opacity:1}};const styles$2=aphrodite.StyleSheet.create({fadeIn:{animationName:fadeInKeyframes$1,animationDuration:"150ms",animationTimingFunction:"ease-in-out"},container:{width:"100%",minHeight:"auto"}});
|
|
45
67
|
|
|
46
|
-
const
|
|
68
|
+
const defaultLabels={defaultOpenerLabel:"Tabs"};const NavigationTabsDropdown=React__namespace.forwardRef((props,ref)=>{const{tabs,selectedTabId,onTabSelected,id:idProp,testId,labels:labelsProp,opened,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag="nav",styles:stylesProp,showDivider=false}=props;const StyledTag=React__namespace.useMemo(()=>wonderBlocksCore.addStyle(tag),[tag]);const labels=React__namespace.useMemo(()=>{return {...defaultLabels,...labelsProp}},[labelsProp]);const selectedTabItem=React__namespace.useMemo(()=>{return tabs.find(tab=>tab.id===selectedTabId)},[tabs,selectedTabId]);const processedTabs=React__namespace.useMemo(()=>{return tabs.map(tab=>({...tab,leftAccessory:tab.icon?React__namespace.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:onTabSelected?()=>onTabSelected(tab.id):undefined}))},[tabs,onTabSelected]);const generatedUniqueId=React__namespace.useId();const uniqueId=idProp??generatedUniqueId;const openerId=`${uniqueId}-opener`;if(tabs.length===0){return jsxRuntime.jsx(React__namespace.Fragment,{})}const menuText=selectedTabItem?.label||labels.defaultOpenerLabel;return jsxRuntime.jsx(StyledTag,{ref:ref,id:uniqueId,"data-testid":testId,style:[showDivider&&styles$1.divider,stylesProp?.root],"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,children:jsxRuntime.jsx(wonderBlocksDropdown.ActionMenu,{opened:opened,id:openerId,menuText:menuText,opener:()=>jsxRuntime.jsx(Button__default["default"],{testId:testId?`${testId}-opener`:undefined,kind:"tertiary",endIcon:caretDown__default["default"],style:[styles$1.opener,stylesProp?.opener],labelStyle:styles$1.labelStyle,"aria-label":selectedTabItem?.["aria-label"],startIcon:selectedTabItem?.icon,children:menuText}),style:[styles$1.actionMenu,stylesProp?.actionMenu],children:processedTabs.map(tab=>{return jsxRuntime.jsx(wonderBlocksDropdown.ActionItem,{label:tab.label,href:tab.href,"aria-label":tab["aria-label"],active:tab.id===selectedTabId,testId:tab.testId,leftAccessory:tab.leftAccessory,onClick:tab.handleClick,rightAccessory:tab.id===selectedTabId?jsxRuntime.jsx(wonderBlocksIcon.PhosphorIcon,{icon:checkCircleIcon__default["default"],size:"medium","aria-hidden":"true"}):undefined},tab.id)})})})});const styles$1=aphrodite.StyleSheet.create({actionMenu:{width:"100%",alignItems:"flex-start"},divider:{borderBlockEnd:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`},opener:{position:"relative",height:"unset",paddingBlockStart:wonderBlocksTokens.sizing.size_120,paddingBlockEnd:wonderBlocksTokens.sizing.size_140,paddingInline:wonderBlocksTokens.sizing.size_180,width:"100%",justifyContent:"space-between",gap:wonderBlocksTokens.sizing.size_020},labelStyle:{flexGrow:1,maxWidth:"100%",textAlign:"start"}});
|
|
47
69
|
|
|
48
|
-
|
|
70
|
+
const ResponsiveNavigationTabs=props=>{const{tabs,selectedTabId,onTabSelected,onLayoutChange,id,testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag,tabsProps,dropdownProps,styles:stylesProp,showDivider=false}=props;const navigationTabsRef=React__namespace.useRef(null);const containerRef=React__namespace.useRef(null);const{showDropdown}=useResponsiveLayout({tabs,elementWithOverflowRef:navigationTabsRef,containerRef,onLayoutChange});const processedTabs=React__namespace.useMemo(()=>{return tabs.map(tab=>({...tab,startIcon:tab.icon?React__namespace.cloneElement(tab.icon,{size:tab.icon.props.size??"medium"}):undefined,handleClick:onTabSelected?()=>onTabSelected(tab.id):undefined}))},[tabs,onTabSelected]);return jsxRuntime.jsx(wonderBlocksCore.View,{ref:containerRef,style:[styles.container,stylesProp?.root],id:id,testId:testId,children:showDropdown?React.createElement(NavigationTabsDropdown,{...dropdownProps,key:"dropdown",tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag:tag,styles:{...dropdownProps?.styles,root:[styles.fadeIn,dropdownProps?.styles?.root]},showDivider:showDivider}):React.createElement(NavigationTabs,{...tabsProps,key:"tabs",ref:navigationTabsRef,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,tag:tag,styles:{...tabsProps?.styles,root:[styles.fadeIn,tabsProps?.styles?.root]},showDivider:showDivider},processedTabs.map(tab=>jsxRuntime.jsx(NavigationTabItem,{current:tab.id===selectedTabId,children:jsxRuntime.jsx(Link__default["default"],{href:tab.href,onClick:tab.handleClick,startIcon:tab.startIcon,"aria-label":tab["aria-label"],testId:tab.testId,children:tab.label})},tab.id)))})};const fadeInKeyframes={from:{opacity:0},to:{opacity:1}};const styles=aphrodite.StyleSheet.create({fadeIn:{animationName:fadeInKeyframes,animationDuration:"150ms",animationTimingFunction:"ease-in-out"},container:{width:"100%",minHeight:"auto"}});
|
|
49
71
|
|
|
50
72
|
exports.NavigationTabItem = NavigationTabItem;
|
|
51
73
|
exports.NavigationTabs = NavigationTabs;
|
|
74
|
+
exports.ResponsiveNavigationTabs = ResponsiveNavigationTabs;
|
|
75
|
+
exports.ResponsiveTabs = ResponsiveTabs;
|
|
52
76
|
exports.Tab = Tab;
|
|
53
77
|
exports.Tabs = Tabs;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Tabs are used to control what content is shown",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.5.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -20,9 +20,14 @@
|
|
|
20
20
|
"module": "dist/es/index.js",
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@phosphor-icons/core": "^2.0.2",
|
|
24
|
+
"@khanacademy/wonder-blocks-button": "11.3.3",
|
|
23
25
|
"@khanacademy/wonder-blocks-core": "12.4.3",
|
|
24
|
-
"@khanacademy/wonder-blocks-
|
|
25
|
-
"@khanacademy/wonder-blocks-
|
|
26
|
+
"@khanacademy/wonder-blocks-dropdown": "10.7.0",
|
|
27
|
+
"@khanacademy/wonder-blocks-icon": "5.3.7",
|
|
28
|
+
"@khanacademy/wonder-blocks-link": "10.1.4",
|
|
29
|
+
"@khanacademy/wonder-blocks-tokens": "15.0.0",
|
|
30
|
+
"@khanacademy/wonder-blocks-typography": "4.2.29"
|
|
26
31
|
},
|
|
27
32
|
"peerDependencies": {
|
|
28
33
|
"aphrodite": "^1.2.5",
|