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