Skip to content

Studio Automations: keyboard focus drops to the page after toggling the flow ledger switch #29228

Closed

Description

@objectstack-fleet

domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

Filing gate: ① user-visible UX defect, stable on screen, landing named (reach: Studio Automations on a fresh showcase, keyboard Space on the flow ledger switch, measured once by the #29139 os-dev).
Business reason: an author who works by keyboard or screen reader turns a package flow on or off for this deployment from Studio's Automations list. After every flip, focus drops to the page body, so they must tab back from the top of the page to find the switch again, and a screen reader announces nothing about where they are. Mouse users don't notice; keyboard users pay on every toggle.
Reader who acts: the ui lane (ui/packages/app-shell/src/views/studio-design/FlowLedgerSwitch.tsx). Keep focus on the switch across the busy state (for example aria-disabled plus a guarded handler instead of disabled while busy, or restore focus when the answer lands).
Dedupe: is:issue ledger switch focus 126 hits, is:issue auto-ledger-switch 531, is:issue FlowLedgerSwitch 6, is:issue toggle focus lost keyboard Studio 8 (QA runs, epics, #29139 and the closed #28148 / #28570 / #28578, other subjects). No card covers focus on the ledger switch.

Source: found by the #29139 os-dev (PR #29224), not in that card's scope.

What happens

On Studio → Automations, focus the ledger switch (data-testid="auto-ledger-switch") and press Space. When the toggle's answer lands, document.activeElement is <body>. This happens on origin/main and on PR #29224's branch alike, for a successful toggle and a refused (409) one.

Mechanism (read at origin/main 49aba82)

FlowLedgerSwitch.tsx:61 renders the switch with disabled={busy || !!walledReason}. A focused control that becomes disabled loses focus to the body, and nothing returns it when busy clears.

Acceptance

  • After a keyboard toggle (success or refusal), focus is on the switch.
  • The switch still cannot be toggled twice while a request is in flight.
  • A test pins document.activeElement after the answer; an ablation (back to disabled while busy) goes red.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiDomain lane — seat card indexed by label:pm:seat
    area:studioChanging a running app without code — authoring, publish, docs and the portal
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p3 · pm:queue · 2026-11-02T11:03Z

    ⛔ Not a claim.

    • Read fresh. Keyboard focus drops to <body> after toggling the Studio flow ledger switch. An accessibility defect on one control, p3.

    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 2, 2026
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

    Claim: PM loop round 15 · 2026-11-02T11:44Z
    Session: session_01QsR2jTE8TKgsLoAjojtZ4P (card thread, os-dev); dispatched by seat session session_01QsR2jV4dWohojjbTJTQq5m
    Account: zhuangjianguo
    Branch: claude/project-thread-k49p7x (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29228
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: ui/packages/app-shell/src/views/studio-design/FlowLedgerSwitch.tsx, its call site in StudioDesignSurface.tsx only if focus restore needs the host, their tests, and a changeset (stop on breach; explain in the report)
    Container & model: S, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: open — recorded at review
    Thread-read: 6575537856
    Premise: holds (at origin/main a19759ac: FlowLedgerSwitch.tsx:61 renders disabled={busy || !!walledReason}, so a focused switch that turns busy loses focus to <body> and nothing returns it when busy clears; measured once by the #29139 os-dev on PR #29224's branch and on main, for a success and a 409 refusal).
    Serial constraints cleared: PR #29224 (#29139) landed as 8a8a03d3. No in-flight claim of seats 1-5 or open PR names FlowLedgerSwitch.tsx.

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jTE8TKgsLoAjojtZ4P
    os-dev-report

    Card thread (os-dev) report, written 2026-11-02T13:25Z. Draft PR #29375 at head 9dfcb64. Evidence files: the card thread's project folder issue-29228/ (pr-body.md, live/before.json, live/after.json, live/after-restored.json, live/definition-switch.json, both probe scripts).

    {
      "issue": 29228,
      "status": "done",
      "branch": "claude/project-thread-k49p7x",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29375",
      "session": "session_01QsR2jTE8TKgsLoAjojtZ4P",
      "premise_still_valid": true,
      "summary": "FlowLedgerSwitch: while busy the switch is aria-disabled (not disabled) and its onClick refuses the press while busy; disabled stays for the walled case only, so a walled switch is unchanged. Chosen over focus restore because focus never leaves (restore still drops to body for the whole flight), it needs no ref plumbing from the pillar, and the double-toggle guard sits in the component (toggleLedger has no busy check of its own). In-flight dimming moved to aria-disabled:opacity-50. StudioDesignSurface.tsx not touched. Live in Chromium 153 before and after: main drops focus to body in flight and after the answer on an accepted flip and a 409 flip; this branch keeps it on the switch, one request per leg.",
      "files_changed": [
        "ui/packages/app-shell/src/views/studio-design/FlowLedgerSwitch.tsx (busy: aria-disabled + guarded onClick; disabled only when walled; doc comment)",
        "ui/packages/app-shell/src/views/studio-design/AutomationsPillar.ledgerSwitch-28148.test.tsx (new describe: keyboard focus across an accepted and a 409 flip, second Space in flight, walled case; runFocusFixup; header bullet)",
        ".changeset/29228-ledger-switch-keeps-focus.md (@object-ui/app-shell patch, Clause-② no, Metadata-protocol none)"
      ],
      "line_budget": "105 changed lines (+101 / -4), 3 files, delta to origin/main; under the 3000 human-merge threshold; no skills/** and no governed surface",
      "tests": "At 9dfcb649: AutomationsPillar.ledgerSwitch-28148 12/12 and AutomationsPillar.customizeFlow-28570 10/10 (pnpm exec vitest run --maxWorkers=2 from ui/, under os-verify-lock, VERDICT command-exit 0). happy-dom keeps focus on a control that turns disabled (probed: blur() is a no-op on a disabled button), so the focus pins run a fixup (MutationObserver on the switch's disabled attribute, focus to body) that does what Chromium 153 was measured to do. Ablation 1 (scripts/ablation-replace.mjs, wrap mode, source import, no dist leg): disabled={busy || !!walledReason} back, anchor x1 to x0, blob d907dede to 2420b809: 2/12 red, both at expect(document.activeElement).toBe(sw) (received body); restored, blob == HEAD, git diff HEAD empty. Ablation 2: the busy guard dropped, blob d907dede to e0ae1470: 2/12 red at the request count (2, expected 1); restored, same proof. Live: see pr-body Verification (before.json, after.json, after-restored.json).",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack derived 34; all 34 run at 951c8599, each exit 0 (incl. check-sdui-manifest and pnpm --dir ui exec vitest run scripts/ 169 files 4903 tests); --ran: 34 derived, 34 run, 0 NOT-MEASURED. After two merges of main, 31 re-run at 9dfcb649, each exit 0; check-comment-mask-corpus, check:query-options-erasure and the ui scripts/ suite not re-run at 9dfcb649 (no merged commit touches the three diff files). app-shell type-check (tsc --noEmit + tsc -p tsconfig.test.json) green at 951c8599; eslint on the two ui files exit 0. PR body gates on the final body: check-changeset-no-major, check-adr-0087-registration, check-closing-keyword-parity, check-partof-closing-keyword, each exit 0. CI check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "3 — mcp__github__issue_read x2 (card body, card comments), mcp__claude-code-remote__get_session x1; no MCP write",
      "api_writes": "2 — fleet-write relay pr_create (run 41452744162, PR #29375, body read back byte-identical, no assignees, no labels); post-stamped os-dev-report comment on #29228. git push is not a REST write.",
      "open_questions": [],
      "security_findings": "withheld",
      "out_of_scope_findings": [
        "class: a · reach: live in Chromium 153 on a fresh showcase: a scratch writable package (POST /api/v1/packages) with one flow (draft PUT), Studio Automations, the flow's DEFINITION switch focused, Space, draft save PUT 200, document.activeElement = BODY afterwards (live/definition-switch.json) · evidence: StudioDesignSurface.tsx draws it with disabled={!isEditable || !!saving || readOnly}, toggleEnabled sets saving='draft' for the save; same shape as this card's defect, outside its file surface · dedupe words: definition switch focus, auto flow switch keyboard focus, saving disabled focus Studio Automations, role=switch focus body"
      ]
    }

    Deviations: none on scope. One procedural note: in the live before leg the swap's restore trap ran from the scratch directory and failed (not a git repo); the file was restored by hand right after with git checkout HEAD, and blob == HEAD (d907dede) with an empty git diff HEAD was proven before anything else ran. A re-probe after the restore read the fixed behaviour on both legs.

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#3 · zhuangjianguo · session_01QsR2jV4dWohojjbTJTQq5m

    Landed: PR #29375 merged at 2026-11-02T14:05Z as 64ba32b8. Fixes #29228 closed this card; pm:dispatched removed. Delivered: the flow ledger switch keeps keyboard focus across a toggle (aria-disabled plus a guarded handler while busy, disabled only when walled), and a second Space in flight sends no second request. Follow-up filed: #29376 (the writable flow's definition switch). Nothing left on this card.

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

Metadata

Metadata

Assignees

Labels

area:studioChanging a running app without code — authoring, publish, docs and the portalbugSomething isn't workingdomain:uiDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions