123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211 |
- // 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 "base/no_destructor.h"
- #include "base/numerics/safe_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 "ui/accessibility/ax_computed_node_data.h"
- #include "ui/accessibility/ax_enums.mojom.h"
- #include "ui/accessibility/ax_hypertext.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/accessibility/ax_tree_manager.h"
- #include "ui/gfx/color_utils.h"
- #include "ui/gfx/geometry/transform.h"
- namespace ui {
- // Definition of static class members.
- constexpr char AXNode::kEmbeddedObjectCharacterUTF8[];
- constexpr char16_t AXNode::kEmbeddedObjectCharacterUTF16[];
- constexpr int AXNode::kEmbeddedObjectCharacterLengthUTF8;
- constexpr int AXNode::kEmbeddedObjectCharacterLengthUTF16;
- AXNode::AXNode(AXNode::OwnerTree* tree,
- AXNode* parent,
- AXNodeID 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;
- AXNodeData&& AXNode::TakeData() {
- return std::move(data_);
- }
- const std::vector<AXNode*>& AXNode::GetAllChildren() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return children_;
- }
- size_t AXNode::GetChildCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return children_.size();
- }
- size_t AXNode::GetChildCountCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager)
- return 1u;
- return GetChildCount();
- }
- size_t AXNode::GetUnignoredChildCount() const {
- // TODO(nektar): Should DCHECK that this node is not ignored.
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return unignored_child_count_;
- }
- size_t AXNode::GetUnignoredChildCountCrossingTreeBoundary() const {
- // TODO(nektar): Should DCHECK that this node is not ignored.
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager) {
- DCHECK_EQ(unignored_child_count_, 0u)
- << "A node cannot be hosting both a child tree and other nodes as "
- "children.";
- return 1u; // A child tree is never ignored.
- }
- return unignored_child_count_;
- }
- AXNode* AXNode::GetChildAtIndex(size_t index) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (index >= GetChildCount())
- return nullptr;
- return children_[index];
- }
- AXNode* AXNode::GetChildAtIndexCrossingTreeBoundary(size_t index) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager) {
- DCHECK_EQ(index, 0u)
- << "A node cannot be hosting both a child tree and other nodes as "
- "children.";
- return child_tree_manager->GetRootAsAXNode();
- }
- return GetChildAtIndex(index);
- }
- AXNode* AXNode::GetUnignoredChildAtIndex(size_t index) const {
- // TODO(nektar): Should DCHECK that this node is not ignored.
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
- if (index == 0)
- return it.get();
- --index;
- }
- return nullptr;
- }
- AXNode* AXNode::GetUnignoredChildAtIndexCrossingTreeBoundary(
- size_t index) const {
- // TODO(nektar): Should DCHECK that this node is not ignored.
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager) {
- DCHECK_EQ(index, 0u)
- << "A node cannot be hosting both a child tree and other nodes as "
- "children.";
- // A child tree is never ignored.
- return child_tree_manager->GetRootAsAXNode();
- }
- return GetUnignoredChildAtIndex(index);
- }
- AXNode* AXNode::GetParent() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return parent_;
- }
- AXNode* AXNode::GetParentCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (parent_)
- return parent_;
- const AXTreeManager* manager = GetManager();
- if (manager)
- return manager->GetParentNodeFromParentTreeAsAXNode();
- return nullptr;
- }
- AXNode* AXNode::GetUnignoredParent() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- AXNode* unignored_parent = GetParent();
- while (unignored_parent && unignored_parent->IsIgnored())
- unignored_parent = unignored_parent->GetParent();
- return unignored_parent;
- }
- AXNode* AXNode::GetUnignoredParentCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- AXNode* unignored_parent = GetUnignoredParent();
- if (!unignored_parent) {
- const AXTreeManager* manager = GetManager();
- if (manager)
- unignored_parent = manager->GetParentNodeFromParentTreeAsAXNode();
- }
- return unignored_parent;
- }
- base::stack<AXNode*> AXNode::GetAncestorsCrossingTreeBoundary() const {
- base::stack<AXNode*> ancestors;
- AXNode* ancestor = const_cast<AXNode*>(this);
- while (ancestor) {
- ancestors.push(ancestor);
- ancestor = ancestor->GetParentCrossingTreeBoundary();
- }
- return ancestors;
- }
- size_t AXNode::GetIndexInParent() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return index_in_parent_;
- }
- size_t AXNode::GetUnignoredIndexInParent() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return unignored_index_in_parent_;
- }
- AXNode* AXNode::GetFirstChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetChildAtIndex(0);
- }
- AXNode* AXNode::GetFirstChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetChildAtIndexCrossingTreeBoundary(0);
- }
- AXNode* AXNode::GetFirstUnignoredChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return ComputeFirstUnignoredChildRecursive();
- }
- AXNode* AXNode::GetFirstUnignoredChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager)
- return child_tree_manager->GetRootAsAXNode();
- return ComputeFirstUnignoredChildRecursive();
- }
- AXNode* AXNode::GetLastChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- size_t n = GetChildCount();
- if (n == 0)
- return nullptr;
- return GetChildAtIndex(n - 1);
- }
- AXNode* AXNode::GetLastChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- size_t n = GetChildCountCrossingTreeBoundary();
- if (n == 0)
- return nullptr;
- return GetChildAtIndexCrossingTreeBoundary(n - 1);
- }
- AXNode* AXNode::GetLastUnignoredChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return ComputeLastUnignoredChildRecursive();
- }
- AXNode* AXNode::GetLastUnignoredChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTreeManager* child_tree_manager = AXTreeManager::ForChildTree(*this);
- if (child_tree_manager)
- return child_tree_manager->GetRootAsAXNode();
- return ComputeLastUnignoredChildRecursive();
- }
- AXNode* AXNode::GetDeepestFirstChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetChildCount())
- return nullptr;
- AXNode* deepest_child = GetFirstChild();
- DCHECK(deepest_child);
- while (deepest_child->GetChildCount())
- deepest_child = deepest_child->GetFirstChild();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestFirstChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetChildCountCrossingTreeBoundary())
- return nullptr;
- AXNode* deepest_child = GetFirstChildCrossingTreeBoundary();
- DCHECK(deepest_child);
- while (deepest_child->GetChildCountCrossingTreeBoundary())
- deepest_child = deepest_child->GetFirstChildCrossingTreeBoundary();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestFirstUnignoredChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetUnignoredChildCount())
- return nullptr;
- AXNode* deepest_child = GetFirstUnignoredChild();
- DCHECK(deepest_child);
- while (deepest_child->GetUnignoredChildCount())
- deepest_child = deepest_child->GetFirstUnignoredChild();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestFirstUnignoredChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetUnignoredChildCountCrossingTreeBoundary())
- return nullptr;
- AXNode* deepest_child = GetFirstUnignoredChildCrossingTreeBoundary();
- DCHECK(deepest_child);
- while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary())
- deepest_child = deepest_child->GetFirstUnignoredChildCrossingTreeBoundary();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestLastChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetChildCount())
- return nullptr;
- AXNode* deepest_child = GetLastChild();
- DCHECK(deepest_child);
- while (deepest_child->GetChildCount())
- deepest_child = deepest_child->GetLastChild();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestLastChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetChildCountCrossingTreeBoundary())
- return nullptr;
- AXNode* deepest_child = GetLastChildCrossingTreeBoundary();
- DCHECK(deepest_child);
- while (deepest_child->GetChildCountCrossingTreeBoundary())
- deepest_child = deepest_child->GetLastChildCrossingTreeBoundary();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestLastUnignoredChild() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetUnignoredChildCount())
- return nullptr;
- AXNode* deepest_child = GetLastUnignoredChild();
- DCHECK(deepest_child);
- while (deepest_child->GetUnignoredChildCount())
- deepest_child = deepest_child->GetLastUnignoredChild();
- return deepest_child;
- }
- AXNode* AXNode::GetDeepestLastUnignoredChildCrossingTreeBoundary() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (!GetUnignoredChildCountCrossingTreeBoundary())
- return nullptr;
- AXNode* deepest_child = GetLastUnignoredChildCrossingTreeBoundary();
- DCHECK(deepest_child);
- while (deepest_child->GetUnignoredChildCountCrossingTreeBoundary())
- deepest_child = deepest_child->GetLastUnignoredChildCrossingTreeBoundary();
- return deepest_child;
- }
- AXNode* AXNode::GetNextSibling() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- AXNode* parent = GetParent();
- if (!parent)
- return nullptr;
- DCHECK(parent || !GetIndexInParent())
- << "Root nodes lack a parent. Their index_in_parent should be 0.";
- size_t nextIndex = GetIndexInParent() + 1;
- if (nextIndex >= parent->GetChildCount())
- return nullptr;
- return parent->GetChildAtIndex(nextIndex);
- }
- // 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->GetParent();
- if (!current || !current->IsIgnored())
- return nullptr;
- // We have already considered all relevant descendants of |current|.
- considerChildren = false;
- }
- }
- return nullptr;
- }
- AXNode* AXNode::GetPreviousSibling() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- DCHECK(GetParent() || !GetIndexInParent())
- << "Root nodes lack a parent. Their index_in_parent should be 0.";
- size_t index = GetIndexInParent();
- if (index == 0)
- return nullptr;
- return GetParent()->GetChildAtIndex(index - 1);
- }
- // 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->GetParent();
- if (!current || !current->IsIgnored())
- return nullptr;
- // We have already considered all relevant descendants of |current|.
- considerChildren = false;
- }
- }
- return nullptr;
- }
- AXNode* AXNode::GetNextUnignoredInTreeOrder() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- 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 {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- AXNode* sibling = GetPreviousUnignoredSibling();
- if (!sibling)
- return GetUnignoredParent();
- if (sibling->GetUnignoredChildCount())
- return sibling->GetDeepestLastUnignoredChild();
- return sibling;
- }
- AXNode::AllChildIterator AXNode::AllChildrenBegin() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return AllChildIterator(this, GetFirstChild());
- }
- AXNode::AllChildIterator AXNode::AllChildrenEnd() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return AllChildIterator(this, nullptr);
- }
- AXNode::AllChildCrossingTreeBoundaryIterator
- AXNode::AllChildrenCrossingTreeBoundaryBegin() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return AllChildCrossingTreeBoundaryIterator(
- this, GetFirstChildCrossingTreeBoundary());
- }
- AXNode::AllChildCrossingTreeBoundaryIterator
- AXNode::AllChildrenCrossingTreeBoundaryEnd() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return AllChildCrossingTreeBoundaryIterator(this, nullptr);
- }
- AXNode::UnignoredChildIterator AXNode::UnignoredChildrenBegin() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return UnignoredChildIterator(this, GetFirstUnignoredChild());
- }
- AXNode::UnignoredChildIterator AXNode::UnignoredChildrenEnd() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return UnignoredChildIterator(this, nullptr);
- }
- AXNode::UnignoredChildCrossingTreeBoundaryIterator
- AXNode::UnignoredChildrenCrossingTreeBoundaryBegin() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return UnignoredChildCrossingTreeBoundaryIterator(
- this, GetFirstUnignoredChildCrossingTreeBoundary());
- }
- AXNode::UnignoredChildCrossingTreeBoundaryIterator
- AXNode::UnignoredChildrenCrossingTreeBoundaryEnd() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return UnignoredChildCrossingTreeBoundaryIterator(this, nullptr);
- }
- bool AXNode::CanFireEvents() const {
- // TODO(nektar): Cache the `IsChildOfLeaf` state in `AXComputedNodeData`.
- return !IsChildOfLeaf();
- }
- absl::optional<int> AXNode::CompareTo(const AXNode& other) const {
- if (this == &other)
- return 0;
- AXNode* common_ancestor = nullptr;
- base::stack<AXNode*> our_ancestors = GetAncestorsCrossingTreeBoundary();
- base::stack<AXNode*> other_ancestors =
- other.GetAncestorsCrossingTreeBoundary();
- while (!our_ancestors.empty() && !other_ancestors.empty() &&
- our_ancestors.top() == other_ancestors.top()) {
- common_ancestor = our_ancestors.top();
- our_ancestors.pop();
- other_ancestors.pop();
- }
- if (!common_ancestor)
- return absl::nullopt;
- if (common_ancestor == this)
- return -1;
- if (common_ancestor == &other)
- return 1;
- if (our_ancestors.empty() || other_ancestors.empty()) {
- NOTREACHED() << "The common ancestor should be followed by two uncommon "
- "children in the two corresponding lists of ancestors.";
- return absl::nullopt;
- }
- size_t this_uncommon_ancestor_index = our_ancestors.top()->GetIndexInParent();
- size_t other_uncommon_ancestor_index =
- other_ancestors.top()->GetIndexInParent();
- DCHECK_NE(this_uncommon_ancestor_index, other_uncommon_ancestor_index)
- << "Deepest uncommon ancestors should truly be uncommon, i.e. not be the "
- "same node.";
- return this_uncommon_ancestor_index - other_uncommon_ancestor_index;
- }
- bool AXNode::IsText() const {
- // Regular list markers only expose their alternative text, but do not expose
- // their descendants; and the descendants should be ignored. This is because
- // the alternative text depends on the counter style and can be different from
- // the actual (visual) marker text, and hence, inconsistent with the
- // descendants. We treat a list marker as non-text only if it still has
- // non-ignored descendants, which happens only when:
- // - The list marker itself is ignored but the descendants are not
- // - Or the list marker contains images
- if (GetRole() == ax::mojom::Role::kListMarker)
- return !GetUnignoredChildCount();
- return ui::IsText(GetRole());
- }
- bool AXNode::IsLineBreak() const {
- // The last condition captures inline text nodes whose only content is an '\n'
- // character.
- return GetRole() == ax::mojom::Role::kLineBreak ||
- (GetRole() == ax::mojom::Role::kInlineTextBox &&
- GetBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject));
- }
- void AXNode::SetData(const AXNodeData& src) {
- data_ = src;
- }
- void AXNode::SetLocation(AXNodeID 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() {
- computed_node_data_.reset();
- if (!IsIgnored())
- UpdateUnignoredCachedValuesRecursive(0);
- }
- void AXNode::SwapChildren(std::vector<AXNode*>* children) {
- children->swap(children_);
- }
- bool AXNode::IsDescendantOf(const AXNode* ancestor) const {
- if (!ancestor)
- return false;
- if (this == ancestor)
- return true;
- if (const AXNode* parent = GetParent())
- return parent->IsDescendantOf(ancestor);
- return false;
- }
- bool AXNode::IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const {
- if (!ancestor)
- return false;
- if (this == ancestor)
- return true;
- if (const AXNode* parent = GetParentCrossingTreeBoundary())
- return parent->IsDescendantOfCrossingTreeBoundary(ancestor);
- return false;
- }
- SkColor AXNode::ComputeColor() const {
- return ComputeColorAttribute(ax::mojom::IntAttribute::kColor);
- }
- SkColor AXNode::ComputeBackgroundColor() const {
- return ComputeColorAttribute(ax::mojom::IntAttribute::kBackgroundColor);
- }
- SkColor AXNode::ComputeColorAttribute(ax::mojom::IntAttribute attr) const {
- SkColor color = GetIntAttribute(attr);
- AXNode* ancestor = GetParent();
- // If the color has some transparency, keep blending with background
- // colors until we get an opaque color or reach the root.
- while (ancestor && SkColorGetA(color) != SK_AlphaOPAQUE) {
- SkColor background_color = ancestor->GetIntAttribute(attr);
- color = color_utils::GetResultingPaintColor(color, background_color);
- ancestor = ancestor->GetParent();
- }
- return color;
- }
- AXTreeManager* AXNode::GetManager() const {
- return AXTreeManager::FromID(tree_->GetAXTreeID());
- }
- bool AXNode::HasVisibleCaretOrSelection() const {
- const OwnerTree::Selection selection = GetSelection();
- const AXNode* focus = tree()->GetFromId(selection.focus_object_id);
- if (!focus || !focus->IsDescendantOf(this))
- return false;
- // A selection or the caret will be visible in a focused text field (including
- // a content editable).
- const AXNode* text_field = GetTextFieldAncestor();
- if (text_field)
- return true;
- // The selection will be visible in non-editable content only if it is not
- // collapsed.
- return !selection.IsCollapsed();
- }
- AXNode::OwnerTree::Selection AXNode::GetSelection() const {
- DCHECK(tree()) << "Cannot retrieve the current selection if the node is not "
- "attached to an accessibility tree.\n"
- << *this;
- return tree()->GetSelection();
- }
- AXNode::OwnerTree::Selection AXNode::GetUnignoredSelection() const {
- DCHECK(tree()) << "Cannot retrieve the current selection if the node is not "
- "attached to an accessibility tree.\n"
- << *this;
- OwnerTree::Selection selection = tree()->GetUnignoredSelection();
- // "selection.anchor_offset" and "selection.focus_ofset" might need to be
- // adjusted if the anchor or the focus nodes include ignored children.
- //
- // TODO(nektar): Move this logic into its own "AXSelection" class and cache
- // the result for faster reuse.
- const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id);
- if (anchor && !anchor->IsLeaf()) {
- DCHECK_GE(selection.anchor_offset, 0);
- if (static_cast<size_t>(selection.anchor_offset) <
- anchor->GetChildCount()) {
- const AXNode* anchor_child =
- anchor->GetChildAtIndex(selection.anchor_offset);
- DCHECK(anchor_child);
- selection.anchor_offset =
- static_cast<int>(anchor_child->GetUnignoredIndexInParent());
- } else {
- selection.anchor_offset =
- static_cast<int>(anchor->GetUnignoredChildCount());
- }
- }
- const AXNode* focus = tree()->GetFromId(selection.focus_object_id);
- if (focus && !focus->IsLeaf()) {
- DCHECK_GE(selection.focus_offset, 0);
- if (static_cast<size_t>(selection.focus_offset) < focus->GetChildCount()) {
- const AXNode* focus_child =
- focus->GetChildAtIndex(selection.focus_offset);
- DCHECK(focus_child);
- selection.focus_offset =
- static_cast<int>(focus_child->GetUnignoredIndexInParent());
- } else {
- selection.focus_offset =
- static_cast<int>(focus->GetUnignoredChildCount());
- }
- }
- return selection;
- }
- bool AXNode::HasStringAttribute(ax::mojom::StringAttribute attribute) const {
- return GetComputedNodeData().HasOrCanComputeAttribute(attribute);
- }
- const std::string& AXNode::GetStringAttribute(
- ax::mojom::StringAttribute attribute) const {
- return GetComputedNodeData().GetOrComputeAttributeUTF8(attribute);
- }
- bool AXNode::GetStringAttribute(ax::mojom::StringAttribute attribute,
- std::string* value) const {
- if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) {
- *value = GetComputedNodeData().GetOrComputeAttributeUTF8(attribute);
- return true;
- }
- return false;
- }
- std::u16string AXNode::GetString16Attribute(
- ax::mojom::StringAttribute attribute) const {
- return GetComputedNodeData().GetOrComputeAttributeUTF16(attribute);
- }
- bool AXNode::GetString16Attribute(ax::mojom::StringAttribute attribute,
- std::u16string* value) const {
- if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) {
- *value = GetComputedNodeData().GetOrComputeAttributeUTF16(attribute);
- return true;
- }
- return false;
- }
- bool AXNode::HasInheritedStringAttribute(
- ax::mojom::StringAttribute attribute) const {
- for (const AXNode* current_node = this; current_node;
- current_node = current_node->GetParent()) {
- if (current_node->HasStringAttribute(attribute))
- return true;
- }
- return false;
- }
- const std::string& AXNode::GetInheritedStringAttribute(
- ax::mojom::StringAttribute attribute) const {
- for (const AXNode* current_node = this; current_node;
- current_node = current_node->GetParent()) {
- if (current_node->HasStringAttribute(attribute))
- return current_node->GetStringAttribute(attribute);
- }
- return base::EmptyString();
- }
- std::u16string AXNode::GetInheritedString16Attribute(
- ax::mojom::StringAttribute attribute) const {
- return base::UTF8ToUTF16(GetInheritedStringAttribute(attribute));
- }
- bool AXNode::HasIntListAttribute(ax::mojom::IntListAttribute attribute) const {
- return GetComputedNodeData().HasOrCanComputeAttribute(attribute);
- }
- const std::vector<int32_t>& AXNode::GetIntListAttribute(
- ax::mojom::IntListAttribute attribute) const {
- return GetComputedNodeData().GetOrComputeAttribute(attribute);
- }
- bool AXNode::GetIntListAttribute(ax::mojom::IntListAttribute attribute,
- std::vector<int32_t>* value) const {
- if (GetComputedNodeData().HasOrCanComputeAttribute(attribute)) {
- *value = GetComputedNodeData().GetOrComputeAttribute(attribute);
- return true;
- }
- return false;
- }
- 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();
- }
- const AXComputedNodeData& AXNode::GetComputedNodeData() const {
- if (!computed_node_data_)
- computed_node_data_ = std::make_unique<AXComputedNodeData>(*this);
- return *computed_node_data_;
- }
- void AXNode::ClearComputedNodeData() {
- computed_node_data_.reset();
- }
- const std::string& AXNode::GetNameUTF8() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXNode* node = this;
- if (GetRole() == ax::mojom::Role::kPortal &&
- GetNameFrom() == ax::mojom::NameFrom::kNone) {
- const AXTreeManager* child_tree_manager =
- AXTreeManager::ForChildTree(*this);
- if (child_tree_manager)
- node = child_tree_manager->GetRootAsAXNode();
- }
- return node->GetStringAttribute(ax::mojom::StringAttribute::kName);
- }
- std::u16string AXNode::GetNameUTF16() const {
- // Storing a copy of the name in UTF16 would probably not be helpful because
- // it could potentially double the memory usage of AXTree.
- return base::UTF8ToUTF16(GetNameUTF8());
- }
- const std::u16string& AXNode::GetHypertext() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- // TODO(nektar): Introduce proper caching of hypertext via
- // `AXHypertext::needs_update`.
- hypertext_ = AXHypertext();
- // Hypertext is not exposed for descendants of leaf nodes. For such nodes,
- // their text content is equivalent to their hypertext. Otherwise, we would
- // never be able to compute equivalent ancestor positions in atomic text
- // fields given an AXPosition on an inline text box descendant, because there
- // is often an ignored generic container between the text descendants and the
- // text field node.
- //
- // For example, look at the following accessibility tree and the text
- // positions indicated using "<>" symbols in the text content of every node,
- // and then imagine what would happen if the generic container was represented
- // by an "embedded object replacement character" in the text of its text field
- // parent.
- // ++kTextField "Hell<o>" IsLeaf=true
- // ++++kGenericContainer "Hell<o>" ignored IsChildOfLeaf=true
- // ++++++kStaticText "Hell<o>" IsChildOfLeaf=true
- // ++++++++kInlineTextBox "Hell<o>" IsChildOfLeaf=true
- if (IsLeaf() || IsChildOfLeaf()) {
- hypertext_.hypertext = GetTextContentUTF16();
- } else {
- // Construct the hypertext for this node, which contains the concatenation
- // of the text content of this node's textual children, and an "object
- // replacement character" for all the other children.
- //
- // Note that the word "hypertext" comes from the IAccessible2 Standard and
- // has nothing to do with HTML.
- static const base::NoDestructor<std::u16string> embedded_character_str(
- AXNode::kEmbeddedObjectCharacterUTF16);
- auto first = UnignoredChildrenCrossingTreeBoundaryBegin();
- for (auto iter = first; iter != UnignoredChildrenCrossingTreeBoundaryEnd();
- ++iter) {
- // 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 (iter->IsText()) {
- hypertext_.hypertext += iter->GetTextContentUTF16();
- } else {
- int character_offset = static_cast<int>(hypertext_.hypertext.size());
- auto inserted =
- hypertext_.hypertext_offset_to_hyperlink_child_index.emplace(
- character_offset, static_cast<int>(std::distance(first, iter)));
- DCHECK(inserted.second) << "An embedded object at " << character_offset
- << " has already been encountered.";
- hypertext_.hypertext += *embedded_character_str;
- }
- }
- }
- hypertext_.needs_update = false;
- return hypertext_.hypertext;
- }
- void AXNode::SetNeedsToUpdateHypertext() {
- old_hypertext_ = hypertext_;
- hypertext_.needs_update = true;
- // TODO(nektar): Introduce proper caching of hypertext via
- // `AXHypertext::needs_update`.
- GetHypertext(); // Forces `hypertext_` to immediately update.
- }
- const std::map<int, int>& AXNode::GetHypertextOffsetToHyperlinkChildIndex()
- const {
- // TODO(nektar): Introduce proper caching of hypertext via
- // `AXHypertext::needs_update`.
- GetHypertext(); // Update `hypertext_` if not up-to-date.
- return hypertext_.hypertext_offset_to_hyperlink_child_index;
- }
- const AXHypertext& AXNode::GetOldHypertext() const {
- // TODO(nektar): Introduce proper caching of hypertext via
- // `AXHypertext::needs_update`.
- GetHypertext(); // Update `hypertext_` if not up-to-date.
- return old_hypertext_;
- }
- const std::string& AXNode::GetTextContentUTF8() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetComputedNodeData().GetOrComputeTextContentUTF8();
- }
- const std::u16string& AXNode::GetTextContentUTF16() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetComputedNodeData().GetOrComputeTextContentUTF16();
- }
- int AXNode::GetTextContentLengthUTF8() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetComputedNodeData().GetOrComputeTextContentLengthUTF8();
- }
- int AXNode::GetTextContentLengthUTF16() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- return GetComputedNodeData().GetOrComputeTextContentLengthUTF16();
- }
- gfx::RectF AXNode::GetTextContentRangeBoundsUTF8(int start_offset,
- int end_offset) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- DCHECK_LE(start_offset, end_offset)
- << "Invalid `start_offset` and `end_offset`.\n"
- << start_offset << ' ' << end_offset << "\nin\n"
- << *this;
- // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to
- // check whether `start_offset` is also in range.
- if (end_offset > GetTextContentLengthUTF8())
- return gfx::RectF();
- // TODO(nektar): Update this to use
- // "base/strings/utf_offset_string_conversions.h" which provides caching of
- // offsets.
- std::u16string out_trancated_string_utf16;
- if (!base::UTF8ToUTF16(GetTextContentUTF8().data(),
- base::checked_cast<size_t>(start_offset),
- &out_trancated_string_utf16)) {
- return gfx::RectF();
- }
- start_offset = base::checked_cast<int>(out_trancated_string_utf16.length());
- if (!base::UTF8ToUTF16(GetTextContentUTF8().data(),
- base::checked_cast<size_t>(end_offset),
- &out_trancated_string_utf16)) {
- return gfx::RectF();
- }
- end_offset = base::checked_cast<int>(out_trancated_string_utf16.length());
- return GetTextContentRangeBoundsUTF16(start_offset, end_offset);
- }
- gfx::RectF AXNode::GetTextContentRangeBoundsUTF16(int start_offset,
- int end_offset) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- DCHECK_LE(start_offset, end_offset)
- << "Invalid `start_offset` and `end_offset`.\n"
- << start_offset << ' ' << end_offset << "\nin\n"
- << *this;
- // Since we DCHECK that `start_offset` <= `end_offset`, there is no need to
- // check whether `start_offset` is also in range.
- if (end_offset > GetTextContentLengthUTF16())
- return gfx::RectF();
- const std::vector<int32_t>& character_offsets =
- GetIntListAttribute(ax::mojom::IntListAttribute::kCharacterOffsets);
- int character_offsets_length =
- base::checked_cast<int>(character_offsets.size());
- // Charactger offsets are always based on the UTF-16 representation of the
- // text.
- if (character_offsets_length < GetTextContentLengthUTF16()) {
- // Blink might not return pixel offsets for all characters. Clamp the
- // character range to be within the number of provided pixels. Note that the
- // first character always starts at pixel 0, so an offset for that character
- // is not provided.
- //
- // TODO(accessibility): We need to fix this bug in Blink.
- start_offset = std::min(start_offset, character_offsets_length);
- end_offset = std::min(end_offset, character_offsets_length);
- }
- // TODO(nektar): Remove all this code and fix up the character offsets vector
- // itself.
- int start_pixel_offset =
- start_offset > 0
- ? character_offsets[base::checked_cast<size_t>(start_offset - 1)]
- : 0;
- int end_pixel_offset =
- end_offset > 0
- ? character_offsets[base::checked_cast<size_t>(end_offset - 1)]
- : 0;
- int max_pixel_offset = character_offsets_length > 0
- ? character_offsets[character_offsets_length - 1]
- : 0;
- const gfx::RectF& node_bounds = data().relative_bounds.bounds;
- gfx::RectF out_bounds;
- switch (static_cast<ax::mojom::WritingDirection>(
- GetIntAttribute(ax::mojom::IntAttribute::kTextDirection))) {
- case ax::mojom::WritingDirection::kNone:
- case ax::mojom::WritingDirection::kLtr:
- out_bounds = gfx::RectF(start_pixel_offset, 0,
- end_pixel_offset - start_pixel_offset,
- node_bounds.height());
- break;
- case ax::mojom::WritingDirection::kRtl: {
- int left = max_pixel_offset - end_pixel_offset;
- int right = max_pixel_offset - start_pixel_offset;
- out_bounds = gfx::RectF(left, 0, right - left, node_bounds.height());
- break;
- }
- case ax::mojom::WritingDirection::kTtb:
- out_bounds = gfx::RectF(0, start_pixel_offset, node_bounds.width(),
- end_pixel_offset - start_pixel_offset);
- break;
- case ax::mojom::WritingDirection::kBtt: {
- int top = max_pixel_offset - end_pixel_offset;
- int bottom = max_pixel_offset - start_pixel_offset;
- out_bounds = gfx::RectF(0, top, node_bounds.width(), bottom - top);
- break;
- }
- }
- return out_bounds;
- }
- std::string AXNode::GetLanguage() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- // Walk up tree considering both detected and author declared languages.
- for (const AXNode* cur = this; cur; cur = cur->GetParent()) {
- // 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.
- if (cur->HasStringAttribute(ax::mojom::StringAttribute::kLanguage))
- return cur->GetStringAttribute(ax::mojom::StringAttribute::kLanguage);
- }
- return std::string();
- }
- std::string AXNode::GetValueForControl() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- if (data().IsTextField()) {
- // Returns the value of a text field. If necessary, computes the value from
- // the field's internal representation in the accessibility tree, in order
- // to minimize cross-process communication between the renderer and the
- // browser processes.
- return GetStringAttribute(ax::mojom::StringAttribute::kValue);
- }
- if (data().IsRangeValueSupported())
- return GetTextForRangeValue();
- if (GetRole() == ax::mojom::Role::kColorWell)
- return GetValueForColorWell();
- if (!IsControl(GetRole()))
- return std::string();
- return 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(GetRole());
- }
- absl::optional<int> AXNode::GetTableColCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return static_cast<int>(table_info->col_count);
- }
- absl::optional<int> AXNode::GetTableRowCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return static_cast<int>(table_info->row_count);
- }
- absl::optional<int> AXNode::GetTableAriaColCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return absl::make_optional(table_info->aria_col_count);
- }
- absl::optional<int> AXNode::GetTableAriaRowCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return absl::make_optional(table_info->aria_row_count);
- }
- absl::optional<int> AXNode::GetTableCellCount() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return static_cast<int>(table_info->unique_cell_ids.size());
- }
- absl::optional<bool> AXNode::GetTableHasColumnOrRowHeaderNode() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- return !table_info->all_headers.empty();
- }
- AXNode* AXNode::GetTableCellFromIndex(int index) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- 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 ||
- static_cast<size_t>(index) >= table_info->unique_cell_ids.size()) {
- return nullptr;
- }
- return tree_->GetFromId(
- table_info->unique_cell_ids[static_cast<size_t>(index)]);
- }
- AXNode* AXNode::GetTableCaption() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- 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 {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- 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 ||
- static_cast<size_t>(row_index) >= table_info->row_count ||
- col_index < 0 ||
- static_cast<size_t>(col_index) >= table_info->col_count) {
- return nullptr;
- }
- return tree_->GetFromId(table_info->cell_ids[static_cast<size_t>(row_index)]
- [static_cast<size_t>(col_index)]);
- }
- std::vector<AXNodeID> AXNode::GetTableColHeaderNodeIds() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return std::vector<AXNodeID>();
- std::vector<AXNodeID> col_header_ids;
- // Flatten and add column header ids of each column to |col_header_ids|.
- for (std::vector<AXNodeID> 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<AXNodeID> AXNode::GetTableColHeaderNodeIds(int col_index) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return std::vector<AXNodeID>();
- if (col_index < 0 || static_cast<size_t>(col_index) >= table_info->col_count)
- return std::vector<AXNodeID>();
- return std::vector<AXNodeID>(
- table_info->col_headers[static_cast<size_t>(col_index)]);
- }
- std::vector<AXNodeID> AXNode::GetTableRowHeaderNodeIds(int row_index) const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return std::vector<AXNodeID>();
- if (row_index < 0 || static_cast<size_t>(row_index) >= table_info->row_count)
- return std::vector<AXNodeID>();
- return std::vector<AXNodeID>(
- table_info->row_headers[static_cast<size_t>(row_index)]);
- }
- std::vector<AXNodeID> AXNode::GetTableUniqueCellIds() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return std::vector<AXNodeID>();
- return std::vector<AXNodeID>(table_info->unique_cell_ids);
- }
- const std::vector<AXNode*>* AXNode::GetExtraMacNodes() const {
- DCHECK(!tree_->GetTreeUpdateInProgressState());
- // 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(GetRole());
- }
- absl::optional<int> AXNode::GetTableRowRowIndex() const {
- if (!IsTableRow())
- return absl::nullopt;
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- const auto& iter = table_info->row_id_to_index.find(id());
- if (iter == table_info->row_id_to_index.end())
- return absl::nullopt;
- return static_cast<int>(iter->second);
- }
- std::vector<AXNodeID> AXNode::GetTableRowNodeIds() const {
- std::vector<AXNodeID> 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->id());
- return row_node_ids;
- }
- #if BUILDFLAG(IS_APPLE)
- //
- // Table column-like nodes. These nodes are only present on macOS.
- //
- bool AXNode::IsTableColumn() const {
- return ui::IsTableColumn(GetRole());
- }
- absl::optional<int> AXNode::GetTableColColIndex() const {
- if (!IsTableColumn())
- return absl::nullopt;
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- int index = 0;
- for (const AXNode* node : table_info->extra_mac_nodes) {
- if (node == this)
- break;
- index++;
- }
- return index;
- }
- #endif // BUILDFLAG(IS_APPLE)
- //
- // Table cell-like nodes.
- //
- bool AXNode::IsTableCellOrHeader() const {
- return IsCellOrTableHeader(GetRole());
- }
- absl::optional<int> AXNode::GetTableCellIndex() const {
- if (!IsTableCellOrHeader())
- return absl::nullopt;
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- const auto& iter = table_info->cell_id_to_index.find(id());
- if (iter != table_info->cell_id_to_index.end())
- return static_cast<int>(iter->second);
- return absl::nullopt;
- }
- absl::optional<int> AXNode::GetTableCellColIndex() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- absl::optional<int> index = GetTableCellIndex();
- if (!index)
- return absl::nullopt;
- return static_cast<int>(table_info->cell_data_vector[*index].col_index);
- }
- absl::optional<int> AXNode::GetTableCellRowIndex() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- absl::optional<int> index = GetTableCellIndex();
- if (!index)
- return absl::nullopt;
- return static_cast<int>(table_info->cell_data_vector[*index].row_index);
- }
- absl::optional<int> AXNode::GetTableCellColSpan() const {
- // If it's not a table cell, don't return a col span.
- if (!IsTableCellOrHeader())
- return absl::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;
- }
- absl::optional<int> AXNode::GetTableCellRowSpan() const {
- // If it's not a table cell, don't return a row span.
- if (!IsTableCellOrHeader())
- return absl::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;
- }
- absl::optional<int> AXNode::GetTableCellAriaColIndex() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- absl::optional<int> index = GetTableCellIndex();
- if (!index)
- return absl::nullopt;
- int aria_col_index =
- static_cast<int>(table_info->cell_data_vector[*index].aria_col_index);
- // |aria-colindex| attribute is one-based, value less than 1 is invalid.
- // https://www.w3.org/TR/wai-aria-1.2/#aria-colindex
- return (aria_col_index > 0) ? absl::optional<int>(aria_col_index)
- : absl::nullopt;
- }
- absl::optional<int> AXNode::GetTableCellAriaRowIndex() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info)
- return absl::nullopt;
- absl::optional<int> index = GetTableCellIndex();
- if (!index)
- return absl::nullopt;
- int aria_row_index =
- static_cast<int>(table_info->cell_data_vector[*index].aria_row_index);
- // |aria-rowindex| attribute is one-based, value less than 1 is invalid.
- // https://www.w3.org/TR/wai-aria-1.2/#aria-rowindex
- return (aria_row_index > 0) ? absl::optional<int>(aria_row_index)
- : absl::nullopt;
- }
- std::vector<AXNodeID> AXNode::GetTableCellColHeaderNodeIds() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info || table_info->col_count <= 0)
- return std::vector<AXNodeID>();
- // 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<AXNodeID>(table_info->col_headers[col_index]);
- }
- void AXNode::GetTableCellColHeaders(std::vector<AXNode*>* col_headers) const {
- DCHECK(col_headers);
- std::vector<AXNodeID> col_header_ids = GetTableCellColHeaderNodeIds();
- IdVectorToNodeVector(col_header_ids, col_headers);
- }
- std::vector<AXNodeID> AXNode::GetTableCellRowHeaderNodeIds() const {
- const AXTableInfo* table_info = GetAncestorTableInfo();
- if (!table_info || table_info->row_count <= 0)
- return std::vector<AXNodeID>();
- // 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<AXNodeID>(table_info->row_headers[row_index]);
- }
- void AXNode::GetTableCellRowHeaders(std::vector<AXNode*>* row_headers) const {
- DCHECK(row_headers);
- std::vector<AXNodeID> row_header_ids = GetTableCellRowHeaderNodeIds();
- IdVectorToNodeVector(row_header_ids, row_headers);
- }
- bool AXNode::IsCellOrHeaderOfAriaGrid() const {
- if (!IsTableCellOrHeader())
- return false;
- const AXNode* node = this;
- while (node && !node->IsTable())
- node = node->GetParent();
- if (!node)
- return false;
- return node->GetRole() == ax::mojom::Role::kGrid ||
- node->GetRole() == ax::mojom::Role::kTreeGrid;
- }
- AXTableInfo* AXNode::GetAncestorTableInfo() const {
- const AXNode* node = this;
- while (node && !node->IsTable())
- node = node->GetParent();
- if (node)
- return tree_->GetTableInfo(node);
- return nullptr;
- }
- void AXNode::IdVectorToNodeVector(const std::vector<AXNodeID>& ids,
- std::vector<AXNode*>* nodes) const {
- for (AXNodeID id : ids) {
- AXNode* node = tree_->GetFromId(id);
- if (node)
- nodes->push_back(node);
- }
- }
- absl::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 absl::nullopt;
- }
- bool AXNode::IsOrderedSetItem() const {
- // Tree grid rows should be treated as ordered set items. Since we don't have
- // a separate row role for tree grid rows, we can't just add the Role::kRow to
- // IsItemLike. We need to validate that the row is indeed part of a tree grid.
- if (IsRowInTreeGrid(GetOrderedSet()))
- return true;
- return ui::IsItemLike(GetRole());
- }
- bool AXNode::IsOrderedSet() const {
- // Tree grid rows should be considered like ordered set items and a tree grid
- // like an ordered set. Continuing that logic, in order to compute the right
- // PosInSet and SetSize, row groups inside of a tree grid should also be
- // ordered sets.
- if (IsRowGroupInTreeGrid())
- return true;
- return ui::IsSetLike(GetRole());
- }
- // Uses AXTree's cache to calculate node's PosInSet.
- absl::optional<int> AXNode::GetPosInSet() {
- return tree_->GetPosInSet(*this);
- }
- // Uses AXTree's cache to calculate node's SetSize.
- absl::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 = GetRole();
- // Tree grid rows should be treated as ordered set items.
- if (IsRowInTreeGrid(ordered_set))
- return true;
- // Switch on role of ordered set
- switch (ordered_set->GetRole()) {
- 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() ||
- GetRole() == ax::mojom::Role::kLabelText ||
- GetRole() == ax::mojom::Role::kListItem ||
- GetRole() == ax::mojom::Role::kGenericContainer ||
- GetRole() == ax::mojom::Role::kScrollView ||
- GetRole() == ax::mojom::Role::kUnknown;
- }
- bool AXNode::IsRowInTreeGrid(const AXNode* ordered_set) const {
- // Tree grid rows have the requirement of being focusable, so we use it to
- // avoid iterating over rows that clearly aren't part of a tree grid.
- if (GetRole() != ax::mojom::Role::kRow ||
- !HasState(ax::mojom::State::kFocusable) || !ordered_set) {
- return false;
- }
- if (ordered_set->GetRole() == ax::mojom::Role::kTreeGrid)
- return true;
- return ordered_set->IsRowGroupInTreeGrid();
- }
- bool AXNode::IsRowGroupInTreeGrid() const {
- // To the best of our understanding, row groups can't be nested.
- //
- // According to https://www.w3.org/TR/wai-aria-1.1/#rowgroup, a row group is a
- // "structural equivalent to the thead, tfoot, and tbody elements in an HTML
- // table". It is specified in the spec of the thead, tfoot and tbody elements
- // that they need to be children of a table element, meaning that there can
- // only be one level of such elements. We assume the same for row groups.
- if (GetRole() != ax::mojom::Role::kRowGroup)
- return false;
- AXNode* ordered_set = GetOrderedSet();
- return ordered_set && ordered_set->GetRole() == ax::mojom::Role::kTreeGrid;
- }
- 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 = GetParent();
- // Continue walking up while parent is invalid, ignored, a generic container,
- // unknown, or embedded group.
- while (result && result->IsIgnoredContainerForOrderedSet()) {
- result = result->GetParent();
- }
- return result;
- }
- bool AXNode::IsReadOnlySupported() const {
- // Grid cells and headers can't be derived solely from the role (need to check
- // the ancestor chain) so check this first.
- if (IsCellOrHeaderOfAriaGrid())
- return true;
- // kPopUpButton is special in that it is the role Blink assigns for both
- // role=button with aria-haspopup set, along with <select> elements.
- // HTML AAM (https://w3c.github.io/html-aam/) maps <select> to the combobox
- // role, which supports readonly, but readonly is not supported for button
- // roles.
- if (GetRole() == ax::mojom::Role::kPopUpButton && !IsMenuListPopUpButton())
- return false;
- return ui::IsReadOnlySupported(GetRole());
- }
- bool AXNode::IsReadOnlyOrDisabled() const {
- switch (data().GetRestriction()) {
- case ax::mojom::Restriction::kReadOnly:
- case ax::mojom::Restriction::kDisabled:
- return true;
- case ax::mojom::Restriction::kNone: {
- if (HasState(ax::mojom::State::kEditable) ||
- HasState(ax::mojom::State::kRichlyEditable)) {
- return false;
- }
- if (ShouldHaveReadonlyStateByDefault(GetRole()))
- return true;
- // When readonly is not supported, we assume that the node is always
- // read-only and mark it as such since this is the default behavior.
- return !IsReadOnlySupported();
- }
- }
- }
- 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 =
- GetStringAttribute(ax::mojom::StringAttribute::kValue);
- float numeric_value;
- if (range_value.empty() &&
- GetFloatAttribute(ax::mojom::FloatAttribute::kValueForRange,
- &numeric_value)) {
- // This method of number to string conversion creates a localized string
- // and avoids padding with extra zeros after the decimal point.
- // For example, 3.5 is converted to "3.5" rather than "3.50000".
- return base::StringPrintf("%g", numeric_value);
- }
- return range_value;
- }
- std::string AXNode::GetValueForColorWell() const {
- DCHECK_EQ(GetRole(), ax::mojom::Role::kColorWell);
- // static cast because SkColor is a 4-byte unsigned int
- unsigned int color = static_cast<unsigned int>(
- 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);
- }
- bool AXNode::IsIgnored() const {
- // If the focus has moved, then it could make a previously ignored node
- // unignored or vice versa. We never ignore focused nodes otherwise users of
- // assistive software might be unable to interact with the webpage.
- return AXTree::ComputeNodeIsIgnored(&tree_->data(), data());
- }
- bool AXNode::IsIgnoredForTextNavigation() const {
- // Splitters do not contribute anything to the tree's text representation, so
- // stopping on a splitter would erroniously appear to a screen reader user
- // that the cursor has stopped on the next unignored object.
- if (GetRole() == ax::mojom::Role::kSplitter)
- return true;
- // A generic container without any unignored children that is not editable
- // should not be used for text-based navigation. Such nodes don't make sense
- // for screen readers to land on, since no role / text will be announced and
- // no action is possible.
- if (GetRole() == ax::mojom::Role::kGenericContainer &&
- !GetUnignoredChildCount() && !HasState(ax::mojom::State::kEditable)) {
- return true;
- }
- return false;
- }
- bool AXNode::IsInvisibleOrIgnored() const {
- return id() != tree_->data().focus_id && (IsIgnored() || data_.IsInvisible());
- }
- bool AXNode::IsChildOfLeaf() const {
- // TODO(nektar): Cache this state in `AXComputedNodeData`.
- for (const AXNode* ancestor = GetUnignoredParent(); ancestor;
- ancestor = ancestor->GetUnignoredParent()) {
- if (ancestor->IsLeaf())
- return true;
- }
- return false;
- }
- bool AXNode::IsEmptyLeaf() const {
- if (!IsLeaf())
- return false;
- if (GetUnignoredChildCountCrossingTreeBoundary())
- return !GetTextContentLengthUTF8();
- // Text exposed by ignored leaf (text) nodes is not exposed to the platforms'
- // accessibility layer, hence such leaf nodes are in effect empty.
- return IsIgnored() || !GetTextContentLengthUTF8();
- }
- bool AXNode::IsLeaf() const {
- // A node is a leaf if it has no descendants, i.e. if it is at the bottom of
- // the tree, regardless whether it is ignored or not.
- if (!GetChildCountCrossingTreeBoundary())
- return true;
- // Ignored nodes with any kind of descendants, (ignored or unignored), cannot
- // be leaves because: A) If some of their descendants are unignored then those
- // descendants need to be exposed to the platform layer, and B) If all of
- // their descendants are ignored they cannot be at the bottom of the platform
- // tree since that tree does not expose any ignored objects.
- if (IsIgnored())
- return false;
- // An unignored node is a leaf if all of its descendants are ignored.
- int child_count = GetUnignoredChildCountCrossingTreeBoundary();
- if (!child_count)
- return true;
- #if BUILDFLAG(IS_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 // BUILDFLAG(IS_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().IsAtomicTextField() || IsText())
- return true;
- // Roles whose children are only presentational according to the ARIA and
- // HTML5 Specs should be hidden from screen readers.
- switch (GetRole()) {
- // 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::kImage: {
- // HTML images (i.e. <img> elements) are not leaves when they are image
- // maps. Therefore, do not truncate descendants except in the case where
- // ARIA role=img or role=image because that's how we want to treat
- // ARIA-based images.
- const std::string role =
- GetStringAttribute(ax::mojom::StringAttribute::kRole);
- return role == "img" || role == "image";
- }
- case ax::mojom::Role::kDocCover:
- case ax::mojom::Role::kGraphicsSymbol:
- case ax::mojom::Role::kMeter:
- case ax::mojom::Role::kScrollBar:
- case ax::mojom::Role::kSpinButton:
- case ax::mojom::Role::kSlider:
- case ax::mojom::Role::kSplitter:
- case ax::mojom::Role::kProgressIndicator:
- return true;
- case ax::mojom::Role::kCheckBox:
- case ax::mojom::Role::kListBoxOption:
- // role="math" is flat. But always return false for kMathMLMath since the
- // children of a <math> tag should be exposed to make MathML accessible.
- case ax::mojom::Role::kMath:
- case ax::mojom::Role::kMenuListOption:
- case ax::mojom::Role::kMenuItem:
- case ax::mojom::Role::kMenuItemCheckBox:
- case ax::mojom::Role::kMenuItemRadio:
- case ax::mojom::Role::kPopUpButton:
- case ax::mojom::Role::kToggleButton:
- case ax::mojom::Role::kRadioButton:
- case ax::mojom::Role::kSwitch:
- case ax::mojom::Role::kTab: {
- // For historical reasons, truncate the children of these roles when they
- // have a single text child and are not editable.
- // TODO(accessibility) Consider removing this in the future, and exposing
- // all descendants, as it seems ATs do a good job of avoiding redundant
- // speech even if they have a text child. Removing this rule would allow
- // AT users to select any text visible in the page, and ensure that all
- // text is available to ATs that use the position of objects on the
- // screen. This has been manually tested in JAWS, NVDA, VoiceOver, Orca
- // and ChromeVox.
- // Note that the ARIA spec says, "User agents SHOULD NOT expose
- // descendants of this element through the platform accessibility API. If
- // user agents do not hide the descendant nodes, some information may be
- // read twice." However, this is not a MUST, and in non-simple cases
- // Chrome and Firefox already expose descendants, without causing issues.
- // Allow up to 2 text nodes so that list items with bullets are leaves.
- if (child_count > 2 || HasState(ax::mojom::State::kEditable))
- return false;
- const AXNode* child1 = GetFirstUnignoredChildCrossingTreeBoundary();
- if (!child1 || !child1->IsText())
- return false;
- const AXNode* child2 = child1->GetNextSibling();
- return !child2 || child2->IsText();
- }
- default:
- return false;
- }
- }
- bool AXNode::IsInListMarker() const {
- if (GetRole() == ax::mojom::Role::kListMarker)
- return true;
- // The children of a list marker node can only be text nodes.
- if (!IsText())
- return false;
- // There is no need to iterate over all the ancestors of the current node
- // since a list marker has descendants that are only 2 levels deep, i.e.:
- // AXLayoutObject role=kListMarker
- // ++StaticText
- // ++++InlineTextBox
- AXNode* parent_node = GetUnignoredParent();
- if (!parent_node)
- return false;
- if (parent_node->GetRole() == ax::mojom::Role::kListMarker)
- return true;
- AXNode* grandparent_node = parent_node->GetUnignoredParent();
- return grandparent_node &&
- grandparent_node->GetRole() == ax::mojom::Role::kListMarker;
- }
- bool AXNode::IsMenuListPopUpButton() const {
- if (GetRole() != ax::mojom::Role::kPopUpButton)
- 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->GetRole() == ax::mojom::Role::kMenuListPopup;
- }
- bool AXNode::IsCollapsedMenuListPopUpButton() const {
- if (!HasState(ax::mojom::State::kCollapsed))
- return false;
- return IsMenuListPopUpButton();
- }
- bool AXNode::IsRootWebAreaForPresentationalIframe() const {
- if (!ui::IsPlatformDocument(GetRole()))
- return false;
- const AXNode* parent = GetUnignoredParentCrossingTreeBoundary();
- if (!parent)
- return false;
- return parent->GetRole() == ax::mojom::Role::kIframePresentational;
- }
- 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->GetRole() != ax::mojom::Role::kPopUpButton) {
- node = node->GetParent();
- if (!node)
- return nullptr;
- }
- return node->IsCollapsedMenuListPopUpButton() ? node : nullptr;
- }
- bool AXNode::IsEmbeddedGroup() const {
- if (GetRole() != ax::mojom::Role::kGroup || !GetParent())
- return false;
- return ui::IsSetLike(GetParent()->GetRole());
- }
- AXNode* AXNode::GetLowestPlatformAncestor() const {
- AXNode* current_node = const_cast<AXNode*>(this);
- AXNode* lowest_unignored_node = current_node;
- for (; lowest_unignored_node && lowest_unignored_node->IsIgnored();
- lowest_unignored_node = lowest_unignored_node->GetParent()) {
- }
- // `highest_leaf_node` could be nullptr.
- AXNode* highest_leaf_node = lowest_unignored_node;
- // For the purposes of this method, a leaf node does not include leaves in the
- // internal accessibility tree, only in the platform exposed tree.
- for (AXNode* ancestor_node = lowest_unignored_node; ancestor_node;
- ancestor_node = ancestor_node->GetUnignoredParent()) {
- if (ancestor_node->IsLeaf())
- highest_leaf_node = ancestor_node;
- }
- if (highest_leaf_node)
- return highest_leaf_node;
- if (lowest_unignored_node)
- return lowest_unignored_node;
- return current_node;
- }
- 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 and placeholder text.
- // TODO(nektar): Fix all such inconsistencies in Blink.
- //
- // Also, ARIA text and search boxes may not have the contenteditable attribute
- // set, but they should still be treated the same as all other text fields.
- // (See `AXNodeData::IsAtomicTextField()` for more details.)
- for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
- ancestor = ancestor->GetUnignoredParent()) {
- if (ancestor->data().IsTextField())
- return ancestor;
- }
- return nullptr;
- }
- AXNode* AXNode::GetTextFieldInnerEditorElement() const {
- if (!data().IsAtomicTextField() || !GetUnignoredChildCount())
- return nullptr;
- // Text fields wrap their static text and inline text boxes in generic
- // containers, and some, like <input type="search">, wrap the wrapper as well.
- // There are several incarnations of this structure.
- // 1. An empty atomic text field:
- // -- Generic container <-- there can be any number of these in a chain.
- // However, some empty text fields have the below structure, with empty
- // text boxes.
- // 2. A single line, an atomic text field with some text in it:
- // -- Generic container <-- there can be any number of these in a chain.
- // ---- Static text
- // ------ Inline text box children (zero or more)
- // ---- Line Break (optional, a placeholder break element if the text data
- // ends with '\n' or '\r')
- // 3. A multiline textarea with some text in it:
- // Similar to #2, but can repeat the static text, line break children
- // multiple times.
- AXNode* text_container = GetDeepestFirstUnignoredChild();
- DCHECK(text_container) << "Unable to retrieve deepest unignored child on\n"
- << *this;
- // Non-empty text fields expose a set of static text objects with one or more
- // inline text boxes each. On some platforms, such as Android, we don't enable
- // inline text boxes, and only the static text objects are exposed.
- if (text_container->GetRole() == ax::mojom::Role::kInlineTextBox)
- text_container = text_container->GetUnignoredParent();
- // Get the parent of the static text or the line break, if any; a line break
- // is possible when the field contains a line break as its first character.
- if (text_container->GetRole() == ax::mojom::Role::kStaticText ||
- text_container->GetRole() == ax::mojom::Role::kLineBreak) {
- text_container = text_container->GetUnignoredParent();
- }
- DCHECK(text_container) << "Unexpected unignored parent while computing text "
- "field inner editor element on\n"
- << *this;
- if (text_container->GetRole() == ax::mojom::Role::kGenericContainer)
- return text_container;
- return nullptr;
- }
- AXNode* AXNode::GetSelectionContainer() const {
- for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
- ancestor = ancestor->GetUnignoredParent()) {
- if (IsContainerWithSelectableChildren(ancestor->GetRole()))
- return ancestor;
- }
- return nullptr;
- }
- AXNode* AXNode::GetTableAncestor() const {
- for (AXNode* ancestor = const_cast<AXNode*>(this); ancestor;
- ancestor = ancestor->GetUnignoredParent()) {
- if (ancestor->IsTable())
- return ancestor;
- }
- return nullptr;
- }
- bool AXNode::IsDescendantOfAtomicTextField() const {
- AXNode* text_field_node = GetTextFieldAncestor();
- return text_field_node && text_field_node->data().IsAtomicTextField();
- }
- } // namespace ui
|