Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MenuUnstyled] Accept callbacks in componentsProps #32997

Merged
merged 7 commits into from
Jun 10, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ describe('MenuItemUnstyled', () => {
},
skip: [
'reactTestRenderer', // Need to be wrapped in MenuUnstyledContext
'componentsPropsCallbacks', // not implemented yet
],
}));
});
30 changes: 13 additions & 17 deletions packages/mui-base/src/MenuItemUnstyled/MenuItemUnstyled.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { MenuItemOwnerState, MenuItemUnstyledProps } from './MenuItemUnstyled.types';
import { appendOwnerState } from '../utils';
import { MenuItemUnstyledOwnerState, MenuItemUnstyledProps } from './MenuItemUnstyled.types';
import { getMenuItemUnstyledUtilityClass } from './menuItemUnstyledClasses';
import useMenuItem from './useMenuItem';
import composeClasses from '../composeClasses';
import useSlotProps from '../utils/useSlotProps';

function getUtilityClasses(ownerState: MenuItemOwnerState) {
function getUtilityClasses(ownerState: MenuItemUnstyledOwnerState) {
const { disabled, focusVisible } = ownerState;

const slots = {
Expand Down Expand Up @@ -42,28 +41,25 @@ const MenuItemUnstyled = React.forwardRef(function MenuItemUnstyled(
...other
} = props;

const Root = component ?? components.Root ?? 'li';

const { getRootProps, disabled, focusVisible } = useMenuItem({
disabled: disabledProp,
ref,
label,
});

const ownerState: MenuItemOwnerState = { ...props, disabled, focusVisible };
const ownerState: MenuItemUnstyledOwnerState = { ...props, disabled, focusVisible };

const classes = getUtilityClasses(ownerState);

const rootProps = appendOwnerState(
Root,
{
...other,
...componentsProps.root,
...getRootProps(other),
className: clsx(classes.root, className, componentsProps.root?.className),
},
const Root = component ?? components.Root ?? 'li';
const rootProps = useSlotProps({
elementType: Root,
getSlotProps: getRootProps,
externalSlotProps: componentsProps.root,
externalForwardedProps: other,
className: [classes.root, className],
ownerState,
);
});

return <Root {...rootProps}>{children}</Root>;
});
Expand Down Expand Up @@ -95,7 +91,7 @@ MenuItemUnstyled.propTypes /* remove-proptypes */ = {
* @ignore
*/
componentsProps: PropTypes.shape({
root: PropTypes.object,
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* If `true`, the menu item will be disabled.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react';
import { SlotComponentProps } from '../utils';

export interface MenuItemUnstyledComponentsPropsOverrides {}

export interface MenuItemOwnerState extends MenuItemUnstyledProps {
export interface MenuItemUnstyledOwnerState extends MenuItemUnstyledProps {
disabled: boolean;
focusVisible: boolean;
}
Expand All @@ -21,7 +22,11 @@ export interface MenuItemUnstyledProps {
Root?: React.ElementType;
};
componentsProps?: {
root?: React.ComponentPropsWithRef<'li'> & MenuItemUnstyledComponentsPropsOverrides;
root?: SlotComponentProps<
'li',
MenuItemUnstyledComponentsPropsOverrides,
MenuItemUnstyledOwnerState
>;
};
/**
* A text representation of the menu item's content.
Expand Down
8 changes: 1 addition & 7 deletions packages/mui-base/src/MenuUnstyled/MenuUnstyled.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,7 @@ describe('MenuUnstyled', () => {
expectedClassName: menuUnstyledClasses.listbox,
},
},
skip: [
'reactTestRenderer',
'propsSpread',
'componentProp',
'componentsProp',
'componentsPropsCallbacks', // not implemented yet
],
skip: ['reactTestRenderer', 'propsSpread', 'componentProp', 'componentsProp'],
}));

describe('keyboard navigation', () => {
Expand Down
54 changes: 28 additions & 26 deletions packages/mui-base/src/MenuUnstyled/MenuUnstyled.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import clsx from 'clsx';
import { HTMLElementType, refType } from '@mui/utils';
import appendOwnerState from '../utils/appendOwnerState';
import { HTMLElementType, refType, unstable_useForkRef as useForkRef } from '@mui/utils';
import MenuUnstyledContext, { MenuUnstyledContextType } from './MenuUnstyledContext';
import {
MenuUnstyledListboxSlotProps,
MenuUnstyledOwnerState,
MenuUnstyledProps,
MenuUnstyledRootSlotProps,
Expand All @@ -14,7 +12,7 @@ import { getMenuUnstyledUtilityClass } from './menuUnstyledClasses';
import useMenu from './useMenu';
import composeClasses from '../composeClasses';
import PopperUnstyled from '../PopperUnstyled';
import { WithOptionalOwnerState } from '../utils';
import useSlotProps from '../utils/useSlotProps';

function getUtilityClasses(ownerState: MenuUnstyledOwnerState) {
const { open } = ownerState;
Expand Down Expand Up @@ -48,6 +46,7 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled(
components = {},
componentsProps = {},
keepMounted = false,
listboxId,
onClose,
open = false,
...other
Expand All @@ -64,8 +63,7 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled(
} = useMenu({
open,
onClose,
listboxRef: componentsProps.listbox?.ref,
listboxId: componentsProps.listbox?.id,
listboxId,
});

React.useImperativeHandle(
Expand All @@ -84,31 +82,31 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled(

const classes = getUtilityClasses(ownerState);

const Popper = component ?? components.Root ?? PopperUnstyled;
const popperProps: MenuUnstyledRootSlotProps = appendOwnerState(
Popper,
{
...other,
const Root = component ?? components.Root ?? PopperUnstyled;
const rootProps: MenuUnstyledRootSlotProps = useSlotProps({
elementType: Root,
externalForwardedProps: other,
externalSlotProps: componentsProps.root,
additionalProps: {
anchorEl,
open,
keepMounted,
role: undefined,
...componentsProps.root,
className: clsx(classes.root, className, componentsProps.root?.className),
},
className: clsx(classes.root, className),
ownerState,
) as MenuUnstyledRootSlotProps;
}) as MenuUnstyledRootSlotProps;

rootProps.ref = useForkRef(rootProps.ref, forwardedRef);

const Listbox = components.Listbox ?? 'ul';
const listboxProps: WithOptionalOwnerState<MenuUnstyledListboxSlotProps> = appendOwnerState(
Listbox,
{
...componentsProps.listbox,
...getListboxProps(),
className: clsx(classes.listbox, componentsProps.listbox?.className),
},
const listboxProps = useSlotProps({
elementType: Listbox,
getSlotProps: getListboxProps,
externalSlotProps: componentsProps.listbox,
ownerState,
);
className: classes.listbox,
});

const contextValue: MenuUnstyledContextType = {
registerItem,
Expand All @@ -119,11 +117,11 @@ const MenuUnstyled = React.forwardRef(function MenuUnstyled(
};

return (
<Popper {...popperProps} ref={forwardedRef}>
<Root {...rootProps}>
<Listbox {...listboxProps}>
<MenuUnstyledContext.Provider value={contextValue}>{children}</MenuUnstyledContext.Provider>
</Listbox>
</Popper>
</Root>
);
});

Expand Down Expand Up @@ -170,8 +168,8 @@ MenuUnstyled.propTypes /* remove-proptypes */ = {
* @ignore
*/
componentsProps: PropTypes.shape({
listbox: PropTypes.object,
root: PropTypes.object,
listbox: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* Always keep the menu in the DOM.
Expand All @@ -180,6 +178,10 @@ MenuUnstyled.propTypes /* remove-proptypes */ = {
* @default false
*/
keepMounted: PropTypes.bool,
/**
* @ignore
*/
listboxId: PropTypes.string,
/**
* Triggered when focus leaves the menu and the menu should close.
*/
Expand Down
18 changes: 14 additions & 4 deletions packages/mui-base/src/MenuUnstyled/MenuUnstyled.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import PopperUnstyled, { PopperUnstyledProps } from '../PopperUnstyled';
import { SlotComponentProps } from '../utils';
import { UseMenuListboxSlotProps } from './useMenu.types';

export interface MenuUnstyledComponentsPropsOverrides {}
Expand Down Expand Up @@ -29,9 +30,16 @@ export interface MenuUnstyledProps {
Listbox?: React.ElementType;
};
componentsProps?: {
root?: Partial<React.ComponentPropsWithRef<typeof PopperUnstyled>> &
MenuUnstyledComponentsPropsOverrides;
listbox?: React.ComponentPropsWithRef<'ul'> & MenuUnstyledComponentsPropsOverrides;
root?: SlotComponentProps<
typeof PopperUnstyled,
MenuUnstyledComponentsPropsOverrides,
MenuUnstyledOwnerState
>;
listbox?: SlotComponentProps<
'ul',
MenuUnstyledComponentsPropsOverrides,
MenuUnstyledOwnerState
>;
};
/**
* Always keep the menu in the DOM.
Expand All @@ -40,6 +48,7 @@ export interface MenuUnstyledProps {
* @default false
*/
keepMounted?: boolean;
listboxId?: string;
/**
* Triggered when focus leaves the menu and the menu should close.
*/
Expand All @@ -58,10 +67,11 @@ export interface MenuUnstyledOwnerState extends MenuUnstyledProps {
export type MenuUnstyledRootSlotProps = {
anchorEl: PopperUnstyledProps['anchorEl'];
children?: React.ReactNode;
className: string | undefined;
className?: string;
keepMounted: PopperUnstyledProps['keepMounted'];
open: boolean;
ownerState: MenuUnstyledOwnerState;
ref: React.Ref<any>;
};

export type MenuUnstyledListboxSlotProps = UseMenuListboxSlotProps & {
Expand Down
34 changes: 22 additions & 12 deletions packages/mui-base/src/MenuUnstyled/useMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import {
useListbox,
ActionTypes,
} from '../ListboxUnstyled';
import { MenuItemMetadata, MenuItemState, UseMenuParameters } from './useMenu.types';
import {
MenuItemMetadata,
MenuItemState,
UseMenuListboxSlotProps,
UseMenuParameters,
} from './useMenu.types';
import { EventHandlers } from '../utils';

function stateReducer(
Expand Down Expand Up @@ -38,7 +43,7 @@ function stateReducer(
return newState;
}

export default function useMenu(parameters: UseMenuParameters) {
export default function useMenu(parameters: UseMenuParameters = {}) {
const { listboxRef: listboxRefProp, open = false, onClose, listboxId } = parameters;

const [menuItems, setMenuItems] = React.useState<Record<string, MenuItemMetadata>>({});
Expand Down Expand Up @@ -97,8 +102,8 @@ export default function useMenu(parameters: UseMenuParameters) {
}
}, [open, highlightFirstItem]);

const createHandleKeyDown = (otherHandlers?: EventHandlers) => (e: React.KeyboardEvent) => {
otherHandlers?.onKeyDown?.(e);
const createHandleKeyDown = (otherHandlers: EventHandlers) => (e: React.KeyboardEvent) => {
otherHandlers.onKeyDown?.(e);
if (e.defaultPrevented) {
return;
}
Expand All @@ -108,8 +113,8 @@ export default function useMenu(parameters: UseMenuParameters) {
}
};

const createHandleBlur = (otherHandlers?: EventHandlers) => (e: React.FocusEvent) => {
otherHandlers?.onBlur(e);
const createHandleBlur = (otherHandlers: EventHandlers) => (e: React.FocusEvent) => {
otherHandlers.onBlur?.(e);

if (!listboxRef.current?.contains(e.relatedTarget)) {
onClose?.();
Expand All @@ -123,15 +128,20 @@ export default function useMenu(parameters: UseMenuParameters) {
}
}, [highlightedOption, menuItems]);

const getListboxProps = (otherHandlers?: EventHandlers) => ({
...otherHandlers,
...getRootProps({
const getListboxProps = <TOther extends EventHandlers>(
otherHandlers: TOther = {} as TOther,
): UseMenuListboxSlotProps => {
const rootProps = getRootProps({
...otherHandlers,
onBlur: createHandleBlur(otherHandlers),
onKeyDown: createHandleKeyDown(otherHandlers),
}),
role: 'menu',
});
});
return {
...otherHandlers,
...rootProps,
role: 'menu',
};
};

const getItemState = (id: string): MenuItemState => {
const { disabled, highlighted } = getOptionState(id);
Expand Down
9 changes: 6 additions & 3 deletions packages/mui-base/src/MenuUnstyled/useMenu.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { UseListboxRootSlotProps } from '../ListboxUnstyled';
export interface MenuItemMetadata {
id: string;
disabled: boolean;
ref: React.RefObject<HTMLElement>;
label?: string;
ref: React.RefObject<HTMLElement>;
}

export interface MenuItemState {
Expand All @@ -17,7 +17,7 @@ export interface UseMenuParameters {
open?: boolean;
onClose?: () => void;
listboxId?: string;
listboxRef?: React.Ref<HTMLElement>;
listboxRef?: React.Ref<any>;
}

interface UseMenuListboxSlotEventHandlers {
Expand All @@ -27,4 +27,7 @@ interface UseMenuListboxSlotEventHandlers {

export type UseMenuListboxSlotProps<TOther = {}> = UseListboxRootSlotProps<
Omit<TOther, keyof UseMenuListboxSlotEventHandlers> & UseMenuListboxSlotEventHandlers
> & { role: React.AriaRole };
> & {
ref: React.Ref<any>;
role: React.AriaRole;
};
1 change: 1 addition & 0 deletions packages/mui-base/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ export { default as appendOwnerState } from './appendOwnerState';
export { default as areArraysEqual } from './areArraysEqual';
export { default as extractEventHandlers } from './extractEventHandlers';
export { default as isHostComponent } from './isHostComponent';
export { default as useSlotProps } from './useSlotProps';
export * from './types';