[web] Pass form validation errors to screen readers via aria-description#180556
[web] Pass form validation errors to screen readers via aria-description#180556auto-submit[bot] merged 22 commits intoflutter:masterfrom
Conversation
23ac5ee to
98e84f3
Compare
98e84f3 to
9958ae4
Compare
| // Note: The visual hintText is already shown as placeholder, so we | ||
| // only need to expose it via semantics when there's no error. | ||
| final String? errorText = | ||
| widget.decoration?.errorText ?? widget.decoration?.error?.toString(); |
There was a problem hiding this comment.
widget toString() contains debug message and can be potentially nonsensical string in release mode. we can't use it as hint. In case they provide an error widget, we should consider let developer provide one string that represent the error
There was a problem hiding this comment.
good catch. updated.
| // only need to expose it via semantics when there's no error. | ||
| final String? errorText = | ||
| widget.decoration?.errorText ?? widget.decoration?.error?.toString(); | ||
| final String? semanticsHint = errorText ?? widget.decoration?.hintText; |
There was a problem hiding this comment.
This ignores hinttext if error text is presented. I would guess they should concatenate together. Can you double check the native material textfield's behavior if both are present?
There was a problem hiding this comment.
according to this link, When an error is set, it replaces the helper text visually. The current logic errorText ?? hintText matches how native Material handles this : errors override helper/hint text rather than being combined with it.
chunhtai
left a comment
There was a problem hiding this comment.
Think a bit more, setting the semantics of hint text should probably be in InputDecorator widget since that is where the UI are built.
| // only need to expose it via semantics when there's no error. | ||
| final String? errorText = | ||
| widget.decoration?.errorText ?? widget.decoration?.error?.toString(); | ||
| final String? semanticsHint = errorText ?? widget.decoration?.hintText; |
| // Note: The visual hintText is already shown as placeholder, so we | ||
| // only need to expose it via semantics when there's no error. | ||
| final String? semanticsHint = | ||
| widget.decoration?.errorText ?? widget.decoration?.hintText; |
There was a problem hiding this comment.
Do we still want to use widget.decoration?.hintText if the widget.decoration?.error widget is present? I imagine the hintText won't be visible in this case?
There was a problem hiding this comment.
Good catch! Updated the logic.
InputDecorator is the visual decoration layer inside. It doesn't have access to the Semantics wrapper that TextField adds around it. The hint property we're setting becomes aria-description on the element, and that wrapping is done by TextField. |
Why does it need to? It can introduce its own semantics wrapper which should merge with TextField's. I remember TextField's semantics wrapper are built to be able to merge with other just for this reason |
| // will address complex cases (custom error widgets, errors outside | ||
| // InputDecoration, custom announcement ordering). See | ||
| // https://github.com/flutter/flutter/issues/180496#issuecomment-3713178684. | ||
| final String? semanticsHint = decoration.errorText; |
There was a problem hiding this comment.
@chunhtai and @mdebbar , I updated the previous logic to this.
Previous logic
final bool hasErrorWidget = decoration.error != null || decoration.errorText != null;
final String? semanticsHint = hasErrorWidget ? decoration.errorText : decoration.hintText;
Issue with previous logic
hintText was entering the semantics tree twice:
- Automatically — the Text(hintText) widget inside InputDecorator gets merged up into the text field's semantics via _RenderDecoration's markAsMergeUp.
- Explicitly — our Semantics(hint: hintText) wrapper also set it on the same node.
The semantics framework concatenates both with \n, producing "Search Google Pay\nSearch Google Pay" error in google internal app.
Fix
Only pass errorText (not hintText) in the explicit wrapper, since hintText already has the automatic path.
There was a problem hiding this comment.
I don't fully understand the semantics merging logic, but from what you said, does that mean decoration.errorText and hintText will be concatenated now?
There are tests for labelText+hintText and for labelText+errorText. Can we try a test for errorText+hintText?
I'll defer to @chunhtai who knows more about this. Everything else looks good to me!
There was a problem hiding this comment.
No, errorText and hintText do not concatenate. They end up in separate semantics properties:
-
hintText → merges into label (via the hint Text widget's RenderParagraph setting config.attributedLabel then markAsMergeUp)
-
errorText → goes into hint (via our explicit Semantics(hint:) wrapper).
Added a test for errorText+hintText to verify this.
There was a problem hiding this comment.
The change sounds good to me. I will take another review
| // will address complex cases (custom error widgets, errors outside | ||
| // InputDecoration, custom announcement ordering). See | ||
| // https://github.com/flutter/flutter/issues/180496#issuecomment-3713178684. | ||
| final String? semanticsHint = decoration.errorText; |
There was a problem hiding this comment.
I don't fully understand the semantics merging logic, but from what you said, does that mean decoration.errorText and hintText will be concatenated now?
There are tests for labelText+hintText and for labelText+errorText. Can we try a test for errorText+hintText?
I'll defer to @chunhtai who knows more about this. Everything else looks good to me!
Fixes part of #180496
Summary
Pass form validation errors to screen readers via
aria-descriptionon text fields.Before:
https://flutter-demo-04-before.web.app/
After:
https://flutter-demo-04-after.web.app/
Limitations & Future Work
This handles string-based errors (
errorText). For more complex cases(brought in #180496 (comment)), a follow-up implementation usingaria-describedbywith element IDs is tracked in #180496:InputDecoration.error)InputDecorationThe
aria-descriptionapproach (current) andaria-describedbyapproach (future) can coexist per ARIA specifications.Related