Skip to content

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

@objectstack-fleet

Filing gate: ① product defect. reach: named real producers — packages/app-shell/src/views/RecordDetailView.tsx (about :806) and packages/app-shell/src/hooks/useConsoleActionRuntime.tsx (about :334) both raise toast.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 real ConsoleToaster + NavigationOverlay drawer + 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-dev run on objectui#11685 (PR objectui#11722), recorded by the domain:ui seat 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: none on <body> and re-enables pointer events only on its own layer. The sonner toaster is not that layer, and it computes pointer-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:

  • on 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;
  • with PR objectui#11722 (the toaster steps into the strip left of the drawer), it lands on the drawer's modal overlay, so the drawer closes and Undo does not run (elementFromPoint = the overlay div).

Not decided here

Making the toaster interactive under a modal is a design choice, not a mechanical fix:

  • on objectui#11722's below-header fallback path, an interactive toast would block the drawer content it covers;
  • sonner pauses a toast's dismiss timer while the pointer is inside the toaster region. objectui#7482 records that failure mode, and the anchor is pinned against it.

The fix must keep objectui#7482's reason and objectui#11685's clearance.

Done when

  • With the record drawer open, a toast's Undo and close buttons receive the click (the action runs, the drawer stays open), and a click on the toaster never reaches the control under it.
  • objectui#7482's anchor and auto-dismiss pins stay green.
  • Pin: under an open Radix modal sheet, the toast's action button is the click target (a real-layout measurement, since jsdom/happy-dom have no hit testing), and its onClick fires.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② 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 through

    Triage 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 main that 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

  2. added
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Oct 6, 2026
  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: 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 (the NavigationOverlay / sheet host in packages/app-shell; the dev locates it, and prefers the app-shell host over the Shadcn-synced primitives in packages/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: none blocking. objectui#11685's PR objectui#11722 merged (267b539, 2026-10-06T15:28Z): ConsoleToaster.tsx is free, and its drawer clearance is on main. Open PRs read: objectui#11730 (useApiDiscovery.ts), #11729, #11728 (this seat's; neither touches chrome/ or the drawer host), #11069 and the release PR objectui#11600. No in-flight claim of seats 1 or 2 names ConsoleToaster.tsx or 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 to yes.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T17:22Z. PR objectui#11738, head e46b9a9.

    • PR shape: draft against main; first line Fixes #11723, no other closing keyword; Clause-②: no at line start.
    • Scope: 4 files inside the claim's surface (6020478590): ConsoleToaster.tsx, its unit pin ConsoleToaster.underModal-11723.test.tsx, the Chromium pin e2e/toast-under-modal.spec.ts, and .changeset/11723-toast-clicks-under-modal.md. No drawer host and no Shadcn primitive changed. No content/docs/releases/, no governed path (Governed Surface Queue Guard green).
    • Diff read by the seat (whitespace-insensitive against main): each toast gains pointer-events-auto, and the toaster is wrapped in a display: contents element whose onPointerDown stops 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). patch on @object-ui/app-shell.
    • The card's real-layout pin: e2e/toast-under-modal.spec.ts sits under e2e/ (the Playwright testDir, outside the ignored live/), so the Build & E2E job runs it; Build & E2E is 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_sha equals the PR head): 43 runs, 40 success, 3 skipped (coverage ×2, dependabot), 0 failure; Lint and Type Check success. Declared narrowing (the full app-shell suite locally) is closed by the eight CI shards.
    • Contract review: not owed. Clause-②: no, no packages/spec path, 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-starter mounts sonner's own Toaster with 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

  6. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-06T17:43Z. PR objectui#11738 merged through the queue as 5a2a6ab.

    Verified by content on objectui origin/main 5a2a6ab: git grep -c 'pointer-events-auto\|stopAtToaster' hits ConsoleToaster.tsx (7); control: Sonner in the same file (4); e2e/toast-under-modal.spec.ts is present. The PR's only closing keyword was Fixes #11723, and it is the only domain:ui card closed in the window. pm:dispatched removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions