feat: checkbox_group carries an accessible name (a11y, #17) - #18
Merged
Conversation
div[role="group"] had no accessible name, so a screen reader announced "checkbox, Ruby" with no indication the boxes belong to a "Tags" group (#17). The leaf invents NO naming API — extra HTML/ARIA attributes pass straight through to the group div, so the caller names it with plain aria: - Bare verb: `f.checkbox_group(..., aria: { label: "Tags" })` (a literal name) or `aria: { labelledby: "some_id" }` (point at an existing element). - Field path: `f.field(:tags, as: :checkbox_group, label:, hint:)` reuses the Control's OWN visible <label>/hint — they get stable ids (label_id:/hint_id: threaded through FormControl AND Plain::Control) and the builder passes aria: { labelledby:, describedby: } through to the group, so the accessible name matches what sighted users see and nothing is duplicated. Absent a name, output is byte-identical to #9 (nil attrs are omitted by Phlex). Id uniqueness follows Rails' form_with contract (derived from scope+name+value, host owns cross-form uniqueness) — documented, not enforced. ## Test Coverage - bare verb: aria: { label: } -> aria-label; aria: { labelledby:/describedby: } passthrough; arbitrary data: passthrough; delimiter-escaping (no breakout); only role+aria-invalid when no aria given (back-compat) - field path: Control's label/hint get ids, group aria-* point at them, exactly one "Tags" text node (no duplicate) - theme parity: bare verb under plain (zero classes); plain field path stamps the Control label/hint ids (guards the daisy/plain drift where Plain::Control overrides view_template) ## Verification - [x] bundle exec rubocop lib spec passes - [x] bundle exec rspec passes (153 examples) Closes #17
mhenrixon
force-pushed
the
issue-17-checkbox-group-accessible-name
branch
from
July 11, 2026 09:43
ff03a6a to
67377b7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #17. Follow-up to #9 —
checkbox_group'sdiv[role="group"]had noaccessible name, so a screen reader announced "checkbox, Ruby" with no clue the
boxes belong to a "Tags" group. Now the group can carry an accessible name.
Approach (revised after review): no bespoke naming API — attributes pass
through. The leaf forwards extra HTML/ARIA attributes straight to the group
div, so the caller names it with plainaria:. This is smaller and moreflexible than an invented
heading:/hint:option.Two entry points
Bare verb — name it with
aria:(a literal string, or an id reference):Field path — reuses the Control's OWN visible
<label>/hint (best practice:the accessible name matches what sighted users see, no duplicate markup):
The builder stamps stable ids on the Control's label/hint (threaded through
FormControlandForms::Plain::Control) and passesaria: { labelledby:, describedby: }through to the group. When no name issupplied, output is byte-identical to #9 (Phlex omits nil attributes).
Id uniqueness (your Q3)
Field ids derive from
scope + name (+ value)exactly like Rails'form_with,which likewise does not guarantee page-wide uniqueness across multiple forms for
the same model. Documented in the README (scope one form to disambiguate) — not
enforced, matching the Rails contract. No code.
Test coverage
aria: { label: }→aria-label;aria: { labelledby:/describedby: }passthrough; arbitrary
data:passthrough; attribute-delimiter escaping (a"in the value is"-escaped so it can't break out —</>are inertinside a quoted attribute); only
role+aria-invalidwhen no aria given.aria-*point at them, exactlyone "Tags" text node (no duplicate).
stamps the Control label/hint ids.
Full suite: 153 examples, 0 failures ·
rubocop lib specclean.Deviations & judgment calls
The first cut added
heading:/hint:options that rendered internal<span id>elements. On review we agreed plain
aria:passthrough is simpler and moreflexible — the leaf now forwards
@attributes.except(:class)to the group andowns no naming vocabulary. Dropped the heading/hint spans, their id generation,
and the
heading_classes/hint_classesseams.aria-label(string) for the bare verb,aria-labelledby(id) for the fieldpath. The field path has a visible label, so pointing at it keeps the
accessible name in sync with what's on screen; the bare verb has no such
element, so a literal
aria-labelstring is the pragmatic default (the callercan still pass
aria: { labelledby: }to reference their own heading).Forms::Plain::Controloverrides
view_template, so it needed the samelabel_id:/hint_id:threading as
FormControl— without it the plain field path pointedaria-labelledbyat a label with no id (a dangling reference). Fixed, with adedicated plain-field-path regression spec.
id:is not settable — the leaf'sid:keyword seeds the checkboxids, so a caller can't override the group div's id via
id:. Out of scope forcheckbox_group: group has no accessible name (no legend/aria-labelledby) #17 (naming); noted here so it isn't mistaken for a passthrough gap.