@khanacademy/wonder-blocks-tabs 0.0.0-PR2913-20251219224115 → 0.0.0-PR2921-20260107172502
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 +4 -7
- package/dist/components/tabs.d.ts +21 -6
- package/dist/es/index.js +10 -20
- package/dist/index.d.ts +0 -2
- package/dist/index.js +7 -22
- package/package.json +3 -7
- package/dist/components/responsive-tabs.d.ts +0 -101
- package/dist/components/tabs-dropdown.d.ts +0 -85
- package/dist/components/types.d.ts +0 -18
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# @khanacademy/wonder-blocks-tabs
|
|
2
2
|
|
|
3
|
-
## 0.0.0-
|
|
4
|
-
|
|
5
|
-
### Minor Changes
|
|
6
|
-
|
|
7
|
-
- 27a30ad: Adds TabsDropdown component
|
|
8
|
-
- 11cf421: Adds ResponsiveTabs component
|
|
3
|
+
## 0.0.0-PR2921-20260107172502
|
|
9
4
|
|
|
10
5
|
### Patch Changes
|
|
11
6
|
|
|
12
|
-
-
|
|
7
|
+
- Updated dependencies [8a3ee1f]
|
|
8
|
+
- @khanacademy/wonder-blocks-typography@0.0.0-PR2921-20260107172502
|
|
9
|
+
- @khanacademy/wonder-blocks-tokens@0.0.0-PR2921-20260107172502
|
|
13
10
|
|
|
14
11
|
## 0.4.1
|
|
15
12
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { AriaProps, PropsFor, StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
3
|
import { Tab } from "./tab";
|
|
4
|
-
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
5
4
|
export type TabRenderProps = Omit<PropsFor<typeof Tab>, "children">;
|
|
6
5
|
export type TabItem = AriaProps & {
|
|
7
6
|
/**
|
|
@@ -37,7 +36,25 @@ export type TabItem = AriaProps & {
|
|
|
37
36
|
*/
|
|
38
37
|
testId?: string;
|
|
39
38
|
};
|
|
40
|
-
|
|
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 = {
|
|
41
58
|
/**
|
|
42
59
|
* A unique id to use as the base of the ids for the elements within the
|
|
43
60
|
* component. If the `id` prop is not provided, a base unique id will be
|
|
@@ -127,8 +144,6 @@ export type TabsProps = {
|
|
|
127
144
|
* tabs have `role=”tab”` and keyboard users can change tabs using arrow keys.
|
|
128
145
|
* For a tabbed interface where the tabs are links, see the NavigationTabs
|
|
129
146
|
* component.
|
|
130
|
-
*
|
|
131
|
-
* For responsive cases where the tabs should switch to a dropdown when there is
|
|
132
|
-
* not enough horizontal space, use the `ResponsiveTabs` component.
|
|
133
147
|
*/
|
|
134
|
-
export declare const Tabs: React.ForwardRefExoticComponent<
|
|
148
|
+
export declare const Tabs: React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLDivElement>>;
|
|
149
|
+
export {};
|
package/dist/es/index.js
CHANGED
|
@@ -1,36 +1,26 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
|
-
import { useOnMountEffect, addStyle, keys
|
|
2
|
+
import { useOnMountEffect, addStyle, keys } from '@khanacademy/wonder-blocks-core';
|
|
3
3
|
import { border, semanticColor, sizing, breakpoint } from '@khanacademy/wonder-blocks-tokens';
|
|
4
4
|
import { StyleSheet } from 'aphrodite';
|
|
5
5
|
import * as React from 'react';
|
|
6
|
-
import {
|
|
7
|
-
import { styles as styles$9 } from '@khanacademy/wonder-blocks-typography';
|
|
8
|
-
import { ActionMenu, ActionItem } from '@khanacademy/wonder-blocks-dropdown';
|
|
9
|
-
import Button from '@khanacademy/wonder-blocks-button';
|
|
10
|
-
import caretDown from '@phosphor-icons/core/bold/caret-down-bold.svg';
|
|
11
|
-
import checkCircleIcon from '@phosphor-icons/core/fill/check-circle-fill.svg';
|
|
12
|
-
import { PhosphorIcon } from '@khanacademy/wonder-blocks-icon';
|
|
6
|
+
import { styles as styles$7 } from '@khanacademy/wonder-blocks-typography';
|
|
13
7
|
|
|
14
|
-
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React.useRef(false);const[underlineStyle,setUnderlineStyle]=React.useState({left:0,width:0});const updateUnderlineStyle=React.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$
|
|
8
|
+
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React.useRef(false);const[underlineStyle,setUnderlineStyle]=React.useState({left:0,width:0});const updateUnderlineStyle=React.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$6.currentUnderline,...positioningStyle,...animated?styles$6.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$6={currentUnderline:{position:"absolute",bottom:0,left:0,height:border.width.thick,backgroundColor:semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}};
|
|
15
9
|
|
|
16
|
-
const StyledUl=addStyle("ul");const StyledDiv$3=addStyle("div");const NavigationTabs=React.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React.useMemo(()=>addStyle(tag),[tag]);const listRef=React.useRef(null);const isTabActive=React.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$
|
|
10
|
+
const StyledUl=addStyle("ul");const StyledDiv$3=addStyle("div");const NavigationTabs=React.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React.useMemo(()=>addStyle(tag),[tag]);const listRef=React.useRef(null);const isTabActive=React.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$5.nav,stylesProp?.root],...otherProps,children:jsxs(StyledDiv$3,{style:styles$5.contents,children:[jsx(StyledUl,{style:[styles$5.list,stylesProp?.list],ref:listRef,children:children}),jsx("div",{...indicatorProps})]})})});const styles$5=StyleSheet.create({nav:{overflowX:"auto"},contents:{position:"relative"},list:{paddingInline:sizing.size_040,paddingBlock:sizing.size_0,margin:sizing.size_0,display:"flex",gap:sizing.size_160,flexWrap:"nowrap"}});
|
|
17
11
|
|
|
18
|
-
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$
|
|
12
|
+
const StyledLi=addStyle("li");const NavigationTabItem=React.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[styles$7.Body,styles$4.link,current&&styles$4.currentLink],"aria-current":current?"page":undefined};if(typeof children==="function"){return children(linkProps)}return React.cloneElement(children,linkProps)}return jsx(StyledLi,{id:id,"data-testid":testId,style:[styles$4.root,current&&styles$4.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$4=StyleSheet.create({root:{listStyle:"none",display:"inline-flex",[":has(a:hover)"]:{boxShadow:`inset 0 calc(${sizing.size_020}*-1) 0 0 ${semanticColor.core.border.instructive.default}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${sizing.size_060}*-1) 0 0 ${semanticColor.core.border.instructive.strong}`},paddingBlockStart:sizing.size_080,paddingBlockEnd:sizing.size_180,[breakpoint.mediaQuery.mdOrLarger]:{paddingBlockStart:sizing.size_200,paddingBlockEnd:sizing.size_240}},current:{[":has(a:hover)"]:{boxShadow:"none"},[":has(a:active):not([aria-disabled=true])"]:{boxShadow:"none"}},currentLink:{color:semanticColor.link.rest,[":active:not([aria-disabled=true])"]:{color:semanticColor.link.rest}},link:{display:"flex",margin:0,color:semanticColor.core.foreground.neutral.subtle,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.link.hover,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:semanticColor.link.press},":focus-visible":{color:semanticColor.link.rest,border:"none",outline:"none",boxShadow:`0 0 0 ${sizing.size_020} ${semanticColor.focus.inner}, 0 0 0 ${sizing.size_040} ${semanticColor.focus.outer}`,borderRadius:border.radius.radius_0}}});
|
|
19
13
|
|
|
20
14
|
const focus={":focus-visible":{boxShadow:`0 0 0 ${border.width.medium} ${semanticColor.focus.inner}`,outline:`${border.width.medium} solid ${semanticColor.focus.outer}`,outlineOffset:border.width.medium}};var focusStyles=Object.freeze({__proto__:null,focus:focus});const pressColor=`color-mix(in srgb, ${semanticColor.core.border.neutral.default} 55%, ${semanticColor.core.border.knockout.default})`;const inverse={":not([aria-disabled=true])":{borderColor:semanticColor.core.border.knockout.default,color:semanticColor.core.foreground.knockout.default},":hover:not([aria-disabled=true])":{color:semanticColor.core.foreground.knockout.default,borderColor:semanticColor.core.border.knockout.default},...focus,":active:not([aria-disabled=true])":{borderRadius:border.radius.radius_080,borderColor:pressColor,background:`color-mix(in srgb, ${semanticColor.core.background.base.default} 5%, transparent)`}};Object.freeze({__proto__:null,inverse:inverse});
|
|
21
15
|
|
|
22
16
|
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
23
17
|
|
|
24
|
-
const StyledDiv$2=addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React.useState(null);const updateHasFocusableElement=React.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$
|
|
18
|
+
const StyledDiv$2=addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React.useState(null);const updateHasFocusableElement=React.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$3.tabPanel,style],children:children})};const styles$3=StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
|
|
25
19
|
|
|
26
|
-
const StyledButton=addStyle("button");const Tab=React.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return jsx(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[styles$
|
|
20
|
+
const StyledButton=addStyle("button");const Tab=React.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return jsx(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[styles$7.Body,styles$2.tab,selected&&styles$2.selectedTab,style],children:children})});const bottomSpacing=sizing.size_140;const styles$2=StyleSheet.create({tab:{display:"flex",alignItems:"center",textWrap:"nowrap",backgroundColor:"transparent",border:"none",margin:0,padding:0,cursor:"pointer",marginBlockStart:sizing.size_080,marginBlockEnd:bottomSpacing,position:"relative",color:semanticColor.core.foreground.neutral.subtle,...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:semanticColor.link.hover,[":after"]:{height:border.width.thin,backgroundColor:semanticColor.link.hover}},[":active:not([aria-selected='true'])"]:{color:semanticColor.link.press,[":after"]:{height:border.width.thick,backgroundColor:semanticColor.link.press}}},selectedTab:{color:semanticColor.link.rest}});
|
|
27
21
|
|
|
28
|
-
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$
|
|
22
|
+
const StyledDiv$1=addStyle("div");const Tablist=React.forwardRef(function Tablist(props,ref){const{id,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur,testId,style}=props;return jsx(StyledDiv$1,{id:id,role:"tablist",style:[styles$1.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$1=StyleSheet.create({tablist:{display:"flex",gap:sizing.size_240,borderBottom:`${border.width.thin} solid ${semanticColor.core.border.neutral.subtle}`,paddingInline:sizing.size_040}});
|
|
29
23
|
|
|
30
|
-
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=addStyle("div");const Tabs=React.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false}=props;const focusedTabId=React.useRef(selectedTabId);const tabRefs=React.useRef({});const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React.useRef(null);const isTabActive=React.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&keys.right:case!isRtl&&keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&keys.left:case!isRtl&&keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case keys.enter:case keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsx(React.Fragment,{})}return jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles
|
|
24
|
+
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=addStyle("div");const Tabs=React.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false}=props;const focusedTabId=React.useRef(selectedTabId);const tabRefs=React.useRef({});const generatedUniqueId=React.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React.useRef(null);const isTabActive=React.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&keys.right:case!isRtl&&keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&keys.left:case!isRtl&&keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case keys.enter:case keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsx(React.Fragment,{})}return jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles.tabs,stylesProp?.root],children:[jsxs(StyledDiv,{style:styles.tablistWrapper,children:[jsx(Tablist,{"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:handleTablistBlur,id:tablistId,testId:testId&&`${testId}-tablist`,ref:tablistRef,style:stylesProp?.tablist,children:tabs.map(tab=>{const{id,label,panel:_,testId:tabTestId,...otherProps}=tab;const tabProps={...otherProps,key:id,id:getTabId(id),testId:tabTestId&&getTabId(tabTestId),selected:id===selectedTabId,"aria-controls":getTabPanelId(id),onClick:()=>{onTabSelected(id);},onKeyDown:handleKeyDown,ref:element=>{tabRefs.current[tab.id]=element;},style:stylesProp?.tab};if(typeof label==="function"){return label(tabProps)}return jsx(Tab,{...tabProps,children:label})})}),jsx("div",{...indicatorProps})]}),tabs.map(tab=>{return jsx(TabPanel,{id:getTabPanelId(tab.id),"aria-labelledby":getTabId(tab.id),active:selectedTabId===tab.id,testId:tab.testId&&getTabPanelId(tab.testId),style:stylesProp?.tabPanel,children:(mountAllPanels||visitedTabsRef.current.has(tab.id))&&tab.panel},tab.id)})]})});const styles=StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const ResponsiveTabs=props=>{const{tabs,selectedTabId,onTabSelected,onLayoutChange,id,testId,tabsProps,dropdownProps,styles:stylesProp,...ariaProps}=props;const[showDropdown,setShowDropdown]=React.useState(false);const tabsRef=React.useRef(null);const containerRef=React.useRef(null);const tabsWidthRef=React.useRef(null);const tabsSignature=React.useMemo(()=>tabs.map(t=>`${t.id}:${t.label}`).join("|"),[tabs]);const checkOverflow=React.useCallback(()=>{const container=containerRef.current;if(!container){return}if(!showDropdown&&tabsRef.current){const tablistWrapper=tabsRef.current.firstElementChild;if(tablistWrapper){const hasOverflow=tablistWrapper.scrollWidth>tablistWrapper.clientWidth;if(hasOverflow){tabsWidthRef.current=tablistWrapper.scrollWidth;setShowDropdown(true);}}}else if(showDropdown&&tabsWidthRef.current){const containerWidth=container.clientWidth;if(containerWidth>=tabsWidthRef.current){setShowDropdown(false);}}},[showDropdown,containerRef]);React.useEffect(()=>{if(showDropdown){tabsWidthRef.current=null;setShowDropdown(false);}else {checkOverflow();}},[tabsSignature]);React.useEffect(()=>{const container=containerRef.current;if(!container||!window.ResizeObserver){return}const resizeObserver=new ResizeObserver(()=>{checkOverflow();});resizeObserver.observe(container);return ()=>{resizeObserver.disconnect();}},[checkOverflow]);React.useEffect(()=>{onLayoutChange?.(showDropdown?"dropdown":"tabs");},[showDropdown,onLayoutChange]);return jsx(View,{ref:containerRef,style:[styles.container,stylesProp?.root],id:id,testId:testId,children:showDropdown?createElement(TabsDropdown,{...dropdownProps,...ariaProps,key:"dropdown",tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...dropdownProps?.styles,root:[styles.fadeIn,dropdownProps?.styles?.root]}}):createElement(Tabs,{...tabsProps,...ariaProps,key:"tabs",ref:tabsRef,tabs:tabs,selectedTabId:selectedTabId,onTabSelected:onTabSelected,styles:{...tabsProps?.styles,root:[styles.fadeIn,tabsProps?.styles?.root]}})})};const fadeInKeyframes={from:{opacity:0},to:{opacity:1}};const styles=StyleSheet.create({fadeIn:{animationName:fadeInKeyframes,animationDuration:"150ms",animationTimingFunction:"ease-in-out"},container:{width:"100%",minHeight:"auto"}});
|
|
35
|
-
|
|
36
|
-
export { NavigationTabItem, NavigationTabs, ResponsiveTabs, Tab, Tabs };
|
|
26
|
+
export { NavigationTabItem, NavigationTabs, Tab, Tabs };
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,3 @@ export { NavigationTabItem } from "./components/navigation-tab-item";
|
|
|
3
3
|
export { Tabs } from "./components/tabs";
|
|
4
4
|
export type { TabItem, TabRenderProps } from "./components/tabs";
|
|
5
5
|
export { Tab } from "./components/tab";
|
|
6
|
-
export { ResponsiveTabs } from "./components/responsive-tabs";
|
|
7
|
-
export type { ResponsiveTabItem } from "./components/responsive-tabs";
|
package/dist/index.js
CHANGED
|
@@ -8,13 +8,6 @@ var wonderBlocksTokens = require('@khanacademy/wonder-blocks-tokens');
|
|
|
8
8
|
var aphrodite = require('aphrodite');
|
|
9
9
|
var React = require('react');
|
|
10
10
|
var wonderBlocksTypography = require('@khanacademy/wonder-blocks-typography');
|
|
11
|
-
var wonderBlocksDropdown = require('@khanacademy/wonder-blocks-dropdown');
|
|
12
|
-
var Button = require('@khanacademy/wonder-blocks-button');
|
|
13
|
-
var caretDown = require('@phosphor-icons/core/bold/caret-down-bold.svg');
|
|
14
|
-
var checkCircleIcon = require('@phosphor-icons/core/fill/check-circle-fill.svg');
|
|
15
|
-
var wonderBlocksIcon = require('@khanacademy/wonder-blocks-icon');
|
|
16
|
-
|
|
17
|
-
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
18
11
|
|
|
19
12
|
function _interopNamespace(e) {
|
|
20
13
|
if (e && e.__esModule) return e;
|
|
@@ -35,34 +28,26 @@ function _interopNamespace(e) {
|
|
|
35
28
|
}
|
|
36
29
|
|
|
37
30
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
38
|
-
var Button__default = /*#__PURE__*/_interopDefaultLegacy(Button);
|
|
39
|
-
var caretDown__default = /*#__PURE__*/_interopDefaultLegacy(caretDown);
|
|
40
|
-
var checkCircleIcon__default = /*#__PURE__*/_interopDefaultLegacy(checkCircleIcon);
|
|
41
31
|
|
|
42
|
-
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React__namespace.useRef(false);const[underlineStyle,setUnderlineStyle]=React__namespace.useState({left:0,width:0});const updateUnderlineStyle=React__namespace.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);wonderBlocksCore.useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$
|
|
32
|
+
const useTabIndicator=props=>{const{animated,tabsContainerRef,isTabActive}=props;const indicatorIsReady=React__namespace.useRef(false);const[underlineStyle,setUnderlineStyle]=React__namespace.useState({left:0,width:0});const updateUnderlineStyle=React__namespace.useCallback(()=>{if(!tabsContainerRef.current){return}const activeTab=Array.from(tabsContainerRef.current.children).find(isTabActive);if(activeTab){const tabRect=activeTab.getBoundingClientRect();const parentRect=tabsContainerRef.current.getBoundingClientRect();const zoomFactor=parentRect.width/tabsContainerRef.current.offsetWidth;const left=(tabRect.left-parentRect.left)/zoomFactor;const width=tabRect.width/zoomFactor;setUnderlineStyle({left,width});}},[setUnderlineStyle,tabsContainerRef,isTabActive]);wonderBlocksCore.useOnMountEffect(()=>{if(!tabsContainerRef.current||!window?.ResizeObserver||!window?.MutationObserver){return}const resizeObserver=new window.ResizeObserver(([entry])=>{if(entry){updateUnderlineStyle();if(!indicatorIsReady.current){indicatorIsReady.current=true;}}});resizeObserver.observe(tabsContainerRef.current);const mutationObserver=new window.MutationObserver(([entry])=>{if(entry){updateUnderlineStyle();}});mutationObserver.observe(tabsContainerRef.current,{attributes:true,childList:true,subtree:true});return ()=>{resizeObserver.disconnect();mutationObserver.disconnect();}});const positioningStyle={transform:`translateX(${underlineStyle.left}px)`,width:`${underlineStyle.width}px`};const indicatorProps={style:{...styles$6.currentUnderline,...positioningStyle,...animated?styles$6.underlineTransition:{},...!indicatorIsReady.current?{display:"none"}:{}},role:"presentation"};return {indicatorProps}};const styles$6={currentUnderline:{position:"absolute",bottom:0,left:0,height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.action.secondary.progressive.default.foreground},underlineTransition:{transition:"transform 0.3s ease, width 0.3s ease"}};
|
|
43
33
|
|
|
44
|
-
const StyledUl=wonderBlocksCore.addStyle("ul");const StyledDiv$3=wonderBlocksCore.addStyle("div");const NavigationTabs=React__namespace.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React__namespace.useMemo(()=>wonderBlocksCore.addStyle(tag),[tag]);const listRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsxRuntime.jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$
|
|
34
|
+
const StyledUl=wonderBlocksCore.addStyle("ul");const StyledDiv$3=wonderBlocksCore.addStyle("div");const NavigationTabs=React__namespace.forwardRef(function NavigationTabs(props,ref){const{id,testId,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,styles:stylesProp,animated=false,tag="nav",...otherProps}=props;const StyledTag=React__namespace.useMemo(()=>wonderBlocksCore.addStyle(tag),[tag]);const listRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(navTabItemElement=>{return navTabItemElement.children[0]?.ariaCurrent==="page"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:listRef,isTabActive});return jsxRuntime.jsx(StyledTag,{id:id,"data-testid":testId,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledBy,ref:ref,style:[styles$5.nav,stylesProp?.root],...otherProps,children:jsxRuntime.jsxs(StyledDiv$3,{style:styles$5.contents,children:[jsxRuntime.jsx(StyledUl,{style:[styles$5.list,stylesProp?.list],ref:listRef,children:children}),jsxRuntime.jsx("div",{...indicatorProps})]})})});const styles$5=aphrodite.StyleSheet.create({nav:{overflowX:"auto"},contents:{position:"relative"},list:{paddingInline:wonderBlocksTokens.sizing.size_040,paddingBlock:wonderBlocksTokens.sizing.size_0,margin:wonderBlocksTokens.sizing.size_0,display:"flex",gap:wonderBlocksTokens.sizing.size_160,flexWrap:"nowrap"}});
|
|
45
35
|
|
|
46
|
-
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$
|
|
36
|
+
const StyledLi=wonderBlocksCore.addStyle("li");const NavigationTabItem=React__namespace.forwardRef(function NavigationTabItem(props,ref){const{children,id,testId,current,style,...otherProps}=props;function renderChildren(){const linkProps={style:[wonderBlocksTypography.styles.Body,styles$4.link,current&&styles$4.currentLink],"aria-current":current?"page":undefined};if(typeof children==="function"){return children(linkProps)}return React__namespace.cloneElement(children,linkProps)}return jsxRuntime.jsx(StyledLi,{id:id,"data-testid":testId,style:[styles$4.root,current&&styles$4.current,style],ref:ref,...otherProps,children:renderChildren()})});const styles$4=aphrodite.StyleSheet.create({root:{listStyle:"none",display:"inline-flex",[":has(a:hover)"]:{boxShadow:`inset 0 calc(${wonderBlocksTokens.sizing.size_020}*-1) 0 0 ${wonderBlocksTokens.semanticColor.core.border.instructive.default}`},[":has(a:active)"]:{boxShadow:`inset 0 calc(${wonderBlocksTokens.sizing.size_060}*-1) 0 0 ${wonderBlocksTokens.semanticColor.core.border.instructive.strong}`},paddingBlockStart:wonderBlocksTokens.sizing.size_080,paddingBlockEnd:wonderBlocksTokens.sizing.size_180,[wonderBlocksTokens.breakpoint.mediaQuery.mdOrLarger]:{paddingBlockStart:wonderBlocksTokens.sizing.size_200,paddingBlockEnd:wonderBlocksTokens.sizing.size_240}},current:{[":has(a:hover)"]:{boxShadow:"none"},[":has(a:active):not([aria-disabled=true])"]:{boxShadow:"none"}},currentLink:{color:wonderBlocksTokens.semanticColor.link.rest,[":active:not([aria-disabled=true])"]:{color:wonderBlocksTokens.semanticColor.link.rest}},link:{display:"flex",margin:0,color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,paddingInline:0,position:"relative",whiteSpace:"nowrap",textDecoration:"none",[":hover:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.link.hover,backgroundColor:"transparent"},[":active:not([aria-disabled=true])"]:{textDecoration:"none",border:"none",outline:"none",color:wonderBlocksTokens.semanticColor.link.press},":focus-visible":{color:wonderBlocksTokens.semanticColor.link.rest,border:"none",outline:"none",boxShadow:`0 0 0 ${wonderBlocksTokens.sizing.size_020} ${wonderBlocksTokens.semanticColor.focus.inner}, 0 0 0 ${wonderBlocksTokens.sizing.size_040} ${wonderBlocksTokens.semanticColor.focus.outer}`,borderRadius:wonderBlocksTokens.border.radius.radius_0}}});
|
|
47
37
|
|
|
48
38
|
const focus={":focus-visible":{boxShadow:`0 0 0 ${wonderBlocksTokens.border.width.medium} ${wonderBlocksTokens.semanticColor.focus.inner}`,outline:`${wonderBlocksTokens.border.width.medium} solid ${wonderBlocksTokens.semanticColor.focus.outer}`,outlineOffset:wonderBlocksTokens.border.width.medium}};var focusStyles=Object.freeze({__proto__:null,focus:focus});const pressColor=`color-mix(in srgb, ${wonderBlocksTokens.semanticColor.core.border.neutral.default} 55%, ${wonderBlocksTokens.semanticColor.core.border.knockout.default})`;const inverse={":not([aria-disabled=true])":{borderColor:wonderBlocksTokens.semanticColor.core.border.knockout.default,color:wonderBlocksTokens.semanticColor.core.foreground.knockout.default},":hover:not([aria-disabled=true])":{color:wonderBlocksTokens.semanticColor.core.foreground.knockout.default,borderColor:wonderBlocksTokens.semanticColor.core.border.knockout.default},...focus,":active:not([aria-disabled=true])":{borderRadius:wonderBlocksTokens.border.radius.radius_080,borderColor:pressColor,background:`color-mix(in srgb, ${wonderBlocksTokens.semanticColor.core.background.base.default} 5%, transparent)`}};Object.freeze({__proto__:null,inverse:inverse});
|
|
49
39
|
|
|
50
40
|
const FOCUSABLE_ELEMENTS='button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';function findFocusableNodes(root){return Array.from(root.querySelectorAll(FOCUSABLE_ELEMENTS))}
|
|
51
41
|
|
|
52
|
-
const StyledDiv$2=wonderBlocksCore.addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React__namespace.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React__namespace.useState(null);const updateHasFocusableElement=React__namespace.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React__namespace.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsxRuntime.jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$
|
|
53
|
-
|
|
54
|
-
const StyledButton=wonderBlocksCore.addStyle("button");const Tab=React__namespace.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return jsxRuntime.jsx(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[wonderBlocksTypography.styles.Body,styles$4.tab,selected&&styles$4.selectedTab,style],children:children})});const bottomSpacing=wonderBlocksTokens.sizing.size_140;const styles$4=aphrodite.StyleSheet.create({tab:{display:"flex",alignItems:"center",textWrap:"nowrap",backgroundColor:"transparent",border:"none",margin:0,padding:0,cursor:"pointer",marginBlockStart:wonderBlocksTokens.sizing.size_080,marginBlockEnd:bottomSpacing,position:"relative",color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.hover,[":after"]:{height:wonderBlocksTokens.border.width.thin,backgroundColor:wonderBlocksTokens.semanticColor.link.hover}},[":active:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.press,[":after"]:{height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.link.press}}},selectedTab:{color:wonderBlocksTokens.semanticColor.link.rest}});
|
|
55
|
-
|
|
56
|
-
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$3.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$3=aphrodite.StyleSheet.create({tablist:{display:"flex",gap:wonderBlocksTokens.sizing.size_240,borderBottom:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,paddingInline:wonderBlocksTokens.sizing.size_040}});
|
|
42
|
+
const StyledDiv$2=wonderBlocksCore.addStyle("div");const TabPanel=props=>{const{children,id,"aria-labelledby":ariaLabelledby,active=false,testId,style}=props;const ref=React__namespace.useRef(null);const[hasFocusableElement,setHasFocusableElement]=React__namespace.useState(null);const updateHasFocusableElement=React__namespace.useCallback(element=>{setHasFocusableElement(findFocusableNodes(element).length>0);},[setHasFocusableElement]);React__namespace.useEffect(()=>{if(ref.current&&children){updateHasFocusableElement(ref.current);if(active){const mutationObserver=new MutationObserver(()=>{if(ref.current){updateHasFocusableElement(ref.current);}});mutationObserver.observe(ref.current,{childList:true,subtree:true});return ()=>{mutationObserver.disconnect();}}}},[active,ref,children,updateHasFocusableElement]);return jsxRuntime.jsx(StyledDiv$2,{ref:ref,role:"tabpanel",id:id,"aria-labelledby":ariaLabelledby,tabIndex:hasFocusableElement===false?0:undefined,hidden:!active,"data-testid":testId,style:active&&[styles$3.tabPanel,style],children:children})};const styles$3=aphrodite.StyleSheet.create({tabPanel:{display:"flex",...focusStyles.focus}});
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
const StyledButton=wonderBlocksCore.addStyle("button");const Tab=React__namespace.forwardRef(function Tab(props,ref){const{children,onClick,id,"aria-controls":ariaControls,selected,onKeyDown,testId,style,...otherProps}=props;return jsxRuntime.jsx(StyledButton,{...otherProps,type:"button",role:"tab",onClick:onClick,ref:ref,id:id,"aria-controls":ariaControls,"aria-selected":selected,tabIndex:selected?0:-1,onKeyDown:onKeyDown,"data-testid":testId,style:[wonderBlocksTypography.styles.Body,styles$2.tab,selected&&styles$2.selectedTab,style],children:children})});const bottomSpacing=wonderBlocksTokens.sizing.size_140;const styles$2=aphrodite.StyleSheet.create({tab:{display:"flex",alignItems:"center",textWrap:"nowrap",backgroundColor:"transparent",border:"none",margin:0,padding:0,cursor:"pointer",marginBlockStart:wonderBlocksTokens.sizing.size_080,marginBlockEnd:bottomSpacing,position:"relative",color:wonderBlocksTokens.semanticColor.core.foreground.neutral.subtle,...focusStyles.focus,":after":{content:"''",position:"absolute",left:0,right:0,bottom:`calc(${bottomSpacing} * -1)`},[":hover:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.hover,[":after"]:{height:wonderBlocksTokens.border.width.thin,backgroundColor:wonderBlocksTokens.semanticColor.link.hover}},[":active:not([aria-selected='true'])"]:{color:wonderBlocksTokens.semanticColor.link.press,[":after"]:{height:wonderBlocksTokens.border.width.thick,backgroundColor:wonderBlocksTokens.semanticColor.link.press}}},selectedTab:{color:wonderBlocksTokens.semanticColor.link.rest}});
|
|
59
45
|
|
|
60
|
-
const
|
|
46
|
+
const StyledDiv$1=wonderBlocksCore.addStyle("div");const Tablist=React__namespace.forwardRef(function Tablist(props,ref){const{id,children,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur,testId,style}=props;return jsxRuntime.jsx(StyledDiv$1,{id:id,role:"tablist",style:[styles$1.tablist,style],ref:ref,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:onBlur,"data-testid":testId,children:children})});const styles$1=aphrodite.StyleSheet.create({tablist:{display:"flex",gap:wonderBlocksTokens.sizing.size_240,borderBottom:`${wonderBlocksTokens.border.width.thin} solid ${wonderBlocksTokens.semanticColor.core.border.neutral.subtle}`,paddingInline:wonderBlocksTokens.sizing.size_040}});
|
|
61
47
|
|
|
62
|
-
const
|
|
48
|
+
function getTabId(tabId){return `${tabId}-tab`}function getTabPanelId(tabId){return `${tabId}-panel`}const StyledDiv=wonderBlocksCore.addStyle("div");const Tabs=React__namespace.forwardRef(function Tabs(props,ref){const{tabs,selectedTabId,onTabSelected,"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,activationMode="manual",id,testId,animated=false,styles:stylesProp,mountAllPanels=false}=props;const focusedTabId=React__namespace.useRef(selectedTabId);const tabRefs=React__namespace.useRef({});const generatedUniqueId=React__namespace.useId();const uniqueId=id??generatedUniqueId;const tablistId=`${uniqueId}-tablist`;const visitedTabsRef=React__namespace.useRef(new Set);visitedTabsRef.current.add(selectedTabId);const tablistRef=React__namespace.useRef(null);const isTabActive=React__namespace.useCallback(tabElement=>{return tabElement.ariaSelected==="true"},[]);const{indicatorProps}=useTabIndicator({animated,tabsContainerRef:tablistRef,isTabActive});React__namespace.useEffect(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);const selectTab=React__namespace.useCallback(tabId=>{if(tabId!==selectedTabId){onTabSelected(tabId);}},[onTabSelected,selectedTabId]);const handleKeyInteraction=React__namespace.useCallback(tabId=>{const tabElement=tabRefs.current[tabId];tabElement?.focus();switch(activationMode){case"manual":{focusedTabId.current=tabId;break}case"automatic":{selectTab(tabId);break}}},[activationMode,selectTab,focusedTabId]);const handleKeyDown=React__namespace.useCallback(event=>{const currentIndex=tabs.findIndex(tab=>tab.id===focusedTabId.current);const element=event.currentTarget;const isRtl=!!element.closest("[dir=rtl]");switch(event.key){case isRtl&&wonderBlocksCore.keys.right:case!isRtl&&wonderBlocksCore.keys.left:{event.preventDefault();const prevIndex=(currentIndex-1+tabs.length)%tabs.length;handleKeyInteraction(tabs[prevIndex].id);break}case isRtl&&wonderBlocksCore.keys.left:case!isRtl&&wonderBlocksCore.keys.right:{event.preventDefault();const nextIndex=(currentIndex+1)%tabs.length;handleKeyInteraction(tabs[nextIndex].id);break}case wonderBlocksCore.keys.home:{event.preventDefault();handleKeyInteraction(tabs[0].id);break}case wonderBlocksCore.keys.end:{event.preventDefault();handleKeyInteraction(tabs[tabs.length-1].id);break}case wonderBlocksCore.keys.enter:case wonderBlocksCore.keys.space:{selectTab(focusedTabId.current);break}}},[handleKeyInteraction,selectTab,tabs,focusedTabId]);const handleTablistBlur=React__namespace.useCallback(()=>{focusedTabId.current=selectedTabId;},[selectedTabId]);if(tabs.length===0){return jsxRuntime.jsx(React__namespace.Fragment,{})}return jsxRuntime.jsxs(StyledDiv,{ref:ref,id:uniqueId,"data-testid":testId,style:[styles.tabs,stylesProp?.root],children:[jsxRuntime.jsxs(StyledDiv,{style:styles.tablistWrapper,children:[jsxRuntime.jsx(Tablist,{"aria-label":ariaLabel,"aria-labelledby":ariaLabelledby,onBlur:handleTablistBlur,id:tablistId,testId:testId&&`${testId}-tablist`,ref:tablistRef,style:stylesProp?.tablist,children:tabs.map(tab=>{const{id,label,panel:_,testId:tabTestId,...otherProps}=tab;const tabProps={...otherProps,key:id,id:getTabId(id),testId:tabTestId&&getTabId(tabTestId),selected:id===selectedTabId,"aria-controls":getTabPanelId(id),onClick:()=>{onTabSelected(id);},onKeyDown:handleKeyDown,ref:element=>{tabRefs.current[tab.id]=element;},style:stylesProp?.tab};if(typeof label==="function"){return label(tabProps)}return jsxRuntime.jsx(Tab,{...tabProps,children:label})})}),jsxRuntime.jsx("div",{...indicatorProps})]}),tabs.map(tab=>{return jsxRuntime.jsx(TabPanel,{id:getTabPanelId(tab.id),"aria-labelledby":getTabId(tab.id),active:selectedTabId===tab.id,testId:tab.testId&&getTabPanelId(tab.testId),style:stylesProp?.tabPanel,children:(mountAllPanels||visitedTabsRef.current.has(tab.id))&&tab.panel},tab.id)})]})});const styles=aphrodite.StyleSheet.create({tabs:{display:"inline-flex",flexDirection:"column",alignItems:"stretch",position:"relative"},tablistWrapper:{position:"relative",overflowX:"auto",flexShrink:0}});
|
|
63
49
|
|
|
64
50
|
exports.NavigationTabItem = NavigationTabItem;
|
|
65
51
|
exports.NavigationTabs = NavigationTabs;
|
|
66
|
-
exports.ResponsiveTabs = ResponsiveTabs;
|
|
67
52
|
exports.Tab = Tab;
|
|
68
53
|
exports.Tabs = Tabs;
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Tabs are used to control what content is shown",
|
|
4
4
|
"author": "Khan Academy",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.0.0-
|
|
6
|
+
"version": "0.0.0-PR2921-20260107172502",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -20,13 +20,9 @@
|
|
|
20
20
|
"module": "dist/es/index.js",
|
|
21
21
|
"types": "dist/index.d.ts",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@phosphor-icons/core": "^2.0.2",
|
|
24
|
-
"@khanacademy/wonder-blocks-button": "11.2.10",
|
|
25
23
|
"@khanacademy/wonder-blocks-core": "12.4.2",
|
|
26
|
-
"@khanacademy/wonder-blocks-
|
|
27
|
-
"@khanacademy/wonder-blocks-
|
|
28
|
-
"@khanacademy/wonder-blocks-typography": "4.2.27",
|
|
29
|
-
"@khanacademy/wonder-blocks-tokens": "14.1.3"
|
|
24
|
+
"@khanacademy/wonder-blocks-tokens": "0.0.0-PR2921-20260107172502",
|
|
25
|
+
"@khanacademy/wonder-blocks-typography": "0.0.0-PR2921-20260107172502"
|
|
30
26
|
},
|
|
31
27
|
"peerDependencies": {
|
|
32
28
|
"aphrodite": "^1.2.5",
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import { TabsProps } from "./tabs";
|
|
4
|
-
import { TabsDropdownProps } from "./tabs-dropdown";
|
|
5
|
-
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
6
|
-
export type ResponsiveTabItem = {
|
|
7
|
-
/**
|
|
8
|
-
* The label of the tab.
|
|
9
|
-
*/
|
|
10
|
-
label: string;
|
|
11
|
-
/**
|
|
12
|
-
* The id of the tab.
|
|
13
|
-
*/
|
|
14
|
-
id: string;
|
|
15
|
-
/**
|
|
16
|
-
* The panel of the tab.
|
|
17
|
-
*/
|
|
18
|
-
panel: React.ReactNode;
|
|
19
|
-
/**
|
|
20
|
-
* Optional test ID for e2e testing of the tab.
|
|
21
|
-
*/
|
|
22
|
-
testId?: string;
|
|
23
|
-
};
|
|
24
|
-
type Props = AriaLabelOrAriaLabelledby & {
|
|
25
|
-
/**
|
|
26
|
-
* A unique id for the component.
|
|
27
|
-
*
|
|
28
|
-
* Here is how the id is used for the different elements in the component:
|
|
29
|
-
* - The root will have an id of `${id}`
|
|
30
|
-
*
|
|
31
|
-
* To set the id of the tabs or dropdown, set the `id` prop in the props:
|
|
32
|
-
* `tabsProps` or `dropdownProps`.
|
|
33
|
-
*/
|
|
34
|
-
id?: string;
|
|
35
|
-
/**
|
|
36
|
-
* Optional test ID for e2e testing.
|
|
37
|
-
*
|
|
38
|
-
* Here is how the test id is used for the different elements in the component:
|
|
39
|
-
* - The root will have a testId of `${testId}`
|
|
40
|
-
*
|
|
41
|
-
* To set the test id of the tabs or dropdown, set the `testId` prop in the props:
|
|
42
|
-
* `tabsProps` or `dropdownProps`.
|
|
43
|
-
*/
|
|
44
|
-
testId?: string;
|
|
45
|
-
/**
|
|
46
|
-
* The tabs to render.
|
|
47
|
-
*/
|
|
48
|
-
tabs: ResponsiveTabItem[];
|
|
49
|
-
/**
|
|
50
|
-
* The id of the tab that is selected.
|
|
51
|
-
*/
|
|
52
|
-
selectedTabId: string;
|
|
53
|
-
/**
|
|
54
|
-
* Called when a tab is selected.
|
|
55
|
-
*/
|
|
56
|
-
onTabSelected: (id: string) => void;
|
|
57
|
-
/**
|
|
58
|
-
* Called when the layout changes.
|
|
59
|
-
*/
|
|
60
|
-
onLayoutChange?: (layout: "tabs" | "dropdown") => void;
|
|
61
|
-
/**
|
|
62
|
-
* Additional props to pass to the Tabs component when it is used.
|
|
63
|
-
*
|
|
64
|
-
* Note: This prop doesn't include the props that are available on the
|
|
65
|
-
* ResponsiveTabs component already.
|
|
66
|
-
*/
|
|
67
|
-
tabsProps?: Omit<TabsProps, "tabs" | "selectedTabId" | "onTabSelected" | "aria-label" | "aria-labelledby">;
|
|
68
|
-
/**
|
|
69
|
-
* Additional props to pass to the TabsDropdown component when it is used.
|
|
70
|
-
*
|
|
71
|
-
* Note: This prop doesn't include the props that are available on the
|
|
72
|
-
* ResponsiveTabs component already.
|
|
73
|
-
*/
|
|
74
|
-
dropdownProps?: Omit<TabsDropdownProps, "tabs" | "selectedTabId" | "onTabSelected" | "aria-label" | "aria-labelledby">;
|
|
75
|
-
/**
|
|
76
|
-
* Custom styles for the ResponsiveTabs component.
|
|
77
|
-
* - `root`: Styles the root `div` element.
|
|
78
|
-
*
|
|
79
|
-
* To customize the styles of the tabs or dropdown, set the `styles` prop on
|
|
80
|
-
* the `tabsProps` or `dropdownProps` props. See the `Tabs` and `TabsDropdown`
|
|
81
|
-
* docs for more details.
|
|
82
|
-
*/
|
|
83
|
-
styles?: {
|
|
84
|
-
root?: StyleType;
|
|
85
|
-
};
|
|
86
|
-
};
|
|
87
|
-
/**
|
|
88
|
-
* Renders the Tabs component when there is enough space to display the tabs as
|
|
89
|
-
* a horizontal layout. When there is not enough space, it renders the
|
|
90
|
-
* tabs as a dropdown.
|
|
91
|
-
*
|
|
92
|
-
* Note: This component switches layouts depending on factors like the container
|
|
93
|
-
* width, the number of tabs, the length of tab labels, zoom level, etc. Once the
|
|
94
|
-
* horizontal Tabs need to start scrolling horizontally, the component will
|
|
95
|
-
* switch to the dropdown.
|
|
96
|
-
*
|
|
97
|
-
* For cases where the tabs should always be in a horizontal layout, use the
|
|
98
|
-
* Tabs component directly.
|
|
99
|
-
*/
|
|
100
|
-
export declare const ResponsiveTabs: (props: Props) => React.JSX.Element;
|
|
101
|
-
export {};
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import * as React from "react";
|
|
2
|
-
import { StyleType } from "@khanacademy/wonder-blocks-core";
|
|
3
|
-
import { AriaLabelOrAriaLabelledby } from "./types";
|
|
4
|
-
type TabDropdownItem = {
|
|
5
|
-
/**
|
|
6
|
-
* The label of the tab
|
|
7
|
-
*/
|
|
8
|
-
label: string;
|
|
9
|
-
/**
|
|
10
|
-
* The id of the tab
|
|
11
|
-
*/
|
|
12
|
-
id: string;
|
|
13
|
-
/**
|
|
14
|
-
* The contents for the tab
|
|
15
|
-
*/
|
|
16
|
-
panel: React.ReactNode;
|
|
17
|
-
/**
|
|
18
|
-
* Optional test ID for e2e testing of the menu item.
|
|
19
|
-
*/
|
|
20
|
-
testId?: string;
|
|
21
|
-
};
|
|
22
|
-
export type TabsDropdownProps = AriaLabelOrAriaLabelledby & {
|
|
23
|
-
/**
|
|
24
|
-
* A unique id for the component. If not provided, a unique base id will be
|
|
25
|
-
* generated automatically.
|
|
26
|
-
*
|
|
27
|
-
* Here is how the id is used for the different elements in the component:
|
|
28
|
-
* - The root will have an id of `${id}`
|
|
29
|
-
* - The opener will have an id of `${id}-opener`
|
|
30
|
-
* - The panel will have an id of `${id}-panel`
|
|
31
|
-
*/
|
|
32
|
-
id?: string;
|
|
33
|
-
/**
|
|
34
|
-
* Optional test ID for e2e testing.
|
|
35
|
-
*
|
|
36
|
-
* Here is how the testId is used for the different elements in the component:
|
|
37
|
-
* - The root will have a testId of `${testId}`
|
|
38
|
-
* - The opener will have a testId of `${testId}-opener`
|
|
39
|
-
* - The panel will have a testId of `${testId}-panel`
|
|
40
|
-
*/
|
|
41
|
-
testId?: string;
|
|
42
|
-
/**
|
|
43
|
-
* The tabs to render in the dropdown.
|
|
44
|
-
*/
|
|
45
|
-
tabs: Array<TabDropdownItem>;
|
|
46
|
-
/**
|
|
47
|
-
* The id of the tab that is selected.
|
|
48
|
-
*
|
|
49
|
-
* If the selectedTabId is not valid, the `labels.defaultOpenerLabel` will
|
|
50
|
-
* be used to label the dropdown opener.
|
|
51
|
-
*/
|
|
52
|
-
selectedTabId: string;
|
|
53
|
-
/**
|
|
54
|
-
* Called when a tab is selected.
|
|
55
|
-
*/
|
|
56
|
-
onTabSelected: (id: string) => unknown;
|
|
57
|
-
/**
|
|
58
|
-
* Labels for the dropdown.
|
|
59
|
-
*/
|
|
60
|
-
labels?: {
|
|
61
|
-
defaultOpenerLabel?: string;
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Can be used to override the opened state for the dropdown
|
|
65
|
-
*/
|
|
66
|
-
opened?: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* Styling for the tabs dropdown.
|
|
69
|
-
*/
|
|
70
|
-
styles?: {
|
|
71
|
-
root?: StyleType;
|
|
72
|
-
actionMenu?: StyleType;
|
|
73
|
-
opener?: StyleType;
|
|
74
|
-
};
|
|
75
|
-
};
|
|
76
|
-
/**
|
|
77
|
-
* The TabsDropdown component is used to represent tabs in an ActionMenu when
|
|
78
|
-
* there is not enough horizontal space to render the tabs as a horizontal layout.
|
|
79
|
-
*
|
|
80
|
-
* Note: This component is meant to be used internally to address responsiveness
|
|
81
|
-
* in the ResponsiveTabs component. Please reach out to the WB team if there is
|
|
82
|
-
* a need to use this component directly.
|
|
83
|
-
*/
|
|
84
|
-
export declare const TabsDropdown: React.ForwardRefExoticComponent<TabsDropdownProps & React.RefAttributes<HTMLDivElement>>;
|
|
85
|
-
export {};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Type to help ensure aria-label or aria-labelledby is set.
|
|
3
|
-
*/
|
|
4
|
-
export type AriaLabelOrAriaLabelledby = {
|
|
5
|
-
/**
|
|
6
|
-
* If there is no visible label for the tabs, set aria-label to a
|
|
7
|
-
* label describing the tabs.
|
|
8
|
-
*/
|
|
9
|
-
"aria-label": string;
|
|
10
|
-
"aria-labelledby"?: never;
|
|
11
|
-
} | {
|
|
12
|
-
/**
|
|
13
|
-
* If the tabs have a visible label, set aria-labelledby to a value
|
|
14
|
-
* that refers to the labelling element.
|
|
15
|
-
*/
|
|
16
|
-
"aria-labelledby": string;
|
|
17
|
-
"aria-label"?: never;
|
|
18
|
-
};
|