From 834f33d53eeaa57571da9a716bfca33604a38aa8 Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Sat, 25 Jul 2026 15:36:11 +0700 Subject: [PATCH] Make Sasquatch bubble open reliably on first tap (mobile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "1st tap does nothing, 2nd/3rd works" bug came from relying solely on pointer-capture tap detection, which is flaky on the first interaction after mount in mobile WebViews — and the earlier e.preventDefault() on pointerup was suppressing the click we could otherwise use. Fix: add a plain onClick as a reliable open path (the most dependable "tapped" signal in a WebView), extract an idempotent openPanel() used by both pointerup and click, and drop the preventDefault so the click isn't swallowed. The justOpenedRef lock still blocks the ghost-click that would re-close it. Double firing is harmless since openPanel just sets visible=true. Co-authored-by: shakespeare.diy --- src/components/SasquatchGuide.tsx | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 88ac9e3..2e8dbc1 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -392,14 +392,15 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { setDragPos({ x: newX, y: newY }); }, [setDragPos]); + // Single source of truth for opening the panel. Sets the "just opened" lock + // so a stray follow-up event from the same tap can't immediately close it. + const openPanel = useCallback(() => { + justOpenedRef.current = Date.now(); + setIsMinimized(false); + setIsVisible(true); + }, [setIsMinimized]); + const onPointerUp = useCallback((e: React.PointerEvent) => { - // Suppress the synthesized mouse "click" that follows a touch pointerup. - // Without this, on mobile WebViews the ghost click lands on whatever element - // is now under the finger (the panel unmounts the bubble the instant we - // open) and can immediately re-close the panel — the "opens then closes" - // bug. - e.preventDefault(); - e.stopPropagation(); try { if (dragRef.current) dragRef.current.releasePointerCapture(e.pointerId); } catch { @@ -409,15 +410,24 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) { dragStart.current = null; dragMoved.current = false; setTimeout(() => setIsDragging(false), 50); + // If this pointer sequence was a genuine tap (no real drag), open here. + // A parallel onClick handler also opens as a reliable fallback for mobile + // WebViews where the pointer-capture tap can be flaky on first interaction; + // openPanel is idempotent so double-firing is harmless. if (!wasDrag) { - // Always open the panel — even on pages without a context message. - // Set a short "just opened" lock so a stray follow-up event from the same - // tap can't immediately minimize/close it. - justOpenedRef.current = Date.now(); - setIsMinimized(false); - setIsVisible(true); + openPanel(); } - }, [setIsMinimized]); + }, [openPanel]); + + // Reliable open path: plain click. On mobile WebViews the pointer-capture tap + // detection can miss the very first interaction after mount (the "1st tap does + // nothing, 2nd works" bug). A normal onClick is the most dependable "I tapped + // this" signal, so we use it as a fallback — but ignore it if the gesture was + // actually a drag. + const onBubbleClick = useCallback(() => { + if (dragMoved.current) return; + openPanel(); + }, [openPanel]); const bubbleStyle = useMemo(() => { if (dragPos) { @@ -682,6 +692,7 @@ Instructions: onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={onPointerUp} + onClick={onBubbleClick} title="Press and drag to move · Tap to open" >