实施 objectui#3290(PR #3298,给对象表单渲染器 packages/components/src/renderers/form/form.tsx 的控件送 aria-required)时顺带扫到的:同一个缺陷在别的渲染器里还有五处,#3290 的范围栅栏只钉了 form.tsx 一处,所以单独立此单,不在那个 PR 里修。
缺陷形状(与 #3290 完全同型)
组件算出了 required,只用它画一颗红星,而控件本身没有任何必填状态——既没有 aria-required,也没有原生 required。屏幕阅读器要么完全不知道该字段必填,要么只能从可访问名里读到一个孤零零的 "asterisk / star"。
清单(已逐处核对,非 grep 猜测)
| 位置 |
星号写法 |
控件上的必填状态 |
packages/app-shell/src/views/ActionParamDialog.tsx:243,260 |
< Label htmlFor > 内的裸 < span >*< /span >(无 aria-hidden) |
无 |
packages/components/src/custom/action-param-dialog.tsx:117,137,173,202,223 |
同上,五处 |
无 |
packages/app-shell/src/views/CreateViewDialog.tsx:510,557,579 |
< label htmlFor > 内裸 < span >*< /span > |
无 |
packages/plugin-detail/src/InlineCreateRelated.tsx:211 |
裸 < span >*< /span > |
无 |
packages/components/src/custom/field.tsx:58 |
CSS 伪元素 after:content-['*'] |
无——FieldContainer 往子 Slot 注入了 id / aria-describedby / aria-invalid,唯独漏了 aria-required |
裸 < span >*< /span > 那四处比 form.tsx 原来的情况更糟一点:老的 form.tsx 至少有 aria-label="required" 让可访问名读作 "Title required";这些只有一个 * 字符落进可访问名,被读成 "Title asterisk"。
已经做对的参照实现
packages/plugin-form/src/EmbeddableForm.tsx:481,489 早就是正确形状,可以直接照抄:
aria-required={consentRequired || undefined}
…
< span aria-hidden="true" className="text-destructive ml-0.5" >*< /span >
PR #3298 让 form.tsx 收敛到的正是这个形状。
建议修法
对每一处:控件加 aria-required={required || undefined}(不是原生 required——理由见 #3290 裁决:会和应用自己的校验双轨),星号加 aria-hidden="true"。custom/field.tsx 的 FieldContainer 只需在已有的 Slot 注入里补一个 aria-required,一处就覆盖它的所有使用方。
明确排除
packages/components/src/renderers/form/{input,textarea,select,checkbox}.tsx 和 basic/text-input.tsx 不在本单内:它们设了原生 required={schema.required},浏览器会自动把它映射成必填状态,a11y 上是通的。那几个是独立 SDUI 控件、不走 react-hook-form,所以原生 required 在那里也不构成 #3290 说的双轨问题。
边界
不动 packages/spec,不动任何 field widget 的 props 契约(aria-required 已经在 FieldWidgetComponentProps & AriaAttributes 里,无需新键)。每处一到两行 + 测试。
关联:objectui#3290、PR #3298。
实施 objectui#3290(PR #3298,给对象表单渲染器
packages/components/src/renderers/form/form.tsx的控件送aria-required)时顺带扫到的:同一个缺陷在别的渲染器里还有五处,#3290 的范围栅栏只钉了form.tsx一处,所以单独立此单,不在那个 PR 里修。缺陷形状(与 #3290 完全同型)
组件算出了
required,只用它画一颗红星,而控件本身没有任何必填状态——既没有aria-required,也没有原生required。屏幕阅读器要么完全不知道该字段必填,要么只能从可访问名里读到一个孤零零的 "asterisk / star"。清单(已逐处核对,非 grep 猜测)
packages/app-shell/src/views/ActionParamDialog.tsx:243,260< Label htmlFor >内的裸< span >*< /span >(无aria-hidden)packages/components/src/custom/action-param-dialog.tsx:117,137,173,202,223packages/app-shell/src/views/CreateViewDialog.tsx:510,557,579< label htmlFor >内裸< span >*< /span >packages/plugin-detail/src/InlineCreateRelated.tsx:211< span >*< /span >packages/components/src/custom/field.tsx:58after:content-['*']FieldContainer往子 Slot 注入了id/aria-describedby/aria-invalid,唯独漏了aria-required裸
< span >*< /span >那四处比form.tsx原来的情况更糟一点:老的form.tsx至少有aria-label="required"让可访问名读作 "Title required";这些只有一个*字符落进可访问名,被读成 "Title asterisk"。已经做对的参照实现
packages/plugin-form/src/EmbeddableForm.tsx:481,489早就是正确形状,可以直接照抄:PR #3298 让
form.tsx收敛到的正是这个形状。建议修法
对每一处:控件加
aria-required={required || undefined}(不是原生required——理由见 #3290 裁决:会和应用自己的校验双轨),星号加aria-hidden="true"。custom/field.tsx的FieldContainer只需在已有的 Slot 注入里补一个aria-required,一处就覆盖它的所有使用方。明确排除
packages/components/src/renderers/form/{input,textarea,select,checkbox}.tsx和basic/text-input.tsx不在本单内:它们设了原生required={schema.required},浏览器会自动把它映射成必填状态,a11y 上是通的。那几个是独立 SDUI 控件、不走 react-hook-form,所以原生required在那里也不构成 #3290 说的双轨问题。边界
不动
packages/spec,不动任何 field widget 的 props 契约(aria-required已经在FieldWidgetComponentProps & AriaAttributes里,无需新键)。每处一到两行 + 测试。关联:objectui#3290、PR #3298。