@khanacademy/wonder-blocks-tabs 0.2.7 → 0.3.1
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 +48 -0
- package/dist/components/navigation-tab-item.d.ts +1 -1
- package/dist/components/tab-panel.d.ts +34 -0
- package/dist/components/tab.d.ts +43 -0
- package/dist/components/tablist.d.ts +39 -0
- package/dist/components/tabs.d.ts +149 -0
- package/dist/es/index.js +19 -6
- package/dist/hooks/use-tab-indicator.d.ts +37 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +17 -2
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-tabs
|
|
2
2
|
|
|
3
|
+
## 0.3.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [a1be4c5]
|
|
8
|
+
- Updated dependencies [d00a6f1]
|
|
9
|
+
- Updated dependencies [abf5496]
|
|
10
|
+
- Updated dependencies [812c167]
|
|
11
|
+
- @khanacademy/wonder-blocks-tokens@10.1.0
|
|
12
|
+
- @khanacademy/wonder-blocks-typography@3.2.2
|
|
13
|
+
|
|
14
|
+
## 0.3.0
|
|
15
|
+
|
|
16
|
+
### Minor Changes
|
|
17
|
+
|
|
18
|
+
- 28fa0c0: Tabs: Add keyboard navigation support and `activationMode` prop.
|
|
19
|
+
- 282fcee: Tabs:
|
|
20
|
+
|
|
21
|
+
- Add styling
|
|
22
|
+
- Add support for `styles` prop
|
|
23
|
+
- Add `animated` prop for enabling transition animations for the selected tab
|
|
24
|
+
|
|
25
|
+
NavigationTabs:
|
|
26
|
+
|
|
27
|
+
- Refactored current tab indicator logic to work with Tabs
|
|
28
|
+
|
|
29
|
+
- 26afba7: Tabs: Add `mountAllPanels` prop. Defaults to `false` so tab panels are only rendered if they are visited
|
|
30
|
+
- 6327e23: Adds Tabs component
|
|
31
|
+
- e44197b: Tabs
|
|
32
|
+
|
|
33
|
+
- Added `id` and `testId` props
|
|
34
|
+
- Cache previously visited tab panels. This is so tabs are only mounted when
|
|
35
|
+
they are selected the first time. Re-visiting a tab doesn't re-mount the panel
|
|
36
|
+
- Support ARIA props in items in the `tabs` prop
|
|
37
|
+
- Support a render function for a tab's label in the `tabs` prop
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- 5acef63: Tabs: Update border token usage
|
|
42
|
+
- 3be8b60: Tabs: Allow keydown events to pass when `Enter` or `Space` is pressed. This is so that a tab could trigger a Popover to open
|
|
43
|
+
- db0c716: Tabs: Fix keyboard navigation for right-to-left
|
|
44
|
+
- 5acef63: Tabs and NavigationTabs: Use inline styles for tab indicator
|
|
45
|
+
- 5acef63: NavigationTabs: Make sure the current tab styles don't change when interacted with
|
|
46
|
+
- Updated dependencies [28fa0c0]
|
|
47
|
+
- Updated dependencies [28fa0c0]
|
|
48
|
+
- @khanacademy/wonder-blocks-core@12.3.0
|
|
49
|
+
- @khanacademy/wonder-blocks-typography@3.2.1
|
|
50
|
+
|
|
3
51
|
## 0.2.7
|
|
4
52
|
|
|
5
53
|
### Patch Changes
|
|
@@ -35,7 +35,7 @@ export declare const NavigationTabItem: React.ForwardRefExoticComponent<Readonly
|
|
|
35
35
|
* cases where the `Link` component is wrapped by another component (like a
|
|
36
36
|
* `Tooltip` or `Popover`), a render function can be used instead. The
|
|
37
37
|
* render function provides the Link props that should be applied to the
|
|
38
|
-
* Link component.
|
|
38
|
+
* Link component.
|
|
39
39
|
*/
|
|
40
40
|
children: React.ReactElement | ((linkProps: NavigationTabItemLinkProps) => React.ReactElement);
|
|
41
41
|
/**
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
type Props = {
|
|
4
|
+
/**
|
|
5
|
+
* The contents of the tab panel.
|
|
6
|
+
*/
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/**
|
|
9
|
+
* A unique id for the tab panel.
|
|
10
|
+
*/
|
|
11
|
+
id: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional test ID for e2e testing.
|
|
14
|
+
*/
|
|
15
|
+
testId?: string;
|
|
16
|
+
/**
|
|
17
|
+
* The id of the associated element with role="tab".
|
|
18
|
+
*/
|
|
19
|
+
"aria-labelledby": string;
|
|
20
|
+
/**
|
|
21
|
+
* Whether the tab panel is active.
|
|
22
|
+
*/
|
|
23
|
+
active?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Custom styles for the `TabPanel` component.
|
|
26
|
+
*/
|
|
27
|
+
style?: StyleType;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A component that has `role="tabpanel"` and is used to represent a tab panel
|
|
31
|
+
* in a tabbed interface.
|
|
32
|
+
*/
|
|
33
|
+
export declare const TabPanel: (props: Props) => React.JSX.Element;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
/**
|
|
4
|
+
* A component that has `role="tab"` and is used to represent a tab in a tabbed
|
|
5
|
+
* interface.
|
|
6
|
+
*/
|
|
7
|
+
export declare const Tab: React.ForwardRefExoticComponent<Readonly<import("@khanacademy/wonder-blocks-core").AriaAttributes> & Readonly<{
|
|
8
|
+
role?: import("@khanacademy/wonder-blocks-core").AriaRole;
|
|
9
|
+
}> & {
|
|
10
|
+
/**
|
|
11
|
+
* The contents of the tab label.
|
|
12
|
+
*/
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* Called when the tab is clicked.
|
|
16
|
+
*/
|
|
17
|
+
onClick?: (event: React.MouseEvent) => unknown;
|
|
18
|
+
/**
|
|
19
|
+
* A unique id for the tab.
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
/**
|
|
23
|
+
* Optional test ID for e2e testing.
|
|
24
|
+
*/
|
|
25
|
+
testId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The id of the panel that the tab controls.
|
|
28
|
+
*/
|
|
29
|
+
"aria-controls": string;
|
|
30
|
+
/**
|
|
31
|
+
* If the tab is currently selected.
|
|
32
|
+
*/
|
|
33
|
+
selected?: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Called when a key is pressed on the tab.
|
|
36
|
+
*/
|
|
37
|
+
onKeyDown?: (event: React.KeyboardEvent<HTMLButtonElement>) => void;
|
|
38
|
+
/**
|
|
39
|
+
* Custom styles for the `Tab` component.
|
|
40
|
+
*/
|
|
41
|
+
style?: StyleType;
|
|
42
|
+
} & React.RefAttributes<HTMLButtonElement>>;
|
|
43
|
+
export declare const styles: import("aphrodite").StyleDeclaration;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
type Props = {
|
|
4
|
+
/**
|
|
5
|
+
* The id of the tablist.
|
|
6
|
+
*/
|
|
7
|
+
id?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Optional test ID for e2e testing.
|
|
10
|
+
*/
|
|
11
|
+
testId?: string;
|
|
12
|
+
/**
|
|
13
|
+
* The contents of the tablist.
|
|
14
|
+
*/
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
/**
|
|
17
|
+
* If there is no visible label for the tablist, set aria-label to a
|
|
18
|
+
* label describing the tablist.
|
|
19
|
+
*/
|
|
20
|
+
"aria-label"?: string;
|
|
21
|
+
/**
|
|
22
|
+
* If the tablist has a visible label, set aria-labelledby to a value
|
|
23
|
+
* that refers to the labelling element.
|
|
24
|
+
*/
|
|
25
|
+
"aria-labelledby"?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Called when focus moves out of the tablist.
|
|
28
|
+
*/
|
|
29
|
+
onBlur?: (event: React.FocusEvent<HTMLDivElement>) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Custom styles for the `Tablist` component.
|
|
32
|
+
*/
|
|
33
|
+
style?: StyleType;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* A component that has `role="tablist"` and is used to group tab elements.
|
|
37
|
+
*/
|
|
38
|
+
export declare const Tablist: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { AriaProps, PropsFor, StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
+
import { Tab } from "./tab";
|
|
4
|
+
export type TabRenderProps = Omit<PropsFor<typeof Tab>, "children">;
|
|
5
|
+
export type TabItem = AriaProps & {
|
|
6
|
+
/**
|
|
7
|
+
* A unique id for the tab.
|
|
8
|
+
*
|
|
9
|
+
* Here is how the id is used for the different elements in the component:
|
|
10
|
+
* - The tab will have an id formatted as `${id}-tab`
|
|
11
|
+
* - The associated tab panel will have an id formatted as `${id}-panel`
|
|
12
|
+
*
|
|
13
|
+
* It is also used to communicate the id of the selected tab via the
|
|
14
|
+
* `selectedTabId` and `onTabSelected` props.
|
|
15
|
+
*/
|
|
16
|
+
id: string;
|
|
17
|
+
/**
|
|
18
|
+
* The contents of the tab label.
|
|
19
|
+
*
|
|
20
|
+
* For specific use cases where the underlying tab element is wrapped
|
|
21
|
+
* by another component (like a `Tooltip` or `Popover`), a render function
|
|
22
|
+
* can be used with the `Tab` component instead. The render function
|
|
23
|
+
* provides the tab props that should be applied to the `Tab` component.
|
|
24
|
+
*/
|
|
25
|
+
label: React.ReactNode | ((tabProps: TabRenderProps) => React.ReactElement);
|
|
26
|
+
/**
|
|
27
|
+
* The contents of the panel associated with the tab.
|
|
28
|
+
*/
|
|
29
|
+
panel: React.ReactNode;
|
|
30
|
+
/**
|
|
31
|
+
* Optional test ID for e2e testing.
|
|
32
|
+
*
|
|
33
|
+
* Here is how the test id is used for the different elements in the component:
|
|
34
|
+
* - The tab will have a testId formatted as `${testId}-tab`
|
|
35
|
+
* - The associated tab panel will have a testId formatted as `${testId}-panel`
|
|
36
|
+
*/
|
|
37
|
+
testId?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Type to help ensure aria-label or aria-labelledby is set.
|
|
41
|
+
*/
|
|
42
|
+
type AriaLabelOrAriaLabelledby = {
|
|
43
|
+
/**
|
|
44
|
+
* If there is no visible label for the tabs, set aria-label to a
|
|
45
|
+
* label describing the tabs.
|
|
46
|
+
*/
|
|
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;
|
|
56
|
+
};
|
|
57
|
+
type Props = {
|
|
58
|
+
/**
|
|
59
|
+
* A unique id to use as the base of the ids for the elements within the
|
|
60
|
+
* component. If the `id` prop is not provided, a base unique id will be
|
|
61
|
+
* auto-generated.
|
|
62
|
+
*
|
|
63
|
+
* Here is how the id is used for the different elements in the component:
|
|
64
|
+
* - The root will have an id of `${id}`
|
|
65
|
+
* - The tablist will have an id formatted as ${id}-tablist
|
|
66
|
+
*
|
|
67
|
+
* If you need to apply an id to a specific tab or tab panel, the `id` for
|
|
68
|
+
* the tab item in the `tabs` prop will be used:
|
|
69
|
+
* - The tab will have an id formatted as `${id}-tab`
|
|
70
|
+
* - The associated tab panel will have an id formatted as `${id}-panel`
|
|
71
|
+
*/
|
|
72
|
+
id?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Optional test ID for e2e testing. Here is how the test id is used for the
|
|
75
|
+
* different elements in the component:
|
|
76
|
+
* - The root will have a testId formatted as `${testId}`
|
|
77
|
+
* - The tablist will have a testId formatted as `${testId}-tablist`
|
|
78
|
+
*
|
|
79
|
+
* If you need to apply a testId to a specific tab or tab panel, add the
|
|
80
|
+
* test id to the tab item in the `tabs` prop:
|
|
81
|
+
* - The tab will have a testId formatted as `${testId}-tab`
|
|
82
|
+
* - The associated tab panel will have a testId formatted as `${testId}-panel`
|
|
83
|
+
*/
|
|
84
|
+
testId?: string;
|
|
85
|
+
/**
|
|
86
|
+
* The tabs to render. The Tabs component will wire up the tab and panel
|
|
87
|
+
* attributes for accessibility.
|
|
88
|
+
*/
|
|
89
|
+
tabs: Array<TabItem>;
|
|
90
|
+
/**
|
|
91
|
+
* The id of the tab that is selected.
|
|
92
|
+
*/
|
|
93
|
+
selectedTabId: string;
|
|
94
|
+
/**
|
|
95
|
+
* Called when a tab is selected.
|
|
96
|
+
*/
|
|
97
|
+
onTabSelected: (id: string) => unknown;
|
|
98
|
+
/**
|
|
99
|
+
* The mode of activation for the tabs for keyboard navigation. Defaults to
|
|
100
|
+
* `manual`.
|
|
101
|
+
*
|
|
102
|
+
* - If `manual`, the tab will only be activated when a tab receives focus
|
|
103
|
+
* and is selected by pressing `Space` or `Enter`.
|
|
104
|
+
* - If `automatic`, the tab will be activated once a tab receives focus.
|
|
105
|
+
*/
|
|
106
|
+
activationMode?: "manual" | "automatic";
|
|
107
|
+
/**
|
|
108
|
+
* Whether to include animation in the `Tabs` component. This should be
|
|
109
|
+
* false if the user has `prefers-reduced-motion` opted in. Defaults to
|
|
110
|
+
* `false`.
|
|
111
|
+
*/
|
|
112
|
+
animated?: boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Custom styles for the `Tabs` component.
|
|
115
|
+
* - `root`: Styles the root `div` element.
|
|
116
|
+
* - `tablist`: Styles the `tablist` element.
|
|
117
|
+
* - `tab`: Styles all `tab` elements.
|
|
118
|
+
* - `tabPanel`: Styles all the `tabpanel` elements.
|
|
119
|
+
*
|
|
120
|
+
* If styles need to be applied to specific tab or tab panel elements,
|
|
121
|
+
* consider setting the styles on the `label` and `panel` content for the
|
|
122
|
+
* `tabs` prop.
|
|
123
|
+
*/
|
|
124
|
+
styles?: {
|
|
125
|
+
root?: StyleType;
|
|
126
|
+
tablist?: StyleType;
|
|
127
|
+
tab?: StyleType;
|
|
128
|
+
tabPanel?: StyleType;
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Whether to mount all tab panels when the component mounts.
|
|
132
|
+
*
|
|
133
|
+
* - When enabled, all tab panels are in the DOM. This is useful if the
|
|
134
|
+
* tab contents should be crawlable for SEO purposes.
|
|
135
|
+
* - When disabled, tab panels are only in the DOM if they've been visited.
|
|
136
|
+
* This is useful for performance so that unvisited panels are not mounted.
|
|
137
|
+
*
|
|
138
|
+
* Defaults to `false`.
|
|
139
|
+
*/
|
|
140
|
+
mountAllPanels?: boolean;
|
|
141
|
+
} & AriaLabelOrAriaLabelledby;
|
|
142
|
+
/**
|
|
143
|
+
* A component that uses a tabbed interface to control a specific view. The
|
|
144
|
+
* tabs have `role=”tab”` and keyboard users can change tabs using arrow keys.
|
|
145
|
+
* For a tabbed interface where the tabs are links, see the NavigationTabs
|
|
146
|
+
* component.
|
|
147
|
+
*/
|
|
148
|
+
export declare const Tabs: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
|
|
149
|
+
export {};
|
package/dist/es/index.js
CHANGED
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { useOnMountEffect, addStyle, keys } from '@khanacademy/wonder-blocks-core';
|
|
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$
|
|
6
|
+
import { styles as styles$7 } from '@khanacademy/wonder-blocks-typography';
|
|
7
|
+
import { focusStyles } from '@khanacademy/wonder-blocks-styles';
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
+
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){return}const observer=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});observer.observe(tabsContainerRef.current);return ()=>{observer.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,updateUnderlineStyle}};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"}};
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
+
const StyledNav=addStyle("nav");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,...otherProps}=props;const listRef=React.useRef(null);const isTabActive=React.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps,updateUnderlineStyle}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});React.useEffect(()=>{updateUnderlineStyle();},[children,updateUnderlineStyle]);return jsx(StyledNav,{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"}});
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
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.action.secondary.progressive.hover.foreground}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${sizing.size_060}*-1) 0 0 ${semanticColor.action.secondary.progressive.press.foreground}`},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.action.secondary.progressive.default.foreground,[":active:not([aria-disabled=true])"]:{color:semanticColor.action.secondary.progressive.default.foreground}},link:{display:"flex",margin:0,color:semanticColor.text.primary,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.action.secondary.progressive.default.foreground,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.action.secondary.progressive.press.foreground},":focus-visible":{color:semanticColor.action.secondary.progressive.default.foreground,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}}});
|
|
14
|
+
|
|
15
|
+
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
16
|
+
|
|
17
|
+
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(false);React.useEffect(()=>{if(ref.current){setHasFocusableElement(findFocusableNodes(ref.current).length>0);}},[active,ref,children]);return jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement?undefined:0,hidden:!active,"data-testid":testId,style:active&&[styles$3.tabPanel,style],children:children})};const styles$3=StyleSheet.create({tabPanel:{display:"flex"}});
|
|
18
|
+
|
|
19
|
+
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,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",...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:semanticColor.action.secondary.progressive.hover.foreground,[":after"]:{height:border.width.thin,backgroundColor:semanticColor.action.secondary.progressive.hover.foreground}},[":active:not([aria-selected='true'])"]:{color:semanticColor.action.secondary.progressive.press.foreground,[":after"]:{height:border.width.thick,backgroundColor:semanticColor.action.secondary.progressive.press.foreground}}},selectedTab:{color:semanticColor.action.secondary.progressive.default.foreground}});
|
|
20
|
+
|
|
21
|
+
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.border.subtle}`,paddingInline:sizing.size_040}});
|
|
22
|
+
|
|
23
|
+
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,updateUnderlineStyle}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);React.useEffect(()=>{updateUnderlineStyle();},[updateUnderlineStyle,tabs,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"}});
|
|
24
|
+
|
|
25
|
+
export { NavigationTabItem, NavigationTabs, Tab, Tabs };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { AriaRole } from "@khanacademy/wonder-blocks-core";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
type IndicatorProps = {
|
|
4
|
+
/**
|
|
5
|
+
* Inline styles for the indicator.
|
|
6
|
+
*/
|
|
7
|
+
style: React.CSSProperties;
|
|
8
|
+
role: AriaRole;
|
|
9
|
+
};
|
|
10
|
+
type Props = {
|
|
11
|
+
/**
|
|
12
|
+
* Whether to include animation.
|
|
13
|
+
*/
|
|
14
|
+
animated: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Ref for the container of the tabs so we can observe when the size of the
|
|
17
|
+
* children changes. This is necessary to update the underline position.
|
|
18
|
+
*/
|
|
19
|
+
tabsContainerRef: React.RefObject<HTMLElement>;
|
|
20
|
+
/**
|
|
21
|
+
* Function that determines if a tab is active. The `childElement` argument
|
|
22
|
+
* is the child element of the `tabsContainerRef` prop
|
|
23
|
+
*/
|
|
24
|
+
isTabActive(childElement: Element): boolean;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A hook that is used to manage the underline current indicator for tabs.
|
|
28
|
+
* It returns:
|
|
29
|
+
* - `indicatorProps`: The props to apply to the underline current indicator
|
|
30
|
+
* - `updateUnderlineStyle`: A function that updates the underline style. Use
|
|
31
|
+
* this function when the component detects a change in the tabs
|
|
32
|
+
*/
|
|
33
|
+
export declare const useTabIndicator: (props: Props) => {
|
|
34
|
+
indicatorProps: IndicatorProps;
|
|
35
|
+
updateUnderlineStyle: () => void;
|
|
36
|
+
};
|
|
37
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { NavigationTabs } from "./components/navigation-tabs";
|
|
2
2
|
export { NavigationTabItem } from "./components/navigation-tab-item";
|
|
3
|
+
export { Tabs } from "./components/tabs";
|
|
4
|
+
export type { TabItem, TabRenderProps } from "./components/tabs";
|
|
5
|
+
export { Tab } from "./components/tab";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ 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 wonderBlocksStyles = require('@khanacademy/wonder-blocks-styles');
|
|
11
12
|
|
|
12
13
|
function _interopNamespace(e) {
|
|
13
14
|
if (e && e.__esModule) return e;
|
|
@@ -29,9 +30,23 @@ function _interopNamespace(e) {
|
|
|
29
30
|
|
|
30
31
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
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){return}const observer=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});observer.observe(tabsContainerRef.current);return ()=>{observer.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,updateUnderlineStyle}};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"}};
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
+
const StyledNav=wonderBlocksCore.addStyle("nav");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,...otherProps}=props;const listRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps,updateUnderlineStyle}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});React__namespace.useEffect(()=>{updateUnderlineStyle();},[children,updateUnderlineStyle]);return jsxRuntime.jsx(StyledNav,{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"}});
|
|
36
|
+
|
|
37
|
+
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.action.secondary.progressive.hover.foreground}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${wonderBlocksTokens.sizing.size_060}*-1) 0 0 ${wonderBlocksTokens.semanticColor.action.secondary.progressive.press.foreground}`},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.action.secondary.progressive.default.foreground,[":active:not([aria-disabled=true])"]:{color:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}},link:{display:"flex",margin:0,color:wonderBlocksTokens.semanticColor.text.primary,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.action.secondary.progressive.press.foreground},":focus-visible":{color:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground,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}}});
|
|
38
|
+
|
|
39
|
+
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
40
|
+
|
|
41
|
+
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(false);React__namespace.useEffect(()=>{if(ref.current){setHasFocusableElement(findFocusableNodes(ref.current).length>0);}},[active,ref,children]);return jsxRuntime.jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement?undefined:0,hidden:!active,"data-testid":testId,style:active&&[styles$3.tabPanel,style],children:children})};const styles$3=aphrodite.StyleSheet.create({tabPanel:{display:"flex"}});
|
|
42
|
+
|
|
43
|
+
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,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",...wonderBlocksStyles.focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.action.secondary.progressive.hover.foreground,[":after"]:{height:wonderBlocksTokens.border.width.thin,backgroundColor:wonderBlocksTokens.semanticColor.action.secondary.progressive.hover.foreground}},[":active:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.action.secondary.progressive.press.foreground,[":after"]:{height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.action.secondary.progressive.press.foreground}}},selectedTab:{color:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground}});
|
|
44
|
+
|
|
45
|
+
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.border.subtle}`,paddingInline:wonderBlocksTokens.sizing.size_040}});
|
|
46
|
+
|
|
47
|
+
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,updateUnderlineStyle}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React__namespace.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);React__namespace.useEffect(()=>{updateUnderlineStyle();},[updateUnderlineStyle,tabs,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"}});
|
|
35
48
|
|
|
36
49
|
exports.NavigationTabItem = NavigationTabItem;
|
|
37
50
|
exports.NavigationTabs = NavigationTabs;
|
|
51
|
+
exports.Tab = Tab;
|
|
52
|
+
exports.Tabs = Tabs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@khanacademy/wonder-blocks-tabs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"design": "v1",
|
|
5
5
|
"description": "Tabs are used to control what content is shown",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@khanacademy/wonder-blocks-core": "12.
|
|
16
|
-
"@khanacademy/wonder-blocks-tokens": "10.
|
|
17
|
-
"@khanacademy/wonder-blocks-typography": "3.2.
|
|
15
|
+
"@khanacademy/wonder-blocks-core": "12.3.0",
|
|
16
|
+
"@khanacademy/wonder-blocks-tokens": "10.1.0",
|
|
17
|
+
"@khanacademy/wonder-blocks-typography": "3.2.2"
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"aphrodite": "^1.2.5",
|