Skip to content

fix(a11y): name InlineCreateRelated's icon-only close button (#3411) - #3413

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3411-close-button-name
Aug 5, 2026
Merged

fix(a11y): name InlineCreateRelated's icon-only close button (#3411)#3413
yinlianghui merged 1 commit into
mainfrom
claude/issue-3411-close-button-name

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3411

问题

InlineCreateRelated 卡片头部的关闭按钮是纯图标按钮:唯一子节点是 lucide 的 X,按钮上没有文本、没有 aria-label / aria-labelledby / title。而 lucide-react 会把「无 children 且无 a11y prop」的图标默认标成 aria-hidden="true",把它排除出可访问性树 —— 于是这个按钮的可访问名没有任何来源,计算结果是空串,屏幕阅读器只会念出一个没有名字的 "button"。WCAG 4.1.2 / 2.4.6。

与同族的 #3381 不同的是,这里连兜底都没有:占位符至少在真实浏览器里还能当最后一档名字来源,而图标按钮在任何实现下都是空串。

改动

不涉及 props / spec / 可见文案变化。

测试

新增 packages/plugin-detail/src/__tests__/InlineCreateRelated.closeButtonName.test.tsx(7 例)。按 issue 的要求写成直接断言:先结构化定位该按钮(卡片头里唯一的那个 button),再断言它的名字 —— 而不是「卡片上不存在无文本按钮」这类会被其它改动带绿的间接形。覆盖 getByRole('button', { name: /^Close$/ }) 命中、toHaveAccessibleName() 非空、名字挂在真正会收起卡片的那个按钮上、图标保持 decorative、link 页签同样有名、同页两个实例各自有名。

反向验证(方向在跑之前就已预判,结果一致):普通的 before-red / after-green —— 名字只有一个来源,下游也没有任何「计数型」的 gate 会反向增量。在未修的树上 7 例全红,且结构化定位本身仍然成功,说明红的是名字而不是元素挪了位:

× names the card header close button
expect(element).toHaveAccessibleName()
Expected element to have accessible name: undefined
Received: (空串)
× exposes the close button to getByRole with its name
Unable to find an accessible element with the role "button" and name `/^Close$/`

修复后(仓根跑,以文件名确认过该文件确实进了过滤集):

✓ …closeButtonName.test.tsx (7 tests) — Test Files 1 passed (1) / Tests 7 passed (7)
npx vitest run packages/plugin-detail → Test Files 49 passed (49) / Tests 442 passed (442)
pnpm --filter @object-ui/plugin-detail type-check → 通过(先 build 了依赖包)
eslint(仅本次改动的两个文件)→ 0 errors(2 个 warning 是既有的 `no-explicit-any`,在未改动的行上)
node scripts/check-control-bytes.mjs → OK (scanned 3609 tracked text file(s))

消费半径已扫:InlineCreateRelated 只有具名导出、未注册进组件 registry,引用它的只有 packages/plugin-detail 自己的四个测试文件,无跨包 fixture 受影响。

changeset:.changeset/inline-create-related-close-button-name.md(patch)。

Refs #3381(PR #3410)、#3341(PR #3380)、#3299

🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

The card header's close button had no accessible name at all. Its only
child is a lucide `X`, and lucide-react excludes childless,
a11y-prop-less icons from the accessibility tree (defaulting them to
`aria-hidden="true"`), so with no text, `aria-label`, `aria-labelledby`
or `title` on the button the computed name was the empty string — a
screen reader announced a nameless "button". Unlike #3381's placeholder
case there was no browser-side fallback to soften it: empty in every
implementation. WCAG 4.1.2 / 2.4.6.
`aria-label="Close"` rather than #3381's visually hidden `<label>`,
because this control has no visible copy for a label to stay in step
with — the drift that ruling guarded against cannot arise here — and it
is the shape the repo's other close buttons already use (shadcn's
dialog/sheet, DashboardEditor). The icon is now explicitly
`aria-hidden`, so the intent is local instead of inherited from the icon
library's default.
Tests locate the button structurally (the card header's only button) and
then assert its name, rather than the indirect "no text-less button
exists" form the issue warned about. Reverse-verified in the plain
direction predicted up front: on the unfixed tree all 7 cases fail with
an empty accessible name while the structural lookup still succeeds.
No props, spec or visible-copy change.
Refs #3411
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 5, 2026 10:31pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-Dv9gN-y2.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.47KB3.09KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)18.38KB4.49KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)3.65KB1.42KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.25KB0.53KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)478.45KB105.05KB
core (index.js)2.25KB0.80KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)136.23KB34.75KB
fields (index.js)228.08KB55.90KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)3.26KB1.44KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.05KB1.53KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)112.01KB28.86KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)231.85KB57.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)111.54KB26.97KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)185.08KB49.04KB
plugin-kanban (index.js)47.89KB13.18KB
plugin-list (index.js)105.02KB25.36KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.55KB10.59KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.34KB2.82KB
plugin-view (index.js)83.67KB20.43KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.46KB1.21KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)0.20KB0.18KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 5, 2026 22:41
@yinlianghui
yinlianghui added this pull request to the merge queueAug 5, 2026
Merged via the queue into main with commit 58a00f0Aug 5, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3411-close-button-name branch August 5, 2026 22:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

InlineCreateRelated 的关闭按钮是纯图标且无可访问名:X 图标被 lucide 默认 aria-hidden,按钮名计算为空串

2 participants

@yinlianghui@claude