in src/tabs-motion/tabs.tsx [107:321]
export function Tabs({
activeKey = '0',
disabled = false,
children,
fill = FILL.intrinsic,
activateOnFocus = true,
onChange,
orientation = ORIENTATION.horizontal,
overrides = {},
renderAll = false,
// @ts-ignore
uid: customUid = null,
endEnhancer,
}: TabsProps) {
// Create unique id prefix for this tabs component
const generatedUid = useUID();
const uid = customUid || generatedUid;
// Unpack overrides
const {
Root: RootOverrides,
TabList: TabListOverrides,
TabHighlight: TabHighlightOverrides,
TabBorder: TabBorderOverrides,
} = overrides;
const [Root, RootProps] = getOverrides(RootOverrides, StyledRoot);
const [TabList, TabListProps] = getOverrides(TabListOverrides, StyledTabList);
const [TabHighlight, TabHighlightProps] = getOverrides(TabHighlightOverrides, StyledTabHighlight);
const [TabBorder, TabBorderProps] = getOverrides(TabBorderOverrides, StyledTabBorder);
const [EndEnhancerContainer, endEnhancerContainerProps] = getOverrides(
overrides.EndEnhancerContainer,
StyledEndEnhancerContainer
);
const [TabBar, tabBarProps] = getOverrides(overrides.TabBar, StyledTabBar);
// Count key updates
// We disable a few things until after first mount:
// - the highlight animation, avoiding an initial slide-in
// - smooth scrolling active tab into view
const [keyUpdated, setKeyUpdated] = React.useState(0);
React.useEffect(() => {
setKeyUpdated(keyUpdated + 1);
}, [activeKey]);
// Positioning the highlight.
const activeTabRef = React.useRef<HTMLElement>();
const [highlightLayout, setHighlightLayout] = React.useState({
length: 0,
distance: 0,
});
// Create a shared, memoized callback for tabs to call on resize.
const updateHighlight = React.useCallback(() => {
if (activeTabRef.current) {
setHighlightLayout(getLayoutParams(activeTabRef.current, orientation));
}
}, [activeTabRef.current, orientation]);
// Update highlight on key, orientation and children changes.
React.useEffect(updateHighlight, [activeTabRef.current, orientation, children]);
// Scroll active tab into view when the parent has scrollbar on mount and
// on key change (smooth scroll). Note, if the active key changes while
// the tab is not in view, the page will scroll it into view.
// TODO: replace with custom scrolling logic.
React.useEffect(() => {
// Flow needs this condition pulled out.
if (activeTabRef.current) {
if (
isHorizontal(orientation)
? // @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.scrollWidth >
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.clientWidth
: // @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.scrollHeight >
// @ts-expect-error todo(flow->ts) maybe parentElement?
activeTabRef.current.parentNode.clientHeight
) {
if (keyUpdated > 1) {
activeTabRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest',
});
} else {
scrollParentToCentreTarget(activeTabRef.current);
}
}
}
}, [activeTabRef.current]);
// Collect shared styling props
const sharedStylingProps = {
$orientation: orientation,
$fill: fill,
};
// Helper for parsing directional keys
// TODO(WPT-6473): move to universal keycode aliases
const [, theme] = useStyletron();
const parseKeyDown = React.useCallback(
(event: { keyCode: number }) => {
if (isHorizontal(orientation)) {
if (isRTL(theme.direction)) {
switch (event.keyCode) {
case 39:
return KEYBOARD_ACTION.previous;
case 37:
return KEYBOARD_ACTION.next;
default:
return null;
}
} else {
switch (event.keyCode) {
case 37:
return KEYBOARD_ACTION.previous;
case 39:
return KEYBOARD_ACTION.next;
default:
return null;
}
}
} else {
switch (event.keyCode) {
case 38:
return KEYBOARD_ACTION.previous;
case 40:
return KEYBOARD_ACTION.next;
default:
return null;
}
}
},
[orientation, theme.direction]
);
return (
<Root {...sharedStylingProps} {...RootProps}>
<TabBar $hasEndEnhancer={Boolean(endEnhancer)} $orientation={orientation} {...tabBarProps}>
<TabList
data-baseweb="tab-list"
role="tablist"
aria-orientation={orientation}
{...sharedStylingProps}
{...TabListProps}
>
{/* @ts-ignore */}
{React.Children.map(children, (child: React.ReactElement, index) => {
if (!child) return;
return (
<InternalTab
childKey={child.key}
childIndex={index}
activeKey={activeKey}
orientation={orientation}
activeTabRef={activeTabRef}
updateHighlight={updateHighlight}
parseKeyDown={parseKeyDown}
activateOnFocus={activateOnFocus}
uid={uid}
disabled={disabled}
sharedStylingProps={sharedStylingProps}
onChange={onChange}
{...child.props}
/>
);
})}
<TabHighlight
data-baseweb="tab-highlight"
$length={highlightLayout.length}
$distance={highlightLayout.distance}
// This avoids the tab sliding in from the side on mount
$animate={keyUpdated > 1}
aria-hidden="true"
role="presentation"
{...sharedStylingProps}
{...TabHighlightProps}
/>
</TabList>
{orientation === ORIENTATION.horizontal &&
endEnhancer !== null &&
endEnhancer !== undefined && (
<EndEnhancerContainer {...endEnhancerContainerProps} $orientation={orientation}>
<RenderEnhancer Enhancer={endEnhancer} />
</EndEnhancerContainer>
)}
</TabBar>
<TabBorder
data-baseweb="tab-border"
aria-hidden="true"
role="presentation"
{...sharedStylingProps}
{...TabBorderProps}
/>
{/* @ts-ignore */}
{React.Children.map(children, (child: React.ReactElement, index) => {
if (!child) return;
return (
<InternalTabPanel
childKey={child.key}
childIndex={index}
activeKey={activeKey}
uid={uid}
sharedStylingProps={sharedStylingProps}
renderAll={renderAll}
{...child.props}
/>
);
})}
</Root>
);
}