Adds semantics input type (#165925)

<!--
Thanks for filing a pull request!
Reviewers are typically assigned within a week of filing a request.
To learn more about code review, see our documentation on Tree Hygiene:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
-->

fixes https://github.com/flutter/flutter/issues/162130

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
chunhtai 2025-04-04 14:15:12 -07:00 committed by GitHub
parent 790d1b1d9a
commit 7afe7a5f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 308 additions and 49 deletions

View File

@ -235,6 +235,7 @@ void sendSemanticsUpdate() {
headingLevel: 0, headingLevel: 0,
linkUrl: '', linkUrl: '',
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
_semanticsUpdate(builder.build()); _semanticsUpdate(builder.build());
} }
@ -289,6 +290,7 @@ void sendSemanticsUpdateWithRole() {
linkUrl: '', linkUrl: '',
role: SemanticsRole.tab, role: SemanticsRole.tab,
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
_semanticsUpdate(builder.build()); _semanticsUpdate(builder.build());
} }

View File

@ -506,6 +506,29 @@ enum SemanticsRole {
alert, alert,
} }
/// Describe the type of data for an input field.
///
/// This is typically used to complement text fields.
enum SemanticsInputType {
/// The default for non text field.
none,
/// Describes a generic text field.
text,
/// Describes a url text field.
url,
/// Describes a text field for phone input.
phone,
/// Describes a text field that act as a search box.
search,
/// Describes a text field for email input.
email,
}
/// A Boolean value that can be associated with a semantics node. /// A Boolean value that can be associated with a semantics node.
// //
// When changes are made to this class, the equivalent APIs in // When changes are made to this class, the equivalent APIs in
@ -1232,6 +1255,7 @@ abstract class SemanticsUpdateBuilder {
SemanticsRole role = SemanticsRole.none, SemanticsRole role = SemanticsRole.none,
required List<String>? controlsNodes, required List<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
required SemanticsInputType inputType,
}); });
/// Update the custom semantics action associated with the given `id`. /// Update the custom semantics action associated with the given `id`.
@ -1310,6 +1334,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
SemanticsRole role = SemanticsRole.none, SemanticsRole role = SemanticsRole.none,
required List<String>? controlsNodes, required List<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
required SemanticsInputType inputType,
}) { }) {
assert(_matrix4IsValid(transform)); assert(_matrix4IsValid(transform));
assert( assert(
@ -1358,6 +1383,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
role.index, role.index,
controlsNodes, controlsNodes,
validationResult.index, validationResult.index,
inputType.index,
); );
} }
@ -1405,6 +1431,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
Int32, Int32,
Handle, Handle,
Int32, Int32,
Int32,
) )
>(symbol: 'SemanticsUpdateBuilder::updateNode') >(symbol: 'SemanticsUpdateBuilder::updateNode')
external void _updateNode( external void _updateNode(
@ -1449,6 +1476,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
int role, int role,
List<String>? controlsNodes, List<String>? controlsNodes,
int validationResultIndex, int validationResultIndex,
int inputType,
); );
@override @override

View File

@ -298,6 +298,9 @@ enum SemanticsRole {
alert, alert,
} }
// Mirrors engine/src/flutter/lib/ui/semantics.dart
enum SemanticsInputType { none, text, url, phone, search, email }
// When adding a new StringAttributeType, the classes in these file must be // When adding a new StringAttributeType, the classes in these file must be
// updated as well. // updated as well.
// * engine/src/flutter/lib/ui/semantics.dart // * engine/src/flutter/lib/ui/semantics.dart
@ -389,6 +392,7 @@ class SemanticsUpdateBuilder {
SemanticsRole role = SemanticsRole.none, SemanticsRole role = SemanticsRole.none,
required List<String>? controlsNodes, required List<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
required SemanticsInputType inputType,
}) { }) {
if (transform.length != 16) { if (transform.length != 16) {
throw ArgumentError('transform argument must have 16 entries.'); throw ArgumentError('transform argument must have 16 entries.');
@ -433,6 +437,7 @@ class SemanticsUpdateBuilder {
role: role, role: role,
controlsNodes: controlsNodes, controlsNodes: controlsNodes,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
), ),
); );
} }

View File

