Skip to content

docs(protocol): widget-contract 收口「谁渲染什么」——校验文案归宿主,required 只反映为 aria-required (#4866) - #5914

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-4866-widget-contract-docs
Aug 6, 2026
Merged

docs(protocol): widget-contract 收口「谁渲染什么」——校验文案归宿主,required 只反映为 aria-required (#4866)#5914
os-zhuang merged 1 commit into
mainfrom
claude/issue-4866-widget-contract-docs

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #4866

纯 docs,单文件:content/docs/protocol/objectui/widget-contract.mdx
packages/spec/src/ui/widget.zod.ts 一个字未改 —— 契约本来就是对的,收口的是教法。

说明:下文组件名都写成 < FormMessage / > 这种尖括号后带空格的形式 —— GitHub 正文消毒器会把 < 紧跟字母当 HTML 标签在存储时剥掉(objectui PR #3289 的正文第一版就被吃掉五处)。文件里写的是正常的行内代码。

真值核实(objectui origin/main 实测,不是照抄 issue)

objectui#3222 已按方向 1 落地(PR #3289,已合并),packages/spec 一个字未改。逐条对代码:

事实 实测出处
error字符串(消息本体),由表单渲染器从 fieldState.error?.message 产出,字段有效时为 undefined packages/components/src/renderers/form/form.tsx:1574
widget 读 error 用来驱动 aria-invalid;文案由 < FormMessage / > 渲染 packages/fields/src/widgets/types.ts:140-155 的 doc comment + form.tsx:1614
required 不在 objectui 的 widget props 里,读 props.required 是编译错误 packages/fields/src/__tests__/spec-symbol-batch7.test.ts_RequiredIsAbsent
必填状态走 aria-required,由渲染器直接注入,48 个 widget 一个没改 form.tsx:1608 'aria-required': required || undefined
必填标记 * 只有一个作者(宿主 < FormLabel >),且已移出可访问性树 form.tsx:1484

与 issue 表述的一处出入,已按实测写:issue 说「error 是布尔信号」。实测它是 string(spec 声明 error?: string,渲染器传的是消息文本)。所以文档写的是「当信号用,不是待渲染文案」——presence 驱动 aria-invalid,文案归宿主。写成「布尔」会与 FieldWidgetPropsSchema 的类型直接矛盾。

改了什么

  1. 示例删掉双份显示那一行({error && …})。在 fix(identity): close generic-write apiMethods hole on sys_presence & sys_metadata (#3220) #3222 之前 widget 拿不到 error,这行永远不执行,问题是隐性的;PR chore(lint): org-identifier authoring guard for deprecated session.tenantId (#3280 follow-up) #3289 让渲染器真的把它传下来之后,照这份文档写出来的第三方 widget 会把同一句话画两遍。
    顺带用上了一直被解构却从未使用required:aria-required={required || undefined} —— 与渲染器里完全同一个写法(|| undefined 而非 String(required),可选字段就不带这个属性)。删行不会造成未使用参数:error 仍被 aria-invalid={!!error} 读。
  2. required 一节措辞限定:「Indicate the required state visually and validate accordingly」→ 反映为控件上的 aria-required,不要自己画必填标记(宿主的 label 拥有 *)。
  3. 同一声明块里 error 的注释原文是「Validation error message to display」,与第 1 点是同一处失实的两半 —— 只删示例那行、留着这句「to display」,这份 PR 自己就自相矛盾了,所以一并改。这是第 1 点的完成范围内,不是顺手扩面。
  4. 新增「Who Renders What」一节:三项归属表 + 三条要点,含 PR chore(lint): org-identifier authoring guard for deprecated session.tenantId (#3280 follow-up) #3289 实测到的 spread 顺序陷阱(宿主的 < FormControl > 是 Radix Slot,本来就递正确的 aria-invalid;widget 在 spread 之后写一个来源不同的 aria-invalid 会把它覆盖成 false —— 七个内置 widget 就是这样从未播报过失效字段)。

:48 的「the source of truth is FieldWidgetPropsSchema保持不动 —— 它是排除方向 3 的关键证据。

验证

docs-only,没有可跑的单测,所以验的是「这页仍然构建得出来、且教的是实测到的事」:

  • pnpm check:doc-authoring362 files clean(含 self-test)
  • pnpm check:nul-bytesscanned 5704 tracked text file(s); no raw ASCII control bytes(含 self-test);另对本文件单独自扫 [\x00-\x08\x0b\x0c\x0e-\x1f\x7f],0 命中
  • pnpm check:docs-audit-scopescope is in sync with content/docs/: 178 hand-written doc(s)
  • pnpm --filter @objectstack/spec run check:skill-examples✅ 205 prose examples type-check(本页改动的两个块没有 {/* os:check */} 标记,也没新增标记;跑它是为了确认没有 orphan marker)
  • MDX 可编译:用仓里同一套 @mdx-js/mdx + remark-gfm 编译本页,42246 bytes JS,通过。
  • 第二条轴 + 反向验证:MDX 语法合法不能证明没有引入未定义组件 —— prose 里一个裸的 < FormMessage / > 语法完全合法,要到渲染期才炸。所以另写探针断言编译产物里的 _missingMdxReference 列表:本页只有 Callout/Card/Cards(改动前就在用),表格与正文里的组件名都留在行内代码里。探针自身先被反向验证过:第一版探针匹配错了模式(_components.X),对故意加了裸标签的副本报绿 —— 按预判方向应当转红,于是改为匹配 _missingMdxReference("X",副本转红(FAIL: FormMessage parsed as COMPONENT(s))、真文件保持绿。先有假绿被抓出来,这条绿才算数。

Changeset

无 —— 纯 docs,不发布任何包,按仓库纪律加 skip-changeset 标签。


🤖 Generated with Claude Code

https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE


Generated by Claude Code

…-required (#4866)

objectui#3222 按方向 1(objectui 跟随 spec)落地后(objectui PR #3289),
本页两处教法会让照抄它的第三方 widget 出现双份显示,按裁定收口。契约本身
是对的,`packages/spec/src/ui/widget.zod.ts` 一个字未改。

1. `CustomRatingField` 示例删掉 `{error && <span className="error">{error}</span>}`。
   在 #3222 之前 widget 拿不到 `error`,这行永远不执行,问题是隐性的;
   PR #3289 让表单渲染器真的把 `fieldState.error?.message` 传下来之后,
   宿主的 FormMessage 与 widget 会把同一句话画两遍。示例改为把 `error`
   只用于 `aria-invalid`,并顺手用上一直被解构但从未使用的 `required`
   (`aria-required={required || undefined}`,与 objectui 渲染器同一写法)。

2. `required` 的「Indicate the required state visually」改为限定表述:
   必填标记 `*` 由宿主的 FormLabel 拥有,widget 只把它反映成控件上的
   `aria-required`。这正是 #3222 决定不把 `required` 布尔下沉到 widget
   props 的理由(objectui#3290:真正需要的是 aria-required,不需要新增契约键)。

同一声明块里 `error` 的注释原文是「Validation error message to display」,
与第 1 点是同一处失实的两半,一并改成「信号而非待渲染文案」。

新增「Who Renders What」一节固定三项归属(aria-invalid 归 widget、文案归宿主
FormMessage、必填标记归宿主 FormLabel),并记下 PR #3289 实测到的 spread
顺序陷阱:宿主的 FormControl(Radix Slot)本来就递正确的 aria-invalid,
widget 在 spread 之后写一个来源不同的 aria-invalid 会把它覆盖成 false。

`:48` 的「the source of truth is FieldWidgetPropsSchema」保持不动。

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GX3sL71LFq8m2usg6VqTSE
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 6, 2026 11:31am

Request Review

@github-actions github-actions Bot added the size/s label Aug 6, 2026
@os-zhuang os-zhuang added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed size/s labels Aug 6, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Aug 6, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 6, 2026 11:41
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 00f2bcd Aug 6, 2026
28 of 29 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-4866-widget-contract-docs branch August 6, 2026 11:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(protocol): widget-contract 的示例教 widget 自己渲染校验文案——会双份显示;required 一节也需按 objectui#3222 的结论重写

2 participants