@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 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 {};
@@ -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
- * If there is no visible label for the tabs, set aria-label to a
45
- * label describing the tabs.
40
+ * Optional icon to display in the tab. Should be a PhosphorIcon or Icon component.
46
41
  */
47
- "aria-label": string;
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 Props = {
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<Props & React.RefAttributes<HTMLDivElement>>;
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 { styles as styles$7 } from '@khanacademy/wonder-blocks-typography';
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$6.currentUnderline,...positioningStyle,...animated?styles$6.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$6={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"}};
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$5.nav,stylesProp?.root],...otherProps,children:jsxs(StyledDiv$3,{style:styles$5.contents,children:[jsx(StyledUl,{style:[styles$5.list,stylesProp?.list],ref:listRef,children:children}),jsx("div",{...indicatorProps})]})})});const styles$5=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"}});
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$7.Body,styles$4.link,current&&styles$4.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$4.root,current&&styles$4.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$4=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}}});
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$3.tabPanel,style],children:children})};const styles$3=StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
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 jsx(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$7.Body,styles$2.tab,selected&&styles$2.selectedTab,style],children:children})});const bottomSpacing=sizing.size_140;const styles$2=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,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}});
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$1.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$1=StyleSheet.create({tablist:{display:"flex",gap:sizing.size_240,borderBottom:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`,paddingInline:sizing.size_040}});
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
- export { NavigationTabItem, NavigationTabs, Tab, Tabs };
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$6.currentUnderline,...positioningStyle,...animated?styles$6.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$6={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"}};
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$5.nav,stylesProp?.root],...otherProps,children:jsxRuntime.jsxs(StyledDiv$3,{style:styles$5.contents,children:[jsxRuntime.jsx(StyledUl,{style:[styles$5.list,stylesProp?.list],ref:listRef,children:children}),jsxRuntime.jsx("div",{...indicatorProps})]})})});const styles$5=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"}});
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$4.link,current&&styles$4.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$4.root,current&&styles$4.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$4=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}}});
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$3.tabPanel,style],children:children})};const styles$3=aphrodite.StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
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 StyledButton=wonderBlocksCore.addStyle("button");const Tab=React__namespace.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return jsxRuntime.jsx(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$2.tab,selected&&styles$2.selectedTab,style],children:children})});const bottomSpacing=wonderBlocksTokens.sizing.size_140;const styles$2=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,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}});
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 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$1.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$1=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}});
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
- 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}=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.tabs,stylesProp?.root],children:[jsxRuntime.jsxs(StyledDiv,{style:styles.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,...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 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=aphrodite.StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
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.4.2",
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-tokens": "14.1.3",
25
- "@khanacademy/wonder-blocks-typography": "4.2.28"
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",