From 741d76076090077e06be26bb409327a4efcdf90c Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 26 Apr 2026 06:12:46 +0000 Subject: [PATCH] fix(radial): use backdrop element for click-outside dismiss Replace unreliable window event listener with transparent full-screen backdrop element. Clicking anywhere outside the radial menu now properly dismisses it. Also handles right-click on backdrop for dismiss. Co-Authored-By: Claude Opus 4.5 --- src/components/RadialMenu.jsx | 270 ++++++++++++++++++---------------- 1 file changed, 140 insertions(+), 130 deletions(-) diff --git a/src/components/RadialMenu.jsx b/src/components/RadialMenu.jsx index 4cff975..fa411b3 100644 --- a/src/components/RadialMenu.jsx +++ b/src/components/RadialMenu.jsx @@ -44,24 +44,6 @@ export default function RadialMenu({ return () => window.removeEventListener('keydown', handleKey) }, [open, onDismiss]) - // Handle click outside - useEffect(() => { - if (!open) return - const handleClick = (e) => { - if (containerRef.current && !containerRef.current.contains(e.target)) { - onDismiss?.() - } - } - // Delay to avoid triggering on the same click that opened the menu - const timer = setTimeout(() => { - window.addEventListener('click', handleClick) - }, 50) - return () => { - clearTimeout(timer) - window.removeEventListener('click', handleClick) - } - }, [open, onDismiss]) - // Calculate which wedge the pointer is over const getWedgeAtPoint = useCallback((clientX, clientY) => { const dx = clientX - x @@ -109,6 +91,17 @@ export default function RadialMenu({ onDismiss?.() }, [getWedgeAtPoint, lat, lon, onDismiss]) + // Handle backdrop click (dismiss menu) + const handleBackdropClick = useCallback((e) => { + e.stopPropagation() + onDismiss?.() + }, [onDismiss]) + + // Prevent menu container clicks from reaching backdrop + const handleContainerClick = useCallback((e) => { + e.stopPropagation() + }, []) + // Generate wedge paths const generateWedgePath = (index) => { const startAngle = (index * wedgeAngle - 90) * (Math.PI / 180) @@ -144,126 +137,143 @@ export default function RadialMenu({ const clampedY = Math.max(padding, Math.min(window.innerHeight - padding, y)) const content = ( -
- + {/* Full-screen transparent backdrop for dismiss */} +
+ + {/* Radial menu container */} +
- {/* Wedges */} - {wedges.map((wedge, i) => { - const iconPos = getIconPosition(i) - const Icon = wedge.icon - return ( - - - - {Icon && ( - - )} - {wedge.requiresAuth && ( - - )} - - {wedge.label} - + + {/* Wedges */} + {wedges.map((wedge, i) => { + const iconPos = getIconPosition(i) + const Icon = wedge.icon + return ( + + + + {Icon && ( + + )} + {wedge.requiresAuth && ( + + )} + + {wedge.label} + + - - ) - })} + ) + })} - {/* Center disc */} - - - {lat?.toFixed(4)} - - - {lon?.toFixed(4)} - - {centerLabel && ( + {/* Center disc */} + - {centerLabel.length > 15 ? centerLabel.slice(0, 15) + '…' : centerLabel} + {lat?.toFixed(4)} - )} - + + {lon?.toFixed(4)} + + {centerLabel && ( + + {centerLabel.length > 15 ? centerLabel.slice(0, 15) + '…' : centerLabel} + + )} + - -
+ +
+ ) return createPortal(content, document.body)