diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index ccca09ca15b..e28527cf2cb 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -121,6 +121,9 @@ class FocusNode extends ChangeNotifier { /// parent [FocusScopeNode] and [FocusScopeNode.isFirstFocus] is true for /// that scope and all its ancestor scopes. /// +/// If a [FocusScopeNode] is removed, then the next sibling node will be set as +/// the focused node by the [FocusManager]. +/// /// See also: /// /// * [FocusNode], which is a leaf node in the focus tree that can receive @@ -326,7 +329,6 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { /// the child. void setFirstFocus(FocusScopeNode child) { assert(child != null); - assert(child._parent == null || child._parent == this); if (_firstChild == child) return; child.detach(); @@ -348,10 +350,11 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { assert(child != null); if (child._parent == null || child._parent == this) return; - if (child.isFirstFocus) + if (child.isFirstFocus) { setFirstFocus(child); - else + } else { child.detach(); + } } /// Remove this scope from its parent child list. @@ -394,8 +397,9 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { /// Manages the focus tree. /// -/// The focus tree keeps track of which widget is the user's current focus. The -/// focused widget often listens for keyboard events. +/// The focus tree keeps track of which [FocusNode] is the user's current +/// keyboard focus. The widget that owns the [FocusNode] often listens for +/// keyboard events. /// /// The focus manager is responsible for holding the [FocusScopeNode] that is /// the root of the focus tree and tracking which [FocusNode] has the overall @@ -406,6 +410,12 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { /// directly. Instead, to find the [FocusScopeNode] for a given [BuildContext], /// use [FocusScope.of]. /// +/// The [FocusManager] knows nothing about [FocusNode]s other than the one that +/// is currently focused. If a [FocusScopeNode] is removed, then the +/// [FocusManager] will attempt to focus the next [FocusScopeNode] in the focus +/// tree that it maintains, but if the current focus in that [FocusScopeNode] is +/// null, it will stop there, and no [FocusNode] will have focus. +/// /// See also: /// /// * [FocusNode], which is a leaf node in the focus tree that can receive diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart index 51724b94f62..39dd0b384d2 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -37,8 +37,12 @@ class _FocusScopeMarker extends InheritedWidget { /// FocusScope.of(context).setFirstFocus(node); /// ``` /// -/// When a [FocusScope] is removed from the tree, the previously active -/// [FocusScope] becomes active again. +/// If a [FocusScope] is removed from the widget tree, then the previously +/// focused node will be focused, but only if the [node] is the same [node] +/// object as in the previous frame. To assure this, you can use a GlobalKey to +/// keep the [FocusScope] widget from being rebuilt from one frame to the next, +/// or pass in the [node] from a parent that is not rebuilt. If there is no next +/// sibling, then the parent scope node will be focused. /// /// See also: /// diff --git a/packages/flutter/test/widgets/focus_scope_test.dart b/packages/flutter/test/widgets/focus_scope_test.dart new file mode 100644 index 00000000000..fb0abcc0380 --- /dev/null +++ b/packages/flutter/test/widgets/focus_scope_test.dart @@ -0,0 +1,729 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/widgets.dart'; + +class TestFocusable extends StatefulWidget { + const TestFocusable({ + Key key, + this.name = 'a', + this.autofocus = false, + }) : super(key: key); + + final String name; + final bool autofocus; + + @override + TestFocusableState createState() => TestFocusableState(); +} + +class TestFocusableState extends State { + final FocusNode focusNode = FocusNode(); + bool _didAutofocus = false; + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + FocusScope.of(context).reparentIfNeeded(focusNode); + if (!_didAutofocus && widget.autofocus) { + _didAutofocus = true; + FocusScope.of(context).autofocus(focusNode); + } + return GestureDetector( + onTap: () { + FocusScope.of(context).requestFocus(focusNode); + }, + child: AnimatedBuilder( + animation: focusNode, + builder: (BuildContext context, Widget child) { + return Text( + focusNode.hasFocus ? '${widget.name.toUpperCase()} FOCUSED' : widget.name.toLowerCase(), + textDirection: TextDirection.ltr, + ); + }, + ), + ); + } +} + +void main() { + testWidgets('Can focus', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + + await tester.pumpWidget( + TestFocusable(key: key, name: 'a'), + ); + + expect(key.currentState.focusNode.hasFocus, isFalse); + + FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode); + await tester.pumpAndSettle(); + + expect(key.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + }); + + testWidgets('Can unfocus', (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + await tester.pumpWidget( + Column( + children: [ + TestFocusable(key: keyA, name: 'a'), + TestFocusable(key: keyB, name: 'b'), + ], + ), + ); + + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + // Set focus to the "B" node to unfocus the "A" node. + FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isTrue); + expect(find.text('B FOCUSED'), findsOneWidget); + }); + + testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + + await tester.pumpWidget( + Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + autofocus: true, + ), + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ); + + // Autofocus is delayed one frame. + await tester.pump(); + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + await tester.tap(find.text('A FOCUSED')); + await tester.pump(); + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + await tester.tap(find.text('b')); + await tester.pump(); + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isTrue); + expect(find.text('B FOCUSED'), findsOneWidget); + await tester.tap(find.text('a')); + await tester.pump(); + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); + + // This moves a focus node first into a focus scope that is added to its + // parent, and then out of that focus scope again. + testWidgets('Can move focus in and out of FocusScope', (WidgetTester tester) async { + final FocusScopeNode parentFocusScope = FocusScopeNode(); + final FocusScopeNode childFocusScope = FocusScopeNode(); + final GlobalKey key = GlobalKey(); + + // Initially create the focus inside of the parent FocusScope. + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + autofocus: true, + child: Column( + children: [ + TestFocusable(key: key, name: 'a'), + ], + ), + ), + ); + + expect(key.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + FocusScope.of(key.currentContext).requestFocus(key.currentState.focusNode); + await tester.pumpAndSettle(); + + expect(key.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + + expect(parentFocusScope, hasAGoodToStringDeep); + expect( + parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info), + equalsIgnoringHashCodes('FocusScopeNode#00000\n' + ' focus: FocusNode#00000(FOCUSED)\n'), + ); + + expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep); + expect( + WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info), + equalsIgnoringHashCodes('FocusScopeNode#00000\n' + ' └─child 1: FocusScopeNode#00000\n' + ' focus: FocusNode#00000(FOCUSED)\n'), + ); + + // Add the child focus scope to the focus tree. + parentFocusScope.setFirstFocus(childFocusScope); + expect(childFocusScope.isFirstFocus, isTrue); + + // Now add the child focus scope with no focus node in it to the tree. + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + child: Column( + children: [ + TestFocusable(key: key), + FocusScope( + node: childFocusScope, + child: Container(), + ), + ], + ), + ), + ); + + expect(key.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + + // Now move the existing focus node into the child focus scope. + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + child: Column( + children: [ + FocusScope( + node: childFocusScope, + child: TestFocusable(key: key), + ), + ], + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(key.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + + // Now remove the child focus scope. + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + child: Column( + children: [ + TestFocusable(key: key), + ], + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(key.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + }); + + // Arguably, this isn't correct behavior, but it is what happens now. + testWidgets("Removing focused widget doesn't move focus to next widget", (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + + await tester.pumpWidget( + Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + await tester.pumpWidget( + Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ); + + await tester.pump(); + + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); + + testWidgets('Adding a new FocusScope attaches the child it to its parent.', (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final FocusScopeNode parentFocusScope = FocusScopeNode(); + final FocusScopeNode childFocusScope = FocusScopeNode(); + + await tester.pumpWidget( + FocusScope( + node: childFocusScope, + child: TestFocusable( + key: keyA, + name: 'a', + ), + ), + ); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(childFocusScope.isFirstFocus, isTrue); + + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + child: FocusScope( + node: childFocusScope, + child: TestFocusable( + key: keyA, + name: 'a', + ), + ), + ), + ); + + await tester.pump(); + + expect(childFocusScope.isFirstFocus, isTrue); + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + }); + + // Arguably, this isn't correct behavior, but it is what happens now. + testWidgets("Removing focused widget doesn't move focus to next widget within FocusScope", (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + final FocusScopeNode parentFocusScope = FocusScopeNode(); + + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + autofocus: true, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + await tester.pumpWidget( + FocusScope( + node: parentFocusScope, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ); + + await tester.pump(); + + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); + + // By "pinned", it means kept in the tree by a GlobalKey. + testWidgets('Removing pinned focused scope moves focus to focused widget within next FocusScope', (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + final GlobalKey scopeKeyA = GlobalKey(); + final GlobalKey scopeKeyB = GlobalKey(); + final FocusScopeNode parentFocusScope1 = FocusScopeNode(); + final FocusScopeNode parentFocusScope2 = FocusScopeNode(); + + await tester.pumpWidget( + Column( + children: [ + FocusScope( + key: scopeKeyA, + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + FocusScope( + key: scopeKeyB, + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ], + ), + ); + + FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue); + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + // Since the FocusScope widgets are pinned with GlobalKeys, when the first + // one gets removed, the second one stays registered with the focus + // manager and ends up getting the focus since it remains as part of the + // focus tree. + await tester.pumpWidget( + Column( + children: [ + FocusScope( + key: scopeKeyB, + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + autofocus: true, + ), + ], + ), + ), + ], + ), + ); + + await tester.pump(); + + expect(keyB.currentState.focusNode.hasFocus, isTrue); + expect(find.text('B FOCUSED'), findsOneWidget); + }); + + // Arguably, this isn't correct behavior, but it is what happens now. + testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + final FocusScopeNode parentFocusScope1 = FocusScopeNode(); + final FocusScopeNode parentFocusScope2 = FocusScopeNode(); + + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ], + ), + ); + + FocusScope.of(keyB.currentContext).requestFocus(keyB.currentState.focusNode); + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyB.currentContext)); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(FocusScope.of(keyA.currentContext).isFirstFocus, isTrue); + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + // If the FocusScope widgets are not pinned with GlobalKeys, then the first + // one remains and gets its guts replaced with the parentFocusScope2 and the + // "B" test widget, and in the process, the focus manager loses track of the + // focus. + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + autofocus: true, + ), + ], + ), + ), + ], + ), + ); + await tester.pump(); + + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); + + // Arguably, this isn't correct behavior, but it is what happens now. + testWidgets('Moving widget from one scope to another does not retain focus', (WidgetTester tester) async { + final FocusScopeNode parentFocusScope1 = FocusScopeNode(); + final FocusScopeNode parentFocusScope2 = FocusScopeNode(); + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ], + ), + ); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + ], + ), + ); + + await tester.pump(); + + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); + + // Arguably, this isn't correct behavior, but it is what happens now. + testWidgets('Moving FocusScopeNodes does not retain focus', (WidgetTester tester) async { + final FocusScopeNode parentFocusScope1 = FocusScopeNode(); + final FocusScopeNode parentFocusScope2 = FocusScopeNode(); + final GlobalKey keyA = GlobalKey(); + final GlobalKey keyB = GlobalKey(); + + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ], + ), + ); + + FocusScope.of(keyA.currentContext).requestFocus(keyA.currentState.focusNode); + WidgetsBinding.instance.focusManager.rootScope.setFirstFocus(FocusScope.of(keyA.currentContext)); + + await tester.pumpAndSettle(); + + expect(keyA.currentState.focusNode.hasFocus, isTrue); + expect(find.text('A FOCUSED'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + + // This just swaps the FocusScopeNodes that the FocusScopes have in them. + await tester.pumpWidget( + Column( + children: [ + FocusScope( + node: parentFocusScope2, + child: Column( + children: [ + TestFocusable( + key: keyA, + name: 'a', + ), + ], + ), + ), + FocusScope( + node: parentFocusScope1, + child: Column( + children: [ + TestFocusable( + key: keyB, + name: 'b', + ), + ], + ), + ), + ], + ), + ); + + await tester.pump(); + + expect(keyA.currentState.focusNode.hasFocus, isFalse); + expect(find.text('a'), findsOneWidget); + expect(keyB.currentState.focusNode.hasFocus, isFalse); + expect(find.text('b'), findsOneWidget); + }); +} diff --git a/packages/flutter/test/widgets/focus_test.dart b/packages/flutter/test/widgets/focus_test.dart deleted file mode 100644 index c02633bb626..00000000000 --- a/packages/flutter/test/widgets/focus_test.dart +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/widgets.dart'; - -class TestFocusable extends StatefulWidget { - const TestFocusable({ - Key key, - this.no, - this.yes, - this.autofocus = true, - }) : super(key: key); - - final String no; - final String yes; - final bool autofocus; - - @override - TestFocusableState createState() => TestFocusableState(); -} - -class TestFocusableState extends State { - final FocusNode focusNode = FocusNode(); - bool _didAutofocus = false; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (!_didAutofocus && widget.autofocus) { - _didAutofocus = true; - FocusScope.of(context).autofocus(focusNode); - } - } - - @override - void dispose() { - focusNode.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { FocusScope.of(context).requestFocus(focusNode); }, - child: AnimatedBuilder( - animation: focusNode, - builder: (BuildContext context, Widget child) { - return Text(focusNode.hasFocus ? widget.yes : widget.no, textDirection: TextDirection.ltr); - }, - ), - ); - } -} - -void main() { - testWidgets('Can have multiple focused children and they update accordingly', (WidgetTester tester) async { - await tester.pumpWidget( - Column( - children: const [ - TestFocusable( - no: 'a', - yes: 'A FOCUSED', - ), - TestFocusable( - no: 'b', - yes: 'B FOCUSED', - ), - ], - ), - ); - - // Autofocus is delayed one frame. - await tester.pump(); - - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - expect(find.text('b'), findsOneWidget); - expect(find.text('B FOCUSED'), findsNothing); - await tester.tap(find.text('A FOCUSED')); - await tester.idle(); - await tester.pump(); - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - expect(find.text('b'), findsOneWidget); - expect(find.text('B FOCUSED'), findsNothing); - await tester.tap(find.text('A FOCUSED')); - await tester.idle(); - await tester.pump(); - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - expect(find.text('b'), findsOneWidget); - expect(find.text('B FOCUSED'), findsNothing); - await tester.tap(find.text('b')); - await tester.idle(); - await tester.pump(); - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - expect(find.text('b'), findsNothing); - expect(find.text('B FOCUSED'), findsOneWidget); - await tester.tap(find.text('a')); - await tester.idle(); - await tester.pump(); - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - expect(find.text('b'), findsOneWidget); - expect(find.text('B FOCUSED'), findsNothing); - }); - - testWidgets('Can blur', (WidgetTester tester) async { - await tester.pumpWidget( - const TestFocusable( - no: 'a', - yes: 'A FOCUSED', - autofocus: false, - ), - ); - - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - - final TestFocusableState state = tester.state(find.byType(TestFocusable)); - FocusScope.of(state.context).requestFocus(state.focusNode); - await tester.idle(); - await tester.pump(); - - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - - state.focusNode.unfocus(); - await tester.idle(); - await tester.pump(); - - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - }); - - testWidgets('Can move focus to scope', (WidgetTester tester) async { - final FocusScopeNode parentFocusScope = FocusScopeNode(); - final FocusScopeNode childFocusScope = FocusScopeNode(); - - await tester.pumpWidget( - FocusScope( - node: parentFocusScope, - autofocus: true, - child: Row( - textDirection: TextDirection.ltr, - children: const [ - TestFocusable( - no: 'a', - yes: 'A FOCUSED', - autofocus: false, - ), - ], - ), - ), - ); - - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - - final TestFocusableState state = tester.state(find.byType(TestFocusable)); - FocusScope.of(state.context).requestFocus(state.focusNode); - await tester.idle(); - await tester.pump(); - - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - - expect(parentFocusScope, hasAGoodToStringDeep); - expect( - parentFocusScope.toStringDeep(minLevel: DiagnosticLevel.info), - equalsIgnoringHashCodes( - 'FocusScopeNode#00000\n' - ' focus: FocusNode#00000(FOCUSED)\n' - ), - ); - - expect(WidgetsBinding.instance.focusManager.rootScope, hasAGoodToStringDeep); - expect( - WidgetsBinding.instance.focusManager.rootScope.toStringDeep(minLevel: DiagnosticLevel.info), - equalsIgnoringHashCodes( - 'FocusScopeNode#00000\n' - ' └─child 1: FocusScopeNode#00000\n' - ' focus: FocusNode#00000(FOCUSED)\n' - ), - ); - - parentFocusScope.setFirstFocus(childFocusScope); - await tester.idle(); - - await tester.pumpWidget( - FocusScope( - node: parentFocusScope, - child: Row( - textDirection: TextDirection.ltr, - children: [ - const TestFocusable( - no: 'a', - yes: 'A FOCUSED', - autofocus: false, - ), - FocusScope( - node: childFocusScope, - child: Container( - width: 50.0, - height: 50.0, - ), - ), - ], - ), - ), - ); - - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - - await tester.pumpWidget( - FocusScope( - node: parentFocusScope, - child: Row( - textDirection: TextDirection.ltr, - children: const [ - TestFocusable( - no: 'a', - yes: 'A FOCUSED', - autofocus: false, - ), - ], - ), - ), - ); - - // Focus has received the removal notification but we haven't rebuilt yet. - expect(find.text('a'), findsOneWidget); - expect(find.text('A FOCUSED'), findsNothing); - - await tester.pump(); - - expect(find.text('a'), findsNothing); - expect(find.text('A FOCUSED'), findsOneWidget); - - parentFocusScope.detach(); - }); -}