diff --git a/packages/flutter/lib/src/material/button.dart b/packages/flutter/lib/src/material/button.dart index 0fad0969ea4..3f438f1261c 100644 --- a/packages/flutter/lib/src/material/button.dart +++ b/packages/flutter/lib/src/material/button.dart @@ -332,6 +332,7 @@ class _RawMaterialButtonState extends State { final Widget result = Focus( focusNode: widget.focusNode, + canRequestFocus: widget.enabled, onFocusChange: _handleFocusedChanged, autofocus: widget.autofocus, child: ConstrainedBox( diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index 6755cb95de0..1d38619a449 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -1728,6 +1728,7 @@ class _RawChipState extends State with TickerProviderStateMixin with AutomaticKeepAliveClientMixi bool get selectionEnabled => widget.selectionEnabled; // End of API for TextSelectionGestureDetectorBuilderDelegate. + bool get _isEnabled => widget.enabled ?? widget.decoration?.enabled ?? true; + InputDecoration _getEffectiveDecoration() { final MaterialLocalizations localizations = MaterialLocalizations.of(context); final ThemeData themeData = Theme.of(context); @@ -755,8 +757,10 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi void initState() { super.initState(); _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this); - if (widget.controller == null) + if (widget.controller == null) { _controller = TextEditingController(); + } + _effectiveFocusNode.canRequestFocus = _isEnabled; } @override @@ -766,11 +770,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi _controller = TextEditingController.fromValue(oldWidget.controller.value); else if (widget.controller != null && oldWidget.controller == null) _controller = null; - final bool isEnabled = widget.enabled ?? widget.decoration?.enabled ?? true; - final bool wasEnabled = oldWidget.enabled ?? oldWidget.decoration?.enabled ?? true; - if (wasEnabled && !isEnabled) { - _effectiveFocusNode.unfocus(); - } + _effectiveFocusNode.canRequestFocus = _isEnabled; if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly) { if(_effectiveController.selection.isCollapsed) { _showSelectionHandles = !widget.readOnly; @@ -1045,7 +1045,7 @@ class _TextFieldState extends State with AutomaticKeepAliveClientMixi onEnter: _handleMouseEnter, onExit: _handleMouseExit, child: IgnorePointer( - ignoring: !(widget.enabled ?? widget.decoration?.enabled ?? true), + ignoring: !_isEnabled, child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: child, diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 07f41ff9901..fbf9c3aff65 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -391,7 +391,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { set skipTraversal(bool value) { if (value != _skipTraversal) { _skipTraversal = value; - _notify(); + _manager?._dirtyNodes?.add(this); + _manager?._markNeedsUpdate(); } } @@ -423,7 +424,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { if (!_canRequestFocus) { unfocus(); } - _notify(); + _manager?._dirtyNodes?.add(this); + _manager?._markNeedsUpdate(); } } diff --git a/packages/flutter/lib/src/widgets/shortcuts.dart b/packages/flutter/lib/src/widgets/shortcuts.dart index d8c3b35246d..4b508e7553c 100644 --- a/packages/flutter/lib/src/widgets/shortcuts.dart +++ b/packages/flutter/lib/src/widgets/shortcuts.dart @@ -345,7 +345,7 @@ class _ShortcutsState extends State { @override Widget build(BuildContext context) { return Focus( - skipTraversal: true, + canRequestFocus: false, onKey: _handleOnKey, child: _ShortcutsMarker( manager: manager, diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index 9c11df2254a..cb9ef7f5aea 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -1837,4 +1837,71 @@ void main() { // Teardown. await gesture.removePointer(); }); + + testWidgets('loses focus when disabled', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'InputChip'); + await tester.pumpWidget( + _wrapForChip( + child: InputChip( + focusNode: focusNode, + autofocus: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0.0))), + avatar: const CircleAvatar(child: Text('A')), + label: const Text('Chip A'), + onPressed: () { }, + ), + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); + + await tester.pumpWidget( + _wrapForChip( + child: InputChip( + focusNode: focusNode, + autofocus: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(0.0))), + avatar: const CircleAvatar(child: Text('A')), + label: const Text('Chip A'), + onPressed: null, + ), + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isFalse); + }); + + testWidgets('cannot be traversed to when disabled', (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: 'InputChip 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: 'InputChip 2'); + await tester.pumpWidget( + _wrapForChip( + child: Column( + children: [ + InputChip( + focusNode: focusNode1, + autofocus: true, + label: const Text('Chip A'), + onPressed: () { }, + ), + InputChip( + focusNode: focusNode2, + autofocus: true, + label: const Text('Chip B'), + onPressed: null, + ), + ], + ), + ), + ); + await tester.pump(); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + + expect(focusNode1.nextFocus(), isTrue); + + await tester.pump(); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + }); } diff --git a/packages/flutter/test/material/icon_button_test.dart b/packages/flutter/test/material/icon_button_test.dart index cff56c4e8b3..4419a94178b 100644 --- a/packages/flutter/test/material/icon_button_test.dart +++ b/packages/flutter/test/material/icon_button_test.dart @@ -332,13 +332,81 @@ void main() { semantics.dispose(); }); + + testWidgets('IconButton loses focus when disabled.', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'IconButton'); + await tester.pumpWidget( + wrap( + child: IconButton( + focusNode: focusNode, + autofocus: true, + onPressed: () {}, + icon: const Icon(Icons.link), + ), + ), + ); + + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); + + await tester.pumpWidget( + wrap( + child: IconButton( + focusNode: focusNode, + autofocus: true, + onPressed: null, + icon: const Icon(Icons.link), + ), + ), + ); + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isFalse); + }); + + testWidgets("Disabled IconButton can't be traversed to when disabled.", (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: 'IconButton 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: 'IconButton 2'); + + await tester.pumpWidget( + wrap( + child: Column( + children: [ + IconButton( + focusNode: focusNode1, + autofocus: true, + onPressed: () {}, + icon: const Icon(Icons.link), + ), + IconButton( + focusNode: focusNode2, + onPressed: null, + icon: const Icon(Icons.link), + ), + ], + ), + ), + ); + await tester.pump(); + + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + + expect(focusNode1.nextFocus(), isTrue); + await tester.pump(); + + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + }); } Widget wrap({ Widget child }) { - return Directionality( - textDirection: TextDirection.ltr, - child: Material( - child: Center(child: child), + return DefaultFocusTraversal( + policy: ReadingOrderTraversalPolicy(), + child: Directionality( + textDirection: TextDirection.ltr, + child: Material( + child: Center(child: child), + ), ), ); } diff --git a/packages/flutter/test/material/raw_material_button_test.dart b/packages/flutter/test/material/raw_material_button_test.dart index da4a1df52a1..94e9dbaa4a1 100644 --- a/packages/flutter/test/material/raw_material_button_test.dart +++ b/packages/flutter/test/material/raw_material_button_test.dart @@ -237,6 +237,78 @@ void main() { expect(box, paints..rect(color: focusColor)); }); + testWidgets('$RawMaterialButton loses focus when disabled.', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'RawMaterialButton'); + await tester.pumpWidget( + MaterialApp( + home: Center( + child: RawMaterialButton( + autofocus: true, + focusNode: focusNode, + onPressed: () {}, + child: Container(width: 100, height: 100, color: const Color(0xffff0000)), + ), + ), + ), + ); + + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: RawMaterialButton( + focusNode: focusNode, + onPressed: null, + child: Container(width: 100, height: 100, color: const Color(0xffff0000)), + ), + ), + ), + ); + + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isFalse); + }); + + testWidgets("Disabled $RawMaterialButton can't be traversed to when disabled.", (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: '$RawMaterialButton 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: '$RawMaterialButton 2'); + + await tester.pumpWidget( + MaterialApp( + home: Center( + child: Column( + children: [ + RawMaterialButton( + autofocus: true, + focusNode: focusNode1, + onPressed: () {}, + child: Container(width: 100, height: 100, color: const Color(0xffff0000)), + ), + RawMaterialButton( + autofocus: true, + focusNode: focusNode2, + onPressed: null, + child: Container(width: 100, height: 100, color: const Color(0xffff0000)), + ), + ], + ), + ), + ), + ); + await tester.pump(); + + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + + expect(focusNode1.nextFocus(), isTrue); + await tester.pump(); + + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + }); + testWidgets('$RawMaterialButton handles hover', (WidgetTester tester) async { const Key key = Key('test'); const Color hoverColor = Color(0xff00ff00); diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index da72c9a2b47..9d49ce62f02 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -3391,6 +3391,83 @@ void main() { semantics.dispose(); }); + testWidgets('TextField loses focus when disabled', (WidgetTester tester) async { + final FocusNode focusNode = FocusNode(debugLabel: 'TextField'); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + focusNode: focusNode, + autofocus: true, + maxLength: 10, + enabled: true, + ), + ), + ), + ), + ); + + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isTrue); + + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: TextField( + focusNode: focusNode, + autofocus: true, + maxLength: 10, + enabled: false, + ), + ), + ), + ), + ); + + await tester.pump(); + expect(focusNode.hasPrimaryFocus, isFalse); + }); + + testWidgets("Disabled TextField can't be traversed to when disabled.", (WidgetTester tester) async { + final FocusNode focusNode1 = FocusNode(debugLabel: 'TextField 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: 'TextField 2'); + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Center( + child: Column( + children: [ + TextField( + focusNode: focusNode1, + autofocus: true, + maxLength: 10, + enabled: true, + ), + TextField( + focusNode: focusNode2, + maxLength: 10, + enabled: false, + ), + ], + ), + ), + ), + ), + ); + + await tester.pump(); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + + expect(focusNode1.nextFocus(), isTrue); + await tester.pump(); + + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasPrimaryFocus, isFalse); + }); + void sendFakeKeyEvent(Map data) { ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( SystemChannels.keyEvent.name, diff --git a/packages/flutter/test/material/text_form_field_test.dart b/packages/flutter/test/material/text_form_field_test.dart index f2fc8d8cb22..acffbc40fc3 100644 --- a/packages/flutter/test/material/text_form_field_test.dart +++ b/packages/flutter/test/material/text_form_field_test.dart @@ -184,36 +184,11 @@ void main() { ); expect(_validateCalled, 1); - await tester.showKeyboard(find.byType(TextField)); await tester.enterText(find.byType(TextField), 'a'); await tester.pump(); expect(_validateCalled, 2); }); - testWidgets('validate is not called if widget is disabled', (WidgetTester tester) async { - int _validateCalled = 0; - - await tester.pumpWidget( - MaterialApp( - home: Material( - child: Center( - child: TextFormField( - enabled: false, - autovalidate: true, - validator: (String value) { _validateCalled += 1; return null; }, - ), - ), - ), - ), - ); - - expect(_validateCalled, 0); - await tester.showKeyboard(find.byType(TextField)); - await tester.enterText(find.byType(TextField), 'a'); - await tester.pump(); - expect(_validateCalled, 0); - }); - testWidgets('validate is called if widget is enabled', (WidgetTester tester) async { int _validateCalled = 0; @@ -232,7 +207,6 @@ void main() { ); expect(_validateCalled, 1); - await tester.showKeyboard(find.byType(TextField)); await tester.enterText(find.byType(TextField), 'a'); await tester.pump(); expect(_validateCalled, 2);