import React, { useMemo, useRef, useState } from 'react'; import { Menu, MenuList, MenuItem, Box, useOutsideClick, MenuButton, type MenuItemProps, type PlacementWithLogical, type AvatarProps, type BoxProps, type DividerProps } from '@chakra-ui/react'; import MyDivider from '../MyDivider'; import type { IconNameType } from '../Icon/type'; import { useSystem } from '../../../hooks/useSystem'; import Avatar from '../Avatar'; export type MenuItemType = 'primary' | 'danger' | 'gray' | 'grayBg'; export type MenuSizeType = 'sm' | 'md' | 'xs' | 'mini'; export type Props = { width?: number | string; offset?: [number, number]; Button: React.ReactNode; trigger?: 'hover' | 'click'; size?: MenuSizeType; placement?: PlacementWithLogical; menuList: { label?: string; children: { isActive?: boolean; type?: MenuItemType; icon?: IconNameType | string; label: string | React.ReactNode; description?: string; onClick?: () => any; menuItemStyles?: MenuItemProps; }[]; }[]; }; const typeMapStyle: Record = { primary: { styles: { _hover: { backgroundColor: 'primary.50', color: 'primary.600' }, _focus: { backgroundColor: 'primary.50', color: 'primary.600' }, _active: { backgroundColor: 'primary.50', color: 'primary.600' } }, iconColor: 'myGray.600' }, gray: { styles: { _hover: { backgroundColor: 'myGray.05', color: 'primary.600' }, _focus: { backgroundColor: 'myGray.05', color: 'primary.600' }, _active: { backgroundColor: 'myGray.05', color: 'primary.600' } }, iconColor: 'myGray.400' }, grayBg: { styles: { _hover: { backgroundColor: 'myGray.05', color: 'primary.600' }, _focus: { backgroundColor: 'myGray.05', color: 'primary.600' }, _active: { backgroundColor: 'myGray.05', color: 'primary.600' } }, iconColor: 'myGray.600' }, danger: { styles: { color: 'red.600', _hover: { background: 'red.1' }, _focus: { background: 'red.1' }, _active: { background: 'red.1' } }, iconColor: 'red.600' } }; const sizeMapStyle: Record< MenuSizeType, { iconStyle: AvatarProps; labelStyle: BoxProps; dividerStyle: DividerProps; menuItemStyle: MenuItemProps; } > = { mini: { iconStyle: { w: '14px' }, labelStyle: { fontSize: 'mini' }, dividerStyle: { my: 0.5 }, menuItemStyle: { py: 1.5, px: 2 } }, xs: { iconStyle: { w: '14px' }, labelStyle: { fontSize: 'sm' }, dividerStyle: { my: 0.5 }, menuItemStyle: { py: 1.5, px: 2 } }, sm: { iconStyle: { w: '1rem' }, labelStyle: { fontSize: 'sm' }, dividerStyle: { my: 1 }, menuItemStyle: { py: 2, px: 3, _notLast: { mb: 0.5 } } }, md: { iconStyle: { w: '2rem', borderRadius: '6px' }, labelStyle: { fontSize: 'sm' }, dividerStyle: { my: 1 }, menuItemStyle: { py: 2, px: 3, _notLast: { mb: 0.5 } } } }; const MyMenu = ({ width = 'auto', trigger = 'hover', size = 'sm', offset, Button, menuList, placement = 'bottom-start' }: Props) => { const { isPc } = useSystem(); const ref = useRef(null); const closeTimer = useRef(); const [isOpen, setIsOpen] = useState(false); const formatTrigger = !isPc ? 'click' : trigger; useOutsideClick({ ref: ref, handler: () => { setIsOpen(false); } }); const computeOffset = useMemo<[number, number]>(() => { if (offset) return offset; if (typeof width === 'number') return [-width / 2, 5]; return [0, 5]; }, [offset]); return ( { if (formatTrigger === 'hover') { setIsOpen(true); } clearTimeout(closeTimer.current); }} onMouseLeave={() => { if (formatTrigger === 'hover') { closeTimer.current = setTimeout(() => { setIsOpen(false); }, 100); } }} > { e.stopPropagation(); if (formatTrigger === 'click') { setIsOpen(!isOpen); } }} > {Button} {menuList.map((item, i) => { return ( {item.label && {item.label}} {i !== 0 && } {item.children.map((child, index) => ( { e.stopPropagation(); if (child.onClick) { setIsOpen(false); child.onClick(); } }} alignItems={'center'} fontSize={'sm'} color={child.isActive ? 'primary.700' : 'myGray.600'} whiteSpace={'pre-wrap'} {...typeMapStyle[child.type || 'primary'].styles} {...sizeMapStyle[size].menuItemStyle} {...child.menuItemStyles} > {!!child.icon && ( )} {child.label} {child.description && ( {child.description} )} ))} ); })} ); }; export default React.memo(MyMenu);