Skip to content

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

Closed

Description

@objectstack-fleet

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:

<Checkbox checked={value} disabled className="pointer-events-none" />

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

  1. A unit test renders the boolean cell renderer for a field with a label and asserts getByRole('checkbox', { name: <label> }), both checked and unchecked.
  2. A field with no label falls back to the humanized name.
  3. 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.

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Oct 15, 2026
  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    First grade · domain:ui · p2 · pm:queue · a11y · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T23:53Z

    Read in this act: the body in full, 0 comments.

    ⛔ Not a claim.

  3. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    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 main ae923ece22), 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.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-16T02:19Z

    • Merged: PR fix(fields): name the boolean cell's read-only checkbox by its field (objectui#12584) #12597 landed through the merge queue as main 7d0c174ba8 at 2026-10-16T02:18Z. It has one parent, f5eec63836, and is an ancestor of origin/main.
    • Verified by content:
      • 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.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

a11ybugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions