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 @@
#include "base/logging.h"
#include "fuchsia/engine/browser/ax_tree_converter.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/gfx/geometry/rect_conversions.h"
namespace {
......@@ -20,6 +21,9 @@ constexpr uint32_t kSemanticNodeRootId = 0;
// maximum sizes of serialized Semantic Nodes.
constexpr size_t kMaxNodesPerUpdate = 16;
// Error allowed for each edge when converting from gfx::RectF to gfx::Rect.
constexpr float kRectConversionError = 0.5;
} // namespace
AccessibilityBridge::AccessibilityBridge(
......@@ -32,11 +36,11 @@ AccessibilityBridge::AccessibilityBridge(
on_error_callback_(std::move(on_error_callback)) {
DCHECK(web_contents_);
Observe(web_contents_);
tree_.AddObserver(this);
ax_tree_.AddObserver(this);
semantics_manager->RegisterViewForSemantics(
std::move(view_ref), binding_.NewBinding(), tree_ptr_.NewRequest());
tree_ptr_.set_error_handler([this](zx_status_t status) mutable {
std::move(view_ref), binding_.NewBinding(), semantic_tree_.NewRequest());
semantic_tree_.set_error_handler([this](zx_status_t status) {
ZX_LOG(ERROR, status) << "SemanticTree disconnected";
std::move(on_error_callback_).Run(ZX_ERR_INTERNAL);
});
......@@ -70,7 +74,7 @@ void AccessibilityBridge::TryCommit() {
}
DispatchSemanticsMessages(range_start, to_send_.size() - range_start);
tree_ptr_->CommitUpdates(
semantic_tree_->CommitUpdates(
fit::bind_member(this, &AccessibilityBridge::OnCommitComplete));
commit_inflight_ = true;
to_send_.clear();
......@@ -83,14 +87,14 @@ void AccessibilityBridge::DispatchSemanticsMessages(size_t start, size_t size) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::UPDATE);
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) {
std::vector<uint32_t> deletes;
for (size_t i = start; i < start + size; i++) {
DCHECK(to_send_.at(i).type == SemanticUpdateOrDelete::Type::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(
const content::AXEventNotificationDetails& details) {
// Updates to AXTree must be applied first.
for (const ui::AXTreeUpdate& update : details.updates) {
if (!tree_.Unserialize(update)) {
// If this fails, it is a fatal error that will cause the current Frame to
// be destroyed.
if (!ax_tree_.Unserialize(update)) {
// If this fails, it is a fatal error that will cause an early exit.
std::move(on_error_callback_).Run(ZX_ERR_INTERNAL);
return;
}
......@@ -136,8 +139,8 @@ void AccessibilityBridge::AccessibilityEventReceived(
// Events to fire after tree has been updated.
for (const ui::AXEvent& event : details.events) {
if (event.event_type == ax::mojom::Event::kHitTestResult) {
if (pending_hit_test_callbacks_.find(event.action_request_id) !=
if (event.event_type == ax::mojom::Event::kHitTestResult &&
pending_hit_test_callbacks_.find(event.action_request_id) !=
pending_hit_test_callbacks_.end()) {
fuchsia::accessibility::semantics::Hit hit;
hit.set_node_id(ConvertToFuchsiaNodeId(event.id));
......@@ -145,7 +148,8 @@ void AccessibilityBridge::AccessibilityEventReceived(
// Run the pending callback with the hit.
pending_hit_test_callbacks_[event.action_request_id](std::move(hit));
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(
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);
callback(true);
}
......@@ -199,8 +219,7 @@ void AccessibilityBridge::OnSemanticsModeChanged(
callback();
}
void AccessibilityBridge::DeleteSubtree(ui::AXTree* tree, ui::AXNode* node) {
DCHECK(tree);
void AccessibilityBridge::DeleteSubtree(ui::AXNode* node) {
DCHECK(node);
// 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) {
ConvertToFuchsiaNodeId(node->id())));
}
for (ui::AXNode* child : node->children())
DeleteSubtree(tree, child);
DeleteSubtree(child);
}
void AccessibilityBridge::OnNodeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
DeleteSubtree(tree, node);
DeleteSubtree(node);
}
void AccessibilityBridge::OnSubtreeWillBeDeleted(ui::AXTree* tree,
ui::AXNode* node) {
DeleteSubtree(tree, node);
DeleteSubtree(node);
}
void AccessibilityBridge::OnAtomicUpdateFinished(
ui::AXTree* tree,
bool root_changed,
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) {
ui::AXNodeData ax_data;
switch (change.type) {
......@@ -246,7 +266,7 @@ void AccessibilityBridge::OnAtomicUpdateFinished(
break;
case ui::AXTreeObserver::NODE_REPARENTED:
case ui::AXTreeObserver::SUBTREE_REPARENTED:
DeleteSubtree(tree, change.node);
DeleteSubtree(change.node);
break;
}
}
......
......@@ -31,7 +31,7 @@ class WebContents;
// 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
// 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|.
class WEB_ENGINE_EXPORT AccessibilityBridge
: public content::WebContentsObserver,
......@@ -49,11 +49,17 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
AccessibilityBridge(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:
FRIEND_TEST_ALL_PREFIXES(AccessibilityBridgeTest, OnSemanticsModeChanged);
// A struct used for caching semantic information. This allows for updates and
// deletes to be stored in the same vector to preserve all ordering
// A struct used for caching semantic information. This allows for updates
// and deletes to be stored in the same vector to preserve all ordering
// information.
struct SemanticUpdateOrDelete {
enum Type { UPDATE, DELETE };
......@@ -79,14 +85,14 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
// Callback for SemanticTree::CommitUpdates.
void OnCommitComplete();
// Converts AXNode ids to Semantic Node ids, and handles special casing of the
// root.
// Converts AXNode ids to Semantic Node ids, and handles special casing of
// the root.
uint32_t ConvertToFuchsiaNodeId(int32_t ax_node_id);
// Deletes all nodes in subtree rooted at and including |node|, unless |node|
// is the root of the tree.
// |tree| and |node| are owned by the accessibility bridge.
void DeleteSubtree(ui::AXTree* tree, ui::AXNode* node);
// Deletes all nodes in subtree rooted at and including |node|, unless
// |node| is the root of the tree. |tree| and |node| are owned by the
// accessibility bridge.
void DeleteSubtree(ui::AXNode* node);
// content::WebContentsObserver implementation.
void AccessibilityEventReceived(
......@@ -110,25 +116,27 @@ class WEB_ENGINE_EXPORT AccessibilityBridge
bool root_changed,
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_;
content::WebContents* web_contents_;
ui::AXSerializableTree tree_;
ui::AXSerializableTree ax_tree_;
// Cache for pending data to be sent to the Semantic Tree between commits.
std::vector<SemanticUpdateOrDelete> to_send_;
bool commit_inflight_ = false;
// Maintain a map of callbacks as multiple hit test events can happen at once.
// These are keyed by the request_id field of ui::AXActionData.
// Maintain a map of callbacks as multiple hit test events can happen at
// once. These are keyed by the request_id field of ui::AXActionData.
base::flat_map<int, HitTestCallback> pending_hit_test_callbacks_;
// 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.
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;
base::OnceClosure event_received_callback_for_test_;
};
#endif // FUCHSIA_ENGINE_BROWSER_ACCESSIBILITY_BRIDGE_H_
......@@ -30,10 +30,10 @@ const char kButtonName2[] = "another button";
const char kButtonName3[] = "button 3";
const char kNodeName[] = "last node";
const char kParagraphName[] = "a third paragraph";
const char kOffscreenNodeName[] = "offscreen node";
const size_t kPage1NodeCount = 9;
const size_t kPage2NodeCount = 190;
fuchsia::math::PointF GetCenterOfBox(fuchsia::ui::gfx::BoundingBox box) {
fuchsia::math::PointF center;
center.x = (box.min.x + box.max.x) / 2;
......@@ -270,3 +270,47 @@ IN_PROC_BROWSER_TEST_F(AccessibilityBridgeTest, Disconnect) {
semantics_manager_.semantic_tree()->Disconnect();
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,
case fuchsia::accessibility::semantics::Action::DEFAULT:
*mojom_action = ax::mojom::Action::kDoDefault;
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::SET_FOCUS:
case fuchsia::accessibility::semantics::Action::SET_VALUE:
case fuchsia::accessibility::semantics::Action::SHOW_ON_SCREEN:
return false;
default:
LOG(WARNING)
......
......@@ -18,7 +18,7 @@
WEB_ENGINE_EXPORT fuchsia::accessibility::semantics::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
// supported in Chromium.
bool ConvertAction(fuchsia::accessibility::semantics::Action fuchsia_action,
......
......@@ -7,5 +7,8 @@
<p>a third paragraph</p>
<button>another button</button>
<button>button 3</button>
<div style='height:1000px; width:1000px;'></div>
<p>offscreen node</p>
<button>button 4</button>
</body>
</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