From 58a62076559585b167c89531dfb81092d744a552 Mon Sep 17 00:00:00 2001 From: Alexander Brusher Date: Tue, 1 Sep 2020 13:33:02 -0700 Subject: [PATCH] Adds fuchsia node roles to accessibility bridge updates. (#20385) --- lib/ui/semantics.dart | 14 +++- lib/ui/semantics/semantics_node.h | 1 + lib/web_ui/lib/src/ui/semantics.dart | 3 + .../fuchsia/flutter/accessibility_bridge.cc | 26 +++++++ .../fuchsia/flutter/accessibility_bridge.h | 5 ++ .../flutter/accessibility_bridge_unittest.cc | 75 +++++++++++++++++++ testing/dart/semantics_test.dart | 2 +- 7 files changed, 123 insertions(+), 3 deletions(-) diff --git a/lib/ui/semantics.dart b/lib/ui/semantics.dart index 7097735f292b2..56e371eb770b6 100644 --- a/lib/ui/semantics.dart +++ b/lib/ui/semantics.dart @@ -300,8 +300,12 @@ class SemanticsFlag { static const int _kIsReadOnlyIndex = 1 << 20; static const int _kIsFocusableIndex = 1 << 21; static const int _kIsLinkIndex = 1 << 22; + static const int _kIsSliderIndex = 1 << 23; // READ THIS: if you add a flag here, you MUST update the numSemanticsFlags - // value in testing/dart/semantics_test.dart, or tests will fail. + // value in testing/dart/semantics_test.dart, or tests will fail. Also, + // please update the Flag enum in + // flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java, + // and the SemanticsFlag class in lib/web_ui/lib/src/ui/semantics.dart. const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison @@ -355,6 +359,9 @@ class SemanticsFlag { /// affordances. static const SemanticsFlag isTextField = SemanticsFlag._(_kIsTextFieldIndex); + /// Whether the semantic node represents a slider. + static const SemanticsFlag isSlider = SemanticsFlag._(_kIsSliderIndex); + /// Whether the semantic node is read only. /// /// Only applicable when [isTextField] is true. @@ -551,7 +558,8 @@ class SemanticsFlag { _kIsReadOnlyIndex: isReadOnly, _kIsFocusableIndex: isFocusable, _kIsLinkIndex: isLink, - }; + _kIsSliderIndex: isSlider, +}; @override String toString() { @@ -602,6 +610,8 @@ class SemanticsFlag { return 'SemanticsFlag.isFocusable'; case _kIsLinkIndex: return 'SemanticsFlag.isLink'; + case _kIsSliderIndex: + return 'SemanticsFlag.isSlider'; } assert(false, 'Unhandled index: $index'); return ''; diff --git a/lib/ui/semantics/semantics_node.h b/lib/ui/semantics/semantics_node.h index cc98c8656a867..6f44dae113f87 100644 --- a/lib/ui/semantics/semantics_node.h +++ b/lib/ui/semantics/semantics_node.h @@ -79,6 +79,7 @@ enum class SemanticsFlags : int32_t { kIsReadOnly = 1 << 20, kIsFocusable = 1 << 21, kIsLink = 1 << 22, + kIsSlider = 1 << 23, }; const int kScrollableSemanticsFlags = diff --git a/lib/web_ui/lib/src/ui/semantics.dart b/lib/web_ui/lib/src/ui/semantics.dart index 24b0acc4f318c..00d9d03a68879 100644 --- a/lib/web_ui/lib/src/ui/semantics.dart +++ b/lib/web_ui/lib/src/ui/semantics.dart @@ -156,6 +156,7 @@ class SemanticsFlag { static const int _kIsReadOnlyIndex = 1 << 20; static const int _kIsFocusableIndex = 1 << 21; static const int _kIsLinkIndex = 1 << 22; + static const int _kIsSliderIndex = 1 << 23; const SemanticsFlag._(this.index) : assert(index != null); // ignore: unnecessary_null_comparison final int index; @@ -182,12 +183,14 @@ class SemanticsFlag { static const SemanticsFlag isToggled = SemanticsFlag._(_kIsToggledIndex); static const SemanticsFlag hasImplicitScrolling = SemanticsFlag._(_kHasImplicitScrollingIndex); static const SemanticsFlag isMultiline = SemanticsFlag._(_kIsMultilineIndex); + static const SemanticsFlag isSlider = SemanticsFlag._(_kIsSliderIndex); static const Map values = { _kHasCheckedStateIndex: hasCheckedState, _kIsCheckedIndex: isChecked, _kIsSelectedIndex: isSelected, _kIsButtonIndex: isButton, _kIsLinkIndex: isLink, + _kIsSliderIndex: isSlider, _kIsTextFieldIndex: isTextField, _kIsFocusableIndex: isFocusable, _kIsFocusedIndex: isFocused, diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.cc b/shell/platform/fuchsia/flutter/accessibility_bridge.cc index ea3b646c53c9f..fdad8cd587034 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.cc @@ -115,6 +115,31 @@ fuchsia::accessibility::semantics::States AccessibilityBridge::GetNodeStates( return states; } +fuchsia::accessibility::semantics::Role AccessibilityBridge::GetNodeRole( + const flutter::SemanticsNode& node) const { + if (node.HasFlag(flutter::SemanticsFlags::kIsButton)) { + return fuchsia::accessibility::semantics::Role::BUTTON; + } + + if (node.HasFlag(flutter::SemanticsFlags::kIsHeader)) { + return fuchsia::accessibility::semantics::Role::HEADER; + } + + if (node.HasFlag(flutter::SemanticsFlags::kIsImage)) { + return fuchsia::accessibility::semantics::Role::IMAGE; + } + + if (node.HasFlag(flutter::SemanticsFlags::kIsTextField)) { + return fuchsia::accessibility::semantics::Role::TEXT_FIELD; + } + + if (node.HasFlag(flutter::SemanticsFlags::kIsSlider)) { + return fuchsia::accessibility::semantics::Role::SLIDER; + } + + return fuchsia::accessibility::semantics::Role::UNKNOWN; +} + std::unordered_set AccessibilityBridge::GetDescendants( int32_t node_id) const { std::unordered_set descendents; @@ -227,6 +252,7 @@ void AccessibilityBridge::AddSemanticsNodeUpdate( .set_transform(GetNodeTransform(flutter_node)) .set_attributes(GetNodeAttributes(flutter_node, &this_node_size)) .set_states(GetNodeStates(flutter_node, &this_node_size)) + .set_role(GetNodeRole(flutter_node)) .set_child_ids(child_ids); this_node_size += kNodeIdSize * flutter_node.childrenInTraversalOrder.size(); diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge.h b/shell/platform/fuchsia/flutter/accessibility_bridge.h index 6888f13327527..ea6261c265700 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge.h +++ b/shell/platform/fuchsia/flutter/accessibility_bridge.h @@ -145,6 +145,11 @@ class AccessibilityBridge const flutter::SemanticsNode& node, size_t* additional_size) const; + // Derives the role for a Fuchsia semantics node from a Flutter semantics + // node. + fuchsia::accessibility::semantics::Role GetNodeRole( + const flutter::SemanticsNode& node) const; + // Gets the set of reachable descendants from the given node id. std::unordered_set GetDescendants(int32_t node_id) const; diff --git a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc index e9b466fb8e1c2..c6ccf1cfc9672 100644 --- a/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc +++ b/shell/platform/fuchsia/flutter/accessibility_bridge_unittest.cc @@ -19,6 +19,20 @@ namespace flutter_runner_test { +namespace { + +void ExpectNodeHasRole( + const fuchsia::accessibility::semantics::Node& node, + const std::unordered_map + roles_by_node_id) { + ASSERT_TRUE(node.has_node_id()); + ASSERT_NE(roles_by_node_id.find(node.node_id()), roles_by_node_id.end()); + EXPECT_TRUE(node.has_role()); + EXPECT_EQ(node.role(), roles_by_node_id.at(node.node_id())); +} + +} // namespace + class AccessibilityBridgeTestDelegate : public flutter_runner::AccessibilityBridge::Delegate { public: @@ -89,6 +103,67 @@ TEST_F(AccessibilityBridgeTest, EnableDisable) { EXPECT_TRUE(accessibility_delegate_.enabled()); } +TEST_F(AccessibilityBridgeTest, UpdatesNodeRoles) { + flutter::SemanticsNodeUpdates updates; + + flutter::SemanticsNode node0; + node0.id = 0; + node0.flags |= static_cast(flutter::SemanticsFlags::kIsButton); + node0.childrenInTraversalOrder = {1, 2, 3, 4}; + node0.childrenInHitTestOrder = {1, 2, 3, 4}; + updates.emplace(0, node0); + + flutter::SemanticsNode node1; + node1.id = 1; + node1.flags |= static_cast(flutter::SemanticsFlags::kIsHeader); + node1.childrenInTraversalOrder = {}; + node1.childrenInHitTestOrder = {}; + updates.emplace(1, node1); + + flutter::SemanticsNode node2; + node2.id = 2; + node2.flags |= static_cast(flutter::SemanticsFlags::kIsImage); + node2.childrenInTraversalOrder = {}; + node2.childrenInHitTestOrder = {}; + updates.emplace(2, node2); + + flutter::SemanticsNode node3; + node3.id = 3; + node3.flags |= static_cast(flutter::SemanticsFlags::kIsTextField); + node3.childrenInTraversalOrder = {}; + node3.childrenInHitTestOrder = {}; + updates.emplace(3, node3); + + flutter::SemanticsNode node4; + node4.childrenInTraversalOrder = {}; + node4.childrenInHitTestOrder = {}; + node4.id = 4; + node4.flags |= static_cast(flutter::SemanticsFlags::kIsSlider); + updates.emplace(4, node4); + + accessibility_bridge_->AddSemanticsNodeUpdate(std::move(updates)); + RunLoopUntilIdle(); + + std::unordered_map + roles_by_node_id = { + {0u, fuchsia::accessibility::semantics::Role::BUTTON}, + {1u, fuchsia::accessibility::semantics::Role::HEADER}, + {2u, fuchsia::accessibility::semantics::Role::IMAGE}, + {3u, fuchsia::accessibility::semantics::Role::TEXT_FIELD}, + {4u, fuchsia::accessibility::semantics::Role::SLIDER}}; + + EXPECT_EQ(0, semantics_manager_.DeleteCount()); + EXPECT_EQ(1, semantics_manager_.UpdateCount()); + EXPECT_EQ(1, semantics_manager_.CommitCount()); + EXPECT_EQ(5U, semantics_manager_.LastUpdatedNodes().size()); + for (const auto& node : semantics_manager_.LastUpdatedNodes()) { + ExpectNodeHasRole(node, roles_by_node_id); + } + + EXPECT_FALSE(semantics_manager_.DeleteOverflowed()); + EXPECT_FALSE(semantics_manager_.UpdateOverflowed()); +} + TEST_F(AccessibilityBridgeTest, DeletesChildrenTransitively) { // Test that when a node is deleted, so are its transitive children. flutter::SemanticsNode node2; diff --git a/testing/dart/semantics_test.dart b/testing/dart/semantics_test.dart index 4257b479b17ec..fe497c63ed4f0 100644 --- a/testing/dart/semantics_test.dart +++ b/testing/dart/semantics_test.dart @@ -9,7 +9,7 @@ import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; /// Verifies Semantics flags and actions. void main() { // This must match the number of flags in lib/ui/semantics.dart - const int numSemanticsFlags = 23; + const int numSemanticsFlags = 24; test('SemanticsFlag.values refers to all flags.', () async { expect(SemanticsFlag.values.length, equals(numSemanticsFlags)); for (int index = 0; index < numSemanticsFlags; ++index) {