diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 89b4d68be1..ab96d9f3f8 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -515,6 +515,9 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { /// /// Does not affect the value of [canRequestFocus] on the descendants. /// + /// If a descendant node loses focus when this value is changed, the focus + /// will move to the scope enclosing this node. + /// /// See also: /// /// * [ExcludeFocus], a widget that uses this property to conditionally @@ -531,12 +534,12 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { if (value == _descendantsAreFocusable) { return; } - if (!value && hasFocus) { - for (final FocusNode child in children) { - child.unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); - } - } + // Set _descendantsAreFocusable before unfocusing, so the scope won't try + // and focus any of the children here again if it is false. _descendantsAreFocusable = value; + if (!value && hasFocus) { + unfocus(disposition: UnfocusDisposition.previouslyFocusedChild); + } _manager?._markPropertiesChanged(this); } diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart index e3b716e5f2..5f3e4e49e9 100644 --- a/packages/flutter/test/widgets/focus_scope_test.dart +++ b/packages/flutter/test/widgets/focus_scope_test.dart @@ -77,7 +77,7 @@ class TestFocusState extends State { } void main() { - group(FocusScope, () { + group('FocusScope', () { testWidgets('Can focus', (WidgetTester tester) async { final GlobalKey key = GlobalKey(); @@ -1078,7 +1078,7 @@ void main() { }); }); - group(Focus, () { + group('Focus', () { testWidgets('Focus.of stops at the nearest Focus widget.', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); @@ -1596,7 +1596,7 @@ void main() { expect(semantics, hasSemantics(expectedSemantics)); }); }); - group(ExcludeFocus, () { + group('ExcludeFocus', () { testWidgets("Descendants of ExcludeFocus aren't focusable.", (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(debugLabel: '1'); final GlobalKey key2 = GlobalKey(debugLabel: '2'); @@ -1635,6 +1635,76 @@ void main() { expect(containerNode.hasFocus, isFalse); expect(unfocusableNode.hasFocus, isFalse); }); + // Regression test for https://github.com/flutter/flutter/issues/61700 + testWidgets("ExcludeFocus doesn't transfer focus to another descendant.", (WidgetTester tester) async { + final FocusNode parentFocusNode = FocusNode(debugLabel: 'group'); + final FocusNode focusNode1 = FocusNode(debugLabel: 'node 1'); + final FocusNode focusNode2 = FocusNode(debugLabel: 'node 2'); + await tester.pumpWidget( + ExcludeFocus( + excluding: false, + child: Focus( + focusNode: parentFocusNode, + child: Column( + children: [ + Focus( + autofocus: true, + focusNode: focusNode1, + child: Container(), + ), + Focus( + focusNode: focusNode2, + child: Container(), + ), + ], + ), + ), + ), + ); + + await tester.pump(); + + expect(parentFocusNode.hasFocus, isTrue); + expect(focusNode1.hasPrimaryFocus, isTrue); + expect(focusNode2.hasFocus, isFalse); + + // Move focus to the second node to create some focus history for the scope. + focusNode2.requestFocus(); + await tester.pump(); + + expect(parentFocusNode.hasFocus, isTrue); + expect(focusNode1.hasFocus, isFalse); + expect(focusNode2.hasPrimaryFocus, isTrue); + + // Now turn off the focus for the subtree. + await tester.pumpWidget( + ExcludeFocus( + excluding: true, + child: Focus( + focusNode: parentFocusNode, + child: Column( + children: [ + Focus( + autofocus: true, + focusNode: focusNode1, + child: Container(), + ), + Focus( + focusNode: focusNode2, + child: Container(), + ), + ], + ), + ), + ), + ); + await tester.pump(); + + expect(focusNode1.hasFocus, isFalse); + expect(focusNode2.hasFocus, isFalse); + expect(parentFocusNode.hasFocus, isFalse); + expect(parentFocusNode.enclosingScope.hasPrimaryFocus, isTrue); + }); testWidgets("ExcludeFocus doesn't introduce a Semantics node", (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(ExcludeFocus(child: Container()));