修 #3962(app-shell ActionParamDialog 的 boolean 分支不下发 id)时,顺手枚举了「渲染共享 field widget 的 host」这条消费半径 —— getLazyFieldWidget 在仓内只有两个非测试调用点。另一个是 packages/plugin-grid/src/components/BulkActionDialog.tsx 的 ParamField,它在 id 这件事上是对的(每种类型都传 id={id}),但 #3299/#3290 为 ActionParamDialog 定下的另外两条约定在这里都没有。与 #3962 不同包、不同站点,所以另立单;不在 #3962 的 PR 范围内。
站点
packages/plugin-grid/src/components/BulkActionDialog.tsx,ParamField(约 585-600 行):
Label htmlFor={id} className="text-xs"
{param.label ?? param.name}
{param.required && span className="text-destructive ml-0.5" * }
/Label
...
Widget
id={id}
value={value ?? null}
onChange={onChange}
field={field}
{...dataSourceProps}
(上面为规避 GitHub 正文清洗器,尖括号已去掉 —— 以仓内实际代码为准。)
两个缺陷
1. 裸 * 折进控件的可访问名(#3299 判过的同一形状)。 那个 span 没有 aria-hidden="true"。* 在 Label htmlFor 内部,按 accname 会进入被引用控件的名字,于是屏幕阅读器把每个必填 bulk param 念成「Label asterisk」。ActionParamDialog 的两个分支都显式加了 aria-hidden="true" 并带注释说明理由(「Visual-only ... without aria-hidden the bare * would fold into the accessible name」),还有 ActionParamDialog.ariaRequired.test.tsx 的钉子;这个站点两样都没有。
2. required 状态从不到达控件。 param.required 是真实生效的声明 —— 同文件 238 行用它做提交前校验 —— 但 Widget 的 props 里没有 aria-required。所以控件本身从不宣告必填状态:视觉上有个星号,可访问性树里什么都没有。#3299 的裁决是「required 是 STATE,走 state channel 到控件」,toDomProps 的白名单按前缀转发 aria-*,通路是通的,只是这里没接。
两条合起来的实测后果:必填的 bulk action param,屏幕阅读器听到的是「星号」这个装饰字符,而唯一真正携带语义的通道是空的。
不是什么
修法草案
与 ActionParamDialog 对齐(不要在这里发明第二套写法):
- 星号
span 补 aria-hidden="true"。
Widget 补 aria-required={param.required || undefined}(|| undefined 使可选 param 完全不带该属性,与 ActionParamDialog 一致,而不是写 "false")。
- 钉子:必填 bulk param 的可访问名恰好等于 param label(不含星号)、控件带
aria-required="true"、可选 param 完全不带该属性。plugin-grid 已有 BulkActionDialog 的测试文件可扩展。
参考
修 #3962(app-shell
ActionParamDialog的 boolean 分支不下发id)时,顺手枚举了「渲染共享 field widget 的 host」这条消费半径 ——getLazyFieldWidget在仓内只有两个非测试调用点。另一个是packages/plugin-grid/src/components/BulkActionDialog.tsx的ParamField,它在id这件事上是对的(每种类型都传id={id}),但 #3299/#3290 为ActionParamDialog定下的另外两条约定在这里都没有。与 #3962 不同包、不同站点,所以另立单;不在 #3962 的 PR 范围内。站点
packages/plugin-grid/src/components/BulkActionDialog.tsx,ParamField(约 585-600 行):(上面为规避 GitHub 正文清洗器,尖括号已去掉 —— 以仓内实际代码为准。)
两个缺陷
1. 裸
*折进控件的可访问名(#3299 判过的同一形状)。 那个span没有aria-hidden="true"。*在Label htmlFor内部,按 accname 会进入被引用控件的名字,于是屏幕阅读器把每个必填 bulk param 念成「Label asterisk」。ActionParamDialog的两个分支都显式加了aria-hidden="true"并带注释说明理由(「Visual-only ... withoutaria-hiddenthe bare*would fold into the accessible name」),还有ActionParamDialog.ariaRequired.test.tsx的钉子;这个站点两样都没有。2.
required状态从不到达控件。param.required是真实生效的声明 —— 同文件 238 行用它做提交前校验 —— 但Widget的 props 里没有aria-required。所以控件本身从不宣告必填状态:视觉上有个星号,可访问性树里什么都没有。#3299 的裁决是「required 是 STATE,走 state channel 到控件」,toDomProps的白名单按前缀转发aria-*,通路是通的,只是这里没接。两条合起来的实测后果:必填的 bulk action param,屏幕阅读器听到的是「星号」这个装饰字符,而唯一真正携带语义的通道是空的。
不是什么
aria-invalid—— #3306 全注册表守卫实测的账本 #3318。 那单是 widget 侧的aria-invalid(29 个注册 widget,校验失败后的通道),这单是 host 侧的aria-required+ 装饰字符,层次和属性都不同。id,于是同一个控件被两个同文本 label 引用(可访问名重复),而它自己的通用分支一直是对的 #3962。 [app-shell] ActionParamDialog 的 boolean 分支不下发id,于是同一个控件被两个同文本 label 引用(可访问名重复),而它自己的通用分支一直是对的 #3962 是id归属与重复 label(app-shell)。这个站点的id本来就下发正确,而且它给的是bulk-param-${param.name}而非param.name—— 在 [fields] a boolean field's visible form label is associated with nothing — BooleanField overrides the control id with the field name, so the form's labelfordangles #3952/PR fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959 让BooleanField尊重 host id 之前,这里的 boolean param 才是真悬空;那一半已经被 fix(fields): BooleanField 使用 host 下发的控件 id,可见 label 不再悬空 (#3952) #3959 修掉了。修法草案
与
ActionParamDialog对齐(不要在这里发明第二套写法):span补aria-hidden="true"。Widget补aria-required={param.required || undefined}(|| undefined使可选 param 完全不带该属性,与ActionParamDialog一致,而不是写"false")。aria-required="true"、可选 param 完全不带该属性。plugin-grid 已有BulkActionDialog的测试文件可扩展。参考
*必须aria-hidden)aria-required从未到达任何输入控件——必填状态只以「可访问名的一部分」存在 #3290(刻意不用原生required:会与 dialog 自己的必填提示形成两个校验器)id,于是同一个控件被两个同文本 label 引用(可访问名重复),而它自己的通用分支一直是对的 #3962 / 本次消费半径枚举的来源aria-invalid—— #3306 全注册表守卫实测的账本 #3318(widget 侧aria-invalid,相邻但不同)