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};