Skip to content

fix(app-shell): page block inspector list labels name their group - #27307

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-f88hdb
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-f88hdb

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #27256

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 页面块检查器里的三个列表编辑器(字段列表如「Columns」、字符串列表、数组)现在各自是一个以其标题命名的分组;只带占位提示的文本行输入框按「标题 + 序号」命名(如「Action names 1」)。
  • 为什么改:这三个标题之前不指向任何控件,读屏用户只听到一串无名的行,分不清它们属于哪个属性。
  • 风险与回滚:只加 ARIA 属性和一个很小的包装组件,不改数据、写入或布局;回滚即 revert 本 PR。
  • 先看:PageBlockInspector.tsx 里新增的 LabelledListGroup 及三处调用;新测试 PageBlockInspector.listGroupLabels-27256.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.listGroupLabels-27256.test.tsx,预期 4 passed。
  • 需要你做的:无,席位落地。

What changes

ui/packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.tsx, the three list editors only:

  • New LabelledListGroup component: the container takes role="group" and aria-labelledby pointing at the label's id, minted with React.useId() per instance. It is a component (not inline in renderField) because renderField is a function run in a loop over array items and cannot call a hook, and a caller-derived id would repeat across array rows. This is the same pattern ColorPropField already uses in this file.
  • FieldListField (Columns, Fields, Card fields …), the string-list case and the array case of renderField all render through it.
  • Row naming (the order asked to check the string-list rows): each string-list row input had no name but a repeated placeholder, so it now carries aria-label = the list label plus its 1-based position ("Action names 1"). Chosen over aria-labelledby plus an index element because no visible per-row index exists to point at. The field-list text fallback rows (shown when the object has no fields) had the same gap and get the same name. The field-list Select trigger rows are left alone: their name is the selected value's text, and an aria-label would hide it.

app-shell is private: true, so no changeset (skip-changeset).

Why

Each of the three labels headed a list of rows and named nothing: getByLabelText('Columns') found the label and no control, and a screen reader heard the rows with no group name. A for pairing does not fit, since one label heads several controls.

Tests

PageBlockInspector.listGroupLabels-27256.test.tsx (4 cases):

  • record:related_list: getByRole('group', { name: 'Columns' }) (field list) and { name: 'Sort' } (array) each resolve; the row controls (combobox, #1, the remove buttons) sit inside their group; the IDREF resolves to the label element inside the group; the two ids differ.
  • record:quick_actions: the Action names group holds textboxes named Action names 1 and Action names 2.
  • Field-list text fallback (object with no fields): the Columns group holds a textbox named Columns 1.
  • record:details with two sections: two Fields groups inside the Sections group, each named by its own label with distinct ids.

Acceptance notes

  • Ablation (one-off, no file kept): scripts/ablation-replace.mjs swapped the group's aria-labelledby={labelId} for an inert attribute → 4 of 4 cases red (Unable to find an accessible element with the role "group" and name …); restore verified blob equal to HEAD and git diff HEAD empty. A first attempt was refused by the tool before running anything (the replacement text already occurred in the file), so it measured nothing.
  • The remove buttons in each list share one name ("Remove" / "Remove item") per row; left as is, out of this card's scope.

Verification

All on head 3402dbdd (base 5b5cff15), each exit captured before any pipe.

  • Closure: pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build under the lock: exit 0.
  • New pin: pnpm --dir ui exec vitest run packages/app-shell/src/views/metadata-admin/inspectors/PageBlockInspector.listGroupLabels-27256.test.tsx: 4 passed, exit 0.
  • Neighbours: all 14 PageBlockInspector*.test.tsx files: 14 files, 90 tests passed, exit 0.
  • pnpm --filter @object-ui/app-shell run type-check (source and tsconfig.test.json): exit 0.
  • pnpm --dir ui run check:new-line-citations: 0 new citations, exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands derived 24 commands; --ran reconciles 24 run, 0 NOT-MEASURED.
Derived gates (24)
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

pnpm --dir ui exec vitest run scripts/ first exited 1 on an unlocked run beside the app-shell type-check: two tree-sweep tests (check-i18n-dead-keys, check-vi-mock-inherit) hit the 15 s test timeout. Rerun alone under the lock: 2 files, 229 tests passed; then the whole suite under the lock: 174 files passed, 2 skipped, exit 0. The recorded exit is that rerun.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (CI runs the repo lint)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing: private package)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English. (the 维护者速读 section is the dispatch contract's Chinese summary)

Generated by Claude Code

The Columns (field list), string-list and array editors drew a label over
a list of rows with no association, so the rows had no group name. Each
container now takes role="group" and aria-labelledby the label's id,
minted per instance by a small LabelledListGroup component (renderField
runs in a loop and cannot call a hook). Text row inputs that carried only
a repeated placeholder are named from the label plus their position.

Claude-Session: https://claude.ai/code/session_01JQwxbZEzQNxEeTKF7YTgo6
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head 3402dbdd · 2026-10-28T16:12Z

席位意见:Studio 页面块检查器里有三个列表编辑器(列、字符串列表、数组),它们的标题标签原先不指向任何控件,屏幕阅读器读不出这组行属于哪个属性。现在每个列表都包在 role="group" 里,用 aria-labelledby 指向标题,id 按实例生成。字符串列表的每行输入框也有了"标签 N"的名称。

  1. Shape: Fixes #27256; 2 files, +170/−9: PageBlockInspector.tsx +33/−9 (a new local LabelledListGroup wrapping the three editors, plus aria-label on the string-list rows and the field-list text-fallback rows), and one new test file (137 lines, 4 pins). skip-changeset: @object-ui/app-shell is private: true.
  2. Premise measured: the order's mechanism held. The seat read the diff: LabelledListGroup owns its React.useId(), so the two inline editors inside renderField's loop get a per-instance id without calling a hook in the loop. It follows ColorPropField's IDREF pattern. Row Select triggers keep their value as their name, and only text inputs gain "<label> N".
  3. Gates / check-runs: PR check-runs on 3402dbdd: 44 success, 16 skipped (path-filtered), 0 failure; mergeable_state clean. Dev: 24 derived, 24 run, reconciled; 14 PageBlockInspector suites, 90/90 tests; app-shell type-check exit 0. Ablation: all 4 pins turn red, then restored.
  4. Accepted deviations: none. The row naming is the option the order asked the dev to choose and state.
  5. Out of scope: none found. Skills-docs: none — a11y wiring only.
  6. Landing: not Tier H (no governed path, 179 lines); ready for landing (ready + auto-merge).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 16:14
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 16:14
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 02a891e Oct 28, 2026
61 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-f88hdb branch October 28, 2026 16:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio page block inspector: the Columns, string-list and array prop labels name no group, so their rows have no accessible group name

2 participants