Repository navigation
Commit dda74d3
fix(app-shell): disabled Studio Publish gives its nothing-pending reason on hover and keyboard focus (#31561)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9Ubb6bDc6Eia7WGdAfa8YTp42f)_
Fixes #31495
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:Studio 顶栏的「发布」按钮在没有待发布草稿时,不再只在禁用按钮上挂一个
`title`,而是和只读包状态一样,外面套一层可聚焦的包裹元素:鼠标悬停、键盘 Tab
聚焦都会弹出提示「没有待发布的草稿」,屏幕阅读器也能读到这句原因。
- **为什么改**:原生禁用的按钮收不到悬停和焦点事件,`title` 谁也看不到,Tab
还会直接跳过「发布」;作者不知道按钮为什么是灰的。QA 规则 `ux-conventions.disabled-control-reason`
要求每个禁用控件都能说明原因。
- **风险与回滚**:只改一个组件的包裹结构,不动文案、不加多语言 key、不碰服务端或
spec。只读包状态和有草稿时的可点击按钮保持原样。回滚即 revert 本 PR。
-
**先看**:`ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx`
中 `publishDisabledReason` 与其下方的 Tooltip 包裹;其次是测试文件
`StudioDesignSurface.publishReadOnlyReason-12303.test.tsx` 末尾新增的
describe 块。
- **两分钟验证**:打开一个没有草稿的可写包的 Studio,在「变更」按钮上按一次
Tab,焦点应落在「发布」上并弹出「没有待发布的草稿」提示;鼠标悬停也应出现同一提示。
- **需要你做的**:无,席位落地。
## What changes
`StudioDesignSurface` computes one `publishDisabledReason`: the
read-only reason (`engine.studio.pkg.readonlyHint`) on a read-only
package with no pair, else the nothing-pending reason
(`engine.studio.publishNoneTitle`) when no draft is pending and no
publish is in flight, else none. Whenever there is a reason, Publish is
drawn inside the one shared wrapper the read-only state already used: a
`tabIndex=0` span marked `data-disabled-reason` that is the Radix
tooltip trigger, with the span and the button both pointing
`aria-describedby` at an `sr-only` copy of the reason. The button gets
`disabled:pointer-events-none` so the wrapper takes the hover. The
`title` the disabled button could never show is dropped; an enabled
Publish keeps its `engine.studio.publishTitle` title.
The nothing-pending `sr-only` reason moves inside the wrapper and keeps
its `publish-none-reason` test id and its id. The read-only branch
renders exactly as before (same wrapper, same text, same ids).
A publish in flight gets no wrapper on purpose: the button shows a
spinner for the moment the publish runs, which is the host's own
transient state and not a condition the author has to act on, so there
is no reason to give.
No locale key is added (the existing `engine.studio.publishNoneTitle` is
reused). No server or `packages/spec` change.
## Tests
The new pins go into the existing shared harness
`StudioDesignSurface.publishReadOnlyReason-12303.test.tsx` as a
top-level describe, with a `drafts` ledger the fetch stub now answers:
- no drafts on a writable package: the wrapper is a tab stop; the button
carries no `title`; the wrapper and the button are both described by "No
drafts pending publish", through the one `publish-none-reason` element;
the button lets the hover through;
- hovering the wrapper opens a tooltip with the reason;
- Tab from "Changes" lands on Publish's wrapper and opens the tooltip;
- CONTROL, a draft pending: a plain enabled Publish with no wrapper, no
description and no reason text.
Two edits to the file's existing read-only block, both forced by the new
behaviour:
- the wait helper now waits for the wrapper to be described by the
reason it expects, not for any wrapper, because an empty ledger now
wraps Publish in the nothing-pending reason before the package list
answers `writable: false`;
- the writable CONTROL leg asserted "no wrapper" on a writable package
with an empty ledger, which is exactly the state this card changes. It
now asserts that leg gives no read-only reason and that its wrapper
carries the nothing-pending reason instead.
Unchanged and green: the objectui#8219 no-draft and pending pins
(`StudioDesignSurface.interfacePolish.test.tsx`), the objectui#12517
posture pins (`StudioDesignSurface.publishPosture-12517.test.tsx`), the
pending-count pins (`StudioDesignSurface.changesCount-11787.test.tsx`,
`StudioDesignSurface.publishCount-11807.test.tsx`).
## Acceptance notes
- While the draft ledger's first read is still on the wire, the surface
already treats the package as having nothing pending, so the wrapper and
the nothing-pending reason show from first paint until the read answers.
This is unchanged from main, where the `sr-only` sentence and the
`title` showed in the same window; this PR only gives that state the
wrapper.
- When a draft lands while the wrapper has focus, the wrapper is
replaced by the plain button, so focus returns to the document body. The
read-only wrapper on main behaves the same way when a package turns
writable.
## Verification
Head `41c616fc` (fix `0c4adb53`, harness import `d612ad6e`, merge of
`origin/main` `41c616fc`, which touched no file under
`ui/packages/app-shell`). Branch base `dabef0f66`.
<details><summary>Tests (app-shell, vitest from ui/, under the verify
lock)</summary>
- The five Publish files
(`StudioDesignSurface.publishReadOnlyReason-12303`, `interfacePolish`,
`publishPosture-12517`, `changesCount-11787`, `publishCount-11807`) at
`d612ad6e`: Test Files 5 passed (5), Tests 27 passed (27).
- 32 neighbour files that import `StudioDesignSurface` and query
Publish, a tooltip, Tab order or a `data-disabled-reason` wrapper, at
`0c4adb53`: Test Files 32 passed (32), Tests 289 passed (289). The other
app-shell suites are CI's Test UI.
- `pnpm exec turbo run type-check --filter=@object-ui/app-shell`: 67
tasks, exit 0 (both `tsc --noEmit` and `tsc -p tsconfig.test.json`).
</details>
<details><summary>Ablation (one leg, committed first, through
scripts/ablation-replace.mjs)</summary>
The nothing-pending arm of `publishDisabledReason` was replaced by
`false` (anchor 1 to 0, blob `5abb9ba96d85` to `f6ae84fc1790`), so a
writable package with no drafts gets neither the wrapper nor the reason.
Six cases go red: the three new nothing-pending cases, the writable
CONTROL leg of the read-only block, the objectui#8219 no-draft pin and
the objectui#12517 nothing-pending pin. The three read-only cases and
the new pending CONTROL stay green. Restore: blob back to
`5abb9ba96d85`, equal to HEAD, `git diff HEAD` empty. The tests import
the surface by relative path, so the mutation is read from `src/` with
no build in between.
</details>
<details><summary>Gates</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derives 36 families; all 36 ran with exit 0
and `--ran` reconciles 36 run, 0 NOT-MEASURED. `check:test-source-alias`
first asked for a module-top `@objectstack/lint` import in the edited
harness (added in `d612ad6e`, then green). `check:query-options-erasure`
and `check:type-check-debt` ran without a timeout, the latter under the
lock. After the merge of main the derived list is unchanged, and the
ratchet families (`check:test-source-alias`,
`check:type-check-coverage`, `check:query-options-erasure`,
`check:cross-package-test-inputs`, `check-sdui-manifest`,
`check-changeset-no-major`, `check-empty-changeset`) re-ran green on
`41c616fc`. `pnpm --dir ui run check:new-line-citations`: 0 new
citations.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QsR2jRPafwMLdY61r8x4Jn)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 983abd9 commit dda74d3
3 files changed
Lines changed: 133 additions & 50 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 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
19 | 26 | | |
20 | 27 | | |
21 | 28 | | |
| |||
28 | 35 | | |
29 | 36 | | |
30 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
31 | 41 | | |
32 | 42 | | |
33 | 43 | | |
34 | 44 | | |
| 45 | + | |
| 46 | + | |
35 | 47 | | |
36 | 48 | | |
37 | 49 | | |
| |||
73 | 85 | | |
74 | 86 | | |
75 | 87 | | |
| 88 | + | |
76 | 89 | | |
77 | 90 | | |
78 | 91 | | |
79 | 92 | | |
80 | 93 | | |
81 | 94 | | |
82 | 95 | | |
| 96 | + | |
83 | 97 | | |
84 | 98 | | |
85 | 99 | | |
86 | 100 | | |
87 | | - | |
| 101 | + | |
88 | 102 | | |
89 | 103 | | |
90 | 104 | | |
| |||
120 | 134 | | |
121 | 135 | | |
122 | 136 | | |
123 | | - | |
124 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
125 | 143 | | |
126 | 144 | | |
127 | 145 | | |
128 | 146 | | |
129 | 147 | | |
| 148 | + | |
130 | 149 | | |
131 | 150 | | |
132 | 151 | | |
133 | 152 | | |
134 | 153 | | |
135 | 154 | | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
136 | 158 | | |
137 | 159 | | |
138 | 160 | | |
| |||
160 | 182 | | |
161 | 183 | | |
162 | 184 | | |
163 | | - | |
| 185 | + | |
164 | 186 | | |
165 | 187 | | |
166 | 188 | | |
| |||
170 | 192 | | |
171 | 193 | | |
172 | 194 | | |
173 | | - | |
| 195 | + | |
| 196 | + | |
174 | 197 | | |
175 | 198 | | |
176 | 199 | | |
177 | 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 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1214 | 1214 | | |
1215 | 1215 | | |
1216 | 1216 | | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
1217 | 1226 | | |
1218 | 1227 | | |
1219 | 1228 | | |
| |||
1512 | 1521 | | |
1513 | 1522 | | |
1514 | 1523 | | |
1515 | | - | |
1516 | | - | |
1517 | | - | |
1518 | | - | |
1519 | | - | |
1520 | | - | |
1521 | | - | |
1522 | | - | |
1523 | | - | |
1524 | | - | |
1525 | | - | |
1526 | | - | |
1527 | | - | |
1528 | | - | |
1529 | | - | |
1530 | | - | |
| 1524 | + | |
| 1525 | + | |
| 1526 | + | |
| 1527 | + | |
1531 | 1528 | | |
1532 | 1529 | | |
1533 | 1530 | | |
| |||
1539 | 1536 | | |
1540 | 1537 | | |
1541 | 1538 | | |
1542 | | - | |
| 1539 | + | |
1543 | 1540 | | |
1544 | 1541 | | |
1545 | 1542 | | |
| |||
1677 | 1674 | | |
1678 | 1675 | | |
1679 | 1676 | | |
1680 | | - | |
1681 | | - | |
1682 | | - | |
1683 | | - | |
1684 | | - | |
1685 | | - | |
1686 | | - | |
1687 | | - | |
1688 | | - | |
1689 | | - | |
1690 | | - | |
1691 | | - | |
1692 | | - | |
1693 | | - | |
1694 | | - | |
1695 | | - | |
1696 | | - | |
1697 | | - | |
1698 | | - | |
1699 | | - | |
1700 | | - | |
1701 | | - | |
1702 | | - | |
1703 | | - | |
| 1677 | + | |
| 1678 | + | |
| 1679 | + | |
| 1680 | + | |
| 1681 | + | |
| 1682 | + | |
| 1683 | + | |
| 1684 | + | |
| 1685 | + | |
| 1686 | + | |
| 1687 | + | |
| 1688 | + | |
1704 | 1689 | | |
1705 | 1690 | | |
1706 | 1691 | | |
1707 | 1692 | | |
1708 | 1693 | | |
1709 | | - | |
| 1694 | + | |
1710 | 1695 | | |
1711 | 1696 | | |
1712 | 1697 | | |
1713 | 1698 | | |
1714 | | - | |
1715 | | - | |
| 1699 | + | |
| 1700 | + | |
| 1701 | + | |
| 1702 | + | |
| 1703 | + | |
| 1704 | + | |
1716 | 1705 | | |
1717 | 1706 | | |
1718 | 1707 | | |
1719 | | - | |
| 1708 | + | |
1720 | 1709 | | |
1721 | 1710 | | |
1722 | 1711 | | |
| |||
0 commit comments