Skip to content

objectui: the shadcn Sheet/Dialog primitives ship a hardcoded English Close sr-only label — every drawer and modal in the console announces it untranslated #5505

Description

@yinlianghui

Found while implementing objectstack#5430 (the nav-overlay Close item). Not fixed there — the fix lands in a No-Touch zone and needs a route decision.

What

Both auto-rendered close buttons in the Shadcn primitives carry their accessible name as a hardcoded English sr-only span:

File Line
packages/components/src/ui/sheet.tsx 80
packages/components/src/ui/dialog.tsx 57

The button is icon-only (a lucide X), so that span is the control's name to a screen reader. Under a zh/ja/es session every drawer and every modal in the console announces "Close" in English.

Why it is live, and how #5430 surfaced it

NavigationOverlay is the one consumer that hides the primitive's own close ([&>button:last-of-type]:hidden) because it renders its own in the header — so there it is display:none and out of the accessibility tree. Everywhere else it renders normally. Roughly 20 SheetContent consumers plus every DialogContent consumer, e.g.:

  • packages/app-shell/src/layout/ChatDock.tsx, layout/ActivityFeed.tsx
  • packages/app-shell/src/views/metadata-admin/* (PackagesPage, PermissionMatrixEditor, AccessExplainPanel, ResourceEditPage, MetadataDetailDrawer)
  • packages/app-shell/src/console/ai/AiChatPage.tsx, console/ai/BuildDebugDrawer.tsx
  • packages/fields/src/widgets/PeoplePicker.tsx, packages/plugin-dashboard/src/RecordDetailDrawer.tsx, packages/plugin-charts/src/ObjectChart.tsx, packages/plugin-chatbot/src/AiPendingActionsInbox.tsx

It surfaced in #5430 as a test artifact: getByRole('button', { name: 'Close' }) matched two elements under an en session, because jsdom does not apply the Tailwind hidden class. The #5430 test worked around it by addressing its own button via title; the primitive's label was left alone on purpose.

Why it is not a one-line fix

packages/components/src/ui/** is a No-Touch zone (AGENTS.md Commandment #7) — those files are overwritten by pnpm shadcn:update / scripts/shadcn-sync.js. Editing the literal in place would be silently reverted by the next sync. Options worth weighing:

  • A. Wrapper in packages/components/src/custom/ — the documented escape hatch, but SheetContent/DialogContent render the close internally, so a wrapper would have to hide the built-in one and re-render its own (what NavigationOverlay already does by hand). That is the pattern to generalize, at the cost of every consumer migrating to the wrapper.
  • B. Teach the sync script a patch/allowlist for this one literal, so the i18n call survives regeneration.
  • C. Accept a prop (closeLabel) threaded from consumers — pushes the burden onto ~20 call sites and leaves the default English.

A relevant precedent: common.close already exists in all ten locale packs (#5430 wired NavigationOverlay's own close to it), so only the delivery mechanism is open, not the key.

Acceptance

  • The close affordance of SheetContent and DialogContent reads common.close under every locale.
  • Whatever route is chosen survives pnpm shadcn:check / a sync run.
  • Tests: en positive + at least one non-en positive + a negative assertion on the English literal, for both primitives.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions