@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.
139 lines (137 loc) • 5.1 kB
JavaScript
'use client';
import * as React from 'react';
import { generateId } from '@base-ui-components/utils/generateId';
import { useForcedRerendering } from '@base-ui-components/utils/useForcedRerendering';
import { useOnMount } from '@base-ui-components/utils/useOnMount';
import { useRenderElement } from "../../utils/useRenderElement.js";
import { useDirection } from "../../direction-provider/DirectionContext.js";
import { useTabsRootContext } from "../root/TabsRootContext.js";
import { tabsStyleHookMapping } from "../root/styleHooks.js";
import { useTabsListContext } from "../list/TabsListContext.js";
import { script as prehydrationScript } from "./prehydrationScript.min.js";
import { TabsIndicatorCssVars } from "./TabsIndicatorCssVars.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
const customStyleHookMapping = {
...tabsStyleHookMapping,
selectedTabPosition: () => null,
selectedTabSize: () => null
};
/**
* A visual indicator that can be styled to match the position of the currently active tab.
* Renders a `<span>` element.
*
* Documentation: [Base UI Tabs](https://base-ui.com/react/components/tabs)
*/
export const TabsIndicator = /*#__PURE__*/React.forwardRef(function TabIndicator(componentProps, forwardedRef) {
const {
className,
render,
renderBeforeHydration = false,
...elementProps
} = componentProps;
const {
getTabElementBySelectedValue,
orientation,
tabActivationDirection,
value
} = useTabsRootContext();
const {
tabsListRef
} = useTabsListContext();
const [instanceId] = React.useState(() => generateId('tab'));
const [isMounted, setIsMounted] = React.useState(false);
const {
value: activeTabValue
} = useTabsRootContext();
const direction = useDirection();
useOnMount(() => setIsMounted(true));
const rerender = useForcedRerendering();
React.useEffect(() => {
if (value != null && tabsListRef.current != null && typeof ResizeObserver !== 'undefined') {
const resizeObserver = new ResizeObserver(() => {
rerender();
});
resizeObserver.observe(tabsListRef.current);
return () => {
resizeObserver.disconnect();
};
}
return undefined;
}, [value, tabsListRef, rerender]);
let left = 0;
let right = 0;
let top = 0;
let bottom = 0;
let width = 0;
let height = 0;
let isTabSelected = false;
if (value != null && tabsListRef.current != null) {
const selectedTab = getTabElementBySelectedValue(value);
const tabsList = tabsListRef.current;
isTabSelected = true;
if (selectedTab != null) {
left = selectedTab.offsetLeft - tabsList.clientLeft;
right = direction === 'ltr' ? tabsList.scrollWidth - selectedTab.offsetLeft - selectedTab.offsetWidth - tabsList.clientLeft : selectedTab.offsetLeft - tabsList.clientLeft;
top = selectedTab.offsetTop - tabsList.clientTop;
bottom = tabsList.scrollHeight - selectedTab.offsetTop - selectedTab.offsetHeight - tabsList.clientTop;
width = selectedTab.offsetWidth;
height = selectedTab.offsetHeight;
}
}
const selectedTabPosition = React.useMemo(() => isTabSelected ? {
left,
right,
top,
bottom
} : null, [left, right, top, bottom, isTabSelected]);
const selectedTabSize = React.useMemo(() => isTabSelected ? {
width,
height
} : null, [width, height, isTabSelected]);
const style = React.useMemo(() => {
if (!isTabSelected) {
return undefined;
}
return {
[TabsIndicatorCssVars.activeTabLeft]: `${left}px`,
[TabsIndicatorCssVars.activeTabRight]: `${right}px`,
[TabsIndicatorCssVars.activeTabTop]: `${top}px`,
[TabsIndicatorCssVars.activeTabBottom]: `${bottom}px`,
[TabsIndicatorCssVars.activeTabWidth]: `${width}px`,
[TabsIndicatorCssVars.activeTabHeight]: `${height}px`
};
}, [left, right, top, bottom, width, height, isTabSelected]);
const displayIndicator = isTabSelected && width > 0 && height > 0;
const state = React.useMemo(() => ({
orientation,
selectedTabPosition,
selectedTabSize,
tabActivationDirection
}), [orientation, selectedTabPosition, selectedTabSize, tabActivationDirection]);
const element = useRenderElement('span', componentProps, {
state,
ref: forwardedRef,
props: [{
role: 'presentation',
style,
hidden: !displayIndicator // do not display the indicator before the layout is settled
}, elementProps, {
['data-instance-id']: !(isMounted && renderBeforeHydration) ? instanceId : undefined,
suppressHydrationWarning: true
}],
customStyleHookMapping
});
if (activeTabValue == null) {
return null;
}
return /*#__PURE__*/_jsxs(React.Fragment, {
children: [element, !isMounted && renderBeforeHydration && /*#__PURE__*/_jsx("script", {
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML: {
__html: prehydrationScript
},
suppressHydrationWarning: true
})]
});
});
if (process.env.NODE_ENV !== "production") TabsIndicator.displayName = "TabsIndicator";