diff --git a/packages/ui-elements/src/components/Menu/Menu.js b/packages/ui-elements/src/components/Menu/Menu.js index 5ad82f5c2d..b2aba4699f 100644 --- a/packages/ui-elements/src/components/Menu/Menu.js +++ b/packages/ui-elements/src/components/Menu/Menu.js @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { css } from '@emotion/react'; import useTheme from '../../hooks/useTheme'; import { Box } from '../Box'; @@ -9,6 +9,11 @@ import { appendClassNames } from '../../lib/appendClassNames'; import { Tooltip } from '../Tooltip'; import { getMenuStyles } from './Menu.styles'; +const MOBILE_BREAKPOINT = 499; + +const getIsMobileViewport = () => + typeof window !== 'undefined' && window.innerWidth <= MOBILE_BREAKPOINT; + const Menu = ({ options = [], className = '', @@ -43,17 +48,39 @@ const Menu = ({ useComponentOverrides('MenuWrapper'); const [isOpen, setOpen] = useState(false); + const [isMobile, setIsMobile] = useState(getIsMobileViewport); + const wrapperRef = useRef(null); const onClick = (action, disabled) => () => { if (!disabled) { action(); - setOpen(!isOpen); + setOpen(false); } }; + useEffect(() => { + if (typeof window === 'undefined') { + return undefined; + } + + const onResize = () => { + setIsMobile(getIsMobileViewport()); + }; + + window.addEventListener('resize', onResize); + + return () => { + window.removeEventListener('resize', onResize); + }; + }, []); + useEffect(() => { const onBodyClick = (e) => { - if (isOpen && !e.target.classList.contains('ec-menu-wrapper')) { + if ( + isOpen && + wrapperRef.current && + !wrapperRef.current.contains(e.target) + ) { setOpen(false); } }; @@ -65,32 +92,61 @@ const Menu = ({ }; }, [isOpen]); + const menuItems = options.map((option, idx) => ( + + )); + + const triggerButton = tooltip.isToolTip ? ( + + { + e.stopPropagation(); + setOpen((prev) => !prev); + }} + /> + + ) : ( + { + e.stopPropagation(); + setOpen((prev) => !prev); + }} + /> + ); + const optionJsx = ( <> - {tooltip.isToolTip ? ( - - { - e.stopPropagation(); - setOpen((prev) => !prev); - }} - /> - - ) : ( - { - e.stopPropagation(); - setOpen((prev) => !prev); - }} - /> - )} - {isOpen ? ( + {triggerButton} + {isOpen && isMobile ? ( + <> + setOpen(false)} /> + e.stopPropagation()} + > + {menuItems} + + + ) : null} + {isOpen && !isMobile ? ( - {options.map((option, idx) => ( - - ))} + {menuItems} ) : null} ); return useWrapper ? ( ) : ( - optionJsx + {optionJsx} ); }; diff --git a/packages/ui-elements/src/components/Menu/Menu.styles.js b/packages/ui-elements/src/components/Menu/Menu.styles.js index 2537aaaef5..b2b680225d 100644 --- a/packages/ui-elements/src/components/Menu/Menu.styles.js +++ b/packages/ui-elements/src/components/Menu/Menu.styles.js @@ -21,6 +21,28 @@ export const getMenuStyles = (theme) => { box-shadow: ${theme.shadows[1]}; background-color: ${theme.colors.background}; `, + + backdrop: css` + position: fixed; + inset: 0; + z-index: ${theme.zIndex?.menu || 1300}; + background: transparent; + `, + + sheet: css` + position: fixed; + left: 0.5rem; + right: 0.5rem; + bottom: 0.5rem; + display: flex; + flex-direction: column; + max-height: min(70vh, calc(100vh - 6rem)); + overflow-y: auto; + z-index: ${(theme.zIndex?.menu || 1300) + 1}; + border-radius: 0.75rem; + padding: 0.75rem 0; + background-color: ${theme.colors.background}; + `, }; return styles; @@ -30,6 +52,7 @@ export const getMenuItemStyles = ({ theme, mode }) => { const styles = { item: css` font-size: 14px; + font-family: inherit; display: flex; flex-direction: row; align-items: center; @@ -46,6 +69,28 @@ export const getMenuItemStyles = ({ theme, mode }) => { } `, + itemMobile: css` + font-size: 14px; + font-family: inherit; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 0.5rem; + padding: 0.75rem 1rem; + white-space: nowrap; + color: ${theme.colors.foreground}; + &:hover { + background-color: ${mode === 'light' + ? darken(theme.colors.background, 0.05) + : lighten(theme.colors.background, 2)}; + cursor: pointer; + } + & + & { + border-top: 1px solid ${theme.colors.border}; + } + `, + disabled: css` cursor: not-allowed !important; color: ${theme.colors.mutedForeground}; diff --git a/packages/ui-elements/src/components/Menu/MenuItem.js b/packages/ui-elements/src/components/Menu/MenuItem.js index f0c69b4e53..4fe7f55982 100644 --- a/packages/ui-elements/src/components/Menu/MenuItem.js +++ b/packages/ui-elements/src/components/Menu/MenuItem.js @@ -7,7 +7,7 @@ import { appendClassNames } from '../../lib/appendClassNames'; import { getMenuItemStyles } from './Menu.styles'; import { useTheme } from '../../hooks'; -const MenuItem = ({ icon, label, action, disabled }) => { +const MenuItem = ({ icon, label, action, disabled, isMobile = false }) => { const { classNames, styleOverrides } = useComponentOverrides( 'MenuItem', disabled && 'disabled' @@ -17,12 +17,15 @@ const MenuItem = ({ icon, label, action, disabled }) => { return ( - + {label} );