You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Grid boolean cells: the read-only checkbox the boolean cell renderer draws has no accessible name, so a screen reader announces "checkbox, not checked" with no field #12584
Filing gate: ① — a user-visible accessibility defect with a named location, measured in the browser's own tree.
Symptom (measured by the cloud session of #12510, on objectui 572bbce, HotCRM 63db83e)
Studio Data pillar › HotCRM › Account › Records tab. The browser's computed tree (CDP Accessibility.getFullAXTree) lists 9 checkbox nodes with an empty name: one per row of the is_active column. The runtime list page draws its cells with the same renderer (decision D of #12446), so it reads the same.
reach: the Studio Records grid and the runtime list grid (UI doors). Measured on the Studio door only.
Cause
packages/fields/src/index.tsx on objectui f96f771, the boolean cell renderer, line 1014:
There is no aria-label, no aria-labelledby, and no text beside it. The renderer's two other outcomes already name themselves:
The completion indicator (completed, is_done, …) carries role="img" and aria-label from fields.boolean.completed / fields.boolean.notCompleted.
The off badge for status fields reads fields.boolean.offBadge with the field's label, field?.label || humanizeLabel(fieldName).
Fix
Name the checkbox by the field, as the off badge already does: aria-label={field?.label || humanizeLabel(fieldName)}. Its checked state is announced by the role, so the name carries no state word. There is no visual change, and no new i18n key is needed.
Acceptance
A unit test renders the boolean cell renderer for a field with a label and asserts getByRole('checkbox', { name: <label> }), both checked and unchecked.
A field with no label falls back to the humanized name.
CONTROL: the completion indicator and the off badge keep their current names.
Who acts
Triage grades and routes it. The fix reader is the domain:ui lane that owns packages/fields. It came out of #12510, which named only buttons, links and comboboxes, and it is not part of epic #12446.
Dedupe (2026-10-15, MCP search_issues on objectstack-ai/objectui, open and closed)
"boolean cell checkbox accessible name aria-label grid read-only checkbox unnamed screen reader": 4 hits, none this defect: objectui#11924, objectui#11051, objectui#10435 (closed a11y findings, so the channel was live) and objectui#8490 (the empty-array value of the same renderer, closed).
Filed by seat domain:ui#4 (session session_01SJn1GEkrbbBQkhGC26wEDw) from the out-of-scope finding of the #12510 report.
Lane: domain:ui, confirmed. The fix is one attribute in packages/fields/src/index.tsx, the boolean cell renderer.
Grade: p2.
A screen reader announces every grid boolean cell as an unnamed checkbox, on the Studio Records grid and on the runtime list grid that shares the renderer.
Claim: PM loop round 1 (priority:p2, a11y)
Session: session_01JZnppgHRp4sREcouhkQYDB
Account: sunhaolin
Branch: claude/issue-12584-boolean-checkbox-name
Worktree: objectui-issue-12584
Domain: domain:ui
Seat: domain:ui#1
File surface: packages/fields/src/index.tsx (the boolean cell renderer's plain-checkbox outcome only, line 1014 on mainae923ece22), one new test file under packages/fields/src/__tests__/, and one @object-ui/fields patch changeset (stop on breach; explain in the report).
Container & model: S, mode:subagent, model: opus (default tier)
Clause-②: no
Responsibility: the risk is produced by objectui's own code, the boolean cell renderer in packages/fields; no platform path covers it (the renderer's completion indicator and off badge already name themselves, its plain-checkbox outcome does not); every screen-reader user of the Studio Records grid and the runtime list grid reaches it, on any boolean column outside the completion and status names (HotCRM Account is_active measured, 9 unnamed checkboxes)
Thread-read: 6204745162
Serial constraints cleared: no open PR touches packages/fields/ (scanned every open objectui PR's file list), and no in-flight claim names it.
os-dev-report
{
"issue": 12584,
"status": "done",
"branch": "claude/issue-12584-boolean-checkbox-name",
"pr": "#12597",
"session": "session_01JZnppgHRp4sREcouhkQYDB — harness-stamped id of this run (subagent, so the parent seat's)",
"premise_still_valid": true,
"summary": "BooleanCellRenderer (packages/fields/src/index.tsx) now names its plain read-only checkbox by the field: aria-label = fieldLabel, one local value (field.label, else humanizeLabel of the field name) that the off badge's text also reads, so the expression exists once. No state word, no new i18n key, no class/prop/export/type change, Checkbox primitive untouched. A1 holds (getByRole('checkbox', { name }) finds the Radix button by name). A2 holds by source: the data-table TableCell wrapper div carries only className/title/data-auto-width-key and VirtualGrid's div only a class; no aria-labelledby/aria-label on either host, so no double naming. A3 measured on humanizeLabel itself: IsPreferred to IsPreferred as authored, Ispreferred lower-cased (the old fallback), so per the dispatch's conditional the fallback reads field.name as authored in both places; matching (completion names, STATUS_FIELD_NAMES) still uses the lower-cased key. For every spec-legal (lowercase snake_case) name the words are byte-identical; humanizeLabel does not split camelCase, so IsPreferred is the better reading, not a perfect one.",
"tests": "New packages/fields/src/tests/booleanCell.accessibleName-12584.test.tsx (registry render, boolean + toggle): 13 passed (13) at 63ba527. Every fields suite named for boolean (5 files): Tests 103 passed (103) at 63ba527. Whole @object-ui/fields (pnpm exec vitest run packages/fields/): Test Files 257 passed | 1 skipped (258), Tests 4084 passed | 7 skipped (4091) at 789a28d (later commit only retypes the new test helper as any to as never). pnpm --filter @object-ui/fields type-check after building the closure (pnpm --workspace-concurrency=2 --filter @object-ui/fields^... build, 11 of 47 projects, exit 0): exit 0 at 63ba527; tsc -p tsconfig.test.json --listFiles lists the new file (1 hit). Census/ratchet sweep (ariaLabelLiteralCensus-12145, iconOnlyButtonCensus-12143, machineLocaleCensus-9909, ellipsis-glyph-3878, emptiness-floor-8496, humanize-label, ObjectGrid.cellFaceLabel-11689, DetailSection.copyObjectValues-8395, widget-dom-leak-sweep, catalog-gallery-render): Test Files 10 passed (10), Tests 878 passed (878) at 789a28d. Ablation (fix committed first; objectstack scripts/ablation-replace.mjs --delete of ' aria-label={fieldLabel}', anchor 1 to 0, blob 8d8a77e75ac8 to 433e77c56102, on-disk count 0 inside the run): Tests 10 failed | 3 passed (13), all 10 naming pins red with Unable to find an accessible element with the role checkbox and name ..., 3 CONTROLs green; restore proven blob == HEAD 8d8a77e75ac8 and git diff HEAD empty. Test imports ../index relatively, subject resolves to source, no dist rebuild needed. First ablation attempt (replacement mode) was refused by the tool as a no-op (replacement text is a substring of the anchor, count 1 to 1), restored, redone in delete mode.",
"gates": "Local subset, exits captured to files before any pipe. At final head 63ba527: check:control-bytes 0, check:new-line-citations 0 (VERDICT 0 new citations, report-only), check-changeset-presence 0, check-changeset-no-major 0, check-changeset-claims 0, check-governed-queue-guard --test 0 (NOT GOVERNED). At 789a28d, also 0: check-changeset-fixed, check-changeset-overwrite, check:pending-changeset-literals, check:i18n-keys, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:comment-mask-corpus, check:shell-escape-residue, check-type-check-coverage. eslint --no-inline-config on the two changed source files: exit 0, 0 errors, no warning in the changed index.tsx lines, 0 in the new test. Gate list derived by hand from objectui package.json check:* and lint/ci/changeset-guard/changeset-presence/repo-gates/line-citation-gate/performance-budget workflows (objectui has no dispatch-gates.mjs, so no --ran reconciliation). PR check-runs on 63ba527 at report time: success: Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label; skipped: Test (coverage), Test (coverage shard N/8), dependabot; in_progress: Lint, Type Check, Test (shard 1/16 .. 16/16), Test (dist pins), Build & E2E, Build Docs, Repo Gates, Bundle Analysis, Live E2E (informational), Spec Main Shape Gate (known red on main, objectui#12464, advisory).",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 relay dispatches via objectstack scripts/pm/fleet-write/dispatch.mjs (each POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/12597/assignees [sunhaolin], read-back identical 7496/7496 bytes; (2) comment = POST /repos//issues/12584/comments (this report). Plus git push of the branch (not REST). 0 label writes. Reads: gh api GET issue 12584, its comments, pulls/12597, commits/63ba527469/check-runs.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (read from source, not reproduced in a browser, no reach): the data-table cell wrapper sets title to String(cellValue) for any non-object value, so hovering a boolean cell shows a raw true / false tooltip in English under every locale; it does not name the checkbox. In PR Acceptance notes. dedupe words: data-table cell title tooltip raw boolean, untranslated true false hover"
],
"files_changed": [
"packages/fields/src/index.tsx (+14 -2, BooleanCellRenderer only)",
"packages/fields/src/tests/booleanCell.accessibleName-12584.test.tsx (new, +115)",
".changeset/12584-boolean-checkbox-name.md (new, @object-ui/fields patch)"
],
"line_budget": "3 files, +140 -2; within the claim's file surface",
"deviations": [
"A3 conditional applied (the fallback did not read right lower-cased), so the off badge's humanized fallback also reads the authored name. Ruling 1 says the off badge's text is unchanged: it is byte-identical for every spec-legal name; it moves only for an unlabelled off-spec status name with an inner capital (IS_ACTIVE: IS ACTIVE — Off, was Is Active — Off). Named here, not silently chosen.",
"Whole-fields suite and census sweep measured at 789a28d, not the final head 63ba527; the only later commit retypes the new test helper (as any to as never), and the new file, the boolean suites and type-check were re-run at 63ba527.",
"First ablation attempt was a tool-refused no-op (replacement mode), restored and redone in delete mode; only the second run's numbers are cited.",
"PR labels tests and package: fields come from the labeler workflow, not from this run."
]
}
All 3 files the PR changes are blob-identical on 7d0c174ba8 to the accepted head 63ba527469.
The squash touches no other file (3 files, +140/−2).
Its patch-id equals the accepted diff's (17f3234af1c8).
Mis-close scan: the PR body's only closing keyword is Fixes #12584. The only issue closed with this merge is this card, as completed.
What now holds:
A boolean column's plain read-only checkbox, in the Studio Records grid and the runtime list grid, is named by its field. The name is the field's label, or its humanized name when it declares none.
The checkbox role announces the state, so the name carries no state word.
The completion indicator and the status field's "Off" badge keep their names. The badge reads the same words, from one derivation.
The chain: triage grade 6204745162, claim 6205971429, ACCEPT 6206421400.
Activity
objectstack-fleet commentedon Oct 15, 2026
First grade ·
domain:ui· p2 ·pm:queue·a11y· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T23:53ZRead in this act: the body in full, 0 comments.
domain:ui, confirmed. The fix is one attribute inpackages/fields/src/index.tsx, the boolean cell renderer.a11y, closed).pm:queue. The body carries the fix, the acceptance with its control, and a dedupe that includes closed cards (4 hits, none this defect).a11yadded, as on Studio: controls without an accessible name (records toolbar, flow nodes, sheets, landing cards) #12510.⛔ Not a claim.
objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 1 (
priority:p2,a11y)Session:
session_01JZnppgHRp4sREcouhkQYDBAccount:
sunhaolinBranch:
claude/issue-12584-boolean-checkbox-nameWorktree:
objectui-issue-12584Domain:
domain:uiSeat:
domain:ui#1File surface:
packages/fields/src/index.tsx(the boolean cell renderer's plain-checkbox outcome only, line 1014 onmainae923ece22), one new test file underpackages/fields/src/__tests__/, and one@object-ui/fieldspatch changeset (stop on breach; explain in the report).Container & model:
S,mode:subagent,model: opus(default tier)Clause-②: no
Responsibility: the risk is produced by objectui's own code, the boolean cell renderer in
packages/fields; no platform path covers it (the renderer's completion indicator and off badge already name themselves, its plain-checkbox outcome does not); every screen-reader user of the Studio Records grid and the runtime list grid reaches it, on any boolean column outside the completion and status names (HotCRM Accountis_activemeasured, 9 unnamed checkboxes)Thread-read: 6204745162
Serial constraints cleared: no open PR touches
packages/fields/(scanned every open objectui PR's file list), and no in-flight claim names it.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
{
"issue": 12584,
"status": "done",
"branch": "claude/issue-12584-boolean-checkbox-name",
"pr": "#12597",
"session": "session_01JZnppgHRp4sREcouhkQYDB — harness-stamped id of this run (subagent, so the parent seat's)",
"premise_still_valid": true,
"summary": "BooleanCellRenderer (packages/fields/src/index.tsx) now names its plain read-only checkbox by the field: aria-label = fieldLabel, one local value (field.label, else humanizeLabel of the field name) that the off badge's text also reads, so the expression exists once. No state word, no new i18n key, no class/prop/export/type change, Checkbox primitive untouched. A1 holds (getByRole('checkbox', { name }) finds the Radix button by name). A2 holds by source: the data-table TableCell wrapper div carries only className/title/data-auto-width-key and VirtualGrid's div only a class; no aria-labelledby/aria-label on either host, so no double naming. A3 measured on humanizeLabel itself: IsPreferred to IsPreferred as authored, Ispreferred lower-cased (the old fallback), so per the dispatch's conditional the fallback reads field.name as authored in both places; matching (completion names, STATUS_FIELD_NAMES) still uses the lower-cased key. For every spec-legal (lowercase snake_case) name the words are byte-identical; humanizeLabel does not split camelCase, so IsPreferred is the better reading, not a perfect one.",
"tests": "New packages/fields/src/tests/booleanCell.accessibleName-12584.test.tsx (registry render, boolean + toggle): 13 passed (13) at 63ba527. Every fields suite named for boolean (5 files): Tests 103 passed (103) at 63ba527. Whole @object-ui/fields (pnpm exec vitest run packages/fields/): Test Files 257 passed | 1 skipped (258), Tests 4084 passed | 7 skipped (4091) at 789a28d (later commit only retypes the new test helper as any to as never). pnpm --filter @object-ui/fields type-check after building the closure (pnpm --workspace-concurrency=2 --filter @object-ui/fields^... build, 11 of 47 projects, exit 0): exit 0 at 63ba527; tsc -p tsconfig.test.json --listFiles lists the new file (1 hit). Census/ratchet sweep (ariaLabelLiteralCensus-12145, iconOnlyButtonCensus-12143, machineLocaleCensus-9909, ellipsis-glyph-3878, emptiness-floor-8496, humanize-label, ObjectGrid.cellFaceLabel-11689, DetailSection.copyObjectValues-8395, widget-dom-leak-sweep, catalog-gallery-render): Test Files 10 passed (10), Tests 878 passed (878) at 789a28d. Ablation (fix committed first; objectstack scripts/ablation-replace.mjs --delete of ' aria-label={fieldLabel}', anchor 1 to 0, blob 8d8a77e75ac8 to 433e77c56102, on-disk count 0 inside the run): Tests 10 failed | 3 passed (13), all 10 naming pins red with Unable to find an accessible element with the role checkbox and name ..., 3 CONTROLs green; restore proven blob == HEAD 8d8a77e75ac8 and git diff HEAD empty. Test imports ../index relatively, subject resolves to source, no dist rebuild needed. First ablation attempt (replacement mode) was refused by the tool as a no-op (replacement text is a substring of the anchor, count 1 to 1), restored, redone in delete mode.",
"gates": "Local subset, exits captured to files before any pipe. At final head 63ba527: check:control-bytes 0, check:new-line-citations 0 (VERDICT 0 new citations, report-only), check-changeset-presence 0, check-changeset-no-major 0, check-changeset-claims 0, check-governed-queue-guard --test 0 (NOT GOVERNED). At 789a28d, also 0: check-changeset-fixed, check-changeset-overwrite, check:pending-changeset-literals, check:i18n-keys, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:comment-mask-corpus, check:shell-escape-residue, check-type-check-coverage. eslint --no-inline-config on the two changed source files: exit 0, 0 errors, no warning in the changed index.tsx lines, 0 in the new test. Gate list derived by hand from objectui package.json check:* and lint/ci/changeset-guard/changeset-presence/repo-gates/line-citation-gate/performance-budget workflows (objectui has no dispatch-gates.mjs, so no --ran reconciliation). PR check-runs on 63ba527 at report time: success: Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label; skipped: Test (coverage), Test (coverage shard N/8), dependabot; in_progress: Lint, Type Check, Test (shard 1/16 .. 16/16), Test (dist pins), Build & E2E, Build Docs, Repo Gates, Bundle Analysis, Live E2E (informational), Spec Main Shape Gate (known red on main, objectui#12464, advisory).",
"mcp_calls": "0 — no MCP GitHub calls",
"api_writes": "2 relay dispatches via objectstack scripts/pm/fleet-write/dispatch.mjs (each POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectui/pulls (draft) + POST /repos//issues/12597/assignees [sunhaolin], read-back identical 7496/7496 bytes; (2) comment = POST /repos//issues/12584/comments (this report). Plus git push of the branch (not REST). 0 label writes. Reads: gh api GET issue 12584, its comments, pulls/12597, commits/63ba527469/check-runs.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed (read from source, not reproduced in a browser, no reach): the data-table cell wrapper sets title to String(cellValue) for any non-object value, so hovering a boolean cell shows a raw true / false tooltip in English under every locale; it does not name the checkbox. In PR Acceptance notes. dedupe words: data-table cell title tooltip raw boolean, untranslated true false hover"
],
"files_changed": [
"packages/fields/src/index.tsx (+14 -2, BooleanCellRenderer only)",
"packages/fields/src/tests/booleanCell.accessibleName-12584.test.tsx (new, +115)",
".changeset/12584-boolean-checkbox-name.md (new, @object-ui/fields patch)"
],
"line_budget": "3 files, +140 -2; within the claim's file surface",
"deviations": [
"A3 conditional applied (the fallback did not read right lower-cased), so the off badge's humanized fallback also reads the authored name. Ruling 1 says the off badge's text is unchanged: it is byte-identical for every spec-legal name; it moves only for an unlabelled off-spec status name with an inner capital (IS_ACTIVE: IS ACTIVE — Off, was Is Active — Off). Named here, not silently chosen.",
"Whole-fields suite and census sweep measured at 789a28d, not the final head 63ba527; the only later commit retypes the new test helper (as any to as never), and the new file, the boolean suites and type-check were re-run at 63ba527.",
"First ablation attempt was a tool-refused no-op (replacement mode), restored and redone in delete mode; only the second run's numbers are cited.",
"PR labels tests and package: fields come from the labeler workflow, not from this run."
]
}
Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:uiseat 1 ·session_01JZnppgHRp4sREcouhkQYDB· 2026-10-16T02:19Zmain7d0c174ba8at 2026-10-16T02:18Z. It has one parent,f5eec63836, and is an ancestor oforigin/main.7d0c174ba8to the accepted head63ba527469.17f3234af1c8).Fixes #12584. The only issue closed with this merge is this card, ascompleted.6204745162, claim6205971429, ACCEPT6206421400.Generated by Claude Code