Add multi-line flag to semantics (#36297)
diff --git a/packages/flutter/lib/src/rendering/custom_paint.dart b/packages/flutter/lib/src/rendering/custom_paint.dart
index 907c78d..3b80e35 100644
--- a/packages/flutter/lib/src/rendering/custom_paint.dart
+++ b/packages/flutter/lib/src/rendering/custom_paint.dart
@@ -846,6 +846,9 @@
if (properties.obscured != null) {
config.isObscured = properties.obscured;
}
+ if (properties.multiline != null) {
+ config.isMultiline = properties.multiline;
+ }
if (properties.hidden != null) {
config.isHidden = properties.hidden;
}
@@ -924,6 +927,12 @@
if (properties.onMoveCursorBackwardByCharacter != null) {
config.onMoveCursorBackwardByCharacter = properties.onMoveCursorBackwardByCharacter;
}
+ if (properties.onMoveCursorForwardByWord != null) {
+ config.onMoveCursorForwardByWord = properties.onMoveCursorForwardByWord;
+ }
+ if (properties.onMoveCursorBackwardByWord != null) {
+ config.onMoveCursorBackwardByWord = properties.onMoveCursorBackwardByWord;
+ }
if (properties.onSetSelection != null) {
config.onSetSelection = properties.onSetSelection;
}
diff --git a/packages/flutter/lib/src/rendering/editable.dart b/packages/flutter/lib/src/rendering/editable.dart
index 231fb40..2519549 100644
--- a/packages/flutter/lib/src/rendering/editable.dart
+++ b/packages/flutter/lib/src/rendering/editable.dart
@@ -992,6 +992,7 @@
? obscuringCharacter * text.toPlainText().length
: text.toPlainText()
..isObscured = obscureText
+ ..isMultiline = _isMultiline
..textDirection = textDirection
..isFocused = hasFocus
..isTextField = true;
diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart
index 16f2c4b..7d2388c 100644
--- a/packages/flutter/lib/src/rendering/proxy_box.dart
+++ b/packages/flutter/lib/src/rendering/proxy_box.dart
@@ -3430,6 +3430,7 @@
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
+ bool multiline,
bool scopesRoute,
bool namesRoute,
bool hidden,
@@ -3478,6 +3479,7 @@
_focused = focused,
_inMutuallyExclusiveGroup = inMutuallyExclusiveGroup,
_obscured = obscured,
+ _multiline = multiline,
_scopesRoute = scopesRoute,
_namesRoute = namesRoute,
_liveRegion = liveRegion,
@@ -3673,6 +3675,17 @@
markNeedsSemanticsUpdate();
}
+ /// If non-null, sets the [SemanticsNode.isMultiline] semantic to the given
+ /// value.
+ bool get multiline => _multiline;
+ bool _multiline;
+ set multiline(bool value) {
+ if (multiline == value)
+ return;
+ _multiline = value;
+ markNeedsSemanticsUpdate();
+ }
+
/// If non-null, sets the [SemanticsNode.scopesRoute] semantic to the give value.
bool get scopesRoute => _scopesRoute;
bool _scopesRoute;
@@ -4272,6 +4285,8 @@
config.isInMutuallyExclusiveGroup = inMutuallyExclusiveGroup;
if (obscured != null)
config.isObscured = obscured;
+ if (multiline != null)
+ config.isMultiline = multiline;
if (hidden != null)
config.isHidden = hidden;
if (image != null)
diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart
index 4eb2d98..463fdfa 100644
--- a/packages/flutter/lib/src/semantics/semantics.dart
+++ b/packages/flutter/lib/src/semantics/semantics.dart
@@ -570,6 +570,7 @@
this.inMutuallyExclusiveGroup,
this.hidden,
this.obscured,
+ this.multiline,
this.scopesRoute,
this.namesRoute,
this.image,
@@ -700,6 +701,15 @@
/// Doing so instructs screen readers to not read out the [value].
final bool obscured;
+ /// Whether the [value] is coming from a field that supports multi-line text
+ /// editing.
+ ///
+ /// This option is only meaningful when [textField] is true to indicate
+ /// whether it's a single-line or multi-line text field.
+ ///
+ /// This option is null when [textField] is false.
+ final bool multiline;
+
/// If non-null, whether the node corresponds to the root of a subtree for
/// which a route name should be announced.
///
@@ -1654,6 +1664,11 @@
TextSelection get textSelection => _textSelection;
TextSelection _textSelection;
+ /// If this node represents a text field, this indicates whether or not it's
+ /// a multi-line text field.
+ bool get isMultiline => _isMultiline;
+ bool _isMultiline;
+
/// The total number of scrollable children that contribute to semantics.
///
/// If the number of children are unknown or unbounded, this value will be
@@ -1678,7 +1693,6 @@
double get scrollPosition => _scrollPosition;
double _scrollPosition;
-
/// Indicates the maximum in-range value for [scrollPosition] if the node is
/// scrollable.
///
@@ -1756,6 +1770,7 @@
_customSemanticsActions = Map<CustomSemanticsAction, VoidCallback>.from(config._customSemanticsActions);
_actionsAsBits = config._actionsAsBits;
_textSelection = config._textSelection;
+ _isMultiline = config.isMultiline;
_scrollPosition = config._scrollPosition;
_scrollExtentMax = config._scrollExtentMax;
_scrollExtentMin = config._scrollExtentMin;
@@ -3531,6 +3546,15 @@
_setFlag(SemanticsFlag.isObscured, value);
}
+ /// Whether the text field is multi-line.
+ ///
+ /// This option is usually set in combination with [textField] to indicate
+ /// that the text field is configured to be multi-line.
+ bool get isMultiline => _hasFlag(SemanticsFlag.isMultiline);
+ set isMultiline(bool value) {
+ _setFlag(SemanticsFlag.isMultiline, value);
+ }
+
/// Whether the platform can scroll the semantics node when the user attempts
/// to move focus to an offscreen child.
///
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 3e6f2e5..183ca3f 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -5926,6 +5926,7 @@
bool focused,
bool inMutuallyExclusiveGroup,
bool obscured,
+ bool multiline,
bool scopesRoute,
bool namesRoute,
bool hidden,
@@ -5976,6 +5977,7 @@
focused: focused,
inMutuallyExclusiveGroup: inMutuallyExclusiveGroup,
obscured: obscured,
+ multiline: multiline,
scopesRoute: scopesRoute,
namesRoute: namesRoute,
hidden: hidden,
@@ -6086,6 +6088,7 @@
liveRegion: properties.liveRegion,
inMutuallyExclusiveGroup: properties.inMutuallyExclusiveGroup,
obscured: properties.obscured,
+ multiline: properties.multiline,
scopesRoute: properties.scopesRoute,
namesRoute: properties.namesRoute,
hidden: properties.hidden,
@@ -6151,6 +6154,7 @@
..focused = properties.focused
..inMutuallyExclusiveGroup = properties.inMutuallyExclusiveGroup
..obscured = properties.obscured
+ ..multiline = properties.multiline
..hidden = properties.hidden
..image = properties.image
..liveRegion = properties.liveRegion
diff --git a/packages/flutter/test/widgets/custom_painter_test.dart b/packages/flutter/test/widgets/custom_painter_test.dart
index a23cfaa..9b62629 100644
--- a/packages/flutter/test/widgets/custom_painter_test.dart
+++ b/packages/flutter/test/widgets/custom_painter_test.dart
@@ -341,6 +341,8 @@
onPaste: () => performedActions.add(SemanticsAction.paste),
onMoveCursorForwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByCharacter),
onMoveCursorBackwardByCharacter: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByCharacter),
+ onMoveCursorForwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorForwardByWord),
+ onMoveCursorBackwardByWord: (bool _) => performedActions.add(SemanticsAction.moveCursorBackwardByWord),
onSetSelection: (TextSelection _) => performedActions.add(SemanticsAction.setSelection),
onDidGainAccessibilityFocus: () => performedActions.add(SemanticsAction.didGainAccessibilityFocus),
onDidLoseAccessibilityFocus: () => performedActions.add(SemanticsAction.didLoseAccessibilityFocus),
@@ -349,8 +351,6 @@
),
));
final Set<SemanticsAction> allActions = SemanticsAction.values.values.toSet()
- ..remove(SemanticsAction.moveCursorForwardByWord)
- ..remove(SemanticsAction.moveCursorBackwardByWord)
..remove(SemanticsAction.customAction) // customAction is not user-exposed.
..remove(SemanticsAction.showOnScreen); // showOnScreen is not user-exposed
@@ -378,6 +378,8 @@
switch (action) {
case SemanticsAction.moveCursorBackwardByCharacter:
case SemanticsAction.moveCursorForwardByCharacter:
+ case SemanticsAction.moveCursorBackwardByWord:
+ case SemanticsAction.moveCursorForwardByWord:
semanticsOwner.performAction(expectedId, action, true);
break;
case SemanticsAction.setSelection:
@@ -417,20 +419,24 @@
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
+ // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
+ //multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
liveRegion: true,
+ toggled: true,
),
),
),
));
List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
+ // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
+ // therefore it has to be removed.
flags
- ..remove(SemanticsFlag.hasImplicitScrolling)
- ..remove(SemanticsFlag.hasToggledState)
- ..remove(SemanticsFlag.hasImplicitScrolling)
- ..remove(SemanticsFlag.isToggled);
+ // TODO(mdebbar): Remove this line after https://github.com/flutter/engine/pull/9894
+ ..remove(SemanticsFlag.isMultiline)
+ ..remove(SemanticsFlag.hasImplicitScrolling);
TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
@@ -454,6 +460,7 @@
rect: Rect.fromLTRB(1.0, 2.0, 3.0, 4.0),
properties: SemanticsProperties(
enabled: true,
+ checked: true,
toggled: true,
selected: true,
hidden: true,
@@ -464,6 +471,8 @@
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
+ // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
+ //multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
@@ -473,11 +482,12 @@
),
));
flags = SemanticsFlag.values.values.toList();
+ // [SemanticsFlag.hasImplicitScrolling] isn't part of [SemanticsProperties]
+ // therefore it has to be removed.
flags
- ..remove(SemanticsFlag.hasImplicitScrolling)
- ..remove(SemanticsFlag.hasCheckedState)
- ..remove(SemanticsFlag.hasImplicitScrolling)
- ..remove(SemanticsFlag.isChecked);
+ // TODO(mdebbar): Remove this line after https://github.com/flutter/engine/pull/9894
+ ..remove(SemanticsFlag.isMultiline)
+ ..remove(SemanticsFlag.hasImplicitScrolling);
expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
diff --git a/packages/flutter/test/widgets/editable_text_test.dart b/packages/flutter/test/widgets/editable_text_test.dart
index 8b66e89..a1301d9 100644
--- a/packages/flutter/test/widgets/editable_text_test.dart
+++ b/packages/flutter/test/widgets/editable_text_test.dart
@@ -910,6 +910,67 @@
semantics.dispose();
});
+ testWidgets('EditableText sets multi-line flag in semantics', (WidgetTester tester) async {
+ final SemanticsTester semantics = SemanticsTester(tester);
+
+ await tester.pumpWidget(
+ MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusScope(
+ node: focusScopeNode,
+ autofocus: true,
+ child: EditableText(
+ backgroundCursorColor: Colors.grey,
+ controller: controller,
+ focusNode: focusNode,
+ style: textStyle,
+ cursorColor: cursorColor,
+ maxLines: 1,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(
+ semantics,
+ includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField]),
+ );
+
+ await tester.pumpWidget(
+ MediaQuery(
+ data: const MediaQueryData(devicePixelRatio: 1.0),
+ child: Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusScope(
+ node: focusScopeNode,
+ autofocus: true,
+ child: EditableText(
+ backgroundCursorColor: Colors.grey,
+ controller: controller,
+ focusNode: focusNode,
+ style: textStyle,
+ cursorColor: cursorColor,
+ maxLines: 3,
+ ),
+ ),
+ ),
+ ),
+ );
+
+ expect(
+ semantics,
+ includesNodeWith(flags: <SemanticsFlag>[
+ SemanticsFlag.isTextField,
+ SemanticsFlag.isMultiline,
+ ]),
+ );
+
+ semantics.dispose();
+ });
+
testWidgets('EditableText includes text as value in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
diff --git a/packages/flutter/test/widgets/semantics_test.dart b/packages/flutter/test/widgets/semantics_test.dart
index b967692..dcea4a6 100644
--- a/packages/flutter/test/widgets/semantics_test.dart
+++ b/packages/flutter/test/widgets/semantics_test.dart
@@ -479,6 +479,8 @@
inMutuallyExclusiveGroup: true,
header: true,
obscured: true,
+ // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
+ //multiline: true,
scopesRoute: true,
namesRoute: true,
image: true,
@@ -487,6 +489,8 @@
);
final List<SemanticsFlag> flags = SemanticsFlag.values.values.toList();
flags
+ // TODO(mdebbar): Remove this line after https://github.com/flutter/engine/pull/9894
+ ..remove(SemanticsFlag.isMultiline)
..remove(SemanticsFlag.hasToggledState)
..remove(SemanticsFlag.isToggled)
..remove(SemanticsFlag.hasImplicitScrolling);
diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart
index 9f32de0..73494da 100644
--- a/packages/flutter_test/lib/src/matchers.dart
+++ b/packages/flutter_test/lib/src/matchers.dart
@@ -419,6 +419,7 @@
bool isInMutuallyExclusiveGroup = false,
bool isHeader = false,
bool isObscured = false,
+ bool isMultiline = false,
bool namesRoute = false,
bool scopesRoute = false,
bool isHidden = false,
@@ -479,6 +480,8 @@
flags.add(SemanticsFlag.isHeader);
if (isObscured)
flags.add(SemanticsFlag.isObscured);
+ if (isMultiline)
+ flags.add(SemanticsFlag.isMultiline);
if (namesRoute)
flags.add(SemanticsFlag.namesRoute);
if (scopesRoute)
diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart
index b2108e2..5fb64f3 100644
--- a/packages/flutter_test/test/matchers_test.dart
+++ b/packages/flutter_test/test/matchers_test.dart
@@ -560,7 +560,9 @@
for (int index in SemanticsAction.values.keys)
actions |= index;
for (int index in SemanticsFlag.values.keys)
- flags |= index;
+ // TODO(mdebbar): Remove this if after https://github.com/flutter/engine/pull/9894
+ if (SemanticsFlag.values[index] != SemanticsFlag.isMultiline)
+ flags |= index;
final SemanticsData data = SemanticsData(
flags: flags,
actions: actions,
@@ -604,6 +606,8 @@
isInMutuallyExclusiveGroup: true,
isHeader: true,
isObscured: true,
+ // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894
+ //isMultiline: true,
namesRoute: true,
scopesRoute: true,
isHidden: true,