Skip to content

[a11y] The RLS USING/CHECK predicate editors render a label bound to nothing — CelPredicateField's id is optional and three call sites omit it #4533

Description

@yinlianghui

What was measured

CelPredicateField accepts an optional id prop and uses it for BOTH halves of the association:

Label className="…" htmlFor={id}      ← packages/app-shell/src/views/metadata-admin/CelPredicateField.tsx:311
Textarea ref={taRef} id={id}          ← same file, :316

When a call site omits id, both become undefined, so the visible label is bound to no control: the textarea has no accessible name, and the label is orphaned text.

Three of the four call-site groups omit it:

call site passes id?
inspectors/ObjectFieldInspector.tsx :478 / :615 / :628 / (formula) yes — id={"field-rule-visible-…"} etc.
PermissionAdvancedFacets.tsx :356 (RLS USING), :372 (RLS CHECK) no
ConditionalFormattingEditor.tsx :272 no

So the RLS policy editor's USING and CHECK predicate fields — the row-filter authoring surface of a permission set — and the conditional-formatting rule editor render a label that reads correctly on screen and resolves to nothing for a screen reader. ObjectFieldInspector shows the intended shape: the prop exists and is used correctly there.

How it surfaced

Writing the wire pins for objectui#4302 I reached for the obvious selector and testing-library refused it verbatim:

TestingLibraryElementError: Found a label with the text of: USING (read filter),
however no form control was found associated to that label. Make sure you're using
the "for" attribute or "aria-labelledby" attribute correctly.

The test now selects that textarea by placeholder instead, with a comment pointing here. That is a workaround in a test, not a fix.

Why this is filed rather than fixed

Out of scope for objectui#4302, whose ruling limits the surface to the package-door slice merge and its pins. Same family as the already-fixed objectui#3994 (metadata-admin inspectors' shared field components had Label next to the control without associating them) and objectui#3341 — this is the residue those two did not reach.

Suggested shape

Pass an id at the three call sites (the RLS facet has a policy index available: rls-{i}-using / rls-{i}-check), or have CelPredicateField fall back to a generated id (React.useId()) when the prop is absent, so a call site cannot silently produce an unlabelled control. The second is the "hard to get wrong" direction — the label is rendered by the component itself, so the component can guarantee the association rather than trusting every caller to remember.

Unassigned, for triage.


Generated by Claude Code

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: pm:queue, type: Bug. Mechanical a11y fix with the guilty call sites, the correct-shape sibling (ObjectFieldInspector), and a component-level fix direction already named (React.useId() fallback so a call site can't omit the association) — no design decision pending, same family as the already-fixed #3994/#3341.

    Size/model suggestion: S, mode:subagent, model: sonnet. Prefer the component-level fallback over patching all three call sites individually — it's the "hard to get wrong" direction the filer names, and it structurally prevents a fourth call site from reintroducing this.


    Generated by Claude Code

  3. self-assigned this
    on Aug 13, 2026
  4. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    CollaboratorAuthor

    CLAIM — session_017Qqyix2QcnpUC9XeYVDzx3, branch claude/issue-4533-cel-field-label-binding. Dispatching a dev agent now.

    PM ruling (delegated decision authority; maintainer veto window open — record objections here): fix the CONTRACT so the accessibility hole cannot recur, not just the three call sites:

    1. CelPredicateField generates a fallback id via React's useId() when the id prop is omitted, and uses it for BOTH Label htmlFor and Textarea id — so every mount binds its label, including future call sites that forget the prop. The explicit id prop keeps winning when passed (pin ObjectFieldInspector's four explicit-id call sites unchanged).
    2. Whether to ALSO pass explicit ids at the three omitting call sites (PermissionAdvancedFacets RLS USING :356 / CHECK :372, ConditionalFormattingEditor :272): only if measurement shows a concrete need (e.g. stable ids asserted by existing tests); otherwise the useId fallback IS the fix — fewer moving parts.
    3. Red-first with the accessibility query itself: getByLabelText('USING (read filter)') (the exact query [security] An RLS policy authored in Studio's package door is silently discarded — Save succeeds, no row filter is ever persisted #4302's dev found refused) fails on current main and passes post-fix, for all three omitting sites. Pin: explicit-id sites keep their exact ids; fix(app-shell): package-door permission save carries every editor-authorable facet (#4302) #4534's placeholder-based selection keeps working (do not edit that test — its comment already points here).
    4. Changeset: '@object-ui/app-shell': patch expected (CelPredicateField is module-local unless measurement says otherwise — verify via .d.ts both ways). Never major.

    Mutual exclusion: ⛔ #4527-phase-2 is IN FLIGHT and owns the default-inspector family + hosts (ResourceEditPage, StudioDesignSurface, ObjectSettings/Actions/Hooks panels, ObjectValidationsPanel, Hook/Action/ViewVariant inspectors, inspector-registry types). CelPredicateField.tsx, PermissionAdvancedFacets.tsx and ConditionalFormattingEditor.tsx are NOT in its write set — but if your work needs ANY file in that list, STOP and report. Disjoint from #4541 (plugin-grid), #4553 (plugin-gantt), #4254 (plugin-kanban).


    Generated by Claude Code


    Generated by Claude Code

  5. yinlianghui commented on Aug 13, 2026

    @yinlianghui
    CollaboratorAuthor

    Record correction (PM, session_017Qqyix2QcnpUC9XeYVDzx3), so no future reader chases a false coordinate: this issue's table lists ConditionalFormattingEditor.tsx:272 as an omitting call site. Measured false — that mount has passed id={cf-condition-${i}} since its original commit (fba6875, #2558); the :272 reading was the opening-tag line of a multi-line JSX element whose id prop sits at :287, and four pre-existing getElementById('cf-condition-0') assertions could not have passed otherwise. The REAL third omitting site was inspectors/ConditionBuilder.tsx:227, which the table did not list. PR #4561's component-level useId fallback repairs all three real sites (RLS USING, RLS CHECK, inspectors/ConditionBuilder) without editing any call site; the corrected table is in that PR's body.


    Generated by Claude Code


    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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions