Uh oh!
There was an error while loading. Please reload this page.
fix(a11y): 给两处表单控件补上 label→control 关联 (#3341) - #3380
Merged
Conversation
InlineCreateRelated's create-tab label had no htmlFor and its Input no id; the custom action-param-dialog's select branch pointed htmlFor at an id that was never rendered on the SelectTrigger. Refs #3341
11 assertions across the two surfaces; all 11 go red when the htmlFor/id pairs are reverted. Measured pre-fix accessible name is the empty string (dom-accessibility-api implements no placeholder fallback), recorded in each test header rather than restating the issue's placeholder claim. Refs #3341
The latest updates on your projects. Learn more about Vercel for GitHub. |
The issue said the name degraded to the placeholder. That holds in a real browser (HTML-AAM last resort) but not in the test harness, where dom-accessibility-api implements no placeholder fallback and the name computed to ''. Say both, so the source comments and the test headers cannot be read as contradicting each other. Refs #3341
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 5, 2026 11:42
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 6, 2026
…aceholder (objectstack-ai#3410) InlineCreateRelated's "Link Existing" search Input had no label of any kind — no `<label>`, no `aria-label`, no `aria-labelledby` — so its accessible name fell through to the placeholder: the HTML-AAM last resort, gone the moment the user types, and absent entirely under `dom-accessibility-api` (measured pre-fix name here: the empty string). Visually hidden `<label htmlFor>` + a `React.useId`-namespaced id, the same shape objectstack-ai#3341/PR objectstack-ai#3380 left on the create tab, rather than `aria-label` — the name stays a real label element on the same expression as the placeholder instead of a detached string that can drift. The id uses a hyphenated `link-search` segment so it cannot collide with a create-tab field named `search`. The magnifier is explicitly `aria-hidden` (lucide already defaults to that for childless icons; spelled out so the intent is local). Rendered copy is unchanged: the placeholder string is byte-identical. Refs objectstack-ai#3381 Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt 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.
Fixes#3341
两处表单控件的 label 与控件之间根本没有程序化关联,导致控件的可访问名错误或缺失。这与 #3299(PR #3340)修的「必填状态不进 a11y 树」是相邻但不同类的缺陷:即便
aria-required已经到位,可访问名仍然是错的。前置核对(对当前 origin/main)
两处缺陷均仍然成立,行号已按合并后代码重新核对:
packages/plugin-detail/src/InlineCreateRelated.tsxL208-L227:label 元素无htmlFor,相邻的 Input 无id,两者也不是包裹关系。packages/components/src/custom/action-param-dialog.tsxselect 分支 L181-L193:Label htmlFor={param.name}指向的 id 从未被渲染出来 —— 该分支的控件是 Radix SelectTrigger,没有设id。其余 textarea / number / date / text 分支的 Input / Textarea 都设了id={param.name},只有 select 漏了。修法
htmlFor、Input 加id。id 用React.useId做前缀命名空间(inline-create-{uid}-{fieldName})——field.name只在单个实例内唯一,而一个详情页每个相关列表挂一个该组件,两个列表都提供name字段时裸用field.name会产生重复 id,htmlFor会一律解析到第一个输入框。id={param.name},让本就存在的htmlFor落到实处。SelectTrigger 渲染的是role="combobox"的 button,而 button 属于 labelable element,因此普通的htmlFor/id配对就是这里的正确关联方式,不需要aria-labelledby(Radix 也没有在 trigger 上设aria-labelledby,那个是 SelectItem 用的)。未动 spec,未动 widget props 契约。
测试
新增 11 条断言,两处各一个测试文件,覆盖验收口径:
getByLabelText命中控件 +toHaveAccessibleName等于 label 文本。反向验证(方向为事前预测的「常规红」):把两处
htmlFor/id还原成修复前的样子,11 条全红;恢复后全绿。失败信息正是缺陷本身 ——Found a label with the text of: Notes, however no form control was found associated to that label。一处与工单正文不符、按实测如实记录的细节:工单说可访问名「退化到 placeholder(Enter name)」。实测在本测试环境下修复前的可访问名是空字符串,不是 placeholder ——
dom-accessibility-api根本没有实现 placeholder 兜底(两个版本里placeholder出现次数均为 0),真实浏览器才会按 HTML-AAM 兜底到 placeholder。两种情况下「可访问名不是 label」这个结论一致,测试钉的也是这一条;但测试头注释里写的是实测到的空字符串,而不是照抄工单的 placeholder 说法。另外两条口径已在注释中写明,避免后来者踩坑:
getByLabelText用的是非必填字段。RTL 的 label-text 查询读的是原始textContent,不尊重aria-hidden,所以必填字段的 label 文本是Name*;必填字段改为在计算出的可访问名上断言(那里*被正确排除)。这两个通道的差异是真实存在的,不是测试瑕疵。labels集合。jsdom 会把 label 上的点击转发给控件的 activation behaviour,但不移动焦点,写toHaveFocus()等于在测 jsdom 而不是本组件。消费半径已扫过:两个组件除各自的测试与 barrel 导出外,没有其他包的 fixture 引用,不存在跨包 fixture 需要一并改。
已附 changeset(patch;按仓库约定不声明 major)。
Generated by Claude Code