Skip to content

Commit

Permalink
[base-ui][useTabs] Align external props handling for useTab/useTabPan…
Browse files Browse the repository at this point in the history
…el/useTabsList (#39037)
  • Loading branch information
mj12albert authored Oct 3, 2023
1 parent bfb4a34 commit 3de335f
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 38 deletions.
4 changes: 2 additions & 2 deletions docs/pages/base-ui/api/use-tab-panel.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"returnValue": {
"getRootProps": {
"type": {
"name": "() => UseTabPanelRootSlotProps",
"description": "() => UseTabPanelRootSlotProps"
"name": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabPanelRootSlotProps<ExternalProps>",
"description": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabPanelRootSlotProps<ExternalProps>"
},
"required": true
},
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/base-ui/api/use-tab.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
"focusVisible": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
"getRootProps": {
"type": {
"name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseTabRootSlotProps<TOther>",
"description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseTabRootSlotProps<TOther>"
"name": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabRootSlotProps<ExternalProps>",
"description": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabRootSlotProps<ExternalProps>"
},
"required": true
},
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/base-ui/api/use-tabs-list.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
},
"getRootProps": {
"type": {
"name": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseTabsListRootSlotProps<TOther>",
"description": "<TOther extends Record<string, any> = {}>(externalProps?: TOther) => UseTabsListRootSlotProps<TOther>"
"name": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabsListRootSlotProps<ExternalProps>",
"description": "<ExternalProps extends Record<string, unknown> = {}>(externalProps?: ExternalProps) => UseTabsListRootSlotProps<ExternalProps>"
},
"required": true
},
Expand Down
63 changes: 63 additions & 0 deletions packages/mui-base/src/useTab/useTab.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, screen, fireEvent } from 'test/utils';
import { Tabs } from '../Tabs';
import { TabsList } from '../TabsList';
import { useTab } from './useTab';

describe('useTab', () => {
const { render } = createRenderer();
describe('getRootProps', () => {
it('returns props for root slot', () => {
function TestTab() {
const rootRef = React.createRef<HTMLDivElement>();
const { getRootProps } = useTab({ rootRef });
return <div {...getRootProps()} />;
}

function Test() {
return (
<Tabs>
<TabsList>
<TestTab />
</TabsList>
</Tabs>
);
}

const { getByRole } = render(<Test />);

const tab = getByRole('tab');
expect(tab).not.to.equal(null);
});

it('forwards external props including event handlers', () => {
const handleClick = spy();

function TestTab() {
const rootRef = React.createRef<HTMLDivElement>();
const { getRootProps } = useTab({ rootRef });
return <div {...getRootProps({ 'data-testid': 'test-tab', onClick: handleClick })} />;
}

function Test() {
return (
<Tabs>
<TabsList>
<TestTab />
</TabsList>
</Tabs>
);
}

render(<Test />);

const tab = screen.getByTestId('test-tab');
expect(tab).not.to.equal(null);

fireEvent.click(tab);
expect(handleClick.callCount).to.equal(1);
});
});
});
23 changes: 9 additions & 14 deletions packages/mui-base/src/useTab/useTab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import * as React from 'react';
import { unstable_useId as useId, unstable_useForkRef as useForkRef } from '@mui/utils';
import { useTabsContext } from '../Tabs';
import { UseTabParameters, UseTabReturnValue, UseTabRootSlotProps } from './useTab.types';
import { EventHandlers } from '../utils';
import { extractEventHandlers } from '../utils/extractEventHandlers';
import { useCompoundItem } from '../utils/useCompoundItem';
import { useListItem } from '../useList';
import { useButton } from '../useButton';
import { TabMetadata } from '../useTabs';
import { combineHooksSlotProps } from '../utils/combineHooksSlotProps';

function tabValueGenerator(otherTabValues: Set<string | number>) {
return otherTabValues.size;
Expand Down Expand Up @@ -64,21 +65,15 @@ function useTab(parameters: UseTabParameters): UseTabReturnValue {

const tabPanelId = value !== undefined ? getTabPanelId(value) : undefined;

const getRootProps = <TOther extends EventHandlers>(
otherHandlers: TOther = {} as TOther,
): UseTabRootSlotProps<TOther> => {
const resolvedTabProps = {
...otherHandlers,
...getTabProps(otherHandlers),
};

const resolvedButtonProps = {
...resolvedTabProps,
...getButtonProps(resolvedTabProps),
};
const getRootProps = <ExternalProps extends Record<string, unknown>>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTabRootSlotProps<ExternalProps> => {
const externalEventHandlers = extractEventHandlers(externalProps);
const getCombinedRootProps = combineHooksSlotProps(getTabProps, getButtonProps);

return {
...resolvedButtonProps,
...externalProps,
...getCombinedRootProps(externalEventHandlers),
role: 'tab',
'aria-controls': tabPanelId,
'aria-selected': selected,
Expand Down
8 changes: 4 additions & 4 deletions packages/mui-base/src/useTab/useTab.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export interface UseTabParameters {
rootRef?: React.Ref<Element>;
}

export type UseTabRootSlotProps<TOther = {}> = UseButtonRootSlotProps<TOther> & {
export type UseTabRootSlotProps<ExternalProps = {}> = UseButtonRootSlotProps<ExternalProps> & {
'aria-controls': React.AriaAttributes['aria-controls'];
'aria-selected': React.AriaAttributes['aria-selected'];
id: string | undefined;
Expand All @@ -45,9 +45,9 @@ export interface UseTabReturnValue {
* @param externalProps props for the root slot
* @returns props that should be spread on the root slot
*/
getRootProps: <TOther extends Record<string, any> = {}>(
externalProps?: TOther,
) => UseTabRootSlotProps<TOther>;
getRootProps: <ExternalProps extends Record<string, unknown> = {}>(
externalProps?: ExternalProps,
) => UseTabRootSlotProps<ExternalProps>;
/**
* If `true`, the tab is active (as in `:active` pseudo-class; in other words, pressed).
*/
Expand Down
66 changes: 66 additions & 0 deletions packages/mui-base/src/useTabPanel/useTabPanel.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, screen, fireEvent } from 'test/utils';
import { Tabs } from '../Tabs';
import { Tab } from '../Tab';
import { TabsList } from '../TabsList';
import { useTabPanel } from './useTabPanel';

describe('useTabPanel', () => {
const { render } = createRenderer();
describe('getRootProps', () => {
it('returns props for root slot', () => {
const rootRef = React.createRef();
function TestTabPanel() {
const { getRootProps } = useTabPanel({ rootRef, id: 'test-tabpanel', value: 0 });
return <div {...getRootProps()} />;
}

function Test() {
return (
<Tabs>
<TabsList>
<Tab value={0}>0</Tab>
</TabsList>
<TestTabPanel />
</Tabs>
);
}

render(<Test />);

const tabpanel = document.querySelector('#test-tabpanel');
expect(tabpanel).to.equal(rootRef.current);
});

it('forwards external props including event handlers', () => {
const handleClick = spy();
const rootRef = React.createRef();

function TestTabPanel() {
const { getRootProps } = useTabPanel({ rootRef, value: 0 });
return <div {...getRootProps({ 'data-testid': 'test-tabpanel', onClick: handleClick })} />;
}

function Test() {
return (
<Tabs>
<TabsList>
<Tab value={0}>0</Tab>
</TabsList>
<TestTabPanel />
</Tabs>
);
}

render(<Test />);

const tabPanel = screen.getByTestId('test-tabpanel');
expect(tabPanel).not.to.equal(null);

fireEvent.click(tabPanel);
expect(handleClick.callCount).to.equal(1);
});
});
});
11 changes: 9 additions & 2 deletions packages/mui-base/src/useTabPanel/useTabPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import * as React from 'react';
import { unstable_useId as useId, unstable_useForkRef as useForkRef } from '@mui/utils';
import { useTabsContext } from '../Tabs';
import { useCompoundItem } from '../utils/useCompoundItem';
import { UseTabPanelParameters, UseTabPanelReturnValue } from './useTabPanel.types';
import {
UseTabPanelParameters,
UseTabPanelReturnValue,
UseTabPanelRootSlotProps,
} from './useTabPanel.types';

function tabPanelValueGenerator(otherTabPanelValues: Set<string | number>) {
return otherTabPanelValues.size;
Expand Down Expand Up @@ -40,11 +44,14 @@ function useTabPanel(parameters: UseTabPanelParameters): UseTabPanelReturnValue

const correspondingTabId = value !== undefined ? getTabId(value) : undefined;

const getRootProps = () => {
const getRootProps = <ExternalProps extends Record<string, any> = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTabPanelRootSlotProps<ExternalProps> => {
return {
'aria-labelledby': correspondingTabId ?? undefined,
hidden,
id: id ?? undefined,
...externalProps,
ref: handleRef,
};
};
Expand Down
10 changes: 8 additions & 2 deletions packages/mui-base/src/useTabPanel/useTabPanel.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,28 @@ export interface UseTabPanelParameters {
value?: number | string;
}

export interface UseTabPanelRootSlotProps {
interface UseTabPanelRootSlotOwnProps {
'aria-labelledby'?: string;
hidden?: boolean;
id?: string;
ref: React.Ref<HTMLElement>;
}

export type UseTabPanelRootSlotProps<ExternalProps = {}> = ExternalProps &
UseTabPanelRootSlotOwnProps;

export interface UseTabPanelReturnValue {
/**
* If `true`, it indicates that the tab panel will be hidden.
*/
hidden: boolean;
/**
* Resolver for the root slot's props.
* @param externalProps additional props for the root slot
* @returns props that should be spread on the root slot
*/
getRootProps: () => UseTabPanelRootSlotProps;
getRootProps: <ExternalProps extends Record<string, unknown> = {}>(
externalProps?: ExternalProps,
) => UseTabPanelRootSlotProps<ExternalProps>;
rootRef: React.Ref<HTMLElement>;
}
58 changes: 58 additions & 0 deletions packages/mui-base/src/useTabsList/useTabsList.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
import { createRenderer, screen, fireEvent } from 'test/utils';
import { Tabs } from '../Tabs';
import { useTabsList } from './useTabsList';

describe('useTabsList', () => {
const { render } = createRenderer();
describe('getRootProps', () => {
it('returns props for root slot', () => {
function TestTabsList() {
const rootRef = React.createRef<HTMLDivElement>();
const { getRootProps } = useTabsList({ rootRef });
return <div {...getRootProps()} />;
}

function Test() {
return (
<Tabs>
<TestTabsList />
</Tabs>
);
}

const { getByRole } = render(<Test />);

const tablist = getByRole('tablist');
expect(tablist).not.to.equal(null);
});

it('forwards external props including event handlers', () => {
const handleClick = spy();

function TestTabsList() {
const rootRef = React.createRef<HTMLDivElement>();
const { getRootProps } = useTabsList({ rootRef });
return <div {...getRootProps({ 'data-testid': 'test-tabslist', onClick: handleClick })} />;
}

function Test() {
return (
<Tabs>
<TestTabsList />
</Tabs>
);
}

render(<Test />);

const tabsList = screen.getByTestId('test-tabslist');
expect(tabsList).not.to.equal(null);

fireEvent.click(tabsList);
expect(handleClick.callCount).to.equal(1);
});
});
});
11 changes: 5 additions & 6 deletions packages/mui-base/src/useTabsList/useTabsList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
UseTabsListRootSlotProps,
ValueChangeAction,
} from './useTabsList.types';
import { EventHandlers } from '../utils';
import { useCompoundParent } from '../utils/useCompound';
import { TabMetadata } from '../useTabs/useTabs';
import { useList, ListState, UseListParameters } from '../useList';
Expand Down Expand Up @@ -142,12 +141,12 @@ function useTabsList(parameters: UseTabsListParameters): UseTabsListReturnValue
}
}, [dispatch, value]);

const getRootProps = <TOther extends EventHandlers = {}>(
otherHandlers: TOther = {} as TOther,
): UseTabsListRootSlotProps<TOther> => {
const getRootProps = <ExternalProps extends Record<string, unknown> = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseTabsListRootSlotProps<ExternalProps> => {
return {
...otherHandlers,
...getListboxRootProps(otherHandlers),
...externalProps,
...getListboxRootProps(externalProps),
'aria-orientation': orientation === 'vertical' ? 'vertical' : undefined,
role: 'tablist',
};
Expand Down
8 changes: 4 additions & 4 deletions packages/mui-base/src/useTabsList/useTabsList.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface UseTabsListParameters {
rootRef: React.Ref<Element>;
}

export type UseTabsListRootSlotProps<TOther = {}> = TOther & {
export type UseTabsListRootSlotProps<ExternalProps = {}> = ExternalProps & {
'aria-label'?: React.AriaAttributes['aria-label'];
'aria-labelledby'?: React.AriaAttributes['aria-labelledby'];
'aria-orientation'?: React.AriaAttributes['aria-orientation'];
Expand All @@ -33,9 +33,9 @@ export interface UseTabsListReturnValue {
* @param externalProps props for the root slot
* @returns props that should be spread on the root slot
*/
getRootProps: <TOther extends Record<string, any> = {}>(
externalProps?: TOther,
) => UseTabsListRootSlotProps<TOther>;
getRootProps: <ExternalProps extends Record<string, unknown> = {}>(
externalProps?: ExternalProps,
) => UseTabsListRootSlotProps<ExternalProps>;
/**
* The value of the currently highlighted tab.
*/
Expand Down

0 comments on commit 3de335f

Please sign in to comment.