[a11y] add SemanticsValidationResult (#165935)
Add `SemanticsValidationResult` to semantics that maps onto `aria-invalid`. Fixes https://github.com/flutter/flutter/issues/162142
This commit is contained in:
parent
10d2631548
commit
fbbe0f9e7a
@ -46,10 +46,12 @@ class SemanticsAction {
|
||||
static const int _kSetTextIndex = 1 << 21;
|
||||
static const int _kFocusIndex = 1 << 22;
|
||||
static const int _kScrollToOffsetIndex = 1 << 23;
|
||||
// READ THIS: if you add an action here, you MUST update the
|
||||
// numSemanticsActions value in testing/dart/semantics_test.dart and
|
||||
// lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests
|
||||
// will fail.
|
||||
// READ THIS:
|
||||
// - The maximum supported bit index on the web (in JS mode) is 1 << 31.
|
||||
// - If you add an action here, you MUST update the numSemanticsActions value
|
||||
// in testing/dart/semantics_test.dart and
|
||||
// lib/web_ui/test/engine/semantics/semantics_api_test.dart, or tests will
|
||||
// fail.
|
||||
|
||||
/// The equivalent of a user briefly tapping the screen with the finger
|
||||
/// without moving it.
|
||||
@ -555,6 +557,7 @@ class SemanticsFlag {
|
||||
static const int _kIsRequiredIndex = 1 << 30;
|
||||
// READ THIS: if you add a flag here, you MUST update the following:
|
||||
//
|
||||
// - The maximum supported bit index on the web (in JS mode) is 1 << 31.
|
||||
// - Add an appropriately named and documented `static const SemanticsFlag`
|
||||
// field to this class.
|
||||
// - Add the new flag to `_kFlagById` in this file.
|
||||
@ -936,6 +939,30 @@ class SemanticsFlag {
|
||||
String toString() => 'SemanticsFlag.$name';
|
||||
}
|
||||
|
||||
/// The validation result of a form field.
|
||||
///
|
||||
/// The type, shape, and correctness of the value is specific to the kind of
|
||||
/// form field used. For example, a phone number text field may check that the
|
||||
/// value is a properly formatted phone number, and/or that the phone number has
|
||||
/// the right area code. A group of radio buttons may validate that the user
|
||||
/// selected at least one radio option.
|
||||
enum SemanticsValidationResult {
|
||||
/// The node has no validation information attached to it.
|
||||
///
|
||||
/// This is the default value. Most semantics nodes do not contain validation
|
||||
/// information. Typically, only nodes that are part of an input form - text
|
||||
/// fields, checkboxes, radio buttons, dropdowns - are validated and attach
|
||||
/// validation results to their corresponding semantics nodes.
|
||||
none,
|
||||
|
||||
/// The entered value is valid, and no error should be displayed to the user.
|
||||
valid,
|
||||
|
||||
/// The entered value is invalid, and an error message should be communicated
|
||||
/// to the user.
|
||||
invalid,
|
||||
}
|
||||
|
||||
// When adding a new StringAttribute, the classes in these files must be
|
||||
// updated as well.
|
||||
// * engine/src/flutter/lib/web_ui/lib/semantics.dart
|
||||
@ -1154,10 +1181,18 @@ abstract class SemanticsUpdateBuilder {
|
||||
/// The `role` describes the role of this node. Defaults to
|
||||
/// [SemanticsRole.none] if not set.
|
||||
///
|
||||
/// If `validationResult` is not null, indicates the result of validating a
|
||||
/// form field. If null, indicates that the node is not being validated, or
|
||||
/// that the result is unknown. Form fields that validate user input but do
|
||||
/// not use this argument should use other ways to communicate validation
|
||||
/// errors to the user, such as embedding validation error text in the label.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/heading_role
|
||||
/// * https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-level
|
||||
/// * [SemanticsValidationResult], that describes possible values for the
|
||||
/// `validationResult` argument.
|
||||
void updateNode({
|
||||
required int id,
|
||||
required int flags,
|
||||
@ -1196,6 +1231,7 @@ abstract class SemanticsUpdateBuilder {
|
||||
String linkUrl = '',
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
});
|
||||
|
||||
/// Update the custom semantics action associated with the given `id`.
|
||||
@ -1273,6 +1309,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
String linkUrl = '',
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
}) {
|
||||
assert(_matrix4IsValid(transform));
|
||||
assert(
|
||||
@ -1320,6 +1357,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
linkUrl,
|
||||
role.index,
|
||||
controlsNodes,
|
||||
validationResult.index,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1366,6 +1404,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
Handle,
|
||||
Int32,
|
||||
Handle,
|
||||
Int32,
|
||||
)
|
||||
>(symbol: 'SemanticsUpdateBuilder::updateNode')
|
||||
external void _updateNode(
|
||||
@ -1409,6 +1448,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
String linkUrl,
|
||||
int role,
|
||||
List<String>? controlsNodes,
|
||||
int validationResultIndex,
|
||||
);
|
||||
|
||||
@override
|
||||
|
@ -100,6 +100,18 @@ enum class SemanticsRole : int32_t {
|
||||
kAlert = 28,
|
||||
};
|
||||
|
||||
/// C/C++ representation of `SemanticsValidationResult` defined in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
///\warning This must match the `SemanticsValidationResult` enum in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
/// See also:
|
||||
/// - file://./../../../lib/ui/semantics.dart
|
||||
enum class SemanticsValidationResult : int32_t {
|
||||
kNone = 0,
|
||||
kValid = 1,
|
||||
kInvalid = 2,
|
||||
};
|
||||
|
||||
/// C/C++ representation of `SemanticsFlags` defined in
|
||||
/// `lib/ui/semantics.dart`.
|
||||
///\warning This must match the `SemanticsFlags` enum in
|
||||
@ -194,6 +206,7 @@ struct SemanticsNode {
|
||||
|
||||
std::string linkUrl;
|
||||
SemanticsRole role;
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult::kNone;
|
||||
};
|
||||
|
||||
// Contains semantic nodes that need to be updated.
|
||||
|
@ -70,7 +70,8 @@ void SemanticsUpdateBuilder::updateNode(
|
||||
int headingLevel,
|
||||
std::string linkUrl,
|
||||
int role,
|
||||
const std::vector<std::string>& controlsNodes) {
|
||||
const std::vector<std::string>& controlsNodes,
|
||||
int validationResult) {
|
||||
FML_CHECK(scrollChildren == 0 ||
|
||||
(scrollChildren > 0 && childrenInHitTestOrder.data()))
|
||||
<< "Semantics update contained scrollChildren but did not have "
|
||||
@ -124,6 +125,8 @@ void SemanticsUpdateBuilder::updateNode(
|
||||
node.headingLevel = headingLevel;
|
||||
node.linkUrl = std::move(linkUrl);
|
||||
node.role = static_cast<SemanticsRole>(role);
|
||||
node.validationResult =
|
||||
static_cast<SemanticsValidationResult>(validationResult);
|
||||
|
||||
nodes_[id] = node;
|
||||
}
|
||||
|
@ -69,7 +69,8 @@ class SemanticsUpdateBuilder
|
||||
int headingLevel,
|
||||
std::string linkUrl,
|
||||
int role,
|
||||
const std::vector<std::string>& controlsNodes);
|
||||
const std::vector<std::string>& controlsNodes,
|
||||
int validationResult);
|
||||
|
||||
void updateCustomAction(int id,
|
||||
std::string label,
|
||||
|
@ -162,6 +162,7 @@ class SemanticsFlag {
|
||||
static const int _kHasSelectedStateIndex = 1 << 28;
|
||||
static const int _kHasRequiredStateIndex = 1 << 29;
|
||||
static const int _kIsRequiredIndex = 1 << 30;
|
||||
// WARNING: JavaScript can only go up to 32 bits!
|
||||
|
||||
static const SemanticsFlag hasCheckedState = SemanticsFlag._(
|
||||
_kHasCheckedStateIndex,
|
||||
@ -343,6 +344,8 @@ class LocaleStringAttribute extends StringAttribute {
|
||||
}
|
||||
}
|
||||
|
||||
enum SemanticsValidationResult { none, valid, invalid }
|
||||
|
||||
class SemanticsUpdateBuilder {
|
||||
SemanticsUpdateBuilder();
|
||||
|
||||
@ -385,6 +388,7 @@ class SemanticsUpdateBuilder {
|
||||
String? linkUrl,
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
}) {
|
||||
if (transform.length != 16) {
|
||||
throw ArgumentError('transform argument must have 16 entries.');
|
||||
@ -428,6 +432,7 @@ class SemanticsUpdateBuilder {
|
||||
linkUrl: linkUrl,
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -103,6 +103,11 @@ class SemanticIncrementable extends SemanticRole {
|
||||
/// tree should be updated.
|
||||
bool _pendingResync = false;
|
||||
|
||||
@override
|
||||
void updateValidationResult() {
|
||||
SemanticRole.updateAriaInvalid(_element, semanticsObject.validationResult);
|
||||
}
|
||||
|
||||
@override
|
||||
void update() {
|
||||
super.update();
|
||||
|
@ -246,6 +246,7 @@ class SemanticsNodeUpdate {
|
||||
this.linkUrl,
|
||||
required this.role,
|
||||
required this.controlsNodes,
|
||||
required this.validationResult,
|
||||
});
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
@ -358,6 +359,9 @@ class SemanticsNodeUpdate {
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final List<String>? controlsNodes;
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final ui.SemanticsValidationResult validationResult;
|
||||
}
|
||||
|
||||
/// Identifies [SemanticRole] implementations.
|
||||
@ -722,6 +726,10 @@ abstract class SemanticRole {
|
||||
/// the object.
|
||||
@mustCallSuper
|
||||
void update() {
|
||||
if (semanticsObject.isValidationResultDirty) {
|
||||
updateValidationResult();
|
||||
}
|
||||
|
||||
final List<SemanticBehavior>? behaviors = _behaviors;
|
||||
if (behaviors == null) {
|
||||
return;
|
||||
@ -767,6 +775,40 @@ abstract class SemanticRole {
|
||||
removeAttribute('aria-controls');
|
||||
}
|
||||
|
||||
/// Applies the current [SemanticsObject.validationResult] to the DOM managed
|
||||
/// by this role.
|
||||
///
|
||||
/// The default implementation applies the `aria-invalid` attribute to the
|
||||
/// root [SemanticsObject.element]. Specific role implementations may prefer
|
||||
/// to apply it to different elements, depending on their use-case. For
|
||||
/// example, a text field may want to apply it on the underlying `<input>`
|
||||
/// element.
|
||||
void updateValidationResult() {
|
||||
updateAriaInvalid(semanticsObject.element, semanticsObject.validationResult);
|
||||
}
|
||||
|
||||
/// Converts [validationResult] to its ARIA value and sets it as the `aria-invalid`
|
||||
/// attribute of the given [element].
|
||||
///
|
||||
/// If [validationResult] is null, removes the `aria-invalid` attribute from
|
||||
/// the element.
|
||||
static void updateAriaInvalid(DomElement element, ui.SemanticsValidationResult validationResult) {
|
||||
switch (validationResult) {
|
||||
case ui.SemanticsValidationResult.none:
|
||||
element.removeAttribute('aria-invalid');
|
||||
case ui.SemanticsValidationResult.valid:
|
||||
// 'false' may seem counter-intuitive for a "valid" result, but it's
|
||||
// because the ARIA attribute is `aria-invalid`, so its value is
|
||||
// reversed.
|
||||
element.setAttribute('aria-invalid', 'false');
|
||||
case ui.SemanticsValidationResult.invalid:
|
||||
// 'true' may seem counter-intuitive for an "invalid" result, but it's
|
||||
// because the ARIA attribute is `aria-invalid`, so its value is
|
||||
// reversed.
|
||||
element.setAttribute('aria-invalid', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this role was disposed of.
|
||||
bool get isDisposed => _isDisposed;
|
||||
bool _isDisposed = false;
|
||||
@ -1353,6 +1395,18 @@ class SemanticsObject {
|
||||
_dirtyFields |= _linkUrlIndex;
|
||||
}
|
||||
|
||||
/// The result of validating a form field, if the form field is being
|
||||
/// validated, and null otherwise.
|
||||
ui.SemanticsValidationResult get validationResult => _validationResult;
|
||||
ui.SemanticsValidationResult _validationResult = ui.SemanticsValidationResult.none;
|
||||
|
||||
static const int _validationResultIndex = 1 << 27;
|
||||
|
||||
bool get isValidationResultDirty => _isDirty(_validationResultIndex);
|
||||
void _markValidationResultDirty() {
|
||||
_dirtyFields |= _validationResultIndex;
|
||||
}
|
||||
|
||||
/// A unique permanent identifier of the semantics node in the tree.
|
||||
final int id;
|
||||
|
||||
@ -1651,6 +1705,11 @@ class SemanticsObject {
|
||||
_markLinkUrlDirty();
|
||||
}
|
||||
|
||||
if (_validationResult != update.validationResult) {
|
||||
_validationResult = update.validationResult;
|
||||
_markValidationResultDirty();
|
||||
}
|
||||
|
||||
role = update.role;
|
||||
|
||||
if (!unorderedListEqual<String>(controlsNodes, update.controlsNodes)) {
|
||||
|
@ -213,6 +213,11 @@ class SemanticTextField extends SemanticRole {
|
||||
/// different from the host [element].
|
||||
late final DomHTMLElement editableElement;
|
||||
|
||||
@override
|
||||
void updateValidationResult() {
|
||||
SemanticRole.updateAriaInvalid(editableElement, semanticsObject.validationResult);
|
||||
}
|
||||
|
||||
@override
|
||||
bool focusAsRouteDefault() {
|
||||
editableElement.focusWithoutScroll();
|
||||
|
@ -324,13 +324,19 @@ class HtmlPatternMatcher extends Matcher {
|
||||
html.Element pattern,
|
||||
) {
|
||||
for (final MapEntry<Object, String> attribute in pattern.attributes.entries) {
|
||||
final String expectedName = attribute.key as String;
|
||||
final (expectedName, expectMissing) = _parseExpectedAttributeName(attribute.key as String);
|
||||
final String expectedValue = attribute.value;
|
||||
final _Breadcrumbs breadcrumb = parent.attribute(expectedName);
|
||||
|
||||
if (expectedName == 'style') {
|
||||
// Style is a complex attribute that deserves a special comparison algorithm.
|
||||
_matchStyle(parent, mismatches, element, pattern);
|
||||
} else if (expectMissing) {
|
||||
if (element.attributes.containsKey(expectedName)) {
|
||||
mismatches.add(
|
||||
'$breadcrumb: expected attribute $expectedName="${element.attributes[expectedName]}" to be missing but it was present.',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!element.attributes.containsKey(expectedName)) {
|
||||
mismatches.add('$breadcrumb: attribute $expectedName="$expectedValue" missing.');
|
||||
@ -347,6 +353,13 @@ class HtmlPatternMatcher extends Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
(String name, bool expectMissing) _parseExpectedAttributeName(String attributeName) {
|
||||
if (attributeName.endsWith('--missing')) {
|
||||
return (attributeName.substring(0, attributeName.indexOf('--missing')), true);
|
||||
}
|
||||
return (attributeName, false);
|
||||
}
|
||||
|
||||
static Map<String, String> parseStyle(html.Element element) {
|
||||
final Map<String, String> result = <String, String>{};
|
||||
|
||||
|
@ -145,6 +145,9 @@ void runSemanticsTests() {
|
||||
group('requirable', () {
|
||||
_testRequirable();
|
||||
});
|
||||
group('SemanticsValidationResult', () {
|
||||
_testSemanticsValidationResult();
|
||||
});
|
||||
}
|
||||
|
||||
void _testSemanticRole() {
|
||||
@ -4763,6 +4766,58 @@ void _testRequirable() {
|
||||
});
|
||||
}
|
||||
|
||||
void _testSemanticsValidationResult() {
|
||||
test('renders validation result', () {
|
||||
semantics()
|
||||
..debugOverrideTimestampFunction(() => _testTime)
|
||||
..semanticsEnabled = true;
|
||||
|
||||
final SemanticsTester tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
children: <SemanticsNodeUpdate>[
|
||||
// This node does not validate its contents and should not have an
|
||||
// aria-invalid attribute at all.
|
||||
tester.updateNode(id: 1),
|
||||
// This node is valid. aria-invalid should be "false".
|
||||
tester.updateNode(id: 2, validationResult: ui.SemanticsValidationResult.valid),
|
||||
// This node is invalid. aria-invalid should be "true".
|
||||
tester.updateNode(id: 3, validationResult: ui.SemanticsValidationResult.invalid),
|
||||
],
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
tester.expectSemantics('''
|
||||
<sem id="flt-semantic-node-0" aria-invalid--missing>
|
||||
<sem id="flt-semantic-node-1" aria-invalid--missing></sem>
|
||||
<sem id="flt-semantic-node-2" aria-invalid="false"></sem>
|
||||
<sem id="flt-semantic-node-3" aria-invalid="true"></sem>
|
||||
</sem>''');
|
||||
|
||||
// Shift all values, observe that the values changed accordingly
|
||||
tester.updateNode(
|
||||
id: 0,
|
||||
children: <SemanticsNodeUpdate>[
|
||||
// This node is valid. aria-invalid should be "false".
|
||||
tester.updateNode(id: 1, validationResult: ui.SemanticsValidationResult.valid),
|
||||
// This node is invalid. aria-invalid should be "true".
|
||||
tester.updateNode(id: 2, validationResult: ui.SemanticsValidationResult.invalid),
|
||||
// This node does not validate its contents and should not have an
|
||||
// aria-invalid attribute at all.
|
||||
tester.updateNode(id: 3),
|
||||
],
|
||||
);
|
||||
tester.apply();
|
||||
|
||||
tester.expectSemantics('''
|
||||
<sem id="flt-semantic-node-0" aria-invalid--missing>
|
||||
<sem id="flt-semantic-node-1" aria-invalid="false"></sem>
|
||||
<sem id="flt-semantic-node-2" aria-invalid="true"></sem>
|
||||
<sem id="flt-semantic-node-3" aria-invalid--missing></sem>
|
||||
</sem>''');
|
||||
});
|
||||
}
|
||||
|
||||
/// A facade in front of [ui.SemanticsUpdateBuilder.updateNode] that
|
||||
/// supplies default values for semantics attributes.
|
||||
void updateNode(
|
||||
|
@ -121,6 +121,7 @@ class SemanticsTester {
|
||||
String? linkUrl,
|
||||
ui.SemanticsRole? role,
|
||||
List<String>? controlsNodes,
|
||||
ui.SemanticsValidationResult validationResult = ui.SemanticsValidationResult.none,
|
||||
}) {
|
||||
// Flags
|
||||
if (hasCheckedState ?? false) {
|
||||
@ -343,6 +344,7 @@ class SemanticsTester {
|
||||
linkUrl: linkUrl,
|
||||
role: role ?? ui.SemanticsRole.none,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
);
|
||||
_nodeUpdates.add(update);
|
||||
return update;
|
||||
|
@ -7,7 +7,6 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
@ -912,6 +912,9 @@ class RenderCustomPaint extends RenderProxyBox {
|
||||
|
||||
final SemanticsProperties properties = newSemantics.properties;
|
||||
final SemanticsConfiguration config = SemanticsConfiguration();
|
||||
if (properties.role != null) {
|
||||
config.role = properties.role!;
|
||||
}
|
||||
if (properties.sortKey != null) {
|
||||
config.sortKey = properties.sortKey;
|
||||
}
|
||||
@ -1017,6 +1020,9 @@ class RenderCustomPaint extends RenderProxyBox {
|
||||
if (properties.textDirection != null) {
|
||||
config.textDirection = properties.textDirection;
|
||||
}
|
||||
if (config.validationResult != properties.validationResult) {
|
||||
config.validationResult = properties.validationResult;
|
||||
}
|
||||
if (properties.onTap != null) {
|
||||
config.onTap = properties.onTap;
|
||||
}
|
||||
|
@ -4546,6 +4546,9 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
|
||||
if (_properties.controlsNodes != null) {
|
||||
config.controlsNodes = _properties.controlsNodes;
|
||||
}
|
||||
if (config.validationResult != _properties.validationResult) {
|
||||
config.validationResult = _properties.validationResult;
|
||||
}
|
||||
|
||||
// Registering _perform* as action handlers instead of the user provided
|
||||
// ones to ensure that changing a user provided handler from a non-null to
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
|
@ -19,6 +19,7 @@ import 'dart:ui'
|
||||
SemanticsRole,
|
||||
SemanticsUpdate,
|
||||
SemanticsUpdateBuilder,
|
||||
SemanticsValidationResult,
|
||||
StringAttribute,
|
||||
TextDirection;
|
||||
|
||||
@ -32,7 +33,16 @@ import 'binding.dart' show SemanticsBinding;
|
||||
import 'semantics_event.dart';
|
||||
|
||||
export 'dart:ui'
|
||||
show Offset, Rect, SemanticsAction, SemanticsFlag, StringAttribute, TextDirection, VoidCallback;
|
||||
show
|
||||
Offset,
|
||||
Rect,
|
||||
SemanticsAction,
|
||||
SemanticsFlag,
|
||||
SemanticsRole,
|
||||
SemanticsValidationResult,
|
||||
StringAttribute,
|
||||
TextDirection,
|
||||
VoidCallback;
|
||||
|
||||
export 'package:flutter/foundation.dart'
|
||||
show
|
||||
@ -726,6 +736,7 @@ class SemanticsData with Diagnosticable {
|
||||
required this.linkUrl,
|
||||
required this.role,
|
||||
required this.controlsNodes,
|
||||
required this.validationResult,
|
||||
this.tags,
|
||||
this.transform,
|
||||
this.customSemanticsActionIds,
|
||||
@ -991,6 +1002,9 @@ class SemanticsData with Diagnosticable {
|
||||
/// {@macro flutter.semantics.SemanticsProperties.controlsNodes}
|
||||
final Set<String>? controlsNodes;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
/// Whether [flags] contains the given flag.
|
||||
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
|
||||
|
||||
@ -1051,6 +1065,18 @@ class SemanticsData with Diagnosticable {
|
||||
if (controlsNodes != null) {
|
||||
properties.add(IterableProperty<String>('controls', controlsNodes, ifEmpty: null));
|
||||
}
|
||||
if (role != SemanticsRole.none) {
|
||||
properties.add(EnumProperty<SemanticsRole>('role', role, defaultValue: SemanticsRole.none));
|
||||
}
|
||||
if (validationResult != SemanticsValidationResult.none) {
|
||||
properties.add(
|
||||
EnumProperty<SemanticsValidationResult>(
|
||||
'validationResult',
|
||||
validationResult,
|
||||
defaultValue: SemanticsValidationResult.none,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@ -1083,6 +1109,7 @@ class SemanticsData with Diagnosticable {
|
||||
other.headingLevel == headingLevel &&
|
||||
other.linkUrl == linkUrl &&
|
||||
other.role == role &&
|
||||
other.validationResult == validationResult &&
|
||||
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
|
||||
setEquals<String>(controlsNodes, other.controlsNodes);
|
||||
}
|
||||
@ -1118,6 +1145,7 @@ class SemanticsData with Diagnosticable {
|
||||
linkUrl,
|
||||
customSemanticsActionIds == null ? null : Object.hashAll(customSemanticsActionIds!),
|
||||
role,
|
||||
validationResult,
|
||||
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
|
||||
),
|
||||
);
|
||||
@ -1289,6 +1317,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
this.customSemanticsActions,
|
||||
this.role,
|
||||
this.controlsNodes,
|
||||
this.validationResult = SemanticsValidationResult.none,
|
||||
}) : assert(
|
||||
label == null || attributedLabel == null,
|
||||
'Only one of label or attributedLabel should be provided',
|
||||
@ -2097,7 +2126,7 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// A enum to describe what role the subtree represents.
|
||||
///
|
||||
/// Setting the role for a widget subtree helps assistive technologies, such
|
||||
/// as screen readers, understand and interact with the UI correctly.
|
||||
/// as screen readers, to understand and interact with the UI correctly.
|
||||
///
|
||||
/// Defaults to [SemanticsRole.none] if not set, which means the subtree does
|
||||
/// not represent any complex ui or controls.
|
||||
@ -2117,6 +2146,21 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
/// {@endtemplate}
|
||||
final Set<String>? controlsNodes;
|
||||
|
||||
/// {@template flutter.semantics.SemanticsProperties.validationResult}
|
||||
/// Describes the validation result for a form field represented by this
|
||||
/// widget.
|
||||
///
|
||||
/// Providing a validation result helps assistive technologies, such as screen
|
||||
/// readers, to communicate to the user whether they provided correct
|
||||
/// information in a form.
|
||||
///
|
||||
/// Defaults to [SemanticsValidationResult.none] if not set, which means no
|
||||
/// validation information is available for the respective semantics node.
|
||||
///
|
||||
/// For a list of available validation results, see [SemanticsValidationResult].
|
||||
/// {@endtemplate}
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
@ -2155,6 +2199,13 @@ class SemanticsProperties extends DiagnosticableTree {
|
||||
properties.add(StringProperty('tooltip', tooltip, defaultValue: null));
|
||||
properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
|
||||
properties.add(EnumProperty<SemanticsRole>('role', role, defaultValue: null));
|
||||
properties.add(
|
||||
EnumProperty<SemanticsValidationResult>(
|
||||
'validationResult',
|
||||
validationResult,
|
||||
defaultValue: SemanticsValidationResult.none,
|
||||
),
|
||||
);
|
||||
properties.add(DiagnosticsProperty<SemanticsSortKey>('sortKey', sortKey, defaultValue: null));
|
||||
properties.add(
|
||||
DiagnosticsProperty<SemanticsHintOverrides>(
|
||||
@ -2744,7 +2795,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_areUserActionsBlocked != config.isBlockingUserActions ||
|
||||
_headingLevel != config._headingLevel ||
|
||||
_linkUrl != config._linkUrl ||
|
||||
_role != config.role;
|
||||
_role != config.role ||
|
||||
_validationResult != config.validationResult;
|
||||
}
|
||||
|
||||
// TAGS, LABELS, ACTIONS
|
||||
@ -3083,6 +3135,10 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
Set<String>? get controlsNodes => _controlsNodes;
|
||||
Set<String>? _controlsNodes = _kEmptyConfig.controlsNodes;
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
|
||||
SemanticsValidationResult get validationResult => _validationResult;
|
||||
SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult;
|
||||
|
||||
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
|
||||
|
||||
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
|
||||
@ -3150,6 +3206,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
_linkUrl = config._linkUrl;
|
||||
_role = config._role;
|
||||
_controlsNodes = config._controlsNodes;
|
||||
_validationResult = config._validationResult;
|
||||
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
|
||||
|
||||
if (mergeAllDescendantsIntoThisNodeValueChanged) {
|
||||
@ -3200,6 +3257,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
Uri? linkUrl = _linkUrl;
|
||||
SemanticsRole role = _role;
|
||||
Set<String>? controlsNodes = _controlsNodes;
|
||||
SemanticsValidationResult validationResult = _validationResult;
|
||||
final Set<int> customSemanticsActionIds = <int>{};
|
||||
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
|
||||
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
|
||||
@ -3305,6 +3363,17 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
controlsNodes = <String>{...controlsNodes!, ...node._controlsNodes!};
|
||||
}
|
||||
|
||||
if (validationResult == SemanticsValidationResult.none) {
|
||||
validationResult = node._validationResult;
|
||||
} else if (validationResult == SemanticsValidationResult.valid) {
|
||||
// When merging nodes, invalid validation result takes precedence.
|
||||
// Otherwise, validation information could be lost.
|
||||
if (node._validationResult != SemanticsValidationResult.none &&
|
||||
node._validationResult != SemanticsValidationResult.valid) {
|
||||
validationResult = node._validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@ -3339,6 +3408,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
linkUrl: linkUrl,
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
);
|
||||
}
|
||||
|
||||
@ -3425,6 +3495,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
|
||||
linkUrl: data.linkUrl?.toString() ?? '',
|
||||
role: data.role,
|
||||
controlsNodes: data.controlsNodes?.toList(),
|
||||
validationResult: data.validationResult,
|
||||
);
|
||||
_dirty = false;
|
||||
}
|
||||
@ -5595,6 +5666,14 @@ class SemanticsConfiguration {
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
|
||||
SemanticsValidationResult get validationResult => _validationResult;
|
||||
SemanticsValidationResult _validationResult = SemanticsValidationResult.none;
|
||||
set validationResult(SemanticsValidationResult value) {
|
||||
_validationResult = value;
|
||||
_hasBeenAnnotated = true;
|
||||
}
|
||||
|
||||
// TAGS
|
||||
|
||||
/// The set of tags that this configuration wants to add to all child
|
||||
@ -5785,6 +5864,15 @@ class SemanticsConfiguration {
|
||||
_controlsNodes = <String>{..._controlsNodes!, ...child._controlsNodes!};
|
||||
}
|
||||
|
||||
if (child._validationResult != _validationResult) {
|
||||
if (child._validationResult == SemanticsValidationResult.invalid) {
|
||||
// Invalid result always takes precedence.
|
||||
_validationResult = SemanticsValidationResult.invalid;
|
||||
} else if (_validationResult == SemanticsValidationResult.none) {
|
||||
_validationResult = child._validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
_hasBeenAnnotated = hasBeenAnnotated || child.hasBeenAnnotated;
|
||||
}
|
||||
|
||||
@ -5827,7 +5915,8 @@ class SemanticsConfiguration {
|
||||
.._headingLevel = _headingLevel
|
||||
.._linkUrl = _linkUrl
|
||||
.._role = _role
|
||||
.._controlsNodes = _controlsNodes;
|
||||
.._controlsNodes = _controlsNodes
|
||||
.._validationResult = _validationResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' as ui show Image, ImageFilter, SemanticsRole, TextHeightBehavior;
|
||||
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior;
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@ -7382,8 +7382,9 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
VoidCallback? onDidLoseAccessibilityFocus,
|
||||
VoidCallback? onFocus,
|
||||
Map<CustomSemanticsAction, VoidCallback>? customSemanticsActions,
|
||||
ui.SemanticsRole? role,
|
||||
SemanticsRole? role,
|
||||
Set<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
}) : this.fromProperties(
|
||||
key: key,
|
||||
child: child,
|
||||
@ -7461,6 +7462,7 @@ class Semantics extends SingleChildRenderObjectWidget {
|
||||
: null,
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -769,6 +769,12 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
|
||||
Form.maybeOf(context)?._register(this);
|
||||
|
||||
final Widget child = Semantics(
|
||||
validationResult:
|
||||
hasError ? SemanticsValidationResult.invalid : SemanticsValidationResult.valid,
|
||||
child: widget.builder(this),
|
||||
);
|
||||
|
||||
if (Form.maybeOf(context)?.widget.autovalidateMode == AutovalidateMode.onUnfocus &&
|
||||
widget.autovalidateMode != AutovalidateMode.always ||
|
||||
widget.autovalidateMode == AutovalidateMode.onUnfocus) {
|
||||
@ -783,11 +789,11 @@ class FormFieldState<T> extends State<FormField<T>> with RestorationMixin {
|
||||
}
|
||||
},
|
||||
focusNode: _focusNode,
|
||||
child: widget.builder(this),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return widget.builder(this);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@
|
||||
library;
|
||||
|
||||
import 'dart:collection';
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
@ -6,7 +6,6 @@
|
||||
library;
|
||||
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -1823,6 +1823,7 @@ void main() {
|
||||
hasPasteAction: true,
|
||||
hasMoveCursorBackwardByCharacterAction: true,
|
||||
hasMoveCursorBackwardByWordAction: true,
|
||||
validationResult: SemanticsValidationResult.valid,
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@ -306,6 +307,7 @@ void main() {
|
||||
hasPasteAction: true,
|
||||
hasMoveCursorBackwardByCharacterAction: true,
|
||||
hasMoveCursorBackwardByWordAction: true,
|
||||
validationResult: SemanticsValidationResult.valid,
|
||||
),
|
||||
);
|
||||
semantics.dispose();
|
||||
|
@ -228,8 +228,9 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
|
||||
required Int32List additionalActions,
|
||||
int headingLevel = 0,
|
||||
String? linkUrl,
|
||||
ui.SemanticsRole role = ui.SemanticsRole.none,
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
}) {
|
||||
// Makes sure we don't send the same id twice.
|
||||
assert(!observations.containsKey(id));
|
||||
|
@ -288,6 +288,94 @@ void _defineTests() {
|
||||
semanticsTester.dispose();
|
||||
});
|
||||
|
||||
testWidgets('provides semantic role', (WidgetTester tester) async {
|
||||
final SemanticsTester semanticsTester = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CustomPaint(
|
||||
foregroundPainter: _PainterWithSemantics(
|
||||
semantics: const CustomPainterSemantics(
|
||||
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
|
||||
properties: SemanticsProperties(
|
||||
role: SemanticsRole.table,
|
||||
label: 'this is a table',
|
||||
textDirection: TextDirection.rtl,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
semanticsTester,
|
||||
hasSemantics(
|
||||
TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
id: 1,
|
||||
rect: TestSemantics.fullScreen,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 2,
|
||||
role: SemanticsRole.table,
|
||||
label: 'this is a table',
|
||||
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
semanticsTester.dispose();
|
||||
});
|
||||
|
||||
testWidgets('provides semantic validation result', (WidgetTester tester) async {
|
||||
final SemanticsTester semanticsTester = SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
CustomPaint(
|
||||
foregroundPainter: _PainterWithSemantics(
|
||||
semantics: const CustomPainterSemantics(
|
||||
rect: Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
|
||||
properties: SemanticsProperties(
|
||||
textField: true,
|
||||
label: 'email address',
|
||||
textDirection: TextDirection.ltr,
|
||||
validationResult: SemanticsValidationResult.invalid,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
semanticsTester,
|
||||
hasSemantics(
|
||||
TestSemantics.root(
|
||||
children: <TestSemantics>[
|
||||
TestSemantics.rootChild(
|
||||
id: 1,
|
||||
rect: TestSemantics.fullScreen,
|
||||
children: <TestSemantics>[
|
||||
TestSemantics(
|
||||
id: 2,
|
||||
flags: <SemanticsFlag>[SemanticsFlag.isTextField],
|
||||
label: 'email address',
|
||||
validationResult: SemanticsValidationResult.invalid,
|
||||
rect: const Rect.fromLTRB(1.0, 1.0, 2.0, 2.0),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
semanticsTester.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Can toggle semantics on, off, on without crash', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
CustomPaint(
|
||||
|
@ -1539,4 +1539,53 @@ void main() {
|
||||
expect(find.text('foo/error'), findsOneWidget);
|
||||
expect(find.text('bar/error'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('FormField adds validation result to the semantics of the child', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
|
||||
String? errorText;
|
||||
|
||||
Future<void> pumpWidget() async {
|
||||
formKey.currentState?.reset();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: Material(
|
||||
child: Form(
|
||||
key: formKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: TextFormField(validator: (String? value) => errorText),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.enterText(find.byType(TextFormField), 'Hello');
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
// Test valid case
|
||||
await pumpWidget();
|
||||
expect(
|
||||
tester.getSemantics(find.byType(TextFormField).last),
|
||||
containsSemantics(isTextField: true, validationResult: SemanticsValidationResult.valid),
|
||||
);
|
||||
|
||||
// Test invalid case
|
||||
errorText = 'Error';
|
||||
await pumpWidget();
|
||||
expect(
|
||||
tester.getSemantics(find.byType(TextFormField).last),
|
||||
containsSemantics(isTextField: true, validationResult: SemanticsValidationResult.invalid),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -1698,6 +1698,127 @@ void main() {
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('RenderSemanticsAnnotations provides validation result', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
Future<SemanticsConfiguration> pumpValidationResult(SemanticsValidationResult result) async {
|
||||
final ValueKey<String> key = ValueKey<String>('validation-$result');
|
||||
await tester.pumpWidget(
|
||||
Semantics(
|
||||
key: key,
|
||||
validationResult: result,
|
||||
child: Text('Validation result $result', textDirection: TextDirection.ltr),
|
||||
),
|
||||
);
|
||||
final RenderSemanticsAnnotations object = tester.renderObject<RenderSemanticsAnnotations>(
|
||||
find.byKey(key),
|
||||
);
|
||||
final SemanticsConfiguration config = SemanticsConfiguration();
|
||||
object.describeSemanticsConfiguration(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
final SemanticsConfiguration noneResult = await pumpValidationResult(
|
||||
SemanticsValidationResult.none,
|
||||
);
|
||||
expect(noneResult.validationResult, SemanticsValidationResult.none);
|
||||
|
||||
final SemanticsConfiguration validResult = await pumpValidationResult(
|
||||
SemanticsValidationResult.valid,
|
||||
);
|
||||
expect(validResult.validationResult, SemanticsValidationResult.valid);
|
||||
|
||||
final SemanticsConfiguration invalidResult = await pumpValidationResult(
|
||||
SemanticsValidationResult.invalid,
|
||||
);
|
||||
expect(invalidResult.validationResult, SemanticsValidationResult.invalid);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets('validation result precedence', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = SemanticsTester(tester);
|
||||
|
||||
Future<void> expectValidationResult({
|
||||
required SemanticsValidationResult outer,
|
||||
required SemanticsValidationResult inner,
|
||||
required SemanticsValidationResult expected,
|
||||
}) async {
|
||||
const ValueKey<String> key = ValueKey<String>('validated-widget');
|
||||
await tester.pumpWidget(
|
||||
Semantics(
|
||||
validationResult: outer,
|
||||
child: Semantics(
|
||||
validationResult: inner,
|
||||
child: Text(
|
||||
key: key,
|
||||
'Outer = $outer; inner = $inner',
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final SemanticsNode result = tester.getSemantics(find.byKey(key));
|
||||
expect(
|
||||
result,
|
||||
containsSemantics(label: 'Outer = $outer; inner = $inner', validationResult: expected),
|
||||
);
|
||||
}
|
||||
|
||||
// Outer is none
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.none,
|
||||
inner: SemanticsValidationResult.none,
|
||||
expected: SemanticsValidationResult.none,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.none,
|
||||
inner: SemanticsValidationResult.valid,
|
||||
expected: SemanticsValidationResult.valid,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.none,
|
||||
inner: SemanticsValidationResult.invalid,
|
||||
expected: SemanticsValidationResult.invalid,
|
||||
);
|
||||
|
||||
// Outer is valid
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.valid,
|
||||
inner: SemanticsValidationResult.none,
|
||||
expected: SemanticsValidationResult.valid,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.valid,
|
||||
inner: SemanticsValidationResult.valid,
|
||||
expected: SemanticsValidationResult.valid,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.valid,
|
||||
inner: SemanticsValidationResult.invalid,
|
||||
expected: SemanticsValidationResult.invalid,
|
||||
);
|
||||
|
||||
// Outer is invalid
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.invalid,
|
||||
inner: SemanticsValidationResult.none,
|
||||
expected: SemanticsValidationResult.invalid,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.invalid,
|
||||
inner: SemanticsValidationResult.valid,
|
||||
expected: SemanticsValidationResult.invalid,
|
||||
);
|
||||
await expectValidationResult(
|
||||
outer: SemanticsValidationResult.invalid,
|
||||
inner: SemanticsValidationResult.invalid,
|
||||
expected: SemanticsValidationResult.invalid,
|
||||
);
|
||||
|
||||
semantics.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
class CustomSortKey extends OrdinalSortKey {
|
||||
|
@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/physics.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
@ -56,6 +54,7 @@ class TestSemantics {
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
this.validationResult = SemanticsValidationResult.none,
|
||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
tags = tags?.toSet() ?? <SemanticsTag>{};
|
||||
@ -80,6 +79,7 @@ class TestSemantics {
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
this.validationResult = SemanticsValidationResult.none,
|
||||
}) : id = 0,
|
||||
assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
@ -120,6 +120,7 @@ class TestSemantics {
|
||||
this.scrollChildren,
|
||||
Iterable<SemanticsTag>? tags,
|
||||
this.role = SemanticsRole.none,
|
||||
this.validationResult = SemanticsValidationResult.none,
|
||||
}) : assert(flags is int || flags is List<SemanticsFlag>),
|
||||
assert(actions is int || actions is List<SemanticsAction>),
|
||||
transform = _applyRootChildScale(transform),
|
||||
@ -232,6 +233,14 @@ class TestSemantics {
|
||||
|
||||
final TextSelection? textSelection;
|
||||
|
||||
/// The validation result for this node, if any.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SemanticsValidationResult], which is the enum listing possible values
|
||||
/// for this field.
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
static Matrix4 _applyRootChildScale(Matrix4? transform) {
|
||||
final Matrix4 result = Matrix4.diagonal3Values(3.0, 3.0, 1.0);
|
||||
if (transform != null) {
|
||||
@ -392,6 +401,12 @@ class TestSemantics {
|
||||
return fail('expected node id $id to have role $role but found role ${node.role}');
|
||||
}
|
||||
|
||||
if (validationResult != node.validationResult) {
|
||||
return fail(
|
||||
'expected node id $id to have validationResult $validationResult but found validationResult ${node.validationResult}',
|
||||
);
|
||||
}
|
||||
|
||||
if (children.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:ui' show SemanticsRole;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
@ -685,6 +685,7 @@ Matcher matchesSemantics({
|
||||
int? platformViewId,
|
||||
int? maxValueLength,
|
||||
int? currentValueLength,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
// Flags //
|
||||
bool hasCheckedState = false,
|
||||
bool isChecked = false,
|
||||
@ -768,6 +769,7 @@ Matcher matchesSemantics({
|
||||
customActions: customActions,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
validationResult: validationResult,
|
||||
// Flags
|
||||
hasCheckedState: hasCheckedState,
|
||||
isChecked: isChecked,
|
||||
@ -879,6 +881,7 @@ Matcher containsSemantics({
|
||||
int? platformViewId,
|
||||
int? maxValueLength,
|
||||
int? currentValueLength,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
// Flags
|
||||
bool? hasCheckedState,
|
||||
bool? isChecked,
|
||||
@ -962,6 +965,7 @@ Matcher containsSemantics({
|
||||
customActions: customActions,
|
||||
maxValueLength: maxValueLength,
|
||||
currentValueLength: currentValueLength,
|
||||
validationResult: validationResult,
|
||||
// Flags
|
||||
hasCheckedState: hasCheckedState,
|
||||
isChecked: isChecked,
|
||||
@ -2398,6 +2402,7 @@ class _MatchesSemanticsData extends Matcher {
|
||||
required this.platformViewId,
|
||||
required this.maxValueLength,
|
||||
required this.currentValueLength,
|
||||
required this.validationResult,
|
||||
// Flags
|
||||
required bool? hasCheckedState,
|
||||
required bool? isChecked,
|
||||
@ -2552,6 +2557,7 @@ class _MatchesSemanticsData extends Matcher {
|
||||
final int? maxValueLength;
|
||||
final int? currentValueLength;
|
||||
final List<Matcher>? children;
|
||||
final SemanticsValidationResult validationResult;
|
||||
|
||||
/// There are three possible states for these two maps:
|
||||
///
|
||||
@ -2665,6 +2671,9 @@ class _MatchesSemanticsData extends Matcher {
|
||||
if (hintOverrides != null) {
|
||||
description.add(' with custom hints: $hintOverrides');
|
||||
}
|
||||
if (validationResult != SemanticsValidationResult.none) {
|
||||
description.add(' with validation result: $validationResult');
|
||||
}
|
||||
if (children != null) {
|
||||
description.add(' with children:\n ');
|
||||
final List<_MatchesSemanticsData> childMatches = children!.cast<_MatchesSemanticsData>();
|
||||
@ -2801,6 +2810,9 @@ class _MatchesSemanticsData extends Matcher {
|
||||
if (maxValueLength != null && maxValueLength != data.maxValueLength) {
|
||||
return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
|
||||
}
|
||||
if (validationResult != data.validationResult) {
|
||||
return failWithDescription(matchState, 'validationResult was: ${data.validationResult}');
|
||||
}
|
||||
if (actions.isNotEmpty) {
|
||||
final List<SemanticsAction> unexpectedActions = <SemanticsAction>[];
|
||||
final List<SemanticsAction> missingActions = <SemanticsAction>[];
|
||||
|
@ -731,6 +731,7 @@ void main() {
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1034,6 +1035,7 @@ void main() {
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1133,6 +1135,7 @@ void main() {
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
@ -1239,6 +1242,7 @@ void main() {
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
|
||||
|
||||
@ -1271,6 +1275,7 @@ void main() {
|
||||
linkUrl: Uri(path: 'l'),
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
|
||||
|
||||
@ -1359,6 +1364,7 @@ void main() {
|
||||
linkUrl: null,
|
||||
role: ui.SemanticsRole.none,
|
||||
controlsNodes: null,
|
||||
validationResult: SemanticsValidationResult.none,
|
||||
);
|
||||
final _FakeSemanticsNode node = _FakeSemanticsNode(data);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user