Commit d4f9d946 authored by Nektarios Paisios's avatar Nektarios Paisios Committed by Commit Bot

Reland "Adds a class that encapsulates a range in Blink's accessibility tree."

This is a reland of f70764f0

Original change's description:
> Adds a class that encapsulates a range in Blink's accessibility tree.
> 
> This is just a basic class that can convert from a DOM selection to an AX range and vice versa. It can also perform a selection on the AX range.
> This class doesn't still handle any of the many corner cases that I will be looking at next, cases that will be covered by tests.
> R=dmazzoni@chromium.org
> 
> Bug: 
> Change-Id: I7c98617e8ef3b8bcca8cfc0d2e5518f2da771063
> Reviewed-on: https://chromium-review.googlesource.com/678016
> Commit-Queue: Nektarios Paisios <nektar@chromium.org>
> Reviewed-by: Yoshifumi Inoue <yosin@chromium.org>
> Reviewed-by: Mounir Lamouri <mlamouri@chromium.org>
> Reviewed-by: Kentaro Hara <haraken@chromium.org>
> Reviewed-by: Nektarios Paisios <nektar@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#542619}

Change-Id: I8c6badcc06ad4b012ac73d69ca9e64c921353601
Reviewed-on: https://chromium-review.googlesource.com/959431Reviewed-by: default avatarMounir Lamouri <mlamouri@chromium.org>
Reviewed-by: default avatarNektarios Paisios <nektar@chromium.org>
Reviewed-by: default avatarYoshifumi Inoue <yosin@chromium.org>
Commit-Queue: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#542953}
parent 1ae35411
......@@ -235,6 +235,8 @@ jumbo_source_set("unit_tests") {
sources = [
"accessibility/AXObjectCacheTest.cpp",
"accessibility/AXObjectTest.cpp",
"accessibility/AXPositionTest.cpp",
"accessibility/AXSelectionTest.cpp",
"accessibility/AccessibilityObjectModelTest.cpp",
"accessibility/testing/AccessibilityTest.cpp",
"accessibility/testing/AccessibilityTest.h",
......
// Copyright 2018 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 "modules/accessibility/AXPosition.h"
#include "core/dom/AXObjectCache.h"
#include "core/dom/Document.h"
#include "core/dom/Node.h"
#include "core/editing/EphemeralRange.h"
#include "core/editing/Position.h"
#include "core/editing/PositionWithAffinity.h"
#include "core/editing/iterators/CharacterIterator.h"
#include "core/editing/iterators/TextIterator.h"
#include "modules/accessibility/AXObject.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
namespace blink {
// static
const AXPosition AXPosition::CreatePositionBeforeObject(const AXObject& child) {
// If |child| is a text object, make behavior the same as
// |CreateFirstPositionInObject| so that equality would hold.
if (child.GetNode() && child.GetNode()->IsTextNode())
return CreateFirstPositionInContainerObject(child);
const AXObject* parent = child.ParentObject();
DCHECK(parent);
AXPosition position(*parent);
position.text_offset_or_child_index_ = child.IndexInParent();
DCHECK(position.IsValid());
return position;
}
// static
const AXPosition AXPosition::CreatePositionAfterObject(const AXObject& child) {
// If |child| is a text object, make behavior the same as
// |CreateLastPositionInObject| so that equality would hold.
if (child.GetNode() && child.GetNode()->IsTextNode())
return CreateLastPositionInContainerObject(child);
const AXObject* parent = child.ParentObject();
DCHECK(parent);
AXPosition position(*parent);
position.text_offset_or_child_index_ = child.IndexInParent() + 1;
DCHECK(position.IsValid());
return position;
}
// static
const AXPosition AXPosition::CreateFirstPositionInContainerObject(
const AXObject& container) {
if (container.GetNode() && container.GetNode()->IsTextNode()) {
AXPosition position(container);
position.text_offset_or_child_index_ = 0;
DCHECK(position.IsValid());
return position;
}
AXPosition position(container);
position.text_offset_or_child_index_ = 0;
DCHECK(position.IsValid());
return position;
}
// static
const AXPosition AXPosition::CreateLastPositionInContainerObject(
const AXObject& container) {
if (container.GetNode() && container.GetNode()->IsTextNode()) {
AXPosition position(container);
const auto first_position =
Position::FirstPositionInNode(*container.GetNode());
const auto last_position =
Position::LastPositionInNode(*container.GetNode());
position.text_offset_or_child_index_ =
TextIterator::RangeLength(first_position, last_position);
DCHECK(position.IsValid());
return position;
}
AXPosition position(container);
position.text_offset_or_child_index_ =
static_cast<int>(container.Children().size());
DCHECK(position.IsValid());
return position;
}
// static
const AXPosition AXPosition::CreatePositionInTextObject(
const AXObject& container,
int offset,
TextAffinity affinity) {
AXPosition position(container);
position.text_offset_or_child_index_ = offset;
position.affinity_ = affinity;
DCHECK(position.IsValid());
return position;
}
// static
const AXPosition AXPosition::FromPosition(const Position& position) {
if (position.IsNull() || position.IsOrphan())
return {};
const Document* document = position.GetDocument();
// Non orphan positions always have a document.
DCHECK(document);
AXObjectCache* ax_object_cache = document->ExistingAXObjectCache();
if (!ax_object_cache)
return {};
auto* ax_object_cache_impl = static_cast<AXObjectCacheImpl*>(ax_object_cache);
const Position& parent_anchored_position = position.ToOffsetInAnchor();
const Node* anchor_node = parent_anchored_position.AnchorNode();
DCHECK(anchor_node);
const AXObject* container = ax_object_cache_impl->GetOrCreate(anchor_node);
DCHECK(container);
AXPosition ax_position(*container);
if (anchor_node->IsTextNode()) {
// Convert from a DOM offset that may have uncompressed white space to a
// character offset.
// TODO(ax-dev): Use LayoutNG offset mapping instead of |TextIterator|.
const auto first_position = Position::FirstPositionInNode(*anchor_node);
int offset =
TextIterator::RangeLength(first_position, parent_anchored_position);
ax_position.text_offset_or_child_index_ = offset;
DCHECK(ax_position.IsValid());
return ax_position;
}
const Node* node_after_position = position.ComputeNodeAfterPosition();
if (!node_after_position) {
ax_position.text_offset_or_child_index_ =
static_cast<int>(container->Children().size());
DCHECK(ax_position.IsValid());
return ax_position;
}
const AXObject* ax_child =
ax_object_cache_impl->GetOrCreate(node_after_position);
DCHECK(ax_child);
if (ax_child->IsDescendantOf(*container)) {
ax_position.text_offset_or_child_index_ = ax_child->IndexInParent();
DCHECK(ax_position.IsValid());
return ax_position;
}
return CreatePositionBeforeObject(*ax_child);
}
// Only for use by |AXSelection| to represent empty selection ranges.
AXPosition::AXPosition()
: container_object_(nullptr),
text_offset_or_child_index_(),
affinity_(TextAffinity::kDownstream) {
#if DCHECK_IS_ON()
dom_tree_version_ = 0;
style_version_ = 0;
#endif
}
AXPosition::AXPosition(const AXObject& container)
: container_object_(&container),
text_offset_or_child_index_(),
affinity_(TextAffinity::kDownstream) {
const Document* document = container_object_->GetDocument();
DCHECK(document);
#if DCHECK_IS_ON()
dom_tree_version_ = document->DomTreeVersion();
style_version_ = document->StyleVersion();
#endif
}
const AXObject* AXPosition::ObjectAfterPosition() const {
if (IsTextPosition() || !IsValid())
return nullptr;
return *(container_object_->Children().begin() + ChildIndex());
}
int AXPosition::ChildIndex() const {
if (!IsTextPosition())
return *text_offset_or_child_index_;
NOTREACHED() << *this << " should not be a text position.";
return 0;
}
int AXPosition::TextOffset() const {
if (IsTextPosition())
return *text_offset_or_child_index_;
NOTREACHED() << *this << " should be a text position.";
return 0;
}
bool AXPosition::IsValid() const {
if (!container_object_ || container_object_->IsDetached())
return false;
if (!container_object_->GetNode() ||
!container_object_->GetNode()->isConnected())
return false;
// We can't have both an object and a text anchored position, but we must have
// at least one of them.
DCHECK(text_offset_or_child_index_);
if (text_offset_or_child_index_ &&
!container_object_->GetNode()->IsTextNode()) {
if (text_offset_or_child_index_ >
static_cast<int>(container_object_->Children().size()))
return false;
}
DCHECK(container_object_->GetNode()->GetDocument().IsActive());
DCHECK(!container_object_->GetNode()->GetDocument().NeedsLayoutTreeUpdate());
#if DCHECK_IS_ON()
DCHECK_EQ(container_object_->GetNode()->GetDocument().DomTreeVersion(),
dom_tree_version_);
DCHECK_EQ(container_object_->GetNode()->GetDocument().StyleVersion(),
style_version_);
#endif // DCHECK_IS_ON()
return true;
}
bool AXPosition::IsTextPosition() const {
return IsValid() && container_object_->GetNode()->IsTextNode();
}
const PositionWithAffinity AXPosition::ToPositionWithAffinity() const {
if (!IsValid())
return {};
const Node* container_node = container_object_->GetNode();
if (!IsTextPosition()) {
if (ChildIndex() ==
static_cast<int>(container_object_->Children().size())) {
return PositionWithAffinity(Position::LastPositionInNode(*container_node),
affinity_);
}
const AXObject* ax_child =
*(container_object_->Children().begin() + ChildIndex());
return PositionWithAffinity(
Position::InParentBeforeNode(*(ax_child->GetNode())), affinity_);
}
// TODO(ax-dev): Use LayoutNG offset mapping instead of |TextIterator|.
const auto first_position = Position::FirstPositionInNode(*container_node);
const auto last_position = Position::LastPositionInNode(*container_node);
CharacterIterator character_iterator(first_position, last_position);
const EphemeralRange range = character_iterator.CalculateCharacterSubrange(
0, *text_offset_or_child_index_);
return PositionWithAffinity(range.EndPosition(), affinity_);
}
bool operator==(const AXPosition& a, const AXPosition& b) {
DCHECK(!a.IsValid() || !b.IsValid());
return a.ContainerObject() == b.ContainerObject() &&
a.ChildIndex() == b.ChildIndex() && a.TextOffset() == b.TextOffset() &&
a.Affinity() == b.Affinity();
}
bool operator!=(const AXPosition& a, const AXPosition& b) {
return !(a == b);
}
std::ostream& operator<<(std::ostream& ostream, const AXPosition& position) {
if (!position.IsValid())
return ostream << "Invalid position";
if (position.IsTextPosition()) {
return ostream << "Text position in " << position.ContainerObject() << ", "
<< position.TextOffset();
}
return ostream << "Object anchored position in " << position.ContainerObject()
<< ", " << position.ChildIndex();
}
} // namespace blink
// Copyright 2018 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 AXPosition_h
#define AXPosition_h
#include <base/logging.h>
#include <base/optional.h>
#include <stdint.h>
#include <ostream>
#include "core/editing/Forward.h"
#include "core/editing/TextAffinity.h"
#include "modules/ModulesExport.h"
#include "platform/heap/Persistent.h"
#include "platform/wtf/Allocator.h"
namespace blink {
class AXObject;
// Describes a position in the Blink accessibility tree.
// A position is either anchored to before or after a child object inside a
// container object, or is anchored to a character inside a text object.
class MODULES_EXPORT AXPosition final {
DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
public:
static const AXPosition CreatePositionBeforeObject(const AXObject& child);
static const AXPosition CreatePositionAfterObject(const AXObject& child);
static const AXPosition CreateFirstPositionInContainerObject(
const AXObject& container);
static const AXPosition CreateLastPositionInContainerObject(
const AXObject& container);
static const AXPosition CreatePositionInTextObject(
const AXObject& container,
int offset,
TextAffinity = TextAffinity::kDownstream);
static const AXPosition FromPosition(const Position&);
static const AXPosition FromPosition(const PositionWithAffinity&);
AXPosition(const AXPosition&) = default;
AXPosition& operator=(const AXPosition&) = default;
~AXPosition() = default;
const AXObject* ContainerObject() const { return container_object_; }
const AXObject* ObjectAfterPosition() const;
int ChildIndex() const;
int TextOffset() const;
TextAffinity Affinity() const { return affinity_; }
// Verifies if the anchor is present and if it's set to a live object with a
// connected node.
bool IsValid() const;
// Returns whether this is a position anchored to a character inside a text
// object.
bool IsTextPosition() const;
const PositionWithAffinity ToPositionWithAffinity() const;
private:
AXPosition();
explicit AXPosition(const AXObject& container);
// The |AXObject| in which the position is present.
// Only valid during a single document lifecycle hence no need to maintain a
// strong reference to it.
WeakPersistent<const AXObject> container_object_;
// If the position is anchored to before or after an object, the number of
// child objects in |container_object_| that come before the position.
// If this is a text position, the number of characters in the canonical text
// of |container_object_| before the position. The canonical text is the DOM
// node's text but with, e.g., whitespace collapsed and any transformations
// applied.
base::Optional<int> text_offset_or_child_index_;
// When the same character offset could correspond to two possible caret
// positions, upstream means it's on the previous line rather than the next
// line.
TextAffinity affinity_;
#if DCHECK_IS_ON()
// TODO(ax-dev): Use layout tree version in place of DOM and style versions.
uint64_t dom_tree_version_;
uint64_t style_version_;
#endif
// For access to our constructor for use when creating empty AX selections.
// There is no sense in creating empty positions in other circomstances so we
// disallow it.
friend class AXSelection;
};
MODULES_EXPORT bool operator==(const AXPosition&, const AXPosition&);
MODULES_EXPORT bool operator!=(const AXPosition&, const AXPosition&);
MODULES_EXPORT std::ostream& operator<<(std::ostream&, const AXPosition&);
} // namespace blink
#endif // AXPosition_h
// Copyright 2018 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 "core/dom/Element.h"
#include "core/dom/Node.h"
#include "core/editing/Position.h"
#include "core/html/HTMLElement.h"
#include "modules/accessibility/AXObject.h"
#include "modules/accessibility/AXPosition.h"
#include "modules/accessibility/testing/AccessibilityTest.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace blink {
//
// Basic tests.
//
TEST_F(AccessibilityTest, PositionInText) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'>Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(AccessibilityRole::kStaticTextRole, ax_static_text->RoleValue());
const auto ax_position =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(text, position.AnchorNode());
EXPECT_EQ(3, position.GetPosition().OffsetInContainerNode());
}
// To prevent surprises when comparing equality of two |AXPosition|s, position
// before text object should be the same as position in text object at offset 0.
TEST_F(AccessibilityTest, PositionBeforeText) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'>Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(AccessibilityRole::kStaticTextRole, ax_static_text->RoleValue());
const auto ax_position =
AXPosition::CreatePositionBeforeObject(*ax_static_text);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(text, position.AnchorNode());
EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, PositionBeforeTextWithFirstLetterCSSRule) {
SetBodyInnerHTML(
R"HTML(<style>p ::first-letter { color: red; font-size: 200%; }</style>"
R"<p id='paragraph'>Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(AccessibilityRole::kStaticTextRole, ax_static_text->RoleValue());
const auto ax_position =
AXPosition::CreatePositionBeforeObject(*ax_static_text);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(text, position.AnchorNode());
EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
}
// To prevent surprises when comparing equality of two |AXPosition|s, position
// after text object should be the same as position in text object at offset
// text length.
TEST_F(AccessibilityTest, PositionAfterText) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'>Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(AccessibilityRole::kStaticTextRole, ax_static_text->RoleValue());
const auto ax_position =
AXPosition::CreatePositionAfterObject(*ax_static_text);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(text, position.AnchorNode());
EXPECT_EQ(5, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, PositionBeforeLineBreak) {
SetBodyInnerHTML(R"HTML(Hello<br id='br'>there)HTML");
const AXObject* ax_br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, ax_br);
const auto ax_position = AXPosition::CreatePositionBeforeObject(*ax_br);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(GetDocument().body(), position.AnchorNode());
EXPECT_EQ(1, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, PositionAfterLineBreak) {
SetBodyInnerHTML(R"HTML(Hello<br id='br'>there)HTML");
const AXObject* ax_br = GetAXObjectByElementId("br");
ASSERT_NE(nullptr, ax_br);
const auto ax_position = AXPosition::CreatePositionAfterObject(*ax_br);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(GetDocument().body(), position.AnchorNode());
EXPECT_EQ(2, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, FirstPositionInContainerDiv) {
SetBodyInnerHTML(R"HTML(<div id='div'>Hello<br>there</div>)HTML");
const Element* div = GetElementById("div");
ASSERT_NE(nullptr, div);
const AXObject* ax_div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, ax_div);
const auto ax_position =
AXPosition::CreateFirstPositionInContainerObject(*ax_div);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(div, position.AnchorNode());
EXPECT_EQ(0, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, LastPositionInContainerDiv) {
SetBodyInnerHTML(R"HTML(<div id='div'>Hello<br>there</div>)HTML");
const Element* div = GetElementById("div");
ASSERT_NE(nullptr, div);
const AXObject* ax_div = GetAXObjectByElementId("div");
ASSERT_NE(nullptr, ax_div);
const auto ax_position =
AXPosition::CreateLastPositionInContainerObject(*ax_div);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(div, position.AnchorNode());
EXPECT_TRUE(position.GetPosition().IsAfterChildren());
}
TEST_F(AccessibilityTest, PositionFromPosition) {}
//
// Test converting to and from visible text with white space.
// The accessibility tree is based on visible text with white space compressed,
// vs. the DOM tree where white space is preserved.
//
TEST_F(AccessibilityTest, PositionInTextWithWhiteSpace) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'> Hello </p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
ASSERT_EQ(AccessibilityRole::kStaticTextRole, ax_static_text->RoleValue());
const auto ax_position =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto position = ax_position.ToPositionWithAffinity();
EXPECT_EQ(text, position.AnchorNode());
EXPECT_EQ(8, position.GetPosition().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, PositionBeforeTextWithWhiteSpace) {}
TEST_F(AccessibilityTest, PositionAfterTextWithWhiteSpace) {}
TEST_F(AccessibilityTest, PositionBeforeLineBreakWithWhiteSpace) {}
TEST_F(AccessibilityTest, PositionAfterLineBreakWithWhiteSpace) {}
TEST_F(AccessibilityTest, FirstPositionInContainerDivWithWhiteSpace) {}
TEST_F(AccessibilityTest, LastPositionInContainerDivWithWhiteSpace) {}
TEST_F(AccessibilityTest, PositionFromTextPositionWithWhiteSpace) {}
//
// Test affinity.
// We need to distinguish between the caret at the end of one line and the
// beginning of the next.
//
TEST_F(AccessibilityTest, PositionInTextWithAffinity) {}
TEST_F(AccessibilityTest, PositionFromTextPositionWithAffinity) {}
TEST_F(AccessibilityTest, PositionInTextWithAffinityAndWhiteSpace) {}
TEST_F(AccessibilityTest, PositionFromTextPositionWithAffinityAndWhiteSpace) {}
//
// Test converting to and from accessibility positions with offsets in labels
// and alt text. Alt text, aria-label and other ARIA relationships can cause the
// accessible name of an object to be different than its DOM text.
//
TEST_F(AccessibilityTest, PositionInHTMLLabel) {}
TEST_F(AccessibilityTest, PositionInARIALabel) {}
TEST_F(AccessibilityTest, PositionInARIALabelledBy) {}
TEST_F(AccessibilityTest, PositionInPlaceholder) {}
TEST_F(AccessibilityTest, PositionInAltText) {}
TEST_F(AccessibilityTest, PositionInTitle) {}
//
// Some objects are accessibility ignored.
//
TEST_F(AccessibilityTest, PositionInIgnoredObject) {}
//
// Aria-hidden can cause things in the DOM to be hidden from accessibility.
//
TEST_F(AccessibilityTest, BeforePositionInARIAHidden) {}
TEST_F(AccessibilityTest, AfterPositionInARIAHidden) {}
TEST_F(AccessibilityTest, FromPositionInARIAHidden) {}
//
// Canvas fallback can cause things to be in the accessibility tree that are not
// in the layout tree.
//
TEST_F(AccessibilityTest, PositionInCanvas) {}
//
// Some layout objects, e.g. list bullets and CSS::before/after content, appears
// in the accessibility tree but is not present in the DOM.
//
TEST_F(AccessibilityTest, PositionBeforeListBullet) {}
TEST_F(AccessibilityTest, PositionAfterListBullet) {}
TEST_F(AccessibilityTest, PositionInCSSContent) {}
//
// Objects deriving from |AXMockObject|, e.g. table columns, are in the
// accessibility tree but are neither in the DOM or layout trees.
//
TEST_F(AccessibilityTest, PositionInTableColumn) {}
} // namespace blink
// Copyright 2018 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 "modules/accessibility/AXSelection.h"
#include "core/dom/Document.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/PositionWithAffinity.h"
#include "core/editing/SelectionTemplate.h"
#include "core/editing/SetSelectionOptions.h"
#include "core/editing/TextAffinity.h"
#include "core/frame/LocalFrame.h"
#include "modules/accessibility/AXObject.h"
namespace blink {
AXSelection::Builder& AXSelection::Builder::SetBase(const AXPosition& base) {
DCHECK(base.IsValid());
selection_.base_ = base;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetBase(const Position& base) {
const auto ax_base = AXPosition::FromPosition(base);
DCHECK(ax_base.IsValid());
selection_.base_ = ax_base;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetExtent(
const AXPosition& extent) {
DCHECK(extent.IsValid());
selection_.extent_ = extent;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetExtent(const Position& extent) {
const auto ax_extent = AXPosition::FromPosition(extent);
DCHECK(ax_extent.IsValid());
selection_.extent_ = ax_extent;
return *this;
}
AXSelection::Builder& AXSelection::Builder::SetSelection(
const SelectionInDOMTree& selection) {
if (selection.IsNone())
return *this;
selection_.base_ = AXPosition::FromPosition(selection.Base());
selection_.extent_ = AXPosition::FromPosition(selection.Extent());
return *this;
}
const AXSelection AXSelection::Builder::Build() {
if (!selection_.Base().IsValid() || !selection_.Extent().IsValid())
return {};
const Document* document = selection_.Base().ContainerObject()->GetDocument();
DCHECK(document);
DCHECK(document->IsActive());
DCHECK(!document->NeedsLayoutTreeUpdate());
#if DCHECK_IS_ON()
selection_.dom_tree_version_ = document->DomTreeVersion();
selection_.style_version_ = document->StyleVersion();
#endif
return selection_;
}
AXSelection::AXSelection() : base_(), extent_() {
#if DCHECK_IS_ON()
dom_tree_version_ = 0;
style_version_ = 0;
#endif
}
bool AXSelection::IsValid() const {
if (!base_.IsValid() || !extent_.IsValid())
return false;
// We don't support selections that span across documents.
if (base_.ContainerObject()->GetDocument() !=
extent_.ContainerObject()->GetDocument())
return false;
DCHECK(!base_.ContainerObject()->GetDocument()->NeedsLayoutTreeUpdate());
#if DCHECK_IS_ON()
DCHECK_EQ(base_.ContainerObject()->GetDocument()->DomTreeVersion(),
dom_tree_version_);
DCHECK_EQ(base_.ContainerObject()->GetDocument()->StyleVersion(),
style_version_);
#endif // DCHECK_IS_ON()
return true;
}
const SelectionInDOMTree AXSelection::AsSelection() const {
if (!IsValid())
return {};
const auto dom_base = base_.ToPositionWithAffinity();
const auto dom_extent = extent_.ToPositionWithAffinity();
SelectionInDOMTree::Builder selection_builder;
selection_builder.SetBaseAndExtent(dom_base.GetPosition(),
dom_extent.GetPosition());
selection_builder.SetAffinity(extent_.Affinity());
return selection_builder.Build();
}
void AXSelection::Select() {
if (!IsValid()) {
NOTREACHED();
return;
}
const SelectionInDOMTree selection = AsSelection();
DCHECK(selection.AssertValid());
Document* document = selection.Base().GetDocument();
if (!document) {
NOTREACHED();
return;
}
LocalFrame* frame = document->GetFrame();
if (!frame) {
NOTREACHED();
return;
}
FrameSelection& frame_selection = frame->Selection();
frame_selection.SetSelection(selection, SetSelectionOptions());
}
bool operator==(const AXSelection& a, const AXSelection& b) {
DCHECK(!a.IsValid() || !b.IsValid());
return a.Base() == b.Base() && a.Extent() == b.Extent();
}
bool operator!=(const AXSelection& a, const AXSelection& b) {
return !(a == b);
}
std::ostream& operator<<(std::ostream& ostream, const AXSelection& selection) {
if (!selection.IsValid())
return ostream << "Invalid selection";
return ostream << "Selection from " << selection.Base() << " to "
<< selection.Extent();
}
} // namespace blink
// Copyright 2018 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 AXSelection_h
#define AXSelection_h
#include <base/logging.h>
#include <stdint.h>
#include <ostream>
#include "core/editing/Forward.h"
#include "modules/ModulesExport.h"
#include "modules/accessibility/AXPosition.h"
#include "platform/wtf/Allocator.h"
namespace blink {
class MODULES_EXPORT AXSelection final {
DISALLOW_NEW_EXCEPT_PLACEMENT_NEW();
public:
class Builder;
AXSelection(const AXSelection&) = default;
AXSelection& operator=(const AXSelection&) = default;
~AXSelection() = default;
const AXPosition Base() const { return base_; }
const AXPosition Extent() const { return extent_; }
// The selection is invalid if either the anchor or the focus position is
// invalid, or if the positions are in two separate documents.
bool IsValid() const;
const SelectionInDOMTree AsSelection() const;
// Tries to set the DOM selection to this.
void Select();
private:
AXSelection();
// The |AXPosition| where the selection starts.
AXPosition base_;
// The |AXPosition| where the selection ends.
AXPosition extent_;
#if DCHECK_IS_ON()
// TODO(ax-dev): Use layout tree version in place of DOM and style versions.
uint64_t dom_tree_version_;
uint64_t style_version_;
#endif
friend class Builder;
};
class MODULES_EXPORT AXSelection::Builder final {
STACK_ALLOCATED();
public:
Builder() = default;
~Builder() = default;
Builder& SetBase(const AXPosition&);
Builder& SetBase(const Position&);
Builder& SetExtent(const AXPosition&);
Builder& SetExtent(const Position&);
Builder& SetSelection(const SelectionInDOMTree&);
const AXSelection Build();
private:
AXSelection selection_;
};
MODULES_EXPORT bool operator==(const AXSelection&, const AXSelection&);
MODULES_EXPORT bool operator!=(const AXSelection&, const AXSelection&);
MODULES_EXPORT std::ostream& operator<<(std::ostream&, const AXSelection&);
} // namespace blink
#endif // AXSelection_h
// Copyright 2018 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 "core/dom/Node.h"
#include "core/editing/Position.h"
#include "core/editing/SelectionTemplate.h"
#include "modules/accessibility/AXObject.h"
#include "modules/accessibility/AXPosition.h"
#include "modules/accessibility/AXSelection.h"
#include "modules/accessibility/testing/AccessibilityTest.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace blink {
//
// Basic tests.
//
TEST_F(AccessibilityTest, SetSelectionInText) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'>Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
const auto ax_base =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto ax_extent = AXPosition::CreatePositionAfterObject(*ax_static_text);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetBase(ax_base).SetExtent(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Base().AnchorNode());
EXPECT_EQ(3, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Extent().AnchorNode());
EXPECT_EQ(5, dom_selection.Extent().OffsetInContainerNode());
}
TEST_F(AccessibilityTest, SetSelectionInTextWithWhiteSpace) {
SetBodyInnerHTML(R"HTML(<p id='paragraph'> Hello</p>)HTML");
const Node* text = GetElementById("paragraph")->firstChild();
ASSERT_NE(nullptr, text);
const AXObject* ax_static_text =
*(GetAXObjectByElementId("paragraph")->Children().begin());
ASSERT_NE(nullptr, ax_static_text);
const auto ax_base =
AXPosition::CreatePositionInTextObject(*ax_static_text, 3);
const auto ax_extent = AXPosition::CreatePositionAfterObject(*ax_static_text);
AXSelection::Builder builder;
const AXSelection ax_selection =
builder.SetBase(ax_base).SetExtent(ax_extent).Build();
const SelectionInDOMTree dom_selection = ax_selection.AsSelection();
EXPECT_EQ(text, dom_selection.Base().AnchorNode());
EXPECT_EQ(8, dom_selection.Base().OffsetInContainerNode());
EXPECT_EQ(text, dom_selection.Extent().AnchorNode());
EXPECT_EQ(10, dom_selection.Extent().OffsetInContainerNode());
}
//
// Get selection tests.
// Retrieving a selection with endpoints which have no corresponding objects in
// the accessibility tree, e.g. which are aria-hidden, should shring
// |AXSelection| to valid endpoints.
//
//
// Set selection tests.
// Setting the selection from an |AXSelection| that has endpoints which are not
// present in the layout tree should shring the selection to visible endpoints.
//
} // namespace blink
......@@ -42,6 +42,8 @@ blink_modules_sources("accessibility") {
"AXObject.h",
"AXObjectCacheImpl.cpp",
"AXObjectCacheImpl.h",
"AXPosition.cpp",
"AXPosition.h",
"AXProgressIndicator.cpp",
"AXProgressIndicator.h",
"AXRadioInput.cpp",
......@@ -50,6 +52,8 @@ blink_modules_sources("accessibility") {
"AXRelationCache.h",
"AXSVGRoot.cpp",
"AXSVGRoot.h",
"AXSelection.cpp",
"AXSelection.h",
"AXSlider.cpp",
"AXSlider.h",
"AXSparseAttributeSetter.cpp",
......
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