blob: ba0f35d4a0cf79259b0124a103d67c7b28961c02 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/accessibility/browser_accessibility.h"
#include <cstddef>
#include <iterator>
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility_manager.h"
#include "content/browser/accessibility/browser_accessibility_state_impl.h"
#include "content/public/common/content_client.h"
#include "third_party/blink/public/strings/grit/blink_accessibility_strings.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/platform/ax_platform.h"
#include "ui/accessibility/platform/ax_platform_tree_manager_delegate.h"
#include "ui/accessibility/platform/ax_unique_id.h"
#include "ui/base/buildflags.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/strings/grit/ax_strings.h"
namespace content {
#if DCHECK_IS_ON()
static int browser_accessibility_count = 0;
static bool has_dumped_possible_leak = false;
// If there are more than 10 million objects alive at once, dump.
// It is likely to be a leak if we have > 100 tabs x 10000 objects.
constexpr int kDumpBrowserAccessibilityLeakNumObjects = 10000000;
#endif
#if !BUILDFLAG(HAS_PLATFORM_ACCESSIBILITY_SUPPORT)
// static
std::unique_ptr<BrowserAccessibility> BrowserAccessibility::Create(
BrowserAccessibilityManager* manager,
ui::AXNode* node) {
return base::WrapUnique(new BrowserAccessibility(manager, node));
}
#endif // !BUILDFLAG(HAS_PLATFORM_ACCESSIBILITY_SUPPORT)
// static
bool BrowserAccessibility::ignore_hovered_state_for_testing_ = false;
// static
BrowserAccessibility* BrowserAccessibility::FromAXPlatformNodeDelegate(
ui::AXPlatformNodeDelegate* delegate) {
if (!delegate || !delegate->IsWebContent())
return nullptr;
return static_cast<BrowserAccessibility*>(delegate);
}
BrowserAccessibility::BrowserAccessibility(BrowserAccessibilityManager* manager,
ui::AXNode* node)
: AXPlatformNodeDelegate(node), manager_(manager) {
DCHECK(manager);
DCHECK(node);
DCHECK(node->IsDataValid());
#if DCHECK_IS_ON()
if (++browser_accessibility_count > kDumpBrowserAccessibilityLeakNumObjects &&
!has_dumped_possible_leak) {
NOTREACHED_IN_MIGRATION();
has_dumped_possible_leak = true;
}
#endif
}
BrowserAccessibility::~BrowserAccessibility() {
#if DCHECK_IS_ON()
--browser_accessibility_count;
#endif
}
namespace {
// Get the native text field's deepest container; the lowest descendant that
// contains all its text. Returns nullptr if the text field is empty, or if it
// is not a native text field (input or textarea).
BrowserAccessibility* GetTextFieldInnerEditorElement(
const BrowserAccessibility& text_field) {
ui::AXNode* text_container =
text_field.node()->GetTextFieldInnerEditorElement();
return text_field.manager()->GetFromAXNode(text_container);
}
} // namespace
bool BrowserAccessibility::IsValid() const {
// Currently we only perform validity checks on non-empty, atomic text fields.
// An atomic text field does not expose its internal implementation to
// assistive software, appearing as a single leaf node in the accessibility
// tree. It includes <input>, <textarea> and Views-based text fields.
if (!IsAtomicTextField())
return true;
// If the input type is not plain or text it may be a complex field, such as
// a datetime input. We don't try to enforce a special structure for those.
const std::string& input_type =
GetStringAttribute(ax::mojom::StringAttribute::kInputType);
DCHECK(IsIgnored() ||
GetStringAttribute(ax::mojom::StringAttribute::kHtmlTag) != "input" ||
!input_type.empty())
<< "By design, all non-hidden <input> elements in the accessibility "
"tree, should have an input type: "
<< *this;
if (input_type != "text" && input_type != "search")
return true; // Not a plain text field, just consider it valid.
// If the atomic text field is aria-hidden then all its descendants are
// ignored. See the dump tree test AccessibilityAriaHiddenFocusedInput.
// TODO(accessibility): We need to fix this by pruning the tree and removing
// the native text field if it is aria-hidden.
if (IsInvisibleOrIgnored() || !InternalGetFirstChild()) {
return true;
}
return GetTextFieldInnerEditorElement(*this);
}
void BrowserAccessibility::OnDataChanged() {
DCHECK(IsValid()) << "Invalid node: " << *this;
}
bool BrowserAccessibility::CanFireEvents() const {
return node()->CanFireEvents();
}
ui::AXPlatformNode* BrowserAccessibility::GetAXPlatformNode() const {
// Not all BrowserAccessibility subclasses can return an AXPlatformNode yet.
// So, here we just return nullptr.
return nullptr;
}
size_t BrowserAccessibility::PlatformChildCount() const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf()) {
return 0u;
}
if (ui::AXTreeManager::ForChildTree(*node())) {
// A child tree might not be connected yet, or might not be hosting platform
// objects.
return manager()->GetFromAXNode(
node()->GetFirstUnignoredChildCrossingTreeBoundary())
? 1u
: 0u;
}
return node()->GetUnignoredChildCountCrossingTreeBoundary();
}
BrowserAccessibility* BrowserAccessibility::PlatformGetParent() const {
ui::AXNode* parent = node()->GetUnignoredParentCrossingTreeBoundary();
return manager()->GetFromAXNode(parent);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetFirstChild() const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf())
return nullptr;
ui::AXNode* first_child =
node()->GetFirstUnignoredChildCrossingTreeBoundary();
return manager()->GetFromAXNode(first_child);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetLastChild() const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf())
return nullptr;
ui::AXNode* last_child = node()->GetLastUnignoredChildCrossingTreeBoundary();
return manager()->GetFromAXNode(last_child);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetNextSibling() const {
return InternalGetNextSibling();
}
BrowserAccessibility* BrowserAccessibility::PlatformGetPreviousSibling() const {
return InternalGetPreviousSibling();
}
BrowserAccessibility::PlatformChildIterator
BrowserAccessibility::PlatformChildrenBegin() const {
return PlatformChildIterator(this, PlatformGetFirstChild());
}
BrowserAccessibility::PlatformChildIterator
BrowserAccessibility::PlatformChildrenEnd() const {
return PlatformChildIterator(this, nullptr);
}
bool BrowserAccessibility::IsDescendantOf(
const BrowserAccessibility* ancestor) const {
if (!ancestor)
return false;
DCHECK(ancestor->node());
return node()->IsDescendantOfCrossingTreeBoundary(ancestor->node());
}
bool BrowserAccessibility::IsIgnoredForTextNavigation() const {
return node()->IsIgnoredForTextNavigation();
}
bool BrowserAccessibility::IsLineBreakObject() const {
return node()->IsLineBreak();
}
BrowserAccessibility* BrowserAccessibility::PlatformGetChild(
size_t child_index) const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf())
return nullptr;
ui::AXNode* child =
node()->GetUnignoredChildAtIndexCrossingTreeBoundary(child_index);
return manager()->GetFromAXNode(child);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetLowestPlatformAncestor()
const {
ui::AXNode* lowest_platform_ancestor = node()->GetLowestPlatformAncestor();
return manager()->GetFromAXNode(lowest_platform_ancestor);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetTextFieldAncestor()
const {
ui::AXNode* text_field_ancestor = node()->GetTextFieldAncestor();
return manager()->GetFromAXNode(text_field_ancestor);
}
BrowserAccessibility* BrowserAccessibility::PlatformGetSelectionContainer()
const {
ui::AXNode* selection_container_ancestor = node()->GetSelectionContainer();
return manager()->GetFromAXNode(selection_container_ancestor);
}
BrowserAccessibility* BrowserAccessibility::PlatformDeepestFirstChild() const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf())
return nullptr;
BrowserAccessibility* deepest_child = PlatformGetFirstChild();
while (!deepest_child->IsLeaf())
deepest_child = deepest_child->PlatformGetFirstChild();
return deepest_child;
}
BrowserAccessibility* BrowserAccessibility::PlatformDeepestLastChild() const {
// We need to explicitly check for leafiness here instead of relying on
// `AXNode::IsLeaf()` because Android has a different notion of this concept.
if (IsLeaf())
return nullptr;
BrowserAccessibility* deepest_child = PlatformGetLastChild();
while (!deepest_child->IsLeaf())
deepest_child = deepest_child->PlatformGetLastChild();
return deepest_child;
}
BrowserAccessibility* BrowserAccessibility::InternalDeepestFirstChild() const {
// By design, this method should be able to traverse platform leaves, hence we
// don't check for leafiness.
ui::AXNode* deepest_descendant = node()->GetDeepestFirstUnignoredDescendant();
return manager()->GetFromAXNode(deepest_descendant);
}
BrowserAccessibility* BrowserAccessibility::InternalDeepestLastChild() const {
// By design, this method should be able to traverse platform leaves, hence we
// don't check for leafiness. We need to explicitly check for leafiness here
// instead of relying on `AXNode::IsLeaf()` because Android has a different
// notion of this concept.
ui::AXNode* deepest_descendant = node()->GetDeepestLastUnignoredDescendant();
return manager()->GetFromAXNode(deepest_descendant);
}
size_t BrowserAccessibility::InternalChildCount() const {
return node()->GetUnignoredChildCount();
}
BrowserAccessibility* BrowserAccessibility::InternalGetChild(
size_t child_index) const {
// By design, this method should be able to traverse platform leaves, hence we
// don't check for leafiness.
ui::AXNode* child_node = node()->GetUnignoredChildAtIndex(child_index);
return manager_->GetFromAXNode(child_node);
}
BrowserAccessibility* BrowserAccessibility::InternalGetParent() const {
ui::AXNode* parent_node = node()->GetUnignoredParent();
return manager_->GetFromAXNode(parent_node);
}
BrowserAccessibility* BrowserAccessibility::InternalGetFirstChild() const {
// By design, this method should be able to traverse platform leaves, hence we
// don't check for leafiness.
ui::AXNode* child_node = node()->GetFirstUnignoredChild();
return manager_->GetFromAXNode(child_node);
}
BrowserAccessibility* BrowserAccessibility::InternalGetLastChild() const {
// By design, this method should be able to traverse platform leaves, hence we
// don't check for leafiness.
ui::AXNode* child_node = node()->GetLastUnignoredChild();
return manager_->GetFromAXNode(child_node);
}
BrowserAccessibility* BrowserAccessibility::InternalGetNextSibling() const {
ui::AXNode* sibling_node = node()->GetNextUnignoredSibling();
return manager_->GetFromAXNode(sibling_node);
}
BrowserAccessibility* BrowserAccessibility::InternalGetPreviousSibling() const {
ui::AXNode* sibling_node = node()->GetPreviousUnignoredSibling();
return manager_->GetFromAXNode(sibling_node);
}
BrowserAccessibility::InternalChildIterator
BrowserAccessibility::InternalChildrenBegin() const {
return InternalChildIterator(this, InternalGetFirstChild());
}
BrowserAccessibility::InternalChildIterator
BrowserAccessibility::InternalChildrenEnd() const {
return InternalChildIterator(this, nullptr);
}
const BrowserAccessibility*
BrowserAccessibility::AllChildrenRange::Iterator::operator*() {
if (child_tree_root_)
return index_ == 0 ? child_tree_root_.get() : nullptr;
// TODO(nektar): Consider using
// `AXNode::GetChildAtIndexCrossingTreeBoundary()`.
ui::AXNode* child = parent_->node()->GetChildAtIndex(index_);
return parent_->manager()->GetFromAXNode(child);
}
gfx::RectF BrowserAccessibility::GetLocation() const {
return GetData().relative_bounds.bounds;
}
gfx::Rect BrowserAccessibility::GetUnclippedRootFrameInnerTextRangeBoundsRect(
const int start_offset,
const int end_offset,
ui::AXOffscreenResult* offscreen_result) const {
return GetInnerTextRangeBoundsRect(
start_offset, end_offset, ui::AXCoordinateSystem::kRootFrame,
ui::AXClippingBehavior::kUnclipped, offscreen_result);
}
gfx::Rect BrowserAccessibility::GetBoundsRect(
const ui::AXCoordinateSystem coordinate_system,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
return RelativeToAbsoluteBounds(gfx::RectF(), coordinate_system,
clipping_behavior, offscreen_result);
}
gfx::Rect BrowserAccessibility::GetHypertextRangeBoundsRect(
const int start_offset,
const int end_offset,
const ui::AXCoordinateSystem coordinate_system,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
int effective_start_offset = start_offset;
int effective_end_offset = end_offset;
if (effective_start_offset == effective_end_offset)
return gfx::Rect();
if (effective_start_offset > effective_end_offset)
std::swap(effective_start_offset, effective_end_offset);
const std::u16string& text_str = GetHypertext();
if (effective_start_offset < 0 ||
effective_start_offset >= static_cast<int>(text_str.size())) {
return gfx::Rect();
}
if (effective_end_offset < 0 ||
effective_end_offset > static_cast<int>(text_str.size())) {
return gfx::Rect();
}
if (coordinate_system == ui::AXCoordinateSystem::kFrame) {
NOTIMPLEMENTED();
return gfx::Rect();
}
// Obtain bounds in root frame coordinates.
gfx::Rect bounds = GetRootFrameHypertextRangeBoundsRect(
effective_start_offset, effective_end_offset - effective_start_offset,
clipping_behavior, offscreen_result);
if (coordinate_system == ui::AXCoordinateSystem::kScreenDIPs ||
coordinate_system == ui::AXCoordinateSystem::kScreenPhysicalPixels) {
// Convert to screen coordinates.
bounds.Offset(
manager()->GetViewBoundsInScreenCoordinates().OffsetFromOrigin());
}
return bounds;
}
gfx::Rect BrowserAccessibility::GetRootFrameHypertextRangeBoundsRect(
int start,
int len,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
// TODO(nektar): Move to `AXNode` as soon as hypertext computation is fully
// migrated to that class.
DCHECK_GE(start, 0);
DCHECK_GE(len, 0);
// Atomic text fields such as textarea have a text container node inside them
// that holds all the text and do not expose any IA2 hypertext. We need to get
// to the flattened representation of the text in the field in order that
// `start` and `len` would be applicable. Non-native text fields, including
// ARIA-based ones expose their actual subtree and do use IA2 hypertext, so
// `start` and `len` would apply in those cases.
if (const BrowserAccessibility* text_container =
GetTextFieldInnerEditorElement(*this)) {
return text_container->GetRootFrameHypertextRangeBoundsRect(
start, len, clipping_behavior, offscreen_result);
}
if (GetRole() != ax::mojom::Role::kStaticText) {
gfx::Rect bounds;
for (InternalChildIterator it = InternalChildrenBegin();
it != InternalChildrenEnd() && len > 0; ++it) {
const BrowserAccessibility* child = it.get();
// Child objects are of length one, since they are represented by a single
// embedded object character. The exception is text-only objects.
int child_length_in_parent = 1;
if (child->IsText())
child_length_in_parent = static_cast<int>(child->GetHypertext().size());
if (start < child_length_in_parent) {
gfx::Rect child_rect;
if (child->IsText()) {
child_rect = child->GetRootFrameHypertextRangeBoundsRect(
start, len, clipping_behavior, offscreen_result);
} else {
child_rect = child->GetRootFrameHypertextRangeBoundsRect(
0, static_cast<int>(child->GetHypertext().size()),
clipping_behavior, offscreen_result);
}
bounds.Union(child_rect);
len -= (child_length_in_parent - start);
}
if (start > child_length_in_parent)
start -= child_length_in_parent;
else
start = 0;
}
// When past the end of text, the area will be 0.
// In this case, use bounds provided for the caret.
return bounds.IsEmpty() ? GetRootFrameHypertextBoundsPastEndOfText(
clipping_behavior, offscreen_result)
: bounds;
}
int end = start + len;
int child_start = 0;
int child_end = 0;
gfx::Rect bounds;
for (InternalChildIterator it = InternalChildrenBegin();
it != InternalChildrenEnd() && child_end < start + len; ++it) {
const BrowserAccessibility* child = it.get();
if (child->GetRole() != ax::mojom::Role::kInlineTextBox) {
DLOG(WARNING) << "BrowserAccessibility objects with role STATIC_TEXT "
<< "should have children of role INLINE_TEXT_BOX.\n";
continue;
}
int child_length = static_cast<int>(child->GetHypertext().size());
child_start = child_end;
child_end += child_length;
if (child_end < start)
continue;
int overlap_start = std::max(start, child_start);
int overlap_end = std::min(end, child_end);
int local_start = overlap_start - child_start;
int local_end = overlap_end - child_start;
// |local_end| and |local_start| may equal |child_length| when the caret is
// at the end of a text field.
DCHECK_GE(local_start, 0);
DCHECK_LE(local_start, child_length);
DCHECK_GE(local_end, 0);
DCHECK_LE(local_end, child_length);
// Don't clip bounds. Some screen magnifiers (e.g. ZoomText) prefer to
// get unclipped bounds so that they can make smooth scrolling calculations.
gfx::Rect absolute_child_rect = child->RelativeToAbsoluteBounds(
child->GetTextContentRangeBoundsUTF16(local_start, local_end),
ui::AXCoordinateSystem::kRootFrame, clipping_behavior,
offscreen_result);
if (bounds.width() == 0 && bounds.height() == 0) {
bounds = absolute_child_rect;
} else {
bounds.Union(absolute_child_rect);
}
}
return bounds;
}
gfx::Rect BrowserAccessibility::GetScreenHypertextRangeBoundsRect(
int start,
int len,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
gfx::Rect bounds = GetRootFrameHypertextRangeBoundsRect(
start, len, clipping_behavior, offscreen_result);
// Adjust the bounds by the top left corner of the containing view's bounds
// in screen coordinates.
bounds.Offset(
manager_->GetViewBoundsInScreenCoordinates().OffsetFromOrigin());
return bounds;
}
gfx::Rect BrowserAccessibility::GetRootFrameHypertextBoundsPastEndOfText(
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
// TODO(nektar): Move to `AXNode` as soon as hypertext computation is fully
// migrated to that class.
// Step 1: get approximate caret bounds. The thickness may not yet be correct.
gfx::Rect bounds;
if (InternalChildCount() > 0) {
// When past the end of text, use bounds provided by a last child if
// available, and then correct for thickness of caret.
BrowserAccessibility* child = InternalGetLastChild();
int child_text_len = child->GetHypertext().size();
bounds = child->GetRootFrameHypertextRangeBoundsRect(
child_text_len, child_text_len, clipping_behavior, offscreen_result);
if (bounds.width() == 0 && bounds.height() == 0)
return bounds; // Inline text boxes info not yet available.
} else {
// Compute bounds of where caret would be, based on bounds of object.
bounds = GetBoundsRect(ui::AXCoordinateSystem::kRootFrame,
clipping_behavior, offscreen_result);
}
// Step 2: correct for the thickness of the caret.
auto text_direction = static_cast<ax::mojom::WritingDirection>(
GetIntAttribute(ax::mojom::IntAttribute::kTextDirection));
constexpr int kCaretThickness = 1;
switch (text_direction) {
case ax::mojom::WritingDirection::kNone:
case ax::mojom::WritingDirection::kLtr: {
bounds.set_width(kCaretThickness);
break;
}
case ax::mojom::WritingDirection::kRtl: {
bounds.set_x(bounds.right() - kCaretThickness);
bounds.set_width(kCaretThickness);
break;
}
case ax::mojom::WritingDirection::kTtb: {
bounds.set_height(kCaretThickness);
break;
}
case ax::mojom::WritingDirection::kBtt: {
bounds.set_y(bounds.bottom() - kCaretThickness);
bounds.set_height(kCaretThickness);
break;
}
}
return bounds;
}
gfx::Rect BrowserAccessibility::GetInnerTextRangeBoundsRect(
const int start_offset,
const int end_offset,
const ui::AXCoordinateSystem coordinate_system,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
const int text_length = GetTextContentLengthUTF16();
if (start_offset < 0 || end_offset > text_length || start_offset > end_offset)
return gfx::Rect();
return GetInnerTextRangeBoundsRectInSubtree(
start_offset, end_offset, coordinate_system, clipping_behavior,
offscreen_result);
}
gfx::Rect BrowserAccessibility::GetInnerTextRangeBoundsRectInSubtree(
const int start_offset,
const int end_offset,
const ui::AXCoordinateSystem coordinate_system,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
// TODO(nektar): Move to `AXNode` as soon as hypertext computation is fully
// migrated to that class.
if (GetRole() == ax::mojom::Role::kInlineTextBox) {
return RelativeToAbsoluteBounds(
GetTextContentRangeBoundsUTF16(start_offset, end_offset),
coordinate_system, clipping_behavior, offscreen_result);
}
gfx::Rect bounds;
int child_offset_in_parent = 0;
for (InternalChildIterator it = InternalChildrenBegin();
it != InternalChildrenEnd(); ++it) {
const BrowserAccessibility* browser_accessibility_child = it.get();
const int child_text_length =
browser_accessibility_child->GetTextContentLengthUTF16();
// The text bounds queried are not in this subtree; skip it and continue.
const int child_start_offset =
std::max(start_offset - child_offset_in_parent, 0);
if (child_start_offset > child_text_length) {
child_offset_in_parent += child_text_length;
continue;
}
// The text bounds queried have already been gathered; short circuit.
const int child_end_offset =
std::min(end_offset - child_offset_in_parent, child_text_length);
if (child_end_offset < 0)
return bounds;
// Increase the text bounds by the subtree text bounds.
const gfx::Rect child_bounds =
browser_accessibility_child->GetInnerTextRangeBoundsRectInSubtree(
child_start_offset, child_end_offset, coordinate_system,
clipping_behavior, offscreen_result);
if (bounds.IsEmpty())
bounds = child_bounds;
else
bounds.Union(child_bounds);
child_offset_in_parent += child_text_length;
}
return bounds;
}
gfx::RectF BrowserAccessibility::GetTextContentRangeBoundsUTF16(
int start_offset,
int end_offset) const {
return node()->GetTextContentRangeBoundsUTF16(start_offset, end_offset);
}
BrowserAccessibility* BrowserAccessibility::ApproximateHitTest(
const gfx::Point& blink_screen_point) {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
// The best result found that's a child of this object.
BrowserAccessibility* child_result = nullptr;
// The best result that's an indirect descendant like grandchild, etc.
BrowserAccessibility* descendant_result = nullptr;
// Walk the children recursively looking for the BrowserAccessibility that
// most tightly encloses the specified point. Walk backwards so that in
// the absence of any other information, we assume the object that occurs
// later in the tree is on top of one that comes before it.
for (BrowserAccessibility* child = PlatformGetLastChild(); child != nullptr;
child = child->PlatformGetPreviousSibling()) {
// Skip table columns because cells are only contained in rows,
// not columns.
if (child->GetRole() == ax::mojom::Role::kColumn)
continue;
if (child->GetClippedScreenBoundsRect().Contains(blink_screen_point)) {
BrowserAccessibility* result =
child->ApproximateHitTest(blink_screen_point);
if (result == child && !child_result)
child_result = result;
if (result != child && !descendant_result)
descendant_result = result;
}
if (child_result && descendant_result)
break;
}
// Explanation of logic: it's possible that this point overlaps more than
// one child of this object. If so, as a heuristic we prefer if the point
// overlaps a descendant of one of the two children and not the other.
// As an example, suppose you have two rows of buttons - the buttons don't
// overlap, but the rows do. Without this heuristic, we'd greedily only
// consider one of the containers.
if (descendant_result)
return descendant_result;
if (child_result)
return child_result;
return this;
}
bool BrowserAccessibility::IsClickable() const {
return GetData().IsClickable();
}
bool BrowserAccessibility::IsTextField() const {
return GetData().IsTextField();
}
bool BrowserAccessibility::IsPasswordField() const {
return GetData().IsPasswordField();
}
bool BrowserAccessibility::IsAtomicTextField() const {
return GetData().IsAtomicTextField();
}
bool BrowserAccessibility::IsNonAtomicTextField() const {
return GetData().IsNonAtomicTextField();
}
bool BrowserAccessibility::HasExplicitlyEmptyName() const {
return GetNameFrom() == ax::mojom::NameFrom::kAttributeExplicitlyEmpty;
}
// |offset| could either be a text character or a child index in case of
// non-text objects.
// Currently, to be safe, we convert to text leaf equivalents and we don't use
// tree positions.
// TODO(nektar): Remove this function once selection fixes in Blink are
// thoroughly tested and convert to tree positions.
BrowserAccessibility::AXPosition
BrowserAccessibility::CreatePositionForSelectionAt(int offset) const {
return CreateTextPositionAt(offset)->AsDomSelectionPosition();
}
std::u16string BrowserAccessibility::GetNameAsString16() const {
return node()->GetNameUTF16();
}
gfx::Rect BrowserAccessibility::RelativeToAbsoluteBounds(
gfx::RectF bounds,
const ui::AXCoordinateSystem coordinate_system,
const ui::AXClippingBehavior clipping_behavior,
ui::AXOffscreenResult* offscreen_result) const {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
const bool clip_bounds =
clipping_behavior == ui::AXClippingBehavior::kClipped;
bool offscreen = false;
const BrowserAccessibility* node = this;
while (node) {
BrowserAccessibilityManager* manager = node->manager();
bounds = manager->ax_tree()->RelativeToTreeBounds(node->node(), bounds,
&offscreen, clip_bounds);
// On some platforms we need to unapply root scroll offsets.
if (!manager->UseRootScrollOffsetsWhenComputingBounds()) {
// Get the node that's the "root scroller", which isn't necessarily
// the root of the tree.
ui::AXNodeID root_scroller_id = manager->GetTreeData().root_scroller_id;
BrowserAccessibility* root_scroller =
manager->GetFromID(root_scroller_id);
if (root_scroller) {
int sx = 0;
int sy = 0;
if (root_scroller->GetIntAttribute(ax::mojom::IntAttribute::kScrollX,
&sx) &&
root_scroller->GetIntAttribute(ax::mojom::IntAttribute::kScrollY,
&sy)) {
bounds.Offset(sx, sy);
}
}
}
if (coordinate_system == ui::AXCoordinateSystem::kFrame)
break;
const BrowserAccessibility* root = manager->GetBrowserAccessibilityRoot();
node = root->PlatformGetParent();
}
if (coordinate_system == ui::AXCoordinateSystem::kScreenDIPs ||
coordinate_system == ui::AXCoordinateSystem::kScreenPhysicalPixels) {
// Most platforms include page scale factor in the transform on the root
// node of the AXTree. That transform gets applied by the call to
// RelativeToTreeBounds() in the loop above. However, if the root transform
// did not include page scale factor, we need to apply it now.
// TODO(crbug.com/40686662): this should probably apply visual viewport
// offset as well.
bool should_include_page_scale_factor_in_root = false;
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)
should_include_page_scale_factor_in_root = true;
#endif
if (!should_include_page_scale_factor_in_root) {
BrowserAccessibilityManager* root_manager =
manager()->GetManagerForRootFrame();
if (root_manager)
bounds.Scale(root_manager->GetPageScaleFactor());
}
bounds.Offset(
manager()->GetViewBoundsInScreenCoordinates().OffsetFromOrigin());
}
if (offscreen_result) {
*offscreen_result = offscreen ? ui::AXOffscreenResult::kOffscreen
: ui::AXOffscreenResult::kOnscreen;
}
return gfx::ToEnclosingRect(bounds);
}
bool BrowserAccessibility::IsOffscreen() const {
ui::AXOffscreenResult offscreen_result = ui::AXOffscreenResult::kOnscreen;
RelativeToAbsoluteBounds(gfx::RectF(), ui::AXCoordinateSystem::kRootFrame,
ui::AXClippingBehavior::kClipped, &offscreen_result);
return offscreen_result == ui::AXOffscreenResult::kOffscreen;
}
bool BrowserAccessibility::IsWebContent() const {
return true;
}
bool BrowserAccessibility::HasVisibleCaretOrSelection() const {
// The caret should be visible if Caret Browsing is enabled.
//
// TODO(crbug.com/40674120): Caret Browsing should be looking at leaf text
// nodes so it might not return expected results in this method.
if (ui::AXPlatform::GetInstance().IsCaretBrowsingEnabled()) {
return true;
}
return node()->HasVisibleCaretOrSelection();
}
std::vector<ui::AXPlatformNode*>
BrowserAccessibility::GetSourceNodesForReverseRelations(
ax::mojom::IntAttribute attr) {
DCHECK(manager_);
DCHECK(ui::IsNodeIdIntAttribute(attr));
return GetNodesFromRelationIdSet(
manager_->ax_tree()->GetReverseRelations(attr, GetData().id));
}
std::vector<ui::AXPlatformNode*>
BrowserAccessibility::GetSourceNodesForReverseRelations(
ax::mojom::IntListAttribute attr) {
DCHECK(manager_);
DCHECK(ui::IsNodeIdIntListAttribute(attr));
return GetNodesFromRelationIdSet(
manager_->ax_tree()->GetReverseRelations(attr, GetData().id));
}
const ui::AXUniqueId& BrowserAccessibility::GetUniqueId() const {
// This is not the same as GetData().id which comes from Blink, because
// those ids are only unique within the Blink process. We need one that is
// unique per OS window.
// For example, Windows ATs use this to retrieve IA2 event targets for events
// that are fired on an OS-level window with an id. They also use it to
// save positions via IAccessible2::get_uniqueID().
return unique_id_;
}
std::string BrowserAccessibility::SubtreeToStringHelper(size_t level) {
std::string result(level * 2, '+');
result += ToString();
result += '\n';
for (InternalChildIterator it = InternalChildrenBegin();
it != InternalChildrenEnd(); ++it) {
BrowserAccessibility* child = it.get();
DCHECK(child);
result += child->SubtreeToStringHelper(level + 1);
}
return result;
}
// TODO(crbug.com/337737555): This extra hop seems redundant, but
// unintuitively, this is the only override of NotifyAccessibilityApiUsage, so
// the the other inheritors of AXPlatformNodeDelegate don't actually ever send
// this notification. But, if this was refactored to be directly called, we end
// up failing bots due to the fact that this can be called by our own API usage,
// which is tracked by the linked bug.
void BrowserAccessibility::NotifyAccessibilityApiUsage() const {
ui::AXPlatform::GetInstance().NotifyAccessibilityApiUsage();
}
const std::vector<gfx::NativeViewAccessible>
BrowserAccessibility::GetUIADirectChildrenInRange(
ui::AXPlatformNodeDelegate* start,
ui::AXPlatformNodeDelegate* end) {
// This method is only called on Windows. Other platforms should not call it.
// The BrowserAccessibilityWin subclass overrides this method.
NOTREACHED_IN_MIGRATION();
return {};
}
//
// AXPlatformNodeDelegate.
//
gfx::NativeViewAccessible BrowserAccessibility::GetParent() const {
BrowserAccessibility* parent = PlatformGetParent();
if (parent)
return parent->GetNativeViewAccessible();
ui::AXPlatformTreeManagerDelegate* delegate =
manager_->GetDelegateFromRootManager();
if (!delegate)
return nullptr;
return delegate->AccessibilityGetNativeViewAccessible();
}
size_t BrowserAccessibility::GetChildCount() const {
return PlatformChildCount();
}
gfx::NativeViewAccessible BrowserAccessibility::ChildAtIndex(
size_t index) const {
BrowserAccessibility* child = PlatformGetChild(index);
if (!child)
return nullptr;
return child->GetNativeViewAccessible();
}
gfx::NativeViewAccessible BrowserAccessibility::GetFirstChild() const {
BrowserAccessibility* child = PlatformGetFirstChild();
if (!child)
return nullptr;
return child->GetNativeViewAccessible();
}
gfx::NativeViewAccessible BrowserAccessibility::GetLastChild() const {
BrowserAccessibility* child = PlatformGetLastChild();
if (!child)
return nullptr;
return child->GetNativeViewAccessible();
}
gfx::NativeViewAccessible BrowserAccessibility::GetNextSibling() const {
BrowserAccessibility* sibling = PlatformGetNextSibling();
if (!sibling)
return nullptr;
return sibling->GetNativeViewAccessible();
}
gfx::NativeViewAccessible BrowserAccessibility::GetPreviousSibling() const {
BrowserAccessibility* sibling = PlatformGetPreviousSibling();
if (!sibling)
return nullptr;
return sibling->GetNativeViewAccessible();
}
bool BrowserAccessibility::IsLeaf() const {
// According to the ARIA and Core-AAM specs:
// https://w3c.github.io/aria/#button,
// https://www.w3.org/TR/core-aam-1.1/#exclude_elements
// button's children are presentational only and should be hidden from
// screen readers. However, we cannot enforce the leafiness of buttons
// because they may contain many rich, interactive descendants such as a day
// in a calendar, and screen readers will need to interact with these
// contents. See https://crbug.com/689204.
// So we decided to not enforce the leafiness of buttons and expose all
// children. The only exception to enforce leafiness is when the button has
// a single text child and to prevent screen readers from double speak.
if (GetRole() == ax::mojom::Role::kButton) {
size_t child_count = InternalChildCount();
return !child_count ||
(child_count == 1 && InternalGetFirstChild()->IsText());
}
if (PlatformGetRootOfChildTree())
return false; // This object is hosting another tree.
return node()->IsLeaf();
}
bool BrowserAccessibility::IsFocused() const {
// TODO(nektar): Create an `ax_focus` class to share focus state between Views
// and Web.
return manager()->GetFocus() == this;
}
bool BrowserAccessibility::IsPlatformDocument() const {
return ui::IsPlatformDocument(GetRole());
}
gfx::NativeViewAccessible BrowserAccessibility::GetLowestPlatformAncestor()
const {
BrowserAccessibility* lowest_platform_ancestor =
PlatformGetLowestPlatformAncestor();
if (lowest_platform_ancestor)
return lowest_platform_ancestor->GetNativeViewAccessible();
return nullptr;
}
gfx::NativeViewAccessible BrowserAccessibility::GetTextFieldAncestor() const {
BrowserAccessibility* text_field_ancestor = PlatformGetTextFieldAncestor();
if (text_field_ancestor)
return text_field_ancestor->GetNativeViewAccessible();
return nullptr;
}
gfx::NativeViewAccessible BrowserAccessibility::GetSelectionContainer() const {
BrowserAccessibility* selection_container = PlatformGetSelectionContainer();
if (selection_container)
return selection_container->GetNativeViewAccessible();
return nullptr;
}
gfx::NativeViewAccessible BrowserAccessibility::GetTableAncestor() const {
BrowserAccessibility* table_ancestor =
manager()->GetFromAXNode(node()->GetTableAncestor());
if (table_ancestor)
return table_ancestor->GetNativeViewAccessible();
return nullptr;
}
BrowserAccessibility::PlatformChildIterator::PlatformChildIterator(
const PlatformChildIterator& it)
: parent_(it.parent_), platform_iterator(it.platform_iterator) {}
BrowserAccessibility::PlatformChildIterator::PlatformChildIterator(
const BrowserAccessibility* parent,
BrowserAccessibility* child)
: parent_(parent), platform_iterator(parent, child) {
DCHECK(parent);
}
BrowserAccessibility::PlatformChildIterator::~PlatformChildIterator() = default;
BrowserAccessibility::PlatformChildIterator&
BrowserAccessibility::PlatformChildIterator::operator++() {
++platform_iterator;
return *this;
}
BrowserAccessibility::PlatformChildIterator
BrowserAccessibility::PlatformChildIterator::operator++(int) {
BrowserAccessibility::PlatformChildIterator previous_state = *this;
++platform_iterator;
return previous_state;
}
BrowserAccessibility::PlatformChildIterator&
BrowserAccessibility::PlatformChildIterator::operator--() {
--platform_iterator;
return *this;
}
BrowserAccessibility::PlatformChildIterator
BrowserAccessibility::PlatformChildIterator::operator--(int) {
BrowserAccessibility::PlatformChildIterator previous_state = *this;
--platform_iterator;
return previous_state;
}
BrowserAccessibility* BrowserAccessibility::PlatformChildIterator::get() const {
return platform_iterator.get();
}
gfx::NativeViewAccessible
BrowserAccessibility::PlatformChildIterator::GetNativeViewAccessible() const {
return platform_iterator->GetNativeViewAccessible();
}
std::optional<size_t>
BrowserAccessibility::PlatformChildIterator::GetIndexInParent() const {
if (platform_iterator == parent_->PlatformChildrenEnd().platform_iterator)
return parent_->PlatformChildCount();
return platform_iterator->GetIndexInParent();
}
BrowserAccessibility& BrowserAccessibility::PlatformChildIterator::operator*()
const {
return *platform_iterator;
}
BrowserAccessibility* BrowserAccessibility::PlatformChildIterator::operator->()
const {
return platform_iterator.get();
}
std::unique_ptr<ui::ChildIterator> BrowserAccessibility::ChildrenBegin() const {
return std::make_unique<PlatformChildIterator>(PlatformChildrenBegin());
}
std::unique_ptr<ui::ChildIterator> BrowserAccessibility::ChildrenEnd() const {
return std::make_unique<PlatformChildIterator>(PlatformChildrenEnd());
}
gfx::NativeViewAccessible BrowserAccessibility::HitTestSync(
int physical_pixel_x,
int physical_pixel_y) const {
BrowserAccessibility* accessible = manager_->CachingAsyncHitTest(
gfx::Point(physical_pixel_x, physical_pixel_y));
if (!accessible)
return nullptr;
return accessible->GetNativeViewAccessible();
}
gfx::NativeViewAccessible BrowserAccessibility::GetFocus() const {
BrowserAccessibility* focused = manager()->GetFocus();
if (!focused)
return nullptr;
return focused->GetNativeViewAccessible();
}
ui::AXPlatformNode* BrowserAccessibility::GetFromNodeID(int32_t id) {
BrowserAccessibility* node = manager_->GetFromID(id);
if (!node)
return nullptr;
return node->GetAXPlatformNode();
}
ui::AXPlatformNode* BrowserAccessibility::GetFromTreeIDAndNodeID(
const ui::AXTreeID& ax_tree_id,
int32_t id) {
BrowserAccessibilityManager* manager =
BrowserAccessibilityManager::FromID(ax_tree_id);
if (!manager)
return nullptr;
BrowserAccessibility* node = manager->GetFromID(id);
if (!node)
return nullptr;
return node->GetAXPlatformNode();
}
std::optional<size_t> BrowserAccessibility::GetIndexInParent() const {
if (manager()->GetBrowserAccessibilityRoot() == this &&
PlatformGetParent() == nullptr) {
// If it is a root node of WebContent, it doesn't have a parent and a
// valid index in parent. So it returns -1 in order to compute its
// index at AXPlatformNodeBase.
return std::nullopt;
}
return node()->GetUnignoredIndexInParent();
}
gfx::AcceleratedWidget
BrowserAccessibility::GetTargetForNativeAccessibilityEvent() {
ui::AXPlatformTreeManagerDelegate* root_delegate =
manager()->GetDelegateFromRootManager();
if (!root_delegate)
return gfx::kNullAcceleratedWidget;
return root_delegate->AccessibilityGetAcceleratedWidget();
}
ui::AXPlatformNode* BrowserAccessibility::GetTableCaption() const {
ui::AXNode* caption = node()->GetTableCaption();
if (caption) {
return const_cast<BrowserAccessibility*>(this)->GetFromNodeID(
caption->id());
}
return nullptr;
}
bool BrowserAccessibility::AccessibilityPerformAction(
const ui::AXActionData& data) {
// TODO(crbug.com/40672441): Move the ability to perform actions to
// `AXTreeManager`.
switch (data.action) {
case ax::mojom::Action::kDoDefault:
manager_->DoDefaultAction(*this);
return true;
case ax::mojom::Action::kBlur:
manager_->Blur(*this);
return true;
case ax::mojom::Action::kFocus:
manager_->SetFocus(*this);
return true;
case ax::mojom::Action::kScrollToPoint: {
// Convert the target point from screen coordinates to frame coordinates.
gfx::Point target =
data.target_point - manager_->GetBrowserAccessibilityRoot()
->GetUnclippedScreenBoundsRect()
.OffsetFromOrigin();
manager_->ScrollToPoint(*this, target);
return true;
}
case ax::mojom::Action::kScrollToMakeVisible:
manager_->ScrollToMakeVisible(
*this, data.target_rect, data.horizontal_scroll_alignment,
data.vertical_scroll_alignment, data.scroll_behavior);
return true;
case ax::mojom::Action::kSetScrollOffset:
manager_->SetScrollOffset(*this, data.target_point);
return true;
case ax::mojom::Action::kSetSelection: {
ui::AXActionData selection = data;
// Prioritize target_tree_id if it was provided, as it is possible on
// some platforms (such as IAccessible2) to initiate a selection in a
// different tree than the current node resides in, as long as the nodes
// being selected share an AXTree with each other.
BrowserAccessibilityManager* selection_manager = nullptr;
if (selection.target_tree_id != ui::AXTreeIDUnknown()) {
selection_manager =
BrowserAccessibilityManager::FromID(selection.target_tree_id);
} else {
selection_manager = manager_;
}
DCHECK(selection_manager);
// "data.anchor_offset" and "data.focus_offset" might need to be adjusted
// if the anchor or the focus nodes include ignored children.
const BrowserAccessibility* anchor_object =
selection_manager->GetFromID(selection.anchor_node_id);
DCHECK(anchor_object);
if (!anchor_object->IsLeaf()) {
DCHECK_GE(selection.anchor_offset, 0);
const BrowserAccessibility* anchor_child =
anchor_object->InternalGetChild(
static_cast<uint32_t>(selection.anchor_offset));
if (anchor_child) {
selection.anchor_offset =
static_cast<int>(anchor_child->node()->index_in_parent());
selection.anchor_node_id = anchor_child->node()->parent()->id();
} else {
// Since the child was not found, the only alternative is that this is
// an "after children" position.
selection.anchor_offset =
static_cast<int>(anchor_object->node()->children().size());
}
}
const BrowserAccessibility* focus_object =
selection_manager->GetFromID(selection.focus_node_id);
DCHECK(focus_object);
// Blink only supports selections between two nodes in the same tree.
DCHECK_EQ(anchor_object->GetTreeData().tree_id,
focus_object->GetTreeData().tree_id);
if (!focus_object->IsLeaf()) {
DCHECK_GE(selection.focus_offset, 0);
const BrowserAccessibility* focus_child =
focus_object->InternalGetChild(
static_cast<uint32_t>(selection.focus_offset));
if (focus_child) {
selection.focus_offset =
static_cast<int>(focus_child->node()->index_in_parent());
selection.focus_node_id = focus_child->node()->parent()->id();
} else {
// Since the child was not found, the only alternative is that this is
// an "after children" position.
selection.focus_offset =
static_cast<int>(focus_object->node()->children().size());
}
}
selection_manager->SetSelection(selection);
return true;
}
case ax::mojom::Action::kSetValue:
manager_->SetValue(*this, data.value);
return true;
case ax::mojom::Action::kSetSequentialFocusNavigationStartingPoint:
manager_->SetSequentialFocusNavigationStartingPoint(*this);
return true;
case ax::mojom::Action::kShowContextMenu:
manager_->ShowContextMenu(*this);
return true;
case ax::mojom::Action::kStitchChildTree:
CHECK_NE(data.target_tree_id, ui::AXTreeIDUnknown());
CHECK_EQ(data.target_tree_id, manager()->GetTreeID());
CHECK_EQ(data.target_node_id, node()->id());
CHECK_NE(data.child_tree_id, ui::AXTreeIDUnknown());
CHECK_NE(data.child_tree_id, manager()->GetTreeID())
<< "Circular tree stitching at node:\n"
<< *this;
manager()->StitchChildTree(*this, data.child_tree_id);
return true;
case ax::mojom::Action::kIncrement:
manager_->Increment(*this);
return true;
case ax::mojom::Action::kDecrement:
manager_->Decrement(*this);
return true;
case ax::mojom::Action::kExpand:
manager_->Expand(*this);
return true;
case ax::mojom::Action::kCollapse:
manager_->Collapse(*this);
return true;
case ax::mojom::Action::kScrollBackward:
case ax::mojom::Action::kScrollForward:
case ax::mojom::Action::kScrollUp:
case ax::mojom::Action::kScrollDown:
case ax::mojom::Action::kScrollLeft:
case ax::mojom::Action::kScrollRight:
manager_->Scroll(*this, data.action);
return true;
default:
return false;
}
}
std::u16string BrowserAccessibility::GetLocalizedStringForImageAnnotationStatus(
ax::mojom::ImageAnnotationStatus status) const {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
ContentClient* content_client = GetContentClient();
int message_id = 0;
switch (status) {
case ax::mojom::ImageAnnotationStatus::kEligibleForAnnotation:
message_id = IDS_AX_IMAGE_ELIGIBLE_FOR_ANNOTATION;
break;
case ax::mojom::ImageAnnotationStatus::kAnnotationPending:
message_id = IDS_AX_IMAGE_ANNOTATION_PENDING;
break;
case ax::mojom::ImageAnnotationStatus::kAnnotationAdult:
message_id = IDS_AX_IMAGE_ANNOTATION_ADULT;
break;
case ax::mojom::ImageAnnotationStatus::kAnnotationEmpty:
case ax::mojom::ImageAnnotationStatus::kAnnotationProcessFailed:
message_id = IDS_AX_IMAGE_ANNOTATION_NO_DESCRIPTION;
break;
case ax::mojom::ImageAnnotationStatus::kNone:
case ax::mojom::ImageAnnotationStatus::kWillNotAnnotateDueToScheme:
case ax::mojom::ImageAnnotationStatus::kIneligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kSilentlyEligibleForAnnotation:
case ax::mojom::ImageAnnotationStatus::kAnnotationSucceeded:
return std::u16string();
}
DCHECK(message_id);
return content_client->GetLocalizedString(message_id);
}
std::u16string
BrowserAccessibility::GetLocalizedRoleDescriptionForUnlabeledImage() const {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
ContentClient* content_client = GetContentClient();
return content_client->GetLocalizedString(
IDS_AX_UNLABELED_IMAGE_ROLE_DESCRIPTION);
}
std::u16string BrowserAccessibility::GetLocalizedStringForLandmarkType() const {
// This method is Web specific and thus cannot be move to `AXNode`.
ContentClient* content_client = GetContentClient();
switch (GetRole()) {
case ax::mojom::Role::kBanner:
case ax::mojom::Role::kHeader:
return content_client->GetLocalizedString(IDS_AX_ROLE_BANNER);
case ax::mojom::Role::kComplementary:
return content_client->GetLocalizedString(IDS_AX_ROLE_COMPLEMENTARY);
case ax::mojom::Role::kContentInfo:
case ax::mojom::Role::kFooter:
return content_client->GetLocalizedString(IDS_AX_ROLE_CONTENT_INFO);
case ax::mojom::Role::kRegion:
return content_client->GetLocalizedString(IDS_AX_ROLE_REGION);
default:
return {};
}
}
std::u16string BrowserAccessibility::GetLocalizedStringForRoleDescription()
const {
// TODO(nektar): Move this method to `AXNode` if possible.
// Localized role description strings live in ui/strings/ax_strings.grd
ContentClient* content_client = GetContentClient();
switch (GetRole()) {
// Things which should never have a role description.
case ax::mojom::Role::kNone:
case ax::mojom::Role::kGenericContainer:
case ax::mojom::Role::kIframePresentational:
case ax::mojom::Role::kImeCandidate:
case ax::mojom::Role::kInlineTextBox:
case ax::mojom::Role::kLayoutTable:
case ax::mojom::Role::kLayoutTableCell:
case ax::mojom::Role::kLayoutTableRow:
case ax::mojom::Role::kLineBreak:
case ax::mojom::Role::kListMarker:
case ax::mojom::Role::kRuby:
case ax::mojom::Role::kRubyAnnotation:
case ax::mojom::Role::kStaticText:
case ax::mojom::Role::kUnknown:
return {};
// TODO(accessibility): Should any of these have a role description?
case ax::mojom::Role::kAbbr:
case ax::mojom::Role::kCaption:
case ax::mojom::Role::kCanvas:
case ax::mojom::Role::kCaret:
case ax::mojom::Role::kCell:
case ax::mojom::Role::kClient:
case ax::mojom::Role::kColumn:
case ax::mojom::Role::kComboBoxGrouping:
case ax::mojom::Role::kComboBoxMenuButton:
case ax::mojom::Role::kDesktop:
case ax::mojom::Role::kFigcaption:
case ax::mojom::Role::kGridCell:
case ax::mojom::Role::kGroup:
case ax::mojom::Role::kIframe:
case ax::mojom::Role::kLegend:
case ax::mojom::Role::kKeyboard:
case ax::mojom::Role::kLabelText:
case ax::mojom::Role::kList:
case ax::mojom::Role::kListBoxOption:
case ax::mojom::Role::kListItem:
case ax::mojom::Role::kMenuListOption:
case ax::mojom::Role::kMenuListPopup:
case ax::mojom::Role::kPane:
case ax::mojom::Role::kParagraph:
case ax::mojom::Role::kPdfRoot:
case ax::mojom::Role::kRow:
case ax::mojom::Role::kScrollView:
case ax::mojom::Role::kTableHeaderContainer:
case ax::mojom::Role::kTextFieldWithComboBox:
case ax::mojom::Role::kTitleBar:
case ax::mojom::Role::kWebView:
case ax::mojom::Role::kWindow:
return {};
// DPUB-ARIA Roles
case ax::mojom::Role::kDocAbstract:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_ABSTRACT);
case ax::mojom::Role::kDocAcknowledgments:
return content_client->GetLocalizedString(
IDS_AX_ROLE_DOC_ACKNOWLEDGMENTS);
case ax::mojom::Role::kDocAfterword:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_AFTERWORD);
case ax::mojom::Role::kDocAppendix:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_APPENDIX);
case ax::mojom::Role::kDocBackLink:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_BACKLINK);
case ax::mojom::Role::kDocBiblioEntry:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_BIBLIO_ENTRY);
case ax::mojom::Role::kDocBibliography:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_BIBLIOGRAPHY);
case ax::mojom::Role::kDocBiblioRef:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_BIBLIO_REF);
case ax::mojom::Role::kDocChapter:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_CHAPTER);
case ax::mojom::Role::kDocColophon:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_COLOPHON);
case ax::mojom::Role::kDocConclusion:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_CONCLUSION);
case ax::mojom::Role::kDocCover:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_COVER);
case ax::mojom::Role::kDocCredit:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_CREDIT);
case ax::mojom::Role::kDocCredits:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_CREDITS);
case ax::mojom::Role::kDocDedication:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_DEDICATION);
case ax::mojom::Role::kDocEndnote:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_ENDNOTE);
case ax::mojom::Role::kDocEndnotes:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_ENDNOTES);
case ax::mojom::Role::kDocEpigraph:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_EPIGRAPH);
case ax::mojom::Role::kDocEpilogue:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_EPILOGUE);
case ax::mojom::Role::kDocErrata:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_ERRATA);
case ax::mojom::Role::kDocExample:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_EXAMPLE);
case ax::mojom::Role::kDocFootnote:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_FOOTNOTE);
case ax::mojom::Role::kDocForeword:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_FOREWORD);
case ax::mojom::Role::kDocGlossary:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_GLOSSARY);
case ax::mojom::Role::kDocGlossRef:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_GLOSS_REF);
case ax::mojom::Role::kDocIndex:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_INDEX);
case ax::mojom::Role::kDocIntroduction:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_INTRODUCTION);
case ax::mojom::Role::kDocNoteRef:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_NOTE_REF);
case ax::mojom::Role::kDocNotice:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_NOTICE);
case ax::mojom::Role::kDocPageBreak:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PAGE_BREAK);
case ax::mojom::Role::kDocPageFooter:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PAGE_FOOTER);
case ax::mojom::Role::kDocPageHeader:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PAGE_HEADER);
case ax::mojom::Role::kDocPageList:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PAGE_LIST);
case ax::mojom::Role::kDocPart:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PART);
case ax::mojom::Role::kDocPreface:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PREFACE);
case ax::mojom::Role::kDocPrologue:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PROLOGUE);
case ax::mojom::Role::kDocPullquote:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_PULLQUOTE);
case ax::mojom::Role::kDocQna:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_QNA);
case ax::mojom::Role::kDocSubtitle:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_SUBTITLE);
case ax::mojom::Role::kDocTip:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_TIP);
case ax::mojom::Role::kDocToc:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOC_TOC);
// Graphics ARIA Roles
case ax::mojom::Role::kGraphicsDocument:
return content_client->GetLocalizedString(IDS_AX_ROLE_GRAPHICS_DOCUMENT);
case ax::mojom::Role::kGraphicsObject:
return content_client->GetLocalizedString(IDS_AX_ROLE_GRAPHICS_OBJECT);
case ax::mojom::Role::kGraphicsSymbol:
return content_client->GetLocalizedString(IDS_AX_ROLE_GRAPHICS_SYMBOL);
// MathML Roles
case ax::mojom::Role::kMathMLMath:
return content_client->GetLocalizedString(IDS_AX_ROLE_MATH);
case ax::mojom::Role::kMathMLFraction:
case ax::mojom::Role::kMathMLIdentifier:
case ax::mojom::Role::kMathMLMultiscripts:
case ax::mojom::Role::kMathMLNoneScript:
case ax::mojom::Role::kMathMLNumber:
case ax::mojom::Role::kMathMLOperator:
case ax::mojom::Role::kMathMLOver:
case ax::mojom::Role::kMathMLPrescriptDelimiter:
case ax::mojom::Role::kMathMLRoot:
case ax::mojom::Role::kMathMLRow:
case ax::mojom::Role::kMathMLSquareRoot:
case ax::mojom::Role::kMathMLStringLiteral:
case ax::mojom::Role::kMathMLSub:
case ax::mojom::Role::kMathMLSubSup:
case ax::mojom::Role::kMathMLSup:
case ax::mojom::Role::kMathMLTable:
case ax::mojom::Role::kMathMLTableCell:
case ax::mojom::Role::kMathMLTableRow:
case ax::mojom::Role::kMathMLText:
case ax::mojom::Role::kMathMLUnder:
case ax::mojom::Role::kMathMLUnderOver:
return {};
// All Other Roles
case ax::mojom::Role::kAlert:
return content_client->GetLocalizedString(IDS_AX_ROLE_ALERT);
case ax::mojom::Role::kAlertDialog:
return content_client->GetLocalizedString(IDS_AX_ROLE_ALERT_DIALOG);
case ax::mojom::Role::kApplication:
return content_client->GetLocalizedString(IDS_AX_ROLE_APPLICATION);
case ax::mojom::Role::kArticle:
return content_client->GetLocalizedString(IDS_AX_ROLE_ARTICLE);
case ax::mojom::Role::kAudio:
// Android returns IDS_AX_MEDIA_AUDIO_ELEMENT, but the string is the same.
return content_client->GetLocalizedString(IDS_AX_ROLE_AUDIO);
case ax::mojom::Role::kBanner:
return content_client->GetLocalizedString(IDS_AX_ROLE_BANNER);
case ax::mojom::Role::kBlockquote:
return content_client->GetLocalizedString(IDS_AX_ROLE_BLOCKQUOTE);
case ax::mojom::Role::kButton:
return content_client->GetLocalizedString(IDS_AX_ROLE_BUTTON);
case ax::mojom::Role::kCheckBox:
return content_client->GetLocalizedString(IDS_AX_ROLE_CHECK_BOX);
case ax::mojom::Role::kCode:
return content_client->GetLocalizedString(IDS_AX_ROLE_CODE);
case ax::mojom::Role::kColorWell:
return content_client->GetLocalizedString(IDS_AX_ROLE_COLOR_WELL);
case ax::mojom::Role::kColumnHeader:
return content_client->GetLocalizedString(IDS_AX_ROLE_COLUMN_HEADER);
case ax::mojom::Role::kComboBoxSelect:
// TODO(crbug.com/40864556): This is used for Mac AXRoleDescription. This
// should be changed at the same time we map this role to
// NSAccessibilityComboBoxRole.
return content_client->GetLocalizedString(IDS_AX_ROLE_POP_UP_BUTTON);
case ax::mojom::Role::kComment:
return content_client->GetLocalizedString(IDS_AX_ROLE_COMMENT);
case ax::mojom::Role::kComplementary:
return content_client->GetLocalizedString(IDS_AX_ROLE_COMPLEMENTARY);
case ax::mojom::Role::kContentDeletion:
return content_client->GetLocalizedString(IDS_AX_ROLE_CONTENT_DELETION);
case ax::mojom::Role::kContentInfo:
return content_client->GetLocalizedString(IDS_AX_ROLE_CONTENT_INFO);
case ax::mojom::Role::kContentInsertion:
return content_client->GetLocalizedString(IDS_AX_ROLE_CONTENT_INSERTION);
case ax::mojom::Role::kDate:
return content_client->GetLocalizedString(IDS_AX_ROLE_DATE);
case ax::mojom::Role::kDateTime: {
std::string input_type;
if (GetStringAttribute(ax::mojom::StringAttribute::kInputType,
&input_type)) {
if (input_type == "datetime-local") {
return content_client->GetLocalizedString(
IDS_AX_ROLE_DATE_TIME_LOCAL);
} else if (input_type == "week") {
return content_client->GetLocalizedString(IDS_AX_ROLE_WEEK);
} else if (input_type == "month") {
return content_client->GetLocalizedString(IDS_AX_ROLE_MONTH);
}
}
return content_client->GetLocalizedString(IDS_AX_ROLE_DATE_TIME);
}
case ax::mojom::Role::kDefinition:
return content_client->GetLocalizedString(IDS_AX_ROLE_DEFINITION);
case ax::mojom::Role::kDescriptionList:
return content_client->GetLocalizedString(IDS_AX_ROLE_DESCRIPTION_LIST);
case ax::mojom::Role::kDetails:
return content_client->GetLocalizedString(IDS_AX_ROLE_DETAILS);
case ax::mojom::Role::kDialog:
return content_client->GetLocalizedString(IDS_AX_ROLE_DIALOG);
case ax::mojom::Role::kDisclosureTriangle:
case ax::mojom::Role::kDisclosureTriangleGrouped:
return content_client->GetLocalizedString(
IDS_AX_ROLE_DISCLOSURE_TRIANGLE);
case ax::mojom::Role::kDocument:
return content_client->GetLocalizedString(IDS_AX_ROLE_DOCUMENT);
case ax::mojom::Role::kEmbeddedObject:
return content_client->GetLocalizedString(IDS_AX_ROLE_EMBEDDED_OBJECT);
case ax::mojom::Role::kEmphasis:
return content_client->GetLocalizedString(IDS_AX_ROLE_EMPHASIS);
case ax::mojom::Role::kFeed:
return content_client->GetLocalizedString(IDS_AX_ROLE_FEED);
case ax::mojom::Role::kFigure:
return content_client->GetLocalizedString(IDS_AX_ROLE_FIGURE);
case ax::mojom::Role::kFooter:
return content_client->GetLocalizedString(IDS_AX_ROLE_FOOTER);
case ax::mojom::Role::kFooterAsNonLandmark:
return content_client->GetLocalizedString(IDS_AX_ROLE_FOOTER);
case ax::mojom::Role::kForm:
return content_client->GetLocalizedString(IDS_AX_ROLE_FORM);
case ax::mojom::Role::kGrid:
return content_client->GetLocalizedString(IDS_AX_ROLE_TABLE);
case ax::mojom::Role::kHeader:
return content_client->GetLocalizedString(IDS_AX_ROLE_HEADER);
case ax::mojom::Role::kHeaderAsNonLandmark:
return content_client->GetLocalizedString(IDS_AX_ROLE_HEADER);
case ax::mojom::Role::kHeading:
return content_client->GetLocalizedString(IDS_AX_ROLE_HEADING);
case ax::mojom::Role::kImage:
return content_client->GetLocalizedString(IDS_AX_ROLE_GRAPHIC);
case ax::mojom::Role::kInputTime:
return content_client->GetLocalizedString(IDS_AX_ROLE_INPUT_TIME);
case ax::mojom::Role::kLink:
return content_client->GetLocalizedString(IDS_AX_ROLE_LINK);
case ax::mojom::Role::kListBox:
return content_client->GetLocalizedString(IDS_AX_ROLE_LIST_BOX);
case ax::mojom::Role::kListGrid:
return {};
case ax::mojom::Role::kLog:
return content_client->GetLocalizedString(IDS_AX_ROLE_LOG);
case ax::mojom::Role::kMain:
return content_client->GetLocalizedString(IDS_AX_ROLE_MAIN_CONTENT);
case ax::mojom::Role::kMark:
return content_client->GetLocalizedString(IDS_AX_ROLE_MARK);
case ax::mojom::Role::kMarquee:
return content_client->GetLocalizedString(IDS_AX_ROLE_MARQUEE);
case ax::mojom::Role::kMath:
return content_client->GetLocalizedString(IDS_AX_ROLE_MATH);
case ax::mojom::Role::kMenu:
return content_client->GetLocalizedString(IDS_AX_ROLE_MENU);
case ax::mojom::Role::kMenuBar:
return content_client->GetLocalizedString(IDS_AX_ROLE_MENU_BAR);
case ax::mojom::Role::kMenuItem:
return content_client->GetLocalizedString(IDS_AX_ROLE_MENU_ITEM);
case ax::mojom::Role::kMenuItemCheckBox:
return {};
case ax::mojom::Role::kMenuItemRadio:
return {};
case ax::mojom::Role::kMeter:
return content_client->GetLocalizedString(IDS_AX_ROLE_METER);
case ax::mojom::Role::kNavigation:
return content_client->GetLocalizedString(IDS_AX_ROLE_NAVIGATIONAL_LINK);
case ax::mojom::Role::kNote:
return content_client->GetLocalizedString(IDS_AX_ROLE_NOTE);
case ax::mojom::Role::kPdfActionableHighlight:
return content_client->GetLocalizedString(IDS_AX_ROLE_PDF_HIGHLIGHT);
case ax::mojom::Role::kPluginObject:
return content_client->GetLocalizedString(IDS_AX_ROLE_EMBEDDED_OBJECT);
case ax::mojom::Role::kPopUpButton:
return content_client->GetLocalizedString(IDS_AX_ROLE_POP_UP_BUTTON);
case ax::mojom::Role::kPortal:
return {};
case ax::mojom::Role::kProgressIndicator:
return content_client->GetLocalizedString(IDS_AX_ROLE_PROGRESS_INDICATOR);
case ax::mojom::Role::kRadioButton:
return content_client->GetLocalizedString(IDS_AX_ROLE_RADIO);
case ax::mojom::Role::kRadioGroup:
return content_client->GetLocalizedString(IDS_AX_ROLE_RADIO_GROUP);
case ax::mojom::Role::kRegion:
return content_client->GetLocalizedString(IDS_AX_ROLE_REGION);
case ax::mojom::Role::kRootWebArea:
// There is IDS_AX_ROLE_WEB_AREA, but only the mac seems to use it.
return {};
case ax::mojom::Role::kRowGroup:
return content_client->GetLocalizedString(IDS_AX_ROLE_ROW_GROUP);
case ax::mojom::Role::kRowHeader:
return content_client->GetLocalizedString(IDS_AX_ROLE_ROW_HEADER);
case ax::mojom::Role::kScrollBar:
return content_client->GetLocalizedString(IDS_AX_ROLE_SCROLL_BAR);
case ax::mojom::Role::kSearch:
return content_client->GetLocalizedString(IDS_AX_ROLE_SEARCH);
case ax::mojom::Role::kSearchBox:
return content_client->GetLocalizedString(IDS_AX_ROLE_SEARCH_BOX);
case ax::mojom::Role::kSection:
case ax::mojom::Role::kSectionWithoutName:
// While there is an IDS_AX_ROLE_SECTION, no one seems to be using it.
return {};
case ax::mojom::Role::kSlider:
return content_client->GetLocalizedString(IDS_AX_ROLE_SLIDER);
case ax::mojom::Role::kSpinButton:
return content_client->GetLocalizedString(IDS_AX_ROLE_SPIN_BUTTON);
case ax::mojom::Role::kSplitter:
return content_client->GetLocalizedString(IDS_AX_ROLE_SPLITTER);
case ax::mojom::Role::kStatus:
return content_client->GetLocalizedString(IDS_AX_ROLE_STATUS);
case ax::mojom::Role::kStrong:
return content_client->GetLocalizedString(IDS_AX_ROLE_STRONG);
case ax::mojom::Role::kSubscript:
return content_client->GetLocalizedString(IDS_AX_ROLE_SUBSCRIPT);
case ax::mojom::Role::kSuggestion:
return content_client->GetLocalizedString(IDS_AX_ROLE_SUGGESTION);
case ax::mojom::Role::kSuperscript:
return content_client->GetLocalizedString(IDS_AX_ROLE_SUPERSCRIPT);
case ax::mojom::Role::kSvgRoot:
return content_client->GetLocalizedString(IDS_AX_ROLE_GRAPHIC);
case ax::mojom::Role::kSwitch:
return content_client->GetLocalizedString(IDS_AX_ROLE_SWITCH);
case ax::mojom::Role::kTab:
return content_client->GetLocalizedString(IDS_AX_ROLE_TAB);
case ax::mojom::Role::kTabList:
return content_client->GetLocalizedString(IDS_AX_ROLE_TAB_LIST);
case ax::mojom::Role::kTabPanel:
return content_client->GetLocalizedString(IDS_AX_ROLE_TAB_PANEL);
case ax::mojom::Role::kTable:
return content_client->GetLocalizedString(IDS_AX_ROLE_TABLE);
case ax::mojom::Role::kTerm:
return content_client->GetLocalizedString(IDS_AX_ROLE_DESCRIPTION_TERM);
case ax::mojom::Role::kTextField: {
std::string input_type;
if (GetStringAttribute(ax::mojom::StringAttribute::kInputType,
&input_type)) {
if (input_type == "email") {
return content_client->GetLocalizedString(IDS_AX_ROLE_EMAIL);
} else if (input_type == "tel") {
return content_client->GetLocalizedString(IDS_AX_ROLE_TELEPHONE);
} else if (input_type == "url") {
return content_client->GetLocalizedString(IDS_AX_ROLE_URL);
}
}
return {};
}
case ax::mojom::Role::kTime:
return content_client->GetLocalizedString(IDS_AX_ROLE_TIME);
case ax::mojom::Role::kTimer:
return content_client->GetLocalizedString(IDS_AX_ROLE_TIMER);
case ax::mojom::Role::kToggleButton:
return content_client->GetLocalizedString(IDS_AX_ROLE_TOGGLE_BUTTON);
case ax::mojom::Role::kToolbar:
return content_client->GetLocalizedString(IDS_AX_ROLE_TOOLBAR);
case ax::mojom::Role::kTooltip:
return content_client->GetLocalizedString(IDS_AX_ROLE_TOOLTIP);
case ax::mojom::Role::kTree:
return content_client->GetLocalizedString(IDS_AX_ROLE_TREE);
case ax::mojom::Role::kTreeGrid:
return content_client->GetLocalizedString(IDS_AX_ROLE_TREE_GRID);
case ax::mojom::Role::kTreeItem:
return content_client->GetLocalizedString(IDS_AX_ROLE_TREE_ITEM);
case ax::mojom::Role::kVideo:
// Android returns IDS_AX_MEDIA_VIDEO_ELEMENT.
return {};
case ax::mojom::Role::kDescriptionListTermDeprecated:
case ax::mojom::Role::kPreDeprecated:
case ax::mojom::Role::kDescriptionListDetailDeprecated:
case ax::mojom::Role::kDirectoryDeprecated:
NOTREACHED_NORETURN();
}
}
std::u16string BrowserAccessibility::GetStyleNameAttributeAsLocalizedString()
const {
// This method is Web specific and thus cannot be moved to `AXNode`.
const BrowserAccessibility* current_node = this;
while (current_node) {
if (current_node->GetRole() == ax::mojom::Role::kMark) {
ContentClient* content_client = GetContentClient();
return content_client->GetLocalizedString(IDS_AX_ROLE_MARK);
}
current_node = current_node->PlatformGetParent();
}
return {};
}
bool BrowserAccessibility::ShouldIgnoreHoveredStateForTesting() {
return ignore_hovered_state_for_testing_;
}
std::optional<int> BrowserAccessibility::GetPosInSet() const {
return node()->GetPosInSet();
}
std::optional<int> BrowserAccessibility::GetSetSize() const {
return node()->GetSetSize();
}
bool BrowserAccessibility::IsInListMarker() const {
return node()->IsInListMarker();
}
BrowserAccessibility* BrowserAccessibility::GetCollapsedMenuListSelectAncestor()
const {
ui::AXNode* popup_button = node()->GetCollapsedMenuListSelectAncestor();
return manager()->GetFromAXNode(popup_button);
}
std::string BrowserAccessibility::ToString() const {
return GetData().ToString();
}
bool BrowserAccessibility::SetHypertextSelection(int start_offset,
int end_offset) {
// TODO(nektar): Move to `AXNode` as soon as hypertext computation is fully
// migrated to that class.
manager()->SetSelection(AXRange(CreatePositionForSelectionAt(start_offset),
CreatePositionForSelectionAt(end_offset)));
return true;
}
BrowserAccessibility* BrowserAccessibility::PlatformGetRootOfChildTree() const {
std::string child_tree_id;
if (!GetStringAttribute(ax::mojom::StringAttribute::kChildTreeId,
&child_tree_id)) {
return nullptr;
}
DCHECK_EQ(node()->children().size(), 0u)
<< "A node should not have both children and a child tree.\n"
<< *node();
BrowserAccessibilityManager* child_manager =
BrowserAccessibilityManager::FromID(
ui::AXTreeID::FromString(child_tree_id));
if (child_manager &&
child_manager->GetBrowserAccessibilityRoot()->PlatformGetParent() == this)
return child_manager->GetBrowserAccessibilityRoot();
return nullptr;
}
ui::TextAttributeList BrowserAccessibility::ComputeTextAttributes() const {
return ui::TextAttributeList();
}
ui::TextAttributeMap BrowserAccessibility::GetSpellingAndGrammarAttributes()
const {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
ui::TextAttributeMap spelling_attributes;
if (IsText()) {
const std::vector<int32_t>& marker_types =
GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerTypes);
const std::vector<int32_t>& highlight_types =
GetIntListAttribute(ax::mojom::IntListAttribute::kHighlightTypes);
const std::vector<int>& marker_starts =
GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerStarts);
const std::vector<int>& marker_ends =
GetIntListAttribute(ax::mojom::IntListAttribute::kMarkerEnds);
CHECK_EQ(marker_types.size(), marker_starts.size());
CHECK_EQ(marker_types.size(), marker_ends.size());
for (size_t i = 0; i < marker_types.size(); ++i) {
bool is_spelling_error =
(marker_types[i] &
static_cast<int32_t>(ax::mojom::MarkerType::kSpelling)) ||
((marker_types[i] &
static_cast<int32_t>(ax::mojom::MarkerType::kHighlight)) &&
highlight_types[i] ==
static_cast<int32_t>(ax::mojom::HighlightType::kSpellingError));
bool is_grammar_error =
(marker_types[i] &
static_cast<int32_t>(ax::mojom::MarkerType::kGrammar)) ||
((marker_types[i] &
static_cast<int32_t>(ax::mojom::MarkerType::kHighlight)) &&
highlight_types[i] ==
static_cast<int32_t>(ax::mojom::HighlightType::kGrammarError));
if (!is_spelling_error && !is_grammar_error)
continue;
ui::TextAttributeList start_attributes;
if (is_spelling_error && is_grammar_error)
start_attributes.push_back(
std::make_pair("invalid", "spelling,grammar"));
else if (is_spelling_error)
start_attributes.push_back(std::make_pair("invalid", "spelling"));
else if (is_grammar_error)
start_attributes.push_back(std::make_pair("invalid", "grammar"));
int start_offset = marker_starts[i];
int end_offset = marker_ends[i];
spelling_attributes[start_offset] = start_attributes;
spelling_attributes[end_offset] = ui::TextAttributeList();
}
}
// In the case of a native text field, text marker information, such as
// misspellings, need to be collected from all the text field's descendants
// and exposed on the text field itself. Otherwise, assistive software (AT)
// won't be able to see them because the native field's descendants are an
// implementation detail that is hidden from AT.
if (IsAtomicTextField() && !node()->GetValueForControl().empty()) {
int start_offset = 0;
// Note that in PDFs, static_text will always be null. This is because text
// fields are not given descendants by `PdfAccessibilityTreeBuilder`.
for (BrowserAccessibility* static_text =
BrowserAccessibilityManager::NextTextOnlyObject(
InternalGetFirstChild());
static_text; static_text = static_text->InternalGetNextSibling()) {
DCHECK(static_text->IsDescendantOf(this));
ui::TextAttributeMap text_spelling_attributes =
static_text->GetSpellingAndGrammarAttributes();
for (auto& attribute : text_spelling_attributes) {
spelling_attributes[start_offset + attribute.first] =
std::move(attribute.second);
}
start_offset += static_cast<int>(static_text->GetHypertext().length());
}
}
return spelling_attributes;
}
// static
void BrowserAccessibility::MergeSpellingAndGrammarIntoTextAttributes(
const ui::TextAttributeMap& spelling_attributes,
int start_offset,
ui::TextAttributeMap* text_attributes) {
if (!text_attributes) {
NOTREACHED_IN_MIGRATION();
return;
}
ui::TextAttributeList prev_attributes;
for (const auto& spelling_attribute : spelling_attributes) {
int offset = start_offset + spelling_attribute.first;
auto iterator = text_attributes->find(offset);
if (iterator == text_attributes->end()) {
text_attributes->emplace(offset, prev_attributes);
iterator = text_attributes->find(offset);
} else {
prev_attributes = iterator->second;
}
ui::TextAttributeList& existing_attributes = iterator->second;
// There might be a spelling attribute already in the list of text
// attributes, originating from "aria-invalid", that is being overwritten
// by a spelling marker. If it already exists, prefer it over this
// automatically computed attribute.
if (!HasInvalidAttribute(existing_attributes)) {
// Does not exist -- insert our own.
existing_attributes.insert(existing_attributes.end(),
spelling_attribute.second.begin(),
spelling_attribute.second.end());
}
}
}
ui::TextAttributeMap BrowserAccessibility::ComputeTextAttributeMap(
const ui::TextAttributeList& default_attributes) const {
// TODO(crbug.com/40672441): This is one of the few methods that won't be
// moved to `AXNode` in the foreseeable future because the functionality it
// provides is not immediately needed in Views.
ui::TextAttributeMap attributes_map;
if (IsLeaf()) {
attributes_map[0] = default_attributes;
const ui::TextAttributeMap spelling_attributes =
GetSpellingAndGrammarAttributes();
MergeSpellingAndGrammarIntoTextAttributes(
spelling_attributes, 0 /* start_offset */, &attributes_map);
return attributes_map;
}
DCHECK(PlatformChildCount());
int start_offset = 0;
for (const auto& child : PlatformChildren()) {
ui::TextAttributeList attributes(child.ComputeTextAttributes());
if (attributes_map.empty()) {
attributes_map[start_offset] = attributes;
} else {
// Only add the attributes for this child if we are at the start of a new
// style span.
ui::TextAttributeList previous_attributes =
attributes_map.rbegin()->second;
// Must check the size, otherwise if attributes is a subset of
// prev_attributes, they would appear to be equal.
if (!base::ranges::equal(attributes, previous_attributes)) {
attributes_map[start_offset] = attributes;
}
}
if (child.IsText()) {
const ui::TextAttributeMap spelling_attributes =
child.GetSpellingAndGrammarAttributes();
MergeSpellingAndGrammarIntoTextAttributes(spelling_attributes,
start_offset, &attributes_map);
start_offset += child.GetHypertext().length();
} else {
start_offset += 1;
}
}
return attributes_map;
}
// static
bool BrowserAccessibility::HasInvalidAttribute(
const ui::TextAttributeList& attributes) {
return base::Contains(attributes, "invalid", &ui::TextAttribute::first);
}
static bool HasListAncestor(const BrowserAccessibility* node) {
if (node == nullptr)
return false;
if (ui::IsStaticList(node->GetRole()))
return true;
return HasListAncestor(node->InternalGetParent());
}
static bool HasListDescendant(const BrowserAccessibility* current,
const BrowserAccessibility* root) {
// Do not check the root when looking for a list descendant.
if (current != root && ui::IsStaticList(current->GetRole()))
return true;
for (auto it = current->InternalChildrenBegin();
it != current->InternalChildrenEnd(); ++it) {
if (HasListDescendant(it.get(), root))
return true;
}
return false;
}
bool BrowserAccessibility::IsHierarchicalList() const {
// TODO(nektar): Move this method to `AXNode` in the immediate future.
if (!ui::IsStaticList(GetRole()))
return false;
return HasListDescendant(this, this) || HasListAncestor(InternalGetParent());
}
} // namespace content