Commit 7b49a280 authored by Julie Jeongeun Kim's avatar Julie Jeongeun Kim Committed by Commit Bot

[a11y] Updates the bounds for sticky positioned nodes on scrolling

This CL introduces AddToFixedOrStickyNodeList and
InvalidateBoundingBoxForFixedOrStickyPosition to AXObjectCacheImpl
to update the bounds for fixed or sticky positioned nodes on scrolling.

AXObjectCacheImpl owns |fixed_or_sticky_node_ids_|, updates when AX
objects retrieve their relative bounds, and clears it when layout is
completed.

Once the scroll position is updated, it invalidates the bounds for
the fixed or sticky positioned nodes by calling
InvalidateBoundingBoxForFixedOrStickyPosition and ScrollPositionChanged
event sends location changes to the browser.

AX-Relnotes: n/a.
Bug: 1027542
Change-Id: I29a6fd1584b64f0c597f44bdacda18e6540f6d63
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2402928
Commit-Queue: Dominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#807147}
parent 0421634a
...@@ -1306,6 +1306,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessiblitiyBoundsFixed) { ...@@ -1306,6 +1306,11 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessiblitiyBoundsFixed) {
RunHtmlTest(FILE_PATH_LITERAL("bounds-fixed.html")); RunHtmlTest(FILE_PATH_LITERAL("bounds-fixed.html"));
} }
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
AccessiblitiyBoundsFixedScrolling) {
RunHtmlTest(FILE_PATH_LITERAL("bounds-fixed-scrolling.html"));
}
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityBR) { IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, AccessibilityBR) {
RunHtmlTest(FILE_PATH_LITERAL("br.html")); RunHtmlTest(FILE_PATH_LITERAL("br.html"));
} }
......
...@@ -781,8 +781,9 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { ...@@ -781,8 +781,9 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() {
std::vector<DirtyObject> dirty_objects = dirty_objects_; std::vector<DirtyObject> dirty_objects = dirty_objects_;
dirty_objects_.clear(); dirty_objects_.clear();
// If there's a layout complete message, we need to send location changes. // If there's a layout complete or a scroll changed message, we need to send
bool had_layout_complete_messages = false; // location changes.
bool need_to_send_location_changes = false;
// If there's a load complete message, we need to change the event schedule // If there's a load complete message, we need to change the event schedule
// mode. // mode.
...@@ -796,8 +797,10 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { ...@@ -796,8 +797,10 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() {
// Loop over each event and generate an updated event message. // Loop over each event and generate an updated event message.
for (ui::AXEvent& event : src_events) { for (ui::AXEvent& event : src_events) {
if (event.event_type == ax::mojom::Event::kLayoutComplete) if (event.event_type == ax::mojom::Event::kLayoutComplete ||
had_layout_complete_messages = true; event.event_type == ax::mojom::Event::kScrollPositionChanged) {
need_to_send_location_changes = true;
}
if (event.event_type == ax::mojom::Event::kLoadComplete) if (event.event_type == ax::mojom::Event::kLoadComplete)
had_load_complete_messages = true; had_load_complete_messages = true;
...@@ -952,7 +955,7 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() { ...@@ -952,7 +955,7 @@ void RenderAccessibilityImpl::SendPendingAccessibilityEvents() {
weak_factory_for_pending_events_.GetWeakPtr())); weak_factory_for_pending_events_.GetWeakPtr()));
reset_token_ = 0; reset_token_ = 0;
if (had_layout_complete_messages) if (need_to_send_location_changes)
SendLocationChanges(); SendLocationChanges();
if (had_load_complete_messages) { if (had_load_complete_messages) {
...@@ -1005,6 +1008,9 @@ void RenderAccessibilityImpl::SendLocationChanges() { ...@@ -1005,6 +1008,9 @@ void RenderAccessibilityImpl::SendLocationChanges() {
tree_source_->SetCachedBoundingBox(id, new_location); tree_source_->SetCachedBoundingBox(id, new_location);
} }
if (changes.empty())
return;
// Ensure that the number of cached bounding boxes doesn't exceed the // Ensure that the number of cached bounding boxes doesn't exceed the
// number of nodes in the tree, that would indicate the cache could // number of nodes in the tree, that would indicate the cache could
// grow without bounds. Calls from the serializer to // grow without bounds. Calls from the serializer to
......
...@@ -180,11 +180,21 @@ class RenderAccessibilityHostInterceptor ...@@ -180,11 +180,21 @@ class RenderAccessibilityHostInterceptor
std::move(callback).Run(); std::move(callback).Run();
} }
void HandleAXLocationChanges(
std::vector<mojom::LocationChangesPtr> changes) override {
for (auto& change : changes)
location_changes_.emplace_back(std::move(change));
}
ui::AXTreeUpdate& last_update() { ui::AXTreeUpdate& last_update() {
CHECK_GE(handled_updates_.size(), 1U); CHECK_GE(handled_updates_.size(), 1U);
return handled_updates_.back(); return handled_updates_.back();
} }
std::vector<mojom::LocationChangesPtr>& location_changes() {
return location_changes_;
}
void ClearHandledUpdates() { handled_updates_.clear(); } void ClearHandledUpdates() { handled_updates_.clear(); }
private: private:
...@@ -196,6 +206,7 @@ class RenderAccessibilityHostInterceptor ...@@ -196,6 +206,7 @@ class RenderAccessibilityHostInterceptor
local_frame_host_remote_; local_frame_host_remote_;
std::vector<::ui::AXTreeUpdate> handled_updates_; std::vector<::ui::AXTreeUpdate> handled_updates_;
std::vector<mojom::LocationChangesPtr> location_changes_;
}; };
class RenderAccessibilityTestRenderFrame : public TestRenderFrame { class RenderAccessibilityTestRenderFrame : public TestRenderFrame {
...@@ -229,6 +240,10 @@ class RenderAccessibilityTestRenderFrame : public TestRenderFrame { ...@@ -229,6 +240,10 @@ class RenderAccessibilityTestRenderFrame : public TestRenderFrame {
render_accessibility_host_->ClearHandledUpdates(); render_accessibility_host_->ClearHandledUpdates();
} }
std::vector<mojom::LocationChangesPtr>& LocationChanges() {
return render_accessibility_host_->location_changes();
}
private: private:
explicit RenderAccessibilityTestRenderFrame( explicit RenderAccessibilityTestRenderFrame(
RenderFrameImpl::CreateParams params) RenderFrameImpl::CreateParams params)
...@@ -359,6 +374,11 @@ class RenderAccessibilityImplTest : public RenderViewTest { ...@@ -359,6 +374,11 @@ class RenderAccessibilityImplTest : public RenderViewTest {
->ClearHandledUpdates(); ->ClearHandledUpdates();
} }
std::vector<mojom::LocationChangesPtr>& GetLocationChanges() {
return static_cast<RenderAccessibilityTestRenderFrame*>(frame())
->LocationChanges();
}
int CountAccessibilityNodesSentToBrowser() { int CountAccessibilityNodesSentToBrowser() {
ui::AXTreeUpdate update = GetLastAccUpdate(); ui::AXTreeUpdate update = GetLastAccUpdate();
return update.nodes.size(); return update.nodes.size();
...@@ -593,6 +613,133 @@ TEST_F(RenderAccessibilityImplTest, ShowAccessibilityObject) { ...@@ -593,6 +613,133 @@ TEST_F(RenderAccessibilityImplTest, ShowAccessibilityObject) {
EXPECT_EQ(2, CountAccessibilityNodesSentToBrowser()); EXPECT_EQ(2, CountAccessibilityNodesSentToBrowser());
} }
// Tests if the bounds of the fixed positioned node is updated after scrolling.
TEST_F(RenderAccessibilityImplTest, TestBoundsForFixedNodeAfterScroll) {
constexpr char html[] = R"HTML(
<div id="positioned" style="position:fixed; top:10px; font-size:40px;"
aria-label="first">title</div>
<div style="padding-top: 50px; font-size:40px;">
<h2>Heading #1</h2>
<h2>Heading #2</h2>
<h2>Heading #3</h2>
<h2>Heading #4</h2>
<h2>Heading #5</h2>
<h2>Heading #6</h2>
<h2>Heading #7</h2>
<h2>Heading #8</h2>
</div>
)HTML";
LoadHTMLAndRefreshAccessibilityTree(html);
int scroll_offset_y = 50;
int32_t expected_id;
ui::AXRelativeBounds expected_bounds;
// Prepare the expected information from the tree.
ui::AXTreeUpdate update = GetLastAccUpdate();
for (ui::AXNodeData& node : update.nodes) {
std::string name;
if (node.GetStringAttribute(ax::mojom::StringAttribute::kName, &name) &&
name == "first") {
expected_id = node.id;
expected_bounds = node.relative_bounds;
expected_bounds.bounds.set_y(expected_bounds.bounds.y() +
scroll_offset_y);
break;
}
}
ClearHandledUpdates();
// Simulate scrolling down using JS.
std::string js("window.scrollTo(0, " + base::NumberToString(scroll_offset_y) +
");");
ExecuteJavaScriptForTests(js.c_str());
WebDocument document = GetMainFrame()->GetDocument();
WebAXObject root_obj = WebAXObject::FromWebDocument(document);
GetRenderAccessibilityImpl()->HandleAXEvent(
ui::AXEvent(root_obj.AxID(), ax::mojom::Event::kScrollPositionChanged));
SendPendingAccessibilityEvents();
EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser());
// Make sure it's the root object that was updated for scrolling.
update = GetLastAccUpdate();
EXPECT_EQ(root_obj.AxID(), update.nodes[0].id);
// Make sure that a location change is sent for the fixed-positioned node.
std::vector<mojom::LocationChangesPtr>& changes = GetLocationChanges();
EXPECT_EQ(changes.size(), 1u);
EXPECT_EQ(changes[0]->id, expected_id);
EXPECT_EQ(changes[0]->new_location, expected_bounds);
}
// Tests if the bounds are updated when it has multiple fixed nodes.
TEST_F(RenderAccessibilityImplTest, TestBoundsForMultipleFixedNodeAfterScroll) {
constexpr char html[] = R"HTML(
<div id="positioned" style="position:fixed; top:10px; font-size:40px;"
aria-label="first">title1</div>
<div id="positioned" style="position:fixed; top:50px; font-size:40px;"
aria-label="second">title2</div>
<div style="padding-top: 50px; font-size:40px;">
<h2>Heading #1</h2>
<h2>Heading #2</h2>
<h2>Heading #3</h2>
<h2>Heading #4</h2>
<h2>Heading #5</h2>
<h2>Heading #6</h2>
<h2>Heading #7</h2>
<h2>Heading #8</h2>
</div>)HTML";
LoadHTMLAndRefreshAccessibilityTree(html);
int scroll_offset_y = 50;
std::map<int32_t, ui::AXRelativeBounds> expected;
// Prepare the expected information from the tree.
ui::AXTreeUpdate update = GetLastAccUpdate();
for (ui::AXNodeData& node : update.nodes) {
std::string name;
node.GetStringAttribute(ax::mojom::StringAttribute::kName, &name);
if (name == "first" || name == "second") {
ui::AXRelativeBounds ax_bounds = node.relative_bounds;
ax_bounds.bounds.set_y(ax_bounds.bounds.y() + scroll_offset_y);
expected[node.id] = ax_bounds;
}
}
ClearHandledUpdates();
// Simulate scrolling down using JS.
std::string js("window.scrollTo(0, " + base::NumberToString(scroll_offset_y) +
");");
ExecuteJavaScriptForTests(js.c_str());
WebDocument document = GetMainFrame()->GetDocument();
WebAXObject root_obj = WebAXObject::FromWebDocument(document);
GetRenderAccessibilityImpl()->HandleAXEvent(
ui::AXEvent(root_obj.AxID(), ax::mojom::Event::kScrollPositionChanged));
SendPendingAccessibilityEvents();
EXPECT_EQ(1, CountAccessibilityNodesSentToBrowser());
// Make sure it's the root object that was updated for scrolling.
update = GetLastAccUpdate();
EXPECT_EQ(root_obj.AxID(), update.nodes[0].id);
// Make sure that a location change is sent for the fixed-positioned node.
std::vector<mojom::LocationChangesPtr>& changes = GetLocationChanges();
EXPECT_EQ(changes.size(), 2u);
for (auto& change : changes) {
auto search = expected.find(change->id);
EXPECT_NE(search, expected.end());
EXPECT_EQ(search->second, change->new_location);
}
}
class MockPluginAccessibilityTreeSource : public content::PluginAXTreeSource { class MockPluginAccessibilityTreeSource : public content::PluginAXTreeSource {
public: public:
MockPluginAccessibilityTreeSource(ui::AXNode::AXID root_node_id) { MockPluginAccessibilityTreeSource(ui::AXNode::AXID root_node_id) {
......
rootWebArea
++genericContainer ignored
++++genericContainer ignored
++++++button name='Scroll'
++++++++staticText name='Scroll'
++++++++++inlineTextBox name='Scroll'
++++++genericContainer location=(8, 70)
++++++++staticText name='title'
++++++++++inlineTextBox name='title'
++++++genericContainer
++++++++heading name='Heading #1'
++++++++++staticText name='Heading #1'
++++++++++++inlineTextBox name='Heading #1'
++++++++heading name='Heading #2'
++++++++++staticText name='Heading #2'
++++++++++++inlineTextBox name='Heading #2'
++++++++heading name='Heading #3'
++++++++++staticText name='Heading #3'
++++++++++++inlineTextBox name='Heading #3'
++++++++heading name='Heading #4'
++++++++++staticText name='Heading #4'
++++++++++++inlineTextBox name='Heading #4'
++++++++heading name='Heading #5'
++++++++++staticText name='Heading #5'
++++++++++++inlineTextBox name='Heading #5'
++++++++heading name='Heading #6'
++++++++++staticText name='Heading #6'
++++++++++++inlineTextBox name='Heading #6'
++++++++heading name='Heading #7'
++++++++++staticText name='Heading #7'
++++++++++++inlineTextBox name='Heading #7'
++++++++heading name='Heading #8'
++++++++++staticText name='Heading #8'
++++++++++++inlineTextBox name='Heading #8'
++++++genericContainer
++++++++staticText name='done'
++++++++++inlineTextBox name='done'
<!--
@BLINK-ALLOW:location=(8, 70)
@WAIT-FOR:done
-->
<!DOCTYPE html>
<html>
<head>
<style>
#positioned {
position:fixed;
top:50px;
height: 100px;
width: 200px;
font-size:40px;
background: #555;
}
#heading_list {
padding-top: 100px;
font-size:40px;
}
</style>
</head>
<body onload="onLoad()">
<button onclick="clicked()">Scroll</button>
<div id="positioned">title</div>
<div id="heading_list">
<h2>Heading #1</h2>
<h2>Heading #2</h2>
<h2>Heading #3</h2>
<h2>Heading #4</h2>
<h2>Heading #5</h2>
<h2>Heading #6</h2>
<h2>Heading #7</h2>
<h2>Heading #8</h2>
</div>
<div id="statusDiv"></div>
<script>
function onLoad() {
window.setTimeout(() => {
window.onscroll = function() {
window.requestAnimationFrame(() => {
statusDiv.innerText = "done";
});
};
document.querySelector('button').click();
}, 100);
}
function clicked() {
window.requestAnimationFrame(() => {
window.scrollTo(0, 20);
});
}
</script>
</body>
</html>
...@@ -3603,6 +3603,11 @@ void AXObject::GetRelativeBounds(AXObject** out_container, ...@@ -3603,6 +3603,11 @@ void AXObject::GetRelativeBounds(AXObject** out_container,
if (!layout_object) if (!layout_object)
return; return;
if (layout_object->IsFixedPositioned() ||
layout_object->IsStickyPositioned()) {
AXObjectCache().AddToFixedOrStickyNodeList(this);
}
if (clips_children) { if (clips_children) {
if (IsWebArea()) if (IsWebArea())
*clips_children = true; *clips_children = true;
......
...@@ -755,6 +755,12 @@ AXID AXObjectCacheImpl::GenerateAXID() const { ...@@ -755,6 +755,12 @@ AXID AXObjectCacheImpl::GenerateAXID() const {
return obj_id; return obj_id;
} }
void AXObjectCacheImpl::AddToFixedOrStickyNodeList(const AXObject* object) {
DCHECK(object);
DCHECK(!object->IsDetached());
fixed_or_sticky_node_ids_.insert(object->AXObjectID());
}
AXID AXObjectCacheImpl::GetOrCreateAXID(AXObject* obj) { AXID AXObjectCacheImpl::GetOrCreateAXID(AXObject* obj) {
// check for already-assigned ID // check for already-assigned ID
const AXID existing_axid = obj->AXObjectID(); const AXID existing_axid = obj->AXObjectID();
...@@ -776,6 +782,8 @@ void AXObjectCacheImpl::RemoveAXID(AXObject* object) { ...@@ -776,6 +782,8 @@ void AXObjectCacheImpl::RemoveAXID(AXObject* object) {
if (!object) if (!object)
return; return;
fixed_or_sticky_node_ids_.clear();
if (active_aria_modal_dialog_ == object) if (active_aria_modal_dialog_ == object)
active_aria_modal_dialog_ = nullptr; active_aria_modal_dialog_ = nullptr;
...@@ -819,6 +827,11 @@ void AXObjectCacheImpl::UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram() { ...@@ -819,6 +827,11 @@ void AXObjectCacheImpl::UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram() {
tree_update_callback_queue_.size()); tree_update_callback_queue_.size());
} }
void AXObjectCacheImpl::InvalidateBoundingBoxForFixedOrStickyPosition() {
for (AXID id : fixed_or_sticky_node_ids_)
changed_bounds_ids_.insert(id);
}
void AXObjectCacheImpl::DeferTreeUpdateInternal(base::OnceClosure callback, void AXObjectCacheImpl::DeferTreeUpdateInternal(base::OnceClosure callback,
AXObject* obj) { AXObject* obj) {
// Called for updates that do not have a DOM node, e.g. a children or text // Called for updates that do not have a DOM node, e.g. a children or text
...@@ -2216,6 +2229,7 @@ void AXObjectCacheImpl::HandleScrollPositionChanged( ...@@ -2216,6 +2229,7 @@ void AXObjectCacheImpl::HandleScrollPositionChanged(
LocalFrameView* frame_view) { LocalFrameView* frame_view) {
SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*frame_view->GetFrame().GetDocument()); SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*frame_view->GetFrame().GetDocument());
InvalidateBoundingBoxForFixedOrStickyPosition();
AXObject* target_ax_object = GetOrCreate(document_); AXObject* target_ax_object = GetOrCreate(document_);
PostNotification(target_ax_object, ax::mojom::Event::kScrollPositionChanged); PostNotification(target_ax_object, ax::mojom::Event::kScrollPositionChanged);
} }
...@@ -2223,6 +2237,7 @@ void AXObjectCacheImpl::HandleScrollPositionChanged( ...@@ -2223,6 +2237,7 @@ void AXObjectCacheImpl::HandleScrollPositionChanged(
void AXObjectCacheImpl::HandleScrollPositionChanged( void AXObjectCacheImpl::HandleScrollPositionChanged(
LayoutObject* layout_object) { LayoutObject* layout_object) {
SCOPED_DISALLOW_LIFECYCLE_TRANSITION(layout_object->GetDocument()); SCOPED_DISALLOW_LIFECYCLE_TRANSITION(layout_object->GetDocument());
InvalidateBoundingBoxForFixedOrStickyPosition();
PostNotification(GetOrCreate(layout_object), PostNotification(GetOrCreate(layout_object),
ax::mojom::Event::kScrollPositionChanged); ax::mojom::Event::kScrollPositionChanged);
} }
......
...@@ -260,6 +260,10 @@ class MODULES_EXPORT AXObjectCacheImpl ...@@ -260,6 +260,10 @@ class MODULES_EXPORT AXObjectCacheImpl
const HeapVector<Member<Element>>& attr_associated_elements, const HeapVector<Member<Element>>& attr_associated_elements,
HeapVector<Member<AXObject>>& owned_children); HeapVector<Member<AXObject>>& owned_children);
// Adds |object| to |fixed_or_sticky_node_ids_| if it has a fixed or sticky
// position.
void AddToFixedOrStickyNodeList(const AXObject* object);
bool MayHaveHTMLLabel(const HTMLElement& elem); bool MayHaveHTMLLabel(const HTMLElement& elem);
// Synchronously returns whether or not we currently have permission to // Synchronously returns whether or not we currently have permission to
...@@ -479,6 +483,12 @@ class MODULES_EXPORT AXObjectCacheImpl ...@@ -479,6 +483,12 @@ class MODULES_EXPORT AXObjectCacheImpl
void UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram(); void UpdateNumTreeUpdatesQueuedBeforeLayoutHistogram();
// Invalidates the bounding boxes of fixed or sticky positioned objects which
// should be updated when the scroll offset is changed. Like
// InvalidateBoundingBox, it can be later retrieved by
// GetAllObjectsWithChangedBounds.
void InvalidateBoundingBoxForFixedOrStickyPosition();
// Whether the user has granted permission for the user to install event // Whether the user has granted permission for the user to install event
// listeners for accessibility events using the AOM. // listeners for accessibility events using the AOM.
mojom::PermissionStatus accessibility_event_permission_; mojom::PermissionStatus accessibility_event_permission_;
...@@ -509,6 +519,9 @@ class MODULES_EXPORT AXObjectCacheImpl ...@@ -509,6 +519,9 @@ class MODULES_EXPORT AXObjectCacheImpl
// GetAllObjectsWithChangedBounds was called. // GetAllObjectsWithChangedBounds was called.
HashSet<AXID> changed_bounds_ids_; HashSet<AXID> changed_bounds_ids_;
// The list of node IDs whose position is fixed or sticky.
HashSet<AXID> fixed_or_sticky_node_ids_;
// The source of the event that is currently being handled. // The source of the event that is currently being handled.
ax::mojom::blink::EventFrom active_event_from_ = ax::mojom::blink::EventFrom active_event_from_ =
ax::mojom::blink::EventFrom::kNone; ax::mojom::blink::EventFrom::kNone;
......
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