Skip to content

[plugin-grid] BulkActionDialog 的必填 param 把裸 * 折进可访问名、且 required 从不下发到控件 —— #3299/#3290 家族里被漏掉的一个站点 #3967

Description

@yinlianghui

#3962(app-shell ActionParamDialog 的 boolean 分支不下发 id)时,顺手枚举了「渲染共享 field widget 的 host」这条消费半径 —— getLazyFieldWidget 在仓内只有两个非测试调用点。另一个是 packages/plugin-grid/src/components/BulkActionDialog.tsxParamField,它在 id 这件事上是对的(每种类型都传 id={id}),但 #3299/#3290ActionParamDialog 定下的另外两条约定在这里都没有。与 #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 对齐(不要在这里发明第二套写法):

  1. 星号 spanaria-hidden="true"
  2. Widgetaria-required={param.required || undefined}(|| undefined 使可选 param 完全不带该属性,与 ActionParamDialog 一致,而不是写 "false")。
  3. 钉子:必填 bulk param 的可访问名恰好等于 param label(不含星号)、控件带 aria-required="true"、可选 param 完全不带该属性。plugin-grid 已有 BulkActionDialog 的测试文件可扩展。

参考

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions