Repository navigation
fix(components): header-bar root receives its BaseSchema DOM channels (ariaLabel, style, id, testId, data-obj-*) (#10496) - #10562
Conversation
…10496) Adds the objectui#10496 pin ahead of the fix. Through the real SchemaRenderer and registry, `ariaLabel`, `style`, `id`, `testId` and `data-obj-*` do not reach the `header` root today; an undeclared key and the keys the renderer consumes stay off the DOM as the control.
…#10496) The registered `header-bar` renderer read `schema` alone, so the props SchemaRenderer hands every component never reached the `header` root: an authored `ariaLabel` never named the banner landmark, and `style`, `id`, `testId` and `data-obj-*` rendered byte-identical to their absence. The root now takes the converged route: the props go through `toDomProps` (the SDUI whitelist in @object-ui/core) and `style` is forwarded by name, as the other converged renderers do. `schema.className` stays the one class channel read (objectui#10397); the `className` prop is taken off the pass-through by name so it cannot ride the spread as a second copy. objectui#10397's control row moves with it: an unauthored root now carries `data-obj-type` beside `class`, because SchemaRenderer injects it unconditionally and the whitelist forwards the open `data-*` family. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
…s no spread props (#10496) The refusal messages and doc comments for the retired header-bar keys explained each refusal with "takes no spread props". The renderer now forwards what the shared DOM whitelist admits, plus `style`, so the sentence says that instead. None of the retired keys is on the whitelist, so every refusal and its conclusion stand. Adds the changeset for @object-ui/components and @object-ui/types. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
CI red on
|
| tree | modules | time | stuck modules |
|---|---|---|---|
this PR's head 0b607015d |
448 | 527s | none |
origin/main d22b37bd8 (control) |
449 | 512s | none |
CI's own merge rebuilt (main at fde4cafbb + 0b607015d) |
449 | 512s | none |
The suite's testTimeout is 15s, so a slow test would fail rather than stall. No shard-4 file and no vitest.config.mts branch reads CI / GITHUB_ACTIONS. The step's own log is not retrievable from this container (its blob redirect is refused by the egress proxy). No fix exists, because nothing in this diff reproduces the stall.
Re-run: this seat has no job re-run op. The branch is behind main, so it gets one base merge (a merge commit carrying main, with no change of its own), and CI runs on the new head. If shard 4 stalls again on that head, it is treated as real and root-caused, ⛔ not re-run again.
Generated by Claude Code
Refreshes the PR against main; no other change. Claude-Session: https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #10496
Clause-②: no
What changed
The registered
header-barrenderer was a function ofschemaalone, so the propsSchemaRendererhands every component beside it never reached the rootheaderelement: an authoredariaLabelnever named the banner landmark, andstyle,id,testIdanddata-obj-*rendered byte-identical to their absence (objectui#10397 had made the root honourclassName, nothing else).The root now takes the converged route, the same one
grid.tsx,box.tsx,stack.tsxandflex.tsxtake (objectui#4435's): the props go throughtoDomProps(the SDUI whitelist in@object-ui/core) andstyleis forwarded by name. No per-key spread and no second route.packages/components/src/renderers/navigation/header-bar.tsx: the registration destructuresschema, theclassNameprop andstyle; spreadstoDomProps(hostProps)onto theheaderelement; setsclassNamefromschema.classNameexactly as objectui#10397 left it, andstyleby name. TheclassNameprop is taken off the spread by name and left unread, becauseclassNameis on the whitelist and would otherwise ride the spread as a second copy of the one class channel. No declared key reaches the DOM twice: the pin asserts each declared value lands on exactly one element, the root.header-bar-classname-10397.test.tsx: its control row moves, re-stated rather than deleted (below).header-bar-root-dom-props-10496.test.tsx, beside the other header-bar tests.@object-ui/typesprose (see "Beyond the claimed file surface")..changeset/10496-header-bar-root-dom-props.md:patchon@object-ui/componentsand@object-ui/types.The objectui#10397 control row: the new exact list and why
It asserted the root's attributes were exactly
['class']. It now asserts exactly['class', 'data-obj-type'](sorted, because the order React sets attributes in is not the contract), plusdata-obj-type="header-bar". Theclassvalue is still byte-identical to the pre-repair string.data-obj-typejoins becauseSchemaRendererhands every componentdata-obj-typeunconditionally, and the whitelist forwards the opendata-*family. Nothing else joins on an unauthored node:data-obj-idandidareundefinedwithout an authoredid(React omits them),aria-labelonly exists whenariaLabelis authored,data-testidis conditional ontestId, andstyleisundefined. Measured: with the fix and the old row, the row failed with exactlyexpected [ 'data-obj-type', 'class' ] to deeply equal [ 'class' ].The widget-dom-leak sweep: reading unchanged, file untouched
packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx, run with the root reverted to readingschemaalone (ablation leg 1 below, i.e. the pre-fix renderer): 230 of 230 green,ui:header-bargreen. On the fix: 230 of 230 green,ui:header-bargreen.data-obj-typeis an HTML-defineddata-*attribute, so it does not register as a leak, and the target's selector is still met by the shared readiness class. No ledger row, count or docblock in that file moves, so it is not in this diff.It does now guard this target against one more regression. Under ablation leg 2 (bare
{...hostProps}spread) the sweep'sui:header-barrow goes red, which it could not do while the root readschemaalone.Ablation: the pin has teeth, both ways
Both legs ran on the committed fix, through objectstack's
scripts/ablation-replace.mjs(literal anchor, must hit exactly once, blob hash checked on disk), wrapped in a driver with an absolute-pathtraprestore. Each restore was proven by blob:header-bar.tsxback toce3850854aef== HEAD, andgit diff HEADempty.headeropening tag replaced by the pre-fixclassName-only one. Anchor x1 to x0, blobce3850854aeftoe99ee6354277. Red:ariaLabel,style,id,testId,data-obj-*, lands-once and exact-list rows, plus the objectui#10397 control row, which reverts to['class']. Green: the harness row and the undeclared-key control. 8 failed of 246 across the three files.{...toDomProps(hostProps)}replaced by{...hostProps}. Anchor x1 to x0, blobce3850854aeftobca6ae4bf89e. Red: the undeclared-key control, the exact-list row, and the sweep'sui:header-bar. Green: every positive-channel row. 3 failed of 246. This is what shows the control can fail: it tells the whitelist apart from a bare spread.Before the fix, the new pin read 8 failed of 9. Only the harness row was green: every declared channel was absent from the root, one key per row, through the real
SchemaRendererand registry.Tests (all on
0b607015d, the head of this PR)pnpm exec vitest run packages/components/ packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsxfrom the repo root: 302 test files passed, 1 skipped; 3196 tests passed, 17 skipped; exit 0.pnpm exec vitest run packages/types/: 235 files, 5199 tests passed, exit 0.pnpm --filter '@object-ui/components^...' build(8 projects), thenpnpm --filter @object-ui/components --filter @object-ui/types type-check: both packages'type-checkscript names echoed,Done, exit 0.tsc -p tsconfig.test.json --listFilesin components does include both header-bar test files.pnpm check:control-bytesexit 0 (OK).pnpm check:new-line-citationsexit 0,0 new citation(s).node scripts/check-changeset-presence.mjsexit 0, 1 changeset declared.node scripts/check-changeset-no-major.mjsexit 0.pnpm check:changeset-claimsexit 0 (report-only). It flags four pending changesets that name a touched file: 6645, 6646, 6349 and 7917. I read each paragraph and all four are still true. 6645 describes, in the past tense,header-bar.tsxbefore crumb icons existed; the other three describeBreadcrumbSchema/BreadcrumbItemand their zod exports, which this diff does not touch.check:component-surface-parity,check:test-path-roots,check:pending-changeset-literals,check:handler-key-reads,check:icon-record-namesandcheck:vi-mock-specifiersall exit 0.--no-inline-config --format json, 5 results, none ignored), per rule against the basefee1da595(base content fed through--stdin --stdin-filenameat the same path): identical.header-bar.tsxhas 1react-refresh/only-export-componentswarning at base and head, and every other file is 0/0. The config enables no type-aware linting, so this diff cannot change eslint's verdict on any untouched file. The repo-widepnpm lintis left to CI.check:sdui-registration-pins, which exits 2 with "No console build to weigh at apps/console/dist/assets" (a prerequisite missing, not a red gate). Left to CI. The registration call, its file and thesideEffectsshape are unchanged.Beyond the claimed file surface:
@object-ui/typesprose this change made falseThe retirement texts for the
header-barkeys (title,logo,nav,left,center,right,sticky,height,variant) explained each refusal with "the renderer ... takes no spread props". That sentence is published, in nine zod refusal messages inpackages/types/src/zod/navigation.zod.tsand four doc comments inpackages/types/src/navigation.ts, and this change makes it false. All thirteen now say the renderer "forwards to its root only what the shared DOM whitelist admits plusstyle". None of those keys is on the whitelist, so every refusal and its conclusion still holds. The tests that pin those messages check theREFUSED (...)prefix and the four named read keys, and both still pass. A tree-widegit grepfor the old phrase now answers 0 hits; the control phrasereads no `title`hits both files. The claim's file surface did not list these two files; the report says so.Acceptance notes
content/docs/components/navigation/header-bar.mdxdocumentsclassNameunder Styling and listsid/testIdin its Schema block, but says nothing aboutariaLabelorstyle. Nothing on it is false, so it is untouched here; a sentence saying the base DOM channels reach the root would help authors.data-*reaches the root, a node that the dev-only structural check marks invalid carriesdata-obj-schema-invalidon theheader, as the other converged renderers already do. Not measured or pinned here.role,aria-describedby,tabIndexand handler keys on the whitelist reach the root by the same route. Only the channels the card names are pinned.Session:
https://claude.ai/code/session_01BA3nKVUwKQJf8DBxrSVtNC(domain:ui seat 1, dispatched by the PM claim on the card).Generated by Claude Code