fix(app-shell): ActionParamDialog 的 boolean 分支下发控件 id,可访问名不再是两份 label 的拼接 (#3962) - #3971
Merged
Merged
Conversation
… 的拼接 (#3962) `ActionParamDialog` 的 boolean 分支渲染 `Label htmlFor={param.name}` 放在控件 旁边,却不给 widget 传 `id` —— 而同一文件下面几十行的通用分支一直传的是 `id={param.name}`。实测(#3952 / PR #3959 之后、真 dialog 渲染)一个 boolean param 产出两个 label 元素指向同一个控件:widget 自己那份 sr-only 的,和 dialog 的可见那份,文本相同。两个 `for` 都能解析,所以这不是 #3341 / #3952 那种悬空 label —— 点击一直是好的;坏的是名字:按 accname 规范,引用同一控件的多个 label 会拼接成可访问名,屏幕阅读器听到的是 "Confirm This Confirm This"。 两个缺陷,相隔一行: 1. 关联是隐式的。它成立只因为 `BooleanField` 的 id 回退链第二项落在 `config.name`,而 `paramToField` 把它设成了 `param.name` —— 两边靠另一个包的 内部实现恰好撞成同一个字符串。一个渲染了 `htmlFor` 的 host 必须自己发出它所 指的 id;widget 契约(`FieldWidgetDomProps`)声明 `id` 就是为这个。 2. 重复的 sr-only label。#3952 / PR #3959 让 `BooleanField` 在 host 下发 id 时 不再发自己的 label(`emitOwnLabel = !hostId`),理由正是「会下发 id 的 host 就是会渲染 label 的 host」。这个分支收不到 id,那条抑制从来没被触发。 修法是让 boolean 分支像它自己的通用分支那样传 `id={param.name}`:关联从隐式变 显式,widget 的 sr-only 副本被抑制,可访问名回到作者声明的那一份。通用分支未动。 钉子挂在两个既有测试文件上。命名一半在 `ActionParamDialog.ariaRequired.test.tsx` (4 条):boolean 控件 id 恰为 `param.name` 且 `label[for]` 恰好 1 个、活下来的 是可见那份而非 sr-only;可访问名恰为一份;必填 boolean 的名字仍不含 `aria-hidden` 的星号且带 `aria-required="true"`;以及不变方向的通用分支 host id / 单 label / 单名字。行为一半在 `ActionParamDialog.test.tsx`(1 条):点击可见 label 翻转开关并 round-trip 到 `resolve`。 刻意没有用 `getAllByLabelText` 做探针(issue 正文提议过): `@testing-library/dom` 10.4.1 的 `queryAllByLabelText` 最后一步是 `Array.from(new Set(matchingLabelledElements))`,并且对多 label 会把名字拆开 分别匹配,修前修后它都只解析出 1 个控件,当探针是空转的。可测量的那一半是名字 与 label 元素个数。 反向验证按预判成立:删掉 boolean 分支那一行 `id={param.name}` 后恰好 3 条命名钉 翻红,而「通用分支不变」与点击 label 那条保持绿 —— 修前 `for` 目标本来就能解析, 这个缺陷是重复而非悬空。变异未提交。 Co-authored-by: Claude <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3962 落地。 核验记录(head
消费半径 out-of-scope:#3967(BulkActionDialog 缺 aria-hidden 星号与 aria-required)由 PM 分诊。 Generated by Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3962
ActionParamDialog的 boolean 分支渲染Label htmlFor={param.name}放在控件旁边,却不给 widget 传id—— 而同一文件下面几十行的通用分支一直传的是id={param.name}。本 PR 让 boolean 分支与它自己的通用分支对齐,一行。修前实测(真 dialog 渲染,#3952/PR #3959 之后)
一个
booleanparam(name: confirmed,label: "Confirm This")产出两个 label 元素指向同一个控件:两个
for都能解析,所以这不是 #3341 / #3952 那种悬空 label —— 点击一直是好的。坏的是名字:按 accname §2D,引用同一控件的多个 label 会拼接成可访问名,屏幕阅读器听到的是 "Confirm This Confirm This"。两个缺陷,相隔一行
BooleanField的 id 回退链第二项落在config.name,而paramToField把它设成了param.name—— 两边靠另一个包的内部实现恰好撞成同一个字符串。一个渲染了htmlFor的 host 必须自己发出它所指的 id;widget 契约(FieldWidgetDomProps)声明id就是为这个。fordangles #3952 / PR fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959 让BooleanField在 host 下发 id 时不再发自己的 label(emitOwnLabel = !hostId),理由正是「会下发 id 的 host 就是会渲染 label 的 host」。这个分支收不到 id,那条抑制从来没被触发。修后:关联从隐式变显式,widget 的 sr-only 副本被抑制,可访问名回到作者声明的那一份 "Confirm This"。通用分支未动。
钉子(两个方向)
ActionParamDialog.ariaRequired.test.tsx新增 4 条(命名一半):id恰为param.name,且label[for]恰好 1 个,而且活下来的那个是可见的那份(不是 sr-only —— 只数个数会让一个只有屏幕阅读器可见名字的 checkbox 行蒙混过关);Confirm This,不是它的两倍;aria-hidden的*不能被抑制重复的动作带进名字里),且控件带aria-required="true";ActionParamDialog.test.tsx新增 1 条(行为一半):点击可见 label 翻转开关并 round-trip 到resolve。刻意没有用
getAllByLabelText做探针(issue 正文提议过):@testing-library/dom10.4.1 的queryAllByLabelText最后一步是Array.from(new Set(matchingLabelledElements)),并且对多 label 会把名字拆开分别匹配 —— 修前修后它都只解析出 1 个控件,当探针是空转的。可测量的那一半是名字与label 元素个数,钉子就钉在这两处。反向验证(先预判,后跑)
预判:删掉 boolean 分支那一行
id={param.name}(只删 24 空格缩进那条,通用分支的同名行不动),恰好 3 条命名钉翻红(2 个 label / 名字翻倍 / 必填那条也因名字翻倍红),而「通用分支不变」与新增的点击 label 钉保持绿 —— 因为修前for目标本来就能解析,这个缺陷是重复而非悬空。实测与预判逐条一致。带修:
Test Files 2 passed (2)/Tests 46 passed (46)。删掉那一行后(id={param.name}出现次数 2 → 1,通用分支那条完好):「通用分支不变」与点击 label 那条在变异树里如预判保持绿。变异随后由
git checkout --+ 重新git apply还原(未用git stash,共享栈),未提交。验证
flock串行 +NODE_OPTIONS=--max-old-space-size=4096:pnpm exec vitest run packages/app-shell --maxWorkers=2→Test Files 308 passed (308)/Tests 2842 passed | 1 skipped (2843)pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build→ 退出 0pnpm exec turbo run type-check --concurrency=2→Tasks: 78 successful, 78 total,退出 0node scripts/check-control-bytes.mjs→ OK(3856 个跟踪文本文件)。改动的 4 个文件另做了一次越过门禁盲区的自扫:用grep -naP匹配 C0 控制字符区间(NUL 到退格、垂直制表、换页、以及 SO 到 US),即除制表符/换行/回车之外的全部控制字节,无命中。消费半径
getLazyFieldWidget在仓内只有两个非测试调用点。另一个 host(plugin-grid的BulkActionDialog)id下发是对的,但缺aria-hidden的星号与aria-required,已另立 #3967,不在本 PR 范围。边界
只动 boolean 分支 + 两个既有测试文件 + changeset。未碰
BooleanField本体(#3959 已定型)、未碰releases/、未碰 #3913 在飞的 metadata-admin 面。