Skip to content

fix(plugin-grid): BulkActionDialog 必填 param 宣告 required 状态而非把星号念进名字 (objectui#3967) - #3989

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3967-bulkaction-boolean-aria
Aug 9, 2026
Merged

fix(plugin-grid): BulkActionDialog 必填 param 宣告 required 状态而非把星号念进名字 (objectui#3967)#3989
yinlianghui merged 1 commit into
mainfrom
claude/issue-3967-bulkaction-boolean-aria

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3967

前提复核(origin/main @ c4768a7

正文两条缺陷都成立,并且比正文更具体的一点值得先写清楚:

  • packages/plugin-grid/src/components/BulkActionDialog.tsx 全文只有一处 aria-hidden(Suspense 骨架屏),星号 span 上没有;全文 aria-required 出现 0 次。
  • 「控件自己会从 field.required 推出 aria-required」这条可能的反驳被证伪:bulkParamToField 确实把 required 写进 field metadata,但 packages/fields/src/widgets/BooleanField.tsxTextField.tsxrequired 出现 0 次 —— toDomProps 按前缀转发 aria-*,它不发明属性。所以 host 不传就是没有,通路空着。
  • param.required 本身是活的声明:同文件的提交前 gate 读它来禁用 Next。也就是说必填语义在逻辑上生效、在可访问性树里缺席,而唯一在场的是那个装饰字符。

改动(仅 2 处,均在 ParamField)

  1. 星号 span 补 aria-hidden="true"。它位于 Label htmlFor 内部,accname 会把被引用 label 的文本折进控件名字,所以每个必填 bulk param 都被念成「Notify owner 星号」。
  2. Widgetaria-required={param.required || undefined}

两条都与 app-shell 的 ActionParamDialog 对齐(#3299 / #3290 的裁决),不在这里发明第二套写法:

id 下发在本站点原本就正确(bulk-param-${param.name}),未动;ActionParamDialog 未动(PR #3971 已定型)。星号仍然可见,变的只是它是否参与可访问名。

与派单口径的一处偏差(请复核)

派单写的是「boolean 分支」,并要求「generic 分支不变作阳性对照」。实际代码里 ParamField 没有类型分支 —— 它是单一路径,所有类型走同一段 JSX(与 app-shell 的 ActionParamDialog 不同,后者确实 fork 了一个 boolean 分支)。因此:

  • 两条缺陷本来就同时命中所有类型,不只 boolean;修改点也只有这一处,自然覆盖全部类型。
  • 「阳性对照」相应改写成:非 boolean(text)param 必须呈现完全相同的形态。这条钉子的作用方向是「保持不变」——将来若有人照 app-shell 的样子 fork 出一个 boolean-only 分支并只修那一支,它会红。

钉子(两个方向必须一起钉)

任何一条单独成立都能被错误地满足:只隐藏星号而不给 aria-required,则关于必填什么都不宣告;只给 aria-required 而不隐藏星号,则宣告两次、其中一次是噪音。所以三个用例都同时断言两侧,并额外断言星号仍然可见(否则「直接删掉星号」也能得到干净的名字)。

  • 必填 boolean:控件 aria-required="true" required 原生属性、可访问名恰为 Notify owner;label 的 textContent 仍含 *,且该 * 在一个 aria-hidden="true" 元素内。
  • 可选 boolean:控件完全不带 aria-required,名字仍为 Notify owner,label 不含 *
  • 非 boolean(必填 text + 可选 text):形态与上面逐条一致。

探针刻意用 host 拥有的 id(document.getElementById 上拼 bulk-param- 前缀加 param 名)而不是 *ByLabelText:后者匹配 label 的原始 textContent,会悄悄依赖本 PR 正在断言的那个 aria-hidden,自证其成。

反向验证(先预判,再跑;变异未提交)

两个变异分别跑,方向都是预判的红:

  • 变异 A —— 删掉 aria-required={param.required || undefined}:预判「两条必填断言红、两条可选(断言属性缺席)保持绿」。实测 2 failed | 5 passed,红的正是 :278(boolean)与 :311(text)两处 toHaveAttribute("aria-required", "true")
  • 变异 B —— 删掉星号 span 的 aria-hidden="true":预判「可访问名断言红」。实测 2 failed | 5 passed,红在 :281:312toHaveAccessibleName —— 直接证明没有 aria-hidden 时星号确实折进了名字。

复原后全绿。

验证

  • pnpm exec vitest run packages/plugin-grid --maxWorkers=2Test Files 57 passed (57) / Tests 523 passed (523)
  • pnpm exec turbo run type-check --concurrency=278 successful, 78 total
  • node scripts/check-control-bytes.mjs → OK(3885 个文件);改动文件另做越界自扫,无命中
  • 消费半径已清:BulkActionDialog 的仓内引用只在 plugin-grid(ObjectGrid.tsx、demo、3 个测试),bulk-param 前缀无其它包的 fixture 依赖,plugin-grid 无快照文件

changeset:@object-ui/plugin-grid patch。

…t the asterisk (objectui#3967)

`ParamField` renders the required marker inside a `<Label htmlFor>` that points
at the control, so accname folded the bare `*` into the control's accessible
name ("Notify owner asterisk"), while nothing carried the required STATE to the
control at all — `param.required` was live only in the dialog's own pre-submit
gate, and no widget derives `aria-required` from `field.required`.

Aligned with app-shell's `ActionParamDialog` (objectui#3299/#3290), not a second
spelling: `aria-hidden="true"` on the marker span, `aria-required={param.required
|| undefined}` on the widget. Deliberately not the native `required` attribute
(#3290: that arms the browser's constraint bubble alongside this dialog's own
gating — two validators, one field), and `|| undefined` so an optional param
carries no attribute rather than `aria-required="false"`.

`id` ownership at this site was already correct and is untouched, as is
`ActionParamDialog`. The marker stays visible; only its participation in the
accessible name changes.

Pins in `bulkActionDialogParams.test.tsx` cover both halves together — either
one alone is satisfiable the wrong way — plus a non-boolean param, since
`ParamField` has no type branches.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 9, 2026 9:04pm

Request Review

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-D0TpPuEF.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 483.34KB 106.60KB
core (index.js) 3.00KB 1.14KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 139.61KB 35.99KB
fields (index.js) 227.92KB 56.45KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.54KB 10.71KB
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.74KB
mobile (index.js) 1.50KB 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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.49KB 17.48KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.39KB 30.67KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.29KB 27.16KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 187.97KB 49.79KB
plugin-kanban (index.js) 48.53KB 13.38KB
plugin-list (index.js) 109.73KB 26.55KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.95KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
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 (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-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
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 (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
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

Copy link
Copy Markdown
Collaborator Author

PM 验收(session_01GTRjn8xBqp75dk7kFupVRt):通过,转 ready 并挂 auto-merge。#3967 落地。

核验记录(head b71f8baa6,基 c4768a760,实物核验 + CI 亲读):

  1. 前提修正型报告,PM 确认:派单摘要写「boolean 分支 + generic 对照」,实际 ParamField 是单一路径无类型分支(issue 正文本身没错,偏差在我的派发词措辞)—— dev 如实指出、修法自然覆盖全部类型、「对照」改写为「text 同形态 + 防未来 fork 只修一支」的方向钉 —— 处置正确。
  2. 两处修与先例对齐:星号 span 补 aria-hidden(对象表单之外的一批渲染器仍把必填状态画成星号而不送进 a11y 树(#3290 的同类缺陷,另外五处) #3299 裁决同款)、aria-required={param.required || undefined}(刻意不用原生 required,aria-required 从未到达任何输入控件——必填状态只以「可访问名的一部分」存在 #3290 的双校验器裁决;可选参数完全无属性而非 "false")。前提复核加强:证伪了「widget 会自己从 field.required 推出 aria-required」这条潜在反驳(fields 两 widget 中 required 零出现,toDomProps 不发明属性)。
  3. 钉子设计:两方向刻意合钉(只藏星号=什么都不宣告 / 只加 aria-required=宣告两次)+ 星号仍可见断言(防「直接删星号」蒙混);探针用 getElementById 而非 ByLabelText(后者会依赖被断言的 aria-hidden 自证其成)—— 探针独立性意识到位。
  4. 反向验证:双变异各自预判命中(2 failed | 5 passed ×2,红点行号精确),复原确认。
  5. 门与规程:plugin-grid 全包 523 测试绿;type-check 78/78;控制字节门 + 自扫;changeset patch 三门绿;fable 0;PR 正文被 sanitizer 吃掉的一处已 update 修复并回读 —— 全套纪律在位。
  6. CI 亲读终态:20/20 全 completed、0 失败(Test shard×4 至 21:11:01Z)。

concern 中 lookup/user picker 触发器的 aria-required 着地未断言 —— 在 issue 验收范围外且 #3306 家族已有先例覆盖路径,同意不另立单,记入分诊轮参考。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 9, 2026 21:17
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 9, 2026
Merged via the queue into main with commit 18c42c6 Aug 9, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3967-bulkaction-boolean-aria branch August 9, 2026 21:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants