diff --git a/apps/client/src/features/base/components/views/view-tabs.tsx b/apps/client/src/features/base/components/views/view-tabs.tsx index b3e367313..b706ad0c0 100644 --- a/apps/client/src/features/base/components/views/view-tabs.tsx +++ b/apps/client/src/features/base/components/views/view-tabs.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback } from "react"; +import { useState, useCallback, useEffect, useRef } from "react"; import { Group, UnstyledButton, @@ -6,7 +6,9 @@ import { ActionIcon, Tooltip, TextInput, - Menu, + Popover, + Stack, + Divider, } from "@mantine/core"; import { IconPlus, IconPencil, IconTrash, IconTable } from "@tabler/icons-react"; import { IBaseView } from "@/features/base/types/base.types"; @@ -15,6 +17,7 @@ import { useDeleteViewMutation, } from "@/features/base/queries/base-view-query"; import { useTranslation } from "react-i18next"; +import cellClasses from "@/features/base/styles/cells.module.css"; type ViewTabsProps = { views: IBaseView[]; @@ -147,6 +150,34 @@ function ViewTab({ }) { const { t } = useTranslation(); const [menuOpened, setMenuOpened] = useState(false); + const targetRef = useRef(null); + const dropdownRef = useRef(null); + + // Mantine Popover's built-in closeOnClickOutside / closeOnEscape + // only fire when focus is inside the dropdown — but the menu opens + // via right-click (focus stays on body), so neither ever triggers. + // Wire the close paths ourselves while the menu is open. Capture + // phase mousedown so we run before grid-container's outside-click + // logic (which would otherwise blur the focused element first and + // cause weirdness). + useEffect(() => { + if (!menuOpened) return; + const onMouseDown = (e: MouseEvent) => { + const target = e.target as Node; + if (dropdownRef.current?.contains(target)) return; + if (targetRef.current?.contains(target)) return; + setMenuOpened(false); + }; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setMenuOpened(false); + }; + document.addEventListener("mousedown", onMouseDown, true); + document.addEventListener("keydown", onKey); + return () => { + document.removeEventListener("mousedown", onMouseDown, true); + document.removeEventListener("keydown", onKey); + }; + }, [menuOpened]); if (isEditing) { return ( @@ -162,22 +193,27 @@ function ViewTab({ ); } + const handleTabClick = useCallback(() => { + // Left-click on the tab is "switch view"; if the menu happens to + // be open from a prior right-click, dismiss it as part of the + // same gesture so the user doesn't have to click twice. + if (menuOpened) setMenuOpened(false); + onClick(); + }, [menuOpened, onClick]); + return ( - setMenuOpened(false)} position="bottom-start" shadow="md" width={180} withinPortal - // Default Menu behavior: closeOnClickOutside, closeOnEscape, and - // closeOnItemClick all true — replaces the manual setMenuOpened - // calls and gives the popover the same outside-click / Esc - // semantics every other Mantine menu in the app has. > - + { e.preventDefault(); setMenuOpened(true); @@ -198,27 +234,41 @@ function ViewTab({ - - - } - onClick={onRenameStart} - > - {t("Rename")} - - {canDelete && ( - <> - - } - onClick={onDelete} - color="red" - > - {t("Delete view")} - - - )} - - + + + + { + setMenuOpened(false); + onRenameStart(); + }} + > + + + {t("Rename")} + + + {canDelete && ( + <> + + { + setMenuOpened(false); + onDelete(); + }} + style={{ color: "var(--mantine-color-red-6)" }} + > + + + {t("Delete view")} + + + + )} + + + ); }