@ -247,6 +247,7 @@ class SemanticsNodeUpdate {
required this.role, required this.role,
required this.controlsNodes, required this.controlsNodes,
required this.validationResult, required this.validationResult,
required this.inputType,
}); });
/// See [ui.SemanticsUpdateBuilder.updateNode]. /// See [ui.SemanticsUpdateBuilder.updateNode].
@ -362,6 +363,9 @@ class SemanticsNodeUpdate {
/// See [ui.SemanticsUpdateBuilder.updateNode]. /// See [ui.SemanticsUpdateBuilder.updateNode].
final ui.SemanticsValidationResult validationResult; final ui.SemanticsValidationResult validationResult;
/// See [ui.SemanticsUpdateBuilder.updateNode].
final ui.SemanticsInputType inputType;
} }
/// Identifies [SemanticRole] implementations. /// Identifies [SemanticRole] implementations.
@ -1416,6 +1420,8 @@ class SemanticsObject {
/// The role of this node. /// The role of this node.
late ui.SemanticsRole role; late ui.SemanticsRole role;
late ui.SemanticsInputType inputType;
/// List of nodes whose contents are controlled by this node. /// List of nodes whose contents are controlled by this node.
/// ///
/// The list contains [identifier]s of those nodes. /// The list contains [identifier]s of those nodes.
@ -1712,6 +1718,8 @@ class SemanticsObject {
role = update.role; role = update.role;
inputType = update.inputType;
if (!unorderedListEqual<String>(controlsNodes, update.controlsNodes)) { if (!unorderedListEqual<String>(controlsNodes, update.controlsNodes)) {
controlsNodes = update.controlsNodes; controlsNodes = update.controlsNodes;
_markControlsNodesDirty(); _markControlsNodesDirty();

View File

@ -226,8 +226,7 @@ class SemanticTextField extends SemanticRole {
} }
DomHTMLInputElement _createSingleLineField() { DomHTMLInputElement _createSingleLineField() {
return createDomHTMLInputElement() return createDomHTMLInputElement();
..type = semanticsObject.hasFlag(ui.SemanticsFlag.isObscured) ? 'password' : 'text';
} }
DomHTMLTextAreaElement _createMultiLineField() { DomHTMLTextAreaElement _createMultiLineField() {
@ -339,12 +338,37 @@ class SemanticTextField extends SemanticRole {
} else { } else {
editableElement.removeAttribute('aria-required'); editableElement.removeAttribute('aria-required');
} }
_updateInputType();
} }
void _updateEnabledState() { void _updateEnabledState() {
(editableElement as DomElementWithDisabledProperty).disabled = !semanticsObject.isEnabled; (editableElement as DomElementWithDisabledProperty).disabled = !semanticsObject.isEnabled;
} }
void _updateInputType() {
if (semanticsObject.hasFlag(ui.SemanticsFlag.isMultiline)) {
// text area can't be annotated with input type
return;
}
final DomHTMLInputElement input = editableElement as DomHTMLInputElement;
if (semanticsObject.hasFlag(ui.SemanticsFlag.isObscured)) {
input.type = 'password';
} else {
switch (semanticsObject.inputType) {
case ui.SemanticsInputType.search:
input.type = 'search';
case ui.SemanticsInputType.email:
input.type = 'email';
case ui.SemanticsInputType.url:
input.type = 'url';
case ui.SemanticsInputType.phone:
input.type = 'tel';
default:
input.type = 'text';
}
}
}
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();

View File

@ -4859,6 +4859,7 @@ void updateNode(
String? linkUrl, String? linkUrl,
List<String>? controlsNodes, List<String>? controlsNodes,
ui.SemanticsRole role = ui.SemanticsRole.none, ui.SemanticsRole role = ui.SemanticsRole.none,
ui.SemanticsInputType inputType = ui.SemanticsInputType.none,
}) { }) {
transform ??= Float64List.fromList(Matrix4.identity().storage); transform ??= Float64List.fromList(Matrix4.identity().storage);
childrenInTraversalOrder ??= Int32List(0); childrenInTraversalOrder ??= Int32List(0);
@ -4902,6 +4903,7 @@ void updateNode(
headingLevel: headingLevel, headingLevel: headingLevel,
linkUrl: linkUrl, linkUrl: linkUrl,
controlsNodes: controlsNodes, controlsNodes: controlsNodes,
inputType: inputType,
); );
} }

View File

