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

View File

@ -1699,36 +1699,39 @@ class _SearchBarState extends State<SearchBar> {
Expanded(
child: Padding(
padding: effectivePadding,
child: TextField(
autofocus: widget.autoFocus,
onTap: widget.onTap,
onTapAlwaysCalled: true,
onTapOutside: widget.onTapOutside,
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
controller: widget.controller,
style: effectiveTextStyle,
enabled: widget.enabled,
decoration: InputDecoration(hintText: widget.hintText).applyDefaults(
InputDecorationTheme(
hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
// Setting `isDense` to true to allow the text field height to be
// smaller than 48.0
isDense: true,
child: Semantics(
inputType: SemanticsInputType.search,
child: TextField(
autofocus: widget.autoFocus,
onTap: widget.onTap,
onTapAlwaysCalled: true,
onTapOutside: widget.onTapOutside,
focusNode: _focusNode,
onChanged: widget.onChanged,
onSubmitted: widget.onSubmitted,
controller: widget.controller,
style: effectiveTextStyle,
enabled: widget.enabled,
decoration: InputDecoration(hintText: widget.hintText).applyDefaults(
InputDecorationTheme(
hintStyle: effectiveHintStyle,
// The configuration below is to make sure that the text field
// in `SearchBar` will not be overridden by the overall `InputDecorationTheme`
enabledBorder: InputBorder.none,
border: InputBorder.none,
focusedBorder: InputBorder.none,
contentPadding: EdgeInsets.zero,
// 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: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:flutter/foundation.dart';
@ -1382,7 +1382,10 @@ class RenderEditable extends RenderBox
..textDirection = textDirection
..isFocused = hasFocus
..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) {
config.onSetSelection = _handleSetSelection;

View File

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

View File

@ -16,6 +16,7 @@ import 'dart:ui'
Rect,
SemanticsAction,
SemanticsFlag,
SemanticsInputType,
SemanticsRole,
SemanticsUpdate,
SemanticsUpdateBuilder,
@ -737,6 +738,7 @@ class SemanticsData with Diagnosticable {
required this.role,
required this.controlsNodes,
required this.validationResult,
required this.inputType,
this.tags,
this.transform,
this.customSemanticsActionIds,
@ -1005,6 +1007,9 @@ class SemanticsData with Diagnosticable {
/// {@macro flutter.semantics.SemanticsProperties.validationResult}
final SemanticsValidationResult validationResult;
/// {@macro flutter.semantics.SemanticsNode.inputType}
final SemanticsInputType inputType;
/// Whether [flags] contains the given flag.
bool hasFlag(SemanticsFlag flag) => (flags & flag.index) != 0;
@ -1110,6 +1115,7 @@ class SemanticsData with Diagnosticable {
other.linkUrl == linkUrl &&
other.role == role &&
other.validationResult == validationResult &&
other.inputType == inputType &&
_sortedListsEqual(other.customSemanticsActionIds, customSemanticsActionIds) &&
setEquals<String>(controlsNodes, other.controlsNodes);
}
@ -1147,6 +1153,7 @@ class SemanticsData with Diagnosticable {
role,
validationResult,
controlsNodes == null ? null : Object.hashAll(controlsNodes!),
inputType,
),
);
@ -1293,6 +1300,9 @@ class SemanticsProperties extends DiagnosticableTree {
this.textDirection,
this.sortKey,
this.tagForChildren,
this.role,
this.controlsNodes,
this.inputType,
this.onTap,
this.onLongPress,
this.onScrollLeft,
@ -1315,8 +1325,6 @@ class SemanticsProperties extends DiagnosticableTree {
this.onFocus,
this.onDismiss,
this.customSemanticsActions,
this.role,
this.controlsNodes,
this.validationResult = SemanticsValidationResult.none,
}) : assert(
label == null || attributedLabel == null,
@ -2161,6 +2169,17 @@ class SemanticsProperties extends DiagnosticableTree {
/// {@endtemplate}
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
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
@ -3139,6 +3158,18 @@ class SemanticsNode with DiagnosticableTreeMixin {
SemanticsValidationResult get validationResult => _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);
static final SemanticsConfiguration _kEmptyConfig = SemanticsConfiguration();
@ -3207,6 +3238,8 @@ class SemanticsNode with DiagnosticableTreeMixin {
_role = config._role;
_controlsNodes = config._controlsNodes;
_validationResult = config._validationResult;
_inputType = config._inputType;
_replaceChildren(childrenInInversePaintOrder ?? const <SemanticsNode>[]);
if (mergeAllDescendantsIntoThisNodeValueChanged) {
@ -3258,6 +3291,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
SemanticsRole role = _role;
Set<String>? controlsNodes = _controlsNodes;
SemanticsValidationResult validationResult = _validationResult;
SemanticsInputType inputType = _inputType;
final Set<int> customSemanticsActionIds = <int>{};
for (final CustomSemanticsAction action in _customSemanticsActions.keys) {
customSemanticsActionIds.add(CustomSemanticsAction.getIdentifier(action));
@ -3316,6 +3350,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
if (role == SemanticsRole.none) {
role = node._role;
}
if (inputType == SemanticsInputType.none) {
inputType = node._inputType;
}
if (tooltip == '') {
tooltip = node._tooltip;
}
@ -3409,6 +3446,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
role: role,
controlsNodes: controlsNodes,
validationResult: validationResult,
inputType: inputType,
);
}
@ -3496,6 +3534,7 @@ class SemanticsNode with DiagnosticableTreeMixin {
role: data.role,
controlsNodes: data.controlsNodes?.toList(),
validationResult: data.validationResult,
inputType: data.inputType,
);
_dirty = false;
}
@ -3693,6 +3732,9 @@ class SemanticsNode with DiagnosticableTreeMixin {
properties.add(DoubleProperty('elevation', elevation, defaultValue: 0.0));
properties.add(DoubleProperty('thickness', thickness, defaultValue: 0.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.
@ -5674,6 +5716,14 @@ class SemanticsConfiguration {
_hasBeenAnnotated = true;
}
/// {@macro flutter.semantics.SemanticsProperties.inputType}
SemanticsInputType get inputType => _inputType;
SemanticsInputType _inputType = SemanticsInputType.none;
set inputType(SemanticsInputType value) {
_inputType = value;
_hasBeenAnnotated = true;
}
// TAGS
/// The set of tags that this configuration wants to add to all child
@ -5846,6 +5896,9 @@ class SemanticsConfiguration {
if (_role == SemanticsRole.none) {
_role = child._role;
}
if (_inputType == SemanticsInputType.none) {
_inputType = child._inputType;
}
_attributedHint = _concatAttributedString(
thisAttributedString: _attributedHint,
thisTextDirection: textDirection,
@ -5916,7 +5969,8 @@ class SemanticsConfiguration {
.._linkUrl = _linkUrl
.._role = _role
.._controlsNodes = _controlsNodes
.._validationResult = _validationResult;
.._validationResult = _validationResult
.._inputType = _inputType;
}
}

View File

@ -9,7 +9,7 @@
library;
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/foundation.dart';
@ -7385,6 +7385,7 @@ class Semantics extends SingleChildRenderObjectWidget {
SemanticsRole? role,
Set<String>? controlsNodes,
SemanticsValidationResult validationResult = SemanticsValidationResult.none,
ui.SemanticsInputType? inputType,
}) : this.fromProperties(
key: key,
child: child,
@ -7463,6 +7464,7 @@ class Semantics extends SingleChildRenderObjectWidget {
role: role,
controlsNodes: controlsNodes,
validationResult: validationResult,
inputType: inputType,
),
);

View File

@ -5563,6 +5563,17 @@ class EditableTextState extends State<EditableText>
(null, final double textScaleFactor) => TextScaler.linear(textScaleFactor),
(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(
compositeCallback: _compositeCallback,
@ -5654,6 +5665,7 @@ class EditableTextState extends State<EditableText>
return CompositedTransformTarget(
link: _toolbarLayerLink,
child: Semantics(
inputType: inputType,
onCopy: _semanticsOnCopy(controls),
onCut: _semanticsOnCut(controls),
onPaste: _semanticsOnPaste(controls),

View File

@ -3223,7 +3223,7 @@ void main() {
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,
) async {
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();
});

View File

@ -3,6 +3,8 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@ -135,6 +137,21 @@ void main() {
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', () {
final InlineSpan multilineSpan = TextSpan(
text: 'liiiiines\n' * 10,

View File

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

View File

@ -624,6 +624,23 @@ void main() {
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 {
final UniqueKey key = UniqueKey();
await tester.pumpWidget(

View File

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

View File

@ -2,6 +2,8 @@
// 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';
@ -587,6 +589,7 @@ class SemanticsTester {
int? currentValueLength,
int? maxValueLength,
SemanticsNode? ancestor,
SemanticsInputType? inputType,
}) {
bool checkNode(SemanticsNode node) {
if (label != null && node.label != label) {
@ -670,6 +673,9 @@ class SemanticsTester {
if (maxValueLength != null && node.maxValueLength != maxValueLength) {
return false;
}
if (inputType != null && node.inputType != inputType) {
return false;
}
return true;
}
@ -950,6 +956,7 @@ class _IncludesNodeWith extends Matcher {
this.scrollExtentMin,
this.maxValueLength,
this.currentValueLength,
this.inputType,
}) : assert(
label != null ||
value != null ||
@ -962,7 +969,8 @@ class _IncludesNodeWith extends Matcher {
scrollExtentMax != null ||
scrollExtentMin != null ||
maxValueLength != null ||
currentValueLength != null,
currentValueLength != null ||
inputType != null,
);
final AttributedString? attributedLabel;
final AttributedString? attributedValue;
@ -981,6 +989,7 @@ class _IncludesNodeWith extends Matcher {
final double? scrollExtentMin;
final int? currentValueLength;
final int? maxValueLength;
final SemanticsInputType? inputType;
@override
bool matches(covariant SemanticsTester item, Map<dynamic, dynamic> matchState) {
@ -1003,6 +1012,7 @@ class _IncludesNodeWith extends Matcher {
scrollExtentMin: scrollExtentMin,
currentValueLength: currentValueLength,
maxValueLength: maxValueLength,
inputType: inputType,
)
.isNotEmpty;
}
@ -1038,6 +1048,7 @@ class _IncludesNodeWith extends Matcher {
if (scrollExtentMin != null) 'scrollExtentMin "$scrollExtentMin"',
if (currentValueLength != null) 'currentValueLength "$currentValueLength"',
if (maxValueLength != null) 'maxValueLength "$maxValueLength"',
if (inputType != null) 'inputType $inputType',
];
return strings.join(', ');
}
@ -1065,6 +1076,7 @@ Matcher includesNodeWith({
double? scrollExtentMin,
int? maxValueLength,
int? currentValueLength,
SemanticsInputType? inputType,
}) {
return _IncludesNodeWith(
label: label,
@ -1084,5 +1096,6 @@ Matcher includesNodeWith({
scrollExtentMin: scrollExtentMin,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
inputType: inputType,
);
}

View File

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

View File

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