mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 18:14:58 +08: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 {
|
import {
|
||||||
Group,
|
Group,
|
||||||
UnstyledButton,
|
UnstyledButton,
|
||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
ActionIcon,
|
ActionIcon,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TextInput,
|
TextInput,
|
||||||
Menu,
|
Popover,
|
||||||
|
Stack,
|
||||||
|
Divider,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconPlus, IconPencil, IconTrash, IconTable } from "@tabler/icons-react";
|
import { IconPlus, IconPencil, IconTrash, IconTable } from "@tabler/icons-react";
|
||||||
import { IBaseView } from "@/features/base/types/base.types";
|
import { IBaseView } from "@/features/base/types/base.types";
|
||||||
@@ -15,6 +17,7 @@ import {
|
|||||||
useDeleteViewMutation,
|
useDeleteViewMutation,
|
||||||
} from "@/features/base/queries/base-view-query";
|
} from "@/features/base/queries/base-view-query";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import cellClasses from "@/features/base/styles/cells.module.css";
|
||||||
|
|
||||||
type ViewTabsProps = {
|
type ViewTabsProps = {
|
||||||
views: IBaseView[];
|
views: IBaseView[];
|
||||||
@@ -147,6 +150,34 @@ function ViewTab({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [menuOpened, setMenuOpened] = useState(false);
|
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) {
|
if (isEditing) {
|
||||||
return (
|
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 (
|
return (
|
||||||
<Menu
|
<Popover
|
||||||
opened={menuOpened}
|
opened={menuOpened}
|
||||||
onChange={setMenuOpened}
|
onClose={() => setMenuOpened(false)}
|
||||||
position="bottom-start"
|
position="bottom-start"
|
||||||
shadow="md"
|
shadow="md"
|
||||||
width={180}
|
width={180}
|
||||||
withinPortal
|
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
|
<UnstyledButton
|
||||||
onClick={onClick}
|
ref={targetRef}
|
||||||
|
onClick={handleTabClick}
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setMenuOpened(true);
|
setMenuOpened(true);
|
||||||
@@ -198,27 +234,41 @@ function ViewTab({
|
|||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
</Menu.Target>
|
</Popover.Target>
|
||||||
<Menu.Dropdown>
|
<Popover.Dropdown ref={dropdownRef} p={4}>
|
||||||
<Menu.Item
|
<Stack gap={0}>
|
||||||
leftSection={<IconPencil size={14} />}
|
<UnstyledButton
|
||||||
onClick={onRenameStart}
|
className={cellClasses.menuItem}
|
||||||
>
|
onClick={() => {
|
||||||
{t("Rename")}
|
setMenuOpened(false);
|
||||||
</Menu.Item>
|
onRenameStart();
|
||||||
{canDelete && (
|
}}
|
||||||
<>
|
>
|
||||||
<Menu.Divider />
|
<Group gap={8} wrap="nowrap">
|
||||||
<Menu.Item
|
<IconPencil size={14} />
|
||||||
leftSection={<IconTrash size={14} />}
|
<Text size="sm">{t("Rename")}</Text>
|
||||||
onClick={onDelete}
|
</Group>
|
||||||
color="red"
|
</UnstyledButton>
|
||||||
>
|
{canDelete && (
|
||||||
{t("Delete view")}
|
<>
|
||||||
</Menu.Item>
|
<Divider my={4} />
|
||||||
</>
|
<UnstyledButton
|
||||||
)}
|
className={cellClasses.menuItem}
|
||||||
</Menu.Dropdown>
|
onClick={() => {
|
||||||
</Menu>
|
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