|
import type { MenuButtonProps, MenuItemProps, MenuListProps, MenuProps } from '@invoke-ai/ui-library'; |
|
import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library'; |
|
import { useDisclosure } from 'common/hooks/useBoolean'; |
|
import type { FocusEventHandler, PointerEvent, RefObject } from 'react'; |
|
import { useCallback, useEffect, useRef } from 'react'; |
|
import { PiCaretRightBold } from 'react-icons/pi'; |
|
import { useDebouncedCallback } from 'use-debounce'; |
|
|
|
const offset: [number, number] = [0, 8]; |
|
|
|
type UseSubMenuReturn = { |
|
parentMenuItemProps: Partial<MenuItemProps>; |
|
menuProps: Partial<MenuProps>; |
|
menuButtonProps: Partial<MenuButtonProps>; |
|
menuListProps: Partial<MenuListProps> & { ref: RefObject<HTMLDivElement> }; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const useSubMenu = (): UseSubMenuReturn => { |
|
const subMenu = useDisclosure(false); |
|
const menuListRef = useRef<HTMLDivElement>(null); |
|
const closeDebounced = useDebouncedCallback(subMenu.close, 300); |
|
const openAndCancelPendingClose = useCallback(() => { |
|
closeDebounced.cancel(); |
|
subMenu.open(); |
|
}, [closeDebounced, subMenu]); |
|
const toggleAndCancelPendingClose = useCallback(() => { |
|
if (subMenu.isOpen) { |
|
subMenu.close(); |
|
return; |
|
} else { |
|
closeDebounced.cancel(); |
|
subMenu.toggle(); |
|
} |
|
}, [closeDebounced, subMenu]); |
|
const onBlurMenuList = useCallback<FocusEventHandler<HTMLDivElement>>( |
|
(e) => { |
|
|
|
if (e.currentTarget.contains(e.relatedTarget)) { |
|
closeDebounced.cancel(); |
|
return; |
|
} |
|
subMenu.close(); |
|
}, |
|
[closeDebounced, subMenu] |
|
); |
|
|
|
const onParentMenuItemPointerLeave = useCallback( |
|
(e: PointerEvent<HTMLButtonElement>) => { |
|
|
|
|
|
|
|
|
|
|
|
if (e.pointerType === 'pen' || e.pointerType === 'touch') { |
|
return; |
|
} |
|
subMenu.close(); |
|
}, |
|
[subMenu] |
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => { |
|
const el = menuListRef.current; |
|
if (!el) { |
|
return; |
|
} |
|
const controller = new AbortController(); |
|
window.addEventListener( |
|
'click', |
|
(e) => { |
|
if (menuListRef.current?.contains(e.target as Node)) { |
|
return; |
|
} |
|
subMenu.close(); |
|
}, |
|
{ signal: controller.signal } |
|
); |
|
return () => { |
|
controller.abort(); |
|
}; |
|
}, [subMenu]); |
|
|
|
return { |
|
parentMenuItemProps: { |
|
onClick: toggleAndCancelPendingClose, |
|
onPointerEnter: openAndCancelPendingClose, |
|
onPointerLeave: onParentMenuItemPointerLeave, |
|
closeOnSelect: false, |
|
}, |
|
menuProps: { |
|
isOpen: subMenu.isOpen, |
|
onClose: subMenu.close, |
|
placement: 'right', |
|
offset: offset, |
|
closeOnBlur: false, |
|
}, |
|
menuButtonProps: { |
|
as: Box, |
|
width: 'full', |
|
height: 'full', |
|
}, |
|
menuListProps: { |
|
ref: menuListRef, |
|
onPointerEnter: openAndCancelPendingClose, |
|
onPointerLeave: closeDebounced, |
|
onBlur: onBlurMenuList, |
|
}, |
|
}; |
|
}; |
|
|
|
export const SubMenuButtonContent = ({ label }: { label: string }) => { |
|
return ( |
|
<Flex w="full" h="full" flexDir="row" justifyContent="space-between" alignItems="center"> |
|
<Text>{label}</Text> |
|
<Icon as={PiCaretRightBold} /> |
|
</Flex> |
|
); |
|
}; |
|
|