Fix Sasquatch panel opening then instantly closing on mobile

On touch devices, the bubble's pointerup opened the panel and unmounted the
bubble; the synthesized "ghost click" from the same tap then landed on the
panel's minimize/close button (which renders in roughly the same bottom-left
spot), instantly re-closing it.

Fix: preventDefault/stopPropagation on pointerup to suppress the ghost click,
guard releasePointerCapture against unmount throws, and add a 400ms
"just opened" lock so stray close/minimize events from the same tap are
ignored.

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy 2026-07-25 15:24:05 +07:00
parent 5ec1c84ef0
commit ebbd0fd125

View file

@ -365,6 +365,10 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
const dragRef = useRef<HTMLDivElement>(null); const dragRef = useRef<HTMLDivElement>(null);
const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null); const dragStart = useRef<{ mx: number; my: number; bx: number; by: number } | null>(null);
const dragMoved = useRef(false); const dragMoved = useRef(false);
// Timestamp of the last panel-open via tap. Used to ignore stray
// close/minimize events that fire from the same tap on mobile.
const justOpenedRef = useRef(0);
const recentlyOpened = () => Date.now() - justOpenedRef.current < 400;
// Dragging handlers // Dragging handlers
const onPointerDown = useCallback((e: React.PointerEvent) => { const onPointerDown = useCallback((e: React.PointerEvent) => {
@ -389,13 +393,27 @@ export function SasquatchGuide({ message, onDismiss }: SasquatchGuideProps) {
}, [setDragPos]); }, [setDragPos]);
const onPointerUp = useCallback((e: React.PointerEvent) => { 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); if (dragRef.current) dragRef.current.releasePointerCapture(e.pointerId);
} catch {
// releasePointerCapture can throw if the element is unmounting — ignore.
}
const wasDrag = dragMoved.current; const wasDrag = dragMoved.current;
dragStart.current = null; dragStart.current = null;
dragMoved.current = false; dragMoved.current = false;
setTimeout(() => setIsDragging(false), 50); setTimeout(() => setIsDragging(false), 50);
if (!wasDrag) { if (!wasDrag) {
// Always open the panel — even on pages without a context message // 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); setIsMinimized(false);
setIsVisible(true); setIsVisible(true);
} }
@ -739,7 +757,7 @@ Instructions:
<div className="flex gap-1 flex-shrink-0"> <div className="flex gap-1 flex-shrink-0">
{/* Minimize — shrinks to draggable bubble */} {/* Minimize — shrinks to draggable bubble */}
<button <button
onClick={() => setIsMinimized(true)} onClick={() => { if (recentlyOpened()) return; setIsMinimized(true); }}
className="w-6 h-6 flex items-center justify-center rounded text-[#ccff00]/40 hover:text-[#ccff00] hover:bg-[#ccff00]/10 transition-colors" className="w-6 h-6 flex items-center justify-center rounded text-[#ccff00]/40 hover:text-[#ccff00] hover:bg-[#ccff00]/10 transition-colors"
aria-label="Minimize to bubble" aria-label="Minimize to bubble"
title="Minimize" title="Minimize"
@ -748,7 +766,7 @@ Instructions:
</button> </button>
{/* Close — hides the panel entirely */} {/* Close — hides the panel entirely */}
<button <button
onClick={() => { setIsVisible(false); if (onDismiss) onDismiss(); }} onClick={() => { if (recentlyOpened()) return; setIsVisible(false); if (onDismiss) onDismiss(); }}
className="w-6 h-6 flex items-center justify-center rounded text-white/30 hover:text-white/80 hover:bg-white/10 transition-colors" className="w-6 h-6 flex items-center justify-center rounded text-white/30 hover:text-white/80 hover:bg-white/10 transition-colors"
aria-label="Close panel" aria-label="Close panel"
title="Close" title="Close"