Unverified Commit 1a7bb1f5 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Added proper focus handling when pushing and popping routes (#40166)

The proposed change will change focus handling when pushing and popping routes so that the FocusScopeNode for the route receives focus when pushed, and that the FocusScopeNode in the navigator receives focus when the route is popped.

This means that the last setFirstFocus call on the scope is used to determine which control actually receives focus. When the focus scope receives focus, it traverses its children, trying to find a non-scope node that is the "first focus" of itself or a child node.

This is a breaking change, because the focus behavior has changed. If you push a route after this change, and had a 'first focus' set on a widget via FocusScopeNode.setFirstFocus, it won't currently receive focus immediately, but after this change it will. Similarly, if you pop a route after this change, the focus will go back to where it was before the route was pushed, which is correct, but different from what happens now.
parent c9383913
...@@ -81,9 +81,12 @@ class FocusAttachment { ...@@ -81,9 +81,12 @@ class FocusAttachment {
assert(_node != null); assert(_node != null);
assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}'])); assert(_focusDebug('Detaching node:', <String>[_node.toString(), 'With enclosing scope ${_node.enclosingScope}']));
if (isAttached) { if (isAttached) {
if (_node.hasPrimaryFocus) { if (_node.hasPrimaryFocus || (_node._manager != null && _node._manager._nextFocus == _node)) {
_node.unfocus(); _node.unfocus(focusPrevious: true);
} }
assert(_node._manager?._nextFocus != _node);
assert(!_node.hasPrimaryFocus);
_node._manager?._dirtyNodes?.remove(_node);
_node._parent?._removeChild(_node); _node._parent?._removeChild(_node);
_node._attachment = null; _node._attachment = null;
} }
...@@ -418,14 +421,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -418,14 +421,15 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// its descendants. /// its descendants.
/// - [FocusTraversalPolicy], a class that can be extended to describe a /// - [FocusTraversalPolicy], a class that can be extended to describe a
/// traversal policy. /// traversal policy.
bool get canRequestFocus => _canRequestFocus; bool get canRequestFocus => _canRequestFocus && (enclosingScope == null || enclosingScope.canRequestFocus);
bool _canRequestFocus; bool _canRequestFocus;
@mustCallSuper
set canRequestFocus(bool value) { set canRequestFocus(bool value) {
if (value != _canRequestFocus) { if (value != _canRequestFocus) {
_canRequestFocus = value; if (!value) {
if (!_canRequestFocus) { unfocus(focusPrevious: true);
unfocus();
} }
_canRequestFocus = value;
_manager?._dirtyNodes?.add(this); _manager?._dirtyNodes?.add(this);
_manager?._markNeedsUpdate(); _manager?._markNeedsUpdate();
} }
...@@ -463,8 +467,11 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -463,8 +467,11 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// An iterator over the children that are allowed to be traversed by the /// An iterator over the children that are allowed to be traversed by the
/// [FocusTraversalPolicy]. /// [FocusTraversalPolicy].
Iterable<FocusNode> get traversalChildren { Iterable<FocusNode> get traversalChildren {
if (!canRequestFocus) {
return const <FocusNode>[];
}
return children.where( return children.where(
(FocusNode node) => !node.skipTraversal && node.canRequestFocus, (FocusNode node) => !node.skipTraversal && node._canRequestFocus,
); );
} }
...@@ -625,18 +632,28 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -625,18 +632,28 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
/// ///
/// Has no effect on nodes that return true from [hasFocus], but false from /// Has no effect on nodes that return true from [hasFocus], but false from
/// [hasPrimaryFocus]. /// [hasPrimaryFocus].
void unfocus() { ///
if (hasPrimaryFocus) { /// if [focusPrevious] is true, then rather than losing all focus, the focus
final FocusScopeNode scope = enclosingScope; /// will be moved to the node that the [enclosingScope] thinks should have it,
assert(scope != null, 'Node has primary focus, but no enclosingScope.'); /// based on its history of nodes that were set as first focus on it using
scope._focusedChildren.remove(this); /// [FocusScopeNode.setFirstFocus].
_manager?._willUnfocusNode(this); void unfocus({ bool focusPrevious = false }) {
assert(focusPrevious != null);
if (!hasFocus && (_manager != null && _manager._nextFocus != this)) {
return; return;
} }
if (hasFocus) { if (!hasPrimaryFocus) {
// If we are in the focus chain, but not the primary focus, then unfocus // If we are in the focus chain, but not the primary focus, then unfocus
// the primary instead. // the primary instead.
_manager.primaryFocus.unfocus(); _manager?.primaryFocus?.unfocus(focusPrevious: focusPrevious);
}
_manager?._willUnfocusNode(this);
final FocusScopeNode scope = enclosingScope;
if (scope != null) {
scope._focusedChildren.remove(this);
if (focusPrevious) {
scope._doRequestFocus();
}
} }
} }
...@@ -667,8 +684,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -667,8 +684,8 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
void _markAsDirty({FocusNode newFocus}) { void _markAsDirty({FocusNode newFocus}) {
if (_manager != null) { if (_manager != null) {
// If we have a manager, then let it handle the focus change. // If we have a manager, then let it handle the focus change.
_manager._dirtyNodes?.add(this);
_manager._markNeedsUpdate(newFocus: newFocus); _manager._markNeedsUpdate(newFocus: newFocus);
_manager._dirtyNodes?.add(this);
} else { } else {
// If we don't have a manager, then change the focus locally. // If we don't have a manager, then change the focus locally.
newFocus?._setAsFocusedChild(); newFocus?._setAsFocusedChild();
...@@ -748,7 +765,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier { ...@@ -748,7 +765,7 @@ class FocusNode with DiagnosticableTreeMixin, ChangeNotifier {
@override @override
void dispose() { void dispose() {
_manager?._willDisposeFocusNode(this); // Detaching will also unfocus and clean up the manager's data structures.
_attachment?.detach(); _attachment?.detach();
super.dispose(); super.dispose();
} }
...@@ -966,10 +983,6 @@ class FocusScopeNode extends FocusNode { ...@@ -966,10 +983,6 @@ class FocusScopeNode extends FocusNode {
@override @override
void _doRequestFocus() { void _doRequestFocus() {
if (!canRequestFocus) {
return;
}
// Start with the primary focus as the focused child of this scope, if there // Start with the primary focus as the focused child of this scope, if there
// is one. Otherwise start with this node itself. // is one. Otherwise start with this node itself.
FocusNode primaryFocus = focusedChild ?? this; FocusNode primaryFocus = focusedChild ?? this;
...@@ -980,22 +993,31 @@ class FocusScopeNode extends FocusNode { ...@@ -980,22 +993,31 @@ class FocusScopeNode extends FocusNode {
final FocusScopeNode scope = primaryFocus; final FocusScopeNode scope = primaryFocus;
primaryFocus = scope.focusedChild; primaryFocus = scope.focusedChild;
} }
if (primaryFocus is FocusScopeNode) { if (identical(primaryFocus, this)) {
// We didn't find a FocusNode at the leaf, so we're focusing the scope. // We didn't find a FocusNode at the leaf, so we're focusing the scope, if
_setAsFocusedChild(); // allowed.
_markAsDirty(newFocus: primaryFocus); if (primaryFocus.canRequestFocus) {
_setAsFocusedChild();
_markAsDirty(newFocus: this);
}
} else { } else {
// We found a FocusScope at the leaf, so ask it to focus itself instead of // We found a FocusScope at the leaf, so ask it to focus itself instead of
// this scope. That will cause this scope to return true from hasFocus, // this scope. That will cause this scope to return true from hasFocus,
// but false from hasPrimaryFocus. // but false from hasPrimaryFocus.
primaryFocus.requestFocus(); primaryFocus._doRequestFocus();
} }
} }
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<FocusNode>('focusedChild', focusedChild, defaultValue: null)); if (_focusedChildren.isEmpty) {
return;
}
final List<String> childList = _focusedChildren.reversed.map<String>((FocusNode child) {
return '${describeIdentity(child)}${child.debugLabel != null && child.debugLabel.isNotEmpty ? '(${child.debugLabel})' : ''}';
}).toList();
properties.add(IterableProperty<String>('focusedChildren', childList, defaultValue: <String>[]));
} }
} }
...@@ -1225,6 +1247,7 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1225,6 +1247,7 @@ class FocusManager with DiagnosticableTreeMixin {
_updateHighlightMode(); _updateHighlightMode();
} }
assert(_focusDebug('Received key event ${event.logicalKey}'));
// Walk the current focus from the leaf to the root, calling each one's // Walk the current focus from the leaf to the root, calling each one's
// onKey on the way up, and if one responds that they handled it, stop. // onKey on the way up, and if one responds that they handled it, stop.
if (_primaryFocus == null) { if (_primaryFocus == null) {
...@@ -1255,26 +1278,18 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1255,26 +1278,18 @@ class FocusManager with DiagnosticableTreeMixin {
// update. // update.
final Set<FocusNode> _dirtyNodes = <FocusNode>{}; final Set<FocusNode> _dirtyNodes = <FocusNode>{};
// Called to indicate that the given node is being disposed.
void _willDisposeFocusNode(FocusNode node) {
assert(node != null);
assert(_focusDebug('Disposing of node:', <String>[node.toString(), 'with enclosing scope ${node.enclosingScope}']));
_willUnfocusNode(node);
_dirtyNodes.remove(node);
}
// Called to indicate that the given node is being unfocused, and that any // Called to indicate that the given node is being unfocused, and that any
// pending request to be focused should be canceled. // pending request to be focused should be canceled.
void _willUnfocusNode(FocusNode node) { void _willUnfocusNode(FocusNode node) {
assert(node != null); assert(node != null);
assert(_focusDebug('Unfocusing node $node')); assert(_focusDebug('Unfocusing node $node'));
if (_primaryFocus == node) { if (_primaryFocus == node || _nextFocus == node) {
_primaryFocus = null; if (_primaryFocus == node) {
_dirtyNodes.add(node); _primaryFocus = null;
_markNeedsUpdate(); }
} if (_nextFocus == node) {
if (_nextFocus == node) { _nextFocus = null;
_nextFocus = null; }
_dirtyNodes.add(node); _dirtyNodes.add(node);
_markNeedsUpdate(); _markNeedsUpdate();
} }
...@@ -1350,6 +1365,7 @@ class FocusManager with DiagnosticableTreeMixin { ...@@ -1350,6 +1365,7 @@ class FocusManager with DiagnosticableTreeMixin {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED')); properties.add(FlagProperty('haveScheduledUpdate', value: _haveScheduledUpdate, ifTrue: 'UPDATE SCHEDULED'));
properties.add(DiagnosticsProperty<FocusNode>('primaryFocus', primaryFocus, defaultValue: null)); properties.add(DiagnosticsProperty<FocusNode>('primaryFocus', primaryFocus, defaultValue: null));
properties.add(DiagnosticsProperty<FocusNode>('nextFocus', _nextFocus, defaultValue: null));
final Element element = primaryFocus?.context; final Element element = primaryFocus?.context;
if (element != null) { if (element != null) {
properties.add(DiagnosticsProperty<String>('primaryFocusCreator', element.debugGetCreatorChain(20))); properties.add(DiagnosticsProperty<String>('primaryFocusCreator', element.debugGetCreatorChain(20)));
......
...@@ -339,9 +339,9 @@ class _FocusState extends State<Focus> { ...@@ -339,9 +339,9 @@ class _FocusState extends State<Focus> {
// _createNode is overridden in _FocusScopeState. // _createNode is overridden in _FocusScopeState.
_internalNode ??= _createNode(); _internalNode ??= _createNode();
} }
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal; focusNode.skipTraversal = widget.skipTraversal ?? focusNode.skipTraversal;
focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus; focusNode.canRequestFocus = widget.canRequestFocus ?? focusNode.canRequestFocus;
_focusAttachment = focusNode.attach(context, onKey: widget.onKey);
_hasFocus = focusNode.hasFocus; _hasFocus = focusNode.hasFocus;
// Add listener even if the _internalNode existed before, since it should // Add listener even if the _internalNode existed before, since it should
......
...@@ -133,7 +133,12 @@ abstract class Route<T> { ...@@ -133,7 +133,12 @@ abstract class Route<T> {
/// The [didChangeNext] and [didChangePrevious] methods are typically called /// The [didChangeNext] and [didChangePrevious] methods are typically called
/// immediately after this method is called. /// immediately after this method is called.
@protected @protected
TickerFuture didPush() => TickerFuture.complete(); @mustCallSuper
TickerFuture didPush() {
return TickerFuture.complete()..then<void>((void _) {
navigator.focusScopeNode.requestFocus();
});
}
/// Called after [install] when the route replaced another in the navigator. /// Called after [install] when the route replaced another in the navigator.
/// ///
......
...@@ -185,6 +185,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -185,6 +185,7 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().'); assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.'); assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
_animation.addStatusListener(_handleStatusChanged); _animation.addStatusListener(_handleStatusChanged);
super.didPush();
return _controller.forward(); return _controller.forward();
} }
...@@ -629,6 +630,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -629,6 +630,9 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
// This should be called to wrap any changes to route.isCurrent, route.canPop, // This should be called to wrap any changes to route.isCurrent, route.canPop,
// and route.offstage. // and route.offstage.
void _routeSetState(VoidCallback fn) { void _routeSetState(VoidCallback fn) {
if (widget.route.isCurrent) {
widget.route.navigator.focusScopeNode.setFirstFocus(focusScopeNode);
}
setState(fn); setState(fn);
} }
...@@ -654,13 +658,16 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -654,13 +658,16 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
widget.route.secondaryAnimation, widget.route.secondaryAnimation,
// This additional AnimatedBuilder is include because if the // This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's // value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget. // only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder( AnimatedBuilder(
animation: widget.route.navigator.userGestureInProgressNotifier, animation: widget.route.navigator.userGestureInProgressNotifier,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
final bool ignoreEvents = widget.route.animation?.status == AnimationStatus.reverse ||
widget.route.navigator.userGestureInProgress;
focusScopeNode.canRequestFocus = !ignoreEvents;
return IgnorePointer( return IgnorePointer(
ignoring: widget.route.navigator.userGestureInProgress ignoring: ignoreEvents,
|| widget.route.animation?.status == AnimationStatus.reverse,
child: child, child: child,
); );
}, },
......
...@@ -342,7 +342,7 @@ void main() { ...@@ -342,7 +342,7 @@ void main() {
expect(helloPosition3.dy, helloPosition4.dy); expect(helloPosition3.dy, helloPosition4.dy);
await gesture.moveBy(const Offset(500.0, 0.0)); await gesture.moveBy(const Offset(500.0, 0.0));
await gesture.up(); await gesture.up();
expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 2); expect(await tester.pumpAndSettle(const Duration(minutes: 1)), 3);
expect(find.text('PUSH'), findsOneWidget); expect(find.text('PUSH'), findsOneWidget);
expect(find.text('HELLO'), findsNothing); expect(find.text('HELLO'), findsNothing);
}); });
......
...@@ -25,6 +25,7 @@ void main() { ...@@ -25,6 +25,7 @@ void main() {
); );
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
expect(focusNode.hasPrimaryFocus, isTrue);
final BuildContext context = tester.element(find.byType(TextField)); final BuildContext context = tester.element(find.byType(TextField));
...@@ -40,11 +41,7 @@ void main() { ...@@ -40,11 +41,7 @@ void main() {
Navigator.of(tester.element(find.text('Dialog'))).pop(); Navigator.of(tester.element(find.text('Dialog'))).pop();
await tester.pump(); await tester.pump();
expect(tester.testTextInput.isVisible, isFalse); expect(focusNode.hasPrimaryFocus, isTrue);
await tester.tap(find.byType(TextField));
await tester.idle();
expect(tester.testTextInput.isVisible, isTrue); expect(tester.testTextInput.isVisible, isTrue);
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
......
...@@ -561,7 +561,7 @@ void main() { ...@@ -561,7 +561,7 @@ void main() {
' └─rootScope: FocusScopeNode#00000\n' ' └─rootScope: FocusScopeNode#00000\n'
' │ FOCUSED\n' ' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n' ' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChild: FocusScopeNode#00000\n' ' │ focusedChildren: FocusScopeNode#00000\n'
' │\n' ' │\n'
' ├─Child 1: FocusScopeNode#00000\n' ' ├─Child 1: FocusScopeNode#00000\n'
' │ │ context: Container-[GlobalKey#00000]\n' ' │ │ context: Container-[GlobalKey#00000]\n'
...@@ -581,7 +581,7 @@ void main() { ...@@ -581,7 +581,7 @@ void main() {
' └─Child 2: FocusScopeNode#00000\n' ' └─Child 2: FocusScopeNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n' ' │ context: Container-[GlobalKey#00000]\n'
' │ FOCUSED\n' ' │ FOCUSED\n'
' │ focusedChild: FocusNode#00000\n' ' │ focusedChildren: FocusNode#00000(Child 4)\n'
' │\n' ' │\n'
' └─Child 1: FocusNode#00000\n' ' └─Child 1: FocusNode#00000\n'
' │ context: Container-[GlobalKey#00000]\n' ' │ context: Container-[GlobalKey#00000]\n'
......
...@@ -226,7 +226,7 @@ void main() { ...@@ -226,7 +226,7 @@ void main() {
' │ context: FocusScope\n' ' │ context: FocusScope\n'
' │ FOCUSED\n' ' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n' ' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChild: FocusNode#00000\n' ' │ focusedChildren: FocusNode#00000(Child)\n'
' │\n' ' │\n'
' └─Child 1: FocusNode#00000\n' ' └─Child 1: FocusNode#00000\n'
' context: Focus\n' ' context: Focus\n'
...@@ -240,13 +240,13 @@ void main() { ...@@ -240,13 +240,13 @@ void main() {
equalsIgnoringHashCodes('FocusScopeNode#00000\n' equalsIgnoringHashCodes('FocusScopeNode#00000\n'
' │ FOCUSED\n' ' │ FOCUSED\n'
' │ debugLabel: "Root Focus Scope"\n' ' │ debugLabel: "Root Focus Scope"\n'
' │ focusedChild: FocusScopeNode#00000\n' ' │ focusedChildren: FocusScopeNode#00000(Parent Scope Node)\n'
' │\n' ' │\n'
' └─Child 1: FocusScopeNode#00000\n' ' └─Child 1: FocusScopeNode#00000\n'
' │ context: FocusScope\n' ' │ context: FocusScope\n'
' │ FOCUSED\n' ' │ FOCUSED\n'
' │ debugLabel: "Parent Scope Node"\n' ' │ debugLabel: "Parent Scope Node"\n'
' │ focusedChild: FocusNode#00000\n' ' │ focusedChildren: FocusNode#00000(Child)\n'
' │\n' ' │\n'
' └─Child 1: FocusNode#00000\n' ' └─Child 1: FocusNode#00000\n'
' context: Focus\n' ' context: Focus\n'
...@@ -733,8 +733,8 @@ void main() { ...@@ -733,8 +733,8 @@ void main() {
await tester.pump(); await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('b'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async { testWidgets("Removing unpinned focused scope doesn't move focus to focused widget within next FocusScope", (WidgetTester tester) async {
...@@ -814,8 +814,8 @@ void main() { ...@@ -814,8 +814,8 @@ void main() {
); );
await tester.pump(); await tester.pump();
expect(keyB.currentState.focusNode.hasFocus, isFalse); expect(keyB.currentState.focusNode.hasFocus, isTrue);
expect(find.text('b'), findsOneWidget); expect(find.text('B FOCUSED'), findsOneWidget);
}); });
testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async { testWidgets('Moving widget from one scope to another retains focus', (WidgetTester tester) async {
......
...@@ -235,6 +235,71 @@ void main() { ...@@ -235,6 +235,71 @@ void main() {
expect(secondFocusNode.hasFocus, isFalse); expect(secondFocusNode.hasFocus, isFalse);
expect(scope.hasFocus, isTrue); expect(scope.hasFocus, isTrue);
}); });
testWidgets('Find the initial focus when a route is pushed or popped.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
final FocusNode testNode1 = FocusNode(debugLabel: 'First Focus Node');
final FocusNode testNode2 = FocusNode(debugLabel: 'Second Focus Node');
await tester.pumpWidget(
MaterialApp(
home: DefaultFocusTraversal(
policy: WidgetOrderFocusTraversalPolicy(),
child: Center(
child: Builder(builder: (BuildContext context) {
return MaterialButton(
key: key1,
focusNode: testNode1,
autofocus: true,
onPressed: () {
Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (BuildContext context) {
return Center(
child: MaterialButton(
key: key2,
focusNode: testNode2,
autofocus: true,
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Go Back'),
),
);
},
),
);
},
child: const Text('Go Forward'),
);
}),
),
),
),
);
final Element firstChild = tester.element(find.text('Go Forward'));
final FocusNode firstFocusNode = Focus.of(firstChild);
final FocusNode scope = Focus.of(firstChild).enclosingScope;
await tester.pump();
expect(firstFocusNode.hasFocus, isTrue);
expect(scope.hasFocus, isTrue);
await tester.tap(find.text('Go Forward'));
await tester.pumpAndSettle();
final Element secondChild = tester.element(find.text('Go Back'));
final FocusNode secondFocusNode = Focus.of(secondChild);
expect(firstFocusNode.hasFocus, isFalse);
expect(secondFocusNode.hasFocus, isTrue);
await tester.tap(find.text('Go Back'));
await tester.pumpAndSettle();
expect(firstFocusNode.hasFocus, isTrue);
expect(scope.hasFocus, isTrue);
});
}); });
group(ReadingOrderTraversalPolicy, () { group(ReadingOrderTraversalPolicy, () {
testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async { testWidgets('Find the initial focus if there is none yet.', (WidgetTester tester) async {
......
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