Skip to content

fix: reset interacting state on pointercancel - #775

Open
ngtpro wants to merge 1 commit into
emilkowalski:mainfrom
ngtpro:fix/reset-interacting-on-pointercancel
Open

fix: reset interacting state on pointercancel#775
ngtpro wants to merge 1 commit into
emilkowalski:mainfrom
ngtpro:fix/reset-interacting-on-pointercancel

Conversation

@ngtpro

@ngtpro ngtpro commented Jul 27, 2026

Copy link
Copy Markdown

Problem

The <ol> in Toaster sets interacting on pointer down and only clears it on pointer up:

onPointerDown={(event) => {
  const isNotDismissible =
    event.target instanceof HTMLElement && event.target.dataset.dismissible === 'false';

  if (isNotDismissible) return;
  setInteracting(true);
}}
onPointerUp={() => setInteracting(false)}

There is no onPointerCancel. When the browser takes over a gesture it fires pointercancel instead of pointerup — the pointerup event never arrives — so interacting stays true.

interacting gates the auto-close timer:

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

…and it lives on the Toaster, not on an individual toast. So one cancelled gesture stops every toast on the page from auto-dismissing for the rest of the page's lifetime — not just the one that was touched. To the user, toasts simply pile up and never go away.

Common pointercancel triggers on mobile:

  • touch-scrolling starts and the browser claims the gesture
  • the touch becomes a browser-level gesture (pull-to-refresh, back-swipe, long-press / text selection)
  • the pointer is interrupted (app switch, incoming call)

We hit this in production on mobile and had to work around it by force-dismissing toasts from the app side.

Fix

Reset the same state on cancel:

onPointerCancel={() => setInteracting(false)}

Test

Adds toast is removed when the pointer gesture is cancelled to test/tests/basic.spec.ts, which dispatches pointerdown followed by pointercancel and asserts the toast still auto-dismisses.

Verified locally against chromium:

  • without the fix: fails — expect(toast).toHaveCount(0) gets 1, the toast never dismisses
  • with the fix: passes

Full suite: 36 passed, 1 failed. The one failure is cancel button dismisses the custom toast with empty id, which fails on the base commit too and is unrelated (it looks like the same thing #724 is addressing).

Notes

  • onPointerLeave may be worth considering separately, for the case where the pointer leaves the element while still down. I left it out to keep this focused.

The browser fires pointercancel instead of pointerup when it takes over
the gesture (touch scrolling, pull-to-refresh, long-press selection, app
switch). Since interacting was only cleared in onPointerUp, it stayed
true forever in that case.

interacting gates the auto-close timer and lives on the Toaster rather
than an individual toast, so a single cancelled gesture stopped every
toast on the page from auto-dismissing for the rest of its lifetime.

Adds an onPointerCancel handler and a regression test.
@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sonner Error Error Jul 27, 2026 5:17pm

Request Review

@ngtpro
ngtpro marked this pull request as ready for review July 27, 2026 17:18
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.

1 participant