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

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 { t } from "@lingui/core/macro";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
HiLink, HiLink,
HiOutlineArrowRightCircle,
HiOutlineCalendar, HiOutlineCalendar,
HiOutlineDocumentDuplicate, HiOutlineDocumentDuplicate,
HiOutlineTag, HiOutlineTag,
HiOutlineTrash, HiOutlineTrash,
HiOutlineUserGroup, HiOutlineUserGroup,
HiOutlineArrowRightCircle,
} from "react-icons/hi2"; } from "react-icons/hi2";
import { useIsomorphicLayoutEffect } from "~/hooks/useIsomorphicLayoutEffect";
export type CardContextMenuAction = export type CardContextMenuAction =
| "members" | "members"
| "move" | "move"
@@ -85,6 +87,55 @@ export function CardContextMenu({
canEdit, canEdit,
}: CardContextMenuProps) { }: CardContextMenuProps) {
const menuRef = useRef<HTMLDivElement>(null); 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(() => { useEffect(() => {
const handleClickOutside = (e: MouseEvent) => { const handleClickOutside = (e: MouseEvent) => {
@@ -103,13 +154,11 @@ export function CardContextMenu({
}; };
}, [onClose]); }, [onClose]);
const items = MENU_ITEMS.filter((item) => !item.requiresEdit || canEdit);
return ( return (
<div <div
ref={menuRef} 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" 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: x, top: y }} style={{ left: position.x, top: position.y }}
> >
{items.map(({ action, label, icon }) => ( {items.map(({ action, label, icon }) => (
<button <button
@@ -119,7 +168,7 @@ export function CardContextMenu({
onAction(action); onAction(action);
onClose(); 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} {icon}
{label} {label}