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,