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.
This commit is contained in:
parent
92125ed38f
commit
05dc9444b9
@ -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);
|
||||
});
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user