Skip to content

Commit

Permalink
[base-ui][useList] Accept arbitrary external props and forward to root (
Browse files Browse the repository at this point in the history
mui#38848)

Signed-off-by: Albert Yu <[email protected]>
Co-authored-by: Michał Dudak <[email protected]>
  • Loading branch information
mj12albert and michaldudak authored Sep 14, 2023
1 parent ad905f5 commit 40aaf7b
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 37 deletions.
27 changes: 27 additions & 0 deletions packages/mui-base/src/useList/useList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
role="listbox"
{...getRootProps({ 'data-testid': 'test-listbox', onClick: handleClick })}
/>
);
}

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

const listbox = getByRole('listbox');
expect(listbox).to.have.attribute('data-testid', 'test-listbox');

fireEvent.click(listbox);
expect(handleClick.callCount).to.equal(1);
});
});
});
25 changes: 14 additions & 11 deletions packages/mui-base/src/useList/useList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {};
Expand Down Expand Up @@ -276,9 +277,9 @@ function useList<
}, [highlightedValue, notifyHighlightChanged]);

const createHandleKeyDown =
(other: Record<string, React.EventHandler<any>>) =>
(externalHandlers: EventHandlers) =>
(event: React.KeyboardEvent<HTMLElement> & MuiCancellableEvent) => {
other.onKeyDown?.(event);
externalHandlers.onKeyDown?.(event);

if (event.defaultMuiPrevented) {
return;
Expand Down Expand Up @@ -314,9 +315,9 @@ function useList<
};

const createHandleBlur =
(other: Record<string, React.EventHandler<any>>) =>
(externalHandlers: EventHandlers) =>
(event: React.FocusEvent<HTMLElement> & MuiCancellableEvent) => {
other.onBlur?.(event);
externalHandlers.onBlur?.(event);

if (event.defaultMuiPrevented) {
return;
Expand All @@ -333,19 +334,21 @@ function useList<
});
};

const getRootProps = <TOther extends EventHandlers = {}>(
otherHandlers: TOther = {} as TOther,
): UseListRootSlotProps<TOther> => {
const getRootProps = <ExternalProps extends Record<string, any> = {}>(
externalProps: ExternalProps = {} as ExternalProps,
): UseListRootSlotProps<ExternalProps> => {
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),
};
};

Expand Down
15 changes: 10 additions & 5 deletions packages/mui-base/src/useList/useList.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
/**
Expand Down Expand Up @@ -248,7 +248,7 @@ interface UseListRootSlotOwnProps {
ref: React.RefCallback<Element> | null;
}

export type UseListRootSlotProps<TOther = {}> = TOther & UseListRootSlotOwnProps;
export type UseListRootSlotProps<ExternalProps = {}> = ExternalProps & UseListRootSlotOwnProps;

export interface UseListReturnValue<
ItemValue,
Expand All @@ -257,9 +257,14 @@ export interface UseListReturnValue<
> {
contextValue: ListContextValue<ItemValue>;
dispatch: (action: CustomAction | ListAction<ItemValue>) => void;
getRootProps: <TOther extends EventHandlers = {}>(
otherHandlers?: TOther,
) => UseListRootSlotProps<TOther>;
/**
* 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 extends Record<string, unknown> = {}>(
externalProps?: ExternalProps,
) => UseListRootSlotProps<ExternalProps>;
rootRef: React.RefCallback<Element> | null;
state: State;
}
34 changes: 20 additions & 14 deletions packages/mui-base/src/useList/useListItem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,8 +66,8 @@ export function useListItem<ItemValue>(
}, [registerSelectionChangeHandler, rerender, selected, item]);

const createHandleClick = React.useCallback(
(other: Record<string, React.EventHandler<any>>) => (event: React.MouseEvent) => {
other.onClick?.(event);
(externalHandlers: EventHandlers) => (event: React.MouseEvent) => {
externalHandlers.onClick?.(event);
if (event.defaultPrevented) {
return;
}
Expand All @@ -81,8 +82,8 @@ export function useListItem<ItemValue>(
);

const createHandlePointerOver = React.useCallback(
(other: Record<string, React.EventHandler<any>>) => (event: React.PointerEvent) => {
other.onMouseOver?.(event);
(externalHandlers: EventHandlers) => (event: React.PointerEvent) => {
externalHandlers.onMouseOver?.(event);
if (event.defaultPrevented) {
return;
}
Expand All @@ -101,15 +102,20 @@ export function useListItem<ItemValue>(
tabIndex = highlighted ? 0 : -1;
}

const getRootProps = <TOther extends EventHandlers = {}>(
otherHandlers: TOther = {} as TOther,
) => ({
...otherHandlers,
onClick: createHandleClick(otherHandlers),
onPointerOver: handlePointerOverEvents ? createHandlePointerOver(otherHandlers) : undefined,
ref: handleRef,
tabIndex,
});
const getRootProps = <ExternalProps extends Record<string, any>>(
externalProps: ExternalProps = {} as ExternalProps,
) => {
const externalEventHandlers = extractEventHandlers(externalProps);
return {
...externalProps,
onClick: createHandleClick(externalEventHandlers),
onPointerOver: handlePointerOverEvents
? createHandlePointerOver(externalEventHandlers)
: undefined,
ref: handleRef,
tabIndex,
};
};

return {
getRootProps,
Expand Down
13 changes: 6 additions & 7 deletions packages/mui-base/src/useList/useListItem.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { EventHandlers } from '../utils';

export interface UseListItemParameters<ItemValue> {
/**
* If `true`, the list item will dispatch the `itemHover` action on pointer over.
Expand Down Expand Up @@ -27,17 +25,18 @@ interface UseListItemRootSlotOwnProps {
tabIndex?: number;
}

export type UseListItemRootSlotProps<TOther = {}> = TOther & UseListItemRootSlotOwnProps;
export type UseListItemRootSlotProps<ExternalProps = {}> = 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: <TOther extends EventHandlers = {}>(
otherHandlers?: TOther,
) => UseListItemRootSlotProps<TOther>;
getRootProps: <ExternalProps extends Record<string, unknown> = {}>(
externalProps?: ExternalProps,
) => UseListItemRootSlotProps<ExternalProps>;
/**
* If `true`, the current item is highlighted.
*/
Expand Down

0 comments on commit 40aaf7b

Please sign in to comment.