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
/// 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() {
......
......@@ -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' : '';
......
......@@ -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();
}
......
......@@ -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);
});
}
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