fix(page): one page, one h1 — page:header owns the title on every page type (#3434) - #3444
Merged
Merged
Conversation
…e type (#3434) PageRenderer rendered the page `title`/`label` as an `<h1>` on every non-record page, while an authored `page:header` block rendered its own `<h1>` in the header region. Both fire on the showcase master-detail page and both say "New Project + Tasks", so the document carries two level-1 headings with the same accessible name: a broken document outline, a title a screen reader announces twice, and a visible duplicate on screen. The live-e2e lane reported it as a Playwright strict-mode violation — `getByRole('heading', { name: 'New Project + Tasks' })` resolved to 2 elements, taking e2e/live/master-detail.spec.ts down in beforeEach. Record pages already delegated the whole title block to `page:header` ("the page:header component in the header region renders the record-bound title instead"). This states the same rule for every page type: when the author puts a titled `page:header` in a region, that component owns the page's h1 and PageRenderer emits none. Delegation is conservative — only a header whose title renders literal text counts. `page:header` drops an empty title and one that interpolates to nothing (`title: '{name}'` with no record in scope), so a page whose header renders no heading keeps its implicit one instead of ending up with zero. The page-level `description` is untouched: it is the page's own prose, not a duplicate of the header `subtitle`. The spec locator is left exactly as it was — it was an honest probe.
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
Collaborator
Author
live-e2e lane 实跑结果:4/4 绿 ✅run 31073012884 —— job "Live E2E (informational)" 全部步骤 success, 对照 #3434 里的首个真实 run(31069352699:2 passed / 2 failed):原先在 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 #3434
结论:是渲染侧的缺陷,spec 一行未改
issue 里两种可能(测试定位器过宽 vs. 应用侧重复渲染两个 h1),按 PM 的裁决用无障碍树证据判定 —— 证据指向渲染侧,
e2e/live/master-detail.spec.ts是一个诚实的探针,原样保留。无障碍树证据
失败页面是 framework 侧的
examples/app-showcase/src/ui/pages/project-workspace.page.ts:两个渲染器同时输出 level-1 标题,名字完全相同:
packages/components/src/renderers/layout/page.tsx:613(改动前)——pageType !== 'record' && (pageTitle || schema.description)时渲染class="text-3xl font-bold tracking-tight text-foreground"的页面级标题。PageView.tsx:131把 spec 的type: 'app'映射成pageType,所以这条对 app/home/utility 页恒成立;pageTitle = schema.title ?? schema.label取到label。packages/components/src/renderers/layout/containers.tsx:1359——page:header的非 record 分支(hasRecord === false,create 模式下没有 RecordContext 数据)在header元素里渲染class="text-2xl font-semibold tracking-tight"的标题。在新增的 DOM 测试里逐字复现了 live 现场:
—— 与 run 31069352699 的 strict mode violation 是同一对元素。不是条件互斥、也不是服务不同 region 的设计形状:两者同时出现在同一个文档、同一个页面上下文里,同名。一个文档两个 level-1 标题 = 文档大纲被打断、读屏把页面标题念两遍;当两个字符串相同时(showcase master-detail、app-showcase
my-work)屏幕上还直接看到重复的一行。这在 showcase/crm 里共影响 5 个已授权页面(my-work/contact-form/page-variables/project-workspace/ crmwelcome)。修法
page.tsx里原本就写着设计意图:也就是说「作者写了
page:header,标题就归它渲染」这条规则本来就存在,只是抑制条件被写死成pageType === 'record',漏掉了其余页面类型。本 PR 把它补成完整的规则:page:header→ PageRenderer 不再输出自己的隐式标题;record 页行为完全不变。page:header会丢弃空标题、以及插值后为空的标题(如无 record 在作用域时的title: '{name}',interpolate()把{field}置空)。否则抑制我们自己的标题会让页面一个 level-1 标题都没有,那是把一个 a11y 缺陷换成另一个。description不受影响:它是页面自己的正文,不是 headersubtitle的副本,交出标题不等于删掉别的内容。作者可见的行为变化:同时声明
label与带标题page:header的页面,只显示 header 的标题(如 crm 只剩 "Welcome to the CRM",不再额外显示 "CRM Welcome")。没有page:header的页面完全不变。验证
新增
packages/components/src/__tests__/page-single-h1.test.tsx—— 断言写在无障碍树上(getAllByRole('heading', { level: 1 })),而不是 class name,钉的正是 e2e 定位器依赖的那个事实。反向验证(方向按预期,先预测后执行):预测「把
page.tsx还原成 origin/main → 4 条正向用例转红,4 条负向对照保持绿」。实测一致:负向对照(「无 page:header」「header 无标题」「header 标题插值为空」「record 页」)前后都绿 —— 它们是防「零个 h1」的那一半,不能被这次改动带红,也不能因为改动而空过。
其余(均从仓根跑,
flock串行 +NODE_OPTIONS=--max-old-space-size=4096):pnpm exec vitest run packages/components/src/__tests__/page-single-h1.test.tsxpnpm exec vitest run packages/componentspnpm exec vitest run packages/app-shell/src/views/metadata-admin/previews packages/app-shell/src/utils/__tests__/pageSchemaIntrospect.test.ts packages/layout packages/plugin-detail/src/synthpnpm --filter @object-ui/components type-checkpnpm --filter @object-ui/components lintnode scripts/check-control-bytes.mjs/check-changeset-no-major.mjs/check-changeset-fixed.mjs消费半径排查:按「规则被谁消费」而不是「改了哪个包」扫了一遍 ——
page:header的其他消费方(app-shell metadata-admin previews、pageSchemaIntrospect、plugin-detail的buildDefaultPageSchema、packages/layout的 authorable-keys、consolepublic-contract)以及e2e/下所有 spec,均无断言依赖「页面级h1与 header 同时存在」。e2e/live/showcase-smoke.spec.ts只断main有内容,不用 heading 定位器。真正的验收是本 PR 上 live-e2e lane 的实跑(期望 4/4 绿),会在 run 完成后回读并补充。
Generated by Claude Code