From 8ed94193d8e742a9d00a6441be7d824c797be4b5 Mon Sep 17 00:00:00 2001 From: weishu Date: Mon, 12 Jan 2026 19:08:01 +0800 Subject: [PATCH] fix: position session action menu at touch/click point - Updated useLongPress.ts to pass click coordinates to onLongPress callback - Modified SessionList.tsx to use menuAnchorPoint state instead of menuAnchorRef - Updated SessionActionMenu.tsx to position menu based on anchorPoint coordinates - Enhanced SessionHeader.tsx to calculate anchorPoint from button position and added stopPropagation to fix toggle issue The menu now appears at the exact touch/click position instead of being centered or aligned to the button edge. --- web/src/components/SessionActionMenu.tsx | 39 +++++++----------------- web/src/components/SessionHeader.tsx | 15 +++++++-- web/src/components/SessionList.tsx | 11 +++---- web/src/hooks/useLongPress.ts | 17 ++++++----- 4 files changed, 38 insertions(+), 44 deletions(-) diff --git a/web/src/components/SessionActionMenu.tsx b/web/src/components/SessionActionMenu.tsx index 5d225602..9f86e5b1 100644 --- a/web/src/components/SessionActionMenu.tsx +++ b/web/src/components/SessionActionMenu.tsx @@ -5,8 +5,7 @@ import { useLayoutEffect, useRef, useState, - type CSSProperties, - type RefObject + type CSSProperties } from 'react' import { useTranslation } from '@/lib/use-translation' @@ -17,8 +16,7 @@ type SessionActionMenuProps = { onRename: () => void onArchive: () => void onDelete: () => void - anchorRef?: RefObject - align?: 'start' | 'end' + anchorPoint: { x: number; y: number } menuId?: string } @@ -101,8 +99,7 @@ export function SessionActionMenu(props: SessionActionMenuProps) { onRename, onArchive, onDelete, - anchorRef, - align = 'end', + anchorPoint, menuId } = props const menuRef = useRef(null) @@ -136,32 +133,19 @@ export function SessionActionMenu(props: SessionActionMenuProps) { const padding = 8 const gap = 8 - let top = (viewportHeight - menuRect.height) / 2 - let left = (viewportWidth - menuRect.width) / 2 - let transformOrigin = 'top center' + const spaceBelow = viewportHeight - anchorPoint.y + const spaceAbove = anchorPoint.y + const openAbove = spaceBelow < menuRect.height + gap && spaceAbove > spaceBelow - const anchorEl = anchorRef?.current - if (anchorEl) { - const anchorRect = anchorEl.getBoundingClientRect() - const spaceBelow = viewportHeight - anchorRect.bottom - const spaceAbove = anchorRect.top - const openAbove = spaceBelow < menuRect.height + gap && spaceAbove > spaceBelow - - top = openAbove ? anchorRect.top - menuRect.height - gap : anchorRect.bottom + gap - if (align === 'start') { - left = anchorRect.left - transformOrigin = openAbove ? 'bottom left' : 'top left' - } else { - left = anchorRect.right - menuRect.width - transformOrigin = openAbove ? 'bottom right' : 'top right' - } - } + let top = openAbove ? anchorPoint.y - menuRect.height - gap : anchorPoint.y + gap + let left = anchorPoint.x - menuRect.width / 2 + const transformOrigin = openAbove ? 'bottom center' : 'top center' top = Math.min(Math.max(top, padding), viewportHeight - menuRect.height - padding) left = Math.min(Math.max(left, padding), viewportWidth - menuRect.width - padding) setMenuPosition({ top, left, transformOrigin }) - }, [align, anchorRef]) + }, [anchorPoint]) useLayoutEffect(() => { if (!isOpen) return @@ -177,7 +161,6 @@ export function SessionActionMenu(props: SessionActionMenuProps) { const handlePointerDown = (event: PointerEvent) => { const target = event.target as Node if (menuRef.current?.contains(target)) return - if (anchorRef?.current?.contains(target)) return onClose() } @@ -202,7 +185,7 @@ export function SessionActionMenu(props: SessionActionMenuProps) { window.removeEventListener('resize', handleReflow) window.removeEventListener('scroll', handleReflow, true) } - }, [anchorRef, isOpen, onClose, updatePosition]) + }, [isOpen, onClose, updatePosition]) useEffect(() => { if (!isOpen) return diff --git a/web/src/components/SessionHeader.tsx b/web/src/components/SessionHeader.tsx index b5d58e9e..3728b267 100644 --- a/web/src/components/SessionHeader.tsx +++ b/web/src/components/SessionHeader.tsx @@ -72,6 +72,7 @@ export function SessionHeader(props: { const worktreeBranch = session.metadata?.worktree?.branch const [menuOpen, setMenuOpen] = useState(false) + const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) const menuId = useId() const menuAnchorRef = useRef(null) const [renameOpen, setRenameOpen] = useState(false) @@ -89,6 +90,14 @@ export function SessionHeader(props: { onSessionDeleted?.() } + const handleMenuToggle = () => { + if (!menuOpen && menuAnchorRef.current) { + const rect = menuAnchorRef.current.getBoundingClientRect() + setMenuAnchorPoint({ x: rect.right, y: rect.bottom }) + } + setMenuOpen((open) => !open) + } + // In Telegram, don't render header (Telegram provides its own) if (isTelegramApp()) { return null @@ -151,7 +160,8 @@ export function SessionHeader(props: {