Skip to content

Commit 3650d79

Browse files
committed
TabsRoot
1 parent 3bf90ed commit 3650d79

File tree

2 files changed

+113
-216
lines changed

2 files changed

+113
-216
lines changed

packages/react/src/tabs/root/TabsRoot.tsx

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
'use client';
22
import * as React from 'react';
3-
import { useComponentRenderer } from '../../utils/useComponentRenderer';
43
import type { BaseUIComponentProps } from '../../utils/types';
4+
import { useControlled } from '../../utils/useControlled';
5+
import { useEventCallback } from '../../utils/useEventCallback';
6+
import { useRenderElement } from '../../utils/useRenderElement';
57
import { CompositeList } from '../../composite/list/CompositeList';
8+
import type { CompositeMetadata } from '../../composite/list/CompositeList';
69
import { useDirection } from '../../direction-provider/DirectionContext';
7-
import { useTabsRoot } from './useTabsRoot';
810
import { TabsRootContext } from './TabsRootContext';
911
import { tabsStyleHookMapping } from './styleHooks';
12+
import type { TabMetadata } from '../tab/TabsTab';
1013
import type { TabPanelMetadata } from '../panel/TabsPanel';
1114

1215
/**
@@ -16,7 +19,7 @@ import type { TabPanelMetadata } from '../panel/TabsPanel';
1619
* Documentation: [Base UI Tabs](https://base-ui.com/react/components/tabs)
1720
*/
1821
export const TabsRoot = React.forwardRef(function TabsRoot(
19-
props: TabsRoot.Props,
22+
componentProps: TabsRoot.Props,
2023
forwardedRef: React.ForwardedRef<HTMLDivElement>,
2124
) {
2225
const {
@@ -26,27 +29,116 @@ export const TabsRoot = React.forwardRef(function TabsRoot(
2629
orientation = 'horizontal',
2730
render,
2831
value: valueProp,
29-
...other
30-
} = props;
32+
...elementProps
33+
} = componentProps;
3134

3235
const direction = useDirection();
3336

34-
const {
35-
getTabElementBySelectedValue,
36-
getTabIdByPanelValueOrIndex,
37-
getTabPanelIdByTabValueOrIndex,
38-
onValueChange,
39-
setTabMap,
40-
setTabPanelMap,
41-
tabActivationDirection,
42-
tabPanelRefs,
43-
value,
44-
} = useTabsRoot({
45-
value: valueProp,
46-
defaultValue,
47-
onValueChange: onValueChangeProp,
37+
const tabPanelRefs = React.useRef<(HTMLElement | null)[]>([]);
38+
39+
const [value, setValue] = useControlled({
40+
controlled: valueProp,
41+
default: defaultValue,
42+
name: 'Tabs',
43+
state: 'value',
4844
});
4945

46+
const [tabPanelMap, setTabPanelMap] = React.useState(
47+
() => new Map<Node, CompositeMetadata<TabPanelMetadata> | null>(),
48+
);
49+
const [tabMap, setTabMap] = React.useState(
50+
() => new Map<Node, CompositeMetadata<TabMetadata> | null>(),
51+
);
52+
53+
const [tabActivationDirection, setTabActivationDirection] =
54+
React.useState<TabActivationDirection>('none');
55+
56+
const onValueChange = useEventCallback(
57+
(newValue: TabValue, activationDirection: TabActivationDirection, event: Event | undefined) => {
58+
setValue(newValue);
59+
setTabActivationDirection(activationDirection);
60+
onValueChangeProp?.(newValue, event);
61+
},
62+
);
63+
64+
// get the `id` attribute of <Tabs.Panel> to set as the value of `aria-controls` on <Tabs.Tab>
65+
const getTabPanelIdByTabValueOrIndex = React.useCallback(
66+
(tabValue: TabValue | undefined, index: number) => {
67+
if (tabValue === undefined && index < 0) {
68+
return undefined;
69+
}
70+
71+
for (const tabPanelMetadata of tabPanelMap.values()) {
72+
// find by tabValue
73+
if (tabValue !== undefined && tabPanelMetadata && tabValue === tabPanelMetadata?.value) {
74+
return tabPanelMetadata.id;
75+
}
76+
77+
// find by index
78+
if (
79+
tabValue === undefined &&
80+
tabPanelMetadata?.index &&
81+
tabPanelMetadata?.index === index
82+
) {
83+
return tabPanelMetadata.id;
84+
}
85+
}
86+
87+
return undefined;
88+
},
89+
[tabPanelMap],
90+
);
91+
92+
// get the `id` attribute of <Tabs.Tab> to set as the value of `aria-labelledby` on <Tabs.Panel>
93+
const getTabIdByPanelValueOrIndex = React.useCallback(
94+
(tabPanelValue: TabValue | undefined, index: number) => {
95+
if (tabPanelValue === undefined && index < 0) {
96+
return undefined;
97+
}
98+
99+
for (const tabMetadata of tabMap.values()) {
100+
// find by tabPanelValue
101+
if (
102+
tabPanelValue !== undefined &&
103+
index > -1 &&
104+
tabPanelValue === (tabMetadata?.value ?? tabMetadata?.index ?? undefined)
105+
) {
106+
return tabMetadata?.id;
107+
}
108+
109+
// find by index
110+
if (
111+
tabPanelValue === undefined &&
112+
index > -1 &&
113+
index === (tabMetadata?.value ?? tabMetadata?.index ?? undefined)
114+
) {
115+
return tabMetadata?.id;
116+
}
117+
}
118+
119+
return undefined;
120+
},
121+
[tabMap],
122+
);
123+
124+
// used in `useActivationDirectionDetector` for setting data-activation-direction
125+
const getTabElementBySelectedValue = React.useCallback(
126+
(selectedValue: TabValue | undefined): HTMLElement | null => {
127+
if (selectedValue === undefined) {
128+
return null;
129+
}
130+
131+
for (const [tabElement, tabMetadata] of tabMap.entries()) {
132+
if (tabMetadata != null && selectedValue === (tabMetadata.value ?? tabMetadata.index)) {
133+
return tabElement as HTMLElement;
134+
}
135+
}
136+
137+
return null;
138+
},
139+
[tabMap],
140+
);
141+
50142
const tabsContextValue: TabsRootContext = React.useMemo(
51143
() => ({
52144
direction,
@@ -77,12 +169,10 @@ export const TabsRoot = React.forwardRef(function TabsRoot(
77169
tabActivationDirection,
78170
};
79171

80-
const { renderElement } = useComponentRenderer({
81-
render: render ?? 'div',
82-
className,
172+
const renderElement = useRenderElement('div', componentProps, {
83173
state,
84-
extraProps: other,
85174
ref: forwardedRef,
175+
props: elementProps,
86176
customStyleHookMapping: tabsStyleHookMapping,
87177
});
88178

packages/react/src/tabs/root/useTabsRoot.ts

Lines changed: 0 additions & 193 deletions
This file was deleted.

0 commit comments

Comments
 (0)