-
Notifications
You must be signed in to change notification settings - Fork 39
Open
Description
Add useSlots hook to support v3 updates.
Example:
const [IconSlot, iconSlotProps] = useSlot('icon', {
elementType: RatingIcon,
className: clsx(classes.icon, {
[classes.iconEmpty]: !isFilled,
[classes.iconFilled]: isFilled,
[classes.iconHover]: isHovered,
[classes.iconFocus]: isFocused,
[classes.iconActive]: isActive,
}),
externalForwardedProps,
ownerState: {
...ownerState,
iconEmpty: !isFilled,
iconFilled: isFilled,
iconHover: isHovered,
iconFocus: isFocused,
iconActive: isActive,
},
additionalProps: {
value: itemValue,
},
internalForwardedProps: {
// TODO: remove this in v7 because `IconContainerComponent` is deprecated
// only forward if `slots.icon` is NOT provided
as: IconContainerComponent,
},
});MUI for reference:
https://github.com/mui/material-ui/blob/v6.x/packages/mui-material/src/utils/useSlot.ts
'use client';
import * as React from 'react';
import { ClassValue } from 'clsx';
import useForkRef from '@mui/utils/useForkRef';
import appendOwnerState from '@mui/utils/appendOwnerState';
import resolveComponentProps from '@mui/utils/resolveComponentProps';
import mergeSlotProps from '@mui/utils/mergeSlotProps';
export type WithCommonProps<T> = T & {
className?: string;
style?: React.CSSProperties;
ref?: React.Ref<any>;
};
type EventHandlers = Record<string, React.EventHandler<any>>;
type ExtractComponentProps<P> = P extends infer T | ((ownerState: any) => infer T) ? T : never;
/**
* An internal function to create a Material UI slot.
*
* This is an advanced version of Base UI `useSlotProps` because Material UI allows leaf component to be customized via `component` prop
* while Base UI does not need to support leaf component customization.
*
* @param {string} name: name of the slot
* @param {object} parameters
* @returns {[Slot, slotProps]} The slot's React component and the slot's props
*
* Note: the returned slot's props
* - will never contain `component` prop.
* - might contain `as` prop.
*/
export default function useSlot<
T extends string,
ElementType extends React.ElementType,
SlotProps,
OwnerState extends {},
ExternalSlotProps extends { component?: React.ElementType; ref?: React.Ref<any> },
ExternalForwardedProps extends {
component?: React.ElementType;
slots?: { [k in T]?: React.ElementType };
slotProps?: {
[k in T]?: ExternalSlotProps | ((ownerState: OwnerState) => ExternalSlotProps);
};
},
AdditionalProps,
SlotOwnerState extends {},
>(
/**
* The slot's name. All Material UI components should have `root` slot.
*
* If the name is `root`, the logic behaves differently from other slots,
* e.g. the `externalForwardedProps` are spread to `root` slot but not other slots.
*/
name: T,
parameters: (T extends 'root' // root slot must pass a `ref` as a parameter
? { ref: React.ForwardedRef<any> }
: { ref?: React.ForwardedRef<any> }) & {
/**
* The slot's className
*/
className: ClassValue | ClassValue[];
/**
* The slot's default styled-component
*/
elementType: ElementType;
/**
* The component's ownerState
*/
ownerState: OwnerState;
/**
* The `other` props from the consumer. It has to contain `component`, `slots`, and `slotProps`.
* The function will use those props to calculate the final rendered element and the returned props.
*
* If the slot is not `root`, the rest of the `externalForwardedProps` are neglected.
*/
externalForwardedProps: ExternalForwardedProps;
getSlotProps?: (other: EventHandlers) => WithCommonProps<SlotProps>;
additionalProps?: WithCommonProps<AdditionalProps>;
/**
* props forward to `T` only if the `slotProps.*.component` is not provided.
* e.g. Autocomplete's listbox uses Popper + StyledComponent
*/
internalForwardedProps?: any;
/**
* Set to true if the `elementType` is a styled component of another Material UI component.
*
* For example, the AlertRoot is a styled component of the Paper component.
* This flag is used to forward the `component` and `slotProps.root.component` to the Paper component.
* Otherwise, the `component` prop will be converted to `as` prop which replaces the Paper component (the paper styles are gone).
*/
shouldForwardComponentProp?: boolean;
},
) {
const {
className,
elementType: initialElementType,
ownerState,
externalForwardedProps,
internalForwardedProps,
shouldForwardComponentProp = false,
...useSlotPropsParams
} = parameters;
const {
component: rootComponent,
slots = { [name]: undefined },
slotProps = { [name]: undefined },
...other
} = externalForwardedProps;
const elementType = slots[name] || initialElementType;
// `slotProps[name]` can be a callback that receives the component's ownerState.
// `resolvedComponentsProps` is always a plain object.
const resolvedComponentsProps = resolveComponentProps(slotProps[name], ownerState);
const {
props: { component: slotComponent, ...mergedProps },
internalRef,
} = mergeSlotProps({
className,
...useSlotPropsParams,
externalForwardedProps: name === 'root' ? other : undefined,
externalSlotProps: resolvedComponentsProps,
});
const ref = useForkRef(internalRef, resolvedComponentsProps?.ref, parameters.ref);
const LeafComponent = (name === 'root' ? slotComponent || rootComponent : slotComponent) as
| React.ElementType
| undefined;
const props = appendOwnerState(
elementType,
{
...(name === 'root' && !rootComponent && !slots[name] && internalForwardedProps),
...(name !== 'root' && !slots[name] && internalForwardedProps),
...mergedProps,
...(LeafComponent &&
!shouldForwardComponentProp && {
as: LeafComponent,
}),
...(LeafComponent &&
shouldForwardComponentProp && {
component: LeafComponent,
}),
ref,
},
ownerState,
);
return [elementType, props] as [
ElementType,
{
className: string;
ownerState: OwnerState & SlotOwnerState;
} & AdditionalProps &
SlotProps &
ExternalSlotProps &
ExtractComponentProps<
Exclude<Exclude<ExternalForwardedProps['slotProps'], undefined>[T], undefined>
>,
];
}Menu.js
'use client';
import * as React from 'react';
import { isFragment } from 'react-is';
import PropTypes from 'prop-types';
import composeClasses from '@mui/utils/composeClasses';
import HTMLElementType from '@mui/utils/HTMLElementType';
import { useRtl } from '@mui/system/RtlProvider';
import useSlotProps from '@mui/utils/useSlotProps';
import MenuList from '../MenuList';
import Popover, { PopoverPaper } from '../Popover';
import rootShouldForwardProp from '../styles/rootShouldForwardProp';
import { styled } from '../zero-styled';
import { useDefaultProps } from '../DefaultPropsProvider';
import { getMenuUtilityClass } from './menuClasses';
import useSlot from '../utils/useSlot';
const RTL_ORIGIN = {
vertical: 'top',
horizontal: 'right',
};
const LTR_ORIGIN = {
vertical: 'top',
horizontal: 'left',
};
const useUtilityClasses = (ownerState) => {
const { classes } = ownerState;
const slots = {
root: ['root'],
paper: ['paper'],
list: ['list'],
};
return composeClasses(slots, getMenuUtilityClass, classes);
};
const MenuRoot = styled(Popover, {
shouldForwardProp: (prop) => rootShouldForwardProp(prop) || prop === 'classes',
name: 'MuiMenu',
slot: 'Root',
})({});
export const MenuPaper = styled(PopoverPaper, {
name: 'MuiMenu',
slot: 'Paper',
})({
// specZ: The maximum height of a simple menu should be one or more rows less than the view
// height. This ensures a tappable area outside of the simple menu with which to dismiss
// the menu.
maxHeight: 'calc(100% - 96px)',
// Add iOS momentum scrolling for iOS < 13.0
WebkitOverflowScrolling: 'touch',
});
const MenuMenuList = styled(MenuList, {
name: 'MuiMenu',
slot: 'List',
})({
// We disable the focus ring for mouse, touch and keyboard users.
outline: 0,
});
const Menu = React.forwardRef(function Menu(inProps, ref) {
const props = useDefaultProps({ props: inProps, name: 'MuiMenu' });
const {
autoFocus = true,
children,
className,
disableAutoFocusItem = false,
onClose,
open,
PopoverClasses,
transitionDuration = 'auto',
variant = 'selectedMenu',
slots = {},
slotProps = {},
...other
} = props;
const isRtl = useRtl();
const ownerState = {
...props,
autoFocus,
disableAutoFocusItem,
transitionDuration,
variant,
};
const classes = useUtilityClasses(ownerState);
const autoFocusItem = autoFocus && !disableAutoFocusItem && open;
const menuListActionsRef = React.useRef(null);
const handleEntering = (element, _isAppearing) => {
if (menuListActionsRef.current) {
menuListActionsRef.current.adjustStyleForScrollbar(element, {
direction: isRtl ? 'rtl' : 'ltr',
});
}
};
const handleListKeyDown = (event) => {
if (event.key === 'Tab') {
event.preventDefault();
if (onClose) {
onClose(event, 'tabKeyDown');
}
}
};
/**
* the index of the item should receive focus
* in a `variant="selectedMenu"` it's the first `selected` item
* otherwise it's the very first item.
*/
let activeItemIndex = -1;
// since we inject focus related props into children we have to do a lookahead
// to check if there is a `selected` item. We're looking for the last `selected`
// item and use the first valid item as a fallback
React.Children.map(children, (child, index) => {
if (!React.isValidElement(child)) {
return;
}
if (process.env.NODE_ENV !== 'production') {
if (isFragment(child)) {
console.error(
[
"MUI: The Menu component doesn't accept a Fragment as a child.",
'Consider providing an array instead.',
].join('\n'),
);
}
}
if (!child.props.disabled) {
if (variant === 'selectedMenu' && child.props.selected) {
activeItemIndex = index;
} else if (activeItemIndex === -1) {
activeItemIndex = index;
}
}
});
const externalForwardedProps = {
slots,
slotProps,
};
const rootSlotProps = useSlotProps({
elementType: slots.root,
externalSlotProps: slotProps.root,
ownerState,
className: [classes.root, className],
});
const [PaperSlot, paperSlotProps] = useSlot('paper', {
className: classes.paper,
elementType: MenuPaper,
externalForwardedProps,
shouldForwardComponentProp: true,
ownerState,
});
const [ListSlot, listSlotProps] = useSlot('list', {
className: classes.list,
elementType: MenuMenuList,
shouldForwardComponentProp: true,
externalForwardedProps,
getSlotProps: (handlers) => ({
...handlers,
onKeyDown: (event) => {
handleListKeyDown(event);
handlers.onKeyDown?.(event);
},
}),
ownerState,
});
const resolvedTransitionProps =
typeof slotProps.transition === 'function'
? slotProps.transition(ownerState)
: slotProps.transition;
return (
<MenuRoot
onClose={onClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: isRtl ? 'right' : 'left',
}}
transformOrigin={isRtl ? RTL_ORIGIN : LTR_ORIGIN}
slots={{
root: slots.root,
paper: PaperSlot,
backdrop: slots.backdrop,
transition: slots.transition,
}}
slotProps={{
root: rootSlotProps,
paper: paperSlotProps,
backdrop:
typeof slotProps.backdrop === 'function'
? slotProps.backdrop(ownerState)
: slotProps.backdrop,
transition: {
...resolvedTransitionProps,
onEntering: (...args) => {
handleEntering(...args);
resolvedTransitionProps?.onEntering?.(...args);
},
},
}}
open={open}
ref={ref}
transitionDuration={transitionDuration}
ownerState={ownerState}
{...other}
classes={PopoverClasses}
>
<ListSlot
actions={menuListActionsRef}
autoFocus={autoFocus && (activeItemIndex === -1 || disableAutoFocusItem)}
autoFocusItem={autoFocusItem}
variant={variant}
{...listSlotProps}
>
{children}
</ListSlot>
</MenuRoot>
);
});
Menu.propTypes /* remove-proptypes */ = {
// ┌────────────────────────────── Warning ──────────────────────────────┐
// │ These PropTypes are generated from the TypeScript type definitions. │
// │ To update them, edit the d.ts file and run `pnpm proptypes`. │
// └─────────────────────────────────────────────────────────────────────┘
/**
* An HTML element, or a function that returns one.
* It's used to set the position of the menu.
*/
anchorEl: PropTypes /* @typescript-to-proptypes-ignore */.oneOfType([
HTMLElementType,
PropTypes.func,
]),
/**
* If `true` (Default) will focus the `[role="menu"]` if no focusable child is found. Disabled
* children are not focusable. If you set this prop to `false` focus will be placed
* on the parent modal container. This has severe accessibility implications
* and should only be considered if you manage focus otherwise.
* @default true
*/
autoFocus: PropTypes.bool,
/**
* Menu contents, normally `MenuItem`s.
*/
children: PropTypes.node,
/**
* Override or extend the styles applied to the component.
*/
classes: PropTypes.object,
/**
* @ignore
*/
className: PropTypes.string,
/**
* When opening the menu will not focus the active item but the `[role="menu"]`
* unless `autoFocus` is also set to `false`. Not using the default means not
* following WAI-ARIA authoring practices. Please be considerate about possible
* accessibility implications.
* @default false
*/
disableAutoFocusItem: PropTypes.bool,
/**
* Callback fired when the component requests to be closed.
*
* @param {object} event The event source of the callback.
* @param {string} reason Can be: `"escapeKeyDown"`, `"backdropClick"`, `"tabKeyDown"`.
*/
onClose: PropTypes.func,
/**
* If `true`, the component is shown.
*/
open: PropTypes.bool.isRequired,
/**
* `classes` prop applied to the [`Popover`](https://mui.com/material-ui/api/popover/) element.
*/
PopoverClasses: PropTypes.object,
/**
* The props used for each slot inside.
* @default {}
*/
slotProps: PropTypes.shape({
backdrop: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
list: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
paper: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
root: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
transition: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
}),
/**
* The components used for each slot inside.
* @default {}
*/
slots: PropTypes.shape({
backdrop: PropTypes.elementType,
list: PropTypes.elementType,
paper: PropTypes.elementType,
root: PropTypes.elementType,
transition: PropTypes.elementType,
}),
/**
* The system prop that allows defining system overrides as well as additional CSS styles.
*/
sx: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])),
PropTypes.func,
PropTypes.object,
]),
/**
* The length of the transition in `ms`, or 'auto'
* @default 'auto'
*/
transitionDuration: PropTypes.oneOfType([
PropTypes.oneOf(['auto']),
PropTypes.number,
PropTypes.shape({
appear: PropTypes.number,
enter: PropTypes.number,
exit: PropTypes.number,
}),
]),
/**
* The variant to use. Use `menu` to prevent selected items from impacting the initial focus.
* @default 'selectedMenu'
*/
variant: PropTypes.oneOf(['menu', 'selectedMenu']),
};
export default Menu;Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels