-
Notifications
You must be signed in to change notification settings - Fork 28.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Accessibility] Add required semantics flags #164585
Conversation
bf897e0
to
c7fa33d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mostly lgtm, left a question
void update() { | ||
if (semanticsObject.isFlagsDirty) { | ||
if (semanticsObject.isRequirable) { | ||
owner.setAttribute('aria-required', semanticsObject.isRequired); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may need to double check for roles that create additional dom element such as text_field, I think this will add aria-required to the wrapper div instead of the input tag
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. It looks like neither the input tag nor its wrapper div have the aria-required
attribute, even though the Flutter semantics tree has isRequired
. I'll investigate and add a test for this scenario!
Repro...
App:
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
void main() {
runApp(
MaterialApp(
home: Scaffold(
body: Semantics(required: true, child: TextField()),
),
),
);
SemanticsBinding.instance.ensureSemantics();
}
Semantics tree:
SemanticsNode#0
│ Rect.fromLTRB(0.0, 0.0, 2400.0, 1604.0)
│
└─SemanticsNode#1
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 802.0) scaled by 2.0x
│ textDirection: ltr
│
└─SemanticsNode#2
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 802.0)
│ sortKey: OrdinalSortKey#cda74(order: 0.0)
│
└─SemanticsNode#3
│ Rect.fromLTRB(0.0, 0.0, 1200.0, 802.0)
│ flags: scopesRoute
│
└─SemanticsNode#4
Rect.fromLTRB(0.0, 0.0, 1200.0, 48.0)
actions: didGainAccessibilityFocus, didLoseAccessibilityFocus,
focus, tap
flags: isTextField, hasEnabledState, isEnabled, hasRequiredState,
isRequired
textDirection: ltr
text selection: [0, 0]
currentValueLength: 0
HTML generated by Flutter Web:
<flutter-view flt-view-id="0" tabindex="0" style="...">
<flt-semantics-host style="...">
<flt-semantics id="flt-semantic-node-0"
style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-1"
style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-2"
style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-3" role="dialog"
style="...">
<flt-semantics-container style="...">
<flt-semantics id="flt-semantic-node-4"
style="...">
<input type="text" spellcheck="false" autocorrect="on" autocomplete="on"
data-semantics-role="text-field"
style="...">
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-container>
</flt-semantics>
</flt-semantics-host>
</flutter-view>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, the responsibility for applying aria-required
probably belongs in SemanticRole
implementation classes: SemanticCheckable
, SemanticIncrementable
, SemanticTextField
. Maybe also radiogroup? Not every role supports this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it is not even on the div, it is probably that the semanticsRole doesn't use basic super constructor. You will need to manually add the behavior to those classes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I verified the following:
SemanticsCheckable
- works as expectedSemanticIncrementable
- Do we have an incrementable control with optional values? The MaterialSlider.value
is non-nullable, you cannot have aSlider
without a value.SemanticsTextField
- fixed!
I believe this is addressed, but please let me know if you have follow-up concerns!
26e3e08
to
7893e01
Compare
@@ -640,6 +642,10 @@ abstract class SemanticRole { | |||
addSemanticBehavior(Expandable(semanticsObject, this)); | |||
} | |||
|
|||
void addRequirableBehavior() { | |||
addSemanticBehavior(Requirable(semanticsObject, this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For performance, can we add the behavior only if semanticsObject.isRequirable
is true? Or do we expect that the value of isRequirable
may change during the lifetime of a node? According to SemanticsConfiguration
below, once isRequired
is set, the node becomes requirable forever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For performance, can we add the behavior only if
semanticsObject.isRequirable
is true? Or do we expect that the value ofisRequirable
may change during the lifetime of a node?
I expect the value of isRequirable
can change during the lifetime of a node. For example, imagine I have a form that has a shipping address, a billing address, and a checkbox to use the shipping address as the billing address. If I uncheck the checkbox, the billing address becomes required.
According to
SemanticsConfiguration
below, onceisRequired
is set, the node becomes requirable forever.
That's correct. However, when a render object's semantics needs to update, the SemanticsConfiguration
is thrown away and a new one is created:
flutter/packages/flutter/lib/src/rendering/object.dart
Lines 5528 to 5537 in c38f0b7
void markNeedsUpdate() { | |
final SemanticsNode? producedSemanticsNode = cachedSemanticsNode; | |
// Dirty the semantics tree starting at `this` until we have reached a | |
// RenderObject that is a semantics boundary. All semantics past this | |
// RenderObject are still up-to date. Therefore, we will later only rebuild | |
// the semantics subtree starting at the identified semantics boundary. | |
final bool wasSemanticsBoundary = | |
producedSemanticsNode != null && configProvider.wasSemanticsBoundary; | |
configProvider.clear(); |
flutter/packages/flutter/lib/src/rendering/object.dart
Lines 4679 to 4683 in c38f0b7
void clear() { | |
_isEffectiveConfigWritable = false; | |
_effectiveConfiguration = null; | |
_originalConfiguration = null; | |
} |
flutter/packages/flutter/lib/src/rendering/object.dart
Lines 4631 to 4648 in c38f0b7
/// The original config without any change through [updateConfig]. | |
/// | |
/// This is typically use to recalculate certain properties when mutating | |
/// [effective] since [effective] may contain stale data from previous update. | |
/// Examples are [SemanticsConfiguration.isBlockingUserActions] or | |
/// [SemanticsConfiguration.elevation]. Otherwise, use [effective] instead. | |
SemanticsConfiguration get original { | |
if (_originalConfiguration == null) { | |
_effectiveConfiguration = _originalConfiguration = SemanticsConfiguration(); | |
_renderObject.describeSemanticsConfiguration(_originalConfiguration!); | |
assert( | |
!_originalConfiguration!.explicitChildNodes || | |
_originalConfiguration!.childConfigurationsDelegate == null, | |
'A SemanticsConfiguration with explicitChildNode set to true cannot have a non-null childConfigsDelegate.', | |
); | |
} | |
return _originalConfiguration!; | |
} |
SemanticsConfiguration
mutation is used when its parent needs to update some information:
flutter/packages/flutter/lib/src/rendering/object.dart
Lines 4605 to 4608 in c38f0b7
/// In some cases during [PipelineOwner.flushSemantics], the config has to be | |
/// mutated due to [_SemanticsParentData] update to propagate updated property | |
/// to semantics node. One should use [updateConfig] to update the config in this | |
/// case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, once all comments are addressed
7893e01
to
53df1e3
Compare
This adds "required" semantic nodes, which indicate a node that requires user input before a form can be submitted.
On Flutter Web, these get converted into
aria-required
attributes.Addresses #162139
Example app
DropdownMenu
which currently produces an incorrect semantics tree. That will be fixed by #163638.Today, you wrap your control in a
Semantics(required: true, child ...)
. For example:Example app with required semantic flags...
Semantics tree...
HTML generated by Flutter web...
In the future, we can update Material and Cupertino widgets to automatically make their semantics node required when desirable.
Pre-launch Checklist
///
).If you need help, consider asking for advice on the #hackers-new channel on Discord.