Skip to content

Commit

Permalink
Dispatch page up/down events on scroll up/down AT actions.
Browse files Browse the repository at this point in the history
To achieve this, we also move the code from RenderAccessibilityImpl down
to the blink layer, in a new method called AXObject::Scroll(). The code
is run through the actions API.

To be able to test the feature, we added a way to trigger scroll up/down
actions through WebAXObjectProxy.

Finally, we expose the scroll actions in those platforms that support
it, by adding the actions to the node data.

Bug: 1099069
Change-Id: I345c902f7d92b83468d0eadf3031eab06ef0fd30
AX-Relnotes: generate page up/down keyboard events on AT scroll actions.
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3583725
Reviewed-by: Nektarios Paisios <nektar@chromium.org>
Commit-Queue: Jacobo Aragunde Pérez <jaragunde@igalia.com>
Cr-Commit-Position: refs/heads/main@{#1017124}
  • Loading branch information
jaragunde authored and Chromium LUCI CQ committed Jun 23, 2022
1 parent 34928dd commit d6a115d
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 60 deletions.
54 changes: 3 additions & 51 deletions content/renderer/accessibility/render_accessibility_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1290,57 +1290,9 @@ void RenderAccessibilityImpl::MarkAllAXObjectsDirty(

void RenderAccessibilityImpl::Scroll(const ui::AXActionTarget* target,
ax::mojom::Action scroll_action) {
gfx::Rect bounds = target->GetRelativeBounds();
if (bounds.IsEmpty())
return;

gfx::Point initial = target->GetScrollOffset();
gfx::Point min = target->MinimumScrollOffset();
gfx::Point max = target->MaximumScrollOffset();

// TODO(anastasi): This 4/5ths came from the Android implementation, revisit
// to find the appropriate modifier to keep enough context onscreen after
// scrolling.
int page_x = std::max((int)(bounds.width() * 4 / 5), 1);
int page_y = std::max((int)(bounds.height() * 4 / 5), 1);

// Forward/backward defaults to down/up unless it can only be scrolled
// horizontally.
if (scroll_action == ax::mojom::Action::kScrollForward)
scroll_action = max.y() > min.y() ? ax::mojom::Action::kScrollDown
: ax::mojom::Action::kScrollRight;
if (scroll_action == ax::mojom::Action::kScrollBackward)
scroll_action = max.y() > min.y() ? ax::mojom::Action::kScrollUp
: ax::mojom::Action::kScrollLeft;

int x = initial.x();
int y = initial.y();
switch (scroll_action) {
case ax::mojom::Action::kScrollUp:
if (initial.y() == min.y())
return;
y = std::max(initial.y() - page_y, min.y());
break;
case ax::mojom::Action::kScrollDown:
if (initial.y() == max.y())
return;
y = std::min(initial.y() + page_y, max.y());
break;
case ax::mojom::Action::kScrollLeft:
if (initial.x() == min.x())
return;
x = std::max(initial.x() - page_x, min.x());
break;
case ax::mojom::Action::kScrollRight:
if (initial.x() == max.x())
return;
x = std::min(initial.x() + page_x, max.x());
break;
default:
NOTREACHED();
}

target->SetScrollOffset(gfx::Point(x, y));
ui::AXActionData action_data;
action_data.action = scroll_action;
target->PerformAction(action_data);
}

void RenderAccessibilityImpl::AddImageAnnotationDebuggingAttributes(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[document web] name='Action verbs' actions=(showContextMenu)
[document web] name='Action verbs' actions=(showContextMenu, scrollUp, scrollDown, scrollLeft, scrollRight, scrollForward, scrollBackward)
++[section] actions=(showContextMenu)
++++[static] name='Generic div' actions=(showContextMenu)
++[heading] name='Heading' actions=(showContextMenu)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
rootWebArea name='Actions'
rootWebArea name='Actions' actions=scrollBackward,scrollDown,scrollForward,scrollLeft,scrollRight,scrollUp
++genericContainer ignored
++++genericContainer
++++++slider horizontal valueForRange=50.00 minValueForRange=1.00 maxValueForRange=100.00 actions=decrement,increment,setValue
Expand Down
16 changes: 16 additions & 0 deletions content/web_test/renderer/web_ax_object_proxy.cc
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,8 @@ gin::ObjectTemplateBuilder WebAXObjectProxy::GetObjectTemplateBuilder(
.SetMethod("scrollToMakeVisibleWithSubFocus",
&WebAXObjectProxy::ScrollToMakeVisibleWithSubFocus)
.SetMethod("scrollToGlobalPoint", &WebAXObjectProxy::ScrollToGlobalPoint)
.SetMethod("scrollUp", &WebAXObjectProxy::ScrollUp)
.SetMethod("scrollDown", &WebAXObjectProxy::ScrollDown)
.SetMethod("scrollX", &WebAXObjectProxy::ScrollX)
.SetMethod("scrollY", &WebAXObjectProxy::ScrollY)
.SetMethod("toString", &WebAXObjectProxy::ToString)
Expand Down Expand Up @@ -1465,6 +1467,20 @@ void WebAXObjectProxy::ScrollToGlobalPoint(int x, int y) {
accessibility_object_.PerformAction(action_data);
}

void WebAXObjectProxy::ScrollUp() {
UpdateLayout();
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kScrollUp;
accessibility_object_.PerformAction(action_data);
}

void WebAXObjectProxy::ScrollDown() {
UpdateLayout();
ui::AXActionData action_data;
action_data.action = ax::mojom::Action::kScrollDown;
accessibility_object_.PerformAction(action_data);
}

int WebAXObjectProxy::ScrollX() {
UpdateLayout();
return GetAXNodeData().GetIntAttribute(ax::mojom::IntAttribute::kScrollX);
Expand Down
2 changes: 2 additions & 0 deletions content/web_test/renderer/web_ax_object_proxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ class WebAXObjectProxy : public gin::Wrappable<WebAXObjectProxy> {
void ScrollToMakeVisible();
void ScrollToMakeVisibleWithSubFocus(int x, int y, int width, int height);
void ScrollToGlobalPoint(int x, int y);
void ScrollUp();
void ScrollDown();
int ScrollX();
int ScrollY();
std::string ToString();
Expand Down
110 changes: 104 additions & 6 deletions third_party/blink/renderer/modules/accessibility/ax_object.cc
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <ostream>

#include "base/auto_reset.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
Expand Down Expand Up @@ -538,6 +539,16 @@ blink::KeyboardEvent* CreateKeyboardEvent(
key.dom_code = static_cast<int>(ui::DomCode::CONTEXT_MENU);
key.native_key_code = key.windows_key_code = blink::VKEY_APPS;
break;
case ax::mojom::blink::Action::kScrollUp:
key.dom_key = ui::DomKey::PAGE_UP;
key.dom_code = static_cast<int>(ui::DomCode::PAGE_UP);
key.native_key_code = key.windows_key_code = blink::VKEY_PRIOR;
break;
case ax::mojom::blink::Action::kScrollDown:
key.dom_key = ui::DomKey::PAGE_DOWN;
key.dom_code = static_cast<int>(ui::DomCode::PAGE_DOWN);
key.native_key_code = key.windows_key_code = blink::VKEY_NEXT;
break;
default:
NOTREACHED();
}
Expand Down Expand Up @@ -1300,6 +1311,14 @@ void AXObject::SerializeActionAttributes(ui::AXNodeData* node_data) {
node_data->AddAction(ax::mojom::blink::Action::kDecrement);
node_data->AddAction(ax::mojom::blink::Action::kIncrement);
}
if (IsUserScrollable()) {
node_data->AddAction(ax::mojom::blink::Action::kScrollUp);
node_data->AddAction(ax::mojom::blink::Action::kScrollDown);
node_data->AddAction(ax::mojom::blink::Action::kScrollLeft);
node_data->AddAction(ax::mojom::blink::Action::kScrollRight);
node_data->AddAction(ax::mojom::blink::Action::kScrollForward);
node_data->AddAction(ax::mojom::blink::Action::kScrollBackward);
}
}

void AXObject::SerializeChildTreeID(ui::AXNodeData* node_data) {
Expand Down Expand Up @@ -5161,6 +5180,82 @@ void AXObject::SetScrollOffset(const gfx::Point& offset) const {
mojom::blink::ScrollType::kProgrammatic);
}

void AXObject::Scroll(ax::mojom::blink::Action scroll_action) const {
AXObject* offset_container = nullptr;
gfx::RectF bounds;
gfx::Transform container_transform;
GetRelativeBounds(&offset_container, bounds, container_transform);
if (bounds.IsEmpty())
return;

gfx::Point initial = GetScrollOffset();
gfx::Point min = MinimumScrollOffset();
gfx::Point max = MaximumScrollOffset();

// TODO(anastasi): This 4/5ths came from the Android implementation, revisit
// to find the appropriate modifier to keep enough context onscreen after
// scrolling.
int page_x = std::max(base::ClampRound<int>(bounds.width() * 4 / 5), 1);
int page_y = std::max(base::ClampRound<int>(bounds.height() * 4 / 5), 1);

// Forward/backward defaults to down/up unless it can only be scrolled
// horizontally.
if (scroll_action == ax::mojom::blink::Action::kScrollForward) {
scroll_action = max.y() > min.y() ? ax::mojom::blink::Action::kScrollDown
: ax::mojom::blink::Action::kScrollRight;
} else if (scroll_action == ax::mojom::blink::Action::kScrollBackward) {
scroll_action = max.y() > min.y() ? ax::mojom::blink::Action::kScrollUp
: ax::mojom::blink::Action::kScrollLeft;
}

int x = initial.x();
int y = initial.y();
switch (scroll_action) {
case ax::mojom::blink::Action::kScrollUp:
if (initial.y() == min.y())
return;
y = std::max(initial.y() - page_y, min.y());
break;
case ax::mojom::blink::Action::kScrollDown:
if (initial.y() == max.y())
return;
y = std::min(initial.y() + page_y, max.y());
break;
case ax::mojom::blink::Action::kScrollLeft:
if (initial.x() == min.x())
return;
x = std::max(initial.x() - page_x, min.x());
break;
case ax::mojom::blink::Action::kScrollRight:
if (initial.x() == max.x())
return;
x = std::min(initial.x() + page_x, max.x());
break;
default:
NOTREACHED();
}

SetScrollOffset(gfx::Point(x, y));

if (!RuntimeEnabledFeatures::
SynthesizedKeyboardEventsForAccessibilityActionsEnabled())
return;

// There are no keys that produce scroll left/right, so we shouldn't
// synthesize any keyboard events for these actions.
if (scroll_action == ax::mojom::blink::Action::kScrollLeft ||
scroll_action == ax::mojom::blink::Action::kScrollRight)
return;

LocalDOMWindow* local_dom_window = GetDocument()->domWindow();
KeyboardEvent* keydown = CreateKeyboardEvent(
local_dom_window, WebInputEvent::Type::kRawKeyDown, scroll_action);
GetNode()->DispatchEvent(*keydown);
KeyboardEvent* keyup = CreateKeyboardEvent(
local_dom_window, WebInputEvent::Type::kKeyUp, scroll_action);
GetNode()->DispatchEvent(*keyup);
}

bool AXObject::IsTableLikeRole() const {
return ui::IsTableLike(RoleValue()) ||
RoleValue() == ax::mojom::blink::Role::kLayoutTable;
Expand Down Expand Up @@ -5563,6 +5658,15 @@ bool AXObject::PerformAction(const ui::AXActionData& action_data) {
case ax::mojom::blink::Action::kShowContextMenu:
return RequestShowContextMenuAction();

case ax::mojom::blink::Action::kScrollBackward:
case ax::mojom::blink::Action::kScrollDown:
case ax::mojom::blink::Action::kScrollForward:
case ax::mojom::blink::Action::kScrollLeft:
case ax::mojom::blink::Action::kScrollRight:
case ax::mojom::blink::Action::kScrollUp:
Scroll(action_data.action);
return true;

case ax::mojom::blink::Action::kAnnotatePageImages:
case ax::mojom::blink::Action::kCollapse:
case ax::mojom::blink::Action::kCustomAction:
Expand All @@ -5575,13 +5679,7 @@ bool AXObject::PerformAction(const ui::AXActionData& action_data) {
case ax::mojom::blink::Action::kLoadInlineTextBoxes:
case ax::mojom::blink::Action::kNone:
case ax::mojom::blink::Action::kReplaceSelectedText:
case ax::mojom::blink::Action::kScrollBackward:
case ax::mojom::blink::Action::kScrollDown:
case ax::mojom::blink::Action::kScrollForward:
case ax::mojom::blink::Action::kScrollLeft:
case ax::mojom::blink::Action::kScrollRight:
case ax::mojom::blink::Action::kScrollToMakeVisible:
case ax::mojom::blink::Action::kScrollUp:
case ax::mojom::blink::Action::kSetSelection:
case ax::mojom::blink::Action::kShowTooltip:
case ax::mojom::blink::Action::kSignalEndOfTest:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,7 @@ class MODULES_EXPORT AXObject : public GarbageCollected<AXObject> {
gfx::Point GetScrollOffset() const;
gfx::Point MinimumScrollOffset() const;
gfx::Point MaximumScrollOffset() const;
void Scroll(ax::mojom::blink::Action scroll_action) const;
void SetScrollOffset(const gfx::Point&) const;

// Tables and grids.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<!DOCTYPE HTML>
<script src="../resources/gc.js"></script>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>

<!--
Accessibility Object Model - synthesized keyboard events for user action events from Assistive Technology
Explainer: https://github.com/WICG/aom/blob/gh-pages/explainer.md#user-action-events-from-assistive-technology
Spec: https://wicg.github.io/aom/spec/
-->
<script>
test(function(t) {
assert_true(internals.runtimeFlags.synthesizedKeyboardEventsForAccessibilityActionsEnabled);
}, "Make sure that keyboard event synthesis is enabled");
</script>

<input id="text" style="margin-top: 10000px;">

<script>
// Traverse ancestors up until reaching web document node, which is the one
// exposing the scroll accessibility actions.
let axDocument = accessibilityController.accessibleElementById("text");
while (axDocument.role != "AXRole: AXRootWebArea")
axDocument = axDocument.parentElement();

promise_test(function(t) {
let oldY;
return new Promise(resolve => {
window.addEventListener('keydown', resolve);
oldY = window.pageYOffset;
axDocument.scrollDown();
}).then(event => {
assert_equals(event.type, "keydown");
assert_equals(event.keyCode, 34); // 34 = page down key
assert_true(window.pageYOffset > oldY);
});
}, "Test for synthesized keydown event in scroll down action");

promise_test(function(t) {
let oldY;
return new Promise(resolve => {
window.addEventListener('keyup', resolve);
oldY = window.pageYOffset;
axDocument.scrollDown();
}).then(event => {
assert_equals(event.type, "keyup");
assert_equals(event.keyCode, 34); // 34 = page down key
assert_true(window.pageYOffset > oldY);
});
}, "Test for synthesized keyup event in scroll down action");

promise_test(function(t) {
let oldY;
return new Promise(resolve => {
window.addEventListener('keydown', resolve);
oldY = window.pageYOffset;
axDocument.scrollUp();
}).then(event => {
assert_equals(event.type, "keydown");
assert_equals(event.keyCode, 33); // 33 = page up key
assert_true(window.pageYOffset < oldY);
});
}, "Test for synthesized keydown event in scroll up action");

promise_test(function(t) {
let oldY;
return new Promise(resolve => {
window.addEventListener('keyup', resolve);
oldY = window.pageYOffset;
axDocument.scrollUp();
}).then(event => {
assert_equals(event.type, "keyup");
assert_equals(event.keyCode, 33); // 33 = page up key
assert_true(window.pageYOffset < oldY);
});
}, "Test for synthesized keyup event in scroll up action");
</script>
6 changes: 5 additions & 1 deletion ui/accessibility/platform/ax_platform_node_auralinux.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5232,7 +5232,11 @@ std::vector<ax::mojom::Action> AXPlatformNodeAuraLinux::GetSupportedActions()
const {
static const base::NoDestructor<std::vector<ax::mojom::Action>>
kActionsThatCanBeExposedViaAtkAction{
{ax::mojom::Action::kDecrement, ax::mojom::Action::kIncrement}};
{ax::mojom::Action::kDecrement, ax::mojom::Action::kIncrement,
ax::mojom::Action::kScrollUp, ax::mojom::Action::kScrollDown,
ax::mojom::Action::kScrollLeft, ax::mojom::Action::kScrollRight,
ax::mojom::Action::kScrollForward,
ax::mojom::Action::kScrollBackward}};
std::vector<ax::mojom::Action> supported_actions;

// The default action, if it exists, must be listed at index 0.
Expand Down

0 comments on commit d6a115d

Please sign in to comment.