Commit 488275d5 authored by Nektarios Paisios's avatar Nektarios Paisios Committed by Commit Bot

Implemented a mechanism that can be used to describe what caused an accessibility event

Assistive software cannot readily distinguish between various
user or page actions when it receives an accessibility event.
For example, if a text change or a selection change event is
raised, the AT will not know if the text changed because the
user has typed something, if a script has written something
to the page, or if the user has pasted something.
This will affect the way that the AT might choose to
verbalize the newly added text, including choosing
not to verbalize the text at all.

On the Mac, VoiceOver needs to be told the cause of
selection change and text change events, AKA the event intents.
On Windows, the AT currently relies on monitoring the
keyboard in order to identify such intents.
On Chrome OS, we currently rely on heuristics.

This patch creates a new class, AXEventIntent, which describes the
cause of an accessibility event, e.g. "cut to the end of word".
Code in Blink should use instances of ScopedAXEventIntent to place
information about which intents are active on the stack.
This patch modifies the accessibility event dispatch mechanism
to include all active intents with each event that is sent to
the browser.

Relnotes: N/A

R=dmazzoni@chromium.org

Change-Id: I547e1c8cd980c1ec7b3a265d085317aae27c86f2
Bug: 989156
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2031047Reviewed-by: default avatarRobert Sesek <rsesek@chromium.org>
Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Commit-Queue: Nektarios Paisios <nektar@chromium.org>
Auto-Submit: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#762642}
parent 2aee6a49
......@@ -232,6 +232,7 @@ source_set("common") {
"//third_party/blink/public:blink_headers",
"//third_party/blink/public/common",
"//ui/accessibility",
"//ui/accessibility/mojom",
"//ui/base/cursor",
"//ui/base/mojom:cursor_type",
"//ui/events/ipc",
......@@ -788,6 +789,7 @@ mojom("mojo_bindings") {
"//skia/public/mojom",
"//third_party/blink/public/mojom:mojom_core",
"//third_party/blink/public/mojom:web_feature_mojo_bindings",
"//ui/accessibility:ax_enums_mojo",
"//ui/accessibility/mojom",
"//ui/base/ime/mojom",
"//ui/base/mojom",
......
......@@ -40,7 +40,7 @@
#include "third_party/blink/public/web/web_view.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_enum_util.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_node.h"
#include "ui/accessibility/ax_role_properties.h"
......@@ -320,9 +320,10 @@ void RenderAccessibilityImpl::HitTest(
tree_source_.SerializeNode(ax_object, &data);
if (data.child_routing_id == MSG_ROUTING_NONE) {
// Otherwise, send an event on the node that was hit.
HandleAXEvent(
ui::AXEvent(ax_object.AxID(), action_data.hit_test_event_to_fire,
ax::mojom::EventFrom::kAction, action_data.request_id));
const std::vector<ui::AXEventIntent> intents;
HandleAXEvent(ui::AXEvent(
ax_object.AxID(), action_data.hit_test_event_to_fire,
ax::mojom::EventFrom::kAction, intents, action_data.request_id));
// The mojo message still needs a reply.
std::move(callback).Run(/*child_frame_hit_test_info=*/nullptr);
......
......@@ -92,6 +92,7 @@ include_rules = [
"+ui/accessibility/ax_enums.mojom-blink.h",
"+ui/accessibility/ax_enums.mojom-blink-forward.h",
"+ui/accessibility/ax_event.h",
"+ui/accessibility/ax_event_intent.h",
"+ui/base/cursor/cursor.h",
"+ui/base/ime/mojom/ime_types.mojom-blink.h",
"+ui/base/ime/mojom/ime_types.mojom-blink-forward.h",
......
......@@ -14,13 +14,21 @@ blink_core_sources("accessibility") {
"ax_object_cache.h",
"ax_object_cache_base.h",
"axid.h",
"blink_ax_event_intent.cc",
"blink_ax_event_intent.h",
"scoped_blink_ax_event_intent.cc",
"scoped_blink_ax_event_intent.h",
]
deps = [ "//ui/accessibility:ax_base" ]
}
blink_core_tests("unit_tests") {
sources = [ "apply_dark_mode_test.cc" ]
sources = [
"apply_dark_mode_test.cc",
"blink_ax_event_intent_test.cc",
"scoped_blink_ax_event_intent_test.cc",
]
deps = [ "//ui/accessibility:ax_base" ]
}
......@@ -31,8 +31,10 @@
#include "base/macros.h"
#include "third_party/blink/renderer/core/accessibility/axid.h"
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/platform/wtf/hash_counted_set.h"
namespace blink {
......@@ -48,6 +50,10 @@ class LocalFrameView;
class CORE_EXPORT AXObjectCache : public GarbageCollected<AXObjectCache> {
public:
using BlinkAXEventIntentsSet = HashCountedSet<const BlinkAXEventIntent,
BlinkAXEventIntentHash,
BlinkAXEventIntentHashTraits>;
static AXObjectCache* Create(Document&);
virtual ~AXObjectCache() = default;
......@@ -147,6 +153,14 @@ class CORE_EXPORT AXObjectCache : public GarbageCollected<AXObjectCache> {
// Static helper functions.
static bool IsInsideFocusableElementOrARIAWidget(const Node&);
protected:
friend class ScopedBlinkAXEventIntent;
FRIEND_TEST_ALL_PREFIXES(ScopedBlinkAXEventIntentTest, SingleIntent);
FRIEND_TEST_ALL_PREFIXES(ScopedBlinkAXEventIntentTest, NestedIntents);
FRIEND_TEST_ALL_PREFIXES(ScopedBlinkAXEventIntentTest, NestedSameIntents);
virtual BlinkAXEventIntentsSet& ActiveEventIntents() = 0;
private:
friend class AXObjectCacheBase;
AXObjectCache() = default;
......@@ -157,4 +171,4 @@ class CORE_EXPORT AXObjectCache : public GarbageCollected<AXObjectCache> {
} // namespace blink
#endif
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_AX_OBJECT_CACHE_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include <limits>
#include "third_party/blink/renderer/platform/wtf/hash_functions.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
namespace blink {
// Creates an empty (uninitialized) instance.
BlinkAXEventIntent::BlinkAXEventIntent() = default;
BlinkAXEventIntent::BlinkAXEventIntent(
ax::mojom::blink::Command command,
ax::mojom::blink::TextBoundary text_boundary,
ax::mojom::blink::MoveDirection move_direction)
: intent_(command, text_boundary, move_direction), is_initialized_(true) {}
BlinkAXEventIntent::BlinkAXEventIntent(WTF::HashTableDeletedValueType type)
: is_initialized_(true), is_deleted_(true) {}
BlinkAXEventIntent::~BlinkAXEventIntent() = default;
BlinkAXEventIntent::BlinkAXEventIntent(const BlinkAXEventIntent& intent) =
default;
BlinkAXEventIntent& BlinkAXEventIntent::operator=(
const BlinkAXEventIntent& intent) = default;
bool operator==(const BlinkAXEventIntent& a, const BlinkAXEventIntent& b) {
return BlinkAXEventIntentHash::GetHash(a) ==
BlinkAXEventIntentHash::GetHash(b);
}
bool operator!=(const BlinkAXEventIntent& a, const BlinkAXEventIntent& b) {
return !(a == b);
}
bool BlinkAXEventIntent::IsHashTableDeletedValue() const {
return is_deleted_;
}
std::string BlinkAXEventIntent::ToString() const {
if (!is_initialized())
return "AXEventIntent(uninitialized)";
if (IsHashTableDeletedValue())
return "AXEventIntent(is_deleted)";
return intent().ToString();
}
// static
unsigned int BlinkAXEventIntentHash::GetHash(const BlinkAXEventIntent& key) {
// If the intent is uninitialized, it is not safe to rely on the memory being
// initialized to zero, because any uninitialized field that might be
// accidentally added in the future will produce a potentially non-zero memory
// value especially in the hard to control "intent_" member.
if (!key.is_initialized())
return 0u;
if (key.IsHashTableDeletedValue())
return std::numeric_limits<unsigned>::max();
unsigned hash = 0u;
WTF::AddIntToHash(hash, static_cast<const unsigned>(key.intent().command));
WTF::AddIntToHash(hash,
static_cast<const unsigned>(key.intent().text_boundary));
WTF::AddIntToHash(hash,
static_cast<const unsigned>(key.intent().move_direction));
return hash;
}
// static
bool BlinkAXEventIntentHash::Equal(const BlinkAXEventIntent& a,
const BlinkAXEventIntent& b) {
return a == b;
}
} // namespace blink
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_BLINK_AX_EVENT_INTENT_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_BLINK_AX_EVENT_INTENT_H_
#include <string>
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/wtf/hash_table_deleted_value_type.h"
#include "third_party/blink/renderer/platform/wtf/hash_traits.h"
#include "ui/accessibility/ax_enums.mojom-blink-forward.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_event_intent.h"
namespace blink {
// Adapts a ui::AXEventIntent for use in a Blink HashCountedSet.
//
// An event intent describes what caused an accessibility event to be raised.
// For example, a character could have been typed, a word replaced, or a line
// deleted. Or, the selection could have been extended to the beginning of the
// previous word, or it could have been moved to the end of the next line.
class CORE_EXPORT BlinkAXEventIntent final {
public:
BlinkAXEventIntent();
BlinkAXEventIntent(ax::mojom::blink::Command command,
ax::mojom::blink::TextBoundary text_boundary,
ax::mojom::blink::MoveDirection move_direction);
// Used by HashCountedSet to create a deleted BlinkAXEventIntent instance.
explicit BlinkAXEventIntent(WTF::HashTableDeletedValueType type);
virtual ~BlinkAXEventIntent();
BlinkAXEventIntent(const BlinkAXEventIntent& intent);
BlinkAXEventIntent& operator=(const BlinkAXEventIntent& intent);
CORE_EXPORT friend bool operator==(const BlinkAXEventIntent& a,
const BlinkAXEventIntent& b);
CORE_EXPORT friend bool operator!=(const BlinkAXEventIntent& a,
const BlinkAXEventIntent& b);
const ui::AXEventIntent& intent() const { return intent_; }
ui::AXEventIntent& intent() { return intent_; }
// Returns "true" if this represents an initialized BlinkAXEventIntent
// instance.
bool is_initialized() const { return is_initialized_; }
// Returns "true" if this represents a deleted BlinkAXEventIntent instance.
bool IsHashTableDeletedValue() const;
// Returns a string representation of this instance.
std::string ToString() const;
private:
ui::AXEventIntent intent_;
// Set to "true" if this represents an initialized BlinkAXEventIntent
// instance. An empty (uninitialized) instance is created either by calling
// the default constructor or by simply zeroing out a block of memory
// equivalent to the size of this class. The latter may be done by the HashSet
// for performance reasons.
//
// This member is needed so that our hash function will never return the same
// value for an uninitialized and an initialized instance. Otherwise an
// uninitialized instance's memory may have random values.
bool is_initialized_ = false;
// Set to "true" if this represents a deleted BlinkAXEventIntent instance.
bool is_deleted_ = false;
};
struct CORE_EXPORT BlinkAXEventIntentHash final {
// Computes the hash of a BlinkAXEventIntent instance.
static unsigned GetHash(const BlinkAXEventIntent& key);
// Used by HashSet to compare two BlinkAXEventIntent instances.
static bool Equal(const BlinkAXEventIntent& a, const BlinkAXEventIntent& b);
// We support creating and comparing with empty (uninitialized) and deleted
// HashSet BlinkAXEventIntent entries.
static constexpr bool safe_to_compare_to_empty_or_deleted = true;
};
struct CORE_EXPORT BlinkAXEventIntentHashTraits final
: WTF::SimpleClassHashTraits<BlinkAXEventIntent> {
// Zeroed memory cannot be used for BlinkAXEventIntent.
static constexpr bool kEmptyValueIsZero = false;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_BLINK_AX_EVENT_INTENT_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/platform/wtf/hash_table_deleted_value_type.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
namespace blink {
namespace test {
TEST(BlinkAXEventIntentTest, Equality) {
BlinkAXEventIntent intent1(ax::mojom::blink::Command::kCut,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward);
BlinkAXEventIntent intent2(ax::mojom::blink::Command::kSetSelection,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward);
BlinkAXEventIntent intent3(ax::mojom::blink::Command::kSetSelection,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward);
EXPECT_NE(BlinkAXEventIntentHash::GetHash(intent1),
BlinkAXEventIntentHash::GetHash(intent2));
EXPECT_NE(BlinkAXEventIntentHash::GetHash(intent1),
BlinkAXEventIntentHash::GetHash(intent3));
EXPECT_EQ(BlinkAXEventIntentHash::GetHash(intent2),
BlinkAXEventIntentHash::GetHash(intent3));
EXPECT_FALSE(BlinkAXEventIntentHash::Equal(intent1, intent2));
EXPECT_FALSE(BlinkAXEventIntentHash::Equal(intent1, intent3));
EXPECT_TRUE(BlinkAXEventIntentHash::Equal(intent2, intent3));
}
TEST(BlinkAXEventIntentTest, EqualityWithEmptyValue) {
BlinkAXEventIntent intent1(ax::mojom::blink::Command::kCut,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward);
// Empty values.
BlinkAXEventIntent intent2;
BlinkAXEventIntent intent3;
EXPECT_NE(BlinkAXEventIntentHash::GetHash(intent1),
BlinkAXEventIntentHash::GetHash(intent2));
EXPECT_FALSE(BlinkAXEventIntentHash::Equal(intent1, intent2));
EXPECT_EQ(BlinkAXEventIntentHash::GetHash(intent2),
BlinkAXEventIntentHash::GetHash(intent3));
EXPECT_TRUE(BlinkAXEventIntentHash::Equal(intent2, intent3));
}
TEST(BlinkAXEventIntentTest, EqualityWithDeletedValue) {
BlinkAXEventIntent intent1(ax::mojom::blink::Command::kCut,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward);
BlinkAXEventIntent intent2(WTF::kHashTableDeletedValue);
BlinkAXEventIntent intent3(WTF::kHashTableDeletedValue);
EXPECT_NE(BlinkAXEventIntentHash::GetHash(intent1),
BlinkAXEventIntentHash::GetHash(intent2));
EXPECT_FALSE(BlinkAXEventIntentHash::Equal(intent1, intent2));
EXPECT_EQ(BlinkAXEventIntentHash::GetHash(intent2),
BlinkAXEventIntentHash::GetHash(intent3));
EXPECT_TRUE(BlinkAXEventIntentHash::Equal(intent2, intent3));
}
} // namespace test
} // namespace blink
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/accessibility/scoped_blink_ax_event_intent.h"
#include "base/macros.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
namespace blink {
ScopedBlinkAXEventIntent::ScopedBlinkAXEventIntent(
const BlinkAXEventIntent& intent,
Document* document)
: intent_(intent), document_(document) {
DCHECK(document_);
DCHECK(document_->IsActive());
if (AXObjectCache* cache = document_->ExistingAXObjectCache()) {
AXObjectCache::BlinkAXEventIntentsSet& active_intents =
cache->ActiveEventIntents();
active_intents.insert(intent);
}
}
ScopedBlinkAXEventIntent::~ScopedBlinkAXEventIntent() {
// If a conservative GC is required, |document_| may become nullptr.
if (!document_ || !document_->IsActive())
return;
if (AXObjectCache* cache = document_->ExistingAXObjectCache()) {
AXObjectCache::BlinkAXEventIntentsSet& active_intents =
cache->ActiveEventIntents();
DCHECK(active_intents.Contains(intent_));
active_intents.erase(intent_);
}
}
} // namespace blink
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_SCOPED_BLINK_AX_EVENT_INTENT_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_SCOPED_BLINK_AX_EVENT_INTENT_H_
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/heap/persistent.h"
namespace blink {
// Annotates all accessibility events that are raised while an instance of this
// class is alive with a specific intent. Multiple instances with different
// intents could be alive at the same time.
//
// An event intent is a description of what caused an accessibility event and
// may include user actions such as paste, or page actions such as a text
// insertion. Before firing a bunch of accessibility events that have the same
// cause, create an instance of ScopedBlinkAXEventIntent. Multiple event intents
// could be in effect at the same time, such as "selection move" and "text
// insertion". As long as an instance of ScopedBlinkAXEventIntent is alive, all
// accessibility events that are raised will be annotated with the same intent
// or intents.
class CORE_EXPORT ScopedBlinkAXEventIntent final {
STACK_ALLOCATED();
public:
ScopedBlinkAXEventIntent(const BlinkAXEventIntent& intent,
Document* document);
virtual ~ScopedBlinkAXEventIntent();
ScopedBlinkAXEventIntent(const ScopedBlinkAXEventIntent& intent) = delete;
ScopedBlinkAXEventIntent& operator=(const ScopedBlinkAXEventIntent& intent) =
delete;
const BlinkAXEventIntent& intent() const { return intent_; }
private:
BlinkAXEventIntent intent_;
// This class is stack allocated, and therefore no WeakPersistent handle is
// required for |document_| and no trace method is necessary.
Document* const document_;
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_ACCESSIBILITY_SCOPED_BLINK_AX_EVENT_INTENT_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/accessibility/scoped_blink_ax_event_intent.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/core/accessibility/ax_context.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "ui/accessibility/ax_enums.mojom-blink.h"
namespace blink {
using ScopedBlinkAXEventIntentTest = RenderingTest;
TEST_F(ScopedBlinkAXEventIntentTest, SingleIntent) {
AXContext ax_context(GetDocument());
AXObjectCache* cache = GetDocument().ExistingAXObjectCache();
ASSERT_NE(nullptr, cache);
{
ScopedBlinkAXEventIntent scoped_intent(
{ax::mojom::blink::Command::kCut,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward},
&GetDocument());
EXPECT_TRUE(cache->ActiveEventIntents().Contains(scoped_intent.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().size());
}
EXPECT_TRUE(cache->ActiveEventIntents().IsEmpty());
}
TEST_F(ScopedBlinkAXEventIntentTest, NestedIntents) {
AXContext ax_context(GetDocument());
AXObjectCache* cache = GetDocument().ExistingAXObjectCache();
ASSERT_NE(nullptr, cache);
{
ScopedBlinkAXEventIntent scoped_intent1(
{ax::mojom::blink::Command::kType,
ax::mojom::blink::TextBoundary::kCharacter,
ax::mojom::blink::MoveDirection::kForward},
&GetDocument());
{
ScopedBlinkAXEventIntent scoped_intent2(
{ax::mojom::blink::Command::kCut,
ax::mojom::blink::TextBoundary::kWordEnd,
ax::mojom::blink::MoveDirection::kForward},
&GetDocument());
EXPECT_TRUE(
cache->ActiveEventIntents().Contains(scoped_intent1.intent()));
EXPECT_TRUE(
cache->ActiveEventIntents().Contains(scoped_intent2.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().count(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().count(scoped_intent2.intent()));
EXPECT_EQ(2u, cache->ActiveEventIntents().size());
}
EXPECT_TRUE(cache->ActiveEventIntents().Contains(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().count(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().size());
}
EXPECT_TRUE(cache->ActiveEventIntents().IsEmpty());
}
TEST_F(ScopedBlinkAXEventIntentTest, NestedSameIntents) {
AXContext ax_context(GetDocument());
AXObjectCache* cache = GetDocument().ExistingAXObjectCache();
ASSERT_NE(nullptr, cache);
{
ScopedBlinkAXEventIntent scoped_intent1(
{ax::mojom::blink::Command::kType,
ax::mojom::blink::TextBoundary::kCharacter,
ax::mojom::blink::MoveDirection::kForward},
&GetDocument());
{
// Create a second, identical intent.
ScopedBlinkAXEventIntent scoped_intent2(
{ax::mojom::blink::Command::kType,
ax::mojom::blink::TextBoundary::kCharacter,
ax::mojom::blink::MoveDirection::kForward},
&GetDocument());
EXPECT_TRUE(
cache->ActiveEventIntents().Contains(scoped_intent1.intent()));
EXPECT_EQ(2u, cache->ActiveEventIntents().count(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().size());
}
EXPECT_TRUE(cache->ActiveEventIntents().Contains(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().count(scoped_intent1.intent()));
EXPECT_EQ(1u, cache->ActiveEventIntents().size());
}
EXPECT_TRUE(cache->ActiveEventIntents().IsEmpty());
}
} // namespace blink
......@@ -1083,13 +1083,14 @@ void AXObjectCacheImpl::PostNotifications(Document& document) {
ax::mojom::blink::Event event_type = params->event_type;
ax::mojom::blink::EventFrom event_from = params->event_from;
const BlinkAXEventIntentsSet& event_intents = params->event_intents;
if (obj->GetDocument() != &document) {
notifications_to_post_.push_back(
MakeGarbageCollected<AXEventParams>(obj, event_type, event_from));
notifications_to_post_.push_back(MakeGarbageCollected<AXEventParams>(
obj, event_type, event_from, event_intents));
continue;
}
FireAXEventImmediately(obj, event_type, event_from);
FireAXEventImmediately(obj, event_type, event_from, event_intents);
}
}
......@@ -1119,12 +1120,13 @@ void AXObjectCacheImpl::PostNotification(AXObject* object,
// immediately rather than deferring them.
if (object->GetDocument()->Lifecycle().GetState() ==
DocumentLifecycle::kInAccessibility) {
FireAXEventImmediately(object, event_type, ComputeEventFrom());
FireAXEventImmediately(object, event_type, ComputeEventFrom(),
ActiveEventIntents());
return;
}
notifications_to_post_.push_back(MakeGarbageCollected<AXEventParams>(
object, event_type, ComputeEventFrom()));
object, event_type, ComputeEventFrom(), ActiveEventIntents()));
// These events are fired during DocumentLifecycle::kInAccessibility,
// ensure there is a visual update scheduled.
......@@ -1169,7 +1171,8 @@ void AXObjectCacheImpl::FireTreeUpdatedEventImmediately(
void AXObjectCacheImpl::FireAXEventImmediately(
AXObject* obj,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from) {
ax::mojom::blink::EventFrom event_from,
const BlinkAXEventIntentsSet& event_intents) {
DCHECK_EQ(obj->GetDocument()->Lifecycle().GetState(),
DocumentLifecycle::kInAccessibility);
......@@ -1186,7 +1189,7 @@ void AXObjectCacheImpl::FireAXEventImmediately(
SCOPED_DISALLOW_LIFECYCLE_TRANSITION(*obj->GetDocument());
#endif // DCHECK_IS_ON()
PostPlatformNotification(obj, event_type, event_from);
PostPlatformNotification(obj, event_type, event_from, event_intents);
if (event_type == ax::mojom::blink::Event::kChildrenChanged &&
obj->CachedParentObject()) {
......@@ -1723,7 +1726,8 @@ bool IsNodeAriaVisible(Node* node) {
void AXObjectCacheImpl::PostPlatformNotification(
AXObject* obj,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from) {
ax::mojom::blink::EventFrom event_from,
const BlinkAXEventIntentsSet& event_intents) {
if (!document_ || !document_->View() ||
!document_->View()->GetFrame().GetPage()) {
return;
......@@ -1736,7 +1740,11 @@ void AXObjectCacheImpl::PostPlatformNotification(
event.id = obj->AXObjectID();
event.event_type = event_type;
event.event_from = event_from;
event.action_request_id = -1;
event.event_intents.resize(event_intents.size());
// We need to filter out the counts from every intent.
std::transform(event_intents.begin(), event_intents.end(),
event.event_intents.begin(),
[](const auto& intent) { return intent.key.intent(); });
web_frame->Client()->PostAccessibilityEvent(event);
}
......
......@@ -39,6 +39,7 @@
#include "third_party/blink/public/mojom/permissions/permission_status.mojom-blink.h"
#include "third_party/blink/public/web/web_ax_enums.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache_base.h"
#include "third_party/blink/renderer/core/accessibility/blink_ax_event_intent.h"
#include "third_party/blink/renderer/core/execution_context/execution_context_lifecycle_observer.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
......@@ -269,12 +270,19 @@ class MODULES_EXPORT AXObjectCacheImpl
void SetAutofillState(AXID id, WebAXAutofillState state);
protected:
void PostPlatformNotification(AXObject* obj,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from =
ax::mojom::blink::EventFrom::kNone);
void PostPlatformNotification(
AXObject* obj,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from =
ax::mojom::blink::EventFrom::kNone,
const BlinkAXEventIntentsSet& event_intents = BlinkAXEventIntentsSet());
void LabelChangedWithCleanLayout(Element*);
// Returns a reference to the set of currently active event intents.
BlinkAXEventIntentsSet& ActiveEventIntents() override {
return active_event_intents_;
}
AXObject* CreateFromRenderer(LayoutObject*);
AXObject* CreateFromNode(Node*);
AXObject* CreateFromInlineTextBox(AbstractInlineTextBox*);
......@@ -283,11 +291,15 @@ class MODULES_EXPORT AXObjectCacheImpl
struct AXEventParams final : public GarbageCollected<AXEventParams> {
AXEventParams(AXObject* target,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from)
: target(target), event_type(event_type), event_from(event_from) {}
ax::mojom::blink::EventFrom event_from,
const BlinkAXEventIntentsSet& intents)
: target(target), event_type(event_type), event_from(event_from) {
std::copy(intents.begin(), intents.end(), event_intents.begin());
}
Member<AXObject> target;
ax::mojom::blink::Event event_type;
ax::mojom::blink::EventFrom event_from;
BlinkAXEventIntentsSet event_intents;
void Trace(Visitor* visitor) { visitor->Trace(target); }
};
......@@ -396,7 +408,8 @@ class MODULES_EXPORT AXObjectCacheImpl
ax::mojom::blink::EventFrom event_from);
void FireAXEventImmediately(AXObject* obj,
ax::mojom::blink::Event event_type,
ax::mojom::blink::EventFrom event_from);
ax::mojom::blink::EventFrom event_from,
const BlinkAXEventIntentsSet& event_intents);
// Whether the user has granted permission for the user to install event
// listeners for accessibility events using the AOM.
......@@ -417,6 +430,9 @@ class MODULES_EXPORT AXObjectCacheImpl
// Maps ids to their object's autofill state.
HashMap<AXID, WebAXAutofillState> autofill_state_map_;
// A set of currently active event intents.
BlinkAXEventIntentsSet active_event_intents_;
DISALLOW_COPY_AND_ASSIGN(AXObjectCacheImpl);
};
......
......@@ -440,6 +440,8 @@ _CONFIG = [
# Accessibility base types and the non-Blink enums they
# depend on.
'ui::AXEvent',
'ui::AXEventIntent',
'ui::AXNodeData',
'ax::mojom::BoolAttribute',
'ax::mojom::HasPopup',
......
......@@ -46,6 +46,8 @@ jumbo_component("ax_base") {
"ax_enum_util.h",
"ax_event.cc",
"ax_event.h",
"ax_event_intent.cc",
"ax_event_intent.h",
"ax_node_data.cc",
"ax_node_data.h",
"ax_node_text_styles.cc",
......@@ -290,6 +292,7 @@ test("accessibility_unittests") {
"ax_tree_source_checker_unittest.cc",
"ax_tree_unittest.cc",
"mojom/ax_action_data_mojom_traits_unittest.cc",
"mojom/ax_event_intent_mojom_traits_unittest.cc",
"mojom/ax_event_mojom_traits_unittest.cc",
"mojom/ax_node_data_mojom_traits_unittest.cc",
"mojom/ax_relative_bounds_mojom_traits_unittest.cc",
......
......@@ -11,13 +11,15 @@ namespace ui {
AXEvent::AXEvent() = default;
AXEvent::AXEvent(int32_t id,
AXEvent::AXEvent(AXNodeData::AXID id,
ax::mojom::Event event_type,
ax::mojom::EventFrom event_from,
const std::vector<AXEventIntent>& event_intents,
int action_request_id)
: id(id),
event_type(event_type),
event_from(event_from),
event_intents(event_intents),
action_request_id(action_request_id) {}
AXEvent::~AXEvent() = default;
......@@ -33,6 +35,13 @@ std::string AXEvent::ToString() const {
result += " on node id=" + base::NumberToString(id);
if (event_from != ax::mojom::EventFrom::kNone)
result += std::string(" from ") + ui::ToString(event_from);
if (!event_intents.empty()) {
result += " caused by [ ";
for (const AXEventIntent& intent : event_intents) {
result += intent.ToString() + ' ';
}
result += ']';
}
if (action_request_id)
result += " action_request_id=" + base::NumberToString(action_request_id);
return result;
......
......@@ -10,15 +10,17 @@
#include "ui/accessibility/ax_base_export.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_node_data.h"
namespace ui {
struct AX_BASE_EXPORT AXEvent {
struct AX_BASE_EXPORT AXEvent final {
AXEvent();
AXEvent(AXNodeData::AXID id,
ax::mojom::Event event_type,
ax::mojom::EventFrom event_from = ax::mojom::EventFrom::kNone,
const std::vector<AXEventIntent>& event_intents = {},
int action_request_id = -1);
virtual ~AXEvent();
AXEvent(const AXEvent& event);
......@@ -33,6 +35,13 @@ struct AX_BASE_EXPORT AXEvent {
// The source of the event.
ax::mojom::EventFrom event_from = ax::mojom::EventFrom::kNone;
// Describes what caused an accessibility event to be raised. For example, in
// the case of a selection changed event, the selection could have been
// extended to the beginning of the previous word, or it could have been moved
// to the end of the next line. Note that there could be multiple causes that
// resulted in an event.
std::vector<AXEventIntent> event_intents;
// The action request ID that was passed in if this event was fired in
// direct response to a ax::mojom::Action.
int action_request_id = -1;
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/ax_enum_util.h"
namespace ui {
AXEventIntent::AXEventIntent() = default;
AXEventIntent::AXEventIntent(ax::mojom::Command command,
ax::mojom::TextBoundary text_boundary,
ax::mojom::MoveDirection move_direction)
: command(command),
text_boundary(text_boundary),
move_direction(move_direction) {}
AXEventIntent::~AXEventIntent() = default;
AXEventIntent::AXEventIntent(const AXEventIntent& intent) = default;
AXEventIntent& AXEventIntent::operator=(const AXEventIntent& intent) = default;
bool operator==(const AXEventIntent& a, const AXEventIntent& b) {
return a.command == b.command && a.text_boundary == b.text_boundary &&
a.move_direction == b.move_direction;
}
bool operator!=(const AXEventIntent& a, const AXEventIntent& b) {
return !(a == b);
}
std::string AXEventIntent::ToString() const {
return std::string("AXEventIntent(") + ui::ToString(command) + ',' +
ui::ToString(text_boundary) + ',' + ui::ToString(move_direction) + ')';
}
} // namespace ui
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_ACCESSIBILITY_AX_EVENT_INTENT_H_
#define UI_ACCESSIBILITY_AX_EVENT_INTENT_H_
#include <string>
#include "ui/accessibility/ax_base_export.h"
#include "ui/accessibility/ax_enums.mojom.h"
namespace ui {
// Describes what caused an accessibility event to be raised. For example, a
// character could have been typed, a word replaced, or a line deleted. Or, the
// selection could have been extended to the beginning of the previous word, or
// it could have been moved to the end of the next line.
struct AX_BASE_EXPORT AXEventIntent final {
AXEventIntent();
AXEventIntent(ax::mojom::Command command,
ax::mojom::TextBoundary text_boundary,
ax::mojom::MoveDirection move_direction);
virtual ~AXEventIntent();
AXEventIntent(const AXEventIntent& intent);
AXEventIntent& operator=(const AXEventIntent& intent);
friend AX_BASE_EXPORT bool operator==(const AXEventIntent& a,
const AXEventIntent& b);
friend AX_BASE_EXPORT bool operator!=(const AXEventIntent& a,
const AXEventIntent& b);
ax::mojom::Command command = ax::mojom::Command::kType;
// TODO(nektar): Split TextBoundary into TextUnit and TextBoundary.
ax::mojom::TextBoundary text_boundary = ax::mojom::TextBoundary::kCharacter;
ax::mojom::MoveDirection move_direction = ax::mojom::MoveDirection::kForward;
// Returns a string representation of this data, for debugging.
std::string ToString() const;
};
} // namespace ui
#endif // UI_ACCESSIBILITY_AX_EVENT_INTENT_H_
......@@ -9,6 +9,7 @@ mojom("mojom") {
"ax_action_data.mojom",
"ax_assistant_structure.mojom",
"ax_event.mojom",
"ax_event_intent.mojom",
"ax_node_data.mojom",
"ax_relative_bounds.mojom",
"ax_tree_data.mojom",
......
......@@ -5,11 +5,13 @@
module ax.mojom;
import "ui/accessibility/ax_enums.mojom";
import "ui/accessibility/mojom/ax_event_intent.mojom";
// See ui::AXEvent for documentation.
struct AXEvent {
Event event_type;
int32 id;
EventFrom event_from;
array<EventIntent> event_intents;
int32 action_request_id;
};
......@@ -9,7 +9,5 @@ sources = [
"ax_event_mojom_traits.cc",
"ax_event_mojom_traits.h",
]
public_deps = [
"//ui/accessibility",
]
public_deps = [ "//ui/accessibility" ]
type_mappings = [ "ax.mojom.AXEvent=::ui::AXEvent" ]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
module ax.mojom;
import "ui/accessibility/ax_enums.mojom";
// See ui::AXEventIntent for documentation.
struct EventIntent {
Command command;
TextBoundary text_boundary;
MoveDirection move_direction;
};
# Copyright 2020 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
mojom = "//ui/accessibility/mojom/ax_event_intent.mojom"
public_headers = [ "//ui/accessibility/ax_event_intent.h" ]
traits_headers = [ "//ui/accessibility/mojom/ax_event_Intent_mojom_traits.h" ]
sources = [
"ax_event_intent_mojom_traits.cc",
"ax_event_intent_mojom_traits.h",
]
public_deps = [ "//ui/accessibility" ]
type_mappings = [ "ax.mojom.AXEventIntent=::ui::AXEventIntent" ]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/mojom/ax_event_intent_mojom_traits.h"
namespace mojo {
// static
bool StructTraits<ax::mojom::EventIntentDataView, ui::AXEventIntent>::Read(
ax::mojom::EventIntentDataView data,
ui::AXEventIntent* out) {
out->command = data.command();
out->text_boundary = data.text_boundary();
out->move_direction = data.move_direction();
return true;
}
} // namespace mojo
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef UI_ACCESSIBILITY_MOJOM_AX_EVENT_INTENT_MOJOM_TRAITS_H_
#define UI_ACCESSIBILITY_MOJOM_AX_EVENT_INTENT_MOJOM_TRAITS_H_
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/mojom/ax_event_intent.mojom.h"
namespace mojo {
template <>
struct StructTraits<ax::mojom::EventIntentDataView, ui::AXEventIntent> {
static ax::mojom::Command command(const ui::AXEventIntent& p) {
return p.command;
}
static ax::mojom::TextBoundary text_boundary(const ui::AXEventIntent& p) {
return p.text_boundary;
}
static ax::mojom::MoveDirection move_direction(const ui::AXEventIntent& p) {
return p.move_direction;
}
static bool Read(ax::mojom::EventIntentDataView data, ui::AXEventIntent* out);
};
} // namespace mojo
#endif // UI_ACCESSIBILITY_MOJOM_AX_EVENT_INTENT_MOJOM_TRAITS_H_
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/mojom/ax_event_intent_mojom_traits.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/mojom/ax_event_intent.mojom.h"
using mojo::test::SerializeAndDeserialize;
TEST(AXEventIntentMojomTraitsTest, RoundTrip) {
ui::AXEventIntent input;
input.command = ax::mojom::Command::kCut;
input.text_boundary = ax::mojom::TextBoundary::kWordEnd;
input.move_direction = ax::mojom::MoveDirection::kForward;
ui::AXEventIntent output;
EXPECT_TRUE(SerializeAndDeserialize<ax::mojom::EventIntent>(&input, &output));
EXPECT_EQ(ax::mojom::Command::kCut, output.command);
EXPECT_EQ(ax::mojom::TextBoundary::kWordEnd, output.text_boundary);
EXPECT_EQ(ax::mojom::MoveDirection::kForward, output.move_direction);
}
......@@ -14,7 +14,7 @@ bool StructTraits<ax::mojom::AXEventDataView, ui::AXEvent>::Read(
out->id = data.id();
out->event_from = data.event_from();
out->action_request_id = data.action_request_id();
return true;
return data.ReadEventIntents(&out->event_intents);
}
} // namespace mojo
......@@ -5,8 +5,13 @@
#ifndef UI_ACCESSIBILITY_MOJOM_AX_EVENT_MOJOM_TRAITS_H_
#define UI_ACCESSIBILITY_MOJOM_AX_EVENT_MOJOM_TRAITS_H_
#include <vector>
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/mojom/ax_event.mojom-shared.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/mojom/ax_event.mojom.h"
#include "ui/accessibility/mojom/ax_event_intent.mojom.h"
#include "ui/accessibility/mojom/ax_event_intent_mojom_traits.h"
namespace mojo {
......@@ -19,6 +24,9 @@ struct StructTraits<ax::mojom::AXEventDataView, ui::AXEvent> {
static ax::mojom::EventFrom event_from(const ui::AXEvent& p) {
return p.event_from;
}
static std::vector<ui::AXEventIntent> event_intents(const ui::AXEvent& p) {
return p.event_intents;
}
static int32_t action_request_id(const ui::AXEvent& p) {
return p.action_request_id;
}
......
......@@ -4,23 +4,37 @@
#include "ui/accessibility/mojom/ax_event_mojom_traits.h"
#include <vector>
#include "mojo/public/cpp/test_support/test_utils.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_event.h"
#include "ui/accessibility/ax_event_intent.h"
#include "ui/accessibility/mojom/ax_event.mojom.h"
#include "ui/accessibility/mojom/ax_event_intent.mojom.h"
using mojo::test::SerializeAndDeserialize;
TEST(AXEventMojomTraitsTest, RoundTrip) {
ui::AXEvent input;
input.event_type = ax::mojom::Event::kFocus;
input.event_type = ax::mojom::Event::kTextChanged;
input.id = 111;
input.event_from = ax::mojom::EventFrom::kUser;
ui::AXEventIntent cut_intent;
cut_intent.command = ax::mojom::Command::kCut;
cut_intent.text_boundary = ax::mojom::TextBoundary::kWordEnd;
cut_intent.move_direction = ax::mojom::MoveDirection::kForward;
const std::vector<ui::AXEventIntent> event_intents{cut_intent};
input.event_intents = event_intents;
input.action_request_id = 222;
ui::AXEvent output;
EXPECT_TRUE(SerializeAndDeserialize<ax::mojom::AXEvent>(&input, &output));
EXPECT_EQ(ax::mojom::Event::kFocus, output.event_type);
EXPECT_EQ(ax::mojom::Event::kTextChanged, output.event_type);
EXPECT_EQ(111, output.id);
EXPECT_EQ(ax::mojom::EventFrom::kUser, output.event_from);
EXPECT_THAT(output.event_intents, testing::ContainerEq(event_intents));
EXPECT_EQ(222, output.action_request_id);
}
......@@ -6,6 +6,7 @@ typemaps = [
"//ui/accessibility/mojom/ax_action_data.typemap",
"//ui/accessibility/mojom/ax_assistant_structure.typemap",
"//ui/accessibility/mojom/ax_event.typemap",
"//ui/accessibility/mojom/ax_event_intent.typemap",
"//ui/accessibility/mojom/ax_node_data.typemap",
"//ui/accessibility/mojom/ax_relative_bounds.typemap",
"//ui/accessibility/mojom/ax_tree_data.typemap",
......
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