1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ui/accessibility/ax_node.h"
6
7 #include <algorithm>
8 #include <utility>
9
10 #include "base/strings/string16.h"
11 #include "base/strings/string_number_conversions.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "build/build_config.h"
16 #include "third_party/skia/include/core/SkColor.h"
17 #include "ui/accessibility/ax_enums.mojom.h"
18 #include "ui/accessibility/ax_language_detection.h"
19 #include "ui/accessibility/ax_role_properties.h"
20 #include "ui/accessibility/ax_table_info.h"
21 #include "ui/accessibility/ax_tree.h"
22 #include "ui/gfx/transform.h"
23
24 namespace ui {
25
26 constexpr AXNode::AXID AXNode::kInvalidAXID;
27
AXNode(AXNode::OwnerTree * tree,AXNode * parent,int32_t id,size_t index_in_parent,size_t unignored_index_in_parent)28 AXNode::AXNode(AXNode::OwnerTree* tree,
29 AXNode* parent,
30 int32_t id,
31 size_t index_in_parent,
32 size_t unignored_index_in_parent)
33 : tree_(tree),
34 index_in_parent_(index_in_parent),
35 unignored_index_in_parent_(unignored_index_in_parent),
36 parent_(parent) {
37 data_.id = id;
38 }
39
40 AXNode::~AXNode() = default;
41
GetUnignoredChildCount() const42 size_t AXNode::GetUnignoredChildCount() const {
43 // TODO(nektar): Should DCHECK if the node is not ignored.
44 DCHECK(!tree_->GetTreeUpdateInProgressState());
45 return unignored_child_count_;
46 }
47
TakeData()48 AXNodeData&& AXNode::TakeData() {
49 return std::move(data_);
50 }
51
GetUnignoredChildAtIndex(size_t index) const52 AXNode* AXNode::GetUnignoredChildAtIndex(size_t index) const {
53 DCHECK(!tree_->GetTreeUpdateInProgressState());
54 size_t count = 0;
55 for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
56 if (count == index)
57 return it.get();
58 ++count;
59 }
60 return nullptr;
61 }
62
GetUnignoredParent() const63 AXNode* AXNode::GetUnignoredParent() const {
64 DCHECK(!tree_->GetTreeUpdateInProgressState());
65 AXNode* result = parent();
66 while (result && result->IsIgnored())
67 result = result->parent();
68 return result;
69 }
70
GetUnignoredIndexInParent() const71 size_t AXNode::GetUnignoredIndexInParent() const {
72 DCHECK(!tree_->GetTreeUpdateInProgressState());
73 return unignored_index_in_parent_;
74 }
75
GetIndexInParent() const76 size_t AXNode::GetIndexInParent() const {
77 DCHECK(!tree_->GetTreeUpdateInProgressState());
78 return index_in_parent_;
79 }
80
GetFirstUnignoredChild() const81 AXNode* AXNode::GetFirstUnignoredChild() const {
82 DCHECK(!tree_->GetTreeUpdateInProgressState());
83 return ComputeFirstUnignoredChildRecursive();
84 }
85
GetLastUnignoredChild() const86 AXNode* AXNode::GetLastUnignoredChild() const {
87 DCHECK(!tree_->GetTreeUpdateInProgressState());
88 return ComputeLastUnignoredChildRecursive();
89 }
90
GetDeepestFirstUnignoredChild() const91 AXNode* AXNode::GetDeepestFirstUnignoredChild() const {
92 if (!GetUnignoredChildCount())
93 return nullptr;
94
95 AXNode* deepest_child = GetFirstUnignoredChild();
96 while (deepest_child->GetUnignoredChildCount()) {
97 deepest_child = deepest_child->GetFirstUnignoredChild();
98 }
99
100 return deepest_child;
101 }
102
GetDeepestLastUnignoredChild() const103 AXNode* AXNode::GetDeepestLastUnignoredChild() const {
104 if (!GetUnignoredChildCount())
105 return nullptr;
106
107 AXNode* deepest_child = GetLastUnignoredChild();
108 while (deepest_child->GetUnignoredChildCount()) {
109 deepest_child = deepest_child->GetLastUnignoredChild();
110 }
111
112 return deepest_child;
113 }
114
115 // Search for the next sibling of this node, skipping over any ignored nodes
116 // encountered.
117 //
118 // In our search:
119 // If we find an ignored sibling, we consider its children as our siblings.
120 // If we run out of siblings, we consider an ignored parent's siblings as our
121 // own siblings.
122 //
123 // Note: this behaviour of 'skipping over' an ignored node makes this subtly
124 // different to finding the next (direct) sibling which is unignored.
125 //
126 // Consider a tree, where (i) marks a node as ignored:
127 //
128 // 1
129 // ├── 2
130 // ├── 3(i)
131 // │ └── 5
132 // └── 4
133 //
134 // The next sibling of node 2 is node 3, which is ignored.
135 // The next unignored sibling of node 2 could be either:
136 // 1) node 4 - next unignored sibling in the literal tree, or
137 // 2) node 5 - next unignored sibling in the logical document.
138 //
139 // There is no next sibling of node 5.
140 // The next unignored sibling of node 5 could be either:
141 // 1) null - no next sibling in the literal tree, or
142 // 2) node 4 - next unignored sibling in the logical document.
143 //
144 // In both cases, this method implements approach (2).
145 //
146 // TODO(chrishall): Can we remove this non-reflexive case by forbidding
147 // GetNextUnignoredSibling calls on an ignored started node?
148 // Note: this means that Next/Previous-UnignoredSibling are not reflexive if
149 // either of the nodes in question are ignored. From above we get an example:
150 // NextUnignoredSibling(3) is 4, but
151 // PreviousUnignoredSibling(4) is 5.
152 //
153 // The view of unignored siblings for node 3 includes both node 2 and node 4:
154 // 2 <-- [3(i)] --> 4
155 //
156 // Whereas nodes 2, 5, and 4 do not consider node 3 to be an unignored sibling:
157 // null <-- [2] --> 5
158 // 2 <-- [5] --> 4
159 // 5 <-- [4] --> null
GetNextUnignoredSibling() const160 AXNode* AXNode::GetNextUnignoredSibling() const {
161 DCHECK(!tree_->GetTreeUpdateInProgressState());
162 const AXNode* current = this;
163
164 // If there are children of the |current| node still to consider.
165 bool considerChildren = false;
166
167 while (current) {
168 // A |candidate| sibling to consider.
169 // If it is unignored then we have found our result.
170 // Otherwise promote it to |current| and consider its children.
171 AXNode* candidate;
172
173 if (considerChildren && (candidate = current->GetFirstChild())) {
174 if (!candidate->IsIgnored())
175 return candidate;
176 current = candidate;
177
178 } else if ((candidate = current->GetNextSibling())) {
179 if (!candidate->IsIgnored())
180 return candidate;
181 current = candidate;
182 // Look through the ignored candidate node to consider their children as
183 // though they were siblings.
184 considerChildren = true;
185
186 } else {
187 // Continue our search through a parent iff they are ignored.
188 //
189 // If |current| has an ignored parent, then we consider the parent's
190 // siblings as though they were siblings of |current|.
191 //
192 // Given a tree:
193 // 1
194 // ├── 2(?)
195 // │ └── [4]
196 // └── 3
197 //
198 // Node 4's view of siblings:
199 // literal tree: null <-- [4] --> null
200 //
201 // If node 2 is not ignored, then node 4's view doesn't change, and we
202 // have no more nodes to consider:
203 // unignored tree: null <-- [4] --> null
204 //
205 // If instead node 2 is ignored, then node 4's view of siblings grows to
206 // include node 3, and we have more nodes to consider:
207 // unignored tree: null <-- [4] --> 3
208 current = current->parent();
209 if (!current || !current->IsIgnored())
210 return nullptr;
211
212 // We have already considered all relevant descendants of |current|.
213 considerChildren = false;
214 }
215 }
216
217 return nullptr;
218 }
219
220 // Search for the previous sibling of this node, skipping over any ignored nodes
221 // encountered.
222 //
223 // In our search for a sibling:
224 // If we find an ignored sibling, we may consider its children as siblings.
225 // If we run out of siblings, we may consider an ignored parent's siblings as
226 // our own.
227 //
228 // See the documentation for |GetNextUnignoredSibling| for more details.
GetPreviousUnignoredSibling() const229 AXNode* AXNode::GetPreviousUnignoredSibling() const {
230 DCHECK(!tree_->GetTreeUpdateInProgressState());
231 const AXNode* current = this;
232
233 // If there are children of the |current| node still to consider.
234 bool considerChildren = false;
235
236 while (current) {
237 // A |candidate| sibling to consider.
238 // If it is unignored then we have found our result.
239 // Otherwise promote it to |current| and consider its children.
240 AXNode* candidate;
241
242 if (considerChildren && (candidate = current->GetLastChild())) {
243 if (!candidate->IsIgnored())
244 return candidate;
245 current = candidate;
246
247 } else if ((candidate = current->GetPreviousSibling())) {
248 if (!candidate->IsIgnored())
249 return candidate;
250 current = candidate;
251 // Look through the ignored candidate node to consider their children as
252 // though they were siblings.
253 considerChildren = true;
254
255 } else {
256 // Continue our search through a parent iff they are ignored.
257 //
258 // If |current| has an ignored parent, then we consider the parent's
259 // siblings as though they were siblings of |current|.
260 //
261 // Given a tree:
262 // 1
263 // ├── 2
264 // └── 3(?)
265 // └── [4]
266 //
267 // Node 4's view of siblings:
268 // literal tree: null <-- [4] --> null
269 //
270 // If node 3 is not ignored, then node 4's view doesn't change, and we
271 // have no more nodes to consider:
272 // unignored tree: null <-- [4] --> null
273 //
274 // If instead node 3 is ignored, then node 4's view of siblings grows to
275 // include node 2, and we have more nodes to consider:
276 // unignored tree: 2 <-- [4] --> null
277 current = current->parent();
278 if (!current || !current->IsIgnored())
279 return nullptr;
280
281 // We have already considered all relevant descendants of |current|.
282 considerChildren = false;
283 }
284 }
285
286 return nullptr;
287 }
288
GetNextUnignoredInTreeOrder() const289 AXNode* AXNode::GetNextUnignoredInTreeOrder() const {
290 if (GetUnignoredChildCount())
291 return GetFirstUnignoredChild();
292
293 const AXNode* node = this;
294 while (node) {
295 AXNode* sibling = node->GetNextUnignoredSibling();
296 if (sibling)
297 return sibling;
298
299 node = node->GetUnignoredParent();
300 }
301
302 return nullptr;
303 }
304
GetPreviousUnignoredInTreeOrder() const305 AXNode* AXNode::GetPreviousUnignoredInTreeOrder() const {
306 AXNode* sibling = GetPreviousUnignoredSibling();
307 if (!sibling)
308 return GetUnignoredParent();
309
310 if (sibling->GetUnignoredChildCount())
311 return sibling->GetDeepestLastUnignoredChild();
312
313 return sibling;
314 }
315
UnignoredChildrenBegin() const316 AXNode::UnignoredChildIterator AXNode::UnignoredChildrenBegin() const {
317 DCHECK(!tree_->GetTreeUpdateInProgressState());
318 return UnignoredChildIterator(this, GetFirstUnignoredChild());
319 }
320
UnignoredChildrenEnd() const321 AXNode::UnignoredChildIterator AXNode::UnignoredChildrenEnd() const {
322 DCHECK(!tree_->GetTreeUpdateInProgressState());
323 return UnignoredChildIterator(this, nullptr);
324 }
325
326 // The first (direct) child, ignored or unignored.
GetFirstChild() const327 AXNode* AXNode::GetFirstChild() const {
328 if (children().empty())
329 return nullptr;
330 return children()[0];
331 }
332
333 // The last (direct) child, ignored or unignored.
GetLastChild() const334 AXNode* AXNode::GetLastChild() const {
335 size_t n = children().size();
336 if (n == 0)
337 return nullptr;
338 return children()[n - 1];
339 }
340
341 // The previous (direct) sibling, ignored or unignored.
GetPreviousSibling() const342 AXNode* AXNode::GetPreviousSibling() const {
343 // Root nodes lack a parent, their index_in_parent should be 0.
344 DCHECK(!parent() ? index_in_parent() == 0 : true);
345 size_t index = index_in_parent();
346 if (index == 0)
347 return nullptr;
348 return parent()->children()[index - 1];
349 }
350
351 // The next (direct) sibling, ignored or unignored.
GetNextSibling() const352 AXNode* AXNode::GetNextSibling() const {
353 if (!parent())
354 return nullptr;
355 size_t nextIndex = index_in_parent() + 1;
356 if (nextIndex >= parent()->children().size())
357 return nullptr;
358 return parent()->children()[nextIndex];
359 }
360
IsText() const361 bool AXNode::IsText() const {
362 // In Legacy Layout, a list marker has no children and is thus represented on
363 // all platforms as a leaf node that exposes the marker itself, i.e., it forms
364 // part of the AX tree's text representation. In contrast, in Layout NG, a
365 // list marker has a static text child.
366 if (data().role == ax::mojom::Role::kListMarker)
367 return !children().size();
368 return ui::IsText(data().role);
369 }
370
IsLineBreak() const371 bool AXNode::IsLineBreak() const {
372 return data().role == ax::mojom::Role::kLineBreak ||
373 (data().role == ax::mojom::Role::kInlineTextBox &&
374 data().GetBoolAttribute(
375 ax::mojom::BoolAttribute::kIsLineBreakingObject));
376 }
377
SetData(const AXNodeData & src)378 void AXNode::SetData(const AXNodeData& src) {
379 data_ = src;
380 }
381
SetLocation(int32_t offset_container_id,const gfx::RectF & location,gfx::Transform * transform)382 void AXNode::SetLocation(int32_t offset_container_id,
383 const gfx::RectF& location,
384 gfx::Transform* transform) {
385 data_.relative_bounds.offset_container_id = offset_container_id;
386 data_.relative_bounds.bounds = location;
387 if (transform) {
388 data_.relative_bounds.transform =
389 std::make_unique<gfx::Transform>(*transform);
390 } else {
391 data_.relative_bounds.transform.reset();
392 }
393 }
394
SetIndexInParent(size_t index_in_parent)395 void AXNode::SetIndexInParent(size_t index_in_parent) {
396 index_in_parent_ = index_in_parent;
397 }
398
UpdateUnignoredCachedValues()399 void AXNode::UpdateUnignoredCachedValues() {
400 if (!IsIgnored())
401 UpdateUnignoredCachedValuesRecursive(0);
402 }
403
SwapChildren(std::vector<AXNode * > * children)404 void AXNode::SwapChildren(std::vector<AXNode*>* children) {
405 children->swap(children_);
406 }
407
Destroy()408 void AXNode::Destroy() {
409 delete this;
410 }
411
IsDescendantOf(const AXNode * ancestor) const412 bool AXNode::IsDescendantOf(const AXNode* ancestor) const {
413 if (this == ancestor)
414 return true;
415 if (parent())
416 return parent()->IsDescendantOf(ancestor);
417
418 return false;
419 }
420
GetOrComputeLineStartOffsets()421 std::vector<int> AXNode::GetOrComputeLineStartOffsets() {
422 std::vector<int> line_offsets;
423 if (data().GetIntListAttribute(ax::mojom::IntListAttribute::kCachedLineStarts,
424 &line_offsets)) {
425 return line_offsets;
426 }
427
428 int start_offset = 0;
429 ComputeLineStartOffsets(&line_offsets, &start_offset);
430 data_.AddIntListAttribute(ax::mojom::IntListAttribute::kCachedLineStarts,
431 line_offsets);
432 return line_offsets;
433 }
434
ComputeLineStartOffsets(std::vector<int> * line_offsets,int * start_offset) const435 void AXNode::ComputeLineStartOffsets(std::vector<int>* line_offsets,
436 int* start_offset) const {
437 DCHECK(line_offsets);
438 DCHECK(start_offset);
439 for (const AXNode* child : children()) {
440 DCHECK(child);
441 if (!child->children().empty()) {
442 child->ComputeLineStartOffsets(line_offsets, start_offset);
443 continue;
444 }
445
446 // Don't report if the first piece of text starts a new line or not.
447 if (*start_offset && !child->data().HasIntAttribute(
448 ax::mojom::IntAttribute::kPreviousOnLineId)) {
449 // If there are multiple objects with an empty accessible label at the
450 // start of a line, only include a single line start offset.
451 if (line_offsets->empty() || line_offsets->back() != *start_offset)
452 line_offsets->push_back(*start_offset);
453 }
454
455 base::string16 text =
456 child->data().GetString16Attribute(ax::mojom::StringAttribute::kName);
457 *start_offset += static_cast<int>(text.length());
458 }
459 }
460
GetInheritedStringAttribute(ax::mojom::StringAttribute attribute) const461 const std::string& AXNode::GetInheritedStringAttribute(
462 ax::mojom::StringAttribute attribute) const {
463 const AXNode* current_node = this;
464 do {
465 if (current_node->data().HasStringAttribute(attribute))
466 return current_node->data().GetStringAttribute(attribute);
467 current_node = current_node->parent();
468 } while (current_node);
469 return base::EmptyString();
470 }
471
GetInheritedString16Attribute(ax::mojom::StringAttribute attribute) const472 base::string16 AXNode::GetInheritedString16Attribute(
473 ax::mojom::StringAttribute attribute) const {
474 return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute));
475 }
476
GetLanguageInfo() const477 AXLanguageInfo* AXNode::GetLanguageInfo() const {
478 return language_info_.get();
479 }
480
SetLanguageInfo(std::unique_ptr<AXLanguageInfo> lang_info)481 void AXNode::SetLanguageInfo(std::unique_ptr<AXLanguageInfo> lang_info) {
482 language_info_ = std::move(lang_info);
483 }
484
ClearLanguageInfo()485 void AXNode::ClearLanguageInfo() {
486 language_info_.reset();
487 }
488
GetHypertext() const489 std::string AXNode::GetHypertext() const {
490 if (IsLeaf())
491 return GetInnerText();
492
493 // Construct the hypertext for this node, which contains the concatenation of
494 // the inner text of this node's textual children, and an embedded object
495 // character for all the other children.
496 const std::string embedded_character_str("\xEF\xBF\xBC");
497 std::string hypertext;
498 for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
499 // Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK
500 // hypertext with the embedded object character. We copy all of their text
501 // instead.
502 if (it->IsText()) {
503 hypertext += it->GetInnerText();
504 } else {
505 hypertext += embedded_character_str;
506 }
507 }
508 return hypertext;
509 }
510
GetInnerText() const511 std::string AXNode::GetInnerText() const {
512 // If a text field has no descendants, then we compute its inner text from its
513 // value or its placeholder. Otherwise we prefer to look at its descendant
514 // text nodes because Blink doesn't always add all trailing white space to the
515 // value attribute.
516 const bool is_plain_text_field_without_descendants =
517 (data().IsTextField() && !GetUnignoredChildCount());
518 if (is_plain_text_field_without_descendants) {
519 std::string value =
520 data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
521 // If the value is empty, then there might be some placeholder text in the
522 // text field, or any other name that is derived from visible contents, even
523 // if the text field has no children.
524 if (!value.empty())
525 return value;
526 }
527
528 // Ordinarily, plain text fields are leaves. We need to exclude them from the
529 // set of leaf nodes when they expose any descendants. This is because we want
530 // to compute their inner text from their descendant text nodes as we don't
531 // always trust the "value" attribute provided by Blink.
532 const bool is_plain_text_field_with_descendants =
533 (data().IsTextField() && GetUnignoredChildCount());
534 if (IsLeaf() && !is_plain_text_field_with_descendants) {
535 switch (data().GetNameFrom()) {
536 case ax::mojom::NameFrom::kNone:
537 case ax::mojom::NameFrom::kUninitialized:
538 // The accessible name is not displayed on screen, e.g. aria-label, or is
539 // not displayed directly inside the node, e.g. an associated label
540 // element.
541 case ax::mojom::NameFrom::kAttribute:
542 // The node's accessible name is explicitly empty.
543 case ax::mojom::NameFrom::kAttributeExplicitlyEmpty:
544 // The accessible name does not represent the entirety of the node's inner
545 // text, e.g. a table's caption or a figure's figcaption.
546 case ax::mojom::NameFrom::kCaption:
547 case ax::mojom::NameFrom::kRelatedElement:
548 // The accessible name is not displayed directly inside the node but is
549 // visible via e.g. a tooltip.
550 case ax::mojom::NameFrom::kTitle:
551 return std::string();
552
553 case ax::mojom::NameFrom::kContents:
554 // The placeholder text is initially displayed inside the text field and
555 // takes the place of its value.
556 case ax::mojom::NameFrom::kPlaceholder:
557 // The value attribute takes the place of the node's inner text, e.g. the
558 // value of a submit button is displayed inside the button itself.
559 case ax::mojom::NameFrom::kValue:
560 return data().GetStringAttribute(ax::mojom::StringAttribute::kName);
561 }
562 }
563
564 std::string inner_text;
565 for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
566 inner_text += it->GetInnerText();
567 }
568 return inner_text;
569 }
570
GetLanguage() const571 std::string AXNode::GetLanguage() const {
572 // Walk up tree considering both detected and author declared languages.
573 for (const AXNode* cur = this; cur; cur = cur->parent()) {
574 // If language detection has assigned a language then we prefer that.
575 const AXLanguageInfo* lang_info = cur->GetLanguageInfo();
576 if (lang_info && !lang_info->language.empty()) {
577 return lang_info->language;
578 }
579
580 // If the page author has declared a language attribute we fallback to that.
581 const AXNodeData& data = cur->data();
582 if (data.HasStringAttribute(ax::mojom::StringAttribute::kLanguage)) {
583 return data.GetStringAttribute(ax::mojom::StringAttribute::kLanguage);
584 }
585 }
586
587 return std::string();
588 }
589
GetValueForControl() const590 std::string AXNode::GetValueForControl() const {
591 if (data().IsTextField())
592 return GetValueForTextField();
593 if (data().IsRangeValueSupported())
594 return GetTextForRangeValue();
595 if (data().role == ax::mojom::Role::kColorWell)
596 return GetValueForColorWell();
597 if (!IsControl(data().role))
598 return std::string();
599 return data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
600 }
601
operator <<(std::ostream & stream,const AXNode & node)602 std::ostream& operator<<(std::ostream& stream, const AXNode& node) {
603 return stream << node.data().ToString();
604 }
605
IsTable() const606 bool AXNode::IsTable() const {
607 return IsTableLike(data().role);
608 }
609
GetTableColCount() const610 base::Optional<int> AXNode::GetTableColCount() const {
611 const AXTableInfo* table_info = GetAncestorTableInfo();
612 if (!table_info)
613 return base::nullopt;
614 return int{table_info->col_count};
615 }
616
GetTableRowCount() const617 base::Optional<int> AXNode::GetTableRowCount() const {
618 const AXTableInfo* table_info = GetAncestorTableInfo();
619 if (!table_info)
620 return base::nullopt;
621 return int{table_info->row_count};
622 }
623
GetTableAriaColCount() const624 base::Optional<int> AXNode::GetTableAriaColCount() const {
625 const AXTableInfo* table_info = GetAncestorTableInfo();
626 if (!table_info)
627 return base::nullopt;
628 return base::make_optional(table_info->aria_col_count);
629 }
630
GetTableAriaRowCount() const631 base::Optional<int> AXNode::GetTableAriaRowCount() const {
632 const AXTableInfo* table_info = GetAncestorTableInfo();
633 if (!table_info)
634 return base::nullopt;
635 return base::make_optional(table_info->aria_row_count);
636 }
637
GetTableCellCount() const638 base::Optional<int> AXNode::GetTableCellCount() const {
639 const AXTableInfo* table_info = GetAncestorTableInfo();
640 if (!table_info)
641 return base::nullopt;
642
643 return static_cast<int>(table_info->unique_cell_ids.size());
644 }
645
GetTableHasColumnOrRowHeaderNode() const646 base::Optional<bool> AXNode::GetTableHasColumnOrRowHeaderNode() const {
647 const AXTableInfo* table_info = GetAncestorTableInfo();
648 if (!table_info)
649 return base::nullopt;
650
651 return !table_info->all_headers.empty();
652 }
653
GetTableCellFromIndex(int index) const654 AXNode* AXNode::GetTableCellFromIndex(int index) const {
655 const AXTableInfo* table_info = GetAncestorTableInfo();
656 if (!table_info)
657 return nullptr;
658
659 // There is a table but there is no cell with the given index.
660 if (index < 0 || size_t{index} >= table_info->unique_cell_ids.size()) {
661 return nullptr;
662 }
663
664 return tree_->GetFromId(table_info->unique_cell_ids[size_t{index}]);
665 }
666
GetTableCaption() const667 AXNode* AXNode::GetTableCaption() const {
668 const AXTableInfo* table_info = GetAncestorTableInfo();
669 if (!table_info)
670 return nullptr;
671
672 return tree_->GetFromId(table_info->caption_id);
673 }
674
GetTableCellFromCoords(int row_index,int col_index) const675 AXNode* AXNode::GetTableCellFromCoords(int row_index, int col_index) const {
676 const AXTableInfo* table_info = GetAncestorTableInfo();
677 if (!table_info)
678 return nullptr;
679
680 // There is a table but the given coordinates are outside the table.
681 if (row_index < 0 || size_t{row_index} >= table_info->row_count ||
682 col_index < 0 || size_t{col_index} >= table_info->col_count) {
683 return nullptr;
684 }
685
686 return tree_->GetFromId(
687 table_info->cell_ids[size_t{row_index}][size_t{col_index}]);
688 }
689
GetTableColHeaderNodeIds() const690 std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds() const {
691 const AXTableInfo* table_info = GetAncestorTableInfo();
692 if (!table_info)
693 return std::vector<AXNode::AXID>();
694
695 std::vector<AXNode::AXID> col_header_ids;
696 // Flatten and add column header ids of each column to |col_header_ids|.
697 for (std::vector<AXNode::AXID> col_headers_at_index :
698 table_info->col_headers) {
699 col_header_ids.insert(col_header_ids.end(), col_headers_at_index.begin(),
700 col_headers_at_index.end());
701 }
702
703 return col_header_ids;
704 }
705
GetTableColHeaderNodeIds(int col_index) const706 std::vector<AXNode::AXID> AXNode::GetTableColHeaderNodeIds(
707 int col_index) const {
708 const AXTableInfo* table_info = GetAncestorTableInfo();
709 if (!table_info)
710 return std::vector<AXNode::AXID>();
711
712 if (col_index < 0 || size_t{col_index} >= table_info->col_count)
713 return std::vector<AXNode::AXID>();
714
715 return std::vector<AXNode::AXID>(table_info->col_headers[size_t{col_index}]);
716 }
717
GetTableRowHeaderNodeIds(int row_index) const718 std::vector<AXNode::AXID> AXNode::GetTableRowHeaderNodeIds(
719 int row_index) const {
720 const AXTableInfo* table_info = GetAncestorTableInfo();
721 if (!table_info)
722 return std::vector<AXNode::AXID>();
723
724 if (row_index < 0 || size_t{row_index} >= table_info->row_count)
725 return std::vector<AXNode::AXID>();
726
727 return std::vector<AXNode::AXID>(table_info->row_headers[size_t{row_index}]);
728 }
729
GetTableUniqueCellIds() const730 std::vector<AXNode::AXID> AXNode::GetTableUniqueCellIds() const {
731 const AXTableInfo* table_info = GetAncestorTableInfo();
732 if (!table_info)
733 return std::vector<AXNode::AXID>();
734
735 return std::vector<AXNode::AXID>(table_info->unique_cell_ids);
736 }
737
GetExtraMacNodes() const738 const std::vector<AXNode*>* AXNode::GetExtraMacNodes() const {
739 // Should only be available on the table node itself, not any of its children.
740 const AXTableInfo* table_info = tree_->GetTableInfo(this);
741 if (!table_info)
742 return nullptr;
743
744 return &table_info->extra_mac_nodes;
745 }
746
747 //
748 // Table row-like nodes.
749 //
750
IsTableRow() const751 bool AXNode::IsTableRow() const {
752 return ui::IsTableRow(data().role);
753 }
754
GetTableRowRowIndex() const755 base::Optional<int> AXNode::GetTableRowRowIndex() const {
756 if (!IsTableRow())
757 return base::nullopt;
758
759 const AXTableInfo* table_info = GetAncestorTableInfo();
760 if (!table_info)
761 return base::nullopt;
762
763 const auto& iter = table_info->row_id_to_index.find(id());
764 if (iter == table_info->row_id_to_index.end())
765 return base::nullopt;
766 return int{iter->second};
767 }
768
GetTableRowNodeIds() const769 std::vector<AXNode::AXID> AXNode::GetTableRowNodeIds() const {
770 std::vector<AXNode::AXID> row_node_ids;
771 const AXTableInfo* table_info = GetAncestorTableInfo();
772 if (!table_info)
773 return row_node_ids;
774
775 for (AXNode* node : table_info->row_nodes)
776 row_node_ids.push_back(node->data().id);
777
778 return row_node_ids;
779 }
780
781 #if defined(OS_APPLE)
782
783 //
784 // Table column-like nodes. These nodes are only present on macOS.
785 //
786
IsTableColumn() const787 bool AXNode::IsTableColumn() const {
788 return ui::IsTableColumn(data().role);
789 }
790
GetTableColColIndex() const791 base::Optional<int> AXNode::GetTableColColIndex() const {
792 if (!IsTableColumn())
793 return base::nullopt;
794
795 const AXTableInfo* table_info = GetAncestorTableInfo();
796 if (!table_info)
797 return base::nullopt;
798
799 int index = 0;
800 for (const AXNode* node : table_info->extra_mac_nodes) {
801 if (node == this)
802 break;
803 index++;
804 }
805 return index;
806 }
807
808 #endif // defined(OS_APPLE)
809
810 //
811 // Table cell-like nodes.
812 //
813
IsTableCellOrHeader() const814 bool AXNode::IsTableCellOrHeader() const {
815 return IsCellOrTableHeader(data().role);
816 }
817
GetTableCellIndex() const818 base::Optional<int> AXNode::GetTableCellIndex() const {
819 if (!IsTableCellOrHeader())
820 return base::nullopt;
821
822 const AXTableInfo* table_info = GetAncestorTableInfo();
823 if (!table_info)
824 return base::nullopt;
825
826 const auto& iter = table_info->cell_id_to_index.find(id());
827 if (iter != table_info->cell_id_to_index.end())
828 return int{iter->second};
829 return base::nullopt;
830 }
831
GetTableCellColIndex() const832 base::Optional<int> AXNode::GetTableCellColIndex() const {
833 const AXTableInfo* table_info = GetAncestorTableInfo();
834 if (!table_info)
835 return base::nullopt;
836
837 base::Optional<int> index = GetTableCellIndex();
838 if (!index)
839 return base::nullopt;
840
841 return int{table_info->cell_data_vector[*index].col_index};
842 }
843
GetTableCellRowIndex() const844 base::Optional<int> AXNode::GetTableCellRowIndex() const {
845 const AXTableInfo* table_info = GetAncestorTableInfo();
846 if (!table_info)
847 return base::nullopt;
848
849 base::Optional<int> index = GetTableCellIndex();
850 if (!index)
851 return base::nullopt;
852
853 return int{table_info->cell_data_vector[*index].row_index};
854 }
855
GetTableCellColSpan() const856 base::Optional<int> AXNode::GetTableCellColSpan() const {
857 // If it's not a table cell, don't return a col span.
858 if (!IsTableCellOrHeader())
859 return base::nullopt;
860
861 // Otherwise, try to return a colspan, with 1 as the default if it's not
862 // specified.
863 int col_span;
864 if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellColumnSpan, &col_span))
865 return col_span;
866 return 1;
867 }
868
GetTableCellRowSpan() const869 base::Optional<int> AXNode::GetTableCellRowSpan() const {
870 // If it's not a table cell, don't return a row span.
871 if (!IsTableCellOrHeader())
872 return base::nullopt;
873
874 // Otherwise, try to return a row span, with 1 as the default if it's not
875 // specified.
876 int row_span;
877 if (GetIntAttribute(ax::mojom::IntAttribute::kTableCellRowSpan, &row_span))
878 return row_span;
879 return 1;
880 }
881
GetTableCellAriaColIndex() const882 base::Optional<int> AXNode::GetTableCellAriaColIndex() const {
883 const AXTableInfo* table_info = GetAncestorTableInfo();
884 if (!table_info)
885 return base::nullopt;
886
887 base::Optional<int> index = GetTableCellIndex();
888 if (!index)
889 return base::nullopt;
890
891 return int{table_info->cell_data_vector[*index].aria_col_index};
892 }
893
GetTableCellAriaRowIndex() const894 base::Optional<int> AXNode::GetTableCellAriaRowIndex() const {
895 const AXTableInfo* table_info = GetAncestorTableInfo();
896 if (!table_info)
897 return base::nullopt;
898
899 base::Optional<int> index = GetTableCellIndex();
900 if (!index)
901 return base::nullopt;
902
903 return int{table_info->cell_data_vector[*index].aria_row_index};
904 }
905
GetTableCellColHeaderNodeIds() const906 std::vector<AXNode::AXID> AXNode::GetTableCellColHeaderNodeIds() const {
907 const AXTableInfo* table_info = GetAncestorTableInfo();
908 if (!table_info || table_info->col_count <= 0)
909 return std::vector<AXNode::AXID>();
910
911 // If this node is not a cell, then return the headers for the first column.
912 int col_index = GetTableCellColIndex().value_or(0);
913
914 return std::vector<AXNode::AXID>(table_info->col_headers[col_index]);
915 }
916
GetTableCellColHeaders(std::vector<AXNode * > * col_headers) const917 void AXNode::GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const {
918 DCHECK(col_headers);
919
920 std::vector<int32_t> col_header_ids = GetTableCellColHeaderNodeIds();
921 IdVectorToNodeVector(col_header_ids, col_headers);
922 }
923
GetTableCellRowHeaderNodeIds() const924 std::vector<AXNode::AXID> AXNode::GetTableCellRowHeaderNodeIds() const {
925 const AXTableInfo* table_info = GetAncestorTableInfo();
926 if (!table_info || table_info->row_count <= 0)
927 return std::vector<AXNode::AXID>();
928
929 // If this node is not a cell, then return the headers for the first row.
930 int row_index = GetTableCellRowIndex().value_or(0);
931
932 return std::vector<AXNode::AXID>(table_info->row_headers[row_index]);
933 }
934
GetTableCellRowHeaders(std::vector<AXNode * > * row_headers) const935 void AXNode::GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const {
936 DCHECK(row_headers);
937
938 std::vector<int32_t> row_header_ids = GetTableCellRowHeaderNodeIds();
939 IdVectorToNodeVector(row_header_ids, row_headers);
940 }
941
IsCellOrHeaderOfARIATable() const942 bool AXNode::IsCellOrHeaderOfARIATable() const {
943 if (!IsTableCellOrHeader())
944 return false;
945
946 const AXNode* node = this;
947 while (node && !node->IsTable())
948 node = node->parent();
949 if (!node)
950 return false;
951
952 return node->data().role == ax::mojom::Role::kTable;
953 }
954
IsCellOrHeaderOfARIAGrid() const955 bool AXNode::IsCellOrHeaderOfARIAGrid() const {
956 if (!IsTableCellOrHeader())
957 return false;
958
959 const AXNode* node = this;
960 while (node && !node->IsTable())
961 node = node->parent();
962 if (!node)
963 return false;
964
965 return node->data().role == ax::mojom::Role::kGrid ||
966 node->data().role == ax::mojom::Role::kTreeGrid;
967 }
968
GetAncestorTableInfo() const969 AXTableInfo* AXNode::GetAncestorTableInfo() const {
970 const AXNode* node = this;
971 while (node && !node->IsTable())
972 node = node->parent();
973 if (node)
974 return tree_->GetTableInfo(node);
975 return nullptr;
976 }
977
IdVectorToNodeVector(const std::vector<int32_t> & ids,std::vector<AXNode * > * nodes) const978 void AXNode::IdVectorToNodeVector(const std::vector<int32_t>& ids,
979 std::vector<AXNode*>* nodes) const {
980 for (int32_t id : ids) {
981 AXNode* node = tree_->GetFromId(id);
982 if (node)
983 nodes->push_back(node);
984 }
985 }
986
GetHierarchicalLevel() const987 base::Optional<int> AXNode::GetHierarchicalLevel() const {
988 int hierarchical_level =
989 GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
990
991 // According to the WAI_ARIA spec, a defined hierarchical level value is
992 // greater than 0.
993 // https://www.w3.org/TR/wai-aria-1.1/#aria-level
994 if (hierarchical_level > 0)
995 return hierarchical_level;
996
997 return base::nullopt;
998 }
999
IsOrderedSetItem() const1000 bool AXNode::IsOrderedSetItem() const {
1001 return ui::IsItemLike(data().role);
1002 }
1003
IsOrderedSet() const1004 bool AXNode::IsOrderedSet() const {
1005 return ui::IsSetLike(data().role);
1006 }
1007
1008 // Uses AXTree's cache to calculate node's PosInSet.
GetPosInSet()1009 base::Optional<int> AXNode::GetPosInSet() {
1010 return tree_->GetPosInSet(*this);
1011 }
1012
1013 // Uses AXTree's cache to calculate node's SetSize.
GetSetSize()1014 base::Optional<int> AXNode::GetSetSize() {
1015 return tree_->GetSetSize(*this);
1016 }
1017
1018 // Returns true if the role of ordered set matches the role of item.
1019 // Returns false otherwise.
SetRoleMatchesItemRole(const AXNode * ordered_set) const1020 bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const {
1021 ax::mojom::Role item_role = data().role;
1022 // Switch on role of ordered set
1023 switch (ordered_set->data().role) {
1024 case ax::mojom::Role::kFeed:
1025 return item_role == ax::mojom::Role::kArticle;
1026 case ax::mojom::Role::kList:
1027 return item_role == ax::mojom::Role::kListItem;
1028 case ax::mojom::Role::kGroup:
1029 return item_role == ax::mojom::Role::kComment ||
1030 item_role == ax::mojom::Role::kListItem ||
1031 item_role == ax::mojom::Role::kMenuItem ||
1032 item_role == ax::mojom::Role::kMenuItemRadio ||
1033 item_role == ax::mojom::Role::kListBoxOption ||
1034 item_role == ax::mojom::Role::kTreeItem;
1035 case ax::mojom::Role::kMenu:
1036 return item_role == ax::mojom::Role::kMenuItem ||
1037 item_role == ax::mojom::Role::kMenuItemRadio ||
1038 item_role == ax::mojom::Role::kMenuItemCheckBox;
1039 case ax::mojom::Role::kMenuBar:
1040 return item_role == ax::mojom::Role::kMenuItem ||
1041 item_role == ax::mojom::Role::kMenuItemRadio ||
1042 item_role == ax::mojom::Role::kMenuItemCheckBox;
1043 case ax::mojom::Role::kTabList:
1044 return item_role == ax::mojom::Role::kTab;
1045 case ax::mojom::Role::kTree:
1046 return item_role == ax::mojom::Role::kTreeItem;
1047 case ax::mojom::Role::kListBox:
1048 return item_role == ax::mojom::Role::kListBoxOption;
1049 case ax::mojom::Role::kMenuListPopup:
1050 return item_role == ax::mojom::Role::kMenuListOption ||
1051 item_role == ax::mojom::Role::kMenuItem ||
1052 item_role == ax::mojom::Role::kMenuItemRadio ||
1053 item_role == ax::mojom::Role::kMenuItemCheckBox;
1054 case ax::mojom::Role::kRadioGroup:
1055 return item_role == ax::mojom::Role::kRadioButton;
1056 case ax::mojom::Role::kDescriptionList:
1057 // Only the term for each description list entry should receive posinset
1058 // and setsize.
1059 return item_role == ax::mojom::Role::kDescriptionListTerm ||
1060 item_role == ax::mojom::Role::kTerm;
1061 case ax::mojom::Role::kPopUpButton:
1062 // kPopUpButtons can wrap a kMenuListPopUp.
1063 return item_role == ax::mojom::Role::kMenuListPopup;
1064 default:
1065 return false;
1066 }
1067 }
1068
IsIgnoredContainerForOrderedSet() const1069 bool AXNode::IsIgnoredContainerForOrderedSet() const {
1070 return IsIgnored() || IsEmbeddedGroup() ||
1071 data().role == ax::mojom::Role::kListItem ||
1072 data().role == ax::mojom::Role::kGenericContainer ||
1073 data().role == ax::mojom::Role::kUnknown;
1074 }
1075
UpdateUnignoredCachedValuesRecursive(int startIndex)1076 int AXNode::UpdateUnignoredCachedValuesRecursive(int startIndex) {
1077 int count = 0;
1078 for (AXNode* child : children_) {
1079 if (child->IsIgnored()) {
1080 child->unignored_index_in_parent_ = 0;
1081 count += child->UpdateUnignoredCachedValuesRecursive(startIndex + count);
1082 } else {
1083 child->unignored_index_in_parent_ = startIndex + count++;
1084 }
1085 }
1086 unignored_child_count_ = count;
1087 return count;
1088 }
1089
1090 // Finds ordered set that contains node.
1091 // Is not required for set's role to match node's role.
GetOrderedSet() const1092 AXNode* AXNode::GetOrderedSet() const {
1093 AXNode* result = parent();
1094 // Continue walking up while parent is invalid, ignored, a generic container,
1095 // unknown, or embedded group.
1096 while (result && result->IsIgnoredContainerForOrderedSet()) {
1097 result = result->parent();
1098 }
1099
1100 return result;
1101 }
1102
ComputeLastUnignoredChildRecursive() const1103 AXNode* AXNode::ComputeLastUnignoredChildRecursive() const {
1104 DCHECK(!tree_->GetTreeUpdateInProgressState());
1105 if (children().empty())
1106 return nullptr;
1107
1108 for (int i = static_cast<int>(children().size()) - 1; i >= 0; --i) {
1109 AXNode* child = children_[i];
1110 if (!child->IsIgnored())
1111 return child;
1112
1113 AXNode* descendant = child->ComputeLastUnignoredChildRecursive();
1114 if (descendant)
1115 return descendant;
1116 }
1117 return nullptr;
1118 }
1119
ComputeFirstUnignoredChildRecursive() const1120 AXNode* AXNode::ComputeFirstUnignoredChildRecursive() const {
1121 DCHECK(!tree_->GetTreeUpdateInProgressState());
1122 for (size_t i = 0; i < children().size(); i++) {
1123 AXNode* child = children_[i];
1124 if (!child->IsIgnored())
1125 return child;
1126
1127 AXNode* descendant = child->ComputeFirstUnignoredChildRecursive();
1128 if (descendant)
1129 return descendant;
1130 }
1131 return nullptr;
1132 }
1133
GetTextForRangeValue() const1134 std::string AXNode::GetTextForRangeValue() const {
1135 DCHECK(data().IsRangeValueSupported());
1136 std::string range_value =
1137 data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
1138 float numeric_value;
1139 if (range_value.empty() &&
1140 data().GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
1141 &numeric_value)) {
1142 range_value = base::NumberToString(numeric_value);
1143 }
1144 return range_value;
1145 }
1146
GetValueForColorWell() const1147 std::string AXNode::GetValueForColorWell() const {
1148 DCHECK_EQ(data().role, ax::mojom::Role::kColorWell);
1149 // static cast because SkColor is a 4-byte unsigned int
1150 unsigned int color = static_cast<unsigned int>(
1151 data().GetIntAttribute(ax::mojom::IntAttribute::kColorValue));
1152
1153 unsigned int red = SkColorGetR(color);
1154 unsigned int green = SkColorGetG(color);
1155 unsigned int blue = SkColorGetB(color);
1156 return base::StringPrintf("%d%% red %d%% green %d%% blue", red * 100 / 255,
1157 green * 100 / 255, blue * 100 / 255);
1158 }
1159
GetValueForTextField() const1160 std::string AXNode::GetValueForTextField() const {
1161 DCHECK(data().IsTextField());
1162 std::string value =
1163 data().GetStringAttribute(ax::mojom::StringAttribute::kValue);
1164 // Some screen readers like Jaws and VoiceOver require a value to be set in
1165 // text fields with rich content, even though the same information is
1166 // available on the children.
1167 if (value.empty() && data().IsRichTextField())
1168 return GetInnerText();
1169 return value;
1170 }
1171
IsIgnored() const1172 bool AXNode::IsIgnored() const {
1173 return data().IsIgnored();
1174 }
1175
IsChildOfLeaf() const1176 bool AXNode::IsChildOfLeaf() const {
1177 const AXNode* ancestor = GetUnignoredParent();
1178 while (ancestor) {
1179 if (ancestor->IsLeaf())
1180 return true;
1181 ancestor = ancestor->GetUnignoredParent();
1182 }
1183 return false;
1184 }
1185
IsLeaf() const1186 bool AXNode::IsLeaf() const {
1187 // A node is also a leaf if all of it's descendants are ignored.
1188 if (children().empty() || !GetUnignoredChildCount())
1189 return true;
1190
1191 #if defined(OS_WIN)
1192 // On Windows, we want to hide the subtree of a collapsed <select> element.
1193 // Otherwise, ATs are always going to announce its options whether it's
1194 // collapsed or expanded. In the AXTree, this element corresponds to a node
1195 // with role ax::mojom::Role::kPopUpButton that is the parent of a node with
1196 // role ax::mojom::Role::kMenuListPopup.
1197 if (IsCollapsedMenuListPopUpButton())
1198 return true;
1199 #endif // defined(OS_WIN)
1200
1201 // These types of objects may have children that we use as internal
1202 // implementation details, but we want to expose them as leaves to platform
1203 // accessibility APIs because screen readers might be confused if they find
1204 // any children.
1205 if (data().IsPlainTextField() || IsText())
1206 return true;
1207
1208 // Roles whose children are only presentational according to the ARIA and
1209 // HTML5 Specs should be hidden from screen readers.
1210 switch (data().role) {
1211 // According to the ARIA and Core-AAM specs:
1212 // https://w3c.github.io/aria/#button,
1213 // https://www.w3.org/TR/core-aam-1.1/#exclude_elements
1214 // buttons' children are presentational only and should be hidden from
1215 // screen readers. However, we cannot enforce the leafiness of buttons
1216 // because they may contain many rich, interactive descendants such as a day
1217 // in a calendar, and screen readers will need to interact with these
1218 // contents. See https://crbug.com/689204.
1219 // So we decided to not enforce the leafiness of buttons and expose all
1220 // children.
1221 case ax::mojom::Role::kButton:
1222 return false;
1223 case ax::mojom::Role::kDocCover:
1224 case ax::mojom::Role::kGraphicsSymbol:
1225 case ax::mojom::Role::kImage:
1226 case ax::mojom::Role::kMeter:
1227 case ax::mojom::Role::kScrollBar:
1228 case ax::mojom::Role::kSlider:
1229 case ax::mojom::Role::kSplitter:
1230 case ax::mojom::Role::kProgressIndicator:
1231 return true;
1232 default:
1233 return false;
1234 }
1235 }
1236
IsInListMarker() const1237 bool AXNode::IsInListMarker() const {
1238 if (data().role == ax::mojom::Role::kListMarker)
1239 return true;
1240
1241 // List marker node's children can only be text elements.
1242 if (!IsText())
1243 return false;
1244
1245 // There is no need to iterate over all the ancestors of the current anchor
1246 // since a list marker node only has children on 2 levels.
1247 // i.e.:
1248 // AXLayoutObject role=kListMarker
1249 // ++StaticText
1250 // ++++InlineTextBox
1251 AXNode* parent_node = GetUnignoredParent();
1252 if (parent_node && parent_node->data().role == ax::mojom::Role::kListMarker)
1253 return true;
1254
1255 AXNode* grandparent_node = parent_node->GetUnignoredParent();
1256 return grandparent_node &&
1257 grandparent_node->data().role == ax::mojom::Role::kListMarker;
1258 }
1259
IsCollapsedMenuListPopUpButton() const1260 bool AXNode::IsCollapsedMenuListPopUpButton() const {
1261 if (data().role != ax::mojom::Role::kPopUpButton ||
1262 !data().HasState(ax::mojom::State::kCollapsed)) {
1263 return false;
1264 }
1265
1266 // When a popup button contains a menu list popup, its only child is unignored
1267 // and is a menu list popup.
1268 AXNode* node = GetFirstUnignoredChild();
1269 if (!node)
1270 return false;
1271
1272 return node->data().role == ax::mojom::Role::kMenuListPopup;
1273 }
1274
GetCollapsedMenuListPopUpButtonAncestor() const1275 AXNode* AXNode::GetCollapsedMenuListPopUpButtonAncestor() const {
1276 AXNode* node = GetOrderedSet();
1277
1278 if (!node)
1279 return nullptr;
1280
1281 // The ordered set returned is either the popup element child of the popup
1282 // button (e.g., the AXMenuListPopup) or the popup button itself. We need
1283 // |node| to point to the popup button itself.
1284 if (node->data().role != ax::mojom::Role::kPopUpButton) {
1285 node = node->parent();
1286 if (!node)
1287 return nullptr;
1288 }
1289
1290 return node->IsCollapsedMenuListPopUpButton() ? node : nullptr;
1291 }
1292
IsEmbeddedGroup() const1293 bool AXNode::IsEmbeddedGroup() const {
1294 if (data().role != ax::mojom::Role::kGroup || !parent())
1295 return false;
1296
1297 return ui::IsSetLike(parent()->data().role);
1298 }
1299
GetTextFieldAncestor() const1300 AXNode* AXNode::GetTextFieldAncestor() const {
1301 // The descendants of a text field usually have State::kEditable, however in
1302 // the case of Role::kSearchBox or Role::kSpinButton being the text field
1303 // ancestor, its immediate descendant can have Role::kGenericContainer without
1304 // State::kEditable.
1305 for (AXNode* ancestor = const_cast<AXNode*>(this);
1306 ancestor &&
1307 (ancestor->data().HasState(ax::mojom::State::kEditable) ||
1308 ancestor->data().role == ax::mojom::Role::kGenericContainer);
1309 ancestor = ancestor->GetUnignoredParent()) {
1310 if (ancestor->data().IsTextField())
1311 return ancestor;
1312 }
1313 return nullptr;
1314 }
1315
IsDescendantOfPlainTextField() const1316 bool AXNode::IsDescendantOfPlainTextField() const {
1317 AXNode* textfield_node = GetTextFieldAncestor();
1318 return textfield_node && textfield_node->data().IsPlainTextField();
1319 }
1320
1321 } // namespace ui
1322