Repository navigation
fix(app-shell): toast actions are clickable under an open Radix modal, and the click stays the toast's (objectui#11723) - #11738
Merged
objectstack-fleet[bot] merged 4 commits intoOct 6, 2026
Conversation
…, and the click stays the toast's (objectui#11723) While the record drawer (or any Radix modal) was open, a toast's Undo and close buttons were dead and the click went through to whatever lay under the toast. Two mechanisms, two answers, both in ConsoleToaster: - Radix sets pointer-events: none on body; the toaster inherited it. Each toast now carries pointer-events-auto (the toast, not the toaster, so a pointer beside a toast does not pause objectui#7482's dismiss timer). - A toast that takes the click is still outside the modal, so Radix's document-level outside-pointerdown closed the drawer. A toast's pointerdown now stops at the toaster. e2e/toast-under-modal.spec.ts pins it in Chromium with the real toaster, the real NavigationOverlay drawer and real Tailwind. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…t an outside click (objectui#11723) Dispatched-event pin in the unit lane, against the real NavigationOverlay drawer: Undo runs and the drawer stays open, close dismisses only the toast, a press on the toast body reaches no outside-click handler, with a control showing the drawer's outside-click listener is armed. The hit-test half is pinned in Chromium by e2e/toast-under-modal.spec.ts. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
… clearance path back, and give the objectui#7482 leg its control (objectui#11723) The timer leg was green on the base tree too, so it could not show that sonner's pause is observable in the harness at all. It now holds a toast past its 4s with the pointer on it before showing the timer runs with the pointer beside it. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…l (objectui#11723) Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
objectstack-fleet
Bot
deleted the
claude/issue-11723-toast-clicks-under-modal
branch
October 6, 2026 17:41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #11723
Clause-②: no
What changed
With the record drawer (or any Radix modal) open, a toast's Undo and close buttons now take the click, the drawer stays open, and a click on a toast reaches nothing under it. Both halves live in
packages/app-shell/src/chrome/ConsoleToaster.tsx; no drawer host and no Shadcn primitive changed. Read the source diff withgit diff -w: wrappingSonnerin one element re-indents its 60 lines.The defect had two mechanisms. The dispatch's hypothesis that the drawer needs both was measured, not assumed (table below):
pointer-events: noneon thebodyelement and re-enables only its own layer; the toaster inheritednone. Each toast now carriespointer-events-auto(intoastOptions.classNames.toast). The toast, not the toaster: the toaster's own box is 356 by 0 px (measured), so a pointer beside a toast is not inside it and the objectui#7482 dismiss timer keeps running. A plain utility on purpose: sonner'spointer-events: noneon a toast hidden beyondvisibleToastsis unlayered CSS and outranks a Tailwind v4 utility, while agroup-[...]form would outrank sonner and make hidden toasts clickable.pointerdownondocument; a toast is outside the modal's content, so the click that ran Undo also closed the drawer. A toast'spointerdownnow stops at the toaster:ConsoleToasterwrapsSonnerin adisplay: contentselement whose ReactonPointerDowncallsstopPropagation(). React delegates at its root, so this runs after sonner's own handlers (swipe, the pause while a toast is pressed) and before the event reachesdocument. One place covers every Radix layer: the record drawer, centred dialogs, the other sheets, popovers.Routes not taken.
DismissableLayerBranch. A branch registers in the module state of@radix-ui/react-dismissable-layer, which@radix-ui/react-dialog1.1.23 pins exactly (1.1.19). App-shell importing its own copy (a new dependency) works only while both resolve to one module instance; in a consumer install where they differ, the branch silently does nothing.onInteractOutside(usePopperAwareInteractOutside, used byNavigationOverlayandMobileDialogContent). It covers only the hosts that use that hook, and every new modal host would have to remember the toaster.Real-layout pin and measurement (Chromium)
The card's pin is
e2e/toast-under-modal.spec.ts, underplaywright.config.ts, which theBuild & E2Ejob runs aspnpm test:e2e --project=chromium. It bundles the realConsoleToaster,ThemeProviderandNavigationOverlay(ShadcnSheetandDialogon@radix-ui/react-dialog) from source with Vite, compiles real Tailwind for their class strings, serves both from a routed origin, and clicks with the real pointer. Fixtures, as the file header lists them:@object-ui/i18nand@object-ui/reactare stubs; the overlay content is aRecordDetailViewstand-in (title, Approve, Reject, filler, every control logging its clicks); theme colours are not compiled; reduced motion is on.Four configurations, each reading its objectui#11685 path back off the toaster: 1440 sm drawer (beside), 1024 md drawer (below header), 390 phone drawer (below header), 1440 centred dialog (corner). In each: Undo is the hit-test target, runs, and the modal stays open; the close button dismisses only the toast; a click on the toast body logs nothing and the modal stays open. Plus the objectui#7482 pair on the below-header path: with the pointer on the toast it is still up past its 4 s, and with the pointer 8 px beside it, it goes.
Local runs used
/opt/pw-browsers/chromium(Chromium 141) through a scratch config with nowebServer: the spec starts nothing and reads no console build.ade5562, as at834c559) swapped in, spec as at HEADpast its 4s while the pointer is on it: false, because under a modal the base toast cannot even be hovered)pointer-events-autohalf (first spec version, 3 drawer configurations, 10 tests)undo:Approvedthendrawer:close; close and body clicks logdrawer:closee46b9a9Region readings (one-off, two toasts raised):
olat x 196, y 24, 356 wide, 0 high; toasts at y 24 to 82 and 96 to 154. 5 px and 14 px above the front toast hit the toast (sonner's 15 px gap bridge, an::after); 20 px above, and 5 px left, right or below, hit the overlay.olat x 644, y 69, 356 by 0; the same pattern, and the points beside hit the drawer.Tests
packages/app-shell/src/chrome/ConsoleToaster.underModal-11723.test.tsx(unit lane, happy-dom, the realNavigationOverlaydrawer): a control (a press outside both closes the drawer, so Radix's listener is armed), Undo runs and the drawer stays open, the close button dismisses only the toast, a body press reaches no outside-click handler, and each toast carriespointer-events-auto(a class echo only; happy-dom has no hit testing, and the file says so).ConsoleToaster.autoDismiss-7482,consoleToasterAnchor.ratchet-7482) and the objectui#11685 pin (ConsoleToaster.drawerClearance-11685) are unchanged and green.Ablations, each from a committed tree, mutated through
ablation-replace.mjs(anchor must hit, 1 to 0), restored fromHEAD, blob verified (d76d47836c74equals HEAD,git diff HEADempty):onPointerDown={stopAtToaster}"drawer:close"; the objectui#7482 leg greenpointer-events-autofrom the toast classThe objectui#7482 leg in that version was green on the base tree and in both ablations, so it could not show the pause was observable at all. It now has a control half (pointer on the toast holds it past 4 s), which is red on the base tree.
Gates (local; the union ran on
e46b9a9)pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' buildVERDICT command-exit 0, scope 29 of 47 projects (app-shell's dependencies, which this branch does not touch; ran one commit earlier, before the changeset)pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/chrome/plus the 10 files named belowTest Files 20 passed (20),Tests 114 passed (114)pnpm exec playwright testone2e/toast-under-modal.spec.ts(chromium)13 passedpnpm --filter @object-ui/app-shell type-checkVERDICT command-exit 0;tsc -p tsconfig.test.json --listFilesreads 5042 files, the new test among thempnpm type-check:e2etsc -p tsconfig.e2e.json, no diagnosticspnpm exec vitest runon the 8 script tests that reade2e/(e2e-type-check,ci-cd-pipeline-doc,check-test-path-roots, and 5 more)Test Files 8 passed (8),Tests 214 passed (214)pnpm check:control-bytesOK (scanned 7680 tracked text file(s); skipped 85 binary)pnpm check:test-path-rootscheck-test-path-roots: OKpnpm check:changeset-claimsNo pending changeset names a file this change touches.pnpm check:pending-changeset-literalsNo test source names a pending changeset.pnpm check:vi-mock-specifierscheck-vi-mock-specifiers: OKpnpm check:new-line-citations0 new citation(s)pnpm check:unreferenced-sourcesEvery shipped source file in every covered package is reachable.pnpm check:phantom-depsEvery in-scope import is declared by the package that publishes it.pnpm check:esm-specifiersno un-ledgered package emits an extensionless relative specifiernode scripts/check-changeset-presence.mjs,-no-major,-fixed,-overwriteDeclared narrowing. The full
pnpm --filter @object-ui/app-shell testholds the shared verify lock for about 30 minutes, so it is NOT MEASURED locally; CI runs it. The narrowed run covered the wholechrome/directory; every test file outside it that namesConsoleToaster(the four consoleApptests, which mock it, andWizardForm.outcomeToastSupersede); and the drawer host's own suites inpackages/components(navigation-overlay-9299,navigation-overlay-chrome-i18n,navigation-overlay-close-i18n,mobile-dialog-content,chrome-i18n-no-provider-fallback).ConsoleToaster's export and props type are byte-unchanged and it has no import-time side effect, so only a test that renders the real component can observe the change.The repo-wide
pnpm lintbelongs to CI. eslint on the three touched source files,--no-inline-config --format json: 3 files, 0 errors, 0 warnings.eslint.config.jssets noparserOptions.project, so the lint is not type-aware and this diff cannot move an untouched file's verdict.Acceptance notes
::after) above each expanded toast. Withexpandon, the front toast's bridge sits above it and, under a modal, now takes clicks too: at 1024 md the strip from y 54 to 69 (the drawer header ends at 45). That is the same as without a modal; left as sonner draws it.examples/console-startermounts sonner's ownToaster(atbottom-right), which has the same click-through under any modal. A private example, not measured here; noted, not filed.content/docs/guide/notifications.mdnamesConsoleToasteronly as where toasts go), so none changed..changeset/11723-toast-clicks-under-modal.md,@object-ui/app-shellpatch. No export, prop, type member or locale key added.Session:
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8Generated by Claude Code