Skip to content

fix: reset swipe and interaction state on pointercancel - #784

Open
solomonaustin wants to merge 1 commit into
emilkowalski:mainfrom
solomonaustin:fix/pointercancel-resets-swipe
Open

fix: reset swipe and interaction state on pointercancel#784
solomonaustin wants to merge 1 commit into
emilkowalski:mainfrom
solomonaustin:fix/pointercancel-resets-swipe

Conversation

@solomonaustin

Copy link
Copy Markdown

Fixes #779.

Problem

The browser fires pointercancel instead of pointerup when it takes a pointer over — an OS-level gesture, another surface stealing the touch, some multi-touch conflicts. Nothing in src/index.tsx listened for it, so both pieces of state a swipe sets were left behind.

On the toast, isSwiping, swipeDirection, pointerStartRef and the --swipe-amount-x/y custom properties all stayed as they were, leaving the toast parked mid-swipe until the next full down/up cycle. That is the reported bug.

On the toaster, there is a second instance of the same gap that the issue does not mention, and it is the worse of the two. onPointerDown sets interacting, and only onPointerUp clears it:

onPointerDown={...setInteracting(true)}
onPointerUp={() => setInteracting(false)}

interacting gates the dismiss timer:

if (expanded || interacting || isDocumentHidden) {
  pauseTimer();
}

So a cancelled gesture left every toast paused and they stopped auto-dismissing entirely, not just visually stuck.

Fix

onPointerCancel in both places. On the toast it unwinds the swipe without dismissing — a cancelled gesture is not a swipe-out. On the toaster it clears interacting so the timer resumes.

Testing

One Playwright test: swipe part way, fire pointercancel, assert data-swiping goes back to false and --swipe-amount-x returns to 0px. It fails on main and passes with the change.

I could not write an honest test for the toaster half, and would rather say so than ship one that looks like coverage. Playwright cannot release its own mouse without emitting pointerup, and pointerup clears interacting anyway — so any test I wrote passed with and without the fix. Going without pointerup keeps pointer capture active, so mouseleave never fires and expanded stays true, which pauses the timer on its own and hides the effect. I confirmed both of those by instrumenting the DOM rather than assuming:

swiping: "false"   <- toast-level fix working
expanded: "true"   <- what actually blocks the timer in the test

Happy to split the toaster change into its own PR if you would rather keep this one to what the test covers.

The browser fires pointercancel instead of pointerup when it takes a pointer
over — an OS gesture, another surface stealing the touch, some multi-touch
conflicts. Nothing listened for it, so both pieces of state a swipe sets were
left behind.

On the toast, isSwiping, swipeDirection, the pointer origin and the
--swipe-amount-* custom properties stayed as they were, leaving the toast
parked mid-swipe until the next full down/up.

On the toaster, onPointerDown sets interacting and only onPointerUp clears
it. interacting pauses the dismiss timer, so a cancelled gesture left every
toast paused and they stopped auto-dismissing.

Fixes emilkowalski#779
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Toast swipe-to-dismiss never resets on pointercancel, only pointerup

1 participant