Repository navigation
fix(fields): name the boolean cell's read-only checkbox by its field (objectui#12584) - #12597
Conversation
The boolean cell renderer's plain-checkbox outcome drew a checkbox with no accessible name, so a screen reader announced every grid boolean cell as a bare "checkbox". It is now named by the field's label, else its humanized name: one local value, also read by the status field's off badge. The fallback humanizes the name as authored rather than the lower-cased matching key. Claude-Session: https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDB Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDB Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Seat ACCEPT: objectui PR #12597 at
|
Fixes #12584
Clause-②: no
What changed
The boolean cell renderer (
BooleanCellRendererinpackages/fields/src/index.tsx) draws a plain read-only checkbox for every boolean column outside the completion and status names. That checkbox had no accessible name, so a screen reader announced each grid cell as a bare "checkbox, not checked", with no field. It is now named by the field:fieldLabel: the field's label, else its humanized name. The off badge's text and the checkbox'saria-labelboth read it, so the expression is written once.checkboxrole announces checked or not checked itself. No new i18n key, no visual change (no class moved), no prop, export or type change. TheCheckboxprimitive is untouched.STATUS_FIELD_NAMESstill uses the lower-cased key, unchanged.Mechanism assumptions, measured
A1: the primitive forwards
aria-labelto the role element. Holds. Every naming pin finds its box withgetByRole('checkbox', { name }), which reads the name off the element that carriesrole="checkbox"(Radix's button), not off a wrapper. The source agrees: theCheckboxwrapper spreads its props ontoCheckboxPrimitive.Root.A2: the grid hosts add no name of their own. Holds, read from both hosts' cell wrappers. The data-table renderer draws
col.cell(...)inside aTableCelland adivthat carries onlyclassName,titleanddata-auto-width-key.VirtualGridwraps it in adivwith a class only. Neither setsaria-labelledbyoraria-label, so nothing is named twice. The card's CDP reading (9 unnamedcheckboxnodes) is the runtime face of the same fact.A3:
fieldNameis the lower-cased name. True.humanizeLabelonly capitalises the first letter of each word, so the lower-cased key read an unlabelled mixed-case name wrong. Measured onhumanizeLabelitself (packages/core/src/utils/humanize-label.ts):IsPreferredIsPreferredIspreferredis_preferredIs PreferredIs Preferredis_activeIs ActiveIs ActiveIS_ACTIVEIS ACTIVEIs ActiveSo the fallback now reads the name as authored, for the checkbox and the off badge alike, as the dispatch ruled. For every spec-legal name the words are byte-identical: the spec refuses an object field name that is not lowercase snake_case ("Field names must be lowercase snake_case"). The off badge's text moves only for an unlabelled, off-spec status name with an upper-case letter inside a word (for example
IS_ACTIVE: "IS ACTIVE — Off", where it read "Is Active — Off").humanizeLabeldoes not split camelCase, by its documented design, soIsPreferredreadsIsPreferred, not "Is Preferred": the better of the two readings, not a perfect one.Tests
New file
packages/fields/src/__tests__/booleanCell.accessibleName-12584.test.tsx. It renders throughgetCellRenderer(resolveCellRendererType(...)), the way a grid host resolves a read-mode cell, for bothbooleanandtoggle:getByRole('checkbox', { name: 'Preferred' });Is Preferred;IsPreferred(A3);is_active,true): the box namedActive, the measured HotCRM cell;EmptyValueaffordance ("No value") and no checkbox.Ablation. The fix was committed first. The mutation went through
ablation-replace.mjs, deletingaria-label={fieldLabel}: anchor 1 to 0, blob8d8a77e75ac8to433e77c56102, on-disk count 0 inside the run. Result:Tests 10 failed | 3 passed (13). All 10 naming pins went red with "Unable to find an accessible element with the role "checkbox" and name ...", and the 3 CONTROLs stayed green. Restore proven: blob equals HEAD8d8a77e75ac8,git diff HEADempty. The test imports../indexrelatively, so the subject resolves to source, notdist/, and no rebuild was needed for the mutation to reach it.Runs, local, on a shared box, through the verify lock:
Tests 13 passed (13)at63ba527469;packages/fieldssuite named for boolean (5 files):Tests 103 passed (103)at63ba527469;@object-ui/fieldssuite:Test Files 257 passed | 1 skipped (258),Tests 4084 passed | 7 skipped (4091)at789a28d784. The only later commit retypes the new test's render helper (as anytoas never);pnpm --filter @object-ui/fields type-check, after building its dependency closure (11 of 47 projects): exit 0 at63ba527469.tsc -p tsconfig.test.json --listFileslists the new test file;humanizeLabel, grid cell face label, detail section, widget DOM-leak sweep, catalog gallery render):Test Files 10 passed (10),Tests 878 passed (878)at789a28d784.Gates
Local subset at the final head
63ba527469, each exit captured before any pipe:check:control-bytes0,check:new-line-citations0 (0 new citations),check-changeset-presence0,check-changeset-no-major0,check-changeset-claims0,check-governed-queue-guard --testNOT GOVERNED. At789a28d784, also 0:check-changeset-fixed,check-changeset-overwrite,check:pending-changeset-literals,check:i18n-keys,check:test-path-roots, the threecheck:vi-mock-*,check:comment-mask-corpus,check:shell-escape-residue,check-type-check-coverage. ESLint on the two changed source files: 0 errors, no warning in the changed lines ofindex.tsx, none in the new test. The full farm is CI's.What the landing seat's browser reading should show
HotCRM, the Account list (the runtime list grid; the Studio Records grid draws the same renderer), the
is_activecolumn. In the browser's accessibility tree (CDPAccessibility.getFullAXTree), each row whoseis_activeis true is acheckboxnode, checked, named by the field's label: "Active" under en (HotCRM declareslabel: 'Active'), the translated column label under another locale. None has an empty name, and no name carries "Yes", "No", "true" or "false". Rows whoseis_activeis false still draw the destructive "Active — Off" badge, unchanged, and no checkbox. Any other boolean column outside the completion and status names reads the same way: one named checkbox per row.Acceptance notes
titletoString(cellValue)for any non-object value, so hovering a boolean cell shows a rawtrue/falsetooltip, in English under every locale. It does not name the checkbox: atitleon an ancestordivis not part of the checkbox's name. Carrier: none.Session:
https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDBGenerated by Claude Code