Repository navigation
Commit e8b10a6
fix(app-shell): Studio's flow definition switch keeps keyboard focus across a toggle (#29449)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb7bgo9GLavj5QghibLqpzHe)_
Fixes #29376
Clause-②: no
Metadata-protocol: none
The two lines are copied from the claim (comment 6579229963).
## 维护者速读
- 改了什么:Studio › Automations
里可写包流程的「启用/停用」开关(流程定义开关,`StudioDesignSurface.tsx`),保存进行中不再是
`disabled`,改为 `aria-disabled`,并在点击处理里拒绝再次按下;只读包、没有画布的情况仍是
`disabled`,说明文字不变。
- 为什么改:浏览器会把焦点从一个变成 `disabled` 的已聚焦控件挪到页面
body,保存结束后也不会还回来。用键盘或读屏的作者每拨一次开关(保存成功或被拒都一样),都得从页面顶部重新 Tab 回来。这与
#29228(PR #29375)修过的部署开关是同一个缺陷,本 PR 用同样的做法。
- 风险与回滚:只改一个开关的 `onClick` / `disabled` / `aria-disabled`
三处属性和一个样式类;请求中视觉上仍是半透明(`aria-disabled:opacity-50`)。保存中按第二次空格不会再发保存,由点击守卫保证(测试与消融都钉住了)。回滚即撤销本
PR。
- 先看:`StudioDesignSurface.tsx` 里定义开关的 `inert` / `busy` 两行和
`onClick`;新测试文件
`StudioDesignSurface.definitionSwitchFocus-29376.test.tsx`。
- 两分钟验证:`pnpm --dir ui exec vitest run
packages/app-shell/src/views/studio-design/StudioDesignSurface.definitionSwitchFocus-29376.test.tsx`,预期
3 个用例全部通过。
- 需要你做的:无,席位落地。
## What changes
-
`ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx`,
the flow definition switch only: while a save is in flight (`saving`),
the switch carries `aria-disabled="true"` instead of `disabled`, and its
`onClick` refuses the press while `saving`. `disabled` stays for the two
cases where the switch cannot be flipped at all (no canvas to edit, or a
read-only package), so a read-only switch is unchanged: disabled, out of
the tab order, described by the package's reason through its focusable
wrapper. The in-flight dimming adds `aria-disabled:opacity-50` beside
`disabled:opacity-50`, so the switch looks as before.
- A comment at the switch says why the busy state is `aria-disabled`.
- `toggleEnabled` is not touched; the guard sits in the switch's
handler, as in PR #29375.
## Why
Measured live by the #29228 os-dev in Chromium 153 on a fresh showcase
(a scratch writable package with one flow): with the definition switch
focused, Space saved the draft (`PUT 200`) and `document.activeElement`
was the page body afterwards. The switch drew `disabled={!isEditable ||
!!saving || readOnly}`, and `toggleEnabled` sets `saving = 'draft'`. A
focused control that turns `disabled` loses focus and nothing returns
it. Same shape as #29228; this applies PR #29375's approach (focus never
leaves, so there is nothing to restore and no host plumbing).
## Tests
New file `StudioDesignSurface.definitionSwitchFocus-29376.test.tsx`, on
the harness of `StudioDesignSurface.automationsReadOnly-11124` (the real
`FlowPreview` and `FlowInspector`, a client double whose draft save is
held on a gate and can be refused with `ITEM_LOCKED`):
- `it.each` over a saved flip and a refused one: focus the switch, press
Space with `@testing-library/user-event`. In flight,
`document.activeElement` is the switch, it is `aria-disabled="true"` and
still enabled; a second Space leaves the switch on the first flip
(`aria-checked="false"`) and the save count at 1. After the answer (off
when saved; the refusal shown and back on when refused), the switch is
the same element, `document.activeElement` is still the switch, and the
save count is still 1.
- Read-only package: the switch is `disabled`, carries no
`aria-disabled`, is described by the read-only reason, and cannot take
focus.
- **Why the test runs a focus fixup.** happy-dom keeps focus on a
control that turns `disabled`; a browser does not. `runFocusFixup`
(copied from PR #29375's test) moves focus to the body when the focused
switch turns disabled, so the focus pins can fail.
- **Why the second press is pinned by state, not only by count.** Draft
saves are queued one at a time (`useDraftSaveGuard`), so a second save
waits behind the held one and the server count alone cannot see it in
flight. The first draft of this test checked only the count and the
guard ablation went red later than intended (at the post-answer
`aria-checked`); the test now pins the switch's state right after the
second Space.
- Ablation 1, the card's: `disabled={inert}` to `disabled={inert ||
!!saving}` through `scripts/ablation-replace.mjs` (anchor x1 to x0, blob
ef4c436 to 987c0f3b): both flip cases red at
`expect(document.activeElement).toBe(sw)` in flight (line 190, received
the body), read-only case green. Restored: blob equals HEAD, `git diff
HEAD` empty.
- Ablation 2, the guard: `if (!saving) void toggleEnabled();` to `if
(true) void toggleEnabled();` (blob ef4c436 to 4e50f0fd): both flip
cases red at the in-flight `aria-checked` pin (line 198). Restored, same
proof.
## Acceptance notes
- A live re-run of the #29228 probe on a fresh showcase was not done
(the card calls it welcome, not owed); the evidence is the happy-dom
pins with the browser focus fixup, and the two ablations.
- The guard reads `saving`, which every save in this pillar sets (the
autosave and publish too), so the switch also refuses a press while an
autosave is in flight; before, it was `disabled` then, so nothing new is
refused.
## Verification
<details><summary>Suites, type-check, lint</summary>
- At 9128595 (after the first merge of main) and again at 39356c4
(after the second): the new file plus the six other files that draw this
switch or the ledger switch (`automationsReadOnly-11124`,
`readOnlyFlowSwitch-27356`, `autosaveSwitch-11232`,
`packageLessFlows-11553`, `AutomationsPillar.customizeFlow-28570`,
`AutomationsPillar.ledgerSwitch-28148`): 7 files, 70 tests passed.
- `turbo run type-check --filter=@object-ui/app-shell` (`tsc --noEmit`
and `tsc -p tsconfig.test.json`): green at a73ccee (the merge brings
nothing into this package's files).
- eslint from `ui/` on the two touched source files: 0 errors; the 46
warnings in `StudioDesignSurface.tsx` are pre-existing, none in the
changed hunk. The repo-wide run is CI's Lint UI.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 34 commands; the list is the same
before and after the merge of main. All 34 ran, each exit 0, including
`node scripts/check-sdui-manifest.mjs` and `pnpm --dir ui exec vitest
run scripts/` (169 files passed, 2 skipped; 4921 tests). After the merge
of main the 31 faster commands ran again at 9128595;
`check-comment-mask-corpus`, `check:query-options-erasure` and the ui
`scripts/` suite were not re-run (the merge brings nothing into this
diff's files). The second merge of main (39356c4) touches none of this
diff's files either; only the seven suites above were re-run there.
`--ran` reconciliation at 9128595: 34 derived, 34 run, 0 NOT-MEASURED.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes. (Measured on the touched
files only; see Verification.)
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01QsR2jSQ9vnVPEcFMUbd1EJ
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jSQ9vnVPEcFMUbd1EJ)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0620fe3 commit e8b10a6
3 files changed
Lines changed: 251 additions & 3 deletions
File tree
- .changeset
- ui/packages/app-shell/src/views/studio-design
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10858 | 10858 | | |
10859 | 10859 | | |
10860 | 10860 | | |
| 10861 | + | |
| 10862 | + | |
| 10863 | + | |
| 10864 | + | |
| 10865 | + | |
| 10866 | + | |
| 10867 | + | |
| 10868 | + | |
| 10869 | + | |
10861 | 10870 | | |
10862 | 10871 | | |
10863 | 10872 | | |
10864 | 10873 | | |
10865 | 10874 | | |
10866 | | - | |
10867 | | - | |
| 10875 | + | |
| 10876 | + | |
| 10877 | + | |
| 10878 | + | |
| 10879 | + | |
10868 | 10880 | | |
10869 | 10881 | | |
10870 | 10882 | | |
| |||
10877 | 10889 | | |
10878 | 10890 | | |
10879 | 10891 | | |
10880 | | - | |
| 10892 | + | |
10881 | 10893 | | |
10882 | 10894 | | |
10883 | 10895 | | |
| |||
0 commit comments