diff --git a/packages/flutter/lib/src/material/input_decorator.dart b/packages/flutter/lib/src/material/input_decorator.dart index e771088253..276669a328 100644 --- a/packages/flutter/lib/src/material/input_decorator.dart +++ b/packages/flutter/lib/src/material/input_decorator.dart @@ -540,12 +540,14 @@ class _RenderDecoration extends RenderBox { @required _Decoration decoration, @required TextDirection textDirection, @required TextBaseline textBaseline, + @required bool isFocused, }) : assert(decoration != null), assert(textDirection != null), assert(textBaseline != null), _decoration = decoration, _textDirection = textDirection, - _textBaseline = textBaseline; + _textBaseline = textBaseline, + _isFocused = isFocused; final Map<_DecorationSlot, RenderBox> slotToChild = <_DecorationSlot, RenderBox>{}; final Map childToSlot = {}; @@ -686,6 +688,16 @@ class _RenderDecoration extends RenderBox { markNeedsLayout(); } + bool get isFocused => _isFocused; + bool _isFocused; + set isFocused(bool value) { + assert(value != null); + if (_isFocused == value) + return; + _isFocused = value; + markNeedsSemanticsUpdate(); + } + @override void attach(PipelineOwner owner) { super.attach(owner); @@ -710,6 +722,35 @@ class _RenderDecoration extends RenderBox { _children.forEach(visitor); } + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (icon != null) + visitor(icon); + if (prefix != null) + 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) + visitor(label); + if (input != null) + visitor(input); + if (suffixIcon != null) + visitor(suffixIcon); + if (suffix != null) + visitor(suffix); + if (container != null) + visitor(container); + if (helperError != null) + visitor(helperError); + if (counter != null) + visitor(counter); + } + @override List debugDescribeChildren() { final List value = []; @@ -1301,6 +1342,7 @@ class _Decorator extends RenderObjectWidget { @required this.decoration, @required this.textDirection, @required this.textBaseline, + @required this.isFocused, }) : assert(decoration != null), assert(textDirection != null), assert(textBaseline != null), @@ -1309,6 +1351,7 @@ class _Decorator extends RenderObjectWidget { final _Decoration decoration; final TextDirection textDirection; final TextBaseline textBaseline; + final bool isFocused; @override _RenderDecorationElement createElement() => new _RenderDecorationElement(this); @@ -1319,6 +1362,7 @@ class _Decorator extends RenderObjectWidget { decoration: decoration, textDirection: textDirection, textBaseline: textBaseline, + isFocused: isFocused, ); } @@ -1327,7 +1371,8 @@ class _Decorator extends RenderObjectWidget { renderObject ..decoration = decoration ..textDirection = textDirection - ..textBaseline = textBaseline; + ..textBaseline = textBaseline + ..isFocused = isFocused; } } @@ -1784,6 +1829,7 @@ class _InputDecoratorState extends State with TickerProviderStat ? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0) : const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0)); } + return new _Decorator( decoration: new _Decoration( contentPadding: contentPadding, @@ -1803,9 +1849,10 @@ class _InputDecoratorState extends State with TickerProviderStat helperError: helperError, counter: counter, container: container, - ), - textDirection: textDirection, - textBaseline: textBaseline, + ), + textDirection: textDirection, + textBaseline: textBaseline, + isFocused: isFocused, ); } } diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 466bf5e036..04178ecec5 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -2250,4 +2250,68 @@ void main() { expect(focusNode.hasFocus, isFalse); }); + testWidgets('TextField semantics', (WidgetTester tester) async { + final SemanticsTester semantics = new SemanticsTester(tester); + final TextEditingController controller = new TextEditingController(); + final Key key = new UniqueKey(); + + await tester.pumpWidget( + overlay( + child: new TextField( + key: key, + controller: controller, + decoration: const InputDecoration( + labelText: 'label', + hintText: 'hint', + helperText: 'helper', + counterText: 'counter', + ), + ), + ), + ); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'label\nhelper\ncounter', + id: 1, + textDirection: TextDirection.ltr, + actions: [ + SemanticsAction.tap, + ], + flags: [ + SemanticsFlag.isTextField, + ], + ), + ], + ), ignoreTransform: true, ignoreRect: true)); + + await tester.tap(find.byType(TextField)); + await tester.pump(); + + expect(semantics, hasSemantics(new TestSemantics.root( + children: [ + new TestSemantics.rootChild( + label: 'hint\nhelper\ncounter', + id: 1, + textDirection: TextDirection.ltr, + textSelection: const TextSelection(baseOffset: 0, extentOffset: 0), + actions: [ + SemanticsAction.tap, + SemanticsAction.setSelection, + SemanticsAction.paste, + ], + flags: [ + SemanticsFlag.isTextField, + SemanticsFlag.isFocused, + ], + ), + ], + ), ignoreTransform: true, ignoreRect: true)); + + controller.text = 'hello'; + await tester.pump(); + semantics.dispose(); + }); + }