Commit 3231ad9c authored by Sharon Yang's avatar Sharon Yang Committed by Commit Bot

[fuchsia] Support scrolling to show offscreen a11y nodes onscreen

Support scrolling an offscreen node to be visible for screen reader use.
Clean up unused param in DeleteSubtree.

Test: AccessibilityBridgeTest.PerformScrollToMakeVisible
Bug: fuchsia:55864
Change-Id: I3ab468e1cfb3084ee7517f6cf45e8cc0e3e8da1c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2142740
Commit-Queue: Sharon Yang <yangsharon@chromium.org>
Reviewed-by: default avatarDavid Dorwin <ddorwin@chromium.org>
Cr-Commit-Position: refs/heads/master@{#797083}
parent 750481ae
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include "base/logging.h" #include "base/logging.h"
#include "fuchsia/engine/browser/ax_tree_converter.h" #include "fuchsia/engine/browser/ax_tree_converter.h"
#include "ui/accessibility/ax_action_data.h" #include "ui/accessibility/ax_action_data.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace { namespace {
...@@ -20,6 +21,9 @@ constexpr uint32_t kSemanticNodeRootId = 0; ...@@ -20,6 +21,9 @@ constexpr uint32_t kSemanticNodeRootId = 0;
// maximum sizes of serialized Semantic Nodes. // maximum sizes of serialized Semantic Nodes.
constexpr size_t kMaxNodesPerUpdate = 16; constexpr size_t kMaxNodesPerUpdate = 16;
// Error allowed for each edge when converting from gfx::RectF to gfx::Rect.
constexpr float kRectConversionError = 0.5;
} // namespace } // namespace
AccessibilityBridge::AccessibilityBridge( AccessibilityBridge::AccessibilityBridge(
...@@ -32,11 +36,11 @@ AccessibilityBridge::AccessibilityBridge( ...@@ -32,11 +36,11 @@ AccessibilityBridge::AccessibilityBridge(
on_error_callback_(std::move(on_error_callback)) { on_error_callback_(std::move(on_error_callback)) {
DCHECK(web_contents_); DCHECK(web_contents_);
Observe(web_contents_); Observe(web_contents_);
tree_.AddObserver(this); ax_tree_.AddObserver(this);
semantics_manager->RegisterViewForSemantics( semantics_manager->RegisterViewForSemantics(
std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest()); std::move(view_ref), binding_.NewBinding(), semantic_tree_.NewRequest());
tree_ptr_.set_error_handler([this](zx_status_t status) mutable { semantic_tree_.set_error_handler([this](zx_status_t status) {
ZX_LOG(ERROR, status) << "SemanticTree disconnected"; ZX_LOG(ERROR, status) << "SemanticTree disconnected";
std::move(on_error_callback_).Run(ZX_ERR_INTERNAL); std::move(on_error_callback_).Run(ZX_ERR_INTERNAL);
}); });
...@@ -70,7 +74,7 @@ void AccessibilityBridge::TryCommit() { ...@@ -70,7 +74,7 @@ void AccessibilityBridge::TryCommit() {
} }
DispatchSemanticsMessages(range_start, to_send_.size() - range_start); DispatchSemanticsMessages(range_start, to_send_.size() - range_start);
tree_ptr_->CommitUpdates( semantic_tree_->CommitUpdates(
fit::bind_member(this, &AccessibilityBridge::OnCommitComplete)); fit::bind_member(this, &AccessibilityBridge::OnCommitComplete));
commit_inflight_ = true; commit_inflight_ = true;
to_send_.clear(); to_send_.clear();
...@@ -83,14 +87,14 @@ void AccessibilityBridge::DispatchSemanticsMessages(size_t start, size_t size) { ...@@ -83,14 +87,14 @@ void AccessibilityBridge::DispatchSemanticsMessages(size_t start, size_t size) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::UPDATE); DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::UPDATE);
updates.push_back(std::move(to_send_.at(i).update_node)); updates.push_back(std::move(to_send_.at(i).update_node));
} }
tree_ptr_->UpdateSemanticNodes(std::move(updates)); semantic_tree_->UpdateSemanticNodes(std::move(updates));
} else if (to_send_.at(start).type == SemanticUpdateOrDelete::Type::DELETE) { } else if (to_send_.at(start).type == SemanticUpdateOrDelete::Type::DELETE) {
std::vector<uint32_t> deletes; std::vector<uint32_t> deletes;
for (size_t i = start; i < start + size; i++) { for (size_t i = start; i < start + size; i++) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::DELETE); DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::DELETE);
deletes.push_back(to_send_.at(i).id_to_delete); deletes.push_back(to_send_.at(i).id_to_delete);
} }
tree_ptr_->DeleteSemanticNodes(deletes); semantic_tree_->DeleteSemanticNodes(deletes);
} }
} }
...@@ -126,9 +130,8 @@ void AccessibilityBridge::AccessibilityEventReceived( ...@@ -126,9 +130,8 @@ void AccessibilityBridge::AccessibilityEventReceived(
const content::AXEventNotificationDetails& details) { const content::AXEventNotificationDetails& details) {
// Updates to AXTree must be applied first. // Updates to AXTree must be applied first.
for (const ui::AXTreeUpdate& update : details.updates) { for (const ui::AXTreeUpdate& update : details.updates) {
if (!tree_.Unserialize(update)) { if (!ax_tree_.Unserialize(update)) {
// If this fails, it is a fatal error that will cause the current Frame to // If this fails, it is a fatal error that will cause an early exit.
// be destroyed.
std::move(on_error_callback_).Run(ZX_ERR_INTERNAL); std::move(on_error_callback_).Run(ZX_ERR_INTERNAL);
return; return;
} }
...@@ -136,8 +139,8 @@ void AccessibilityBridge::AccessibilityEventReceived( ...@@ -136,8 +139,8 @@ void AccessibilityBridge::AccessibilityEventReceived(
// Events to fire after tree has been updated. // Events to fire after tree has been updated.
for (const ui::AXEvent& event : details.events) { for (const ui::AXEvent& event : details.events) {
if (event.event_type == ax::mojom::Event::kHitTestResult) { if (event.event_type == ax::mojom::Event::kHitTestResult &&
if (pending_hit_test_callbacks_.find(event.action_request_id) != pending_hit_test_callbacks_.find(event.action_request_id) !=
pending_hit_test_callbacks_.end()) { pending_hit_test_callbacks_.end()) {
fuchsia::accessibility::semantics::Hit hit; fuchsia::accessibility::semantics::Hit hit;
hit.set_node_id(ConvertToFuchsiaNodeId(event.id)); hit.set_node_id(ConvertToFuchsiaNodeId(event.id));
...@@ -145,7 +148,8 @@ void AccessibilityBridge::AccessibilityEventReceived( ...@@ -145,7 +148,8 @@ void AccessibilityBridge::AccessibilityEventReceived(
// Run the pending callback with the hit. // Run the pending callback with the hit.
pending_hit_test_callbacks_[event.action_request_id](std::move(hit)); pending_hit_test_callbacks_[event.action_request_id](std::move(hit));
pending_hit_test_callbacks_.erase(event.action_request_id); pending_hit_test_callbacks_.erase(event.action_request_id);
} } else if (event_received_callback_for_test_) {
std::move(event_received_callback_for_test_).Run();
} }
} }
} }
...@@ -163,6 +167,22 @@ void AccessibilityBridge::OnAccessibilityActionRequested( ...@@ -163,6 +167,22 @@ void AccessibilityBridge::OnAccessibilityActionRequested(
action_data.target_node_id = node_id; action_data.target_node_id = node_id;
if (action == fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN) {
ui::AXNode* node = ax_tree_.GetFromId(node_id);
if (!node) {
callback(false);
return;
}
action_data.target_rect = gfx::ToEnclosedRectIgnoringError(
node->data().relative_bounds.bounds, kRectConversionError);
action_data.horizontal_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action_data.vertical_scroll_alignment =
ax::mojom::ScrollAlignment::kScrollAlignmentCenter;
action_data.scroll_behavior = ax::mojom::ScrollBehavior::kScrollIfVisible;
}
web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data); web_contents_->GetMainFrame()->AccessibilityPerformAction(action_data);
callback(true); callback(true);
} }
...@@ -199,8 +219,7 @@ void AccessibilityBridge::OnSemanticsModeChanged( ...@@ -199,8 +219,7 @@ void AccessibilityBridge::OnSemanticsModeChanged(
callback(); callback();
} }
void AccessibilityBridge::DeleteSubtree(ui::AXTree* tree, ui::AXNode* node) { void AccessibilityBridge::DeleteSubtree(ui::AXNode* node) {
DCHECK(tree);
DCHECK(node); DCHECK(node);
// When navigating, page 1, including the root, is deleted after page 2 has // When navigating, page 1, including the root, is deleted after page 2 has
...@@ -212,24 +231,25 @@ void AccessibilityBridge::DeleteSubtree(ui::AXTree* tree, ui::AXNode* node) { ...@@ -212,24 +231,25 @@ void AccessibilityBridge::DeleteSubtree(ui::AXTree* tree, ui::AXNode* node) {
ConvertToFuchsiaNodeId(node->id()))); ConvertToFuchsiaNodeId(node->id())));
} }
for (ui::AXNode* child : node->children()) for (ui::AXNode* child : node->children())
DeleteSubtree(tree, child); DeleteSubtree(child);
} }
void AccessibilityBridge::OnNodeWillBeDeleted(ui::AXTree* tree, void AccessibilityBridge::OnNodeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) { ui::AXNode* node) {
DeleteSubtree(tree, node); DeleteSubtree(node);
} }
void AccessibilityBridge::OnSubtreeWillBeDeleted(ui::AXTree* tree, void AccessibilityBridge::OnSubtreeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) { ui::AXNode* node) {
DeleteSubtree(tree, node); DeleteSubtree(node);
} }
void AccessibilityBridge::OnAtomicUpdateFinished( void AccessibilityBridge::OnAtomicUpdateFinished(
ui::AXTree* tree, ui::AXTree* tree,
bool root_changed, bool root_changed,
const std::vector<ui::AXTreeObserver::Change>& changes) { const std::vector<ui::AXTreeObserver::Change>& changes) {
root_id_ = tree_.root()->id(); DCHECK_EQ(tree, &ax_tree_);
root_id_ = ax_tree_.root()->id();
for (const ui::AXTreeObserver::Change& change : changes) { for (const ui::AXTreeObserver::Change& change : changes) {
ui::AXNodeData ax_data; ui::AXNodeData ax_data;
switch (change.type) { switch (change.type) {
...@@ -246,7 +266,7 @@ void AccessibilityBridge::OnAtomicUpdateFinished( ...@@ -246,7 +266,7 @@ void AccessibilityBridge::OnAtomicUpdateFinished(
break; break;
case ui::AXTreeObserver::NODE_REPARENTED: case ui::AXTreeObserver::NODE_REPARENTED:
case ui::AXTreeObserver::SUBTREE_REPARENTED: case ui::AXTreeObserver::SUBTREE_REPARENTED:
DeleteSubtree(tree, change.node); DeleteSubtree(change.node);
break; break;
} }
} }
......
...@@ -31,7 +31,7 @@ class WebContents; ...@@ -31,7 +31,7 @@ class WebContents;
// The lifetime of an instance of AccessibilityBridge is the same as that of a // The lifetime of an instance of AccessibilityBridge is the same as that of a
// View created by FrameImpl. This class refers to the View via the // View created by FrameImpl. This class refers to the View via the
// caller-supplied ViewRef. // caller-supplied ViewRef.
// If |tree_ptr_| gets disconnected, it will cause the FrameImpl that owns // If |semantic_tree_| gets disconnected, it will cause the FrameImpl that owns
// |this| to close, which will also destroy |this|. // |this| to close, which will also destroy |this|.
class WEB_ENGINE_EXPORT AccessibilityBridge class WEB_ENGINE_EXPORT AccessibilityBridge
: public content::WebContentsObserver, : public content::WebContentsObserver,
...@@ -49,11 +49,17 @@ class WEB_ENGINE_EXPORT AccessibilityBridge ...@@ -49,11 +49,17 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
AccessibilityBridge(const AccessibilityBridge&) = delete; AccessibilityBridge(const AccessibilityBridge&) = delete;
AccessibilityBridge& operator=(const AccessibilityBridge&) = delete; AccessibilityBridge& operator=(const AccessibilityBridge&) = delete;
const ui::AXSerializableTree* ax_tree_for_test() { return &ax_tree_; }
void set_event_received_callback_for_test(base::OnceClosure callback) {
event_received_callback_for_test_ = std::move(callback);
}
private: private:
FRIEND_TEST_ALL_PREFIXES(AccessibilityBridgeTest, OnSemanticsModeChanged); FRIEND_TEST_ALL_PREFIXES(AccessibilityBridgeTest, OnSemanticsModeChanged);
// A struct used for caching semantic information. This allows for updates and // A struct used for caching semantic information. This allows for updates
// deletes to be stored in the same vector to preserve all ordering // and deletes to be stored in the same vector to preserve all ordering
// information. // information.
struct SemanticUpdateOrDelete { struct SemanticUpdateOrDelete {
enum Type { UPDATE, DELETE }; enum Type { UPDATE, DELETE };
...@@ -79,14 +85,14 @@ class WEB_ENGINE_EXPORT AccessibilityBridge ...@@ -79,14 +85,14 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
// Callback for SemanticTree::CommitUpdates. // Callback for SemanticTree::CommitUpdates.
void OnCommitComplete(); void OnCommitComplete();
// Converts AXNode ids to Semantic Node ids, and handles special casing of the // Converts AXNode ids to Semantic Node ids, and handles special casing of
// root. // the root.
uint32_t ConvertToFuchsiaNodeId(int32_t ax_node_id); uint32_t ConvertToFuchsiaNodeId(int32_t ax_node_id);
// Deletes all nodes in subtree rooted at and including |node|, unless |node| // Deletes all nodes in subtree rooted at and including |node|, unless
// is the root of the tree. // |node| is the root of the tree. |tree| and |node| are owned by the
// |tree| and |node| are owned by the accessibility bridge. // accessibility bridge.
void DeleteSubtree(ui::AXTree* tree, ui::AXNode* node); void DeleteSubtree(ui::AXNode* node);
// content::WebContentsObserver implementation. // content::WebContentsObserver implementation.
void AccessibilityEventReceived( void AccessibilityEventReceived(
...@@ -110,25 +116,27 @@ class WEB_ENGINE_EXPORT AccessibilityBridge ...@@ -110,25 +116,27 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
bool root_changed, bool root_changed,
const std::vector<ui::AXTreeObserver::Change>& changes) override; const std::vector<ui::AXTreeObserver::Change>& changes) override;
fuchsia::accessibility::semantics::SemanticTreePtr tree_ptr_; fuchsia::accessibility::semantics::SemanticTreePtr semantic_tree_;
fidl::Binding<fuchsia::accessibility::semantics::SemanticListener> binding_; fidl::Binding<fuchsia::accessibility::semantics::SemanticListener> binding_;
content::WebContents* web_contents_; content::WebContents* web_contents_;
ui::AXSerializableTree tree_; ui::AXSerializableTree ax_tree_;
// Cache for pending data to be sent to the Semantic Tree between commits. // Cache for pending data to be sent to the Semantic Tree between commits.
std::vector<SemanticUpdateOrDelete> to_send_; std::vector<SemanticUpdateOrDelete> to_send_;
bool commit_inflight_ = false; bool commit_inflight_ = false;
// Maintain a map of callbacks as multiple hit test events can happen at once. // Maintain a map of callbacks as multiple hit test events can happen at
// These are keyed by the request_id field of ui::AXActionData. // once. These are keyed by the request_id field of ui::AXActionData.
base::flat_map<int, HitTestCallback> pending_hit_test_callbacks_; base::flat_map<int, HitTestCallback> pending_hit_test_callbacks_;
// Run in the case of an internal error that cannot be recovered from. This // Run in the case of an internal error that cannot be recovered from. This
// will cause the frame |this| is owned by to be torn down. // will cause the frame |this| is owned by to be torn down.
base::OnceCallback<void(zx_status_t)> on_error_callback_; base::OnceCallback<void(zx_status_t)> on_error_callback_;
// The root id of |tree_|. // The root id of |ax_tree_|.
int32_t root_id_ = 0; int32_t root_id_ = 0;
base::OnceClosure event_received_callback_for_test_;
}; };
#endif // FUCHSIA_ENGINE_BROWSER_ACCESSIBILITY_BRIDGE_H_ #endif // FUCHSIA_ENGINE_BROWSER_ACCESSIBILITY_BRIDGE_H_
...@@ -30,10 +30,10 @@ const char kButtonName2[] = "another button"; ...@@ -30,10 +30,10 @@ const char kButtonName2[] = "another button";
const char kButtonName3[] = "button 3"; const char kButtonName3[] = "button 3";
const char kNodeName[] = "last node"; const char kNodeName[] = "last node";
const char kParagraphName[] = "a third paragraph"; const char kParagraphName[] = "a third paragraph";
const char kOffscreenNodeName[] = "offscreen node";
const size_t kPage1NodeCount = 9; const size_t kPage1NodeCount = 9;
const size_t kPage2NodeCount = 190; const size_t kPage2NodeCount = 190;
fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) { fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) {
fuchsia::math::PointF center; fuchsia::math::PointF center;
center.x = (box.min.x + box.max.x) / 2; center.x = (box.min.x + box.max.x) / 2;
...@@ -270,3 +270,47 @@ IN_PROC_BROWSER_TEST_F(AccessibilityBridgeTest, Disconnect) { ...@@ -270,3 +270,47 @@ IN_PROC_BROWSER_TEST_F(AccessibilityBridgeTest, Disconnect) {
semantics_manager_.semantic_tree()->Disconnect(); semantics_manager_.semantic_tree()->Disconnect();
run_loop.Run(); run_loop.Run();
} }
IN_PROC_BROWSER_TEST_F(AccessibilityBridgeTest, PerformScrollToMakeVisible) {
constexpr int kScreenWidth = 720;
constexpr int kScreenHeight = 640;
gfx::Rect screen_bounds(kScreenWidth, kScreenHeight);
GURL page_url(embedded_test_server()->GetURL(kPage1Path));
ASSERT_TRUE(cr_fuchsia::LoadUrlAndExpectResponse(
navigation_controller_.get(), fuchsia::web::LoadUrlParams(),
page_url.spec()));
navigation_listener_.RunUntilUrlAndTitleEquals(page_url, kPage1Title);
semantics_manager_.semantic_tree()->RunUntilNodeCountAtLeast(kPage1NodeCount);
auto* content_view =
frame_impl_->web_contents_for_test()->GetContentNativeView();
content_view->SetBounds(screen_bounds);
// Get a node that is off the screen.
fuchsia::accessibility::semantics::Node* node =
semantics_manager_.semantic_tree()->GetNodeFromLabel(kOffscreenNodeName);
ASSERT_TRUE(node);
AccessibilityBridge* bridge = frame_impl_->accessibility_bridge_for_test();
ui::AXNode* ax_node = bridge->ax_tree_for_test()->GetFromId(node->node_id());
ASSERT_TRUE(ax_node);
bool is_offscreen = false;
bridge->ax_tree_for_test()->GetTreeBounds(ax_node, &is_offscreen);
EXPECT_TRUE(is_offscreen);
// Perform SHOW_ON_SCREEN on that node and check that it is on the screen.
base::RunLoop run_loop;
bridge->set_event_received_callback_for_test(run_loop.QuitClosure());
semantics_manager_.RequestAccessibilityAction(
node->node_id(),
fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN);
semantics_manager_.RunUntilNumActionsHandledEquals(1);
run_loop.Run();
// Initialize |is_offscreen| to false before calling GetTreeBounds as
// specified by the API.
is_offscreen = false;
bridge->ax_tree_for_test()->GetTreeBounds(ax_node, &is_offscreen);
EXPECT_FALSE(is_offscreen);
}
...@@ -178,10 +178,12 @@ bool ConvertAction(fuchsia::accessibility::semantics::Action fuchsia_action, ...@@ -178,10 +178,12 @@ bool ConvertAction(fuchsia::accessibility::semantics::Action fuchsia_action,
case fuchsia::accessibility::semantics::Action::DEFAULT: case fuchsia::accessibility::semantics::Action::DEFAULT:
*mojom_action = ax::mojom::Action::kDoDefault; *mojom_action = ax::mojom::Action::kDoDefault;
return true; return true;
case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
*mojom_action = ax::mojom::Action::kScrollToMakeVisible;
return true;
case fuchsia::accessibility::semantics::Action::SECONDARY: case fuchsia::accessibility::semantics::Action::SECONDARY:
case fuchsia::accessibility::semantics::Action::SET_FOCUS: case fuchsia::accessibility::semantics::Action::SET_FOCUS:
case fuchsia::accessibility::semantics::Action::SET_VALUE: case fuchsia::accessibility::semantics::Action::SET_VALUE:
case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
return false; return false;
default: default:
LOG(WARNING) LOG(WARNING)
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
WEB_ENGINE_EXPORT fuchsia::accessibility::semantics::Node WEB_ENGINE_EXPORT fuchsia::accessibility::semantics::Node
AXNodeDataToSemanticNode(const ui::AXNodeData& node); AXNodeDataToSemanticNode(const ui::AXNodeData& node);
// Converts Fuchsia action of type |fuchsia_action| to an ax::mojom action of // Converts Fuchsia action of type |fuchsia_action| to an ax::mojom::Action of
// type |mojom_action|. Function will return true if |fuchsia_action| is // type |mojom_action|. Function will return true if |fuchsia_action| is
// supported in Chromium. // supported in Chromium.
bool ConvertAction(fuchsia::accessibility::semantics::Action fuchsia_action, bool ConvertAction(fuchsia::accessibility::semantics::Action fuchsia_action,
......
...@@ -7,5 +7,8 @@ ...@@ -7,5 +7,8 @@
<p>a third paragraph</p> <p>a third paragraph</p>
<button>another button</button> <button>another button</button>
<button>button 3</button> <button>button 3</button>
<div style='height:1000px; width:1000px;'></div>
<p>offscreen node</p>
<button>button 4</button>
</body> </body>
</html> </html>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment