From 3de335fd73c11699fc1ee1ec817226a1733df694 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Tue, 3 Oct 2023 20:57:45 +0800 Subject: [PATCH] [base-ui][useTabs] Align external props handling for useTab/useTabPanel/useTabsList (#39037) --- docs/pages/base-ui/api/use-tab-panel.json | 4 +- docs/pages/base-ui/api/use-tab.json | 4 +- docs/pages/base-ui/api/use-tabs-list.json | 4 +- packages/mui-base/src/useTab/useTab.test.tsx | 63 ++++++++++++++++++ packages/mui-base/src/useTab/useTab.ts | 23 +++---- packages/mui-base/src/useTab/useTab.types.ts | 8 +-- .../src/useTabPanel/useTabPanel.test.js | 66 +++++++++++++++++++ .../mui-base/src/useTabPanel/useTabPanel.ts | 11 +++- .../src/useTabPanel/useTabPanel.types.ts | 10 ++- .../src/useTabsList/useTabsList.test.tsx | 58 ++++++++++++++++ .../mui-base/src/useTabsList/useTabsList.ts | 11 ++-- .../src/useTabsList/useTabsList.types.ts | 8 +-- 12 files changed, 232 insertions(+), 38 deletions(-) create mode 100644 packages/mui-base/src/useTab/useTab.test.tsx create mode 100644 packages/mui-base/src/useTabPanel/useTabPanel.test.js create mode 100644 packages/mui-base/src/useTabsList/useTabsList.test.tsx diff --git a/docs/pages/base-ui/api/use-tab-panel.json b/docs/pages/base-ui/api/use-tab-panel.json index 6a42b1c8137830..1a3a16364f8c72 100644 --- a/docs/pages/base-ui/api/use-tab-panel.json +++ b/docs/pages/base-ui/api/use-tab-panel.json @@ -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 }, diff --git a/docs/pages/base-ui/api/use-tab.json b/docs/pages/base-ui/api/use-tab.json index 1650f8d65a5314..3f8cbe9fad95d6 100644 --- a/docs/pages/base-ui/api/use-tab.json +++ b/docs/pages/base-ui/api/use-tab.json @@ -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 }, diff --git a/docs/pages/base-ui/api/use-tabs-list.json b/docs/pages/base-ui/api/use-tabs-list.json index e67be1be8e0e09..a853f2012c8b7b 100644 --- a/docs/pages/base-ui/api/use-tabs-list.json +++ b/docs/pages/base-ui/api/use-tabs-list.json @@ -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 }, diff --git a/packages/mui-base/src/useTab/useTab.test.tsx b/packages/mui-base/src/useTab/useTab.test.tsx new file mode 100644 index 00000000000000..ba64678a81591b --- /dev/null +++ b/packages/mui-base/src/useTab/useTab.test.tsx @@ -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(); + const { getRootProps } = useTab({ rootRef }); + return
; + } + + function Test() { + return ( + + + + + + ); + } + + const { getByRole } = render(); + + 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(); + const { getRootProps } = useTab({ rootRef }); + return
; + } + + function Test() { + return ( + + + + + + ); + } + + render(); + + const tab = screen.getByTestId('test-tab'); + expect(tab).not.to.equal(null); + + fireEvent.click(tab); + expect(handleClick.callCount).to.equal(1); + }); + }); +}); diff --git a/packages/mui-base/src/useTab/useTab.ts b/packages/mui-base/src/useTab/useTab.ts index 5e8f09b4ee9afc..9954e99a621137 100644 --- a/packages/mui-base/src/useTab/useTab.ts +++ b/packages/mui-base/src/useTab/useTab.ts @@ -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) { return otherTabValues.size; @@ -64,21 +65,15 @@ function useTab(parameters: UseTabParameters): UseTabReturnValue { const tabPanelId = value !== undefined ? getTabPanelId(value) : undefined; - const getRootProps = ( - otherHandlers: TOther = {} as TOther, - ): UseTabRootSlotProps => { - const resolvedTabProps = { - ...otherHandlers, - ...getTabProps(otherHandlers), - }; - - const resolvedButtonProps = { - ...resolvedTabProps, - ...getButtonProps(resolvedTabProps), - }; + const getRootProps = >( + externalProps: ExternalProps = {} as ExternalProps, + ): UseTabRootSlotProps => { + const externalEventHandlers = extractEventHandlers(externalProps); + const getCombinedRootProps = combineHooksSlotProps(getTabProps, getButtonProps); return { - ...resolvedButtonProps, + ...externalProps, + ...getCombinedRootProps(externalEventHandlers), role: 'tab', 'aria-controls': tabPanelId, 'aria-selected': selected, diff --git a/packages/mui-base/src/useTab/useTab.types.ts b/packages/mui-base/src/useTab/useTab.types.ts index eb01fc1a7ccc80..9ab0c7b0c6117d 100644 --- a/packages/mui-base/src/useTab/useTab.types.ts +++ b/packages/mui-base/src/useTab/useTab.types.ts @@ -31,7 +31,7 @@ export interface UseTabParameters { rootRef?: React.Ref; } -export type UseTabRootSlotProps = UseButtonRootSlotProps & { +export type UseTabRootSlotProps = UseButtonRootSlotProps & { 'aria-controls': React.AriaAttributes['aria-controls']; 'aria-selected': React.AriaAttributes['aria-selected']; id: string | undefined; @@ -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: = {}>( - externalProps?: TOther, - ) => UseTabRootSlotProps; + getRootProps: = {}>( + externalProps?: ExternalProps, + ) => UseTabRootSlotProps; /** * If `true`, the tab is active (as in `:active` pseudo-class; in other words, pressed). */ diff --git a/packages/mui-base/src/useTabPanel/useTabPanel.test.js b/packages/mui-base/src/useTabPanel/useTabPanel.test.js new file mode 100644 index 00000000000000..3914738c36d629 --- /dev/null +++ b/packages/mui-base/src/useTabPanel/useTabPanel.test.js @@ -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
; + } + + function Test() { + return ( + + + 0 + + + + ); + } + + render(); + + 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
; + } + + function Test() { + return ( + + + 0 + + + + ); + } + + render(); + + const tabPanel = screen.getByTestId('test-tabpanel'); + expect(tabPanel).not.to.equal(null); + + fireEvent.click(tabPanel); + expect(handleClick.callCount).to.equal(1); + }); + }); +}); diff --git a/packages/mui-base/src/useTabPanel/useTabPanel.ts b/packages/mui-base/src/useTabPanel/useTabPanel.ts index cd6006e55c99b5..0dc8454d59c8b8 100644 --- a/packages/mui-base/src/useTabPanel/useTabPanel.ts +++ b/packages/mui-base/src/useTabPanel/useTabPanel.ts @@ -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) { return otherTabPanelValues.size; @@ -40,11 +44,14 @@ function useTabPanel(parameters: UseTabPanelParameters): UseTabPanelReturnValue const correspondingTabId = value !== undefined ? getTabId(value) : undefined; - const getRootProps = () => { + const getRootProps = = {}>( + externalProps: ExternalProps = {} as ExternalProps, + ): UseTabPanelRootSlotProps => { return { 'aria-labelledby': correspondingTabId ?? undefined, hidden, id: id ?? undefined, + ...externalProps, ref: handleRef, }; }; diff --git a/packages/mui-base/src/useTabPanel/useTabPanel.types.ts b/packages/mui-base/src/useTabPanel/useTabPanel.types.ts index 7cba93e9d8963f..446d9ee8419683 100644 --- a/packages/mui-base/src/useTabPanel/useTabPanel.types.ts +++ b/packages/mui-base/src/useTabPanel/useTabPanel.types.ts @@ -13,13 +13,16 @@ export interface UseTabPanelParameters { value?: number | string; } -export interface UseTabPanelRootSlotProps { +interface UseTabPanelRootSlotOwnProps { 'aria-labelledby'?: string; hidden?: boolean; id?: string; ref: React.Ref; } +export type UseTabPanelRootSlotProps = ExternalProps & + UseTabPanelRootSlotOwnProps; + export interface UseTabPanelReturnValue { /** * If `true`, it indicates that the tab panel will be hidden. @@ -27,8 +30,11 @@ export interface UseTabPanelReturnValue { 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?: ExternalProps, + ) => UseTabPanelRootSlotProps; rootRef: React.Ref; } diff --git a/packages/mui-base/src/useTabsList/useTabsList.test.tsx b/packages/mui-base/src/useTabsList/useTabsList.test.tsx new file mode 100644 index 00000000000000..5f7d2755de0fe4 --- /dev/null +++ b/packages/mui-base/src/useTabsList/useTabsList.test.tsx @@ -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(); + const { getRootProps } = useTabsList({ rootRef }); + return
; + } + + function Test() { + return ( + + + + ); + } + + const { getByRole } = render(); + + 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(); + const { getRootProps } = useTabsList({ rootRef }); + return
; + } + + function Test() { + return ( + + + + ); + } + + render(); + + const tabsList = screen.getByTestId('test-tabslist'); + expect(tabsList).not.to.equal(null); + + fireEvent.click(tabsList); + expect(handleClick.callCount).to.equal(1); + }); + }); +}); diff --git a/packages/mui-base/src/useTabsList/useTabsList.ts b/packages/mui-base/src/useTabsList/useTabsList.ts index eb5fa4f290594f..9507508e1bb50e 100644 --- a/packages/mui-base/src/useTabsList/useTabsList.ts +++ b/packages/mui-base/src/useTabsList/useTabsList.ts @@ -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'; @@ -142,12 +141,12 @@ function useTabsList(parameters: UseTabsListParameters): UseTabsListReturnValue } }, [dispatch, value]); - const getRootProps = ( - otherHandlers: TOther = {} as TOther, - ): UseTabsListRootSlotProps => { + const getRootProps = = {}>( + externalProps: ExternalProps = {} as ExternalProps, + ): UseTabsListRootSlotProps => { return { - ...otherHandlers, - ...getListboxRootProps(otherHandlers), + ...externalProps, + ...getListboxRootProps(externalProps), 'aria-orientation': orientation === 'vertical' ? 'vertical' : undefined, role: 'tablist', }; diff --git a/packages/mui-base/src/useTabsList/useTabsList.types.ts b/packages/mui-base/src/useTabsList/useTabsList.types.ts index 49a273fc4d8ca6..9459da11c885ed 100644 --- a/packages/mui-base/src/useTabsList/useTabsList.types.ts +++ b/packages/mui-base/src/useTabsList/useTabsList.types.ts @@ -9,7 +9,7 @@ export interface UseTabsListParameters { rootRef: React.Ref; } -export type UseTabsListRootSlotProps = TOther & { +export type UseTabsListRootSlotProps = ExternalProps & { 'aria-label'?: React.AriaAttributes['aria-label']; 'aria-labelledby'?: React.AriaAttributes['aria-labelledby']; 'aria-orientation'?: React.AriaAttributes['aria-orientation']; @@ -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: = {}>( - externalProps?: TOther, - ) => UseTabsListRootSlotProps; + getRootProps: = {}>( + externalProps?: ExternalProps, + ) => UseTabsListRootSlotProps; /** * The value of the currently highlighted tab. */