mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Error toasts now stay on screen until the user closes them, to meet WCAG 2.2.1 Timing Adjustable. Success, info, and warning toasts keep Sonner's default timer. Fixes [DEQ-463](https://linear.app/codercom/issue/DEQ-463/timed-toast-without-user-control) (Open Cursor Desktop failure) and [DEQ-310](https://linear.app/codercom/issue/DEQ-310/timed-toast-without-user-control) (adding a user who is already in a group). Sonner has no default duration per toast type, so this adds `site/src/components/Toaster/toast.ts`, a thin wrapper around Sonner's `toast`: - `toast.error` defaults to `duration: Infinity`. A caller can still set its own `duration`. - `toast.promise` failures get the same default. Sonner renders a rejected promise without calling `toast.error`, and the group member flow (DEQ-310) uses `toast.promise`. - All other methods pass straight through. All `toast` imports now go through the wrapper. A Biome `noRestrictedImports` rule blocks importing `toast` directly from `sonner`, following the existing `@pierre/diffs` rule. Tests that mocked `sonner` now mock the wrapper. Most of the 130-file diff is the import swap. ### Proof Error toast on the create token page (duplicate name), with the pointer off the toast so Sonner doesn't pause the timer. Shots are taken once the enter animation has finished: | | 1 second after the error | 11 seconds after the error | |---|---|---| | Before |  |  | | After |  |  | Also checked in the running app: the close button dismisses the error toast, and a "Token created" success toast still disappears on its own. `toast.test.tsx` covers the error and rejected `toast.promise` cases staying visible, dismissing them with the close button, and success toasts still timing out. With the persistent duration removed, the two error cases fail. <details> <summary>Decision log</summary> - A first version set `duration: Infinity` on the shared `<Toaster>` for every toast type. Product feedback: only error toasts were reported, so success and info toasts should keep their timer. - The wrapper sets the duration when each toast is created. The alternative was to watch toasts from `<Toaster>` with `useSonner` and change them afterward. That would add subscription and timer-ordering issues. - I didn't change Sonner's exported `toast` object in place. The wrapper plus a lint rule is explicit and enforced. - The experimental chat board undo toast (`runPlan.ts`) uses `toast()` with a timer that matches its undo window, so it isn't affected. - Not reproduced end to end: the Groups flow needs a Premium license, and the Cursor flow needs the app installed. Both use the `toast.promise` and `toast.error` paths the tests cover. </details> --- _Generated by Coder Agents on behalf of @chrifro._