@ -122,6 +122,7 @@ class SemanticsTester {
ui.SemanticsRole? role, ui.SemanticsRole? role,
List<String>? controlsNodes, List<String>? controlsNodes,
ui.SemanticsValidationResult validationResult = ui.SemanticsValidationResult.none, ui.SemanticsValidationResult validationResult = ui.SemanticsValidationResult.none,
ui.SemanticsInputType inputType = ui.SemanticsInputType.none,
}) { }) {
// Flags // Flags
if (hasCheckedState ?? false) { if (hasCheckedState ?? false) {
@ -345,6 +346,7 @@ class SemanticsTester {
role: role ?? ui.SemanticsRole.none, role: role ?? ui.SemanticsRole.none,
controlsNodes: controlsNodes, controlsNodes: controlsNodes,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
); );
_nodeUpdates.add(update); _nodeUpdates.add(update);
return update; return update;

View File

@ -117,6 +117,22 @@ void testMain() {
expect(inputElement.disabled, isFalse); expect(inputElement.disabled, isFalse);
}); });
test('renders text fields with input types', () {
const inputTypeEnumToString = <ui.SemanticsInputType, String>{
ui.SemanticsInputType.none: 'text',
ui.SemanticsInputType.text: 'text',
ui.SemanticsInputType.url: 'url',
ui.SemanticsInputType.phone: 'tel',
ui.SemanticsInputType.search: 'search',
ui.SemanticsInputType.email: 'email',
};
for (final ui.SemanticsInputType type in ui.SemanticsInputType.values) {
createTextFieldSemantics(value: 'text', inputType: type);
expectSemanticsTree(owner(), '<sem><input type="${inputTypeEnumToString[type]}" /></sem>');
}
});
test('renders a disabled text field', () { test('renders a disabled text field', () {
createTextFieldSemantics(isEnabled: false, value: 'hello'); createTextFieldSemantics(isEnabled: false, value: 'hello');
expectSemanticsTree(owner(), '''<sem><input /></sem>'''); expectSemanticsTree(owner(), '''<sem><input /></sem>''');
@ -498,6 +514,7 @@ SemanticsObject createTextFieldSemantics({
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50), ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
int textSelectionBase = 0, int textSelectionBase = 0,
int textSelectionExtent = 0, int textSelectionExtent = 0,
ui.SemanticsInputType inputType = ui.SemanticsInputType.text,
}) { }) {
final tester = SemanticsTester(owner()); final tester = SemanticsTester(owner());
tester.updateNode( tester.updateNode(
@ -516,6 +533,7 @@ SemanticsObject createTextFieldSemantics({
textDirection: ui.TextDirection.ltr, textDirection: ui.TextDirection.ltr,
textSelectionBase: textSelectionBase, textSelectionBase: textSelectionBase,
textSelectionExtent: textSelectionExtent, textSelectionExtent: textSelectionExtent,
inputType: inputType,
); );
tester.apply(); tester.apply();
return tester.getSemanticsObject(0); return tester.getSemanticsObject(0);

View File

@ -199,6 +199,7 @@ Future<void> a11y_main() async {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
) )
..updateNode( ..updateNode(
id: 84, id: 84,
@ -235,6 +236,7 @@ Future<void> a11y_main() async {
childrenInHitTestOrder: Int32List(0), childrenInHitTestOrder: Int32List(0),
childrenInTraversalOrder: Int32List(0), childrenInTraversalOrder: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
) )
..updateNode( ..updateNode(
id: 96, id: 96,
@ -271,6 +273,7 @@ Future<void> a11y_main() async {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
) )
..updateNode( ..updateNode(
id: 128, id: 128,
@ -307,6 +310,7 @@ Future<void> a11y_main() async {
childrenInHitTestOrder: Int32List(0), childrenInHitTestOrder: Int32List(0),
childrenInTraversalOrder: Int32List(0), childrenInTraversalOrder: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
) )
..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message'); ..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message');
@ -395,6 +399,7 @@ Future<void> a11y_string_attributes() async {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
PlatformDispatcher.instance.views.first.updateSemantics(builder.build()); PlatformDispatcher.instance.views.first.updateSemantics(builder.build());
@ -1692,6 +1697,7 @@ Future<void> a11y_main_multi_view() async {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
} }

View File

@ -467,6 +467,7 @@ Future<void> sendSemanticsTreeInfo() async {
additionalActions: additionalActions, additionalActions: additionalActions,
role: ui.SemanticsRole.tab, role: ui.SemanticsRole.tab,
controlsNodes: null, controlsNodes: null,
inputType: ui.SemanticsInputType.none,
); );
return builder.build(); return builder.build();
} }

View File

@ -76,6 +76,7 @@ class LocaleInitialization extends Scenario {
childrenInHitTestOrder: Int32List(0), childrenInHitTestOrder: Int32List(0),
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build();
@ -137,6 +138,7 @@ class LocaleInitialization extends Scenario {
childrenInHitTestOrder: Int32List(0), childrenInHitTestOrder: Int32List(0),
additionalActions: Int32List(0), additionalActions: Int32List(0),
controlsNodes: null, controlsNodes: null,
inputType: SemanticsInputType.none,
); );
final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build(); final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build();

View File

@ -1699,36 +1699,39 @@ class _SearchBarState extends State<SearchBar> {
Expanded( Expanded(
child: Padding( child: Padding(
padding: effectivePadding, padding: effectivePadding,
child: TextField( child: Semantics(
autofocus: widget.autoFocus, inputType: SemanticsInputType.search,
onTap: widget.onTap, child: TextField(
onTapAlwaysCalled: true, autofocus: widget.autoFocus,
onTapOutside: widget.onTapOutside, onTap: widget.onTap,
focusNode: _focusNode, onTapAlwaysCalled: true,
onChanged: widget.onChanged, onTapOutside: widget.onTapOutside,
onSubmitted: widget.onSubmitted, focusNode: _focusNode,
controller: widget.controller, onChanged: widget.onChanged,
style: effectiveTextStyle, onSubmitted: widget.onSubmitted,
enabled: widget.enabled, controller: widget.controller,
decoration: InputDecoration(hintText: widget.hintText).applyDefaults( style: effectiveTextStyle,
InputDecorationTheme( enabled: widget.enabled,
hintStyle: effectiveHintStyle, decoration: InputDecoration(hintText: widget.hintText).applyDefaults(
// The configuration below is to make sure that the text field InputDecorationTheme(
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme` hintStyle: effectiveHintStyle,
enabledBorder: InputBorder.none, // The configuration below is to make sure that the text field
border: InputBorder.none, // in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
focusedBorder: InputBorder.none, enabledBorder: InputBorder.none,
contentPadding: EdgeInsets.zero, border: InputBorder.none,
// Setting `isDense` to true to allow the text field height to be focusedBorder: InputBorder.none,
// smaller than 48.0 contentPadding: EdgeInsets.zero,
isDense: true, // Setting `isDense` to true to allow the text field height to be
// smaller than 48.0
isDense: true,
),
), ),
textCapitalization: effectiveTextCapitalization,
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
scrollPadding: widget.scrollPadding,
contextMenuBuilder: widget.contextMenuBuilder,
), ),
textCapitalization: effectiveTextCapitalization,
textInputAction: widget.textInputAction,
keyboardType: widget.keyboardType,
scrollPadding: widget.scrollPadding,
contextMenuBuilder: widget.contextMenuBuilder,
), ),
), ),
), ),

View File

@ -7,7 +7,7 @@ library;
import 'dart:collection'; import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, LineMetrics, TextBox; import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, LineMetrics, SemanticsInputType, TextBox;
import 'package:characters/characters.dart'; import 'package:characters/characters.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -1382,7 +1382,10 @@ class RenderEditable extends RenderBox
..textDirection = textDirection ..textDirection = textDirection
..isFocused = hasFocus ..isFocused = hasFocus
..isTextField = true ..isTextField = true
..isReadOnly = readOnly; ..isReadOnly = readOnly
// This is the default for customer that uses RenderEditable directly.
// The real value is typically set by EditableText.
..inputType = ui.SemanticsInputType.text;
if (hasFocus && selectionEnabled) { if (hasFocus && selectionEnabled) {
config.onSetSelection = _handleSetSelection; config.onSetSelection = _handleSetSelection;

View File

@ -4550,6 +4550,10 @@ class RenderSemanticsAnnotations extends RenderProxyBox {
config.validationResult = _properties.validationResult; config.validationResult = _properties.validationResult;
} }
if (_properties.inputType != null) {
config.inputType = _properties.inputType!;
}
// Registering _perform* as action handlers instead of the user provided // Registering _perform* as action handlers instead of the user provided
// ones to ensure that changing a user provided handler from a non-null to // ones to ensure that changing a user provided handler from a non-null to
// another non-null value doesn't require a semantics update. // another non-null value doesn't require a semantics update.

View File

@ -16,6 +16,7 @@ import 'dart:ui'
Rect, Rect,
SemanticsAction, SemanticsAction,
SemanticsFlag, SemanticsFlag,
SemanticsInputType,
SemanticsRole, SemanticsRole,
SemanticsUpdate, SemanticsUpdate,
SemanticsUpdateBuilder, SemanticsUpdateBuilder,
@ -737,6 +738,7 @@ class SemanticsData with Diagnosticable {
required this.role, required this.role,
required this.controlsNodes, required this.controlsNodes,
required this.validationResult, required this.validationResult,
required this.inputType,
this.tags, this.tags,
this.transform, this.transform,
this.customSemanticsActionIds, this.customSemanticsActionIds,
@ -1005,6 +1007,9 @@ class SemanticsData with Diagnosticable {
/// {@macro flutter.semantics.SemanticsProperties.validationResult} /// {@macro flutter.semantics.SemanticsProperties.validationResult}
final SemanticsValidationResult validationResult; final SemanticsValidationResult validationResult;
/// {@macro flutter.semantics.SemanticsNode.inputType}
final SemanticsInputType inputType;
/// Whether [flags] contains the given flag. /// Whether [flags] contains the given flag.
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0; bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
@ -1110,6 +1115,7 @@ class SemanticsData with Diagnosticable {
other.linkUrl == linkUrl && other.linkUrl == linkUrl &&
other.role == role && other.role == role &&
other.validationResult == validationResult && other.validationResult == validationResult &&
other.inputType == inputType &&
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) && _sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
setEquals<String>(controlsNodes, other.controlsNodes); setEquals<String>(controlsNodes, other.controlsNodes);
} }
@ -1147,6 +1153,7 @@ class SemanticsData with Diagnosticable {
role, role,
validationResult, validationResult,
controlsNodes == null ? null : Object.hashAll(controlsNodes!), controlsNodes == null ? null : Object.hashAll(controlsNodes!),
inputType,
), ),
); );
@ -1293,6 +1300,9 @@ class SemanticsProperties extends DiagnosticableTree {
this.textDirection, this.textDirection,
this.sortKey, this.sortKey,
this.tagForChildren, this.tagForChildren,
this.role,
this.controlsNodes,
this.inputType,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
this.onScrollLeft, this.onScrollLeft,
@ -1315,8 +1325,6 @@ class SemanticsProperties extends DiagnosticableTree {
this.onFocus, this.onFocus,
this.onDismiss, this.onDismiss,
this.customSemanticsActions, this.customSemanticsActions,
this.role,
this.controlsNodes,
this.validationResult = SemanticsValidationResult.none, this.validationResult = SemanticsValidationResult.none,
}) : assert( }) : assert(
label == null || attributedLabel == null, label == null || attributedLabel == null,
@ -2161,6 +2169,17 @@ class SemanticsProperties extends DiagnosticableTree {
/// {@endtemplate} /// {@endtemplate}
final SemanticsValidationResult validationResult; final SemanticsValidationResult validationResult;
/// {@template flutter.semantics.SemanticsProperties.inputType}
/// The input type for of a editable widget.
///
/// This property is only used when the subtree represents a text field.
///
/// Assistive technologies use this property to provide better information to
/// users. For example, screen reader reads out the input type of text field
/// when focused.
/// {@endtemplate}
final SemanticsInputType? inputType;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
@ -3139,6 +3158,18 @@ class SemanticsNode with DiagnosticableTreeMixin {
SemanticsValidationResult get validationResult => _validationResult; SemanticsValidationResult get validationResult => _validationResult;
SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult; SemanticsValidationResult _validationResult = _kEmptyConfig.validationResult;
/// {@template flutter.semantics.SemanticsNode.inputType}
/// The input type for of a editable node.
///
/// This property is only used when this node represents a text field.
///
/// Assistive technologies use this property to provide better information to
/// users. For example, screen reader reads out the input type of text field
/// when focused.
/// {@endtemplate}
SemanticsInputType get inputType => _inputType;
SemanticsInputType _inputType = _kEmptyConfig.inputType;
bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action); bool _canPerformAction(SemanticsAction action) => _actions.containsKey(action);
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration(); static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
@ -3207,6 +3238,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
_role = config._role; _role = config._role;
_controlsNodes = config._controlsNodes; _controlsNodes = config._controlsNodes;
_validationResult = config._validationResult; _validationResult = config._validationResult;
_inputType = config._inputType;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]); _replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
if (mergeAllDescendantsIntoThisNodeValueChanged) { if (mergeAllDescendantsIntoThisNodeValueChanged) {
@ -3258,6 +3291,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
SemanticsRole role = _role; SemanticsRole role = _role;
Set<String>? controlsNodes = _controlsNodes; Set<String>? controlsNodes = _controlsNodes;
SemanticsValidationResult validationResult = _validationResult; SemanticsValidationResult validationResult = _validationResult;
SemanticsInputType inputType = _inputType;
final Set<int> customSemanticsActionIds = <int>{}; final Set<int> customSemanticsActionIds = <int>{};
for (final CustomSemanticsAction action in _customSemanticsActions.keys) { for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action)); customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
@ -3316,6 +3350,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
if (role == SemanticsRole.none) { if (role == SemanticsRole.none) {
role = node._role; role = node._role;
} }
if (inputType == SemanticsInputType.none) {
inputType = node._inputType;
}
if (tooltip == '') { if (tooltip == '') {
tooltip = node._tooltip; tooltip = node._tooltip;
} }
@ -3409,6 +3446,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
role: role, role: role,
controlsNodes: controlsNodes, controlsNodes: controlsNodes,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
); );
} }
@ -3496,6 +3534,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
role: data.role, role: data.role,
controlsNodes: data.controlsNodes?.toList(), controlsNodes: data.controlsNodes?.toList(),
validationResult: data.validationResult, validationResult: data.validationResult,
inputType: data.inputType,
); );
_dirty = false; _dirty = false;
} }
@ -3693,6 +3732,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0)); properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0)); properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.0));
properties.add(IntProperty('headingLevel', _headingLevel, defaultValue: 0)); properties.add(IntProperty('headingLevel', _headingLevel, defaultValue: 0));
if (_inputType != SemanticsInputType.none) {
properties.add(EnumProperty<SemanticsInputType>('inputType', _inputType));
}
} }
/// Returns a string representation of this node and its descendants. /// Returns a string representation of this node and its descendants.
@ -5674,6 +5716,14 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true; _hasBeenAnnotated = true;
} }
/// {@macro flutter.semantics.SemanticsProperties.inputType}
SemanticsInputType get inputType => _inputType;
SemanticsInputType _inputType = SemanticsInputType.none;
set inputType(SemanticsInputType value) {
_inputType = value;
_hasBeenAnnotated = true;
}
// TAGS // TAGS
/// The set of tags that this configuration wants to add to all child /// The set of tags that this configuration wants to add to all child
@ -5846,6 +5896,9 @@ class SemanticsConfiguration {
if (_role == SemanticsRole.none) { if (_role == SemanticsRole.none) {
_role = child._role; _role = child._role;
} }
if (_inputType == SemanticsInputType.none) {
_inputType = child._inputType;
}
_attributedHint = _concatAttributedString( _attributedHint = _concatAttributedString(
thisAttributedString: _attributedHint, thisAttributedString: _attributedHint,
thisTextDirection: textDirection, thisTextDirection: textDirection,
@ -5916,7 +5969,8 @@ class SemanticsConfiguration {
.._linkUrl = _linkUrl .._linkUrl = _linkUrl
.._role = _role .._role = _role
.._controlsNodes = _controlsNodes .._controlsNodes = _controlsNodes
.._validationResult = _validationResult; .._validationResult = _validationResult
.._inputType = _inputType;
} }
} }

View File

@ -9,7 +9,7 @@
library; library;
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show Image, ImageFilter, TextHeightBehavior; import 'dart:ui' as ui show Image, ImageFilter, SemanticsInputType, TextHeightBehavior;
import 'package:flutter/animation.dart'; import 'package:flutter/animation.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -7385,6 +7385,7 @@ class Semantics extends SingleChildRenderObjectWidget {
SemanticsRole? role, SemanticsRole? role,
Set<String>? controlsNodes, Set<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
ui.SemanticsInputType? inputType,
}) : this.fromProperties( }) : this.fromProperties(
key: key, key: key,
child: child, child: child,
@ -7463,6 +7464,7 @@ class Semantics extends SingleChildRenderObjectWidget {
role: role, role: role,
controlsNodes: controlsNodes, controlsNodes: controlsNodes,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
), ),
); );

View File

@ -5563,6 +5563,17 @@ class EditableTextState extends State<EditableText>
(null, final double textScaleFactor) => TextScaler.linear(textScaleFactor), (null, final double textScaleFactor) => TextScaler.linear(textScaleFactor),
(null, null) => MediaQuery.textScalerOf(context), (null, null) => MediaQuery.textScalerOf(context),
}; };
final ui.SemanticsInputType inputType;
switch (widget.keyboardType) {
case TextInputType.phone:
inputType = ui.SemanticsInputType.phone;
case TextInputType.url:
inputType = ui.SemanticsInputType.url;
case TextInputType.emailAddress:
inputType = ui.SemanticsInputType.email;
default:
inputType = ui.SemanticsInputType.text;
}
return _CompositionCallback( return _CompositionCallback(
compositeCallback: _compositeCallback, compositeCallback: _compositeCallback,
@ -5654,6 +5665,7 @@ class EditableTextState extends State<EditableText>
return CompositedTransformTarget( return CompositedTransformTarget(
link: _toolbarLayerLink, link: _toolbarLayerLink,
child: Semantics( child: Semantics(
inputType: inputType,
onCopy: _semanticsOnCopy(controls), onCopy: _semanticsOnCopy(controls),
onCut: _semanticsOnCut(controls), onCut: _semanticsOnCut(controls),
onPaste: _semanticsOnPaste(controls), onPaste: _semanticsOnPaste(controls),

View File

@ -3223,7 +3223,7 @@ void main() {
expect(controller.value.text, initValue); expect(controller.value.text, initValue);
}); });
testWidgets('Disabled SearchBar semantics node still contains value', ( testWidgets('Disabled SearchBar semantics node still contains value and inputType', (
WidgetTester tester, WidgetTester tester,
) async { ) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
@ -3236,7 +3236,23 @@ void main() {
), ),
); );
expect(semantics, includesNodeWith(actions: <SemanticsAction>[], value: 'text')); expect(
semantics,
includesNodeWith(
actions: <SemanticsAction>[],
value: 'text',
inputType: SemanticsInputType.search,
),
);
semantics.dispose();
});
testWidgets('SearchBar semantics node has search input type', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(const MaterialApp(home: Material(child: Center(child: SearchBar()))));
expect(semantics, includesNodeWith(inputType: SemanticsInputType.search));
semantics.dispose(); semantics.dispose();
}); });

View File

@ -3,6 +3,8 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -135,6 +137,21 @@ void main() {
expect(editable.size.height, 100); expect(editable.size.height, 100);
}); });
test('has default semantics input type', () {
const InlineSpan text = TextSpan(text: 'text');
final RenderEditable editable = RenderEditable(
textDirection: TextDirection.ltr,
startHandleLayerLink: LayerLink(),
endHandleLayerLink: LayerLink(),
offset: ViewportOffset.zero(),
textSelectionDelegate: _FakeEditableTextState(),
text: text,
);
final SemanticsConfiguration config = SemanticsConfiguration();
editable.describeSemanticsConfiguration(config);
expect(config.inputType, SemanticsInputType.text);
});
test('Reports the height of the first line when maxLines is 1', () { test('Reports the height of the first line when maxLines is 1', () {
final InlineSpan multilineSpan = TextSpan( final InlineSpan multilineSpan = TextSpan(
text: 'liiiiines\n' * 10, text: 'liiiiines\n' * 10,

View File

@ -231,6 +231,7 @@ class SemanticsUpdateBuilderSpy extends Fake implements ui.SemanticsUpdateBuilde
SemanticsRole role = SemanticsRole.none, SemanticsRole role = SemanticsRole.none,
required List<String>? controlsNodes, required List<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
required ui.SemanticsInputType inputType,
}) { }) {
// Makes sure we don't send the same id twice. // Makes sure we don't send the same id twice.
assert(!observations.containsKey(id)); assert(!observations.containsKey(id));

View File

@ -624,6 +624,23 @@ void main() {
expect(data.controlsNodes, <String>{'abc', 'ghi', 'def'}); expect(data.controlsNodes, <String>{'abc', 'ghi', 'def'});
}); });
testWidgets('Semantics can set semantics input type', (WidgetTester tester) async {
final UniqueKey key1 = UniqueKey();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Semantics(
key: key1,
inputType: SemanticsInputType.phone,
child: const SizedBox(width: 10, height: 10),
),
),
),
);
final SemanticsNode node1 = tester.getSemantics(find.byKey(key1));
expect(node1.inputType, SemanticsInputType.phone);
});
testWidgets('Semantics can set alert rule', (WidgetTester tester) async { testWidgets('Semantics can set alert rule', (WidgetTester tester) async {
final UniqueKey key = UniqueKey(); final UniqueKey key = UniqueKey();
await tester.pumpWidget( await tester.pumpWidget(

View File

@ -4135,8 +4135,8 @@ void main() {
), ),
); );
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first; final SemanticsNode node = find.semantics.byValue('test').evaluate().first;
final int semanticsId = render.debugSemantics!.id; final int semanticsId = node.id;
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4); expect(controller.selection.extentOffset, 4);
@ -4241,8 +4241,8 @@ void main() {
), ),
); );
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first; final SemanticsNode node = find.semantics.byValue('test for words').evaluate().first;
final int semanticsId = render.debugSemantics!.id; final int semanticsId = node.id;
expect(controller.selection.baseOffset, 14); expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14); expect(controller.selection.extentOffset, 14);
@ -4356,8 +4356,8 @@ void main() {
), ),
); );
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first; final SemanticsNode node = find.semantics.byValue('test').evaluate().first;
final int semanticsId = render.debugSemantics!.id; final int semanticsId = node.id;
expect(controller.selection.baseOffset, 4); expect(controller.selection.baseOffset, 4);
expect(controller.selection.extentOffset, 4); expect(controller.selection.extentOffset, 4);
@ -4473,8 +4473,8 @@ void main() {
), ),
); );
final RenderEditable render = tester.allRenderObjects.whereType<RenderEditable>().first; final SemanticsNode node = find.semantics.byValue('test for words').evaluate().first;
final int semanticsId = render.debugSemantics!.id; final int semanticsId = node.id;
expect(controller.selection.baseOffset, 14); expect(controller.selection.baseOffset, 14);
expect(controller.selection.extentOffset, 14); expect(controller.selection.extentOffset, 14);
@ -4932,7 +4932,7 @@ void main() {
await tester.pump(); await tester.pump();
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner!; final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner!;
const int expectedNodeId = 5; const int expectedNodeId = 4;
expect( expect(
semantics, semantics,
@ -5016,7 +5016,8 @@ void main() {
await tester.pump(); await tester.pump();
final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner!; final SemanticsOwner owner = tester.binding.pipelineOwner.semanticsOwner!;
const int expectedNodeId = 5; final SemanticsNode node = find.semantics.byValue('ABCDEFG').evaluate().first;
final int expectedNodeId = node.id;
expect(controller.value.selection.isCollapsed, isTrue); expect(controller.value.selection.isCollapsed, isTrue);

View File

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/physics.dart'; import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
@ -587,6 +589,7 @@ class SemanticsTester {
int? currentValueLength, int? currentValueLength,
int? maxValueLength, int? maxValueLength,
SemanticsNode? ancestor, SemanticsNode? ancestor,
SemanticsInputType? inputType,
}) { }) {
bool checkNode(SemanticsNode node) { bool checkNode(SemanticsNode node) {
if (label != null && node.label != label) { if (label != null && node.label != label) {
@ -670,6 +673,9 @@ class SemanticsTester {
if (maxValueLength != null && node.maxValueLength != maxValueLength) { if (maxValueLength != null && node.maxValueLength != maxValueLength) {
return false; return false;
} }
if (inputType != null && node.inputType != inputType) {
return false;
}
return true; return true;
} }
@ -950,6 +956,7 @@ class _IncludesNodeWith extends Matcher {
this.scrollExtentMin, this.scrollExtentMin,
this.maxValueLength, this.maxValueLength,
this.currentValueLength, this.currentValueLength,
this.inputType,
}) : assert( }) : assert(
label != null || label != null ||
value != null || value != null ||
@ -962,7 +969,8 @@ class _IncludesNodeWith extends Matcher {
scrollExtentMax != null || scrollExtentMax != null ||
scrollExtentMin != null || scrollExtentMin != null ||
maxValueLength != null || maxValueLength != null ||
currentValueLength != null, currentValueLength != null ||
inputType != null,
); );
final AttributedString? attributedLabel; final AttributedString? attributedLabel;
final AttributedString? attributedValue; final AttributedString? attributedValue;
@ -981,6 +989,7 @@ class _IncludesNodeWith extends Matcher {
final double? scrollExtentMin; final double? scrollExtentMin;
final int? currentValueLength; final int? currentValueLength;
final int? maxValueLength; final int? maxValueLength;
final SemanticsInputType? inputType;
@override @override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) { bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
@ -1003,6 +1012,7 @@ class _IncludesNodeWith extends Matcher {
scrollExtentMin: scrollExtentMin, scrollExtentMin: scrollExtentMin,
currentValueLength: currentValueLength, currentValueLength: currentValueLength,
maxValueLength: maxValueLength, maxValueLength: maxValueLength,
inputType: inputType,
) )
.isNotEmpty; .isNotEmpty;
} }
@ -1038,6 +1048,7 @@ class _IncludesNodeWith extends Matcher {
if (scrollExtentMin != null) 'scrollExtentMin "$scrollExtentMin"', if (scrollExtentMin != null) 'scrollExtentMin "$scrollExtentMin"',
if (currentValueLength != null) 'currentValueLength "$currentValueLength"', if (currentValueLength != null) 'currentValueLength "$currentValueLength"',
if (maxValueLength != null) 'maxValueLength "$maxValueLength"', if (maxValueLength != null) 'maxValueLength "$maxValueLength"',
if (inputType != null) 'inputType $inputType',
]; ];
return strings.join(', '); return strings.join(', ');
} }
@ -1065,6 +1076,7 @@ Matcher includesNodeWith({
double? scrollExtentMin, double? scrollExtentMin,
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
SemanticsInputType? inputType,
}) { }) {
return _IncludesNodeWith( return _IncludesNodeWith(
label: label, label: label,
@ -1084,5 +1096,6 @@ Matcher includesNodeWith({
scrollExtentMin: scrollExtentMin, scrollExtentMin: scrollExtentMin,
maxValueLength: maxValueLength, maxValueLength: maxValueLength,
currentValueLength: currentValueLength, currentValueLength: currentValueLength,
inputType: inputType,
); );
} }

View File

@ -686,6 +686,7 @@ Matcher matchesSemantics({
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
ui.SemanticsInputType? inputType,
// Flags // // Flags //
bool hasCheckedState = false, bool hasCheckedState = false,
bool isChecked = false, bool isChecked = false,
@ -770,6 +771,7 @@ Matcher matchesSemantics({
maxValueLength: maxValueLength, maxValueLength: maxValueLength,
currentValueLength: currentValueLength, currentValueLength: currentValueLength,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
// Flags // Flags
hasCheckedState: hasCheckedState, hasCheckedState: hasCheckedState,
isChecked: isChecked, isChecked: isChecked,
@ -882,6 +884,7 @@ Matcher containsSemantics({
int? maxValueLength, int? maxValueLength,
int? currentValueLength, int? currentValueLength,
SemanticsValidationResult validationResult = SemanticsValidationResult.none, SemanticsValidationResult validationResult = SemanticsValidationResult.none,
ui.SemanticsInputType? inputType,
// Flags // Flags
bool? hasCheckedState, bool? hasCheckedState,
bool? isChecked, bool? isChecked,
@ -966,6 +969,7 @@ Matcher containsSemantics({
maxValueLength: maxValueLength, maxValueLength: maxValueLength,
currentValueLength: currentValueLength, currentValueLength: currentValueLength,
validationResult: validationResult, validationResult: validationResult,
inputType: inputType,
// Flags // Flags
hasCheckedState: hasCheckedState, hasCheckedState: hasCheckedState,
isChecked: isChecked, isChecked: isChecked,
@ -2403,6 +2407,7 @@ class _MatchesSemanticsData extends Matcher {
required this.maxValueLength, required this.maxValueLength,
required this.currentValueLength, required this.currentValueLength,
required this.validationResult, required this.validationResult,
required this.inputType,
// Flags // Flags
required bool? hasCheckedState, required bool? hasCheckedState,
required bool? isChecked, required bool? isChecked,
@ -2556,6 +2561,7 @@ class _MatchesSemanticsData extends Matcher {
final int? platformViewId; final int? platformViewId;
final int? maxValueLength; final int? maxValueLength;
final int? currentValueLength; final int? currentValueLength;
final ui.SemanticsInputType? inputType;
final List<Matcher>? children; final List<Matcher>? children;
final SemanticsValidationResult validationResult; final SemanticsValidationResult validationResult;
@ -2603,6 +2609,9 @@ class _MatchesSemanticsData extends Matcher {
if (tooltip != null) { if (tooltip != null) {
description.add(' with tooltip: $tooltip'); description.add(' with tooltip: $tooltip');
} }
if (inputType != null) {
description.add(' with inputType: $inputType');
}
if (actions.isNotEmpty) { if (actions.isNotEmpty) {
final List<SemanticsAction> expectedActions = final List<SemanticsAction> expectedActions =
actions.entries actions.entries
@ -2813,6 +2822,9 @@ class _MatchesSemanticsData extends Matcher {
if (validationResult != data.validationResult) { if (validationResult != data.validationResult) {
return failWithDescription(matchState, 'validationResult was: ${data.validationResult}'); return failWithDescription(matchState, 'validationResult was: ${data.validationResult}');
} }
if (inputType != null && inputType != data.inputType) {
return failWithDescription(matchState, 'inputType was: ${data.inputType}');
}
if (actions.isNotEmpty) { if (actions.isNotEmpty) {
final List<SemanticsAction> unexpectedActions = <SemanticsAction>[]; final List<SemanticsAction> unexpectedActions = <SemanticsAction>[];
final List<SemanticsAction> missingActions = <SemanticsAction>[]; final List<SemanticsAction> missingActions = <SemanticsAction>[];

View File

@ -732,6 +732,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode node = _FakeSemanticsNode(data); final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@ -1036,6 +1037,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode node = _FakeSemanticsNode(data); final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@ -1136,6 +1138,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode node = _FakeSemanticsNode(data); final _FakeSemanticsNode node = _FakeSemanticsNode(data);
@ -1243,6 +1246,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData); final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData);
@ -1276,6 +1280,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData); final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData);
@ -1365,6 +1370,7 @@ void main() {
role: ui.SemanticsRole.none, role: ui.SemanticsRole.none,
controlsNodes: null, controlsNodes: null,
validationResult: SemanticsValidationResult.none, validationResult: SemanticsValidationResult.none,
inputType: ui.SemanticsInputType.none,
); );
final _FakeSemanticsNode node = _FakeSemanticsNode(data); final _FakeSemanticsNode node = _FakeSemanticsNode(data);