修 #3952 时为确认「BooleanField 的 id 回退链里 `config.name` 一项不能删」而实测了这个对话框,顺带量到一个既有缺陷。与 #3952 同族但不同站点、不同包,所以另立单。 ## 实测(在 #3952 修复之后、真 dialog 渲染) ``` controlId=confirmed role=checkbox labels=[ { "text": "Confirm This", "for": "confirmed", "srOnly": true, "resolves": true }, <- widget 自己发的 { "text": "Confirm This", "for": "confirmed", "srOnly": false, "resolves": true } <- dialog 发的可见 label ] ``` 两个 label、同一段文本、同一个目标控件。按 accname 规范,引用同一控件的多个 label 元素会**拼接**成可访问名,所以屏幕阅读器听到的是 "Confirm This Confirm This"。`for` 都能解析,所以这不是 #3952 那种悬空 —— 是重复。 ## 成因 `packages/app-shell/src/views/ActionParamDialog.tsx` 的 boolean 分支渲染 `Label htmlFor={param.name}`,但**不**给 widget 传 `id`: - 通用分支(同文件,下面几十行)传的是 `id={param.name}`; - boolean 分支没传,全靠 `BooleanField` 的 id 回退链第二项(`config.name`,`paramToField` 设 `name: param.name`)恰好产出同一个字符串,关联才成立。 也就是说这个分支的 label 关联是**隐式**成立的:它依赖 widget 内部的回退实现细节,而不是自己下发 id。同时因为 widget 收不到 host id,就无从知道 host 已经渲染了 label,于是照旧发一份自己的 sr-only label —— 重复由此产生。 (#3952 之前,表单渲染器那条路径上这个重复更严重:那边可见 label 指向的 id 根本不存在。这里两个 label 都指对了,所以只是名字重复,不影响点击。) ## 修法(#3952 落地后是一行) boolean 分支像它自己的通用分支那样传 `id={param.name}`: - 关联从隐式变**显式**,不再依赖 widget 的回退链实现(回退链本身在 PR #3959 里已被钉住,但一个 host 不该靠别人的回退过日子); - `BooleanField` 收到 host id 后会自动**不再**发自己的 sr-only label(PR #3959 的第二半),可访问名回到单份 "Confirm This"。 需要补一个钉子:boolean param 的可访问名恰好等于 param label(不是它的两倍),且 `getAllByLabelText` 只解析出一个控件。`packages/app-shell/src/views/ActionParamDialog.ariaRequired.test.tsx` 已经有这个 dialog 的 boolean 分支用例,是自然的落点。 ## 参考 - #3952 / PR #3959(`BooleanField`:host 拥有控件 id;本单的一行修法依赖它已落地) - #3341(同一个 dialog 的 components 版 select 分支 `htmlFor` 悬空 —— 同一失效家族的先例) - #3961(复合/分组 widget 的组标签关联,另一层裁决)
修 #3952 时为确认「BooleanField 的 id 回退链里
config.name一项不能删」而实测了这个对话框,顺带量到一个既有缺陷。与 #3952 同族但不同站点、不同包,所以另立单。实测(在 #3952 修复之后、真 dialog 渲染)
两个 label、同一段文本、同一个目标控件。按 accname 规范,引用同一控件的多个 label 元素会拼接成可访问名,所以屏幕阅读器听到的是 "Confirm This Confirm This"。
for都能解析,所以这不是 #3952 那种悬空 —— 是重复。成因
packages/app-shell/src/views/ActionParamDialog.tsx的 boolean 分支渲染Label htmlFor={param.name},但不给 widget 传id:id={param.name};BooleanField的 id 回退链第二项(config.name,paramToField设name: param.name)恰好产出同一个字符串,关联才成立。也就是说这个分支的 label 关联是隐式成立的:它依赖 widget 内部的回退实现细节,而不是自己下发 id。同时因为 widget 收不到 host id,就无从知道 host 已经渲染了 label,于是照旧发一份自己的 sr-only label —— 重复由此产生。
(#3952 之前,表单渲染器那条路径上这个重复更严重:那边可见 label 指向的 id 根本不存在。这里两个 label 都指对了,所以只是名字重复,不影响点击。)
修法(#3952 落地后是一行)
boolean 分支像它自己的通用分支那样传
id={param.name}:BooleanField收到 host id 后会自动不再发自己的 sr-only label(PR fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959 的第二半),可访问名回到单份 "Confirm This"。需要补一个钉子:boolean param 的可访问名恰好等于 param label(不是它的两倍),且
getAllByLabelText只解析出一个控件。packages/app-shell/src/views/ActionParamDialog.ariaRequired.test.tsx已经有这个 dialog 的 boolean 分支用例,是自然的落点。参考
fordangles #3952 / PR fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959(BooleanField:host 拥有控件 id;本单的一行修法依赖它已落地)htmlFor悬空 —— 同一失效家族的先例)for悬空,checkboxes / radio / rating / file 的for落在不可 label 的 div 上 #3961(复合/分组 widget 的组标签关联,另一层裁决)