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:
@@ -235,6 +235,7 @@ void sendSemanticsUpdate() {
|
||||
headingLevel: 0,
|
||||
linkUrl: '',
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
_semanticsUpdate(builder.build());
|
||||
}
|
||||
@@ -289,6 +290,7 @@ void sendSemanticsUpdateWithRole() {
|
||||
linkUrl: '',
|
||||
role: SemanticsRole.tab,
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
_semanticsUpdate(builder.build());
|
||||
}
|
||||
|
||||
@@ -506,6 +506,29 @@ enum SemanticsRole {
|
||||
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.
|
||||
//
|
||||
// When changes are made to this class, the equivalent APIs in
|
||||
@@ -1232,6 +1255,7 @@ abstract class SemanticsUpdateBuilder {
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
required SemanticsInputType inputType,
|
||||
});
|
||||
|
||||
/// Update the custom semantics action associated with the given `id`.
|
||||
@@ -1310,6 +1334,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
required SemanticsInputType inputType,
|
||||
}) {
|
||||
assert(_matrix4IsValid(transform));
|
||||
assert(
|
||||
@@ -1358,6 +1383,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
role.index,
|
||||
controlsNodes,
|
||||
validationResult.index,
|
||||
inputType.index,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1405,6 +1431,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
Int32,
|
||||
Handle,
|
||||
Int32,
|
||||
Int32,
|
||||
)
|
||||
>(symbol: 'SemanticsUpdateBuilder::updateNode')
|
||||
external void _updateNode(
|
||||
@@ -1449,6 +1476,7 @@ base class _NativeSemanticsUpdateBuilder extends NativeFieldWrapperClass1
|
||||
int role,
|
||||
List<String>? controlsNodes,
|
||||
int validationResultIndex,
|
||||
int inputType,
|
||||
);
|
||||
|
||||
@override
|
||||
|
||||
@@ -298,6 +298,9 @@ enum SemanticsRole {
|
||||
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
|
||||
// updated as well.
|
||||
// * engine/src/flutter/lib/ui/semantics.dart
|
||||
@@ -389,6 +392,7 @@ class SemanticsUpdateBuilder {
|
||||
SemanticsRole role = SemanticsRole.none,
|
||||
required List<String>? controlsNodes,
|
||||
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
|
||||
required SemanticsInputType inputType,
|
||||
}) {
|
||||
if (transform.length != 16) {
|
||||
throw ArgumentError('transform argument must have 16 entries.');
|
||||
@@ -433,6 +437,7 @@ class SemanticsUpdateBuilder {
|
||||
role: role,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
inputType: inputType,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -247,6 +247,7 @@ class SemanticsNodeUpdate {
|
||||
required this.role,
|
||||
required this.controlsNodes,
|
||||
required this.validationResult,
|
||||
required this.inputType,
|
||||
});
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
@@ -362,6 +363,9 @@ class SemanticsNodeUpdate {
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final ui.SemanticsValidationResult validationResult;
|
||||
|
||||
/// See [ui.SemanticsUpdateBuilder.updateNode].
|
||||
final ui.SemanticsInputType inputType;
|
||||
}
|
||||
|
||||
/// Identifies [SemanticRole] implementations.
|
||||
@@ -1416,6 +1420,8 @@ class SemanticsObject {
|
||||
/// The role of this node.
|
||||
late ui.SemanticsRole role;
|
||||
|
||||
late ui.SemanticsInputType inputType;
|
||||
|
||||
/// List of nodes whose contents are controlled by this node.
|
||||
///
|
||||
/// The list contains [identifier]s of those nodes.
|
||||
@@ -1712,6 +1718,8 @@ class SemanticsObject {
|
||||
|
||||
role = update.role;
|
||||
|
||||
inputType = update.inputType;
|
||||
|
||||
if (!unorderedListEqual<String>(controlsNodes, update.controlsNodes)) {
|
||||
controlsNodes = update.controlsNodes;
|
||||
_markControlsNodesDirty();
|
||||
|
||||
@@ -226,8 +226,7 @@ class SemanticTextField extends SemanticRole {
|
||||
}
|
||||
|
||||
DomHTMLInputElement _createSingleLineField() {
|
||||
return createDomHTMLInputElement()
|
||||
..type = semanticsObject.hasFlag(ui.SemanticsFlag.isObscured) ? 'password' : 'text';
|
||||
return createDomHTMLInputElement();
|
||||
}
|
||||
|
||||
DomHTMLTextAreaElement _createMultiLineField() {
|
||||
@@ -339,12 +338,37 @@ class SemanticTextField extends SemanticRole {
|
||||
} else {
|
||||
editableElement.removeAttribute('aria-required');
|
||||
}
|
||||
_updateInputType();
|
||||
}
|
||||
|
||||
void _updateEnabledState() {
|
||||
(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
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
@@ -4859,6 +4859,7 @@ void updateNode(
|
||||
String? linkUrl,
|
||||
List<String>? controlsNodes,
|
||||
ui.SemanticsRole role = ui.SemanticsRole.none,
|
||||
ui.SemanticsInputType inputType = ui.SemanticsInputType.none,
|
||||
}) {
|
||||
transform ??= Float64List.fromList(Matrix4.identity().storage);
|
||||
childrenInTraversalOrder ??= Int32List(0);
|
||||
@@ -4902,6 +4903,7 @@ void updateNode(
|
||||
headingLevel: headingLevel,
|
||||
linkUrl: linkUrl,
|
||||
controlsNodes: controlsNodes,
|
||||
inputType: inputType,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ class SemanticsTester {
|
||||
ui.SemanticsRole? role,
|
||||
List<String>? controlsNodes,
|
||||
ui.SemanticsValidationResult validationResult = ui.SemanticsValidationResult.none,
|
||||
ui.SemanticsInputType inputType = ui.SemanticsInputType.none,
|
||||
}) {
|
||||
// Flags
|
||||
if (hasCheckedState ?? false) {
|
||||
@@ -345,6 +346,7 @@ class SemanticsTester {
|
||||
role: role ?? ui.SemanticsRole.none,
|
||||
controlsNodes: controlsNodes,
|
||||
validationResult: validationResult,
|
||||
inputType: inputType,
|
||||
);
|
||||
_nodeUpdates.add(update);
|
||||
return update;
|
||||
|
||||
@@ -117,6 +117,22 @@ void testMain() {
|
||||
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', () {
|
||||
createTextFieldSemantics(isEnabled: false, value: 'hello');
|
||||
expectSemanticsTree(owner(), '''<sem><input /></sem>''');
|
||||
@@ -498,6 +514,7 @@ SemanticsObject createTextFieldSemantics({
|
||||
ui.Rect rect = const ui.Rect.fromLTRB(0, 0, 100, 50),
|
||||
int textSelectionBase = 0,
|
||||
int textSelectionExtent = 0,
|
||||
ui.SemanticsInputType inputType = ui.SemanticsInputType.text,
|
||||
}) {
|
||||
final tester = SemanticsTester(owner());
|
||||
tester.updateNode(
|
||||
@@ -516,6 +533,7 @@ SemanticsObject createTextFieldSemantics({
|
||||
textDirection: ui.TextDirection.ltr,
|
||||
textSelectionBase: textSelectionBase,
|
||||
textSelectionExtent: textSelectionExtent,
|
||||
inputType: inputType,
|
||||
);
|
||||
tester.apply();
|
||||
return tester.getSemanticsObject(0);
|
||||
|
||||
@@ -199,6 +199,7 @@ Future<void> a11y_main() async {
|
||||
textDirection: TextDirection.ltr,
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
)
|
||||
..updateNode(
|
||||
id: 84,
|
||||
@@ -235,6 +236,7 @@ Future<void> a11y_main() async {
|
||||
childrenInHitTestOrder: Int32List(0),
|
||||
childrenInTraversalOrder: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
)
|
||||
..updateNode(
|
||||
id: 96,
|
||||
@@ -271,6 +273,7 @@ Future<void> a11y_main() async {
|
||||
textDirection: TextDirection.ltr,
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
)
|
||||
..updateNode(
|
||||
id: 128,
|
||||
@@ -307,6 +310,7 @@ Future<void> a11y_main() async {
|
||||
childrenInHitTestOrder: Int32List(0),
|
||||
childrenInTraversalOrder: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
)
|
||||
..updateCustomAction(id: 21, label: 'Archive', hint: 'archive message');
|
||||
|
||||
@@ -395,6 +399,7 @@ Future<void> a11y_string_attributes() async {
|
||||
textDirection: TextDirection.ltr,
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
|
||||
PlatformDispatcher.instance.views.first.updateSemantics(builder.build());
|
||||
@@ -1692,6 +1697,7 @@ Future<void> a11y_main_multi_view() async {
|
||||
textDirection: TextDirection.ltr,
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -467,6 +467,7 @@ Future<void> sendSemanticsTreeInfo() async {
|
||||
additionalActions: additionalActions,
|
||||
role: ui.SemanticsRole.tab,
|
||||
controlsNodes: null,
|
||||
inputType: ui.SemanticsInputType.none,
|
||||
);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ class LocaleInitialization extends Scenario {
|
||||
childrenInHitTestOrder: Int32List(0),
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
|
||||
final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build();
|
||||
@@ -137,6 +138,7 @@ class LocaleInitialization extends Scenario {
|
||||
childrenInHitTestOrder: Int32List(0),
|
||||
additionalActions: Int32List(0),
|
||||
controlsNodes: null,
|
||||
inputType: SemanticsInputType.none,
|
||||
);
|
||||
|
||||
final SemanticsUpdate semanticsUpdate = semanticsUpdateBuilder.build();
|
||||
|
||||
Reference in New Issue
Block a user