修复卡片右键菜单底部定位

This commit is contained in:
2026-08-25 22:02:43 +08:00
parent ed5b8d1644
commit 0f57279c77

View File

@@ -1,15 +1,17 @@
import { t } from "@lingui/core/macro";
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import {
HiLink,
HiOutlineArrowRightCircle,
HiOutlineCalendar,
HiOutlineDocumentDuplicate,
HiOutlineTag,
HiOutlineTrash,
HiOutlineUserGroup,
HiOutlineArrowRightCircle,
} from "react-icons/hi2";
import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect";
export type CardContextMenuAction =
| "members"
| "move"
@@ -85,6 +87,55 @@ export function CardContextMenu({
canEdit,
}: CardContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState({ x, y });
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
useIsomorphicLayoutEffect(() => {
const menu = menuRef.current;
if (!menu) return;
const gutter = 8;
const updatePosition = () => {
const { width, height } = menu.getBoundingClientRect();
const viewportWidth = window.visualViewport?.width ?? window.innerWidth;
const viewportHeight =
window.visualViewport?.height ?? window.innerHeight;
const maxX = Math.max(gutter, viewportWidth - width - gutter);
const maxY = Math.max(gutter, viewportHeight - height - gutter);
const nextPosition = {
x: Math.min(Math.max(x, gutter), maxX),
y: Math.min(Math.max(y, gutter), maxY),
};
setPosition((currentPosition) =>
currentPosition.x === nextPosition.x &&
currentPosition.y === nextPosition.y
? currentPosition
: nextPosition,
);
};
updatePosition();
const resizeObserver =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(updatePosition)
: null;
resizeObserver?.observe(menu);
window.addEventListener("resize", updatePosition);
window.addEventListener("scroll", updatePosition, true);
window.visualViewport?.addEventListener("resize", updatePosition);
window.visualViewport?.addEventListener("scroll", updatePosition);
return () => {
resizeObserver?.disconnect();
window.removeEventListener("resize", updatePosition);
window.removeEventListener("scroll", updatePosition, true);
window.visualViewport?.removeEventListener("resize", updatePosition);
window.visualViewport?.removeEventListener("scroll", updatePosition);
};
}, [x, y, canEdit, items.length]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
@@ -103,13 +154,11 @@ export function CardContextMenu({
};
}, [onClose]);
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
return (
<div
ref={menuRef}
className="fixed z-[200] min-w-[200px] rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: x, top: y }}
className="fixed z-[200] max-h-[calc(100dvh-1rem)] min-w-[min(200px,calc(100vw-1rem))] max-w-[calc(100vw-1rem)] overflow-y-auto overflow-x-hidden rounded-md border border-light-200 bg-white py-1 shadow-lg dark:border-dark-400 dark:bg-dark-200"
style={{ left: position.x, top: position.y }}
>
{items.map(({ action, label, icon }) => (
<button
@@ -119,7 +168,7 @@ export function CardContextMenu({
onAction(action);
onClose();
}}
className="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
className="flex w-full items-center gap-2 break-words px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
{icon}
{label}