From b2cc97013d50ad02a7e6a4e7dc2fc603807214a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BC=AF=E8=A8=80?= Date: Thu, 18 Jul 2019 01:00:06 +0800 Subject: [PATCH] Fix FocusTraversalPolicy makes focus lost (#34153) (#34712) FocusTraversalPolicy keep the previously visited node to avoid hysteresis. But even if the visited focus has been disposed, FocusTraversalPolicy will still use it to requestFocus, which will cause FocusManger to get an abandoned node to get the focus. --- .../lib/src/widgets/focus_traversal.dart | 9 +++ .../test/widgets/focus_traversal_test.dart | 60 +++++++++++++++++++ 2 files changed, 69 insertions(+) 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); + }); }); }