[flutter release cp] Reland "Fixes ability to call nextFocus() on a node to focus its desc… (#138014)
â¦â¦ (#136898)
â¦endant" (#136894)"
This reverts commit c2bd2c1175f5b81e9543955760aec5b876b4e57e.
fixes https://github.com/flutter/flutter/issues/134854
Context: https://github.com/flutter/flutter/issues/137071. Updated from [commit in chunhtai/flutter](https://github.com/chunhtai/flutter/commit/adacf2227ef59591b5d2dbdada621c8d5a7ee467)
diff --git a/packages/flutter/lib/src/widgets/focus_traversal.dart b/packages/flutter/lib/src/widgets/focus_traversal.dart
index 1212e27..3f4707c 100644
--- a/packages/flutter/lib/src/widgets/focus_traversal.dart
+++ b/packages/flutter/lib/src/widgets/focus_traversal.dart
@@ -241,7 +241,7 @@
final FocusScopeNode scope = currentNode.nearestScope!;
FocusNode? candidate = scope.focusedChild;
if (ignoreCurrentFocus || candidate == null && scope.descendants.isNotEmpty) {
- final Iterable<FocusNode> sorted = _sortAllDescendants(scope, currentNode);
+ final Iterable<FocusNode> sorted = _sortAllDescendants(scope, currentNode).where((FocusNode node) => _canRequestTraversalFocus(node));
if (sorted.isEmpty) {
candidate = null;
} else {
@@ -340,10 +340,25 @@
@protected
Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
- Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
+ static bool _canRequestTraversalFocus(FocusNode node) {
+ return node.canRequestFocus && !node.skipTraversal;
+ }
+
+ static Iterable<FocusNode> _getDescendantsWithoutExpandingScope(FocusNode node) {
+ final List<FocusNode> result = <FocusNode>[];
+ for (final FocusNode child in node.children) {
+ result.add(child);
+ if (child is! FocusScopeNode) {
+ result.addAll(_getDescendantsWithoutExpandingScope(child));
+ }
+ }
+ return result;
+ }
+
+ static Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
- for (final FocusNode node in scope.descendants) {
+ for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) {
final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
// Group nodes need to be added to their parent's node, or to the "null"
// node if no parent is found. This creates the hierarchy of group nodes
@@ -413,7 +428,7 @@
// They were left in above because they were needed to find their members
// during sorting.
sortedDescendants.removeWhere((FocusNode node) {
- return node != currentNode && (!node.canRequestFocus || node.skipTraversal);
+ return node != currentNode && !_canRequestTraversalFocus(node);
});
// Sanity check to make sure that the algorithm above doesn't diverge from
@@ -421,7 +436,7 @@
// finds.
assert((){
final Set<FocusNode> difference = sortedDescendants.toSet().difference(scope.traversalDescendants.toSet());
- if (currentNode.skipTraversal || !currentNode.canRequestFocus) {
+ if (!_canRequestTraversalFocus(currentNode)) {
// The scope.traversalDescendants will not contain currentNode if it
// skips traversal or not focusable.
assert(
diff --git a/packages/flutter/test/widgets/focus_traversal_test.dart b/packages/flutter/test/widgets/focus_traversal_test.dart
index 5b2abf4..bf447bb 100644
--- a/packages/flutter/test/widgets/focus_traversal_test.dart
+++ b/packages/flutter/test/widgets/focus_traversal_test.dart
@@ -96,6 +96,113 @@
expect(scope.hasFocus, isTrue);
});
+ testWidgetsWithLeakTracking('focus traversal should work case 1', (WidgetTester tester) async {
+ final FocusNode outer1 = FocusNode(debugLabel: 'outer1', skipTraversal: true);
+ final FocusNode outer2 = FocusNode(debugLabel: 'outer2', skipTraversal: true);
+ final FocusNode inner1 = FocusNode(debugLabel: 'inner1', );
+ final FocusNode inner2 = FocusNode(debugLabel: 'inner2', );
+ addTearDown(() {
+ outer1.dispose();
+ outer2.dispose();
+ inner1.dispose();
+ inner2.dispose();
+ });
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusTraversalGroup(
+ child: Row(
+ children: <Widget>[
+ FocusScope(
+ child: Focus(
+ focusNode: outer1,
+ child: Focus(
+ focusNode: inner1,
+ child: const SizedBox(width: 10, height: 10),
+ ),
+ ),
+ ),
+ FocusScope(
+ child: Focus(
+ focusNode: outer2,
+ // Add a padding to ensure both Focus widgets have different
+ // sizes.
+ child: Padding(
+ padding: const EdgeInsets.all(5),
+ child: Focus(
+ focusNode: inner2,
+ child: const SizedBox(width: 10, height: 10),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ expect(FocusManager.instance.primaryFocus, isNull);
+ inner1.requestFocus();
+ await tester.pump();
+ expect(FocusManager.instance.primaryFocus, inner1);
+ outer2.nextFocus();
+ await tester.pump();
+ expect(FocusManager.instance.primaryFocus, inner2);
+ });
+
+ testWidgetsWithLeakTracking('focus traversal should work case 2', (WidgetTester tester) async {
+ final FocusNode outer1 = FocusNode(debugLabel: 'outer1', skipTraversal: true);
+ final FocusNode outer2 = FocusNode(debugLabel: 'outer2', skipTraversal: true);
+ final FocusNode inner1 = FocusNode(debugLabel: 'inner1', );
+ final FocusNode inner2 = FocusNode(debugLabel: 'inner2', );
+ addTearDown(() {
+ outer1.dispose();
+ outer2.dispose();
+ inner1.dispose();
+ inner2.dispose();
+ });
+
+ await tester.pumpWidget(
+ Directionality(
+ textDirection: TextDirection.ltr,
+ child: FocusTraversalGroup(
+ child: Row(
+ children: <Widget>[
+ FocusScope(
+ child: Focus(
+ focusNode: outer1,
+ child: Focus(
+ focusNode: inner1,
+ child: const SizedBox(width: 10, height: 10),
+ ),
+ ),
+ ),
+ FocusScope(
+ child: Focus(
+ focusNode: outer2,
+ child: Focus(
+ focusNode: inner2,
+ child: const SizedBox(width: 10, height: 10),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+
+ expect(FocusManager.instance.primaryFocus, isNull);
+ inner1.requestFocus();
+ await tester.pump();
+ expect(FocusManager.instance.primaryFocus, inner1);
+ outer2.nextFocus();
+ await tester.pump();
+ expect(FocusManager.instance.primaryFocus, inner2);
+ });
+
testWidgetsWithLeakTracking('Move focus to next node.', (WidgetTester tester) async {
final GlobalKey key1 = GlobalKey(debugLabel: '1');
final GlobalKey key2 = GlobalKey(debugLabel: '2');
@@ -626,8 +733,13 @@
final bool didFindNode = node1.nextFocus();
await tester.pump();
expect(didFindNode, isTrue);
- expect(node1.hasPrimaryFocus, isFalse);
- expect(node2.hasPrimaryFocus, isTrue);
+ if (canRequestFocus) {
+ expect(node1.hasPrimaryFocus, isTrue);
+ expect(node2.hasPrimaryFocus, isFalse);
+ } else {
+ expect(node1.hasPrimaryFocus, isFalse);
+ expect(node2.hasPrimaryFocus, isTrue);
+ }
}
});