fix(many): keep form field messages out of the control's accessible name#2625
fix(many): keep form field messages out of the control's accessible name#2625git-nandor wants to merge 2 commits into
Conversation
|
Visual regression report✅ No changes.
Baselines come from the |
| // Any consumer-provided `aria-labelledby` (e.g. Select's pill handling) | ||
| // takes precedence. |
There was a problem hiding this comment.
I think the Select's pill handling doesn't use aria-labelledby anymore see #2616 (comment) so this is not a relevant example
Form control messages (error/hint/success) render inside the wrapping <label>, so they were read as part of the field's accessible name. Point each control's name at the label text via aria-labelledby and reference the messages via aria-describedby, so screen readers announce them as the field's description. Label click-to-focus is preserved. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9e15d7b to
8d8c8f4
Compare
ToMESSKa
left a comment
There was a problem hiding this comment.
A vitest test is failing, can you please check?
matyasf
left a comment
There was a problem hiding this comment.
- dateTimeInput is wrong, see the example in the /forms page: aria-describedby and aria-labelledby reference nothing.
- While it works, I generally dislike this component structure. All this mess is needed, because we wrap everything in a , the nice way would be not to do so. I think its OK for now, but we need to completely revamp this in the future. (also support SSR better, now the layout falls apart when loading)
- see my other comments
Address PR review feedback on the form-field messages work: - The aria-describedby wiring referenced a messages element even when the message had no text. FormField (via FormFieldLayout) only renders messages that have text, so this left a dangling reference — e.g. DateTimeInput passes an empty-text error message to its sub-inputs just to force the invalid styling. Gate the wiring on messages that actually render (`messages.some(m => !!m.text)`). Checkbox is unchanged here: it renders messages via FormFieldMessages on `length > 0`, so its reference always resolves. - Drop the now-unnecessary type casts around the aria-* reads in Checkbox, TextArea, NumberInput and RangeInput. - TextInput: expand the comment to explain why messages must be kept out of the name. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matyasf
left a comment
There was a problem hiding this comment.
looks good, just a small nitpick
| // FormField only renders messages that have text, so only treat the field | ||
| // as having messages then. Otherwise the input's `aria-describedby` would | ||
| // reference a messages element that was never rendered — e.g. DateTimeInput | ||
| // passes an empty-text error message to its sub-inputs just to force the | ||
| // invalid styling. |
There was a problem hiding this comment.
Please make a bit more brief comments. Claude can be very very verbose and this just makes the code hard to read. For example this comment is totally unnecessary IMO, the code is pretty self-explanatory
| // FormField renders this control and its `messages` inside a single wrapping | ||
| // <label>, so by default the messages' text becomes part of the control's | ||
| // accessible *name* (e.g. "Password Password must be at least 6 characters"), | ||
| // which is confusing and repeats on every announcement. Messages should be | ||
| // the field's *description* instead. So when there are messages, reference | ||
| // them via `aria-describedby` and pin the name to the label text only via | ||
| // `aria-labelledby`. Keeping the messages inside the <label> preserves the | ||
| // native click-on-label focus behavior. A consumer-provided `aria-labelledby` | ||
| // takes precedence. |
There was a problem hiding this comment.
Again, way too verbose. I'd just write:
"Use the contents of the messages field as ARIA label; user supplied value takes precedence"
INSTUI-4724
Summary
Form control messages render inside the wrapping , so screen readers were reading them as part of the field's accessible name (e.g. "Password Password must be at least 6 characters") instead of as a description.
Each single form control now points its accessible name at the label text only (aria-labelledby) and references the messages as its description (aria-describedby), so a screen reader announces the label first, then the messages. The wrapping is unchanged, so click-on-label focus is preserved.
Test plan
Tab through the given exaple and verify in DevTools Accessibility tab that the Name and Description values are correct. With a screen reader enabled, confirm that the label is announced as the field name, followed by a brief pause before the description is announced.
Co-Authored-By: 🤖 Claude