Skip to content

fix(frontend): pause the timer of a toast that a bottom sheet hides - #131

Merged
pdevito3 merged 2 commits into
mainfrom
fix/issue-128-hidden-toast-timers
Oct 25, 2026
Merged

pdevito3 merged 2 commits into
mainfrom
fix/issue-128-hidden-toast-timers

Conversation

@pdevito3

@pdevito3 pdevito3 commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Why

On a phone, while a bottom sheet is open, index.css shows only the newest toast and hides the others visually (#126). React Aria starts the timer of a toast when the toast mounts, so the hidden toasts closed while the user could not see them. An error toast behind a newer success toast was lost.

Closes #128

What changed

  • toastQueue is now a ShownToastQueue. It pauses the timer of a hidden toast, and gives the toast its full time again when it shows. Its resumeAll does not restart a hidden toast when the pointer or the focus leaves the toast region. It pauses a timer before it starts it again, because React Aria starts a second timeout for a timer that runs.
  • Toaster puts a ResizeObserver on each toast. A toast that index.css hides is 1 px by 1 px, so the observer tells the queue that it is hidden. index.css stays the one place that decides which toasts show.

Scope

  • Each toast shows in turn while the sheet is open, each for its full time. When the sheet closes, all toasts that are left show, as before.
  • No "+N more" line. The user sees each waiting toast when the toast before it closes, and the line would make the toast area over the sheet taller again.
  • Desktop and narrow screens without a bottom sheet keep their timing, with one exception. On main, a toast added while the pointer was on the toasts closed at 5 s under a second hover, because React Aria started a second timer. Now it waits for the hover to end. No toast mounts again, so a screen reader announces each toast once.

Tradeoffs

  • A "+N more" line alone does not stop an error from timing out unseen.
  • To show the next toast only when the visible one leaves, the region would render one toast at a time. Each older toast would mount again and a screen reader would announce it a second time.
  • A JavaScript copy of the CSS condition (screen width and an open bottom sheet) would put the same rule in two places.

Blast Radius

Every toast goes through this queue. On a desktop, the observer always reports a shown toast, and markShown does nothing for a toast that was never hidden. The queue uses the public pause, resume, and reset methods of the React Aria timer, and package.json already pins react-aria-components to ~1.22.1 for the UNSTABLE_ toast API.

Verification

  • pnpm test:e2e: three new scenarios in tests/features/toasts/toasts.spec.ts stop the clock at 667x375 and move it by hand. They fail on main and pass with the fix. 36 passed. The toast specs passed 180 of 180 with --repeat-each=10 and --repeat-each=20 on 4 workers.
  • pnpm test: a new test in toaster.test.tsx adds a toast while the pointer is on the toasts, then hides it with a newer toast. It fails on main and passed 10 of 10 with the fix. 81 passed.
  • Real time, sheet open, Success then Error then Info, at 667x375 and 390x844. Before: Success was hidden and closed at 5.2 s unseen, and Error showed for 0.95 s. After: Info shows 0.3-5.2 s, Error 5.2-11.2 s, and Success 11.2-16.2 s.
  • Screenshots in light and dark at 667x375, 390x844, and 1280x800. Desktop toasts have the same pixels before and after, except for run-to-run noise in the theme toggle. pnpm lint and pnpm build pass.

Sheet open, Success then Error then Info, 5 s later. Before: no toast shows. After: the error shows.

Before After

On a phone on its side, a bottom sheet hides each toast but the newest,
and the hidden toasts keep their timers. The three functional scenarios
stop the clock before the page opens, move it by hand, and record which
toasts show as time passes. So the real time that a slow browser takes
does not count. All three fail. The success toast never shows, and the
error toast closes while it waits, also after the focus leaves the
toasts.

A component test adds a toast while the pointer is on the toasts, and
then a newer toast hides it. The hidden toast must not close at its
first deadline.
While a bottom sheet is open on a phone, index.css shows only the newest
toast and hides the others visually. React Aria started the timer of
each toast when it mounted, so a hidden toast closed before the user
saw it. An error toast behind a newer success toast was lost.

The toast queue now pauses the timer of a hidden toast. When the toast
shows again, it gets its full time. A ResizeObserver on each toast tells
the queue when the toast is hidden, so index.css stays the one place
that decides which toasts show. A pointer or focus that leaves the toast
region resumes only the toasts that show.

React Aria starts a second timeout when it resumes or resets a timer
that runs, and a pause stops only the newest one. So the queue pauses
a timer before it starts it again, and it does not resume a toast that
has not rendered yet, because the toast starts its timer when it mounts.
@pdevito3

Copy link
Copy Markdown
Owner Author

Open findings from the review gate. Both stay as they are, for the reasons below.

L1 (low): the pause flag can stay set when the region empties while it has focus. The queue copies React Aria's own behavior here. ToastRegion stays mounted when it is empty, so its focus-within listener clears the flag on the next focus change. Opening a bottom sheet moves the focus, so the flag is clear before a toast can be hidden.

Not re-reviewed: the last change to resumeAll. The final review found that React Aria starts a second timeout when it resumes a timer that already runs, and that a pause stops only the newest one. A toast that came while the pointer was on the toasts could then close at its first deadline while it was hidden. While I wrote the test for it, I found a second path to the same fault: resumeAll started the timer of a toast that had not rendered yet, and React Aria then started it again on mount. The queue now pauses a timer before it starts it again, and it skips a toast that has not rendered yet. The new test in toaster.test.tsx fails on main and failed 6 of 6 with the earlier queue. It passed 10 of 10 with this one. The review playbook allows only two review passes, so no independent reviewer has read this change.

@pdevito3
pdevito3 merged commit cca7349 into main Oct 25, 2026
16 checks passed
@pdevito3
pdevito3 deleted the fix/issue-128-hidden-toast-timers branch October 25, 2026 15:22
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.

Toasts that a bottom sheet hides can time out before the user sees them

1 participant