Instantly share code, notes, and snippets.
ogizanagi/DropMenu.tsx Secret
Created
April 20, 2023 14:33
-
Star
(0)
0
You must be signed in to star a gist -
Fork
(0)
0
You must be signed in to fork a gist
-
Save ogizanagi/db03004d72b680ab03998308f3c18823 to your computer and use it in GitHub Desktop.
React slots : DropMenu example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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