Skip to content

Commit

Permalink
Use Composite for TabsRoot and TabPanel
Browse files Browse the repository at this point in the history
  • Loading branch information
mj12albert committed Nov 13, 2024
1 parent 9dfea65 commit c31706a
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 65 deletions.
20 changes: 11 additions & 9 deletions packages/mui-base/src/Tabs/Root/TabsRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { useComponentRenderer } from '../../utils/useComponentRenderer';
import type { BaseUIComponentProps } from '../../utils/types';
import { CompoundComponentContext } from '../../useCompound';
import { CompositeList } from '../../Composite/List/CompositeList';
import { useTabsRoot } from './useTabsRoot';
import { TabsRootContext } from './TabsRootContext';
import { tabsStyleHookMapping } from './styleHooks';
import { TabPanelMetadata } from '../TabPanel/useTabPanel';

/**
*
Expand Down Expand Up @@ -35,13 +36,14 @@ const TabsRoot = React.forwardRef(function TabsRoot(

const {
getRootProps,
compoundContext,
direction,
getTabId,
getTabPanelId,
getTabPanelIdByTabValueOrIndex,
onSelected,
registerTabIdLookup,
setTabPanelMap,
tabActivationDirection,
tabPanelRefs,
value,
} = useTabsRoot({
value: valueProp,
Expand All @@ -54,7 +56,7 @@ const TabsRoot = React.forwardRef(function TabsRoot(
() => ({
direction,
getTabId,
getTabPanelId,
getTabPanelIdByTabValueOrIndex,
onSelected,
orientation,
registerTabIdLookup,
Expand All @@ -64,7 +66,7 @@ const TabsRoot = React.forwardRef(function TabsRoot(
[
direction,
getTabId,
getTabPanelId,
getTabPanelIdByTabValueOrIndex,
onSelected,
orientation,
registerTabIdLookup,
Expand All @@ -90,11 +92,11 @@ const TabsRoot = React.forwardRef(function TabsRoot(
});

return (
<CompoundComponentContext.Provider value={compoundContext}>
<TabsRootContext.Provider value={tabsContextValue}>
<TabsRootContext.Provider value={tabsContextValue}>
<CompositeList<TabPanelMetadata> elementsRef={tabPanelRefs} onMapChange={setTabPanelMap}>
{renderElement()}
</TabsRootContext.Provider>
</CompoundComponentContext.Provider>
</CompositeList>
</TabsRootContext.Provider>
);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/mui-base/src/Tabs/Root/TabsRootContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export interface TabsRootContext {
* Gets the id of the tab panel with the given value.
* @param value Value to find the tab panel for.
*/
getTabPanelId: (value: any) => string | undefined;
getTabPanelIdByTabValueOrIndex: (tabValue: any, index: number) => string | undefined;
/**
* The position of the active tab relative to the previously active tab.
*/
Expand Down
72 changes: 46 additions & 26 deletions packages/mui-base/src/Tabs/Root/useTabsRoot.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
'use client';
import * as React from 'react';
import type { TabActivationDirection } from './TabsRoot';
import { CompoundComponentContextValue, useCompoundParent } from '../../useCompound';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { useControlled } from '../../utils/useControlled';

interface TabPanelMetadata {
id: string | undefined;
ref: React.RefObject<HTMLElement>;
}
import type { TabPanelMetadata } from '../TabPanel/useTabPanel';
import type { TabActivationDirection } from './TabsRoot';

export interface TabMetadata {
disabled: boolean;
Expand All @@ -21,13 +16,20 @@ type IdLookupFunction = (id: any) => string | undefined;
function useTabsRoot(parameters: useTabsRoot.Parameters): useTabsRoot.ReturnValue {
const { value: valueProp, defaultValue, onValueChange, direction = 'ltr' } = parameters;

const tabPanelRefs = React.useRef<(HTMLElement | null)[]>([]);
const getTabIdByPanelValueRef = React.useRef<IdLookupFunction>(() => undefined);

const [value, setValue] = useControlled({
controlled: valueProp,
default: defaultValue,
name: 'Tabs',
state: 'value',
});

const [tabPanelMap, setTabPanelMap] = React.useState(
() => new Map<Node, (TabPanelMetadata & { index?: number | null }) | null>(),
);

const [tabActivationDirection, setTabActivationDirection] =
React.useState<TabActivationDirection>('none');

Expand All @@ -44,26 +46,39 @@ function useTabsRoot(parameters: useTabsRoot.Parameters): useTabsRoot.ReturnValu
[onValueChange, setValue],
);

const { subitems: tabPanels, contextValue: compoundComponentContextValue } = useCompoundParent<
any,
TabPanelMetadata
>();

const tabIdLookup = React.useRef<IdLookupFunction>(() => undefined);

const getTabPanelId = React.useCallback(
(tabValue: any) => {
return tabPanels.get(tabValue)?.id;
const getTabPanelIdByTabValueOrIndex = React.useCallback(
(tabValue: any | undefined, index: number) => {
if (tabValue === undefined && index < 0) {
return undefined;
}

for (const tabPanelMetadata of tabPanelMap.values()) {
// find by tabValue
if (tabValue !== undefined && tabPanelMetadata && tabValue === tabPanelMetadata?.value) {
return tabPanelMetadata.id;
}

// find by index
if (
tabValue === undefined &&
tabPanelMetadata?.index &&
tabPanelMetadata?.index === index
) {
return tabPanelMetadata.id;
}
}

return undefined;
},
[tabPanels],
[tabPanelMap],
);

const getTabId = React.useCallback((tabPanelId: any) => {
return tabIdLookup.current(tabPanelId);
const getTabIdByPanelValue = React.useCallback((tabPanelValue: any) => {
return getTabIdByPanelValueRef.current(tabPanelValue);
}, []);

const registerTabIdLookup = React.useCallback((lookupFunction: IdLookupFunction) => {
tabIdLookup.current = lookupFunction;
getTabIdByPanelValueRef.current = lookupFunction;
}, []);

const getRootProps: useTabsRoot.ReturnValue['getRootProps'] = React.useCallback(
Expand All @@ -75,14 +90,15 @@ function useTabsRoot(parameters: useTabsRoot.Parameters): useTabsRoot.ReturnValu
);

return {
compoundContext: compoundComponentContextValue,
getRootProps,
direction,
getTabId,
getTabPanelId,
getTabId: getTabIdByPanelValue,
getTabPanelIdByTabValueOrIndex,
onSelected,
registerTabIdLookup,
setTabPanelMap,
tabActivationDirection,
tabPanelRefs,
value,
};
}
Expand Down Expand Up @@ -113,7 +129,6 @@ namespace useTabsRoot {
getRootProps: (
externalProps?: React.ComponentPropsWithRef<'div'>,
) => React.ComponentPropsWithRef<'div'>;
compoundContext: CompoundComponentContextValue<any, TabPanelMetadata>;
/**
* The direction of the text.
*/
Expand All @@ -127,7 +142,10 @@ namespace useTabsRoot {
* Gets the id of the tab panel with the given value.
* @param value Value to find the tab panel for.
*/
getTabPanelId: (value: any) => string | undefined;
getTabPanelIdByTabValueOrIndex: (
tabValue: any | undefined,
tabIndex: number,
) => string | undefined;

/**
* Callback for setting new value.
Expand All @@ -141,10 +159,12 @@ namespace useTabsRoot {
* Registers a function that returns the id of the tab with the given value.
*/
registerTabIdLookup: (lookupFunction: (id: any) => string | undefined) => void;
setTabPanelMap: (map: Map<Node, (TabPanelMetadata & { index?: number | null }) | null>) => void;
/**
* The position of the active tab relative to the previously active tab.
*/
tabActivationDirection: TabActivationDirection;
tabPanelRefs: React.RefObject<(HTMLElement | null)[]>;
/**
* The currently selected tab's value.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-base/src/Tabs/Tab/Tab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ describe('<Tabs.Tab />', () => {
onSelected() {},
registerTabIdLookup() {},
getTabId: () => '',
getTabPanelId: () => '',
getTabPanelIdByTabValueOrIndex: () => '',
orientation: 'horizontal',
direction: 'ltr',
tabActivationDirection: 'none',
Expand Down
8 changes: 6 additions & 2 deletions packages/mui-base/src/Tabs/Tab/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ const Tab = React.forwardRef(function Tab(
) {
const { className, disabled = false, render, value: valueProp, ...other } = props;

const { value: selectedValue, getTabPanelId, orientation } = useTabsRootContext();
const {
value: selectedValue,
getTabPanelIdByTabValueOrIndex,
orientation,
} = useTabsRootContext();

const { selected, getRootProps } = useTab({
...props,
getTabPanelId,
getTabPanelIdByTabValueOrIndex,
isSelected: valueProp === selectedValue,
rootRef: forwardedRef,
});
Expand Down
4 changes: 2 additions & 2 deletions packages/mui-base/src/Tabs/Tab/useTab.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('useTab', () => {
const { getRootProps } = useTab({
rootRef,
isSelected: true,
getTabPanelId: () => undefined,
getTabPanelIdByTabValueOrIndex: () => undefined,
});
return <button {...getRootProps()} />;
}
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('useTab', () => {
const { getRootProps } = useTab({
rootRef,
isSelected: true,
getTabPanelId: () => undefined,
getTabPanelIdByTabValueOrIndex: () => undefined,
});
return (
<button
Expand Down
6 changes: 3 additions & 3 deletions packages/mui-base/src/Tabs/Tab/useTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function useTab(parameters: useTab.Parameters): useTab.ReturnValue {
value: valueParam,
rootRef: externalRef,
disabled = false,
getTabPanelId,
getTabPanelIdByTabValueOrIndex,
id: idParam,
isSelected,
} = parameters;
Expand Down Expand Up @@ -46,7 +46,7 @@ function useTab(parameters: useTab.Parameters): useTab.ReturnValue {

const handleRef = useForkRef(tabRef, externalRef, buttonRefHandler);

const tabPanelId = value !== undefined ? getTabPanelId(value) : undefined;
const tabPanelId = index > -1 ? getTabPanelIdByTabValueOrIndex(valueParam, index) : undefined;

const getRootProps = React.useCallback(
(externalProps = {}) => {
Expand Down Expand Up @@ -79,7 +79,7 @@ function useTab(parameters: useTab.Parameters): useTab.ReturnValue {
}

namespace useTab {
export interface Parameters extends Pick<TabsRootContext, 'getTabPanelId'> {
export interface Parameters extends Pick<TabsRootContext, 'getTabPanelIdByTabValueOrIndex'> {
/**
* The value of the tab.
* It's used to associate the tab with a tab panel(s) with the same value.
Expand Down
2 changes: 1 addition & 1 deletion packages/mui-base/src/Tabs/TabPanel/TabPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('<Tabs.Panel />', () => {
onSelected: () => {},
registerTabIdLookup() {},
getTabId: () => '',
getTabPanelId: () => '',
getTabPanelIdByTabValueOrIndex: () => '',
direction: 'ltr',
orientation: 'horizontal',
tabActivationDirection: 'none',
Expand Down
65 changes: 46 additions & 19 deletions packages/mui-base/src/Tabs/TabPanel/useTabPanel.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,76 @@
'use client';
import * as React from 'react';
import type { TabsRootContext } from '../Root/TabsRootContext';
import { useCompoundItem } from '../../useCompound';
import { useId } from '../../utils/useId';
import { useForkRef } from '../../utils/useForkRef';
import { mergeReactProps } from '../../utils/mergeReactProps';
import { useCompositeListContext } from '../../Composite/List/CompositeListContext';
import { useCompositeListItem } from '../../Composite/List/useCompositeListItem';

function tabPanelValueGenerator(otherTabPanelValues: Set<any>) {
return otherTabPanelValues.size;
export interface TabPanelMetadata {
id?: string;
value: useTabPanel.Parameters['value'];
}

function useTabPanel(parameters: useTabPanel.Parameters): useTabPanel.ReturnValue {
const {
getTabId,
id: idParam,
rootRef: externalRef,
selectedValue,
value: valueParam,
} = parameters;
const { rootRef: externalRef, selectedValue, value: valueParam } = parameters;

const id = useId(idParam);
const ref = React.useRef<HTMLElement>(null);
const handleRef = useForkRef(ref, externalRef);
const metadata = React.useMemo(() => ({ id, ref }), [id]);
const id = useId();

const { id: value } = useCompoundItem(valueParam ?? tabPanelValueGenerator, metadata);
const metadata = React.useMemo(
() => ({
id,
value: valueParam,
}),
[id, valueParam],
);

// tabPanelCompositeMap
const { map: tabPanelCompositeMap } = useCompositeListContext();

const { ref: listItemRef, index } = useCompositeListItem<TabPanelMetadata>({
metadata,
});

const tabPanelValue = valueParam ?? index;

const panelRef = React.useRef<HTMLElement>(null);
const handleRef = useForkRef(panelRef, listItemRef, externalRef);

const hidden = tabPanelValue !== selectedValue;

// const correspondingTabId = tabPanelValue !== undefined ? getTabId(tabPanelValue) : undefined;

const tabId = React.useMemo(() => {
if (index < 0) {
return undefined;
}

const hidden = value !== selectedValue;
// TODO: this is incorrect, should be tabButtonMap
for (const value of tabPanelCompositeMap.values()) {
if (value.index > -1 && value.index === index) {
return value.id;
}
}

const correspondingTabId = value !== undefined ? getTabId(value) : undefined;
return undefined;
}, [index, tabPanelCompositeMap]);

const getRootProps = React.useCallback(
(
externalProps: React.ComponentPropsWithoutRef<'div'> = {},
): React.ComponentPropsWithRef<'div'> => {
return mergeReactProps(externalProps, {
'aria-labelledby': correspondingTabId ?? undefined,
'aria-labelledby': undefined,
hidden,
id: id ?? undefined,
role: 'tabpanel',
tabIndex: hidden ? -1 : 0,
ref: handleRef,
'data-index': index,
});
},
[correspondingTabId, handleRef, hidden, id],
[handleRef, hidden, id, index],
);

return {
Expand Down
Loading

0 comments on commit c31706a

Please sign in to comment.