fix(app-shell): InspectorComboField 的 label 关联 trigger,无名 combo 不再可编译 (#3997) - #4012
Merged
Merged
Conversation
…#3997) 第四个字段原子,与 PR #3996 修掉的 `_shared.tsx` 三个原子形状完全相同 —— `Label` 是控件的兄弟节点,没有 `htmlFor`,trigger 没有 `id` 也没有 `aria-label`。 它在自己的模块里,所以那三个修好之后它仍旧带着缺陷。标签与 `button[role=combobox]` 之间只有视觉邻接:焦点落上去读到匿名 combobox,可见标签是 一段无归属文本,`getByLabelText` 到不了它,点标签什么都不会发生。非测试调用点 18 处 (对象字段 / 数据集 / 仪表盘部件 / 应用导航 / 视图变体检查器),打开任一面板即渲染。 带 label 分支照 #3994 的定型机制闭合:`React.useId()` 在原子内部生成 id,`Label` 补 `htmlFor`,id 落到 `PopoverTrigger asChild` 渲染出的那个 `Button`。⛔ 不落 `Popover` —— Radix `Popover.Root` 是纯 context provider,不渲染 DOM,给它的 id 会被静默丢弃、 `for` 随之悬空(#3976 / #3994 已为此付过两次学费)。 ## 无 label 分支:命名改成类型级要求,而不是消费端兜底 `label` 原本可选,无 label 分支是同缺陷更重一档 —— combobox 完全无名,而 18 处调用点 里有 5 处正是这么写的。没有采用宽松兜底(用 placeholder 合成名字会把「Select…」念成 字段名);改为「三条通道恰选其一」,零条和两条都不可编译: - `label` —— 原子渲染可见标签并自持关联。已传 label 的 13 处不变。 - `ariaLabel` —— 重复行里本就没有可见标签、加一个会破坏栅格:应用导航 URL 过滤条件的 `field = value` 行、数据集的 join 列表、依赖查找的「添加字段」选择器。 - `id` —— 外部 `Label htmlFor` 已经持有命名。`DashboardWidgetInspector` 的 `Field` 包装器渲染 `Label htmlFor={id}` 并把同一个 id 交给被包控件,其余字段都履行了这个约定 (`Input id`、`SelectTrigger id`),只有 dataset combo 落不下去,因为原子不收 id —— 那个 `for` 指向一个没有任何元素持有的 id,是悬空 IDREF,比无标签更糟,因为工具会报告 一个解析不到的关联。 两条通道同时给出是 #3961/#3978 要避免的重复播报,故也一并禁掉。两种错误都没有组件能 自行发现并报告的运行期症状 —— 无名 combobox 渲染、布局、提交值全都正常,只对看不见它 的用户是错的 —— 所以检查只能发生在编写期,否则就不存在。 ## 钉子与反向验证 - `_shared.labels.test.tsx`:第四原子直接加入既有的 `describe.each`(它已把「for 有 宿主 / 命中可聚焦控件 / 可访问名 / 单一命名通道 / 多实例不撞车」写成与组件无关的形 状),另加 trigger 落点、自定义值、`ariaLabel` 分支、外部 `id` 分支四组。 - `DashboardWidgetInspector.test.tsx`:真实调用点上钉 Dataset 标签解析到 combo trigger。 刻意只锁这一对,不做整面板「无悬空 for」扫描 —— `widget-color` 的 `ColorVariantPicker` 同样不收 id,那是另一个组件、本单范围外,已单独立单 #4010。 - `InspectorComboField.naming.types.test.tsx`(新增,列入 `tsconfig.typetests.json`): 类型级断言。第一稿把 `@ts-expect-error` 写在 `_shared.labels.test.tsx` 里,变异实测 显示那是**假绿** —— 把命名改回可选,`tsc --noEmit` 完全通过,因为包的构建 tsconfig 排除 `**/*.test.tsx`、vitest 又擦除类型,没有任何编译器读它。这正是 #3009 的失效模式, 故移入被 typetests 项目编译的独立文件;同一变异现在报 6 条错(4 条 assignability 断言 + 2 条 unused `@ts-expect-error`)。 反向验证三向(先预判后跑,变异未提交):撤 `htmlFor` → combo 8 行翻红,其中「mints exactly one owner for the id」按预判保持绿(id 还在,只是没人指向它);id 改落 `Popover` Root → 11 行翻红(多出 owner 计数、外部 id 分支、真实调用点钉);命名改回可选 → 运行期 全绿、信号只在 typetests。 Fixes #3997 Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3997 落地 —— 检查器第四个字段原子 InspectorComboField 闭合,a11y label 家族在 metadata-admin 面收口。 核验记录(head 实物 + CI 亲读):
Generated by Claude Code |
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 free
to 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 #3997
第四个字段原子,与 PR #3996 修掉的
_shared.tsx三个原子形状完全相同:Label是控件的兄弟节点,没有htmlFor;trigger 没有id,也没有aria-label兜底。它住在自己的模块里,所以那三个修好之后它仍旧带着缺陷。标签与button[role=combobox]之间只有视觉邻接:焦点落上去读到的是匿名 combobox,可见标签是一段无归属文本,getByLabelText到不了它,点标签什么都不会发生。非测试调用点 18 处(对象字段 / 数据集 / 仪表盘部件 / 应用导航 / 视图变体检查器 —— lookup 显示与描述字段、
lookupFilters行、汇总聚合、数据集维度与度量、导航目标),打开任一面板即渲染,不是 dormant。带 label 分支
照 #3994 的定型机制闭合:
React.useId()在原子内部生成 id,Label补htmlFor,id 落到PopoverTrigger asChild渲染出的那个Button。⛔ 不落
Popover—— RadixPopover.Root是纯 context provider,不渲染任何 DOM 元素,给它的id会被静默丢弃、for随之悬空。这是 #3976(PR #3992)与 #3994(PR #3996)已经付过两次学费的同一机制,所以这次把「落点在 DOM 真实元素上」单独钉成一条断言,而不是靠注释约束。无 label 分支:命名改成类型级要求,而不是消费端兜底
label原本可选,无 label 分支是同缺陷更重一档 —— combobox 完全无名(既没有for也没有aria-label),而 18 处调用点里有 5 处正是这么写的。没有采用宽松兜底:用
placeholder合成名字会把「Select…」念成字段名,而「消费端容忍」正是 AI 写出的元数据错误藏身与繁殖的地方。改为「三条通道恰选其一」,零条与两条都不可编译:labelariaLabelfield = value行、数据集 join 列表、依赖查找的「添加字段」选择器、仪表盘部件的按过滤器绑定行idLabel htmlFor已经持有命名DashboardWidgetInspector的widget-datasetid这一条是被真实站点逼出来的:DashboardWidgetInspector用一个Field包装器渲染Label htmlFor={id}并把同一个 id 交给被包控件,其余字段都履行了这个约定(Input id、SelectTrigger id),只有 dataset combo 落不下去,因为原子根本不收 id。那个for指向一个没有任何元素持有的 id —— 悬空 IDREF,比无标签更糟,因为工具会报告一个解析不到的关联。两条通道同时给出是 #3961/#3978 要避免的重复播报,故一并禁掉。
之所以把它做成类型而不是运行期守卫:两种错误都没有组件能自行发现并报告的运行期症状 —— 无名 combobox 渲染、布局、提交值全都正常,只对看不见它的用户是错的。所以检查只能发生在编写期,否则就不存在。
反向验证(先预判方向,再跑变异;变异均未提交)
三个方向都实测了,其中第三个的预判是错的,写在这里而不是抹平:
htmlFor→ 预判 combo 各行翻红。实测 8 行红,其余三原子与InspectorCheckboxField保持绿。预判里特意点出「mints exactly one owner for the id应当保持绿」(id 还在 Button 上,只是没人指向它)—— 实测确认,该行是这一变异唯一测不到的行。PopoverRoot → 预判 combo 各行红,且外部id分支一并红。实测 11 行红:上面 8 行 +mints exactly one owner(id 被整个丢弃,owner 计数 1 到 0)+ 外部id分支 +DashboardWidgetInspector真实调用点的钉子。Radix 不报任何警告,静默丢弃如注释所述。@ts-expect-error翻红。实测完全绿 —— 假绿。 第一稿把@ts-expect-error写在_shared.labels.test.tsx里,而包的构建 tsconfig 排除**/*.test.tsx、vitest 又擦除类型,没有任何编译器读它:那两条指令既不会在错误消失时失败,也不会在错误从来不存在时失败。这正是 build(types): type-check this package's tests, so the spec-derivation guards actually run (objectstack#4074) #3009 的失效模式(断言从未运行,而文件头声称它们是「真正的强制」),tsconfig.typetests.json自己的头注释也在警告同一件事。于是类型级断言移入独立文件InspectorComboField.naming.types.test.tsx并列入tsconfig.typetests.json;同一变异现在报 6 条错(4 条 assignability 断言 + 2 条 unused@ts-expect-error)。钉子
_shared.labels.test.tsx—— 第四原子直接加入既有的describe.each(它已把「for 有宿主 / 命中可聚焦控件 / 可访问名 / 单一命名通道 / 多实例不撞车」写成与组件无关的形状,这正是按名册而非按组件写的回报),另加四组:trigger 落点(含 owner 计数)、自定义值仍有名、ariaLabel分支、外部id分支。DashboardWidgetInspector.test.tsx—— 真实调用点上钉 Dataset 标签解析到 combo trigger。刻意只锁这一对,没有做整面板「无悬空 for」扫描:widget-color的ColorVariantPicker同样不收 id,那个for也悬空、且它的 radiogroup 完全无可访问名 —— 另一个组件、本单范围外,已单独立单 [app-shell] DashboardWidgetInspector 的 Field(id="widget-color") 的 for 悬空 —— ColorVariantPicker 不收 id,颜色 radiogroup 完全无可访问名 #4010(未认领,交 triage)。整面板扫描会把它锁成红,或更糟,锁成一条将来被「修绿」的假断言。InspectorComboField.naming.types.test.tsx(新增)—— 类型级断言,来历见上。范围
主体是
InspectorComboField这一个已核实站点 + 其无 label 分支的命名兜底。issue 正文附的 18 处宽面清单不在本 PR(逐处「Label 下面到底是不是控件、还是纯分组小标题」未核)。实施中顺手核实到的两处判断已回传给 PM,代码未动。新增一对 i18n 字符串(
engine.inspector.widget.filterBindingField,en-US + zh-CN),给仪表盘部件里按过滤器的绑定 combo 命名 —— 它上方那个标题同时统辖一个 checkbox 和这个 combo,是行级标题而非控件标签,故不去改动它的归属(那正属于宽面 sweep 要裁定的事)。验证
pnpm exec vitest run packages/app-shell --maxWorkers=2—— 312 文件 / 2921 通过 / 1 跳过。pnpm exec turbo run type-check --concurrency=2—— 78/78 成功。node scripts/check-control-bytes.mjs、check-type-check-coverage.mjs、check-i18n-call-site-keys.mjs、check-i18n-en-drift.mjs全绿;@object-ui/app-shelllint 0 error。Generated by Claude Code