Uh oh!
There was an error while loading. Please reload this page.
fix(fields,components): 字段 widget 的校验槽位改用 spec 的 error,并接上生产者 (#3222) - #3289
Merged
xuyushun441-sys merged 1 commit intoAug 3, 2026
Merged
Conversation
…as `error` (#3222) `@objectstack/spec/ui`'s `FieldWidgetPropsSchema` declares `error?: string`; `@object-ui/fields` declared `errorMessage?: string`. That looked like a naming split, but the slot was dead under BOTH spellings: nothing in packages/ or apps/ ever produced it, so the seven widgets computing `aria-invalid={!!errorMessage}` computed it from a permanent `undefined`. `aria-invalid` had never once been set on a failing field — and because a widget writes its own attribute AFTER the props spread, those seven actively OVERWROTE the correct `aria-invalid` that `<FormControl>`'s Radix Slot hands down. Two halves, both required: - the form renderer now passes `fieldState.error?.message` down as `error` when it renders a registered widget (the producer that never existed); - the slot adopts the spec's name across `FieldWidgetComponentProps` and the 7 widgets, with no alias kept. #3221 had already closed the type, so the compiler — not grep — validated the rename. Responsibilities stay split: the widget consumes `error` only to drive `aria-invalid` on the control it renders; the message text remains with `<FormMessage/>`. `required` is deliberately NOT lowered into widget props — the required marker has exactly one author (`<FormLabel>`), and the a11y state a widget could carry is `aria-required`, which needs no contract change. Builtin field types strip `error` instead: they render inside `<FormControl>`, whose Slot already supplies `aria-invalid`, so the prop would only reach the DOM as a stray attribute. Docs synced (plugin-development guide + skill, component.prompt.md — which also used the spec's non-generic alias as a generic and destructured a `mode` prop that exists on neither type). 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. |
This was referenced Aug 3, 2026
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
xuyushun441-sys
marked this pull request as ready for review
August 3, 2026 11:13
Uh oh!
There was an error while loading. Please reload this page.
xuyushun441-sys
deleted the
claude/issue-3222-field-widget-error-contract
branch
August 3, 2026 11:14
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#3222
按 PM 裁定的方向 1(objectui 跟随 spec),并且是「改名 + 接上生产者」两件一起。
packages/spec一个字未改。这修的不是命名,是一个当下就存在的可访问性缺陷
issue 的实测把这单的性质改了:
这个槽位在两种拼写下都是死的。 后果比「纸面不一致」严重一档 —— 而且比 issue 里描述的还要糟:
< FormControl >是 RadixSlot,它本来就把正确的aria-invalid递给子元素。但 widget 自己的aria-invalid={!!errorMessage}写在{...props}展开之后,所以它覆盖了 Slot 给的正确值。也就是说这 7 个 widget 不只是「没拿到信号」,而是主动把对的值改写成了false。变异验证时抓到的真实 DOM 就是这个现场 ——
aria-describedby已经指向了错误消息(表单知道这个字段挂了),而aria-invalid是false:反证也成立:
TextField从不自己算aria-invalid,Slot 的值原样到达,它一直是对的。坏的恰好只有那 7 个自己算的。这条已作为测试钉住。改了什么
fieldState.error?.message作为error传下去。errorMessage→error,FieldWidgetComponentProps+ 7 个 widget,不保留别名。FieldWidgetComponentProps的[key: string]: any让每个 widget prop 检查失效 #3221 已删索引签名,所以漏改点是编译错误 —— 改名的正确性由编译器验证,不是靠 grep。error驱动aria-invalid;校验文案仍由< FormMessage / >渲染。有测试钉住「文案只出现一次」。error:它们直接渲染在< FormControl >里,Slot 已给aria-invalid,这里没有读者,传下去只会变成 DOM 上一个多余的error="…"属性 —— 所以在stripRendererOnlyProps里剥掉。required:评估后维持现状,不下沉按要求单独评估,结论是不接,理由不是「工作量大」:
< FormLabel >(那个带aria-label="required"的*,label 关联会把它并进控件的可访问名)。把 flag 递给 widget 就等于给这个标记第二个作者,下一个 AI 写的 widget 很自然会自己画一个星号 —— 这正是本 PR 里让校验文案留在< FormMessage / >的同一个「双份显示」理由。两处应当同一结论。aria-required的必要条件:FieldWidgetComponentProps已经& AriaAttributes,aria-required本来就是这份契约里已声明、已类型化的键,而且每个 widget 都已经把剩余 props 转发到控件上。也就是说「让 required 状态到达 DOM」不需要新增任何契约键,更不需要 48 个 widget 各自记得把布尔翻译成属性。required」和「拿到aria-required」是两件事,只有后者是 a11y 修复,而后者是一个独立的一行改动,不依赖本 PR。已另开 issue 记录,未指派。结论:接了会让契约更差(多一个双份显示的入口),不接也不妨碍真正的 a11y 目标。 不为对齐而对齐。
测试
packages/components/src/renderers/form/__tests__/form-error-delivery.test.tsx—— 生产者半边(探针 widget 刻意不展开剩余 props,否则 Slot 的aria-invalid会让测试假绿)。packages/fields/src/__tests__/widget-aria-invalid-e2e.test.tsx—— 端到端:真表单 + 真 widget + 真 react-hook-form 校验,7 个 widget 各一条「失败后aria-invalid为 true、失败前为 false」。validation-feedback/widget-props-contract/spec-symbol-batch7的钉子随之翻面(_ErrorIsAbsent撤销,新增_OldNameRetired钉住没有保留别名)。变异验证(硬要求):撤掉
error: fieldState.error?.message一行后,15 条里 12 条转红;恢复后 61 文件 / 615 测试全绿。已知取舍
不读
error的已注册 widget 会把它当剩余 prop 展开到 DOM,失败时留下error="T is required"属性(实测 0 条 React 警告,因为是全小写)。这是有意的:按 widget 类型做白名单会对第三方 widget 重建「声明了但没交付」,而契约恰恰是为它们存在的。同一个 DOM 上早就有更严重的一例(schema="[object Object]"),已另开 issue。文档同步
content/docs/guide/plugin-development.md/skills/objectui/guides/plugin-development.md:改名 + 新增「谁渲染什么」的职责表;顺带修掉 skill 里仍在教的[key: string]: any(FieldWidgetComponentProps的[key: string]: any让每个 widget prop 检查失效 #3221 已删)。.github/prompts/component.prompt.md:除改名外修掉 issue 点名的另两处 —— 把 spec 的非泛型类型别名当泛型用(FieldWidgetProps< number >),以及解构一个两边都不存在的modeprop。content/docs/protocol/objectui/widget-contract.mdx在 objectstack 仓,不在本仓 —— 已另开 issue 跟进(它的示例目前教 widget 自己渲染文案,按本 PR 的结论应改成只驱动aria-invalid)。Changeset:
minor(按仓库版本策略,objectui 自身的破坏性变更也标 minor),正文含 FROM → TO 迁移指引。🤖 Generated with Claude Code
https://claude.ai/code/session_01NVPjPzmmAJ2Ngtvgg5MSRa