From 38bca1fe005f9b6d77bc48322ba1a54d7b9c6b3c Mon Sep 17 00:00:00 2001 From: Aryan-Verma-999 Date: Wed, 18 Mar 2026 19:38:05 +0530 Subject: [PATCH 1/2] fix(mobile): responsive kebab menu with bottom sheet on small viewports --- .../ui-elements/src/components/Menu/Menu.js | 141 ++++++++++++++---- .../src/components/Menu/Menu.styles.js | 65 ++++++++ .../src/components/Menu/MenuItem.js | 9 +- 3 files changed, 181 insertions(+), 34 deletions(-) diff --git a/packages/ui-elements/src/components/Menu/Menu.js b/packages/ui-elements/src/components/Menu/Menu.js index 5ad82f5c2d..4387ad5282 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, { useCallback, useEffect, useMemo, useState } from 'react'; import { css } from '@emotion/react'; import useTheme from '../../hooks/useTheme'; import { Box } from '../Box'; @@ -9,6 +9,23 @@ import { appendClassNames } from '../../lib/appendClassNames'; import { Tooltip } from '../Tooltip'; import { getMenuStyles } from './Menu.styles'; +const MOBILE_BREAKPOINT = 768; + +const useIsMobile = () => { + const [isMobile, setIsMobile] = useState( + () => window.innerWidth < MOBILE_BREAKPOINT + ); + + useEffect(() => { + const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); + const handler = (e) => setIsMobile(e.matches); + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + }, []); + + return isMobile; +}; + const Menu = ({ options = [], className = '', @@ -20,11 +37,14 @@ const Menu = ({ }) => { const { theme } = useTheme(); const styles = getMenuStyles(theme); + const isMobile = useIsMobile(); + const { classNames, styleOverrides } = useComponentOverrides( 'Menu', className, style ); + const anchorStyle = useMemo(() => { const positions = anchor.split(/\s+/); const styleAnchor = {}; @@ -44,52 +64,110 @@ const Menu = ({ const [isOpen, setOpen] = useState(false); + const close = useCallback(() => setOpen(false), []); + const onClick = (action, disabled) => () => { if (!disabled) { action(); - setOpen(!isOpen); + setOpen(false); } }; + // Close on outside click (desktop only — mobile uses backdrop) useEffect(() => { + if (isMobile || !isOpen) return undefined; const onBodyClick = (e) => { - if (isOpen && !e.target.classList.contains('ec-menu-wrapper')) { + if (!e.target.classList.contains('ec-menu-wrapper')) { setOpen(false); } }; - document.addEventListener('click', onBodyClick); + return () => document.removeEventListener('click', onBodyClick); + }, [isOpen, isMobile]); - return () => { - document.removeEventListener('click', onBodyClick); - }; - }, [isOpen]); + // Close on Escape + useEffect(() => { + if (!isOpen) return undefined; + const onKey = (e) => e.key === 'Escape' && close(); + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [isOpen, close]); + + const triggerButton = tooltip.isToolTip ? ( + + { + e.stopPropagation(); + setOpen((prev) => !prev); + }} + /> + + ) : ( + { + e.stopPropagation(); + setOpen((prev) => !prev); + }} + /> + ); + + // ── Mobile bottom sheet ────────────────────────────────────────────────── + if (isMobile) { + return ( + + {triggerButton} + {isOpen && ( + <> + {/* Backdrop */} +