Uh oh!
There was an error while loading. Please reload this page.
fix(site): Schema Catalog 卡片外壳去 button 化,示例预览不再嵌套按钮 (#3903) - #3964
Merged
Conversation
`SchemaCatalogIndex` 把每张卡片渲染成 `button`,而卡片里嵌的 `SchemaThumbnail` 用真实 `SchemaRenderer` 渲染示例本身 —— 目录 423 个示例里有 85 个含 `"type": "button"` 节点,于是这些卡片在 `/docs/guide/schema-catalog` 上落成 「按钮套按钮」。React 把它判为 hydration error 而非样式瑕疵:HTML 解析器会把内层 按钮提出外层,server HTML 与 client 树因此不一致;它同时本身就是可访问性缺陷。 改法两条: 1. 卡片外壳改为非交互 `div`(加 `relative`,边框/hover/间距类不变),点击目标改为 绝对定位的覆盖层 `button` 兄弟节点(`absolute inset-0`),`aria-label` 给出示例 标题与 id。缩略图与文字都不再位于任何交互控件的子树内。正文提到的 `div role="button"` 写法会把两个问题都留下,因此没有采用。 2. `SchemaThumbnail` 的缩放预览层加 `inert`。仅有覆盖层还不够:预览内部的控件依然 可聚焦,而缩略图框本身是 `aria-hidden`(`aria-hidden` 子树里存在可聚焦节点本身 就是 axe `aria-hidden-focus` 违规),且 DOM 顺序上预览在覆盖层之前 —— 实测修前 从第一张卡 Tab 出去依次落在 Submit / Save Draft / Delete / Cancel,才到第二张卡。 `inert` 同时移除聚焦与命中测试,这才让该组件文档里的「scaled, non-interactive preview」成立;原来的 `pointer-events: none` 只管住了鼠标。 浏览器实证(Chromium + next dev,滚动加载缩略图后统计):`button button` 节点 136 → 0,React 嵌套按钮控制台报错 2 → 0(hydration error 1 → 0),aria-hidden 预览 内可聚焦节点 180 → 0;抽 3 张卡实点仍正常打开对应 dialog,Tab 现在一卡一停。 结构钉放在 `scripts/__tests__/`:`apps/site` 没有 vitest 面(根配置 exclude `apps/**`,只注册了 `apps/console`),而缺陷形态是静态可判的 —— 用 TS AST 遍历 apps/site 全部 tsx,断言没有任何 schema 预览宿主落在 `button` 或 `role="button"` 的子树里,并自测检测器对四种合成样本的判定,避免空转绿。
The latest updates on your projects. Learn more about Vercel for GitHub. |
yinlianghui
commented
Aug 9, 2026
CollaboratorAuthor
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3903 落地。 核验记录(head
out-of-scope #3965(48 个示例用废弃 Generated by Claude Code |
yinlianghui
marked this pull request as ready for review
August 9, 2026 17:54
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 10, 2026
…bjectstack-ai#3998) `DivRenderer` 原先在每一次渲染里 `console.warn`。单个 `div` 的页面看不出问题, `div` 多的页面就被刷成噪声墙:docs 的 schema-catalog 索引页渲染 400+ 个示例缩略图, 实测刷出约 190 条完全相同的提示,把页面真正的报错埋在下面 —— objectstack-ai#3903 / PR objectstack-ai#3964 的两条 嵌套按钮报错就是从这堆噪声里捞出来的,浏览器实证也因此两次丢掉信号。 废弃本身一字未动:dev 构建照旧提示,文案与迁移建议不变,production 构建照旧静默, 去掉的只是重复。守卫是模块级 `Set`(按 type 记),production 的提前返回发生在标记之前, 所以一次 production 渲染不会吞掉后续 dev 构建的提示。 反向验证(先预判后执行):把无守卫的旧写法改回去,「production 静默」用例按预期保持 绿(旧写法同样按 NODE_ENV 门控),「只报一次」与「后续渲染不再报」两条按预期翻红 (9 条提示 vs 期望 1 条;后续渲染多出 1 条)。 注:本 PR 只完成 objectstack-ai#3965 的第二半(warn 去重)。第一半(catalog 48 个示例换掉废弃的 `div`)在现有组件词表下无法完整完成,已在 issue 上以证据升级给维护者定夺,见 PR 正文。 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#3903
问题
SchemaCatalogIndex把每张卡片渲染成button(修前:111),而卡片里嵌的SchemaThumbnail(:116)用真实SchemaRenderer渲染示例本身。目录 423 个示例里 85 个含"type": "button"节点,于是这些卡片在/docs/guide/schema-catalog上落成「按钮套按钮」。React 把它判为 hydration error 而非样式瑕疵:HTML 解析器会把内层按钮提出外层,server HTML 与 client 树因此不一致。它同时本身就是可访问性缺陷 —— 交互控件套交互控件,键盘与读屏行为未定义。
前提已在
origin/main@69becd2d1上复核:AST 扫描 apps/site 全部 17 个 tsx,命中恰好一处 ——SchemaCatalogIndex.tsx:116 SchemaThumbnail inside button@111,与 issue 正文与分诊评论完全一致。改法
1. 卡片外壳去 button 化。 外壳改为非交互
div(加relative,边框 / hover / 间距类不变),点击目标改为绝对定位的覆盖层button兄弟节点(absolute inset-0)。缩略图与文字都不再位于任何交互控件的子树内。正文提到的div role="button"写法会把 hydration 与 a11y 两个问题都留下,因此没有采用(结构钉也把它一并禁掉)。2.
SchemaThumbnail的缩放预览层加inert。 仅有覆盖层不够 —— 见下方焦点顺序实测。a11y 论证
button是卡片内唯一可聚焦节点,焦点顺序 = 卡片视觉顺序,一卡一停。修前 DOM 顺序上缩略图在前,从第一张卡 Tab 出去依次落在示例内部的 Submit / Save Draft / Delete / Cancel 才到第二张卡(实测轨迹见下)。inert同时移除聚焦与命中测试,这才让该组件文档里写的「scaled, non-interactive preview」成立;原来的pointer-events: none只管住了鼠标。另外缩略图框本身带aria-hidden,子树里存在可聚焦节点本身就是 axearia-hidden-focus违规。focus-visible:ring-2画在覆盖层上,而覆盖层inset-0解析到外壳的 padding box,因此视觉上仍是「整卡获得焦点」(修后截图里 Simple Login Form 那张卡的环即是)。aria-label为Open {title} ({id}),包含卡片上可见的标题与 id,满足 WCAG 2.5.3 label-in-name;role 由button元素本身提供。cursor-pointer(Tailwind v4 preflight 不再给 button 设 pointer)。浏览器实证
Chromium(
/opt/pw-browsers/chromium)+next dev,同一 dev server 上先复现后修复;缩略图是 IntersectionObserver 懒挂载,因此滚动加载后再统计。document.querySelectorAll('button button').lengthcannot be a descendant of/cannot contain a nested控制台报错This will cause a hydration error[inert]预览层修前报错样本(祖先栈直接点名 GalleryCard 的外壳 button 套住 ButtonRenderer 的 button):
点击导航实测(抽 3 张卡实点,dialog 的 aria-label 与卡片标题一致,Esc 关闭):
焦点顺序实测(聚焦第一张卡后连按 Tab):
截图与完整控制台日志留在 scratchpad(
3903-before.png/3903-after.png/3903-*-console.log)。测试
apps/site没有 vitest 面(根vitest.config.mts的sharedExclude含apps/**,projects只额外注册了apps/console),而这个缺陷形态是静态可判的,所以结构钉放在scripts/__tests__/—— 与site-playground-layout-registration-3904.test.ts同一高度、同一理由,且它在 CI 的根 vitest 分片里真的会跑(不是只能靠一个默认被 skip 的 docs-smoke e2e)。钉子用 TS AST 遍历 apps/site 全部 tsx:任何 schema 预览宿主(
SchemaRenderer/SchemaThumbnail/InteractiveDemo/LiveSplitDemo/SchemaExample)与任何button,都不得落在button或role="button"的子树里 —— 按发现而非硬编码文件清单,明天新加的第二个宿主会被同样抓到。防空转绿三重:检测器先对四份合成样本自测(修前形状、button 套 button、div role=button改写、覆盖层形状),再断言扫描确实覆盖到 ≥10 个文件且含卡片文件,并断言卡片文件里确实同时存在预览宿主与 button。最后一条 suite 钉inert+pointer-events-none。反向验证(方向在跑之前就已预判,两条都是朴素红):
其余门(仓根,全部在共享 verify 锁内串行):
changeset
无需 changeset:
.changeset/config.json把@object-ui/site列在ignore,且scripts/check-changeset-presence.mjs只守fixed组里每个包自己的src目录(即 包名 +/src/**);本 PR 改的是apps/site/app/**与scripts/__tests__/**,两者都不在守护面内。范围之外
content/**/*.mdx里的SchemaExample用法不在这条 AST 规则的扫描面(只扫 tsx),跨组件边界的交互性分析也不在 —— 缺陷本身与回归都会是同一文件内的 JSX 形状,更大的全图分析是另一件工具。button [aria-hidden]仍有 91 处:那是页面上其它 button(站点 chrome 与预览内部按钮)里的 aria-hidden 图标节点,与本单无关,非残留。