From 4406efe1fd31217aa2a803462e85ef793ee8ab33 Mon Sep 17 00:00:00 2001 From: upendrasingh Date: Sun, 22 Mar 2026 23:09:12 +0530 Subject: [PATCH] fix: add touchcancel handler to prevent stuck gesture state Implement handleTouchCancel to properly clean up gesture state when touch sequences are interrupted by OS or browser events (notifications, system gestures, focus loss, etc.). Changes: - Add handleTouchCancel function that clears all touch tracking - Clear draggingTimeout to prevent spurious click events - Release active drag state if present - Reset all gesture state (tracking, moved, pinch, released count) - Update TouchArea interface and component to wire up the handler - ScreenMirror automatically receives handler via spread operator This prevents stuck drag states and pressed inputs that previously occurred when touchcancel events were unhandled, significantly improving mobile UX reliability. Fixes #75 Co-Authored-By: Claude Sonnet 4.5 --- src/components/Trackpad/TouchArea.tsx | 2 ++ src/hooks/useTrackpadGesture.ts | 39 +++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/components/Trackpad/TouchArea.tsx b/src/components/Trackpad/TouchArea.tsx index 03c367e8..16062095 100644 --- a/src/components/Trackpad/TouchArea.tsx +++ b/src/components/Trackpad/TouchArea.tsx @@ -7,6 +7,7 @@ interface TouchAreaProps { onTouchStart: (e: React.TouchEvent) => void onTouchMove: (e: React.TouchEvent) => void onTouchEnd: (e: React.TouchEvent) => void + onTouchCancel: (e: React.TouchEvent) => void } } @@ -32,6 +33,7 @@ export const TouchArea: React.FC = ({ onTouchStart={handleStart} onTouchMove={handlers.onTouchMove} onTouchEnd={handlers.onTouchEnd} + onTouchCancel={handlers.onTouchCancel} onMouseDown={handlePreventFocus} >
diff --git a/src/hooks/useTrackpadGesture.ts b/src/hooks/useTrackpadGesture.ts index 66f801d8..61bc6dfd 100644 --- a/src/hooks/useTrackpadGesture.ts +++ b/src/hooks/useTrackpadGesture.ts @@ -254,12 +254,51 @@ export const useTrackpadGesture = ( } } + const handleTouchCancel = (e: React.TouchEvent) => { + // touchcancel fires when the browser interrupts the touch sequence + // (e.g., OS notification, system gesture, loss of focus) + // We must clean up all gesture state to prevent stuck drag/press + + const touches = e.changedTouches + + // Remove all cancelled touches + for (let i = 0; i < touches.length; i++) { + ongoingTouches.current.delete(touches[i].identifier) + } + + // Clear drag timeout to prevent spurious click events + if (draggingTimeout.current) { + clearTimeout(draggingTimeout.current) + draggingTimeout.current = null + } + + // Release any active drag state + if (dragging.current) { + dragging.current = false + send({ type: "click", button: "left", press: false }) + } + + // Reset all gesture state if no touches remain + if (ongoingTouches.current.size === 0) { + setIsTracking(false) + moved.current = false + releasedCount.current = 0 + lastPinchDist.current = null + pinching.current = false + } else if (ongoingTouches.current.size < 2) { + // If we still have touches but less than 2, reset pinch state + lastPinchDist.current = null + pinching.current = false + } + } + return { isTracking, handlers: { onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, + onTouchCancel: handleTouchCancel, }, } }