[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);
+        }
       }
     });