diff --git a/web/components/menu.react.js b/web/components/menu.react.js --- a/web/components/menu.react.js +++ b/web/components/menu.react.js @@ -3,6 +3,7 @@ import classnames from 'classnames'; import * as React from 'react'; +import { useRenderMenu } from '../menu-provider.react'; import css from './menu.css'; type MenuVariant = 'thread-actions' | 'member-actions'; @@ -11,53 +12,113 @@ +icon: React.Node, +children?: React.Node, +variant?: MenuVariant, + +onChange?: boolean => void, }; function Menu(props: MenuProps): React.Node { - const [isOpen, setIsOpen] = React.useState(false); + const buttonRef = React.useRef(); + const { + renderMenu, + setMenuPosition, + closeMenu, + setCurrentOpenMenu, + currentOpenMenu, + } = useRenderMenu(); + const { icon, children, variant = 'thread-actions', onChange } = props; + const ourSymbol = React.useRef(Symbol()); + const menuActionListClasses = React.useMemo( + () => + classnames(css.menuActionList, { + [css.menuActionListThreadActions]: variant === 'thread-actions', + [css.menuActionListMemberActions]: variant === 'member-actions', + }), + [variant], + ); - const { icon, children, variant = 'thread-actions' } = props; + const menuActionList = React.useMemo( + () =>
{children}
, + [children, menuActionListClasses], + ); - const closeMenuCallback = React.useCallback(() => { - document.removeEventListener('click', closeMenuCallback); - if (isOpen) { - setIsOpen(false); + const updatePosition = React.useCallback(() => { + if (buttonRef.current && currentOpenMenu === ourSymbol.current) { + const { top, left } = buttonRef.current.getBoundingClientRect(); + setMenuPosition({ top, left }); } - }, [isOpen]); + }, [currentOpenMenu, setMenuPosition]); React.useEffect(() => { - if (!document || !isOpen) { + if (!window) { + return undefined; + } + + window.addEventListener('resize', updatePosition); + return () => window.removeEventListener('resize', updatePosition); + }, [updatePosition]); + + // useLayoutEffect is necessary so that the menu position is immediately + // updated in the first render of component + React.useLayoutEffect(() => { + updatePosition(); + }, [updatePosition]); + + const closeMenuCallback = React.useCallback(() => { + closeMenu(menuActionList); + }, [closeMenu, menuActionList]); + + const isOurMenuOpen = React.useMemo( + () => currentOpenMenu === ourSymbol.current, + [currentOpenMenu], + ); + + React.useEffect(() => { + onChange?.(isOurMenuOpen); + }, [isOurMenuOpen, onChange]); + + React.useEffect(() => { + if (ourSymbol.current !== currentOpenMenu) { return undefined; } document.addEventListener('click', closeMenuCallback); - return () => document.removeEventListener('click', closeMenuCallback); - }, [closeMenuCallback, isOpen]); + return () => { + document.removeEventListener('click', closeMenuCallback); + }; + }, [closeMenuCallback, currentOpenMenu, menuActionList]); - const switchMenuCallback = React.useCallback(() => { - setIsOpen(isMenuOpen => !isMenuOpen); - }, []); + const prevActionListRef = React.useRef(null); + React.useEffect(() => { + if (currentOpenMenu !== ourSymbol.current) { + prevActionListRef.current = null; + return; + } + if (prevActionListRef.current === menuActionList) { + return; + } + renderMenu(menuActionList); - if (React.Children.count(children) === 0) { - return null; - } + prevActionListRef.current = menuActionList; + }, [currentOpenMenu, menuActionList, renderMenu]); + + React.useEffect(() => { + return () => closeMenu(prevActionListRef.current); + }, [closeMenu]); - let menuActionList = null; - if (isOpen) { - const menuActionListClasses = classnames(css.menuActionList, { - [css.menuActionListThreadActions]: variant === 'thread-actions', - [css.menuActionListMemberActions]: variant === 'member-actions', - }); + const onClickMenuCallback = React.useCallback(() => { + setCurrentOpenMenu(ourSymbol.current); + }, [setCurrentOpenMenu]); - menuActionList =
{children}
; + if (React.Children.count(children) === 0) { + return null; } return ( -
- - {menuActionList} -
+ ); }