diff --git a/web/chat/thread-menu.css b/web/chat/thread-menu.css --- a/web/chat/thread-menu.css +++ b/web/chat/thread-menu.css @@ -1,58 +1,6 @@ -button.topBarMenuButton { - background-color: transparent; - border: none; - cursor: pointer; - color: var(--thread-top-bar-menu-color); -} - -div.topBarMenuActionList { - position: absolute; - right: 10px; - top: 55px; - z-index: 1; - display: flex; - flex-direction: column; - background-color: var(--thread-menu-bg); - border-radius: 4px; - padding: 4px 0; -} - -button.topBarMenuAction { - z-index: 1; - background-color: transparent; - padding: 12px 16px; - color: var(--thread-menu-color); - background-color: var(--thread-menu-bg); - font-size: var(--m-font-16); - line-height: 1.5; - border: none; - cursor: pointer; - display: flex; - align-items: center; -} - -button.topBarMenuAction:hover { - color: var(--thread-menu-color-hover); -} - -div.topBarMenuActionIcon { - font-size: var(--l-font-18); - display: flex; - justify-content: center; - margin-right: 8px; - width: 20px; -} - -button.topBarMenuActionDangerous { - color: var(--thread-menu-color-dangerous); -} -button.topBarMenuActionDangerous:hover { - color: var(--thread-menu-color-dangerous-hover); -} - hr.separator { height: 1px; - background: var(--thread-menu-separator-color); + background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; diff --git a/web/chat/thread-menu.react.js b/web/chat/thread-menu.react.js --- a/web/chat/thread-menu.react.js +++ b/web/chat/thread-menu.react.js @@ -31,13 +31,14 @@ useDispatchActionPromise, } from 'lib/utils/action-utils'; +import MenuItem from '../components/menu-item.react'; +import Menu from '../components/menu.react'; import SidebarListModal from '../modals/chat/sidebar-list-modal.react'; import { useModalContext } from '../modals/modal-provider.react'; import ConfirmLeaveThreadModal from '../modals/threads/confirm-leave-thread-modal.react'; import ThreadSettingsModal from '../modals/threads/thread-settings-modal.react'; import { useSelector } from '../redux/redux-utils'; import SWMansionIcon from '../SWMansionIcon.react'; -import ThreadMenuItem from './thread-menu-item.react'; import css from './thread-menu.css'; type ThreadMenuProps = { @@ -45,8 +46,6 @@ }; function ThreadMenu(props: ThreadMenuProps): React.Node { - const [isOpen, setIsOpen] = React.useState(false); - const { setModal, clearModal } = useModalContext(); const { threadInfo } = props; @@ -58,7 +57,7 @@ const settingsItem = React.useMemo(() => { return ( - <ThreadMenuItem + <MenuItem key="settings" text="Settings" icon={faCog} @@ -71,7 +70,7 @@ if (threadInfo.type === threadTypes.PERSONAL) { return null; } - return <ThreadMenuItem key="members" text="Members" icon={faUserFriends} />; + return <MenuItem key="members" text="Members" icon={faUserFriends} />; }, [threadInfo.type]); const childThreads = useSelector( @@ -94,7 +93,7 @@ return null; } return ( - <ThreadMenuItem + <MenuItem key="sidebars" text="Sidebars" icon={faArrowRight} @@ -117,11 +116,7 @@ return null; } return ( - <ThreadMenuItem - key="subchannels" - text="Subchannels" - icon={faCommentAlt} - /> + <MenuItem key="subchannels" text="Subchannels" icon={faCommentAlt} /> ); }, [canCreateSubchannels, hasSubchannels]); @@ -130,7 +125,7 @@ return null; } return ( - <ThreadMenuItem + <MenuItem key="newSubchannel" text="Create new subchannel" icon={faPlusCircle} @@ -170,7 +165,7 @@ return null; } return ( - <ThreadMenuItem + <MenuItem key="leave" text="Leave Thread" icon={faSignOutAlt} @@ -182,7 +177,7 @@ const menuItems = React.useMemo(() => { const notificationsItem = ( - <ThreadMenuItem key="notifications" text="Notifications" icon={faBell} /> + <MenuItem key="notifications" text="Notifications" icon={faBell} /> ); const separator = <hr key="separator" className={css.separator} />; @@ -211,45 +206,11 @@ createSubchannelsItem, leaveThreadItem, ]); - - const closeMenuCallback = React.useCallback(() => { - document.removeEventListener('click', closeMenuCallback); - if (isOpen) { - setIsOpen(false); - } - }, [isOpen]); - - React.useEffect(() => { - if (!document || !isOpen) { - return undefined; - } - document.addEventListener('click', closeMenuCallback); - return () => document.removeEventListener('click', closeMenuCallback); - }, [closeMenuCallback, isOpen]); - - const switchMenuCallback = React.useCallback(() => { - setIsOpen(isMenuOpen => !isMenuOpen); - }, []); - - if (menuItems.length === 0) { - return null; - } - - let menuActionList = null; - if (isOpen) { - menuActionList = ( - <div className={css.topBarMenuActionList}>{menuItems}</div> - ); - } - - return ( - <> - <button className={css.topBarMenuButton} onClick={switchMenuCallback}> - <SWMansionIcon icon="menu-vertical" size={24} /> - </button> - {menuActionList} - </> + const icon = React.useMemo( + () => <SWMansionIcon icon="menu-vertical" size={20} />, + [], ); + return <Menu icon={icon}>{menuItems}</Menu>; } export default ThreadMenu; diff --git a/web/chat/thread-menu-item.react.js b/web/components/menu-item.react.js rename from web/chat/thread-menu-item.react.js rename to web/components/menu-item.react.js --- a/web/chat/thread-menu-item.react.js +++ b/web/components/menu-item.react.js @@ -5,24 +5,24 @@ import classNames from 'classnames'; import * as React from 'react'; -import css from './thread-menu.css'; +import css from './menu.css'; -type ThreadMenuItemProps = { +type MenuItemProps = { +onClick?: () => mixed, +icon: IconDefinition, +text: string, +dangerous?: boolean, }; -function ThreadMenuItem(props: ThreadMenuItemProps): React.Node { +function MenuItem(props: MenuItemProps): React.Node { const { onClick, icon, text, dangerous } = props; - const itemClasses = classNames(css.topBarMenuAction, { - [css.topBarMenuActionDangerous]: dangerous, + const itemClasses = classNames(css.menuAction, { + [css.menuActionDangerous]: dangerous, }); return ( <button className={itemClasses} onClick={onClick}> - <div className={css.topBarMenuActionIcon}> + <div className={css.menuActionIcon}> <FontAwesomeIcon icon={icon} className={css.promptIcon} /> </div> <div>{text}</div> @@ -30,8 +30,8 @@ ); } -const MemoizedThreadMenuItem: React.ComponentType<ThreadMenuItemProps> = React.memo( - ThreadMenuItem, +const MemoizedMenuItem: React.ComponentType<MenuItemProps> = React.memo( + MenuItem, ); -export default MemoizedThreadMenuItem; +export default MemoizedMenuItem; diff --git a/web/chat/thread-menu.css b/web/components/menu.css copy from web/chat/thread-menu.css copy to web/components/menu.css --- a/web/chat/thread-menu.css +++ b/web/components/menu.css @@ -1,28 +1,28 @@ -button.topBarMenuButton { +button.menuButton { background-color: transparent; border: none; cursor: pointer; color: var(--thread-top-bar-menu-color); } -div.topBarMenuActionList { +div.menuActionList { position: absolute; right: 10px; top: 55px; z-index: 1; display: flex; flex-direction: column; - background-color: var(--thread-menu-bg); + background-color: var(--menu-bg); border-radius: 4px; padding: 4px 0; } -button.topBarMenuAction { +button.menuAction { z-index: 1; background-color: transparent; padding: 12px 16px; - color: var(--thread-menu-color); - background-color: var(--thread-menu-bg); + color: var(--menu-color); + background-color: var(--menu-bg); font-size: var(--m-font-16); line-height: 1.5; border: none; @@ -31,11 +31,11 @@ align-items: center; } -button.topBarMenuAction:hover { - color: var(--thread-menu-color-hover); +button.menuAction:hover { + color: var(--menu-color-hover); } -div.topBarMenuActionIcon { +div.menuActionIcon { font-size: var(--l-font-18); display: flex; justify-content: center; @@ -43,16 +43,16 @@ width: 20px; } -button.topBarMenuActionDangerous { - color: var(--thread-menu-color-dangerous); +button.menuActionDangerous { + color: var(--menu-color-dangerous); } -button.topBarMenuActionDangerous:hover { - color: var(--thread-menu-color-dangerous-hover); +button.menuActionDangerous:hover { + color: var(--menu-color-dangerous-hover); } hr.separator { height: 1px; - background: var(--thread-menu-separator-color); + background: var(--menu-separator-color); margin: 10px 16px; max-width: 130px; border: none; diff --git a/web/components/menu.react.js b/web/components/menu.react.js new file mode 100644 --- /dev/null +++ b/web/components/menu.react.js @@ -0,0 +1,55 @@ +// @flow + +import * as React from 'react'; + +import css from './menu.css'; + +type MenuProps = { + +icon: React.Node, + +children?: React.Node, +}; + +function Menu(props: MenuProps): React.Node { + const [isOpen, setIsOpen] = React.useState(false); + + const { icon, children } = props; + + const closeMenuCallback = React.useCallback(() => { + document.removeEventListener('click', closeMenuCallback); + if (isOpen) { + setIsOpen(false); + } + }, [isOpen]); + + React.useEffect(() => { + if (!document || !isOpen) { + return undefined; + } + document.addEventListener('click', closeMenuCallback); + return () => document.removeEventListener('click', closeMenuCallback); + }, [closeMenuCallback, isOpen]); + + const switchMenuCallback = React.useCallback(() => { + setIsOpen(isMenuOpen => !isMenuOpen); + }, []); + + if (React.Children.count(children) === 0) { + return null; + } + + let menuActionList = null; + if (isOpen) { + menuActionList = <div className={css.menuActionList}>{children}</div>; + } + + return ( + <div> + <button className={css.menuButton} onClick={switchMenuCallback}> + {icon} + </button> + {menuActionList} + </div> + ); +} + +export default Menu; diff --git a/web/theme.css b/web/theme.css --- a/web/theme.css +++ b/web/theme.css @@ -104,12 +104,12 @@ --thread-ancestor-separator-color: var(--shades-white-60); --text-message-default-background: var(--shades-black-80); --message-action-tooltip-bg: var(--shades-black-90); - --thread-menu-bg: var(--shades-black-90); - --thread-menu-separator-color: var(--shades-black-80); - --thread-menu-color: var(--shades-black-60); - --thread-menu-color-hover: var(--shades-white-100); - --thread-menu-color-dangerous: var(--error-primary); - --thread-menu-color-dangerous-hover: var(--error-light-50); + --menu-bg: var(--shades-black-90); + --menu-separator-color: var(--shades-black-80); + --menu-color: var(--shades-black-60); + --menu-color-hover: var(--shades-white-100); + --menu-color-dangerous: var(--error-primary); + --menu-color-dangerous-hover: var(--error-light-50); --app-list-icon-read-only-color: var(--shades-black-60); --app-list-icon-enabled-color: var(--success-primary); --app-list-icon-disabled-color: var(--shades-white-80);