Repository navigation
fix(plugin-detail): name the activity panel's landmark by the heading it draws, not 'Discussion' - #10237
Conversation
… it draws
RecordActivityTimeline named its section landmark with a fixed
t('detail.discussion') while the visible h2 read
titleLabel ?? t('detail.activity'). A record:activity mount passes no
titleLabel, so its region was spoken "Discussion" under a heading
reading "Activity (N)" - against the objectui#4118 rule that the
landmark's spoken name must be the heading. record:chatter passes
titleLabel = discussion, so it agreed; that asymmetry is the control.
The section now takes its name from the heading's title text via
aria-labelledby (the (N) count stays visible and out of the name), so
an authored titleLabel names both.
- new pin: both mounts plus an authored titleLabel
- objectui#4645 pin: premise sentence corrected and dated; the ten
locale cases now assert detail.activity via the accessible name
- objectui#9556 pin: reads the inner section's name off the a11y tree
instead of an aria-label attribute selector
… not aria-labelledby aria-labelledby needs a per-instance id (record:activity and record:chatter can share a page), and React.useId() differs on every mount. Two instruments byte-compare two mounts' HTML and went red on it: the record-block reach probe (apps/console) and the record:chatter alias pin (app-shell). The section's aria-label now reads the same `title` value the heading prints - one value, both places - which is the objectui#4118 precedent's own shape. The objectui#9556 pin returns to its base bytes (its section[aria-label] selector holds again), and the objectui#4645 pin keeps reading the attribute, now expecting detail.activity.
…ies, dropping the any cast
✅ 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
|
Contract reviewServed-tier: Rendered by an isolated review subagent spawned by this seat. It was fed only the card, the PR and the two trees. Its served tier was checked by this seat against the stamp in its transcript. Adopted by this seat. ① Derived judgments
② Semver level
Two items were sent back for a prose-only re-review:
③ Boundary flags
Implemented-by: VERDICT: PASS |
…the #9998 names as accessible names Prose only, frontmatter untouched in both files. - record-detail-builtin-label-i18n-4645: the timeline sentences stop publishing what objectui#9998 proves false. Its section always had a visible heading, and its landmark is named by that heading's title (detail.activity, or an authored titleLabel). The HeaderHighlight half is unchanged in substance. - 9998-activity-landmark-named-by-heading: the unmeasured AT "announced" phrasings become the measured accessible names, and the closing paragraph no longer describes the #4645 entry as still saying what this commit corrects.
✅ 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
|
Contract review — prose-only re-reviewServed-tier: ② Changeset prose
The one claim dropped (that #4645 routed the timeline through
③ Boundary flags
Rendered by an isolated review subagent spawned by this seat; its served tier was checked against its transcript stamp. Adopted by this seat. Implemented-by: VERDICT: PASS |
…ivity-landmark-name
✅ 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 #9998
Clause-②: no
What was wrong
RecordActivityTimelinenamed itssectionlandmark with a fixedaria-labelfromt('detail.discussion'). The visibleh2inside that section readstitleLabel ?? t('detail.activity')followed by an(N)count.record:activitypasses notitleLabel, so its region's accessible name was "Discussion" under a heading reading "Activity (N)".record:chatterpassestitleLabel={t('detail.discussion')}on both ofRecordChatterPanel's paths (sidebar and inline), so its heading and landmark agreed. That difference between two mounts of the same component is the control.The contract is this repo's own objectui#4118 pin, in
ReportConfigPanel.panelTitle.test.tsx: "A speech-input user says what they see, so the landmark's spoken name must be the heading, not a paraphrase of it."The change (net diff against base
62597c58: 5 files — the claimed landing site, plus the objectui#4645 changeset's prose correction the seat added in round 2)packages/plugin-detail/src/RecordActivityTimeline.tsx: the component readsconst title = titleLabel ?? t('detail.activity')once and uses it for both the heading's title and the section'saria-label. The heading's markup is byte-identical to base; the only DOM change is the value of the section'saria-label. ThetitleLabeldoc comment now says it names both.packages/plugin-detail/src/__tests__/RecordActivityTimeline.landmarkNamedByHeading-9998.test.tsx, read through the realenpack and the accessibility tree:record:activity(real renderer, notitleLabel): the landmark is named "Activity", the heading's title, and no region named "Discussion" exists;record:chatter, inline path and sidebar path: still "Discussion" = heading (the control);titleLabel("Deal history") names both the heading and the landmark.detail-landmark-aria-i18n-4645.test.tsx): the premise sentence is corrected and dated, and none of its 20 assertions were deleted. Its ten timeline cases now expect each pack'sdetail.activitywhere they used to expectdetail.discussion. The ten HeaderHighlight cases are unchanged..changeset/9998-activity-landmark-named-by-heading.md:@object-ui/plugin-detailpatch..changeset/record-detail-builtin-label-i18n-4645.md: prose-only correction of its timeline sentences (round 2, commit31542304), frontmatter untouched.Mechanism decisions (dispatch Zone 2, each measured)
Premise: re-read at
62597c58and confirmed. The fixedaria-labelon the section ist('detail.discussion'); the heading readstitleLabel ?? t('detail.activity');RecordChatterPanelpassestitleLabel={t('detail.discussion')}on both paths. The failing pin reproduced the defect before any source edit (see Evidence).aria-labelledby, the card's suggestion, was tried first, then measured and dropped. It shipped in the first commit,9eb61082. It needs a per-instance id, because arecord:activityand arecord:chattercan share one page and a fixed id would collide. The only such id isReact.useId(), and its value differs on every mount. Two existing checks compare two mounts' HTML byte for byte, and both went red on that id alone (_r_0_against_r_6_):apps/consolerecord-block-record-reach.test.tsx, whose stability control failed with the message "Find the unstable output (an id, …)";@object-ui/app-shellpalette-discussion-alias.test.tsx, where the alias and the canonical block's HTML must be identical.Both are outside this card's file surface, and loosening either would weaken it. The final route is the objectui#4118 precedent's own shape: an
aria-labelequal to the heading text, read from one variable. The name and the heading cannot drift, and output is deterministic.The
(N)count is excluded from the name. It is live status:filtered.lengthmoves with the filter dropdown, and it is styled as secondary text. People say "Activity" to name the panel, not the count. The card itself treatsrecord:chatter's "Discussion" landmark under a "Discussion (N)" heading as agreeing, and that is only true with the count left out. Including it would turn the card's own control into a second instance of the defect. The pin still asserts that the count is on screen.Authored
arianames: no caller can pass one into the timeline, because it has no aria prop. As objectui#9946's closed measurement found, a block's authoredariabag lands on the renderer's outer container throughuseRecordAriaProps. The objectui#9556 composition pin (recordComponentAria-9556.test.tsx) is green and unchanged: its selectorsection[aria-label]still matches, and the inner name "Activity" is not the authored one.The objectui#4645 premise sentence was false on the day it was written, not just today. The card said it was once true. The section and its
h2were introduced together in86c04f1ff2(2026-05-21, the de-boxing that replaced the Card), and the console i18n residual on 17.0.0 GA: built-in record-detail tabs untranslated in ja-JP / es-ES (zh-CN fine), plus two English aria-labels in every locale #4645 pin landed in8033ad14d6(2026-09-09). I read the file at both shas through the contents API.axe-core / vitest-axe: not used. A pin built on them could not go red for this defect. objectui#9946's axe-core 4.13.0 run (wcag2a/2aa/21a/21aa/22aa/best-practice) on the unfixed tree, with "Discussion" under "Activity (N)", found zero landmark violations. No axe rule relates a region's name to its visible heading.
Evidence (final head
95fe1497unless stated)Tests 2 failed | 2 passed (4).record:activity: expected accessible name "Activity", received "Discussion". AuthoredtitleLabel: expected "Deal history", received "Discussion". Both chatter controls passed.95fe1497, from the repo root:pnpm exec vitest runover the new pin, the console i18n residual on 17.0.0 GA: built-in record-detail tabs untranslated in ja-JP / es-ES (zh-CN fine), plus two English aria-labels in every locale #4645 pin, the finding(plugin-detail):ariais declared on six record component interfaces and read by one of them — whilerecord:quick_actions, the renderer that reads it hardest, has no declared props interface at all #9556 pin, the six app-shellRecordDetailViewdiscussion/activity tests,palette-discussion-alias,canvas-display-meta,PagePreview, consolerecord-block-record-reach, and i18nraw-key-call-sites-3546. Result:Test Files 14 passed (14),Tests 170 passed (170), lockVERDICT command-exit 0.pnpm exec vitest run packages/plugin-detail/at066dbc32:Test Files 198 passed (198),Tests 1989 passed (1989).95fe1497differs from it only in the new pin's fixture typing (satisfiesinstead of anas anycast), and the union above ran that file at95fe1497.pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-detail^...' build(exit 0), then ranpnpm --filter @object-ui/plugin-detail type-check(tsc --noEmit && tsc -p tsconfig.test.json, exit 0) on the95fe1497tree.tsc -p tsconfig.test.json --listFilesOnlylists all three touched.ts/.tsxfiles.95fe1497:scripts/ablation-replace.mjsput the oldaria-label={t('detail.discussion')}back in place ofaria-label={title}. The mutation landed on disk (anchor 1 to 0, blob3d38c741toabcc219a) and ran with a restore trap. Result:Tests 12 failed | 12 passed (24).record:activitycase, the authored-titleLabelcase, and all ten console i18n residual on 17.0.0 GA: built-in record-detail tabs untranslated in ja-JP / es-ES (zh-CN fine), plus two English aria-labels in every locale #4645 timeline locale cases.git diff HEADis empty,git statusis clean. The test subject is imported by relative path from source, not through a packageexportsentry, so no build step was needed.95fe1497, all exit 0:pnpm check:control-bytespnpm check:new-line-citations: 0 new citationsnode scripts/check-changeset-presence.mjspnpm check:changeset-claimsnode scripts/check-vi-mock-override-shape.mjsnode scripts/check-test-path-roots.mjspnpm check:i18n-keys,pnpm check:i18n-driftandpnpm check:pending-changeset-literals, run at066dbc32; the last commit touched no call site.eslint .inpackages/plugin-detail(the package's ownlintscript): 256 files per the JSON output, 0 errors. The config has no type-aware linting, so this diff cannot change a verdict on an untouched file. The three touched files also pass with--max-warnings 0.Acceptance notes
31542304), at the seat's direction in round 2: the review-tier record found it would otherwise publish a sentence this PR proves false.aria-labelledby+useIdshape cannot be used by anyrecord:*block while the two byte-compare checks named above stay as they are. This is recorded as an observation, not a proposal.Spec Main Shape Gateis red, that is the known objectui#10229 drift on test files this change does not touch.Generated by Claude Code