diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.cc b/shell/platform/fuchsia/flutter/accessibility_bridge.cc index 1a8088e6a3574..80f84612342de 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.cc @@ -314,6 +314,7 @@ void AccessibilityBridge::AddSemanticsNodeUpdate( 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 @@ fuchsia::accessibility::semantics::Node AccessibilityBridge::GetRootNodeUpdate( 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 = @@ -561,7 +563,36 @@ std::optional AccessibilityBridge::GetHitNode(int32_t node_id, 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 5ba1d981b0aaa..4f3b540956eb8 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.h +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.h @@ -107,6 +107,7 @@ class AccessibilityBridge struct SemanticsNode { int32_t id; int32_t flags; + bool is_focusable; SkRect rect; SkRect screen_rect; SkM44 transform; @@ -194,6 +195,9 @@ class AccessibilityBridge // Assumes that SemanticsNode::screen_rect is up to date. std::optional 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 00303c5f6ccd2..fcd2c7012009f 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc @@ -716,23 +716,32 @@ TEST_F(AccessibilityBridgeTest, HitTest) { flutter::SemanticsNode node0; node0.id = 0; node0.rect.setLTRB(0, 0, 100, 100); + node0.flags |= static_cast(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(flutter::SemanticsFlags::kIsFocusable); node0.childrenInTraversalOrder = {1, 2, 3, 4}; node0.childrenInHitTestOrder = {1, 2, 3, 4}; @@ -772,20 +781,59 @@ TEST_F(AccessibilityBridgeTest, HitTest) { EXPECT_EQ(hit_node_id, 4u); } +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(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(flutter::SemanticsFlags::kIsFocusable); flutter::SemanticsNode node1; node1.id = 1; node1.rect.setLTRB(0, 0, 100, 100); + node1.flags |= static_cast(flutter::SemanticsFlags::kIsFocusable); flutter::SemanticsNode node2; node2.id = 2; node2.rect.setLTRB(25, 10, 45, 20); + node2.flags |= static_cast(flutter::SemanticsFlags::kIsFocusable); node0.childrenInTraversalOrder = {1, 2}; node0.childrenInHitTestOrder = {2, 1};