diff --git a/web/components/menu.react.js b/web/components/menu.react.js index fedd647c7..3d968ba44 100644 --- a/web/components/menu.react.js +++ b/web/components/menu.react.js @@ -1,118 +1,119 @@ // @flow 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'; type MenuProps = { +icon: React.Node, +children?: React.Node, +variant?: MenuVariant, +onChange?: boolean => void, }; function Menu(props: MenuProps): React.Node { 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 menuActionList = React.useMemo( () =>
{children}
, [children, menuActionListClasses], ); const isOurMenuOpen = currentOpenMenu === ourSymbol.current; const updatePosition = React.useCallback(() => { if (buttonRef.current && isOurMenuOpen) { const { top, left } = buttonRef.current.getBoundingClientRect(); setMenuPosition({ top, left }); } }, [isOurMenuOpen, setMenuPosition]); React.useEffect(() => { if (!window) { return undefined; } window.addEventListener('resize', updatePosition); return () => window.removeEventListener('resize', updatePosition); }, [updatePosition]); React.useEffect(updatePosition, [updatePosition]); const closeMenuCallback = React.useCallback(() => { - closeMenu(menuActionList); - }, [closeMenu, menuActionList]); + closeMenu(ourSymbol.current); + }, [closeMenu]); React.useEffect(() => { onChange?.(isOurMenuOpen); }, [isOurMenuOpen, onChange]); React.useEffect(() => { if (!isOurMenuOpen) { return undefined; } document.addEventListener('click', closeMenuCallback); return () => { document.removeEventListener('click', closeMenuCallback); }; }, [closeMenuCallback, isOurMenuOpen]); const prevActionListRef = React.useRef(null); React.useEffect(() => { if (!isOurMenuOpen) { prevActionListRef.current = null; return; } if (prevActionListRef.current === menuActionList) { return; } renderMenu(menuActionList); prevActionListRef.current = menuActionList; }, [isOurMenuOpen, menuActionList, renderMenu]); React.useEffect(() => { - return () => closeMenu(prevActionListRef.current); + const ourSymbolValue = ourSymbol.current; + return () => closeMenu(ourSymbolValue); }, [closeMenu]); const onClickMenuCallback = React.useCallback(() => { setCurrentOpenMenu(ourSymbol.current); }, [setCurrentOpenMenu]); if (React.Children.count(children) === 0) { return null; } return ( ); } export default Menu; diff --git a/web/menu-provider.react.js b/web/menu-provider.react.js index 55265467f..265223229 100644 --- a/web/menu-provider.react.js +++ b/web/menu-provider.react.js @@ -1,84 +1,106 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import type { SetState } from 'lib/types/hook-types'; import css from './menu.css'; type MenuPosition = { +top: number, +left: number, }; type Props = { +children: React.Node, }; type MenuContextType = { - +renderMenu: SetState, + +renderMenu: React.Node => void, +setMenuPosition: SetState, - +closeMenu: React.Node => void, - +currentOpenMenu: symbol, - +setCurrentOpenMenu: SetState, + +closeMenu: (symbol) => void, + +currentOpenMenu: ?symbol, + +setCurrentOpenMenu: (symbol) => void, }; const MenuContext: React.Context = React.createContext( { renderMenu: () => {}, setMenuPosition: () => {}, closeMenu: () => {}, - currentOpenMenu: Symbol(), + currentOpenMenu: null, setCurrentOpenMenu: () => {}, }, ); +type Menu = { + +node: ?React.Node, + +symbol: ?symbol, +}; + function MenuProvider(props: Props): React.Node { const { children } = props; - const [menu, setMenu] = React.useState(null); - const [currentOpenMenu, setCurrentOpenMenu] = React.useState( - Symbol(), - ); + const [menu, setMenu] = React.useState({ node: null, symbol: null }); const [position, setPosition] = React.useState({ top: 0, left: 0, }); - const closeMenu = React.useCallback((menuToClose: React.Node) => { - setCurrentOpenMenu(Symbol()); - setMenu(oldMenu => { - if (oldMenu === menuToClose) { - return null; - } else { - return oldMenu; + const setMenuSymbol = React.useCallback( + (newSymbol: symbol) => + setMenu(prevMenu => { + if (prevMenu.symbol === newSymbol) { + return prevMenu; + } + return { node: null, symbol: newSymbol }; + }), + [], + ); + + const setMenuNode = React.useCallback( + (newMenuNode: React.Node) => + setMenu(prevMenu => { + if (prevMenu.node === newMenuNode) { + return prevMenu; + } + return { ...prevMenu, node: newMenuNode }; + }), + [], + ); + + const closeMenu = React.useCallback((menuToCloseSymbol: symbol) => { + setMenu(currentMenu => { + if (currentMenu.symbol === menuToCloseSymbol) { + return { node: null, symbol: null }; } + return currentMenu; }); }, []); const value = React.useMemo( () => ({ - renderMenu: setMenu, + renderMenu: setMenuNode, setMenuPosition: setPosition, closeMenu, - setCurrentOpenMenu, - currentOpenMenu, + setCurrentOpenMenu: setMenuSymbol, + currentOpenMenu: menu.symbol, }), - [closeMenu, currentOpenMenu], + [closeMenu, menu.symbol, setMenuNode, setMenuSymbol], ); return ( <> {children}
- {menu} + {menu.node}
); } function useRenderMenu(): MenuContextType { const context = React.useContext(MenuContext); invariant(context, 'MenuContext not found'); return context; } export { MenuProvider, useRenderMenu };