Skip to content

aria-required 从未到达任何输入控件——必填状态只以「可访问名的一部分」存在 #3290

Description

@xuyushun441-sys

在 objectui#3222(把校验槽位改名为 spec 的 error 并接上生产者)中,按裁定单独评估了 required 是否应下沉到 widget props。结论是不下沉(理由见 PR #3289),但评估过程里暴露了一个独立的、真实的可访问性缺口,单独立此单。

事实

表单渲染器算出的 required(静态 required + requiredWhen CEL,form.tsx 的 ruleState.required)只用于一处:画 < FormLabel > 里的红星。

{required && (
  < span className="text-destructive ml-1" aria-label="required" >*< /span >
)}

它没有进入 renderFieldComponent 的 props,< FormControl >(Radix Slot,注入 id / aria-describedby / aria-invalid)也不注入 aria-required。全仓 grep aria-required:0 处。

也就是说必填状态今天只通过一条路径到达屏幕阅读器:< label for > 关联把那个 aria-label="required" 的 span 并进控件的可访问名,于是字段被读成「Title required」。

为什么这不够好

  • 它把一个状态塞进了名字。AT 有专门的 required 状态通道(aria-required / 原生 required),会用一致的方式播报;混进名字里则依赖朗读顺序,且无法被「只列必填项」这类导航功能识别。
  • 字段没有 label 时(紧凑布局、grid 内联编辑、label 为空的表单)红星根本不渲染,必填信号完全消失。
  • requiredWhen 让必填是动态的,状态通道能表达变化,名字不能。

修法(便宜得出乎意料,且不需要改 widget 契约)

FieldWidgetComponentProps 已经 & AriaAttributes,aria-required 本来就是这份契约里已声明、已类型化的键;而每个 widget 都已经把剩余 props 转发到它渲染的控件上。内置分支同理(stripRendererOnlyProps 不剥 aria-*)。

所以大概是表单渲染器里的一行:

   dependentValues: ruleRecord,
   error: fieldState.error?.message,
+  'aria-required': required || undefined,

—— 48 个 widget 一个都不用改。这正是 #3222 里拒绝把 required 布尔下沉的理由:下沉会给必填标记第二个作者(下一个 AI 写的 widget 自己画星号 → 双份显示),而 aria-required 直达 DOM,既拿到 a11y 收益又不新增契约键。

需要确认的点

  1. 红星的 aria-label="required" 是否同时保留?保留会让 AT 播报两次(名字里一次、状态一次)。倾向:把 span 改成 aria-hidden="true"(它是纯视觉冗余),让状态通道成为唯一来源。这是行为变更,需要拍板。
  2. 是否顺带给内置控件上原生 required?不建议 —— 会触发浏览器原生校验气泡,和 react-hook-form 的校验双轨。aria-required 只报状态、不触发原生校验,是正确的那个。

边界

packages/components/src/renderers/form/form.tsx 一行 + 测试。不动 packages/spec,不动任何 widget。

关联:objectui#3222、PR #3289。

Activity

  1. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    ContributorAuthor

    维护者裁决(2026-08-03)

    批准,进 v17 窗口。

    • 修法照本单:宿主 form.tsx 一行 'aria-required': required || undefined,48 个 widget 零改动。
    • 确认点 1:红星 span 改 aria-hidden="true",状态通道成为唯一来源(接受该行为变更,避免 AT 双重播报)。
    • 确认点 2:不加原生 required——避免浏览器原生校验气泡与 react-hook-form 双轨。

    摘 needs-user-decision,加 pm:queue。派发注意:与 #3233 都动 form.tsx,不得同批并行(串行或并单)。


    Generated by Claude Code


    Generated by Claude Code

  2. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    ContributorAuthor

    预认领(PM 会话 session_01NVPjPzmmAJ2Ngtvgg5MSRa)


    Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't workingpm:queue

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions