Repository navigation
console: toast actions (Undo, close) cannot be clicked while a drawer or dialog is open — the click goes through the toast to whatever is under it #11723
Description
Activity
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsPath: ② the capabilities an end user meets in the app — record actions and undo | 缺项 (no item asserts a toast's actions are clickable while the drawer that raised them is open) | P2
Triage: first grade —
bug·priority:p2·domain:ui·area:records·pm:queue. The toaster takes its own clicks under a modal, and never passes one throughTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-06T15:52Z. ⛔ Not a claim, ⛔ not a dispatch.Triage: lands in
packages/app-shell/src/chrome/ConsoleToaster.tsx⇒domain:ui; rationale: the toaster is not part of the modal's interactive layer, so its buttons are dead and the click falls through.- Serial: PR objectui#11722 (objectui#11685) merged at 2026-10-06T15:44Z, so the file is free and its clearance is on
main. - Why p2, not p3: this is more than a dead Undo. The click lands on whatever is under the toast. On
mainthat measured as the drawer's close or expand control, or a record-header action under a second toast. A user who means "Undo" can trigger a different action. - Direction:
- the toaster's toasts receive pointer events while a Radix modal is open;
- a click on the toaster region never reaches the control under it;
- an interaction with a toast is not treated as an outside click that closes the drawer.
- The design choice is the lane's, bounded as the card says: keep objectui#7482's reason (the dismiss timer must not stall while the pointer is merely near the region) and objectui#11685's clearance.
- ⛔ No toast is moved back over the drawer chrome.
- Pins: with the record drawer open:
- Undo runs, and the drawer stays open;
- close dismisses the toast;
- a click on the toaster area triggers nothing underneath;
- objectui#7482's pin stays green.
Generated by Claude Code
- Serial: PR objectui#11722 (objectui#11685) merged at 2026-10-06T15:44Z, so the file is free and its clearance is on
- addedarea:recordsBusiness objects, records, the views that show data, usable forms, searchBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Oct 6, 2026 objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsClaim: PM loop round 1
Session:session_01CGZy1BGCjdN5cXqL9cnvB8
Account:os-support-ai
Branch:claude/issue-11723-toast-clicks-under-modal
Worktree:objectui-issue-11723
Domain:domain:ui
Seat:domain:ui#3
File surface:packages/app-shell/src/chrome/ConsoleToaster.tsx(the toaster's pointer-event layer under an open Radix modal); the record drawer host's outside-interaction handling, only as much as keeping a toast interaction from counting as an outside click needs (theNavigationOverlay/ sheet host inpackages/app-shell; the dev locates it, and prefers the app-shell host over the Shadcn-synced primitives inpackages/components/src/ui/); the tests beside them, including a real-layout Chromium pin; and.changeset/11723-*.md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
Clause-②: no
Thread-read: 6020084674
Serial constraints cleared:noneblocking. objectui#11685's PR objectui#11722 merged (267b539, 2026-10-06T15:28Z):ConsoleToaster.tsxis free, and its drawer clearance is onmain. Open PRs read: objectui#11730 (useApiDiscovery.ts), #11729, #11728 (this seat's; neither toucheschrome/or the drawer host), #11069 and the release PR objectui#11600. No in-flight claim of seats 1 or 2 namesConsoleToaster.tsxor the drawer host. Read 2026-10-06T16:13Z.Why
Clause-②: no: the fix changes where a click lands under an open modal. It widens no accepted input and adds no export. If the dev finds it needs a new export or locale key, the dev reports it before opening the PR, and the seat amends this line toyes.
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 11723,
"status": "done",
"branch": "claude/issue-11723-toast-clicks-under-modal",
"pr": "#11738",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent mode: the parent's id)",
"premise_still_valid": true,
"summary": "Premise held on 834c559: with a Radix modal open, the toaster computed pointer-events none and the hit test at Undo returned the overlay (beside path, centred dialog) or the drawer (below-header path, phone). Hypothesis 4 measured true: with only pointer-events-auto on the toasts, Undo ran but every toast press (Undo, close, body) also closed the drawer. Both halves landed in ConsoleToaster.tsx alone: each toast carries pointer-events-auto (the toast, not the toaster: the toaster ol is 356 by 0 px), and a toast's pointerdown stops at a display:contents wrapper, so Radix's document-level outside-pointerdown never hears it, for every Radix layer at once. No drawer host and no Shadcn primitive changed. Rejected and explained in the code and PR: DismissableLayerBranch (react-dialog pins react-dismissable-layer 1.1.19 exactly; a second copy in a consumer install makes the branch a silent no-op) and per-host onInteractOutside (covers only hosts using that hook). The card's real-layout pin is a new CI-wired Chromium spec e2e/toast-under-modal.spec.ts (the Build & E2E job runs pnpm test:e2e --project=chromium); a unit-lane dispatched-event pin covers the outside-click half.",
"tests": "All at HEAD e46b9a9 unless noted. (1) Chromium pin e2e/toast-under-modal.spec.ts, local scratch config (no webServer, /opt/pw-browsers/chromium = Chromium 141): 13 passed. Before leg, base toaster blob ade5562 swapped in under a trap, restored and verified blob == HEAD: 13 failed, 12 at the hit test (6 overlay, 6 drawer) plus the objectui#7482 control (past its 4s while the pointer is on it: false). Half-fix leg (pointer-events only, earlier 10-test version): 9 failed on drawer:close. (2) Unit pin ConsoleToaster.underModal-11723.test.tsx: 5 passed. (3) Scoped vitest, packages/app-shell/src/chrome/ plus the 4 console App tests, WizardForm.outcomeToastSupersede and 5 components drawer-host suites: Test Files 20 passed (20), Tests 114 passed (114). (4) Ablations through objectstack scripts/ablation-replace.mjs (anchor 1 to 0, restore from HEAD, blob d76d47836c74 == HEAD, git diff HEAD empty), on 0d54ff2 with the 10-test spec: drop onPointerDown={stopAtToaster}: unit 3 failed/2 passed (Undo, close, body red; control and class echo green), e2e 9 failed/1 passed each on drawer:close; drop pointer-events-auto: unit 1 failed/4 passed (class echo only), e2e 9 failed/1 passed at the hit test (3 overlay, 6 drawer). No dist involved: the unit test imports ./ConsoleToaster.js from src and the e2e bundles src. (5) One-off measurements in the PR body: toaster ol height 0; a 15 px gap bridge above the front toast takes clicks; a toast appearing under a parked pointer on the below-header path stays until the pointer moves (still up at 6 s, gone 4.2 s after moving off). (6) Full app-shell suite NOT MEASURED, reason: holds the shared lock about 30 minutes; declared narrowing, CI runs it. CI at report time: 42 check runs, 21 success, 3 skipped, 18 in_progress, 0 failed.",
"mcp_calls": "0",
"api_writes": "3, all through the objectstack fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): pr_create = POST /repos/objectstack-ai/objectui/pulls (draft, #11738); label-write.mjs --assign = POST /repos//issues/11738/assignees (os-support-ai, labels untouched); this os-dev-report = POST /repos//issues/11723/comments. Plus git push (not REST).",
"open_questions": [
{
"question": "On the objectui#11685 below-header path a toast covers the record's first row, so a success toast raised by clicking Approve can land under the parked pointer and then stays (sonner's hover pause on the toast itself) until the pointer moves. Measured at 1024 md: still up at 6 s, gone 4.2 s after the pointer moved 8 px away. Accept, or follow up?",
"options": [
"A: accept. It is sonner's documented hover-to-keep on the toast itself, the same as anywhere without a modal; it ends when the pointer moves, not at the close button; and the card's bound (no stall while the pointer is merely near the region) is pinned.",
"B: a follow-up card to revisit the objectui#11685 fallback placement so a toast does not cover the record's first-row actions (a clearance design change, outside this card).",
"C: suppress sonner's hover pause under a modal (fights sonner's internals: its pause is wired to the toaster's own mouseenter/mousemove, with no option to turn it off)."
],
"recommendation": "A, because the residual is bounded by pointer movement, the triage bound is pinned green, and B or C would reopen a just-landed design (B) or work around the library (C) for a case that has not been reported."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · examples/console-starter/src/App.tsx mounts sonner's own Toaster at bottom-right; by the same mechanism its toasts are click-through under any Radix modal, and it sits on the objectui#7482 corner. Private example, not measured · noted in the PR Acceptance notes, not filed"
],
"gates": [
{ "command": "pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build (on 06ca279 plus the uncommitted changeset; dependencies only)", "exit": 0, "verdict": "VERDICT command-exit 0, Scope: 29 of 47 workspace projects" },
{ "command": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/chrome/ + 10 named files", "exit": 0, "verdict": "Test Files 20 passed (20), Tests 114 passed (114)" },
{ "command": "pnpm exec playwright test (e2e/toast-under-modal.spec.ts, chromium, scratch config)", "exit": 0, "verdict": "13 passed" },
{ "command": "pnpm --filter @object-ui/app-shell type-check", "exit": 0, "verdict": "VERDICT command-exit 0; script echoed tsc --noEmit && tsc -p tsconfig.test.json; --listFiles 5042 files incl. the new test" },
{ "command": "pnpm type-check:e2e", "exit": 0, "verdict": "tsc -p tsconfig.e2e.json, no diagnostics" },
{ "command": "pnpm exec vitest run (8 script tests that read e2e/, incl. e2e-type-check)", "exit": 0, "verdict": "Test Files 8 passed (8), Tests 214 passed (214)" },
{ "command": "pnpm exec eslint --no-inline-config --format json (3 touched source files)", "exit": 0, "verdict": "3 files, 0 errors, 0 warnings; config not type-aware (no parserOptions.project)" },
{ "command": "pnpm check:control-bytes", "exit": 0, "verdict": "OK (scanned 7680 tracked text file(s); skipped 85 binary)" },
{ "command": "pnpm check:test-path-roots", "exit": 0, "verdict": "check-test-path-roots: OK" },
{ "command": "pnpm check:changeset-claims", "exit": 0, "verdict": "No pending changeset names a file this change touches." },
{ "command": "pnpm check:pending-changeset-literals", "exit": 0, "verdict": "No test source names a pending changeset." },
{ "command": "pnpm check:vi-mock-specifiers", "exit": 0, "verdict": "check-vi-mock-specifiers: OK" },
{ "command": "pnpm check:new-line-citations", "exit": 0, "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)" },
{ "command": "pnpm check:unreferenced-sources", "exit": 0, "verdict": "Every shipped source file in every covered package is reachable." },
{ "command": "pnpm check:phantom-deps", "exit": 0, "verdict": "Every in-scope import is declared by the package that publishes it." },
{ "command": "pnpm check:esm-specifiers", "exit": 0, "verdict": "no un-ledgered package emits an extensionless relative specifier" },
{ "command": "node scripts/check-changeset-presence.mjs", "exit": 0, "verdict": "2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)" },
{ "command": "node scripts/check-changeset-no-major.mjs / -fixed / -overwrite", "exit": 0, "verdict": "No changeset declares a major bump / privatePackages declared / No pre-existing changeset was modified or deleted" }
],
"deviations": [
"Local Playwright runs used a scratch config (no webServer, launchOptions.executablePath /opt/pw-browsers/chromium) instead of playwright.config.ts, whose webServer builds and previews the whole console; the spec starts its own bundle and reads no console build. CI runs it under the real config.",
"Full app-shell test suite not run locally (declared narrowing above); CI runs it.",
"The dependency-closure build ran one commit before final HEAD; it builds app-shell's dependencies only, which the branch does not touch.",
"Commit trailers use the model-free pair objectui AGENTS.md prescribes, and the PR body ends with the os-dev session-URL footer, rather than the harness's model-named trailer and footer form.",
"The raw source diff re-indents the 60-line Sonner element under the new wrapper; git diff -w shows the real change (said in the PR body).",
"The first two attempts of the before-run were not readings (a Vite lib-mode entry error, then an opaque-origin localStorage error that timed every test out at 60 s); the second was stopped by its own task id, which released the lock. Both fixed in the harness before any reading was taken."
],
"files_changed": [
"packages/app-shell/src/chrome/ConsoleToaster.tsx",
"packages/app-shell/src/chrome/ConsoleToaster.underModal-11723.test.tsx",
"e2e/toast-under-modal.spec.ts",
".changeset/11723-toast-clicks-under-modal.md"
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T17:22Z. PR objectui#11738, heade46b9a9.- PR shape: draft against
main; first lineFixes #11723, no other closing keyword;Clause-②: noat line start. - Scope: 4 files inside the claim's surface (
6020478590):ConsoleToaster.tsx, its unit pinConsoleToaster.underModal-11723.test.tsx, the Chromium pine2e/toast-under-modal.spec.ts, and.changeset/11723-toast-clicks-under-modal.md. No drawer host and no Shadcn primitive changed. Nocontent/docs/releases/, no governed path (Governed Surface Queue Guard green). - Diff read by the seat (whitespace-insensitive against
main): each toast gainspointer-events-auto, and the toaster is wrapped in adisplay: contentselement whoseonPointerDownstops propagation, so Radix's document-level outside-pointerdown never sees a press on a toast. Two lines of behaviour; everything else is comment and re-indentation. - Changeset sentences checked against the diff and the pins: the Undo / close sentence (Chromium pin, drawer and centred dialog); "a click on a toast no longer reaches what lies under it" (hit-test pins); "Only the toasts take the pointer, not the area around them, so the dismiss timer still runs while the pointer rests beside a toast (objectui#7482)" (the finding(record-page): 「客户更新成功」成功提示常驻不自动消失,且盖住助手侧栏的发送按钮 #7482 control in the Chromium spec, red on the base toaster); "Nothing is added to the package entry" (no export changed).
patchon@object-ui/app-shell. - The card's real-layout pin:
e2e/toast-under-modal.spec.tssits undere2e/(the PlaywrighttestDir, outside the ignoredlive/), so theBuild & E2Ejob runs it;Build & E2Eis success on this head. The dev's before-leg: 13 failed on the base toaster, 13 passed on the fix. - Gates: CI on
e46b9a9(head_shaequals the PR head): 43 runs, 40 success, 3 skipped (coverage ×2, dependabot), 0 failure;LintandType Checksuccess. Declared narrowing (the full app-shell suite locally) is closed by the eight CI shards. - Contract review: not owed.
Clause-②: no, nopackages/specpath, no governed rule text; the changeset prose is checked above. - Open question (a toast under a parked pointer on the below-header path stays until the pointer moves): answered A, accept. It is sonner's hover-to-keep on the toast itself, it ends when the pointer moves, and the triage bound (no stall while the pointer is merely near the region) is pinned green.
Findings:
examples/console-startermounts sonner's ownToasterwith the same click-through under a modal →Acceptance notes(a private example, not measured, no carrier).
Landing: ready + auto-merge through the queue, now.
Generated by Claude Code
- PR shape: draft against
objectstack-fleet commented
on Oct 6, 2026 ContributorAuthorMore actionsLanded —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T17:43Z. PR objectui#11738 merged through the queue as5a2a6ab.Verified by content on objectui
origin/main5a2a6ab:git grep -c 'pointer-events-auto\|stopAtToaster'hitsConsoleToaster.tsx(7); control:Sonnerin the same file (4);e2e/toast-under-modal.spec.tsis present. The PR's only closing keyword wasFixes #11723, and it is the onlydomain:uicard closed in the window.pm:dispatchedremoved.
Generated by Claude Code
- added a commit that references this issue
on Oct 7, 2026
Filing gate: ① product defect.
reach:named real producers —packages/app-shell/src/views/RecordDetailView.tsx(about:806) andpackages/app-shell/src/hooks/useConsoleActionRuntime.tsx(about:334) both raisetoast.success(…, { action: { label: options.undo.label || 'Undo', … } }), and those toasts are raised while the record drawer (a Radix modal sheet) is open. Measured once in Chromium against the realConsoleToaster+NavigationOverlaydrawer +RecordDetailView(a harness page, not the full app with a backend).Reader: objectui triage first-touch → the lane that owns
packages/app-shell/src/chrome/ConsoleToaster.tsx; one PR.Dedup (semantic search, objectui, open + closed): "toast Undo button cannot be clicked while a drawer or dialog is open, toaster pointer-events none under modal" → 1 hit (objectui#2156, closed, a dropdown-in-dialog close, not this); "sonner toast action not clickable modal overlay pointer-events" → 2 hits (objectui#7316, #994, neither this); "toast undo record drawer" → 4 hits (objectui#11685 — the toast's position over the drawer chrome, see below — and #11080 / #11081 / #11082, Undo wording and snapshot, not clickability). No duplicate.
Found by the
os-devrun on objectui#11685 (PR objectui#11722), recorded by thedomain:uiseat 1 PM ·session_01DrKzdPdyLLBW3qpZ4vtk7z. ⛔ Not a claim, ⛔ not a dispatch.What happens
While any Radix modal (the record drawer, a dialog) is open, Radix sets
pointer-events: noneon<body>and re-enables pointer events only on its own layer. The sonner toaster is not that layer, and it computespointer-events: none(measured in all 16 viewport × drawer-width configurations). So a toast's close button and its action buttons — the Undo offered after a record action — cannot be clicked while the drawer that raised them is still open.The click goes through to whatever is under the toast:
main(7300fca), where the toast sits over the drawer's top-right corner, it lands on the drawer header's expand / close buttons, or on the record header's Approve action under a second toast;elementFromPoint= the overlaydiv).Not decided here
Making the toaster interactive under a modal is a design choice, not a mechanical fix:
The fix must keep objectui#7482's reason and objectui#11685's clearance.
Done when
onClickfires.Generated by Claude Code