mirror of
https://github.com/docmost/docmost.git
synced 2026-07-11 06:14:44 +10:00
fix(base): keep view-tab context menu right-click-only, close on outside / Esc
Switching to <Menu> in the previous fix made left-click on a tab also open the menu — Mantine Menu auto-toggles via its Target's click handler in controlled mode, which we don't want (left-click should switch view, only right-click should open the context menu). Switch back to <Popover> (no auto-toggle on Target click) and wire outside-click / Escape close paths manually with a useEffect that's active only while the menu is open. Capture-phase mousedown so we run before grid-container's outside-click logic. Left-click on the tab now calls onClick (switch view) and dismisses the menu in the same gesture if it happens to be open.
This commit is contained in:
@@ -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<HTMLButtonElement | null>(null);
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(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 (
|
||||
<Menu
|
||||
<Popover
|
||||
opened={menuOpened}
|
||||
onChange={setMenuOpened}
|
||||
onClose={() => 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.
|
||||
>
|
||||
<Menu.Target>
|
||||
<Popover.Target>
|
||||
<UnstyledButton
|
||||
onClick={onClick}
|
||||
ref={targetRef}
|
||||
onClick={handleTabClick}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setMenuOpened(true);
|
||||
@@ -198,27 +234,41 @@ function ViewTab({
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
leftSection={<IconPencil size={14} />}
|
||||
onClick={onRenameStart}
|
||||
>
|
||||
{t("Rename")}
|
||||
</Menu.Item>
|
||||
{canDelete && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
leftSection={<IconTrash size={14} />}
|
||||
onClick={onDelete}
|
||||
color="red"
|
||||
>
|
||||
{t("Delete view")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown ref={dropdownRef} p={4}>
|
||||
<Stack gap={0}>
|
||||
<UnstyledButton
|
||||
className={cellClasses.menuItem}
|
||||
onClick={() => {
|
||||
setMenuOpened(false);
|
||||
onRenameStart();
|
||||
}}
|
||||
>
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<IconPencil size={14} />
|
||||
<Text size="sm">{t("Rename")}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
{canDelete && (
|
||||
<>
|
||||
<Divider my={4} />
|
||||
<UnstyledButton
|
||||
className={cellClasses.menuItem}
|
||||
onClick={() => {
|
||||
setMenuOpened(false);
|
||||
onDelete();
|
||||
}}
|
||||
style={{ color: "var(--mantine-color-red-6)" }}
|
||||
>
|
||||
<Group gap={8} wrap="nowrap">
|
||||
<IconTrash size={14} />
|
||||
<Text size="sm">{t("Delete view")}</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user