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. |
Collaborator
Author
|
PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3903 落地。 核验记录(head
out-of-scope #3965(48 个示例用废弃 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 #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 图标节点,与本单无关,非残留。