Make helper and error text separate widgets, make error and counter live region (#21752)

diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart
index 44ae7e3..a923f78 100644
--- a/packages/flutter/lib/src/material/input_decorator.dart
+++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -320,32 +320,39 @@
 
   Widget _buildHelper() {
     assert(widget.helperText != null);
-    return Opacity(
-      opacity: 1.0 - _controller.value,
-      child: Text(
-        widget.helperText,
-        style: widget.helperStyle,
-        textAlign: widget.textAlign,
-        overflow: TextOverflow.ellipsis,
+    return Semantics(
+      container: true,
+      child: Opacity(
+        opacity: 1.0 - _controller.value,
+        child: Text(
+          widget.helperText,
+          style: widget.helperStyle,
+          textAlign: widget.textAlign,
+          overflow: TextOverflow.ellipsis,
+        ),
       ),
     );
   }
 
   Widget _buildError() {
     assert(widget.errorText != null);
-    return Opacity(
-      opacity: _controller.value,
-      child: FractionalTranslation(
-        translation: Tween<Offset>(
-          begin: const Offset(0.0, -0.25),
-          end: const Offset(0.0, 0.0),
-        ).evaluate(_controller.view),
-        child: Text(
-          widget.errorText,
-          style: widget.errorStyle,
-          textAlign: widget.textAlign,
-          overflow: TextOverflow.ellipsis,
-          maxLines: widget.errorMaxLines,
+    return Semantics(
+      container: true,
+      liveRegion: true,
+      child: Opacity(
+        opacity: _controller.value,
+        child: FractionalTranslation(
+          translation: Tween<Offset>(
+            begin: const Offset(0.0, -0.25),
+            end: const Offset(0.0, 0.0),
+          ).evaluate(_controller.view),
+          child: Text(
+            widget.errorText,
+            style: widget.errorStyle,
+            textAlign: widget.textAlign,
+            overflow: TextOverflow.ellipsis,
+            maxLines: widget.errorMaxLines,
+          ),
         ),
       ),
     );
@@ -1815,6 +1822,7 @@
     final Widget counter = decoration.counterText == null ? null :
       Semantics(
         container: true,
+        liveRegion: isFocused,
         child: Text(
           decoration.counterText,
           style: _getHelperStyle(themeData).merge(decoration.counterStyle),
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart
index 813055e..5a1d8d6 100644
--- a/packages/flutter/test/material/text_field_test.dart
+++ b/packages/flutter/test/material/text_field_test.dart
@@ -2843,7 +2843,7 @@
     expect(semantics, hasSemantics(TestSemantics.root(
       children: <TestSemantics>[
         TestSemantics.rootChild(
-          label: 'label\nhelper',
+          label: 'label',
           id: 1,
           textDirection: TextDirection.ltr,
           actions: <SemanticsAction>[
@@ -2855,6 +2855,11 @@
           children: <TestSemantics>[
             TestSemantics(
               id: 2,
+              label: 'helper',
+              textDirection: TextDirection.ltr,
+            ),
+            TestSemantics(
+              id: 3,
               label: '10 characters remaining',
               textDirection: TextDirection.ltr,
             ),
@@ -2869,7 +2874,7 @@
     expect(semantics, hasSemantics(TestSemantics.root(
       children: <TestSemantics>[
         TestSemantics.rootChild(
-          label: 'hint\nhelper',
+          label: 'hint',
           id: 1,
           textDirection: TextDirection.ltr,
           textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),
@@ -2885,7 +2890,15 @@
           children: <TestSemantics>[
             TestSemantics(
               id: 2,
+              label: 'helper',
+              textDirection: TextDirection.ltr,
+            ),
+            TestSemantics(
+              id: 3,
               label: '10 characters remaining',
+              flags: <SemanticsFlag>[
+                SemanticsFlag.isLiveRegion,
+              ],
               textDirection: TextDirection.ltr,
             ),
           ],
@@ -2922,8 +2935,7 @@
     expect(semantics, hasSemantics(TestSemantics.root(
       children: <TestSemantics>[
         TestSemantics.rootChild(
-          label: 'label\nhelper',
-          id: 1,
+          label: 'label',
           textDirection: TextDirection.ltr,
           actions: <SemanticsAction>[
             SemanticsAction.tap,
@@ -2933,6 +2945,10 @@
           ],
           children: <TestSemantics>[
             TestSemantics(
+              label: 'helper',
+              textDirection: TextDirection.ltr,
+            ),
+            TestSemantics(
               label: '0 out of 10',
               textDirection: TextDirection.ltr,
             ),
@@ -2944,6 +2960,52 @@
     semantics.dispose();
   });
 
+  testWidgets('InputDecoration errorText semantics', (WidgetTester tester) async {
+    final SemanticsTester semantics = SemanticsTester(tester);
+    final TextEditingController controller = TextEditingController();
+    final Key key = UniqueKey();
+
+    await tester.pumpWidget(
+      overlay(
+        child: TextField(
+          key: key,
+          controller: controller,
+          decoration: const InputDecoration(
+            labelText: 'label',
+            hintText: 'hint',
+            errorText: 'oh no!',
+          ),
+        ),
+      ),
+    );
+
+    expect(semantics, hasSemantics(TestSemantics.root(
+      children: <TestSemantics>[
+        TestSemantics.rootChild(
+          label: 'label',
+          textDirection: TextDirection.ltr,
+          actions: <SemanticsAction>[
+            SemanticsAction.tap,
+          ],
+          flags: <SemanticsFlag>[
+            SemanticsFlag.isTextField,
+          ],
+          children: <TestSemantics>[
+            TestSemantics(
+              label: 'oh no!',
+              flags: <SemanticsFlag>[
+                SemanticsFlag.isLiveRegion,
+              ],
+              textDirection: TextDirection.ltr,
+            ),
+          ],
+        ),
+      ],
+    ), ignoreTransform: true, ignoreRect: true, ignoreId: true));
+
+    semantics.dispose();
+  });
+
   testWidgets('floating label does not overlap with value at large textScaleFactors', (WidgetTester tester) async {
     final TextEditingController controller = TextEditingController(text: 'Just some text');
     await tester.pumpWidget(
@@ -2964,6 +3026,7 @@
         ),
       ),
     );
+
     await tester.tap(find.byType(TextField));
     final Rect labelRect = tester.getRect(find.text('Label'));
     final Rect fieldRect = tester.getRect(find.text('Just some text'));