diff --git a/packages/flutter/lib/src/widgets/editable_text.dart b/packages/flutter/lib/src/widgets/editable_text.dart index a93c46d16c1165613251d1d2e9db97ca316ef379..e63affede8ab55b81276f38c71fae84cec2a2cf9 100644 --- a/packages/flutter/lib/src/widgets/editable_text.dart +++ b/packages/flutter/lib/src/widgets/editable_text.dart @@ -873,10 +873,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien /// focus, the control will then attach to the keyboard and request that the /// keyboard become visible. void requestKeyboard() { - if (_hasFocus) + if (_hasFocus) { _openInputConnection(); - else + } else { + final List<FocusScopeNode> ancestorScopes = FocusScope.ancestorsOf(context); + for (int i = ancestorScopes.length - 1; i >= 1; i -= 1) + ancestorScopes[i].setFirstFocus(ancestorScopes[i - 1]); FocusScope.of(context).requestFocus(widget.focusNode); + } } void _hideSelectionOverlayIfNeeded() { diff --git a/packages/flutter/lib/src/widgets/focus_manager.dart b/packages/flutter/lib/src/widgets/focus_manager.dart index 3453c2641c824f6072ca5f7dcbcf835e339032d7..ccca09ca15b302f09566c86b266484e37dddc785 100644 --- a/packages/flutter/lib/src/widgets/focus_manager.dart +++ b/packages/flutter/lib/src/widgets/focus_manager.dart @@ -140,10 +140,23 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { FocusScopeNode _lastChild; FocusNode _focus; + List<FocusScopeNode> _focusPath; /// Whether this scope is currently active in its parent scope. bool get isFirstFocus => _parent == null || _parent._firstChild == this; + // Returns this FocusScopeNode's ancestors, starting with the node + // below the FocusManager's rootScope. + List<FocusScopeNode> _getFocusPath() { + final List<FocusScopeNode> nodes = <FocusScopeNode>[this]; + FocusScopeNode node = _parent; + while(node != null && node != _manager?.rootScope) { + nodes.add(node); + node = node._parent; + } + return nodes; + } + void _prepend(FocusScopeNode child) { assert(child != this); assert(child != _firstChild); @@ -246,7 +259,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { /// has received the overall focus in a microtask. void requestFocus(FocusNode node) { assert(node != null); - if (_focus == node) + if (_focus == node && listEquals<FocusScopeNode>(_focusPath, _manager?._getCurrentFocusPath())) return; _focus?.unfocus(); node._hasKeyboardToken = true; @@ -292,6 +305,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { _focus._parent = this; _focus._manager = _manager; _focus._hasKeyboardToken = true; + _focusPath = _getFocusPath(); _didChangeFocusChain(); } @@ -412,7 +426,7 @@ class FocusManager { /// The root [FocusScopeNode] in the focus tree. /// - /// This field is rarely used direction. Instead, to find the + /// This field is rarely used directly. Instead, to find the /// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of]. final FocusScopeNode rootScope = FocusScopeNode(); @@ -450,6 +464,8 @@ class FocusManager { _currentFocus?._notify(); } + List<FocusScopeNode> _getCurrentFocusPath() => _currentFocus?._parent?._getFocusPath(); + @override String toString() { final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; diff --git a/packages/flutter/lib/src/widgets/focus_scope.dart b/packages/flutter/lib/src/widgets/focus_scope.dart index 6d0ff2df44b9c6e1ac650134c428dcf579836edf..51724b94f624005e91f4a26161ef3fb77d072369 100644 --- a/packages/flutter/lib/src/widgets/focus_scope.dart +++ b/packages/flutter/lib/src/widgets/focus_scope.dart @@ -72,11 +72,39 @@ class FocusScope extends StatefulWidget { /// Returns the [node] of the [FocusScope] that most tightly encloses the /// given [BuildContext]. + /// + /// The [context] argument must not be null. static FocusScopeNode of(BuildContext context) { + assert(context != null); final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker); return scope?.node ?? context.owner.focusManager.rootScope; } + /// A list of the [FocusScopeNode]s for each [FocusScope] ancestor of + /// the given [BuildContext]. The first element of the list is the + /// nearest ancestor's [FocusScopeNode]. + /// + /// The returned list does not include the [FocusManager]'s `rootScope`. + /// Only the [FocusScopeNode]s that belong to [FocusScope] widgets are + /// returned. + /// + /// The [context] argument must not be null. + static List<FocusScopeNode> ancestorsOf(BuildContext context) { + assert(context != null); + final List<FocusScopeNode> ancestors = <FocusScopeNode>[]; + while (true) { + context = context.ancestorInheritedElementForWidgetOfExactType(_FocusScopeMarker); + if (context == null) + return ancestors; + final _FocusScopeMarker scope = context.widget; + ancestors.add(scope.node); + context.visitAncestorElements((Element parent) { + context = parent; + return false; + }); + } + } + @override _FocusScopeState createState() => _FocusScopeState(); } diff --git a/packages/flutter/test/material/text_field_focus_test.dart b/packages/flutter/test/material/text_field_focus_test.dart index a21594723c40c658568584803c1eade6064d9030..67039eaf693497ac5a9867d68e9a75c43cdf4a10 100644 --- a/packages/flutter/test/material/text_field_focus_test.dart +++ b/packages/flutter/test/material/text_field_focus_test.dart @@ -227,4 +227,145 @@ void main() { await tester.idle(); expect(tester.testTextInput.isVisible, isTrue); }); + + testWidgets('Sibling FocusScopes', (WidgetTester tester) async { + expect(tester.testTextInput.isVisible, isFalse); + + final FocusScopeNode focusScopeNode0 = FocusScopeNode(); + final FocusScopeNode focusScopeNode1 = FocusScopeNode(); + final Key textField0 = UniqueKey(); + final Key textField1 = UniqueKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + FocusScope( + node: focusScopeNode0, + child: Builder( + builder: (BuildContext context) => TextField(key: textField0) + ), + ), + FocusScope( + node: focusScopeNode1, + child: Builder( + builder: (BuildContext context) => TextField(key: textField1), + ), + ), + ], + ), + ), + ), + ), + ); + + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + tester.testTextInput.hide(); + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField1)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.tap(find.byKey(textField1)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + tester.testTextInput.hide(); + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.pumpWidget(Container()); + expect(tester.testTextInput.isVisible, isFalse); + }); + + testWidgets('Sibling Navigators', (WidgetTester tester) async { + expect(tester.testTextInput.isVisible, isFalse); + + final Key textField0 = UniqueKey(); + final Key textField1 = UniqueKey(); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: Column( + children: <Widget>[ + Expanded( + child: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return TextField(key: textField0); + }, + settings: settings, + ); + }, + ), + ), + Expanded( + child: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute<void>( + builder: (BuildContext context) { + return TextField(key: textField1); + }, + settings: settings, + ); + }, + ), + ), + ], + ), + ), + ), + ), + ); + + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + tester.testTextInput.hide(); + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField1)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.tap(find.byKey(textField1)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + tester.testTextInput.hide(); + expect(tester.testTextInput.isVisible, isFalse); + + await tester.tap(find.byKey(textField0)); + await tester.idle(); + expect(tester.testTextInput.isVisible, isTrue); + + await tester.pumpWidget(Container()); + expect(tester.testTextInput.isVisible, isFalse); + }); }