Commit 75373457 authored by Brian Liu Xu's avatar Brian Liu Xu Committed by Commit Bot

Add support for kIgnored in AXVirtualView

Implements support for |kIgnored| in |AXVirtualView| so that elements with role or state |kIgnored| are hidden from the accessibility tree without affecting their contents. When an element has |kIgnored| set, that means that its accessible node should not be exposed to the platform. Thus, the accessibility tree shows its descendants instead of the ignored node.

|kIgnored| should not be confused with |kInvisible|. Invisible nodes can still appear in the accessibility tree, and the invisible state is inherited by descendant nodes; they are hidden in the sense that a special state is reported to screen readers that understand it. In contrast, ignored nodes do not appear in the accessibility tree, and the state is not inherited; they are hidden in the sense that they are not exposed at all to the platform.

This change is a prerequisite to eliminating the hidden root node of |TreeView| so that screen reader users can access descendant nodes using the review cursor. |TreeView| currently uses |kInvisible| to hide the root node, but that results in the entire subtree becoming invisible to screen reader users. We plan to make a subsequent change to move the root node from |kInvisible| to |kIgnored|.

Bug: 811277
Change-Id: I9f30c6cc479c64919d1a3c4397a9455d4f1f1988
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2103714
Commit-Queue: Nektarios Paisios <nektar@chromium.org>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#750951}
parent f71e417d
......@@ -6843,7 +6843,7 @@ bool AXPlatformNodeWin::IsUIAControl() const {
const ui::AXNodeData& data = GetData();
return !((IsReadOnlySupported(data.role) && data.IsReadOnlyOrDisabled()) ||
data.HasState(ax::mojom::State::kInvisible) ||
data.role == ax::mojom::Role::kIgnored);
(data.IsIgnored() && !data.HasState(ax::mojom::State::kFocusable)));
}
base::Optional<LONG> AXPlatformNodeWin::ComputeUIALandmarkType() const {
......
......@@ -68,7 +68,7 @@ void AXVirtualView::AddChildView(std::unique_ptr<AXVirtualView> view) {
DCHECK(view);
if (view->virtual_parent_view_ == this)
return; // Already a child of this virtual view.
AddChildViewAt(std::move(view), GetChildCount());
AddChildViewAt(std::move(view), int{children_.size()});
}
void AXVirtualView::AddChildViewAt(std::unique_ptr<AXVirtualView> view,
......@@ -82,7 +82,7 @@ void AXVirtualView::AddChildViewAt(std::unique_ptr<AXVirtualView> view,
"AXVirtualView parent. Call "
"RemoveChildView first.";
DCHECK_GE(index, 0);
DCHECK_LE(index, GetChildCount());
DCHECK_LE(index, int{children_.size()});
view->virtual_parent_view_ = this;
children_.insert(children_.begin() + index, std::move(view));
......@@ -94,10 +94,10 @@ void AXVirtualView::AddChildViewAt(std::unique_ptr<AXVirtualView> view,
void AXVirtualView::ReorderChildView(AXVirtualView* view, int index) {
DCHECK(view);
if (index >= GetChildCount())
if (index >= int{children_.size()})
return;
if (index < 0)
index = GetChildCount() - 1;
index = int{children_.size()} - 1;
DCHECK_EQ(view->virtual_parent_view_, this);
if (children_[index].get() == view)
......@@ -240,15 +240,36 @@ const ui::AXNodeData& AXVirtualView::GetData() const {
}
int AXVirtualView::GetChildCount() {
return static_cast<int>(children_.size());
int count = 0;
for (const std::unique_ptr<AXVirtualView>& child : children_) {
if (child->IsIgnored()) {
count += child->GetChildCount();
continue;
}
count++;
}
return count;
}
gfx::NativeViewAccessible AXVirtualView::ChildAtIndex(int index) {
DCHECK_GE(index, 0) << "Child indices should be greater or equal to 0.";
DCHECK_LT(index, GetChildCount())
<< "Child indices should be less than the child count.";
if (index >= 0 && index < GetChildCount())
return children_[index]->GetNativeObject();
int i = 0;
for (const std::unique_ptr<AXVirtualView>& child : children_) {
if (child->IsIgnored()) {
if (index - i < child->GetChildCount()) {
gfx::NativeViewAccessible result = child->ChildAtIndex(index - i);
if (result)
return result;
}
i += child->GetChildCount();
continue;
}
if (i == index)
return child->GetNativeObject();
i++;
}
return nullptr;
}
......@@ -267,8 +288,11 @@ gfx::NativeViewAccessible AXVirtualView::GetParent() {
if (parent_view_)
return parent_view_->GetNativeObject();
if (virtual_parent_view_)
if (virtual_parent_view_) {
if (virtual_parent_view_->IsIgnored())
return virtual_parent_view_->GetParent();
return virtual_parent_view_->GetNativeObject();
}
// This virtual view hasn't been added to a parent view yet.
return nullptr;
......@@ -291,8 +315,21 @@ gfx::Rect AXVirtualView::GetBoundsRect(
}
gfx::NativeViewAccessible AXVirtualView::HitTestSync(int x, int y) {
// TODO(nektar): Implement.
return GetNativeObject();
if (custom_data_.relative_bounds.bounds.Contains(static_cast<float>(x),
static_cast<float>(y))) {
if (!IsIgnored())
return GetNativeObject();
}
// Check if the point is within any of the virtual children of this view.
// AXVirtualView's HitTestSync is a recursive function that will return the
// deepest child, since it does not support relative bounds.
for (const std::unique_ptr<AXVirtualView>& child : children_) {
gfx::NativeViewAccessible result = child->HitTestSync(x, y);
if (result)
return result;
}
return nullptr;
}
gfx::NativeViewAccessible AXVirtualView::GetFocus() {
......@@ -344,6 +381,18 @@ gfx::AcceleratedWidget AXVirtualView::GetTargetForNativeAccessibilityEvent() {
return gfx::kNullAcceleratedWidget;
}
bool AXVirtualView::IsIgnored() const {
const ui::AXNodeData& node_data = GetData();
// According to the ARIA spec, the node should not be ignored if it is
// focusable. This is to ensure that the focusable node is both understandable
// and operable.
if (node_data.HasState(ax::mojom::State::kFocusable))
return false;
return node_data.IsIgnored();
}
bool AXVirtualView::HandleAccessibleAction(
const ui::AXActionData& action_data) {
if (!GetOwnerView())
......
......@@ -126,8 +126,13 @@ class VIEWS_EXPORT AXVirtualView : public ui::AXPlatformNodeDelegateBase {
base::RepeatingCallback<void(ui::AXNodeData*)> callback);
void UnsetPopulateDataCallback();
// ui::AXPlatformNodeDelegate. Note that some of these functions have
// Mac-specific implementations in ax_virtual_view_mac.mm.
// ui::AXPlatformNodeDelegate. Note that
// - Some of these functions have Mac-specific implementations in
// ax_virtual_view_mac.mm.
// - GetChildCount(), ChildAtIndex(), and GetParent() are used by assistive
// technologies to access the unignored accessibility tree, which doesn't
// necessarily reflect the internal descendant tree. (An ignored node means
// that the node should not be exposed to the platform.)
const ui::AXNodeData& GetData() const override;
int GetChildCount() override;
gfx::NativeViewAccessible ChildAtIndex(int index) override;
......@@ -153,6 +158,10 @@ class VIEWS_EXPORT AXVirtualView : public ui::AXPlatformNodeDelegateBase {
// Gets or creates a wrapper suitable for use with tree sources.
AXVirtualViewWrapper* GetOrCreateWrapper(views::AXAuraObjCache* cache);
// Returns true if this node is ignored and should be hidden from the
// accessibility tree. This does not impact the node's descendants.
bool IsIgnored() const;
// Handle a request from assistive technology to perform an action on this
// virtual view. Returns true on success, but note that the success/failure is
// not propagated to the client that requested the action, since the
......
......@@ -365,6 +365,122 @@ TEST_F(AXVirtualViewTest, InvisibleVirtualViews) {
button_->SetVisible(true);
}
// Verify that ignored virtual views are removed from the accessible tree and
// that their contents are intact.
TEST_F(AXVirtualViewTest, IgnoredVirtualViews) {
ASSERT_EQ(0, virtual_label_->GetChildCount());
// An ignored node should not be exposed.
AXVirtualView* virtual_child_1 = new AXVirtualView;
virtual_label_->AddChildView(base::WrapUnique(virtual_child_1));
virtual_child_1->GetCustomData().AddState(ax::mojom::State::kIgnored);
ASSERT_EQ(0, virtual_label_->GetChildCount());
ASSERT_EQ(0, virtual_child_1->GetChildCount());
// The contents of ignored nodes should be exposed.
AXVirtualView* virtual_child_2 = new AXVirtualView;
virtual_child_1->AddChildView(base::WrapUnique(virtual_child_2));
AXVirtualView* virtual_child_3 = new AXVirtualView;
virtual_child_2->AddChildView(base::WrapUnique(virtual_child_3));
AXVirtualView* virtual_child_4 = new AXVirtualView;
virtual_child_2->AddChildView(base::WrapUnique(virtual_child_4));
ASSERT_EQ(1, virtual_label_->GetChildCount());
ASSERT_EQ(1, virtual_child_1->GetChildCount());
ASSERT_EQ(2, virtual_child_2->GetChildCount());
ASSERT_EQ(0, virtual_child_3->GetChildCount());
ASSERT_EQ(0, virtual_child_4->GetChildCount());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_1->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_2->GetParent());
EXPECT_EQ(virtual_child_2->GetNativeObject(), virtual_child_3->GetParent());
EXPECT_EQ(virtual_child_2->GetNativeObject(), virtual_child_4->GetParent());
EXPECT_EQ(virtual_child_2->GetNativeObject(),
virtual_label_->ChildAtIndex(0));
EXPECT_EQ(virtual_child_2->GetNativeObject(),
virtual_child_1->ChildAtIndex(0));
EXPECT_EQ(virtual_child_3->GetNativeObject(),
virtual_child_2->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
virtual_child_2->ChildAtIndex(1));
// The contents of ignored nodes should be unignored accessibility subtrees.
virtual_child_2->GetCustomData().role = ax::mojom::Role::kIgnored;
ASSERT_EQ(2, virtual_label_->GetChildCount());
ASSERT_EQ(2, virtual_child_1->GetChildCount());
ASSERT_EQ(2, virtual_child_2->GetChildCount());
ASSERT_EQ(0, virtual_child_3->GetChildCount());
ASSERT_EQ(0, virtual_child_4->GetChildCount());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_1->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_2->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_3->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_4->GetParent());
EXPECT_EQ(virtual_child_3->GetNativeObject(),
virtual_label_->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
virtual_label_->ChildAtIndex(1));
EXPECT_EQ(virtual_child_3->GetNativeObject(),
virtual_child_1->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
virtual_child_1->ChildAtIndex(1));
EXPECT_EQ(virtual_child_3->GetNativeObject(),
virtual_child_2->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
virtual_child_2->ChildAtIndex(1));
// Test for mixed ignored and unignored virtual children.
AXVirtualView* virtual_child_5 = new AXVirtualView;
virtual_child_1->AddChildView(base::WrapUnique(virtual_child_5));
ASSERT_EQ(3, virtual_label_->GetChildCount());
ASSERT_EQ(3, virtual_child_1->GetChildCount());
ASSERT_EQ(2, virtual_child_2->GetChildCount());
ASSERT_EQ(0, virtual_child_5->GetChildCount());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_1->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_2->GetParent());
EXPECT_EQ(virtual_label_->GetNativeObject(), virtual_child_5->GetParent());
EXPECT_EQ(virtual_child_3->GetNativeObject(),
virtual_label_->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
virtual_label_->ChildAtIndex(1));
EXPECT_EQ(virtual_child_5->GetNativeObject(),
virtual_label_->ChildAtIndex(2));
// An ignored root node should not be exposed.
virtual_label_->GetCustomData().AddState(ax::mojom::State::kIgnored);
ASSERT_EQ(3, GetButtonAccessibility()->GetChildCount());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_label_->GetParent());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_child_1->GetParent());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_child_2->GetParent());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_child_3->GetParent());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_child_4->GetParent());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_child_5->GetParent());
EXPECT_EQ(virtual_child_3->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(0));
EXPECT_EQ(virtual_child_4->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(1));
EXPECT_EQ(virtual_child_5->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(2));
// Test for mixed ignored and unignored root nodes.
AXVirtualView* virtual_label_2 = new AXVirtualView;
virtual_label_2->GetCustomData().role = ax::mojom::Role::kStaticText;
virtual_label_2->GetCustomData().SetName("Label");
button_->GetViewAccessibility().AddVirtualChildView(
base::WrapUnique(virtual_label_2));
ASSERT_EQ(4, GetButtonAccessibility()->GetChildCount());
ASSERT_EQ(0, virtual_label_2->GetChildCount());
EXPECT_EQ(button_->GetNativeViewAccessible(), virtual_label_2->GetParent());
EXPECT_EQ(virtual_label_2->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(3));
// A focusable node should not be ignored.
virtual_child_1->GetCustomData().AddState(ax::mojom::State::kFocusable);
ASSERT_EQ(2, GetButtonAccessibility()->GetChildCount());
ASSERT_EQ(1, virtual_label_->GetChildCount());
EXPECT_EQ(virtual_child_1->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(0));
EXPECT_EQ(virtual_label_2->GetNativeObject(),
GetButtonAccessibility()->ChildAtIndex(1));
}
TEST_F(AXVirtualViewTest, OverrideFocus) {
ViewAccessibility& button_accessibility = button_->GetViewAccessibility();
ASSERT_NE(nullptr, button_accessibility.GetNativeObject());
......
......@@ -276,8 +276,17 @@ int ViewAXPlatformNodeDelegate::GetChildCount() {
if (IsLeaf())
return 0;
if (!virtual_children().empty())
return int{virtual_children().size()};
if (!virtual_children().empty()) {
int count = 0;
for (const std::unique_ptr<AXVirtualView>& child : virtual_children()) {
if (child->IsIgnored()) {
count += child->GetChildCount();
continue;
}
count++;
}
return count;
}
const auto child_widgets_result = GetChildWidgets();
if (child_widgets_result.is_tab_modal_showing) {
......@@ -296,8 +305,24 @@ gfx::NativeViewAccessible ViewAXPlatformNodeDelegate::ChildAtIndex(int index) {
return nullptr;
size_t child_index = size_t{index};
if (!virtual_children().empty())
return virtual_children()[child_index]->GetNativeObject();
if (!virtual_children().empty()) {
int i = 0;
for (const std::unique_ptr<AXVirtualView>& child : virtual_children()) {
if (child->IsIgnored()) {
if (index - i < child->GetChildCount()) {
gfx::NativeViewAccessible result = child->ChildAtIndex(index - i);
if (result)
return result;
}
i += child->GetChildCount();
continue;
}
if (i == index)
return child->GetNativeObject();
i++;
}
return nullptr;
}
// If this is a root view, our widget might have child widgets. Include
const auto child_widgets_result = GetChildWidgets();
......@@ -548,7 +573,7 @@ void ViewAXPlatformNodeDelegate::GetViewsInGroupForSet(
ViewAXPlatformNodeDelegate* ax_delegate =
static_cast<ViewAXPlatformNodeDelegate*>(&view_accessibility);
if (ax_delegate)
is_ignored = is_ignored || ax_delegate->GetData().IsIgnored();
is_ignored = is_ignored || ax_delegate->IsIgnored();
return is_ignored;
}),
views_in_group->end());
......
......@@ -49,6 +49,10 @@ class ViewAXPlatformNodeDelegate : public ViewAccessibility,
void FireFocusAfterMenuClose() override;
// ui::AXPlatformNodeDelegate
// Note that, for parents of virtual views, GetChildCount() and ChildAtIndex()
// present to assistive technologies the unignored accessibility subtree,
// which doesn't necessarily reflect the internal descendant tree. (An ignored
// node means that the node should not be exposed to the platform.)
const ui::AXNodeData& GetData() const override;
int GetChildCount() override;
gfx::NativeViewAccessible ChildAtIndex(int index) override;
......
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