From 40aaf7b72400904b48bc0d31752bff7e49a05499 Mon Sep 17 00:00:00 2001 From: Albert Yu Date: Thu, 14 Sep 2023 23:21:23 +0800 Subject: [PATCH] [base-ui][useList] Accept arbitrary external props and forward to root (#38848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Albert Yu Co-authored-by: MichaƂ Dudak --- .../mui-base/src/useList/useList.test.tsx | 27 +++++++++++++++ packages/mui-base/src/useList/useList.ts | 25 ++++++++------ .../mui-base/src/useList/useList.types.ts | 15 +++++--- packages/mui-base/src/useList/useListItem.ts | 34 +++++++++++-------- .../mui-base/src/useList/useListItem.types.ts | 13 ++++--- 5 files changed, 77 insertions(+), 37 deletions(-) diff --git a/packages/mui-base/src/useList/useList.test.tsx b/packages/mui-base/src/useList/useList.test.tsx index 02b12be6f39bb6..31689bb6e4a82e 100644 --- a/packages/mui-base/src/useList/useList.test.tsx +++ b/packages/mui-base/src/useList/useList.test.tsx @@ -85,4 +85,31 @@ describe('useList', () => { }), ); }); + + describe('external props', () => { + it('should pass arbitrary props to the root slot', () => { + const handleClick = spy(); + + function Listbox() { + const { getRootProps } = useList({ + items: [], + getItemId: () => undefined, + }); + return ( +
+ ); + } + + const { getByRole } = render(); + + const listbox = getByRole('listbox'); + expect(listbox).to.have.attribute('data-testid', 'test-listbox'); + + fireEvent.click(listbox); + expect(handleClick.callCount).to.equal(1); + }); + }); }); diff --git a/packages/mui-base/src/useList/useList.ts b/packages/mui-base/src/useList/useList.ts index 8451d1bb4b8023..fca8c10f364291 100644 --- a/packages/mui-base/src/useList/useList.ts +++ b/packages/mui-base/src/useList/useList.ts @@ -20,10 +20,11 @@ import { StateComparers, } from '../utils/useControllableReducer.types'; import { areArraysEqual } from '../utils/areArraysEqual'; -import { EventHandlers } from '../utils/types'; import { useLatest } from '../utils/useLatest'; import { useTextNavigation } from '../utils/useTextNavigation'; import { MuiCancellableEvent } from '../utils/MuiCancellableEvent'; +import { extractEventHandlers } from '../utils/extractEventHandlers'; +import { EventHandlers } from '../utils/types'; const EMPTY_OBJECT = {}; const NOOP = () => {}; @@ -276,9 +277,9 @@ function useList< }, [highlightedValue, notifyHighlightChanged]); const createHandleKeyDown = - (other: Record>) => + (externalHandlers: EventHandlers) => (event: React.KeyboardEvent & MuiCancellableEvent) => { - other.onKeyDown?.(event); + externalHandlers.onKeyDown?.(event); if (event.defaultMuiPrevented) { return; @@ -314,9 +315,9 @@ function useList< }; const createHandleBlur = - (other: Record>) => + (externalHandlers: EventHandlers) => (event: React.FocusEvent & MuiCancellableEvent) => { - other.onBlur?.(event); + externalHandlers.onBlur?.(event); if (event.defaultMuiPrevented) { return; @@ -333,19 +334,21 @@ function useList< }); }; - const getRootProps = ( - otherHandlers: TOther = {} as TOther, - ): UseListRootSlotProps => { + const getRootProps = = {}>( + externalProps: ExternalProps = {} as ExternalProps, + ): UseListRootSlotProps => { + const externalEventHandlers = extractEventHandlers(externalProps); return { - ...otherHandlers, + ...externalProps, 'aria-activedescendant': focusManagement === 'activeDescendant' && highlightedValue != null ? getItemId!(highlightedValue) : undefined, - onBlur: createHandleBlur(otherHandlers), - onKeyDown: createHandleKeyDown(otherHandlers), tabIndex: focusManagement === 'DOM' ? -1 : 0, ref: handleRef, + ...externalEventHandlers, + onBlur: createHandleBlur(externalEventHandlers), + onKeyDown: createHandleKeyDown(externalEventHandlers), }; }; diff --git a/packages/mui-base/src/useList/useList.types.ts b/packages/mui-base/src/useList/useList.types.ts index 83285651a3119a..b6df0f4058ead2 100644 --- a/packages/mui-base/src/useList/useList.types.ts +++ b/packages/mui-base/src/useList/useList.types.ts @@ -6,7 +6,6 @@ import { ControllableReducerAction, StateChangeCallback, } from '../utils/useControllableReducer.types'; -import { EventHandlers } from '../utils'; import type { ListContextValue } from './ListContext'; import { MuiCancellableEventHandler } from '../utils/MuiCancellableEvent'; @@ -106,6 +105,7 @@ export interface UseListParameters< /** * The focus management strategy used by the list. * Controls the attributes used to set focus on the list items. + * @default 'activeDescendant' */ focusManagement?: FocusManagementType; /** @@ -248,7 +248,7 @@ interface UseListRootSlotOwnProps { ref: React.RefCallback | null; } -export type UseListRootSlotProps = TOther & UseListRootSlotOwnProps; +export type UseListRootSlotProps = ExternalProps & UseListRootSlotOwnProps; export interface UseListReturnValue< ItemValue, @@ -257,9 +257,14 @@ export interface UseListReturnValue< > { contextValue: ListContextValue; dispatch: (action: CustomAction | ListAction) => void; - getRootProps: ( - otherHandlers?: TOther, - ) => UseListRootSlotProps; + /** + * 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: = {}>( + externalProps?: ExternalProps, + ) => UseListRootSlotProps; rootRef: React.RefCallback | null; state: State; } diff --git a/packages/mui-base/src/useList/useListItem.ts b/packages/mui-base/src/useList/useListItem.ts index 03412478a40570..c3564637802ce4 100644 --- a/packages/mui-base/src/useList/useListItem.ts +++ b/packages/mui-base/src/useList/useListItem.ts @@ -4,8 +4,9 @@ import { unstable_useForkRef as useForkRef, unstable_useEnhancedEffect as useEnhancedEffect, } from '@mui/utils'; -import { EventHandlers } from '../utils/types'; import { useForcedRerendering } from '../utils/useForcedRerendering'; +import { extractEventHandlers } from '../utils/extractEventHandlers'; +import { EventHandlers } from '../utils/types'; import { UseListItemParameters, UseListItemReturnValue } from './useListItem.types'; import { ListActionTypes } from './listActions.types'; import { ListContext } from './ListContext'; @@ -65,8 +66,8 @@ export function useListItem( }, [registerSelectionChangeHandler, rerender, selected, item]); const createHandleClick = React.useCallback( - (other: Record>) => (event: React.MouseEvent) => { - other.onClick?.(event); + (externalHandlers: EventHandlers) => (event: React.MouseEvent) => { + externalHandlers.onClick?.(event); if (event.defaultPrevented) { return; } @@ -81,8 +82,8 @@ export function useListItem( ); const createHandlePointerOver = React.useCallback( - (other: Record>) => (event: React.PointerEvent) => { - other.onMouseOver?.(event); + (externalHandlers: EventHandlers) => (event: React.PointerEvent) => { + externalHandlers.onMouseOver?.(event); if (event.defaultPrevented) { return; } @@ -101,15 +102,20 @@ export function useListItem( tabIndex = highlighted ? 0 : -1; } - const getRootProps = ( - otherHandlers: TOther = {} as TOther, - ) => ({ - ...otherHandlers, - onClick: createHandleClick(otherHandlers), - onPointerOver: handlePointerOverEvents ? createHandlePointerOver(otherHandlers) : undefined, - ref: handleRef, - tabIndex, - }); + const getRootProps = >( + externalProps: ExternalProps = {} as ExternalProps, + ) => { + const externalEventHandlers = extractEventHandlers(externalProps); + return { + ...externalProps, + onClick: createHandleClick(externalEventHandlers), + onPointerOver: handlePointerOverEvents + ? createHandlePointerOver(externalEventHandlers) + : undefined, + ref: handleRef, + tabIndex, + }; + }; return { getRootProps, diff --git a/packages/mui-base/src/useList/useListItem.types.ts b/packages/mui-base/src/useList/useListItem.types.ts index b2e9fe8bc756df..94599d0aa4cc1e 100644 --- a/packages/mui-base/src/useList/useListItem.types.ts +++ b/packages/mui-base/src/useList/useListItem.types.ts @@ -1,5 +1,3 @@ -import { EventHandlers } from '../utils'; - export interface UseListItemParameters { /** * If `true`, the list item will dispatch the `itemHover` action on pointer over. @@ -27,17 +25,18 @@ interface UseListItemRootSlotOwnProps { tabIndex?: number; } -export type UseListItemRootSlotProps = TOther & UseListItemRootSlotOwnProps; +export type UseListItemRootSlotProps = ExternalProps & + UseListItemRootSlotOwnProps; export interface UseListItemReturnValue { /** * Resolver for the root slot's props. - * @param otherHandlers event handlers for the root slot + * @param externalProps additional props to be forwarded to the root slot * @returns props that should be spread on the root slot */ - getRootProps: ( - otherHandlers?: TOther, - ) => UseListItemRootSlotProps; + getRootProps: = {}>( + externalProps?: ExternalProps, + ) => UseListItemRootSlotProps; /** * If `true`, the current item is highlighted. */