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
25 changed files with 308 additions and 49 deletions

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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,
),
);
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -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,
);
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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,
);
}

View File

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

View File

@@ -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();