A toast button that could never be clicked
Adding an 'Add a note' action to a toast revealed that no toast button in the app had ever worked: the swipe-to-dismiss handler captured the pointer and stole every click.
When habit notes shipped, ticking a habit started showing a toast, Journal: done, with an Add a note button. In the browser test, the button did nothing. Not an error. Nothing.
Suspects
The first guess was that the habit row had re-rendered and the callback was
stale. It hadn’t. The second was that the test had clicked an older, stacked
toast. Partly true, and irrelevant: clicking the right one did nothing either.
The tell was that the toast didn’t dismiss, and the button calls dismiss
after the action.
The cause
Toasts can be swiped away. The swipe handler did this on pointerdown:
ref.current?.setPointerCapture(e.pointerId);
Pointer capture redirects every later event for that pointer, including the
pointerup that makes a click, to the capturing element. The click fired on
the toast container, never on the button inside it. Every toast button in the
app had been dead since swipe-to-dismiss was added, including the X to close
one. Nobody noticed because most toasts have no button and all of them time
out on their own.
The fix
Don’t start a drag from a button:
onPointerDown={(e) => {
if ((e.target as HTMLElement).closest("button")) return;
drag.current = { x: e.clientX, t: performance.now() };
ref.current?.setPointerCapture(e.pointerId);
}}
The lesson
Pointer capture is the right tool for drags and the wrong default for a container that holds controls. And a feature with no test is a feature nobody knows is broken: the bug surfaced only because a new feature used a toast button in an end-to-end run.
AlzyWelzy
Building PaceStreak. GitHub
Related
PostmortemsFavourite disciplines belong on the account, not the device
Picked once at onboarding, read from localStorage ever since - and gone the moment someone switched phones or cleared their browser. It was account state that had been living like a device preference.
2 min
DesignKeyboard first, and thumb first
A command palette, G-then-a-letter shortcuts, swipe and long-press on habits, pull to refresh, and why each one is a shortcut on top of a button rather than the only way in.
2 min
InfrastructureA 308 that sent every link home
Reloading any page in the app, or opening a shared link, landed on Today. The SPA rewrite rules pointed at /index.html, and Cloudflare Pages redirects that to /. It had been true for every deployment.
1 min