Uh oh!
There was an error while loading. Please reload this page.
fix(components): deliver aria-required to field controls (#3290) - #3298
Merged
Conversation
The form renderer computed a correct `required` (static flag merged with the `requiredWhen` CEL verdict) and spent it on one thing: the red asterisk in `<FormLabel>`. That asterisk carried `aria-label="required"`, so the ONLY path from the computed state to assistive tech was `<label for>` folding it into the control's ACCESSIBLE NAME — the field was announced "Title required". A state smuggled through a name is broken three ways: it is read in name order rather than announced as a state and "list the required fields" navigation cannot see it; a field rendered without a `label` (compact layouts, inline grid editing) draws no asterisk so the signal vanishes entirely; and `requiredWhen` makes required dynamic, which a state channel expresses and a name does not. The host now forwards `'aria-required': required || undefined` to the widget props. No widget changed: `aria-required` is already declared and typed on `FieldWidgetComponentProps & AriaAttributes`, every widget forwards leftover props to its control, and neither `stripRegisteredFieldProps` nor `stripRendererOnlyProps` touches `aria-*`. Verified against seven real widgets. The asterisk becomes `aria-hidden="true"` so the state is announced once, not twice, and picks up `data-required-marker` as an explicit locator — the one in-repo e2e that selected on its `aria-label` moves to that hook. Deliberately NOT the native `required` attribute: it would arm the browser's constraint-validation bubble alongside react-hook-form's messages. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 13:18
Uh oh!
There was an error while loading. Please reload this page.
akarma-synetal pushed a commit
to akarma-synetal/objectui
that referenced
this pull request
Aug 4, 2026
… five renderer sites outside the object form (objectstack-ai#3299) (objectstack-ai#3340) Five renderers computed `required`, painted a red asterisk, and put NO required state on the control — the same defect objectstack-ai#3290/objectstack-ai#3298 fixed in form.tsx, at the five sites its scope fence excluded. Four of them were worse than pre-fix form.tsx: the bare `*` had no `aria-hidden`, so the only "signal" was a literal asterisk folded into the accessible name ("Title asterisk"). Every site converges on the reference shape (EmbeddableForm.tsx:481,489): - control: `aria-required={required || undefined}` — absence, not "false", for optional fields; - asterisk: `aria-hidden="true"` — announced once, as a state, never as part of the name. Sites: - app-shell/ActionParamDialog: both branches (boolean row + default); the state rides the widget props and reaches the DOM through the fields' `toDomProps` whitelist, which forwards `aria-*` by prefix. - app-shell/CreateViewDialog: display label, machine name, and each type-specific required-field selector (statically required, so a literal `aria-required="true"`). - components/custom/action-param-dialog: all five typed branches; for select the state lands on the Radix trigger (the focusable combobox — the root renders no element). - components/custom/field: FieldContainer injects `aria-required` via the same Slot that already injects id/aria-describedby/aria-invalid — one line covers every consumer. Its asterisk is a CSS pseudo-element, which never enters the a11y tree, so no marker change is needed there. - plugin-detail/InlineCreateRelated: create-tab inputs. Deliberately NOT native `required` (objectstack-ai#3290 ruling): each host runs its own validation; native required would arm the browser's constraint- validation bubble beside it. The SDUI controls that already set native `required` (renderers/form/{input,textarea,select,checkbox}.tsx, basic/text-input.tsx) are excluded per the issue — no second validator there, so their channel is already correct. Tests per site, with mutation verification on the two load-bearing deliveries (FieldContainer's Slot injection; the app-shell dialog's widget-chain delivery): attribute removed -> red, restored -> green. Fixesobjectstack-ai#3299 Claude-Session: https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes#3290
必填状态过去只以「可访问名的一部分」存在——表单渲染器算出的
required(静态required+requiredWhenCEL)只驱动< FormLabel >里的红星,而那颗红星带着aria-label="required",于是状态经由< label for >关联被并进控件的可访问名,字段被读成「Title required」。把状态塞进名字有三处失效:按名字顺序朗读而非以状态播报、「只列必填项」这类导航看不见它、无label的字段(紧凑布局 / grid 内联编辑)根本不画星号因而信号彻底消失,且requiredWhen让必填是动态的——状态通道能表达翻转,名字不能。裁决执行清单
'aria-required': required || undefinedform.tsx转发进 widget 的 props(紧接error: fieldState.error?.message)aria-hidden="true"aria-label="required",状态通道成为唯一来源required前置核对(三条都是复核,不是假设)
field与schema是同一个意思的两个载体,~25 个 widget 读field || schema#3233(PR refactor(types,react,components,fields)!: converge the widget metadata carrier tofield, retiringschema(#3233) #3296)——worktree 基于origin/main@6e794a19e,packages/fields/src/withFieldCarrier.tsx存在;renderFieldComponent已是 field-widget / 裸名 SDUI 双路径的现状。aria-required——stripRendererOnlyProps与stripRegisteredFieldProps都是定长解构,aria-*不在其中,因此原样透传。不止读代码:临时跑了一个真 widget(EmailField / PhoneField / UrlField / TextAreaField / CurrencyField / PercentField / TextField)穿真表单渲染器的检查,七个全部把aria-required="true"落到真实的input/textarea元素上,零 widget 改动。该临时文件已删除,未进提交。< FormControl >确实不注入aria-required——packages/components/src/ui/form.tsx里的 RadixSlot只注入id/aria-describedby/aria-invalid。(附带更正 issue 正文一处措辞:全仓 greparia-required并非 0 处,plugin-form的EmbeddableForm和 console 的ProfilePage各有手写的一处;表单渲染器这条路径上确为 0,结论不变。)为什么是
|| undefined而不是String(required)非必填字段应当完全没有该属性,而不是
aria-required="false"。后者合法但语义更嘈杂,且requiredWhen翻成 FALSE 时必须产出的正是「属性消失」。测试里单独钉住了这个拼写。测试
新增
packages/components/src/renderers/form/__tests__/form-aria-required-delivery.test.tsx(9 条)。每条都做了 sabotage 验证——改坏见红、还原见绿:'aria-required'一行String(required)aria-label="required"requiredaria-required覆盖:内置分支、注册 widget 分支、非必填属性缺席、
requiredWhen动态翻转、无 label 的必填字段(老设计下信号完全不存在的那个场景)、可访问名不再含 "required"(用toHaveAccessibleName,不是 querySelector)、红星仍渲染但在 a11y 树外、无原生required。关于最后一条有个坑值得记:
toBeRequired()不能用来断言「没有原生 required」——jest-dom 把aria-required="true"也算作 required,那个 matcher 无论修成哪样都会绿。改为直接断言原生属性缺席 +el.required === false。顺带改到
packages/components之外的一处(是本改动自身的调用点修复,非范围外顺手修)e2e/live/field-conditional-rules.spec.ts的isRequired()helper 用span[aria-label="required"]定位红星——红星移出 a11y 树后这个选择器必然失效。红星因此获得data-required-marker="true"作为显式定位钩子(ADR-0054 C4 的稳定 locator 惯例),helper 改用它。a11y 属性不该兼任测试钩子,这正是这条aria-label能活这么久的原因。这是全仓唯一一处该选择器。给邻接单的两条提示
aria-label="required"字符串已随本 PR 消失——它现在是aria-hidden的纯视觉星号,不再有任何面向用户的文本需要翻译。届时请核对并从清单划掉。packages/fields/src/widgets/types.ts:29有一句注释仍在描述旧行为(「< FormLabel >里带aria-label="required"的*」)。按本单范围栅栏未动packages/fields;这是一处陈旧注释而非功能缺陷,留给维护者决定是单独收还是搭车下次改 fields 时顺手更正。changeset
已加
.changeset/aria-required-reaches-the-control.md(@object-ui/components: patch)。依据:AGENTS.md 说纯 bug 修复不必写,但本改动用户可见——每个必填控件多出一个 DOM 属性、可访问名从「Title required」变为「Title」——且移除aria-label="required"对任何按它选择的下游是破坏性的(本仓 e2e 就是活证)。定patch而非minor:无 API、无 props 契约、无 schema 变更,红星对视力用户的呈现一模一样。若维护者认为 DOM/a11y 行为变更应按仓规的「自身破坏性变更也标minor」处理,改一个字即可。验证输出
未动
packages/spec、任何 widget、packages/fields/types/react、content/docs/releases/。