Ensures that hit testing only resturns focusable nodes. (#23931) (#24389)

Co-authored-by: Alexander Brusher <abrush21@users.noreply.github.com>
diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.cc b/shell/platform/fuchsia/flutter/accessibility_bridge.cc
index 9a0039f..0d801fb 100644
--- a/shell/platform/fuchsia/flutter/accessibility_bridge.cc
+++ b/shell/platform/fuchsia/flutter/accessibility_bridge.cc
@@ -314,6 +314,7 @@
     nodes_[flutter_node.id] = {
         .id = flutter_node.id,
         .flags = flutter_node.flags,
+        .is_focusable = IsFocusable(flutter_node),
         .rect = flutter_node.rect,
         .transform = flutter_node.transform,
         .children_in_hit_test_order = flutter_node.childrenInHitTestOrder,
@@ -412,6 +413,7 @@
   nodes_[root_flutter_semantics_node_.id] = {
       .id = root_flutter_semantics_node_.id,
       .flags = root_flutter_semantics_node_.flags,
+      .is_focusable = IsFocusable(root_flutter_semantics_node_),
       .rect = root_flutter_semantics_node_.rect,
       .transform = result,
       .children_in_hit_test_order =
@@ -570,7 +572,36 @@
       return candidate;
     }
   }
-  return node_id;
+
+  if (node.is_focusable) {
+    return node_id;
+  }
+
+  return {};
+}
+
+bool AccessibilityBridge::IsFocusable(
+    const flutter::SemanticsNode& node) const {
+  if (node.HasFlag(flutter::SemanticsFlags::kScopesRoute)) {
+    return false;
+  }
+
+  if (node.HasFlag(flutter::SemanticsFlags::kIsFocusable)) {
+    return true;
+  }
+
+  // Always consider platform views focusable.
+  if (node.IsPlatformViewNode()) {
+    return true;
+  }
+
+  // Always conider actionable nodes focusable.
+  if (node.actions != 0) {
+    return true;
+  }
+
+  // Consider text nodes focusable.
+  return !node.label.empty() || !node.value.empty() || !node.hint.empty();
 }
 
 // |fuchsia::accessibility::semantics::SemanticListener|
diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.h b/shell/platform/fuchsia/flutter/accessibility_bridge.h
index 4328c8b..8b0e939 100644
--- a/shell/platform/fuchsia/flutter/accessibility_bridge.h
+++ b/shell/platform/fuchsia/flutter/accessibility_bridge.h
@@ -110,6 +110,7 @@
   struct SemanticsNode {
     int32_t id;
     int32_t flags;
+    bool is_focusable;
     SkRect rect;
     SkRect screen_rect;
     SkM44 transform;
@@ -197,6 +198,9 @@
   // Assumes that SemanticsNode::screen_rect is up to date.
   std::optional<int32_t> GetHitNode(int32_t node_id, float x, float y);
 
+  // Returns whether the node is considered focusable.
+  bool IsFocusable(const flutter::SemanticsNode& node) const;
+
   // Converts a fuchsia::accessibility::semantics::Action to a
   // flutter::SemanticsAction.
   //
diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc
index c9de9f7..44d5174 100644
--- a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc
+++ b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc
@@ -726,23 +726,32 @@
   flutter::SemanticsNode node0;
   node0.id = 0;
   node0.rect.setLTRB(0, 0, 100, 100);
+  node0.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
 
   flutter::SemanticsNode node1;
   node1.id = 1;
   node1.rect.setLTRB(10, 10, 20, 20);
+  // Setting platform view id ensures this node is considered focusable.
+  node1.platformViewId = 1u;
 
   flutter::SemanticsNode node2;
   node2.id = 2;
   node2.rect.setLTRB(25, 10, 45, 20);
+  // Setting label ensures this node is considered focusable.
+  node2.label = "label";
 
   flutter::SemanticsNode node3;
   node3.id = 3;
   node3.rect.setLTRB(10, 25, 20, 45);
+  // Setting actions to a nonzero value ensures this node is considered
+  // focusable.
+  node3.actions = 1u;
 
   flutter::SemanticsNode node4;
   node4.id = 4;
   node4.rect.setLTRB(10, 10, 20, 20);
   node4.transform.setTranslate(20, 20, 0);
+  node4.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
 
   node0.childrenInTraversalOrder = {1, 2, 3, 4};
   node0.childrenInHitTestOrder = {1, 2, 3, 4};
@@ -782,20 +791,59 @@
   EXPECT_EQ(hit_node_id, 4u);
 }
 
-TEST_F(AccessibilityBridgeTest, HitTestOverlapping) {
-  // Tests that the first node in hit test order wins, even if a later node
-  // would be able to recieve the hit.
+TEST_F(AccessibilityBridgeTest, HitTestUnfocusableChild) {
   flutter::SemanticsNode node0;
   node0.id = 0;
   node0.rect.setLTRB(0, 0, 100, 100);
 
   flutter::SemanticsNode node1;
   node1.id = 1;
+  node1.rect.setLTRB(10, 10, 60, 60);
+
+  flutter::SemanticsNode node2;
+  node2.id = 2;
+  node2.rect.setLTRB(50, 50, 100, 100);
+  node2.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
+
+  node0.childrenInTraversalOrder = {1, 2};
+  node0.childrenInHitTestOrder = {1, 2};
+
+  accessibility_bridge_->AddSemanticsNodeUpdate(
+      {
+          {0, node0},
+          {1, node1},
+          {2, node2},
+      },
+      1.f);
+  RunLoopUntilIdle();
+
+  uint32_t hit_node_id;
+  auto callback = [&hit_node_id](fuchsia::accessibility::semantics::Hit hit) {
+    EXPECT_TRUE(hit.has_node_id());
+    hit_node_id = hit.node_id();
+  };
+
+  accessibility_bridge_->HitTest({55, 55}, callback);
+  EXPECT_EQ(hit_node_id, 2u);
+}
+
+TEST_F(AccessibilityBridgeTest, HitTestOverlapping) {
+  // Tests that the first node in hit test order wins, even if a later node
+  // would be able to recieve the hit.
+  flutter::SemanticsNode node0;
+  node0.id = 0;
+  node0.rect.setLTRB(0, 0, 100, 100);
+  node0.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
+
+  flutter::SemanticsNode node1;
+  node1.id = 1;
   node1.rect.setLTRB(0, 0, 100, 100);
+  node1.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
 
   flutter::SemanticsNode node2;
   node2.id = 2;
   node2.rect.setLTRB(25, 10, 45, 20);
+  node2.flags |= static_cast<int32_t>(flutter::SemanticsFlags::kIsFocusable);
 
   node0.childrenInTraversalOrder = {1, 2};
   node0.childrenInHitTestOrder = {2, 1};