From 8e07c5f79bc63db56c151d095df214fef9a8dad3 Mon Sep 17 00:00:00 2001 From: codemastervy Date: Tue, 1 Sep 2026 13:57:25 +1200 Subject: [PATCH] Fix context menu items firing nothing on click The dismiss-on-outside-click listener was a capture-phase handler on document, which runs before the event reaches (or bubbles back out of) any descendant -- including the menu's own buttons. So it closed the menu on every mousedown inside it too, unmounting the component before the button's click ever fired. The container's onMouseDown{stopPropagation} could not prevent this: stopPropagation only affects listeners still pending in the dispatch order, and a document-level capture listener has already run by the time a bubble-phase React handler further down the tree executes. Fixed by checking containment in the capture handler itself (only dismiss when the mousedown target is outside the menu), rather than relying on a bubble-phase stopPropagation to save it after the fact. Reproduced with a real Puppeteer mouse-click sequence (not element.click(), which bypasses the actual event order this bug lives in) against a locally-run build: right-click a folder, click 'Share via SMB' -- before the fix, no modal opens and no request fires; after, both work. Verified the same for 'New folder' end-to-end including the resulting POST, and confirmed clicking outside the menu still correctly dismisses it (no regression). Backend's 51 tests unaffected, as expected for a frontend-only fix. --- frontend/src/components/ContextMenu.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/ContextMenu.tsx b/frontend/src/components/ContextMenu.tsx index 1e842d2..3e4bd24 100644 --- a/frontend/src/components/ContextMenu.tsx +++ b/frontend/src/components/ContextMenu.tsx @@ -33,14 +33,24 @@ export function ContextMenu({ x, y, items, onClose }: Props) { }, [x, y]) useEffect(() => { + // Capture phase, so this sees the click before anything underneath it + // does -- but that also means it fires before the menu's OWN buttons get + // their click, so a click on a menu item must be excluded here rather + // than relying on stopPropagation() to save it: propagation only + // affects listeners still pending in the dispatch order, and a capture + // listener on `document` has already run before the event reaches (and + // bubbles back out of) a descendant like a menu button. + const dismissIfOutside = (e: MouseEvent) => { + if (ref.current?.contains(e.target as Node)) return + onClose() + } const dismiss = () => onClose() const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() } - // `capture` so a click anywhere closes before it activates something else. - document.addEventListener('mousedown', dismiss, true) + document.addEventListener('mousedown', dismissIfOutside, true) document.addEventListener('scroll', dismiss, true) document.addEventListener('keydown', onKey) return () => { - document.removeEventListener('mousedown', dismiss, true) + document.removeEventListener('mousedown', dismissIfOutside, true) document.removeEventListener('scroll', dismiss, true) document.removeEventListener('keydown', onKey) } @@ -48,8 +58,7 @@ export function ContextMenu({ x, y, items, onClose }: Props) { return (
e.stopPropagation()}> + style={{ left: pos.x, top: pos.y }}> {items.map((item, i) => ( 0}>