Skip to content

Instantly share code, notes, and snippets.

@ogizanagi
Created April 20, 2023 14:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ogizanagi/db03004d72b680ab03998308f3c18823 to your computer and use it in GitHub Desktop.
Save ogizanagi/db03004d72b680ab03998308f3c18823 to your computer and use it in GitHub Desktop.
React slots : DropMenu example
import { MenuUnstyledProps } from '@mui/base';
import React, {
Children,
cloneElement,
FunctionComponent,
HTMLAttributes,
ReactElement,
ReactHTML,
useState,
} from 'react';
import clsx from 'clsx';
import DropMenuItems, { DropMenuItem } from '@app/components/UI/DropMenu/DropMenuItems';
function ofType(slotType: FunctionComponent): (child: ReactElement<unknown, FunctionComponent>) => boolean {
return (child) => child.type === slotType;
}
function findSlotOfType(children, slotType: FunctionComponent): ReactElement|null {
return Children.toArray(children).find(ofType(slotType)) as ReactElement|null;
}
interface Props extends MenuUnstyledProps {
Component?: keyof ReactHTML
children: [
// Unfortunately, not working as expected with JSX (but let's keep for doc purposes):
// https://react-typescript-cheatsheet.netlify.app/docs/advanced/patterns_by_usecase#what-you-cannot-do
ReactElement<unknown, typeof DropMenuTrigger>,
ReactElement<unknown, typeof DropMenuItems>,
]
}
export default function DropMenu({
Component = 'div',
children,
className = '',
}: Props) {
const [anchorEl, setAnchorEl] = useState(null);
const isOpened = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const handleClose = () => setAnchorEl(null);
const DropMenuTriggerElement = findSlotOfType(children, DropMenuTrigger);
if (!DropMenuTriggerElement) {
throw new Error('You MUST provide a <DropMenu.Trigger> component as a child of <DropMenu>.');
}
const TriggerElement = cloneElement(DropMenuTriggerElement, {
onClick: (e) => {
if (DropMenuTriggerElement.props.onClick) {
DropMenuTriggerElement.props.onClick(e);
}
handleClick(e);
},
'aria-expanded': isOpened ? 'true' : 'false',
});
const DropMenuItemsElement = findSlotOfType(children, DropMenuItems);
if (!DropMenuItemsElement) {
throw new Error('You MUST provide a <DropMenu.Items> component as a child of <DropMenu>.');
}
return <Component className={clsx('drop-menu', className)}>
{TriggerElement}
<DropMenuItems.Slot {...DropMenuItemsElement.props} anchorEl={anchorEl} close={handleClose} isOpened={isOpened}>
{DropMenuItemsElement}
</DropMenuItems.Slot>
</Component>;
}
export function DropMenuTrigger({ children, ...remainingProps }: {
children: React.ReactElement
} & HTMLAttributes<unknown>) {
return cloneElement(children, { ...remainingProps });
}
// https://react-typescript-cheatsheet.netlify.app/docs/advanced/misc_concerns#namespaced-components
DropMenu.Trigger = DropMenuTrigger;
DropMenu.Items = DropMenuItems;
DropMenu.Item = DropMenuItem;
import React, { ReactElement } from 'react';
import { MenuItemUnstyled, MenuUnstyled } from '@mui/base';
import clsx from 'clsx';
export const DropMenuItem = MenuItemUnstyled;
interface Props {
children: Iterable<ReactElement<unknown, typeof DropMenuItems>>
| ReactElement<unknown, typeof DropMenuItems>
}
export default function DropMenuItems({ children }: Props) {
return <>{children}</>;
}
interface SlotProps {
children: Iterable<React.ReactElement>
isOpened: boolean
anchorEl: HTMLElement | null
close: () => void
className?: string
}
DropMenuItems.Slot = function DropMenuItemsSlot({
children,
className = '',
anchorEl,
isOpened,
close,
}: SlotProps) {
return <MenuUnstyled
className={(clsx('drop-menu__options', className))}
anchorEl={anchorEl}
open={isOpened}
onClose={close}
>
{children}
</MenuUnstyled>;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment