Skip to content

fix(plugin-detail,i18n): announce record:path stage state in the accessible name - #5958

Merged
os-warren merged 1 commit into
mainfrom
claude/issue-5916-record-path-accessible-state
Aug 24, 2026
Merged

fix(plugin-detail,i18n): announce record:path stage state in the accessible name#5958
os-warren merged 1 commit into
mainfrom
claude/issue-5916-record-path-accessible-state

Conversation

@os-warren

@os-warren os-warren commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Fixes #5916

record:path distinguished travelled, upcoming and lost-terminal stages with colour plus a / glyph, and both glyphs are aria-hidden decoration. aria-current="step" marked the current stage and nothing else, so a screen-reader user heard a run of identically-announced items — and a lost terminal announced exactly like an ordinary stage the record had not reached yet. WCAG 2.2 SC 1.4.1 Use of Color.

Premise check (the card was filed from inside a rewrite of this file)

The card was written while #5917 (render record:path as a readout, not a fake segmented control, closing #5768) was in flight, and its JSX snippet is not current source. Re-read against the landed file at 0b12a3315, the defect stands unchanged:

claim landed source
the is aria-hidden record-path.tsx:177
the is aria-hidden record-path.tsx:175
aria-current="step" is the only programmatic signal record-path.tsx:170

#5917 did add data-stage-state / data-stage-terminal, but its own header scopes them to test assertability — a data-* attribute is not in the accessibility tree. That header now says so explicitly.

The shape, and why this one

The card offered two shapes and picked neither: visually-hidden per-stage text, or an aria-label composing label + state. The choice is a measurement, not a preference. listitem is not a name-from-contents role, so text placed inside a stage leaves the accessible name empty. Measured on this tree with the suite's own harness before writing the fix (markup described rather than pasted — GitHub's body sanitizer strips bare tags, including inside a fence):

stage markup computed accessible name
role="listitem" wrapping a span.sr-only reading completed, plus the label 草稿 '' — the visually-hidden shape
role="listitem" carrying aria-label="草稿, completed" '草稿, completed'

The visually-hidden shape would have satisfied a DOM assertion ("the span is there") while delivering nothing to the accessibility tree — the exact failure mode the card warned against. So: aria-label on the listitem, composing the stage's own already picklist-localized stage.label with its state. Because both halves render from that one variable, the announced name cannot drift from what is on screen (asserted as group D). The glyphs stay decorative, per the triage ruling.

i18n

Five keys — detail.pathStage{Completed,Current,Upcoming,LostCurrent,LostUpcoming} — in all ten locale packs, and mirrored byte-identically into DETAIL_DEFAULT_TRANSLATIONS for the provider-less path.

The ten packs and the defaults map are not scope creep beyond the card's stated surface ("the i18n keys the change needs"); the repo's gates make them mandatory. all-locales-key-parity fails a key en has that any pack lacks and a key a pack has that en lacks, and defaults-maps-mirror-en-pack fails a map row whose key the pack lacks. Five keys therefore means fifty pack rows plus five map rows, or nothing.

Verification

All commands below were run at f4cb3fec4, the branch head.

Testspnpm exec vitest run from the repo root (this repo's guard refuses the per-package pnpm --filter … test form):

scope result
packages/plugin-detail (whole package) 97 files, 916 passed
packages/i18n/src/__tests__/ + app-shell/…/defaults-maps-mirror-en-pack.test.tsx 52 files, 893 passed
cross-package consumers of record:path (console ×3, app-shell previews ×1) 4 files, 129 passed

Gates — verdict lines as each gate printed them, exit codes captured before any pipe:

  • check:control-bytes✅ check-control-bytes: OK (scanned 4942 tracked text file(s); skipped 85 binary).
  • check:i18n-keysEvery in-scope call-site key resolves against the en pack (2934 keys), every literal inline defaultValue matches the value the pack serves, every call site passes exactly the arguments that value has holes for… (this is what confirms { stage } matches the {{stage}} hole in all five keys)
  • check:i18n-driftNo en value changed in this range. / 5 key(s) added, 0 removed — those are all-locales-key-parity's
  • check-changeset-presence.mjs✅ 14 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)
  • turbo run type-check --filter=@object-ui/plugin-detail --filter=@object-ui/i18n — 14 tasks successful (plugin-detail runs tsc --noEmit followed by tsc -p tsconfig.test.json, so the new suites are type-checked too)
  • turbo run lint for both packages — 3 tasks successful, 0 errors. record-path.tsx reports 15 warnings; the file at the merge-base reports the same 15, so this change adds none.

Ablation — the new tests bite, and only where they should. Deleting the single aria-label line from renderStage, then re-running the two new suites plus the pre-existing record-path.inertReadout.test.tsx:

== before: marker count = 1
== after:  marker count = 0
 Test Files  2 failed | 1 passed (3)
      Tests  15 failed | 12 passed (27)

15 red / 12 green, matching the direction predicted in both suite headers before running. Red: every case whose subject is the accessible name — group B (6), the three name-bearing cases of group C, group D (1), and all five locale cases. Green: group A (the #5768 readout contract — aria-current, the decorative glyphs, the labels on screen), group C's "the ✗ stays aria-hidden" case, and the whole pre-existing inertReadout suite, none of which ever depended on the name.

The mutation was confirmed on disk by grepping for the deleted text (1 → 0) rather than by the editor's exit code, and the script carried trap … EXIT INT TERM; the restore was verified byte-identical with diff -q against a pre-ablation copy, and the marker count returned to 1. No rebuild was needed and none is claimed: both suites import the subject relatively (../record-path) and resolve cross-package imports to each package's own src through the root vitest.config.mts alias table, so nothing resolves through a dist/ — the property record-path.inertReadout.test.tsx already records for itself.

Not in this PR


Generated by Claude Code

…ssible name

record:path conveyed travelled / upcoming / lost-terminal stages with colour
plus a glyph, and both glyphs are aria-hidden decoration. aria-current="step"
marked the current stage and nothing else, so a screen-reader user heard a run
of identically-announced items, and a rejected stage announced exactly like an
ordinary stage the record had not reached yet. WCAG 2.2 SC 1.4.1.

Each stage now carries an accessible name composing its already
picklist-localized label with its state, from five new detail.pathStage* keys
translated in all ten locale packs and mirrored byte-identically into
DETAIL_DEFAULT_TRANSLATIONS for the provider-less path.

The name is composed into aria-label rather than visually-hidden text because
listitem takes its name from the author only: measured on this tree, sr-only
text inside a role="listitem" computes to an EMPTY accessible name, so that
shape would have satisfied a DOM assertion while delivering nothing to the
accessibility tree.

The glyphs stay decorative and the #5768 readout contract (role, aria-current,
data-stage-* instruments) is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZyKZejBWZoCSj1NP35wcp
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 52 chunks) 3232.0 KB 3990.2 KB
Main entry chunk (gzip) 153.6 KB 350 KB
Entry file index-DxyIWzfb.js
Status PASS

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 10.13KB 3.77KB
app-shell (runtime-config.js) 13.57KB 4.78KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 504.18KB 114.10KB
core (index.js) 4.92KB 1.97KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 164.55KB 45.67KB
fields (index.js) 238.40KB 59.89KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 188.21KB 44.67KB
plugin-dashboard (index.js) 133.35KB 34.44KB
plugin-designer (index.js) 212.30KB 42.80KB
plugin-detail (index.js) 244.00KB 61.86KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 125.63KB 30.64KB
plugin-gantt (index.js) 164.15KB 39.88KB
plugin-grid (index.js) 200.79KB 54.26KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.86KB 27.22KB
plugin-map (index.js) 20.06KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.49KB 7.59KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.57KB 20.74KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 52.40KB 17.45KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.35KB 0.70KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 12.13KB 3.65KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 7.54KB 2.63KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.88KB 1.85KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants