diff --git a/frontend/src/Components/Menu/Menu.tsx b/frontend/src/Components/Menu/Menu.tsx index ca935a0e6..2baaee3b2 100644 --- a/frontend/src/Components/Menu/Menu.tsx +++ b/frontend/src/Components/Menu/Menu.tsx @@ -1,41 +1,22 @@ +import { + autoUpdate, + flip, + FloatingPortal, + shift, + useClick, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; import React, { ReactElement, useCallback, useEffect, useId, - useRef, useState, } from 'react'; -import { Manager, Popper, PopperProps, Reference } from 'react-popper'; -import Portal from 'Components/Portal'; import styles from './Menu.css'; -const sharedPopperOptions = { - modifiers: { - preventOverflow: { - padding: 0, - }, - flip: { - padding: 0, - }, - }, -}; - -const popperOptions: { - right: Partial; - left: Partial; -} = { - right: { - ...sharedPopperOptions, - placement: 'bottom-end', - }, - - left: { - ...sharedPopperOptions, - placement: 'bottom-start', - }, -}; - interface MenuProps { className?: string; children: React.ReactNode; @@ -49,9 +30,7 @@ function Menu({ alignMenu = 'left', enforceMaxHeight = true, }: MenuProps) { - const updater = useRef<(() => void) | null>(null); const menuButtonId = useId(); - const menuContentId = useId(); const [maxHeight, setMaxHeight] = useState(0); const [isMenuOpen, setIsMenuOpen] = useState(false); @@ -70,45 +49,24 @@ function Menu({ setMaxHeight(height); }, [menuButtonId]); - const handleWindowClick = useCallback( - (event: MouseEvent) => { - const menuButton = document.getElementById(menuButtonId); + const handleMenuButtonPress = useCallback(() => { + setIsMenuOpen((isOpen) => !isOpen); + }, []); - if (!menuButton) { - return; - } + const childrenArray = React.Children.toArray(children); + const button = React.cloneElement(childrenArray[0] as ReactElement, { + onPress: handleMenuButtonPress, + }); - if (!menuButton.contains(event.target as Node)) { - setIsMenuOpen(false); - } - }, - [menuButtonId] - ); + const handleFloaterPress = useCallback((_event: MouseEvent) => { + // TODO: Menu items should handle closing when they are clicked. + // This is handled before the menu item click event is handled, so wait 100ms before closing. + setTimeout(() => { + setIsMenuOpen(false); + }, 100); - const handleTouchStart = useCallback( - (event: TouchEvent) => { - const menuButton = document.getElementById(menuButtonId); - const menuContent = document.getElementById(menuContentId); - - if (!menuButton || !menuContent) { - return; - } - - if (event.targetTouches.length !== 1) { - return; - } - - const target = event.targetTouches[0].target; - - if ( - !menuButton.contains(target as Node) && - !menuContent.contains(target as Node) - ) { - setIsMenuOpen(false); - } - }, - [menuButtonId, menuContentId] - ); + return true; + }, []); const handleWindowResize = useCallback(() => { updateMaxHeight(); @@ -120,32 +78,15 @@ function Menu({ } }, [isMenuOpen, updateMaxHeight]); - const handleMenuButtonPress = useCallback(() => { - setIsMenuOpen((isOpen) => !isOpen); - }, []); - - const childrenArray = React.Children.toArray(children); - const button = React.cloneElement(childrenArray[0] as ReactElement, { - onPress: handleMenuButtonPress, - }); - useEffect(() => { if (enforceMaxHeight) { updateMaxHeight(); } }, [enforceMaxHeight, updateMaxHeight]); - useEffect(() => { - if (updater.current && isMenuOpen) { - updater.current(); - } - }, [isMenuOpen]); - useEffect(() => { // Listen to resize events on the window and scroll events // on all elements to ensure the menu is the best size possible. - // Listen for click events on the window to support closing the - // menu on clicks outside. if (!isMenuOpen) { return; @@ -153,52 +94,65 @@ function Menu({ window.addEventListener('resize', handleWindowResize); window.addEventListener('scroll', handleWindowScroll, { capture: true }); - window.addEventListener('click', handleWindowClick); - window.addEventListener('touchstart', handleTouchStart); return () => { window.removeEventListener('resize', handleWindowResize); window.removeEventListener('scroll', handleWindowScroll, { capture: true, }); - window.removeEventListener('click', handleWindowClick); - window.removeEventListener('touchstart', handleTouchStart); }; - }, [ - isMenuOpen, - handleWindowResize, - handleWindowScroll, - handleWindowClick, - handleTouchStart, + }, [isMenuOpen, handleWindowResize, handleWindowScroll]); + + const { refs, context, floatingStyles } = useFloating({ + middleware: [ + flip({ + crossAxis: false, + mainAxis: true, + }), + // offset({ mainAxis: 10 }), + shift(), + ], + open: isMenuOpen, + placement: alignMenu === 'left' ? 'bottom-start' : 'bottom-end', + whileElementsMounted: autoUpdate, + onOpenChange: setIsMenuOpen, + }); + + const click = useClick(context); + const dismiss = useDismiss(context, { + outsidePress: handleFloaterPress, + }); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + click, + dismiss, ]); return ( - - - {({ ref }) => ( -
- {button} -
- )} -
+ <> +
+ {button} +
- - - {({ ref, style, scheduleUpdate }) => { - updater.current = scheduleUpdate; - - return React.cloneElement(childrenArray[1] as ReactElement, { - forwardedRef: ref, - style: { - ...style, - maxHeight, - }, - isOpen: isMenuOpen, - }); - }} - - -
+ {isMenuOpen ? ( + + {React.cloneElement(childrenArray[1] as ReactElement, { + forwardedRef: refs.setFloating, + style: { + maxHeight, + ...floatingStyles, + }, + isOpen: isMenuOpen, + ...getFloatingProps(), + })} + + ) : null} + ); }