@base-ui-components/react
Version:
Base UI is a library of headless ('unstyled') React components and low-level hooks. You gain complete control over your app's CSS and accessibility features.
174 lines (169 loc) • 5.6 kB
JavaScript
'use client';
import * as React from 'react';
import { ownerDocument } from '@base-ui-components/utils/owner';
import { useIsoLayoutEffect } from '@base-ui-components/utils/useIsoLayoutEffect';
import { useBaseUiId } from "../../utils/useBaseUiId.js";
import { useRenderElement } from "../../utils/useRenderElement.js";
import { useButton } from "../../use-button/index.js";
import { ACTIVE_COMPOSITE_ITEM } from "../../composite/constants.js";
import { useCompositeItem } from "../../composite/item/useCompositeItem.js";
import { useTabsRootContext } from "../root/TabsRootContext.js";
import { useTabsListContext } from "../list/TabsListContext.js";
import { createChangeEventDetails } from "../../utils/createBaseUIEventDetails.js";
import { REASONS } from "../../utils/reasons.js";
import { activeElement, contains } from "../../floating-ui-react/utils.js";
/**
* An individual interactive tab button that toggles the corresponding panel.
* Renders a `<button>` element.
*
* Documentation: [Base UI Tabs](https://base-ui.com/react/components/tabs)
*/
export const TabsTab = /*#__PURE__*/React.forwardRef(function TabsTab(componentProps, forwardedRef) {
const {
className,
disabled = false,
render,
value: valueProp,
id: idProp,
nativeButton = true,
...elementProps
} = componentProps;
const {
value: activeTabValue,
getTabPanelIdByTabValueOrIndex,
orientation
} = useTabsRootContext();
const {
activateOnFocus,
highlightedTabIndex,
onTabActivation,
setHighlightedTabIndex,
tabsListElement
} = useTabsListContext();
const id = useBaseUiId(idProp);
const tabMetadata = React.useMemo(() => ({
disabled,
id,
value: valueProp
}), [disabled, id, valueProp]);
const {
compositeProps,
compositeRef,
index
// hook is used instead of the CompositeItem component
// because the index is needed for Tab internals
} = useCompositeItem({
metadata: tabMetadata
});
const tabValue = valueProp ?? index;
// the `active` state isn't set on the server (it relies on effects to be calculated),
// so we fall back to checking the `value` param with the activeTabValue from the TabsContext
const active = React.useMemo(() => {
if (valueProp === undefined) {
return index < 0 ? false : index === activeTabValue;
}
return valueProp === activeTabValue;
}, [index, activeTabValue, valueProp]);
const isNavigatingRef = React.useRef(false);
// Keep the highlighted item in sync with the currently active tab
// when the value prop changes externally (controlled mode)
useIsoLayoutEffect(() => {
if (isNavigatingRef.current) {
isNavigatingRef.current = false;
return;
}
if (!(active && index > -1 && highlightedTabIndex !== index)) {
return;
}
// If focus is currently within the tabs list, don't override the roving
// focus highlight. This keeps keyboard navigation relative to the focused
// item after an external/asynchronous selection change.
const listElement = tabsListElement;
if (listElement != null) {
const activeEl = activeElement(ownerDocument(listElement));
if (activeEl && contains(listElement, activeEl)) {
return;
}
}
setHighlightedTabIndex(index);
}, [active, index, highlightedTabIndex, setHighlightedTabIndex, disabled, tabsListElement]);
const {
getButtonProps,
buttonRef
} = useButton({
disabled,
native: nativeButton,
focusableWhenDisabled: true
});
const tabPanelId = index > -1 ? getTabPanelIdByTabValueOrIndex(valueProp, index) : undefined;
const isPressingRef = React.useRef(false);
const isMainButtonRef = React.useRef(false);
function onClick(event) {
if (active || disabled) {
return;
}
onTabActivation(tabValue, createChangeEventDetails(REASONS.none, event.nativeEvent, undefined, {
activationDirection: 'none'
}));
}
function onFocus(event) {
if (active) {
return;
}
if (index > -1) {
setHighlightedTabIndex(index);
}
if (disabled) {
return;
}
if (activateOnFocus && (!isPressingRef.current ||
// keyboard or touch focus
isPressingRef.current && isMainButtonRef.current) // mouse focus
) {
onTabActivation(tabValue, createChangeEventDetails(REASONS.none, event.nativeEvent, undefined, {
activationDirection: 'none'
}));
}
}
function onPointerDown(event) {
if (active || disabled) {
return;
}
isPressingRef.current = true;
function handlePointerUp() {
isPressingRef.current = false;
isMainButtonRef.current = false;
}
if (!event.button || event.button === 0) {
isMainButtonRef.current = true;
const doc = ownerDocument(event.currentTarget);
doc.addEventListener('pointerup', handlePointerUp, {
once: true
});
}
}
const state = React.useMemo(() => ({
disabled,
active,
orientation
}), [disabled, active, orientation]);
const element = useRenderElement('button', componentProps, {
state,
ref: [forwardedRef, buttonRef, compositeRef],
props: [compositeProps, {
role: 'tab',
'aria-controls': tabPanelId,
'aria-selected': active,
id,
onClick,
onFocus,
onPointerDown,
[ACTIVE_COMPOSITE_ITEM]: active ? '' : undefined,
onKeyDownCapture() {
isNavigatingRef.current = true;
}
}, elementProps, getButtonProps]
});
return element;
});
if (process.env.NODE_ENV !== "production") TabsTab.displayName = "TabsTab";