blob: 977a8e850c1aa11835c890485144821de14a4d0b [file] [log] [blame]
// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/accessibility/ax_node.h"
#include <algorithm>
#include <utility>
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_language_detection.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_table_info.h"
#include "ui/accessibility/ax_tree.h"
#include "ui/gfx/transform.h"
namespace ui {
constexpr AXNode::AXID AXNode::kInvalidAXID;
AXNode::AXNode(AXNode::OwnerTree* tree,
AXNode* parent,
int32_t id,
size_t index_in_parent,
size_t unignored_index_in_parent)
: tree_(tree),
index_in_parent_(index_in_parent),
unignored_index_in_parent_(unignored_index_in_parent),
parent_(parent) {
data_.id = id;
}
AXNode::~AXNode() = default;
size_t AXNode::GetUnignoredChildCount() const {
// TODO(nektar): Should DCHECK if the node is not ignored.
DCHECK(!tree_->GetTreeUpdateInProgressState());
return unignored_child_count_;
}
AXNodeData&& AXNode::TakeData() {
return std::move(data_);
}
AXNode* AXNode::GetUnignoredChildAtIndex(size_t index) const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
size_t count = 0;
for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
if (count == index)
return it.get();
++count;
}
return nullptr;
}
AXNode* AXNode::GetUnignoredParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
AXNode* result = parent();
while (result && result->IsIgnored())
result = result->parent();
return result;
}
size_t AXNode::GetUnignoredIndexInParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return unignored_index_in_parent_;
}
size_t AXNode::GetIndexInParent() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return index_in_parent_;
}
AXNode* AXNode::GetFirstUnignoredChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return ComputeFirstUnignoredChildRecursive();
}
AXNode* AXNode::GetLastUnignoredChild() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return ComputeLastUnignoredChildRecursive();
}
AXNode* AXNode::GetDeepestFirstUnignoredChild() const {
if (!GetUnignoredChildCount())
return nullptr;
AXNode* deepest_child = GetFirstUnignoredChild();
while (deepest_child->GetUnignoredChildCount()) {
deepest_child = deepest_child->GetFirstUnignoredChild();
}
return deepest_child;
}
AXNode* AXNode::GetDeepestLastUnignoredChild() const {
if (!GetUnignoredChildCount())
return nullptr;
AXNode* deepest_child = GetLastUnignoredChild();
while (deepest_child->GetUnignoredChildCount()) {
deepest_child = deepest_child->GetLastUnignoredChild();
}
return deepest_child;
}
// Search for the next sibling of this node, skipping over any ignored nodes
// encountered.
//
// In our search:
// If we find an ignored sibling, we consider its children as our siblings.
// If we run out of siblings, we consider an ignored parent's siblings as our
// own siblings.
//
// Note: this behaviour of 'skipping over' an ignored node makes this subtly
// different to finding the next (direct) sibling which is unignored.
//
// Consider a tree, where (i) marks a node as ignored:
//
// 1
// ├── 2
// ├── 3(i)
// │ └── 5
// └── 4
//
// The next sibling of node 2 is node 3, which is ignored.
// The next unignored sibling of node 2 could be either:
// 1) node 4 - next unignored sibling in the literal tree, or
// 2) node 5 - next unignored sibling in the logical document.
//
// There is no next sibling of node 5.
// The next unignored sibling of node 5 could be either:
// 1) null - no next sibling in the literal tree, or
// 2) node 4 - next unignored sibling in the logical document.
//
// In both cases, this method implements approach (2).
//
// TODO(chrishall): Can we remove this non-reflexive case by forbidding
// GetNextUnignoredSibling calls on an ignored started node?
// Note: this means that Next/Previous-UnignoredSibling are not reflexive if
// either of the nodes in question are ignored. From above we get an example:
// NextUnignoredSibling(3) is 4, but
// PreviousUnignoredSibling(4) is 5.
//
// The view of unignored siblings for node 3 includes both node 2 and node 4:
// 2 <-- [3(i)] --> 4
//
// Whereas nodes 2, 5, and 4 do not consider node 3 to be an unignored sibling:
// null <-- [2] --> 5
// 2 <-- [5] --> 4
// 5 <-- [4] --> null
AXNode* AXNode::GetNextUnignoredSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXNode* current = this;
// If there are children of the |current| node still to consider.
bool considerChildren = false;
while (current) {
// A |candidate| sibling to consider.
// If it is unignored then we have found our result.
// Otherwise promote it to |current| and consider its children.
AXNode* candidate;
if (considerChildren && (candidate = current->GetFirstChild())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
} else if ((candidate = current->GetNextSibling())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
// Look through the ignored candidate node to consider their children as
// though they were siblings.
considerChildren = true;
} else {
// Continue our search through a parent iff they are ignored.
//
// If |current| has an ignored parent, then we consider the parent's
// siblings as though they were siblings of |current|.
//
// Given a tree:
// 1
// ├── 2(?)
// │ └── [4]
// └── 3
//
// Node 4's view of siblings:
// literal tree: null <-- [4] --> null
//
// If node 2 is not ignored, then node 4's view doesn't change, and we
// have no more nodes to consider:
// unignored tree: null <-- [4] --> null
//
// If instead node 2 is ignored, then node 4's view of siblings grows to
// include node 3, and we have more nodes to consider:
// unignored tree: null <-- [4] --> 3
current = current->parent();
if (!current || !current->IsIgnored())
return nullptr;
// We have already considered all relevant descendants of |current|.
considerChildren = false;
}
}
return nullptr;
}
// Search for the previous sibling of this node, skipping over any ignored nodes
// encountered.
//
// In our search for a sibling:
// If we find an ignored sibling, we may consider its children as siblings.
// If we run out of siblings, we may consider an ignored parent's siblings as
// our own.
//
// See the documentation for |GetNextUnignoredSibling| for more details.
AXNode* AXNode::GetPreviousUnignoredSibling() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
const AXNode* current = this;
// If there are children of the |current| node still to consider.
bool considerChildren = false;
while (current) {
// A |candidate| sibling to consider.
// If it is unignored then we have found our result.
// Otherwise promote it to |current| and consider its children.
AXNode* candidate;
if (considerChildren && (candidate = current->GetLastChild())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
} else if ((candidate = current->GetPreviousSibling())) {
if (!candidate->IsIgnored())
return candidate;
current = candidate;
// Look through the ignored candidate node to consider their children as
// though they were siblings.
considerChildren = true;
} else {
// Continue our search through a parent iff they are ignored.
//
// If |current| has an ignored parent, then we consider the parent's
// siblings as though they were siblings of |current|.
//
// Given a tree:
// 1
// ├── 2
// └── 3(?)
// └── [4]
//
// Node 4's view of siblings:
// literal tree: null <-- [4] --> null
//
// If node 3 is not ignored, then node 4's view doesn't change, and we
// have no more nodes to consider:
// unignored tree: null <-- [4] --> null
//
// If instead node 3 is ignored, then node 4's view of siblings grows to
// include node 2, and we have more nodes to consider:
// unignored tree: 2 <-- [4] --> null
current = current->parent();
if (!current || !current->IsIgnored())
return nullptr;
// We have already considered all relevant descendants of |current|.
considerChildren = false;
}
}
return nullptr;
}
AXNode* AXNode::GetNextUnignoredInTreeOrder() const {
if (GetUnignoredChildCount())
return GetFirstUnignoredChild();
const AXNode* node = this;
while (node) {
AXNode* sibling = node->GetNextUnignoredSibling();
if (sibling)
return sibling;
node = node->GetUnignoredParent();
}
return nullptr;
}
AXNode* AXNode::GetPreviousUnignoredInTreeOrder() const {
AXNode* sibling = GetPreviousUnignoredSibling();
if (!sibling)
return GetUnignoredParent();
if (sibling->GetUnignoredChildCount())
return sibling->GetDeepestLastUnignoredChild();
return sibling;
}
AXNode::UnignoredChildIterator AXNode::UnignoredChildrenBegin() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildIterator(this, GetFirstUnignoredChild());
}
AXNode::UnignoredChildIterator AXNode::UnignoredChildrenEnd() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
return UnignoredChildIterator(this, nullptr);
}
// The first (direct) child, ignored or unignored.
AXNode* AXNode::GetFirstChild() const {
if (children().empty())
return nullptr;
return children()[0];
}
// The last (direct) child, ignored or unignored.
AXNode* AXNode::GetLastChild() const {
size_t n = children().size();
if (n == 0)
return nullptr;
return children()[n - 1];
}
// The previous (direct) sibling, ignored or unignored.
AXNode* AXNode::GetPreviousSibling() const {
// Root nodes lack a parent, their index_in_parent should be 0.
DCHECK(!parent() ? index_in_parent() == 0 : true);
size_t index = index_in_parent();
if (index == 0)
return nullptr;
return parent()->children()[index - 1];
}
// The next (direct) sibling, ignored or unignored.
AXNode* AXNode::GetNextSibling() const {
if (!parent())
return nullptr;
size_t nextIndex = index_in_parent() + 1;
if (nextIndex >= parent()->children().size())
return nullptr;
return parent()->children()[nextIndex];
}
bool AXNode::IsText() const {
// In Legacy Layout, a list marker has no children and is thus represented on
// all platforms as a leaf node that exposes the marker itself, i.e., it forms
// part of the AX tree's text representation. In contrast, in Layout NG, a
// list marker has a static text child.
if (data().role == ax::mojom::Role::kListMarker)
return !children().size();
return ui::IsText(data().role);
}
bool AXNode::IsLineBreak() const {
return data().role == ax::mojom::Role::kLineBreak ||
(data().role == ax::mojom::Role::kInlineTextBox &&
data().GetBoolAttribute(
ax::mojom::BoolAttribute::kIsLineBreakingObject));
}
void AXNode::SetData(const AXNodeData& src) {
data_ = src;
}
void AXNode::SetLocation(int32_t offset_container_id,
const gfx::RectF& location,
gfx::Transform* transform) {
data_.relative_bounds.offset_container_id = offset_container_id;
data_.relative_bounds.bounds = location;
if (transform) {
data_.relative_bounds.transform =
std::make_unique<gfx::Transform>(*transform);
} else {
data_.relative_bounds.transform.reset();
}
}
void AXNode::SetIndexInParent(size_t index_in_parent) {
index_in_parent_ = index_in_parent;
}
void AXNode::UpdateUnignoredCachedValues() {
if (!IsIgnored())
UpdateUnignoredCachedValuesRecursive(0);
}
void AXNode::SwapChildren(std::vector<AXNode*>* children) {
children->swap(children_);
}
void AXNode::Destroy() {
delete this;
}
bool AXNode::IsDescendantOf(const AXNode* ancestor) const {
if (this == ancestor)
return true;
if (parent())
return parent()->IsDescendantOf(ancestor);
return false;
}
std::vector<int> AXNode::GetOrComputeLineStartOffsets() {
std::vector<int> line_offsets;
if (data().GetIntListAttribute(ax::mojom::IntListAttribute::kCachedLineStarts,
&line_offsets)) {
return line_offsets;
}
int start_offset = 0;
ComputeLineStartOffsets(&line_offsets, &start_offset);
data_.AddIntListAttribute(ax::mojom::IntListAttribute::kCachedLineStarts,
line_offsets);
return line_offsets;
}
void AXNode::ComputeLineStartOffsets(std::vector<int>* line_offsets,
int* start_offset) const {
DCHECK(line_offsets);
DCHECK(start_offset);
for (const AXNode* child : children()) {
DCHECK(child);
if (!child->children().empty()) {
child->ComputeLineStartOffsets(line_offsets, start_offset);
continue;
}
// Don't report if the first piece of text starts a new line or not.
if (*start_offset && !child->data().HasIntAttribute(
ax::mojom::IntAttribute::kPreviousOnLineId)) {
// If there are multiple objects with an empty accessible label at the
// start of a line, only include a single line start offset.
if (line_offsets->empty() || line_offsets->back() != *start_offset)
line_offsets->push_back(*start_offset);
}
base::string16 text =
child->data().GetString16Attribute(ax::mojom::StringAttribute::kName);
*start_offset += static_cast<int>(text.length());
}
}
const std::string& AXNode::GetInheritedStringAttribute(
ax::mojom::StringAttribute attribute) const {
const AXNode* current_node = this;
do {
if (current_node->data().HasStringAttribute(attribute))
return current_node->data().GetStringAttribute(attribute);
current_node = current_node->parent();
} while (current_node);
return base::EmptyString();
}
base::string16 AXNode::GetInheritedString16Attribute(
ax::mojom::StringAttribute attribute) const {
return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute));
}
AXLanguageInfo* AXNode::GetLanguageInfo() const {
return language_info_.get();
}
void AXNode::SetLanguageInfo(std::unique_ptr<AXLanguageInfo> lang_info) {
language_info_ = std::move(lang_info);
}
void AXNode::ClearLanguageInfo() {
language_info_.reset();
}
std::string AXNode::GetHypertext() const {
if (IsLeaf())
return GetInnerText();
// Construct the hypertext for this node, which contains the concatenation of
// the inner text of this node's textual children, and an embedded object
// character for all the other children.
const std::string embedded_character_str("\xEF\xBF\xBC");
std::string hypertext;
for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
// Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK
// hypertext with the embedded object character. We copy all of their text
// instead.
if (it->IsText()) {
hypertext += it->GetInnerText();
} else {
hypertext += embedded_character_str;
}
}
return hypertext;
}
std::string AXNode::GetInnerText() const {
// If a text field has no descendants, then we compute its inner text from its
// value or its placeholder. Otherwise we prefer to look at its descendant
// text nodes because Blink doesn't always add all trailing white space to the
// value attribute.
const bool is_plain_text_field_without_descendants =
(data().IsTextField() && !GetUnignoredChildCount());
if (is_plain_text_field_without_descendants) {
std::string value =
data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
// If the value is empty, then there might be some placeholder text in the
// text field, or any other name that is derived from visible contents, even
// if the text field has no children.
if (!value.empty())
return value;
}
// Ordinarily, plain text fields are leaves. We need to exclude them from the
// set of leaf nodes when they expose any descendants. This is because we want
// to compute their inner text from their descendant text nodes as we don't
// always trust the "value" attribute provided by Blink.
const bool is_plain_text_field_with_descendants =
(data().IsTextField() && GetUnignoredChildCount());
if (IsLeaf() && !is_plain_text_field_with_descendants) {
switch (data().GetNameFrom()) {
case ax::mojom::NameFrom::kNone:
case ax::mojom::NameFrom::kUninitialized:
// The accessible name is not displayed on screen, e.g. aria-label, or is
// not displayed directly inside the node, e.g. an associated label
// element.
case ax::mojom::NameFrom::kAttribute:
// The node's accessible name is explicitly empty.
case ax::mojom::NameFrom::kAttributeExplicitlyEmpty:
// The accessible name does not represent the entirety of the node's inner
// text, e.g. a table's caption or a figure's figcaption.
case ax::mojom::NameFrom::kCaption:
case ax::mojom::NameFrom::kRelatedElement:
// The accessible name is not displayed directly inside the node but is
// visible via e.g. a tooltip.
case ax::mojom::NameFrom::kTitle:
return std::string();
case ax::mojom::NameFrom::kContents:
// The placeholder text is initially displayed inside the text field and
// takes the place of its value.
case ax::mojom::NameFrom::kPlaceholder:
// The value attribute takes the place of the node's inner text, e.g. the
// value of a submit button is displayed inside the button itself.
case ax::mojom::NameFrom::kValue:
return data().GetStringAttribute(ax::mojom::StringAttribute::kName);
}
}
std::string inner_text;
for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
inner_text += it->GetInnerText();
}
return inner_text;
}
std::string AXNode::GetLanguage() const {
// Walk up tree considering both detected and author declared languages.
for (const AXNode* cur = this; cur; cur = cur->parent()) {
// If language detection has assigned a language then we prefer that.
const AXLanguageInfo* lang_info = cur->GetLanguageInfo();
if (lang_info && !lang_info->language.empty()) {
return lang_info->language;
}
// If the page author has declared a language attribute we fallback to that.
const AXNodeData& data = cur->data();
if (data.HasStringAttribute(ax::mojom::StringAttribute::kLanguage)) {
return data.GetStringAttribute(ax::mojom::StringAttribute::kLanguage);
}
}
return std::string();
}
std::string AXNode::GetValueForControl() const {
if (data().IsTextField())
return GetValueForTextField();
if (data().IsRangeValueSupported())
return GetTextForRangeValue();
if (data().role == ax::mojom::Role::kColorWell)
return GetValueForColorWell();
if (!IsControl(data().role))
return std::string();
return data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
}
std::ostream& operator<<(std::ostream& stream, const AXNode& node) {
return stream << node.data().ToString();
}
bool AXNode::IsTable() const {
return IsTableLike(data().role);
}
base::Optional<int> AXNode::GetTableColCount() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return int{table_info->col_count};
}
base::Optional<int> AXNode::GetTableRowCount() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return int{table_info->row_count};
}
base::Optional<int> AXNode::GetTableAriaColCount() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return base::make_optional(table_info->aria_col_count);
}
base::Optional<int> AXNode::GetTableAriaRowCount() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return base::make_optional(table_info->aria_row_count);
}
base::Optional<int> AXNode::GetTableCellCount() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return static_cast<int>(table_info->unique_cell_ids.size());
}
base::Optional<bool> AXNode::GetTableHasColumnOrRowHeaderNode() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
return !table_info->all_headers.empty();
}
AXNode* AXNode::GetTableCellFromIndex(int index) const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
// There is a table but there is no cell with the given index.
if (index < 0 || size_t{index} >= table_info->unique_cell_ids.size()) {
return nullptr;
}
return tree_->GetFromId(table_info->unique_cell_ids[size_t{index}]);
}
AXNode* AXNode::GetTableCaption() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
return tree_->GetFromId(table_info->caption_id);
}
AXNode* AXNode::GetTableCellFromCoords(int row_index, int col_index) const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return nullptr;
// There is a table but the given coordinates are outside the table.
if (row_index < 0 || size_t{row_index} >= table_info->row_count ||
col_index < 0 || size_t{col_index} >= table_info->col_count) {
return nullptr;
}
return tree_->GetFromId(
table_info->cell_ids[size_t{row_index}][size_t{col_index}]);
}
std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNode::AXID>();
std::vector<AXNode::AXID> col_header_ids;
// Flatten and add column header ids of each column to |col_header_ids|.
for (std::vector<AXNode::AXID> col_headers_at_index :
table_info->col_headers) {
col_header_ids.insert(col_header_ids.end(), col_headers_at_index.begin(),
col_headers_at_index.end());
}
return col_header_ids;
}
std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds(
int col_index) const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNode::AXID>();
if (col_index < 0 || size_t{col_index} >= table_info->col_count)
return std::vector<AXNode::AXID>();
return std::vector<AXNode::AXID>(table_info->col_headers[size_t{col_index}]);
}
std::vector<AXNode::AXID> AXNode::GetTableRowHeaderNodeIds(
int row_index) const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNode::AXID>();
if (row_index < 0 || size_t{row_index} >= table_info->row_count)
return std::vector<AXNode::AXID>();
return std::vector<AXNode::AXID>(table_info->row_headers[size_t{row_index}]);
}
std::vector<AXNode::AXID> AXNode::GetTableUniqueCellIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return std::vector<AXNode::AXID>();
return std::vector<AXNode::AXID>(table_info->unique_cell_ids);
}
const std::vector<AXNode*>* AXNode::GetExtraMacNodes() const {
// Should only be available on the table node itself, not any of its children.
const AXTableInfo* table_info = tree_->GetTableInfo(this);
if (!table_info)
return nullptr;
return &table_info->extra_mac_nodes;
}
//
// Table row-like nodes.
//
bool AXNode::IsTableRow() const {
return ui::IsTableRow(data().role);
}
base::Optional<int> AXNode::GetTableRowRowIndex() const {
if (!IsTableRow())
return base::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
const auto& iter = table_info->row_id_to_index.find(id());
if (iter == table_info->row_id_to_index.end())
return base::nullopt;
return int{iter->second};
}
std::vector<AXNode::AXID> AXNode::GetTableRowNodeIds() const {
std::vector<AXNode::AXID> row_node_ids;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return row_node_ids;
for (AXNode* node : table_info->row_nodes)
row_node_ids.push_back(node->data().id);
return row_node_ids;
}
#if defined(OS_APPLE)
//
// Table column-like nodes. These nodes are only present on macOS.
//
bool AXNode::IsTableColumn() const {
return ui::IsTableColumn(data().role);
}
base::Optional<int> AXNode::GetTableColColIndex() const {
if (!IsTableColumn())
return base::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
int index = 0;
for (const AXNode* node : table_info->extra_mac_nodes) {
if (node == this)
break;
index++;
}
return index;
}
#endif // defined(OS_APPLE)
//
// Table cell-like nodes.
//
bool AXNode::IsTableCellOrHeader() const {
return IsCellOrTableHeader(data().role);
}
base::Optional<int> AXNode::GetTableCellIndex() const {
if (!IsTableCellOrHeader())
return base::nullopt;
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
const auto& iter = table_info->cell_id_to_index.find(id());
if (iter != table_info->cell_id_to_index.end())
return int{iter->second};
return base::nullopt;
}
base::Optional<int> AXNode::GetTableCellColIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
base::Optional<int> index = GetTableCellIndex();
if (!index)
return base::nullopt;
return int{table_info->cell_data_vector[*index].col_index};
}
base::Optional<int> AXNode::GetTableCellRowIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
base::Optional<int> index = GetTableCellIndex();
if (!index)
return base::nullopt;
return int{table_info->cell_data_vector[*index].row_index};
}
base::Optional<int> AXNode::GetTableCellColSpan() const {
// If it's not a table cell, don't return a col span.
if (!IsTableCellOrHeader())
return base::nullopt;
// Otherwise, try to return a colspan, with 1 as the default if it's not
// specified.
int col_span;
if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, &col_span))
return col_span;
return 1;
}
base::Optional<int> AXNode::GetTableCellRowSpan() const {
// If it's not a table cell, don't return a row span.
if (!IsTableCellOrHeader())
return base::nullopt;
// Otherwise, try to return a row span, with 1 as the default if it's not
// specified.
int row_span;
if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, &row_span))
return row_span;
return 1;
}
base::Optional<int> AXNode::GetTableCellAriaColIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
base::Optional<int> index = GetTableCellIndex();
if (!index)
return base::nullopt;
return int{table_info->cell_data_vector[*index].aria_col_index};
}
base::Optional<int> AXNode::GetTableCellAriaRowIndex() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info)
return base::nullopt;
base::Optional<int> index = GetTableCellIndex();
if (!index)
return base::nullopt;
return int{table_info->cell_data_vector[*index].aria_row_index};
}
std::vector<AXNode::AXID> AXNode::GetTableCellColHeaderNodeIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info || table_info->col_count <= 0)
return std::vector<AXNode::AXID>();
// If this node is not a cell, then return the headers for the first column.
int col_index = GetTableCellColIndex().value_or(0);
return std::vector<AXNode::AXID>(table_info->col_headers[col_index]);
}
void AXNode::GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const {
DCHECK(col_headers);
std::vector<int32_t> col_header_ids = GetTableCellColHeaderNodeIds();
IdVectorToNodeVector(col_header_ids, col_headers);
}
std::vector<AXNode::AXID> AXNode::GetTableCellRowHeaderNodeIds() const {
const AXTableInfo* table_info = GetAncestorTableInfo();
if (!table_info || table_info->row_count <= 0)
return std::vector<AXNode::AXID>();
// If this node is not a cell, then return the headers for the first row.
int row_index = GetTableCellRowIndex().value_or(0);
return std::vector<AXNode::AXID>(table_info->row_headers[row_index]);
}
void AXNode::GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const {
DCHECK(row_headers);
std::vector<int32_t> row_header_ids = GetTableCellRowHeaderNodeIds();
IdVectorToNodeVector(row_header_ids, row_headers);
}
bool AXNode::IsCellOrHeaderOfARIATable() const {
if (!IsTableCellOrHeader())
return false;
const AXNode* node = this;
while (node && !node->IsTable())
node = node->parent();
if (!node)
return false;
return node->data().role == ax::mojom::Role::kTable;
}
bool AXNode::IsCellOrHeaderOfARIAGrid() const {
if (!IsTableCellOrHeader())
return false;
const AXNode* node = this;
while (node && !node->IsTable())
node = node->parent();
if (!node)
return false;
return node->data().role == ax::mojom::Role::kGrid ||
node->data().role == ax::mojom::Role::kTreeGrid;
}
AXTableInfo* AXNode::GetAncestorTableInfo() const {
const AXNode* node = this;
while (node && !node->IsTable())
node = node->parent();
if (node)
return tree_->GetTableInfo(node);
return nullptr;
}
void AXNode::IdVectorToNodeVector(const std::vector<int32_t>& ids,
std::vector<AXNode*>* nodes) const {
for (int32_t id : ids) {
AXNode* node = tree_->GetFromId(id);
if (node)
nodes->push_back(node);
}
}
base::Optional<int> AXNode::GetHierarchicalLevel() const {
int hierarchical_level =
GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
// According to the WAI_ARIA spec, a defined hierarchical level value is
// greater than 0.
// https://www.w3.org/TR/wai-aria-1.1/#aria-level
if (hierarchical_level > 0)
return hierarchical_level;
return base::nullopt;
}
bool AXNode::IsOrderedSetItem() const {
return ui::IsItemLike(data().role);
}
bool AXNode::IsOrderedSet() const {
return ui::IsSetLike(data().role);
}
// Uses AXTree's cache to calculate node's PosInSet.
base::Optional<int> AXNode::GetPosInSet() {
return tree_->GetPosInSet(*this);
}
// Uses AXTree's cache to calculate node's SetSize.
base::Optional<int> AXNode::GetSetSize() {
return tree_->GetSetSize(*this);
}
// Returns true if the role of ordered set matches the role of item.
// Returns false otherwise.
bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const {
ax::mojom::Role item_role = data().role;
// Switch on role of ordered set
switch (ordered_set->data().role) {
case ax::mojom::Role::kFeed:
return item_role == ax::mojom::Role::kArticle;
case ax::mojom::Role::kList:
return item_role == ax::mojom::Role::kListItem;
case ax::mojom::Role::kGroup:
return item_role == ax::mojom::Role::kComment ||
item_role == ax::mojom::Role::kListItem ||
item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kListBoxOption ||
item_role == ax::mojom::Role::kTreeItem;
case ax::mojom::Role::kMenu:
return item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kMenuBar:
return item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kTabList:
return item_role == ax::mojom::Role::kTab;
case ax::mojom::Role::kTree:
return item_role == ax::mojom::Role::kTreeItem;
case ax::mojom::Role::kListBox:
return item_role == ax::mojom::Role::kListBoxOption;
case ax::mojom::Role::kMenuListPopup:
return item_role == ax::mojom::Role::kMenuListOption ||
item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kMenuItemCheckBox;
case ax::mojom::Role::kRadioGroup:
return item_role == ax::mojom::Role::kRadioButton;
case ax::mojom::Role::kDescriptionList:
// Only the term for each description list entry should receive posinset
// and setsize.
return item_role == ax::mojom::Role::kDescriptionListTerm ||
item_role == ax::mojom::Role::kTerm;
case ax::mojom::Role::kPopUpButton:
// kPopUpButtons can wrap a kMenuListPopUp.
return item_role == ax::mojom::Role::kMenuListPopup;
default:
return false;
}
}
bool AXNode::IsIgnoredContainerForOrderedSet() const {
return IsIgnored() || IsEmbeddedGroup() ||
data().role == ax::mojom::Role::kListItem ||
data().role == ax::mojom::Role::kGenericContainer ||
data().role == ax::mojom::Role::kUnknown;
}
int AXNode::UpdateUnignoredCachedValuesRecursive(int startIndex) {
int count = 0;
for (AXNode* child : children_) {
if (child->IsIgnored()) {
child->unignored_index_in_parent_ = 0;
count += child->UpdateUnignoredCachedValuesRecursive(startIndex + count);
} else {
child->unignored_index_in_parent_ = startIndex + count++;
}
}
unignored_child_count_ = count;
return count;
}
// Finds ordered set that contains node.
// Is not required for set's role to match node's role.
AXNode* AXNode::GetOrderedSet() const {
AXNode* result = parent();
// Continue walking up while parent is invalid, ignored, a generic container,
// unknown, or embedded group.
while (result && result->IsIgnoredContainerForOrderedSet()) {
result = result->parent();
}
return result;
}
AXNode* AXNode::ComputeLastUnignoredChildRecursive() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
if (children().empty())
return nullptr;
for (int i = static_cast<int>(children().size()) - 1; i >= 0; --i) {
AXNode* child = children_[i];
if (!child->IsIgnored())
return child;
AXNode* descendant = child->ComputeLastUnignoredChildRecursive();
if (descendant)
return descendant;
}
return nullptr;
}
AXNode* AXNode::ComputeFirstUnignoredChildRecursive() const {
DCHECK(!tree_->GetTreeUpdateInProgressState());
for (size_t i = 0; i < children().size(); i++) {
AXNode* child = children_[i];
if (!child->IsIgnored())
return child;
AXNode* descendant = child->ComputeFirstUnignoredChildRecursive();
if (descendant)
return descendant;
}
return nullptr;
}
std::string AXNode::GetTextForRangeValue() const {
DCHECK(data().IsRangeValueSupported());
std::string range_value =
data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
float numeric_value;
if (range_value.empty() &&
data().GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
&numeric_value)) {
range_value = base::NumberToString(numeric_value);
}
return range_value;
}
std::string AXNode::GetValueForColorWell() const {
DCHECK_EQ(data().role, ax::mojom::Role::kColorWell);
// static cast because SkColor is a 4-byte unsigned int
unsigned int color = static_cast<unsigned int>(
data().GetIntAttribute(ax::mojom::IntAttribute::kColorValue));
unsigned int red = SkColorGetR(color);
unsigned int green = SkColorGetG(color);
unsigned int blue = SkColorGetB(color);
return base::StringPrintf("%d%% red %d%% green %d%% blue", red * 100 / 255,
green * 100 / 255, blue * 100 / 255);
}
std::string AXNode::GetValueForTextField() const {
DCHECK(data().IsTextField());
std::string value =
data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
// Some screen readers like Jaws and VoiceOver require a value to be set in
// text fields with rich content, even though the same information is
// available on the children.
if (value.empty() && data().IsRichTextField())
return GetInnerText();
return value;
}
bool AXNode::IsIgnored() const {
return data().IsIgnored();
}
bool AXNode::IsInvisibleOrIgnored() const {
if (!data().IsInvisibleOrIgnored())
return false;
return !IsFocusedWithinThisTree();
}
bool AXNode::IsFocusedWithinThisTree() const {
return id() == tree_->data().focus_id;
}
bool AXNode::IsChildOfLeaf() const {
const AXNode* ancestor = GetUnignoredParent();
while (ancestor) {
if (ancestor->IsLeaf())
return true;
ancestor = ancestor->GetUnignoredParent();
}
return false;
}
bool AXNode::IsLeaf() const {
// A node is also a leaf if all of it's descendants are ignored.
if (children().empty() || !GetUnignoredChildCount())
return true;
#if defined(OS_WIN)
// On Windows, we want to hide the subtree of a collapsed <select> element.
// Otherwise, ATs are always going to announce its options whether it's
// collapsed or expanded. In the AXTree, this element corresponds to a node
// with role ax::mojom::Role::kPopUpButton that is the parent of a node with
// role ax::mojom::Role::kMenuListPopup.
if (IsCollapsedMenuListPopUpButton())
return true;
#endif // defined(OS_WIN)
// These types of objects may have children that we use as internal
// implementation details, but we want to expose them as leaves to platform
// accessibility APIs because screen readers might be confused if they find
// any children.
if (data().IsPlainTextField() || IsText())
return true;
// Roles whose children are only presentational according to the ARIA and
// HTML5 Specs should be hidden from screen readers.
switch (data().role) {
// 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
// buttons' 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.
case ax::mojom::Role::kButton:
return false;
case ax::mojom::Role::kDocCover:
case ax::mojom::Role::kGraphicsSymbol:
case ax::mojom::Role::kImage:
case ax::mojom::Role::kMeter:
case ax::mojom::Role::kScrollBar:
case ax::mojom::Role::kSlider:
case ax::mojom::Role::kSplitter:
case ax::mojom::Role::kProgressIndicator:
return true;
default:
return false;
}
}
bool AXNode::IsInListMarker() const {
if (data().role == ax::mojom::Role::kListMarker)
return true;
// List marker node's children can only be text elements.
if (!IsText())
return false;
// There is no need to iterate over all the ancestors of the current anchor
// since a list marker node only has children on 2 levels.
// i.e.:
// AXLayoutObject role=kListMarker
// ++StaticText
// ++++InlineTextBox
AXNode* parent_node = GetUnignoredParent();
if (parent_node && parent_node->data().role == ax::mojom::Role::kListMarker)
return true;
AXNode* grandparent_node = parent_node->GetUnignoredParent();
return grandparent_node &&
grandparent_node->data().role == ax::mojom::Role::kListMarker;
}
bool AXNode::IsCollapsedMenuListPopUpButton() const {
if (data().role != ax::mojom::Role::kPopUpButton ||
!data().HasState(ax::mojom::State::kCollapsed)) {
return false;
}
// When a popup button contains a menu list popup, its only child is unignored
// and is a menu list popup.
AXNode* node = GetFirstUnignoredChild();
if (!node)
return false;
return node->data().role == ax::mojom::Role::kMenuListPopup;
}
AXNode* AXNode::GetCollapsedMenuListPopUpButtonAncestor() const {
AXNode* node = GetOrderedSet();
if (!node)
return nullptr;
// The ordered set returned is either the popup element child of the popup
// button (e.g., the AXMenuListPopup) or the popup button itself. We need
// |node| to point to the popup button itself.
if (node->data().role != ax::mojom::Role::kPopUpButton) {
node = node->parent();
if (!node)
return nullptr;
}
return node->IsCollapsedMenuListPopUpButton() ? node : nullptr;
}
bool AXNode::IsEmbeddedGroup() const {
if (data().role != ax::mojom::Role::kGroup || !parent())
return false;
return ui::IsSetLike(parent()->data().role);
}
AXNode* AXNode::GetTextFieldAncestor() const {
// The descendants of a text field usually have State::kEditable, however in
// the case of Role::kSearchBox or Role::kSpinButton being the text field
// ancestor, its immediate descendant can have Role::kGenericContainer without
// State::kEditable. Same with inline text boxes.
// TODO(nektar): Fix all such inconsistencies in Blink.
for (AXNode* ancestor = const_cast<AXNode*>(this);
ancestor &&
(ancestor->data().HasState(ax::mojom::State::kEditable) ||
ancestor->data().role == ax::mojom::Role::kGenericContainer ||
ancestor->data().role == ax::mojom::Role::kInlineTextBox);
ancestor = ancestor->GetUnignoredParent()) {
if (ancestor->data().IsTextField())
return ancestor;
}
return nullptr;
}
bool AXNode::IsDescendantOfPlainTextField() const {
AXNode* text_field_node = GetTextFieldAncestor();
return text_field_node && text_field_node->data().IsPlainTextField();
}
} // namespace ui