From 0f57279c771b466146d62a8c58b0beef05b61c91 Mon Sep 17 00:00:00 2001 From: shiyue <935732994@qq.com> Date: Tue, 25 Aug 2026 22:02:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=8D=A1=E7=89=87=E5=8F=B3?= =?UTF-8?q?=E9=94=AE=E8=8F=9C=E5=8D=95=E5=BA=95=E9=83=A8=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../board/components/CardContextMenu.tsx | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/apps/web/src/views/board/components/CardContextMenu.tsx b/apps/web/src/views/board/components/CardContextMenu.tsx index 3b84e3cd..40f53e86 100644 --- a/apps/web/src/views/board/components/CardContextMenu.tsx +++ b/apps/web/src/views/board/components/CardContextMenu.tsx @@ -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(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 (
{items.map(({ action, label, icon }) => (