a11y improvements for textfield (#40390)
diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index d51f53ac..35e8b0a 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart
@@ -827,14 +827,18 @@ visitor(prefix); if (prefixIcon != null) visitor(prefixIcon); - if (isFocused && hint != null) { - // Bypass opacity to always read hint when focused. This prevents the - // label from changing when text is entered. - final RenderProxyBox typedHint = hint; - visitor(typedHint.child); - } else if (!isFocused && label != null) { + + if (label != null) { visitor(label); } + if (hint != null) { + if (isFocused) { + visitor(hint); + } else if (label == null) { + visitor(hint); + } + } + if (input != null) visitor(input); if (suffixIcon != null) @@ -2060,6 +2064,7 @@ opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0, duration: _kTransitionDuration, curve: _kTransitionCurve, + alwaysIncludeSemantics: true, child: Text( decoration.hintText, style: hintStyle,
diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 40b8364..98c4d07 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart
@@ -1274,6 +1274,7 @@ @required this.opacity, Curve curve = Curves.linear, @required Duration duration, + this.alwaysIncludeSemantics = false, }) : assert(opacity != null && opacity >= 0.0 && opacity <= 1.0), super(key: key, curve: curve, duration: duration); @@ -1290,6 +1291,16 @@ /// The opacity must not be null. final double opacity; + /// Whether the semantic information of the children is always included. + /// + /// Defaults to false. + /// + /// When true, regardless of the opacity settings the child semantic + /// information is exposed as if the widget were fully visible. This is + /// useful in cases where labels may be hidden during animations that + /// would otherwise contribute relevant semantics. + final bool alwaysIncludeSemantics; + @override _AnimatedOpacityState createState() => _AnimatedOpacityState(); @@ -1319,6 +1330,7 @@ return FadeTransition( opacity: _opacityAnimation, child: widget.child, + alwaysIncludeSemantics: widget.alwaysIncludeSemantics, ); } }
diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 9d49ce6..1c458d9 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart
@@ -4212,6 +4212,100 @@ expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); }); + testWidgets('TextField semantics include label when unfocused and label/hint when focused', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final TextEditingController controller = TextEditingController(text: 'value'); + final Key key = UniqueKey(); + + await tester.pumpWidget( + overlay( + child: TextField( + key: key, + controller: controller, + decoration: const InputDecoration( + hintText: 'hint', + labelText: 'label', + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + + expect(node.label, 'label'); + expect(node.value, 'value'); + + // Focus text field. + await tester.tap(find.byKey(key)); + await tester.pump(); + + expect(node.label, 'label\nhint'); + expect(node.value, 'value'); + semantics.dispose(); + }); + + testWidgets('TextField semantics always include label when no hint is given', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final TextEditingController controller = TextEditingController(text: 'value'); + final Key key = UniqueKey(); + + await tester.pumpWidget( + overlay( + child: TextField( + key: key, + controller: controller, + decoration: const InputDecoration( + labelText: 'label', + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + + expect(node.label, 'label'); + expect(node.value, 'value'); + + // Focus text field. + await tester.tap(find.byKey(key)); + await tester.pump(); + + expect(node.label, 'label'); + expect(node.value, 'value'); + semantics.dispose(); + }); + + testWidgets('TextField semantics always include hint when no label is given', (WidgetTester tester) async { + final SemanticsTester semantics = SemanticsTester(tester); + final TextEditingController controller = TextEditingController(text: 'value'); + final Key key = UniqueKey(); + + await tester.pumpWidget( + overlay( + child: TextField( + key: key, + controller: controller, + decoration: const InputDecoration( + hintText: 'hint', + ), + ), + ), + ); + + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + + expect(node.label, 'hint'); + expect(node.value, 'value'); + + // Focus text field. + await tester.tap(find.byKey(key)); + await tester.pump(); + + expect(node.label, 'hint'); + expect(node.value, 'value'); + semantics.dispose(); + }); + testWidgets('TextField semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final TextEditingController controller = TextEditingController(); @@ -4769,7 +4863,7 @@ expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics.rootChild( - label: 'hint', + label: 'label\nhint', id: 1, textDirection: TextDirection.ltr, textSelection: const TextSelection(baseOffset: 0, extentOffset: 0),