@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 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. See example in the docs for more details.
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 { addStyle, useOnMountEffect, View } from '@khanacademy/wonder-blocks-core';
3
- import { sizing, semanticColor, breakpoint, border } from '@khanacademy/wonder-blocks-tokens';
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$2 } from '@khanacademy/wonder-blocks-typography';
6
+ import { styles as styles$7 } from '@khanacademy/wonder-blocks-typography';
7
+ import { focusStyles } from '@khanacademy/wonder-blocks-styles';
7
8
 
8
- const StyledNav=addStyle("nav");const StyledUl=addStyle("ul");const StyledDiv=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 indicatorIsReady=React.useRef(false);const[underlineStyle,setUnderlineStyle]=React.useState({left:0,width:0});const updateUnderlineStyle=React.useCallback(()=>{if(!listRef.current){return}const activeTab=Array.from(listRef.current.children).find(child=>child.children[0]?.ariaCurrent);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=listRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/listRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,listRef]);useOnMountEffect(()=>{if(!listRef.current||!window.ResizeObserver){return}const observer=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});observer.observe(listRef.current);return ()=>{observer.disconnect();}});React.useEffect(()=>{updateUnderlineStyle();},[children,updateUnderlineStyle]);return jsx(StyledNav,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$1.nav,stylesProp?.root],...otherProps,children:jsxs(StyledDiv,{style:styles$1.contents,children:[jsx(StyledUl,{style:[styles$1.list,stylesProp?.list],ref:listRef,children:children}),indicatorIsReady.current&&jsx(View,{style:[{transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`},styles$1.currentUnderline,animated&&styles$1.underlineTransition],role:"presentation"})]})})});const styles$1=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"},currentUnderline:{position:"absolute",bottom:0,left:0,height:sizing.size_040,backgroundColor:semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}});
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 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$2.Body,styles.link,current&&styles.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.root,current&&styles.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles=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"}},currentLink:{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}}});
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
- export { NavigationTabItem, NavigationTabs };
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 StyledNav=wonderBlocksCore.addStyle("nav");const StyledUl=wonderBlocksCore.addStyle("ul");const StyledDiv=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 indicatorIsReady=React__namespace.useRef(false);const[underlineStyle,setUnderlineStyle]=React__namespace.useState({left:0,width:0});const updateUnderlineStyle=React__namespace.useCallback(()=>{if(!listRef.current){return}const activeTab=Array.from(listRef.current.children).find(child=>child.children[0]?.ariaCurrent);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=listRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/listRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,listRef]);wonderBlocksCore.useOnMountEffect(()=>{if(!listRef.current||!window.ResizeObserver){return}const observer=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});observer.observe(listRef.current);return ()=>{observer.disconnect();}});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$1.nav,stylesProp?.root],...otherProps,children:jsxRuntime.jsxs(StyledDiv,{style:styles$1.contents,children:[jsxRuntime.jsx(StyledUl,{style:[styles$1.list,stylesProp?.list],ref:listRef,children:children}),indicatorIsReady.current&&jsxRuntime.jsx(wonderBlocksCore.View,{style:[{transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`},styles$1.currentUnderline,animated&&styles$1.underlineTransition],role:"presentation"})]})})});const styles$1=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"},currentUnderline:{position:"absolute",bottom:0,left:0,height:wonderBlocksTokens.sizing.size_040,backgroundColor:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}});
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 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.link,current&&styles.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.root,current&&styles.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles=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"}},currentLink:{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}}});
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.2.7",
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.2.1",
16
- "@khanacademy/wonder-blocks-tokens": "10.0.0",
17
- "@khanacademy/wonder-blocks-typography": "3.2.0"
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",