Toast swipe-to-dismiss never resets on pointercancel, only pointerup
Description
A toast's swipe-to-dismiss handling only listens for onPointerDown/onPointerUp/onPointerMove, there's no onPointerCancel anywhere in src/index.tsx.
onPointerDown={(event) => {
// ...
(event.target as HTMLElement).setPointerCapture(event.pointerId);
// ...
setSwiping(true);
pointerStartRef.current = { x: event.clientX, y: event.clientY };
}}
onPointerUp={() => {
// ... resets isSwiped/setSwiping(false)/swipeDirection/etc, or triggers dismiss
}}
onPointerMove={(event) => {
// ... updates --swipe-amount-x/--swipe-amount-y based on pointerStartRef
}}Per the Pointer Events spec, pointercancel fires instead of pointerup when the browser decides it can no longer generate events for a pointer (an OS-level gesture taking over the touch, another app/overlay stealing the interaction, some multi-touch conflicts). None of the swiping state (isSwiping, swipeDirection, pointerStartRef.current, the --swipe-amount-x/--swipe-amount-y CSS custom properties) gets reset when that happens, since only onPointerUp clears it.
A concrete recent precedent for this exact bug class: @vueuse/core's useDraggable had the identical gap (listened for pointerup but not pointercancel, leaving isDragging stuck true until the next full down/up cycle), fixed in vueuse/vueuse#5550.
Reproduction
- Trigger a toast on a touch device.
- Start a swipe gesture on it (
pointerdown+ a fewpointermoves past the point whereswipeDirectionlocks in). - Interrupt the gesture with something that fires
pointercancelbefore apointerup(a real one: the browser deciding the touch is actually a page scroll/pan and taking it over mid-gesture; synthetically, dispatch aPointerEvent('pointercancel', { pointerId })for the same pointer instead ofpointerup).
Expected: the toast returns to its normal, non-swiping state.
Actual: isSwiping/data-swiping and the swipe-offset custom properties stay applied to the toast (visually and interactively "stuck" mid-swipe), since nothing ever ran the onPointerUp reset logic for that pointer.
Suggested fix
Add an onPointerCancel handler alongside the existing onPointerUp one, running the same reset path (pointerStartRef.current = null; setIsSwiped(false); setSwiping(false); setSwipeDirection(null);, skipping the dismiss-threshold logic since a cancelled gesture shouldn't dismiss the toast).
Source: emilkowalski/sonner