diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart index 7fb6c72eac0..f288fd5e866 100644 --- a/packages/flutter/lib/src/widgets/focus_traversal.dart +++ b/packages/flutter/lib/src/widgets/focus_traversal.dart @@ -311,6 +311,15 @@ mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { bool _popPolicyDataIfNeeded(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) { final _DirectionalPolicyData policyData = _policyData[nearestScope]; if (policyData != null && policyData.history.isNotEmpty && policyData.history.first.direction != direction) { + if (policyData.history.last.node.parent == null) { + // If a node has been removed from the tree, then we should stop + // referencing it and reset the scope data so that we don't try and + // request focus on it. This can happen in slivers where the rendered node + // has been unmounted. This has the side effect that hysteresis might not + // be avoided when items that go off screen get unmounted. + invalidateScopeData(nearestScope); + return false; + } switch (direction) { case TraversalDirection.down: case TraversalDirection.up: diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart index 693ffb1e688..deddeaac6a5 100644 --- a/packages/flutter/test/widgets/focus_traversal_test.dart +++ b/packages/flutter/test/widgets/focus_traversal_test.dart @@ -799,5 +799,65 @@ void main() { expect(policy.findFirstFocusInDirection(scope, TraversalDirection.left), equals(upperRightNode)); expect(policy.findFirstFocusInDirection(scope, TraversalDirection.right), equals(upperLeftNode)); }); + testWidgets('Can find focus when policy data dirty', (WidgetTester tester) async { + final FocusNode focusTop = FocusNode(debugLabel: 'top'); + final FocusNode focusCenter = FocusNode(debugLabel: 'center'); + final FocusNode focusBottom = FocusNode(debugLabel: 'bottom'); + + final FocusTraversalPolicy policy = ReadingOrderTraversalPolicy(); + await tester.pumpWidget(DefaultFocusTraversal( + policy: policy, + child: FocusScope( + debugLabel: 'Scope', + child: Column( + children: [ + Focus( + focusNode: focusTop, + child: Container(width: 100, height: 100)), + Focus( + focusNode: focusCenter, + child: Container(width: 100, height: 100)), + Focus( + focusNode: focusBottom, + child: Container(width: 100, height: 100)), + ], + ), + ), + )); + + focusTop.requestFocus(); + final FocusNode scope = focusTop.enclosingScope; + + scope.focusInDirection(TraversalDirection.down); + scope.focusInDirection(TraversalDirection.down); + + await tester.pump(); + expect(focusBottom.hasFocus, isTrue); + + // Remove center focus node. + await tester.pumpWidget(DefaultFocusTraversal( + policy: policy, + child: FocusScope( + debugLabel: 'Scope', + child: Column( + children: [ + Focus( + focusNode: focusTop, + child: Container(width: 100, height: 100)), + Focus( + focusNode: focusBottom, + child: Container(width: 100, height: 100)), + ], + ), + ), + )); + + expect(focusBottom.hasFocus, isTrue); + scope.focusInDirection(TraversalDirection.up); + await tester.pump(); + + expect(focusCenter.hasFocus, isFalse); + expect(focusTop.hasFocus, isTrue); + }); }); }