Unverified Commit 05dc9444 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Updated focus handling for nested FocusScopes (#27365)

Updated focus handling in FocusManager et al and EditableText so that TextFields within nested FocusScopes can gain the focus and show the keybaord.
parent 92125ed3
...@@ -873,10 +873,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -873,10 +873,14 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// focus, the control will then attach to the keyboard and request that the /// focus, the control will then attach to the keyboard and request that the
/// keyboard become visible. /// keyboard become visible.
void requestKeyboard() { void requestKeyboard() {
if (_hasFocus) if (_hasFocus) {
_openInputConnection(); _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); FocusScope.of(context).requestFocus(widget.focusNode);
}
} }
void _hideSelectionOverlayIfNeeded() { void _hideSelectionOverlayIfNeeded() {
......
...@@ -140,10 +140,23 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -140,10 +140,23 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
FocusScopeNode _lastChild; FocusScopeNode _lastChild;
FocusNode _focus; FocusNode _focus;
List<FocusScopeNode> _focusPath;
/// Whether this scope is currently active in its parent scope. /// Whether this scope is currently active in its parent scope.
bool get isFirstFocus => _parent == null || _parent._firstChild == this; 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) { void _prepend(FocusScopeNode child) {
assert(child != this); assert(child != this);
assert(child != _firstChild); assert(child != _firstChild);
...@@ -246,7 +259,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -246,7 +259,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
/// has received the overall focus in a microtask. /// has received the overall focus in a microtask.
void requestFocus(FocusNode node) { void requestFocus(FocusNode node) {
assert(node != null); assert(node != null);
if (_focus == node) if (_focus == node && listEquals<FocusScopeNode>(_focusPath, _manager?._getCurrentFocusPath()))
return; return;
_focus?.unfocus(); _focus?.unfocus();
node._hasKeyboardToken = true; node._hasKeyboardToken = true;
...@@ -292,6 +305,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin { ...@@ -292,6 +305,7 @@ class FocusScopeNode extends Object with DiagnosticableTreeMixin {
_focus._parent = this; _focus._parent = this;
_focus._manager = _manager; _focus._manager = _manager;
_focus._hasKeyboardToken = true; _focus._hasKeyboardToken = true;
_focusPath = _getFocusPath();
_didChangeFocusChain(); _didChangeFocusChain();
} }
...@@ -412,7 +426,7 @@ class FocusManager { ...@@ -412,7 +426,7 @@ class FocusManager {
/// The root [FocusScopeNode] in the focus tree. /// 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]. /// [FocusScopeNode] for a given [BuildContext], use [FocusScope.of].
final FocusScopeNode rootScope = FocusScopeNode(); final FocusScopeNode rootScope = FocusScopeNode();
...@@ -450,6 +464,8 @@ class FocusManager { ...@@ -450,6 +464,8 @@ class FocusManager {
_currentFocus?._notify(); _currentFocus?._notify();
} }
List<FocusScopeNode> _getCurrentFocusPath() => _currentFocus?._parent?._getFocusPath();
@override @override
String toString() { String toString() {
final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : ''; final String status = _haveScheduledUpdate ? ' UPDATE SCHEDULED' : '';
......
...@@ -72,11 +72,39 @@ class FocusScope extends StatefulWidget { ...@@ -72,11 +72,39 @@ class FocusScope extends StatefulWidget {
/// Returns the [node] of the [FocusScope] that most tightly encloses the /// Returns the [node] of the [FocusScope] that most tightly encloses the
/// given [BuildContext]. /// given [BuildContext].
///
/// The [context] argument must not be null.
static FocusScopeNode of(BuildContext context) { static FocusScopeNode of(BuildContext context) {
assert(context != null);
final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker); final _FocusScopeMarker scope = context.inheritFromWidgetOfExactType(_FocusScopeMarker);
return scope?.node ?? context.owner.focusManager.rootScope; 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 @override
_FocusScopeState createState() => _FocusScopeState(); _FocusScopeState createState() => _FocusScopeState();
} }
......
...@@ -227,4 +227,145 @@ void main() { ...@@ -227,4 +227,145 @@ void main() {
await tester.idle(); await tester.idle();
expect(tester.testTextInput.isVisible, isTrue); 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);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment