blob: a57ae3d9672e5fc5ae6370cf902a6fff3d985605 [file] [log] [blame]
/*
* Copyright (C) 2008, 2009, 2011 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/modules/accessibility/ax_object.h"
#include <algorithm>
#include <ostream>
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "skia/ext/skia_matrix_44.h"
#include "third_party/blink/public/common/input/web_menu_source_type.h"
#include "third_party/blink/public/mojom/frame/user_activation_notification_type.mojom-blink.h"
#include "third_party/blink/public/mojom/input/focus_type.mojom-blink.h"
#include "third_party/blink/renderer/core/aom/accessible_node.h"
#include "third_party/blink/renderer/core/aom/accessible_node_list.h"
#include "third_party/blink/renderer/core/css/resolver/style_resolver.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/dom_node_ids.h"
#include "third_party/blink/renderer/core/dom/events/simulated_click_options.h"
#include "third_party/blink/renderer/core/dom/focus_params.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h"
#include "third_party/blink/renderer/core/html/custom/element_internals.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_select_element.h"
#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h"
#include "third_party/blink/renderer/core/html/forms/text_control_element.h"
#include "third_party/blink/renderer/core/html/html_dialog_element.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_frame_owner_element.h"
#include "third_party/blink/renderer/core/html/html_head_element.h"
#include "third_party/blink/renderer/core/html/html_image_element.h"
#include "third_party/blink/renderer/core/html/html_map_element.h"
#include "third_party/blink/renderer/core/html/html_script_element.h"
#include "third_party/blink/renderer/core/html/html_slot_element.h"
#include "third_party/blink/renderer/core/html/html_style_element.h"
#include "third_party/blink/renderer/core/html/html_table_cell_element.h"
#include "third_party/blink/renderer/core/html/html_table_element.h"
#include "third_party/blink/renderer/core/html/html_table_row_element.h"
#include "third_party/blink/renderer/core/html/html_table_section_element.h"
#include "third_party/blink/renderer/core/html/html_title_element.h"
#include "third_party/blink/renderer/core/html/media/html_media_element.h"
#include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h"
#include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h"
#include "third_party/blink/renderer/core/input/event_handler.h"
#include "third_party/blink/renderer/core/input_type_names.h"
#include "third_party/blink/renderer/core/layout/layout_box.h"
#include "third_party/blink/renderer/core/layout/layout_box_model_object.h"
#include "third_party/blink/renderer/core/layout/layout_image.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/focus_controller.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/scrolling/top_document_root_scroller_controller.h"
#include "third_party/blink/renderer/core/svg/svg_element.h"
#include "third_party/blink/renderer/core/svg/svg_g_element.h"
#include "third_party/blink/renderer/core/svg/svg_style_element.h"
#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h"
#include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h"
#include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h"
#include "third_party/blink/renderer/modules/accessibility/ax_range.h"
#include "third_party/blink/renderer/modules/accessibility/ax_selection.h"
#include "third_party/blink/renderer/modules/accessibility/ax_sparse_attribute_setter.h"
#include "third_party/blink/renderer/platform/instrumentation/tracing/trace_event.h"
#include "third_party/blink/renderer/platform/language.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/platform_locale.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "third_party/blink/renderer/platform/wtf/wtf_size_t.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_common.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
namespace blink {
namespace {
#if defined(AX_FAIL_FAST_BUILD)
// TODO(accessibility) Move this out of DEBUG by having a new enum in
// ax_enums.mojom, and a matching ToString() in ax_enum_utils, as well as move
// out duplicate code of String IgnoredReasonName(AXIgnoredReason reason) in
// inspector_type_builder_helper.cc.
String IgnoredReasonName(AXIgnoredReason reason) {
switch (reason) {
case kAXActiveModalDialog:
return "activeModalDialog";
case kAXAriaModalDialog:
return "activeAriaModalDialog";
case kAXAriaHiddenElement:
return "ariaHiddenElement";
case kAXAriaHiddenSubtree:
return "ariaHiddenSubtree";
case kAXEmptyAlt:
return "emptyAlt";
case kAXEmptyText:
return "emptyText";
case kAXInertElement:
return "inertElement";
case kAXInertSubtree:
return "inertSubtree";
case kAXLabelContainer:
return "labelContainer";
case kAXLabelFor:
return "labelFor";
case kAXNotRendered:
return "notRendered";
case kAXNotVisible:
return "notVisible";
case kAXPresentational:
return "presentationalRole";
case kAXProbablyPresentational:
return "probablyPresentational";
case kAXUninteresting:
return "uninteresting";
}
NOTREACHED();
return "";
}
String GetIgnoredReasonsDebugString(AXObject::IgnoredReasons& reasons) {
if (reasons.size() == 0)
return "";
String string_builder = "(";
for (wtf_size_t count = 0; count < reasons.size(); count++) {
if (count > 0)
string_builder = string_builder + ',';
string_builder = string_builder + IgnoredReasonName(reasons[count].reason);
}
string_builder = string_builder + ")";
return string_builder;
}
#endif
String GetNodeString(Node* node) {
if (node->IsTextNode()) {
String string_builder = "\"";
string_builder = string_builder + node->nodeValue();
string_builder = string_builder + "\"";
return string_builder;
}
Element* element = DynamicTo<Element>(node);
if (!element)
return "<null>";
String string_builder = "<";
string_builder = string_builder + element->tagName().LowerASCII();
// Cannot safely get @class from SVG elements.
if (!element->IsSVGElement() &&
element->FastHasAttribute(html_names::kClassAttr)) {
string_builder = string_builder + "." +
element->FastGetAttribute(html_names::kClassAttr);
}
if (element->FastHasAttribute(html_names::kIdAttr)) {
string_builder =
string_builder + "#" + element->FastGetAttribute(html_names::kIdAttr);
}
return string_builder + ">";
}
Node* GetParentNodeForComputeParent(Node* node) {
if (!node)
return nullptr;
// Use LayoutTreeBuilderTraversal::Parent(), which handles pseudo content.
// This can return nullptr for a node that is never visited by
// LayoutTreeBuilderTraversal's child traversal. For example, while an element
// can be appended as a <textarea>'s child, it is never visited by
// LayoutTreeBuilderTraversal's child traversal. Therefore, returning null in
// this case is appropriate, because that child content is not attached to any
// parent as far as rendering or accessibility are concerned.
// Whenever null is returned from this function, then a parent cannot be
// computed, and when a parent is not provided or computed, the accessible
// object will not be created.
Node* parent = LayoutTreeBuilderTraversal::Parent(*node);
HTMLMapElement* map_element = DynamicTo<HTMLMapElement>(parent);
if (!map_element)
return parent;
// For a <map>, return the <img> associated with it. This is necessary because
// the AX tree is flat, adding image map children as children of the <img>,
// whereas in the DOM they are actually children of the <map>.
// Therefore, if a node is a DOM child of a map, its AX parent is the image.
// This code double checks that the image actually uses the map.
HTMLImageElement* image_element = map_element->ImageElement();
return AXObject::GetMapForImage(image_element) == map_element ? image_element
: nullptr;
}
#if DCHECK_IS_ON()
bool IsValidRole(ax::mojom::blink::Role role) {
// Check for illegal roles that should not be assigned in Blink.
switch (role) {
case ax::mojom::blink::Role::kCaret:
case ax::mojom::blink::Role::kClient:
case ax::mojom::blink::Role::kColumn:
case ax::mojom::blink::Role::kDesktop:
case ax::mojom::blink::Role::kKeyboard:
case ax::mojom::blink::Role::kImeCandidate:
case ax::mojom::blink::Role::kListGrid:
case ax::mojom::blink::Role::kPane:
case ax::mojom::blink::Role::kPdfActionableHighlight:
case ax::mojom::blink::Role::kPdfRoot:
case ax::mojom::blink::Role::kTableHeaderContainer:
case ax::mojom::blink::Role::kTitleBar:
case ax::mojom::blink::Role::kUnknown:
case ax::mojom::blink::Role::kWebView:
case ax::mojom::blink::Role::kWindow:
return false;
default:
return true;
}
}
#endif
struct RoleHashTraits : HashTraits<ax::mojom::blink::Role> {
static const bool kEmptyValueIsZero = true;
static ax::mojom::blink::Role EmptyValue() {
return ax::mojom::blink::Role::kUnknown;
}
};
constexpr wtf_size_t kNumRoles =
static_cast<wtf_size_t>(ax::mojom::blink::Role::kMaxValue) + 1;
using ARIARoleMap = HashMap<String,
ax::mojom::blink::Role,
CaseFoldingHash,
HashTraits<String>,
RoleHashTraits>;
struct RoleEntry {
const char* role_name;
ax::mojom::blink::Role role;
};
// Mapping of ARIA role name to internal role name.
// This is used for the following:
// 1. Map from an ARIA role to the internal role when building tree.
// 2. Map from an internal role to an ARIA role name, for debugging, the
// xml-roles object attribute and element.computedRole.
const RoleEntry kAriaRoles[] = {
{"alert", ax::mojom::blink::Role::kAlert},
{"alertdialog", ax::mojom::blink::Role::kAlertDialog},
{"application", ax::mojom::blink::Role::kApplication},
{"article", ax::mojom::blink::Role::kArticle},
{"banner", ax::mojom::blink::Role::kBanner},
{"blockquote", ax::mojom::blink::Role::kBlockquote},
{"button", ax::mojom::blink::Role::kButton},
{"caption", ax::mojom::blink::Role::kCaption},
{"cell", ax::mojom::blink::Role::kCell},
{"code", ax::mojom::blink::Role::kCode},
{"checkbox", ax::mojom::blink::Role::kCheckBox},
{"columnheader", ax::mojom::blink::Role::kColumnHeader},
{"combobox", ax::mojom::blink::Role::kComboBoxGrouping},
{"comment", ax::mojom::blink::Role::kComment},
{"complementary", ax::mojom::blink::Role::kComplementary},
{"contentinfo", ax::mojom::blink::Role::kContentInfo},
{"definition", ax::mojom::blink::Role::kDefinition},
{"deletion", ax::mojom::blink::Role::kContentDeletion},
{"dialog", ax::mojom::blink::Role::kDialog},
{"directory", ax::mojom::blink::Role::kDirectory},
// -------------------------------------------------
// DPub Roles:
// www.w3.org/TR/dpub-aam-1.0/#mapping_role_table
{"doc-abstract", ax::mojom::blink::Role::kDocAbstract},
{"doc-acknowledgments", ax::mojom::blink::Role::kDocAcknowledgments},
{"doc-afterword", ax::mojom::blink::Role::kDocAfterword},
{"doc-appendix", ax::mojom::blink::Role::kDocAppendix},
{"doc-backlink", ax::mojom::blink::Role::kDocBackLink},
// Deprecated in DPUB-ARIA 1.1. Use a listitem inside of a doc-bibliography.
{"doc-biblioentry", ax::mojom::blink::Role::kDocBiblioEntry},
{"doc-bibliography", ax::mojom::blink::Role::kDocBibliography},
{"doc-biblioref", ax::mojom::blink::Role::kDocBiblioRef},
{"doc-chapter", ax::mojom::blink::Role::kDocChapter},
{"doc-colophon", ax::mojom::blink::Role::kDocColophon},
{"doc-conclusion", ax::mojom::blink::Role::kDocConclusion},
{"doc-cover", ax::mojom::blink::Role::kDocCover},
{"doc-credit", ax::mojom::blink::Role::kDocCredit},
{"doc-credits", ax::mojom::blink::Role::kDocCredits},
{"doc-dedication", ax::mojom::blink::Role::kDocDedication},
// Deprecated in DPUB-ARIA 1.1. Use a listitem inside of a doc-endnotes.
{"doc-endnote", ax::mojom::blink::Role::kDocEndnote},
{"doc-endnotes", ax::mojom::blink::Role::kDocEndnotes},
{"doc-epigraph", ax::mojom::blink::Role::kDocEpigraph},
{"doc-epilogue", ax::mojom::blink::Role::kDocEpilogue},
{"doc-errata", ax::mojom::blink::Role::kDocErrata},
{"doc-example", ax::mojom::blink::Role::kDocExample},
{"doc-footnote", ax::mojom::blink::Role::kDocFootnote},
{"doc-foreword", ax::mojom::blink::Role::kDocForeword},
{"doc-glossary", ax::mojom::blink::Role::kDocGlossary},
{"doc-glossref", ax::mojom::blink::Role::kDocGlossRef},
{"doc-index", ax::mojom::blink::Role::kDocIndex},
{"doc-introduction", ax::mojom::blink::Role::kDocIntroduction},
{"doc-noteref", ax::mojom::blink::Role::kDocNoteRef},
{"doc-notice", ax::mojom::blink::Role::kDocNotice},
{"doc-pagebreak", ax::mojom::blink::Role::kDocPageBreak},
{"doc-pagefooter", ax::mojom::blink::Role::kDocPageFooter},
{"doc-pageheader", ax::mojom::blink::Role::kDocPageHeader},
{"doc-pagelist", ax::mojom::blink::Role::kDocPageList},
{"doc-part", ax::mojom::blink::Role::kDocPart},
{"doc-preface", ax::mojom::blink::Role::kDocPreface},
{"doc-prologue", ax::mojom::blink::Role::kDocPrologue},
{"doc-pullquote", ax::mojom::blink::Role::kDocPullquote},
{"doc-qna", ax::mojom::blink::Role::kDocQna},
{"doc-subtitle", ax::mojom::blink::Role::kDocSubtitle},
{"doc-tip", ax::mojom::blink::Role::kDocTip},
{"doc-toc", ax::mojom::blink::Role::kDocToc},
// End DPub roles.
// -------------------------------------------------
{"document", ax::mojom::blink::Role::kDocument},
{"emphasis", ax::mojom::blink::Role::kEmphasis},
{"feed", ax::mojom::blink::Role::kFeed},
{"figure", ax::mojom::blink::Role::kFigure},
{"form", ax::mojom::blink::Role::kForm},
{"generic", ax::mojom::blink::Role::kGenericContainer},
// -------------------------------------------------
// ARIA Graphics module roles:
// https://rawgit.com/w3c/graphics-aam/master/
{"graphics-document", ax::mojom::blink::Role::kGraphicsDocument},
{"graphics-object", ax::mojom::blink::Role::kGraphicsObject},
{"graphics-symbol", ax::mojom::blink::Role::kGraphicsSymbol},
// End ARIA Graphics module roles.
// -------------------------------------------------
{"grid", ax::mojom::blink::Role::kGrid},
{"gridcell", ax::mojom::blink::Role::kCell},
{"group", ax::mojom::blink::Role::kGroup},
{"heading", ax::mojom::blink::Role::kHeading},
{"img", ax::mojom::blink::Role::kImage},
{"insertion", ax::mojom::blink::Role::kContentInsertion},
{"link", ax::mojom::blink::Role::kLink},
{"list", ax::mojom::blink::Role::kList},
{"listbox", ax::mojom::blink::Role::kListBox},
{"listitem", ax::mojom::blink::Role::kListItem},
{"log", ax::mojom::blink::Role::kLog},
{"main", ax::mojom::blink::Role::kMain},
{"marquee", ax::mojom::blink::Role::kMarquee},
{"math", ax::mojom::blink::Role::kMath},
{"menu", ax::mojom::blink::Role::kMenu},
{"menubar", ax::mojom::blink::Role::kMenuBar},
{"menuitem", ax::mojom::blink::Role::kMenuItem},
{"menuitemcheckbox", ax::mojom::blink::Role::kMenuItemCheckBox},
{"menuitemradio", ax::mojom::blink::Role::kMenuItemRadio},
{"mark", ax::mojom::blink::Role::kMark},
{"meter", ax::mojom::blink::Role::kMeter},
{"navigation", ax::mojom::blink::Role::kNavigation},
// role="presentation" is the same as role="none".
{"presentation", ax::mojom::blink::Role::kNone},
// role="none" is listed after role="presentation", so that it is the
// canonical name in devtools and tests.
{"none", ax::mojom::blink::Role::kNone},
{"note", ax::mojom::blink::Role::kNote},
{"option", ax::mojom::blink::Role::kListBoxOption},
{"paragraph", ax::mojom::blink::Role::kParagraph},
{"progressbar", ax::mojom::blink::Role::kProgressIndicator},
{"radio", ax::mojom::blink::Role::kRadioButton},
{"radiogroup", ax::mojom::blink::Role::kRadioGroup},
{"region", ax::mojom::blink::Role::kRegion},
{"row", ax::mojom::blink::Role::kRow},
{"rowgroup", ax::mojom::blink::Role::kRowGroup},
{"rowheader", ax::mojom::blink::Role::kRowHeader},
{"scrollbar", ax::mojom::blink::Role::kScrollBar},
{"search", ax::mojom::blink::Role::kSearch},
{"searchbox", ax::mojom::blink::Role::kSearchBox},
{"separator", ax::mojom::blink::Role::kSplitter},
{"slider", ax::mojom::blink::Role::kSlider},
{"spinbutton", ax::mojom::blink::Role::kSpinButton},
{"status", ax::mojom::blink::Role::kStatus},
{"strong", ax::mojom::blink::Role::kStrong},
{"suggestion", ax::mojom::blink::Role::kSuggestion},
{"switch", ax::mojom::blink::Role::kSwitch},
{"tab", ax::mojom::blink::Role::kTab},
{"table", ax::mojom::blink::Role::kTable},
{"tablist", ax::mojom::blink::Role::kTabList},
{"tabpanel", ax::mojom::blink::Role::kTabPanel},
{"term", ax::mojom::blink::Role::kTerm},
{"textbox", ax::mojom::blink::Role::kTextField},
{"time", ax::mojom::blink::Role::kTime},
{"timer", ax::mojom::blink::Role::kTimer},
{"toolbar", ax::mojom::blink::Role::kToolbar},
{"tooltip", ax::mojom::blink::Role::kTooltip},
{"tree", ax::mojom::blink::Role::kTree},
{"treegrid", ax::mojom::blink::Role::kTreeGrid},
{"treeitem", ax::mojom::blink::Role::kTreeItem}};
// More friendly names for debugging. These are roles which don't map from
// the ARIA role name to the internal role when building the tree, but when
// debugging, we want to show the ARIA role name, since it is close in meaning.
const RoleEntry kReverseRoles[] = {
{"banner", ax::mojom::blink::Role::kHeader},
{"button", ax::mojom::blink::Role::kToggleButton},
{"combobox", ax::mojom::blink::Role::kPopUpButton},
{"contentinfo", ax::mojom::blink::Role::kFooter},
{"menuitem", ax::mojom::blink::Role::kMenuListOption},
{"combobox", ax::mojom::blink::Role::kComboBoxMenuButton},
{"combobox", ax::mojom::blink::Role::kTextFieldWithComboBox}};
static ARIARoleMap* CreateARIARoleMap() {
ARIARoleMap* role_map = new ARIARoleMap;
for (auto aria_role : kAriaRoles)
role_map->Set(String(aria_role.role_name), aria_role.role);
return role_map;
}
// The role name vector contains only ARIA roles, and no internal roles.
static Vector<AtomicString>* CreateARIARoleNameVector() {
Vector<AtomicString>* role_name_vector = new Vector<AtomicString>(kNumRoles);
role_name_vector->Fill(g_null_atom, kNumRoles);
for (auto aria_role : kAriaRoles) {
(*role_name_vector)[static_cast<wtf_size_t>(aria_role.role)] =
AtomicString(aria_role.role_name);
}
for (auto reverse_role : kReverseRoles) {
(*role_name_vector)[static_cast<wtf_size_t>(reverse_role.role)] =
AtomicString(reverse_role.role_name);
}
return role_name_vector;
}
HTMLDialogElement* GetActiveDialogElement(Node* node) {
return node->GetDocument().ActiveModalDialog();
}
} // namespace
int32_t ToAXMarkerType(DocumentMarker::MarkerType marker_type) {
ax::mojom::blink::MarkerType result;
switch (marker_type) {
case DocumentMarker::kSpelling:
result = ax::mojom::blink::MarkerType::kSpelling;
break;
case DocumentMarker::kGrammar:
result = ax::mojom::blink::MarkerType::kGrammar;
break;
case DocumentMarker::kTextFragment:
case DocumentMarker::kTextMatch:
result = ax::mojom::blink::MarkerType::kTextMatch;
break;
case DocumentMarker::kActiveSuggestion:
result = ax::mojom::blink::MarkerType::kActiveSuggestion;
break;
case DocumentMarker::kSuggestion:
result = ax::mojom::blink::MarkerType::kSuggestion;
break;
default:
result = ax::mojom::blink::MarkerType::kNone;
break;
}
return static_cast<int32_t>(result);
}
// static
unsigned AXObject::number_of_live_ax_objects_ = 0;
AXObject::AXObject(AXObjectCacheImpl& ax_object_cache)
: id_(0),
parent_(nullptr),
role_(ax::mojom::blink::Role::kUnknown),
explicit_container_id_(0),
last_modification_count_(-1),
cached_is_ignored_(false),
cached_is_ignored_but_included_in_tree_(false),
cached_is_inert_or_aria_hidden_(false),
cached_is_descendant_of_disabled_node_(false),
cached_live_region_root_(nullptr),
cached_aria_column_index_(0),
cached_aria_row_index_(0),
ax_object_cache_(&ax_object_cache) {
++number_of_live_ax_objects_;
}
AXObject::~AXObject() {
DCHECK(IsDetached());
--number_of_live_ax_objects_;
}
void AXObject::Init(AXObject* parent) {
#if DCHECK_IS_ON()
DCHECK(!parent_) << "Should not already have a cached parent:"
<< "\n* Child = " << GetNode() << " / " << GetLayoutObject()
<< "\n* Parent = " << parent_->ToString(true, true)
<< "\n* Equal to passed-in parent? " << (parent == parent_);
DCHECK(!is_initializing_);
base::AutoReset<bool> reentrancy_protector(&is_initializing_, true);
#endif // DCHECK_IS_ON()
// The role must be determined immediately.
// Note: in order to avoid reentrancy, the role computation cannot use the
// ParentObject(), although it can use the DOM parent.
role_ = DetermineAccessibilityRole();
#if DCHECK_IS_ON()
DCHECK(IsValidRole(role_)) << "Illegal " << role_ << " for\n"
<< GetNode() << '\n'
<< GetLayoutObject();
#endif // DCHECK_IS_ON()
// Determine the parent as soon as possible.
// Every AXObject must have a parent unless it's the root.
SetParent(parent);
DCHECK(parent_ || IsRoot())
<< "The following node should have a parent: " << GetNode();
// The parent cannot have children. This object must be destroyed.
DCHECK(!parent_ || parent_->CanHaveChildren())
<< "Tried to set a parent that cannot have children:"
<< "\n* Parent = " << parent_->ToString(true, true)
<< "\n* Child = " << ToString(true, true);
// This is one after the role_ is computed, because the role is used to
// determine whether an AXObject can have children.
children_dirty_ = CanHaveChildren();
// Ensure that the aria-owns relationship is set before attempting
// to update cached attribute values.
if (GetNode())
AXObjectCache().MaybeNewRelationTarget(*GetNode(), this);
UpdateCachedAttributeValuesIfNeeded(false);
}
void AXObject::Detach() {
// Prevents LastKnown*() methods from returning the wrong values.
cached_is_ignored_ = true;
cached_is_ignored_but_included_in_tree_ = false;
if (IsDetached()) {
// Only mock objects can end up being detached twice, because their owner
// may have needed to detach them when they were detached, but couldn't
// remove them from the object cache yet.
DCHECK(IsMockObject()) << "Object detached twice: " << RoleValue();
return;
}
#if DCHECK_IS_ON()
DCHECK(ax_object_cache_);
DCHECK(!ax_object_cache_->IsFrozen())
<< "Do not detach children while the tree is frozen, in order to avoid "
"an object detaching itself in the middle of computing its own "
"accessibility properties.";
#endif
#if defined(AX_FAIL_FAST_BUILD)
SANITIZER_CHECK(!is_adding_children_) << ToString(true, true);
#endif
// Clear any children and call DetachFromParent() on them so that
// no children are left with dangling pointers to their parent.
ClearChildren();
parent_ = nullptr;
ax_object_cache_ = nullptr;
children_dirty_ = false;
}
bool AXObject::IsDetached() const {
return !ax_object_cache_;
}
bool AXObject::IsRoot() const {
return GetNode() && GetNode() == &AXObjectCache().GetDocument();
}
void AXObject::SetParent(AXObject* new_parent) const {
#if DCHECK_IS_ON()
if (!new_parent && !IsRoot()) {
std::ostringstream message;
message << "Parent cannot be null, except at the root. "
"Parent chain from DOM, starting at |this|:";
int count = 0;
for (Node* node = GetNode(); node;
node = GetParentNodeForComputeParent(node)) {
message << "\n"
<< (++count) << ". " << node
<< "\n LayoutObject=" << node->GetLayoutObject();
if (AXObject* obj = AXObjectCache().Get(node))
message << "\n " << obj->ToString(true, true);
}
NOTREACHED() << message.str();
}
if (new_parent) {
DCHECK(!new_parent->IsDetached())
<< "Cannot set parent to a detached object:"
<< "\n* Child: " << ToString(true, true)
<< "\n* New parent: " << new_parent->ToString(true, true);
}
// Check to ensure that if the parent is changing from a previous parent,
// that |this| is not still a child of that one.
// This is similar to the IsParentUnignoredOf() check in
// BlinkAXTreeSource, but closer to where the problem would occur.
if (parent_ && new_parent != parent_ && !parent_->NeedsToUpdateChildren() &&
!parent_->IsDetached()) {
for (const auto& child : parent_->ChildrenIncludingIgnored()) {
DCHECK(child != this) << "Previous parent still has |this| child:\n"
<< ToString(true, true) << " should be a child of "
<< new_parent->ToString(true, true) << " not of "
<< parent_->ToString(true, true);
}
// TODO(accessibility) This should not be reached unless this method is
// called on an AXObject of role kRootWebArea or when the parent's
// children are dirty, aka parent_->NeedsToUpdateChildren());
// Ideally we will also ensure |this| is in the parent's children now, so
// that ClearChildren() can later find the child to detach from the parent.
}
#endif
parent_ = new_parent;
}
bool AXObject::IsMissingParent() const {
if (!parent_) {
// Do not attempt to repair the ParentObject() of a validation message
// object, because hidden ones are purposely kept around without being in
// the tree, and without a parent, for potential later reuse.
// TODO(accessibility) This is ugly. Consider destroying validation message
// objects between uses instead. See GetOrCreateValidationMessageObject().
return !IsRoot() && !IsValidationMessage();
}
if (parent_->IsDetached())
return true;
return false;
}
void AXObject::RepairMissingParent() const {
DCHECK(IsMissingParent());
SetParent(ComputeParent());
}
// In many cases, ComputeParent() is not called, because the parent adding
// the parent adding the child will pass itself into AXObjectCacheImpl.
// ComputeParent() is still necessary because some parts of the code,
// especially web tests, result in AXObjects being created in the middle of
// the tree before their parents are created.
// TODO(accessibility) Consider forcing all ax objects to be created from
// the top down, eliminating the need for ComputeParent().
AXObject* AXObject::ComputeParent() const {
#if defined(AX_FAIL_FAST_BUILD)
SANITIZER_CHECK(!IsDetached());
SANITIZER_CHECK(!IsMockObject())
<< "A mock object must have a parent, and cannot exist without one. "
"The parent is set when the object is constructed.";
SANITIZER_CHECK(GetNode() || GetLayoutObject() || IsVirtualObject())
<< "Can't compute parent on AXObjects without a backing Node "
"LayoutObject, "
" or AccessibleNode. Objects without those must set the "
"parent in Init(), |this| = "
<< RoleValue();
#endif
AXObject* ax_parent = nullptr;
if (AXObjectCache().IsAriaOwned(this)) {
ax_parent = AXObjectCache().ValidatedAriaOwner(this);
} else if (IsVirtualObject()) {
ax_parent =
ComputeAccessibleNodeParent(AXObjectCache(), *GetAccessibleNode());
}
if (!ax_parent) {
ax_parent =
ComputeNonARIAParent(AXObjectCache(), GetNode(), GetLayoutObject());
}
CHECK(!ax_parent || !ax_parent->IsDetached())
<< "Computed parent should never be detached:"
<< "\n* Child: " << GetNode()
<< "\n* Parent: " << ax_parent->ToString(true, true);
return ax_parent;
}
// static
bool AXObject::CanComputeAsNaturalParent(Node* node) {
// A <select> menulist that will use AXMenuList is not allowed.
if (AXObjectCacheImpl::UseAXMenuList()) {
if (auto* select = DynamicTo<HTMLSelectElement>(node)) {
if (select->UsesMenuList())
return false;
}
}
// A <br> can only support AXInlineTextBox children, which is never the result
// of a parent computation (the parent of the children is set at Init()).
if (IsA<HTMLBRElement>(node))
return false;
// Image map parent-child relationships work as follows:
// - The image is the parent
// - The DOM children of the ssociated <map> are the children
// This is accomplished by having GetParentNodeForComputeParent() return the
// <img> instead of the <map> for the map's children.
if (IsA<HTMLMapElement>(node))
return false;
return true;
}
// static
AXObject* AXObject::ComputeAccessibleNodeParent(
AXObjectCacheImpl& cache,
AccessibleNode& accessible_node) {
if (AccessibleNode* parent_accessible_node = accessible_node.GetParent()) {
if (AXObject* parent = cache.Get(parent_accessible_node))
return parent;
// If |accessible_node|'s parent is attached to a DOM element, we return the
// AXObject of the DOM element as the parent AXObject of |accessible_node|,
// since the accessible node directly attached to an element should not have
// its own AXObject.
if (Element* element = parent_accessible_node->element())
return cache.GetOrCreate(element);
// Compute grandparent first, since constructing parent AXObject for
// |accessible_node| requires grandparent to be provided.
AXObject* grandparent_object =
AXObject::ComputeAccessibleNodeParent(cache, *parent_accessible_node);
if (grandparent_object)
return cache.GetOrCreate(parent_accessible_node, grandparent_object);
}
return nullptr;
}
// static
HTMLMapElement* AXObject::GetMapForImage(Node* image) {
if (!IsA<HTMLImageElement>(image))
return nullptr;
LayoutImage* layout_image = DynamicTo<LayoutImage>(image->GetLayoutObject());
if (!layout_image)
return nullptr;
HTMLMapElement* map_element = layout_image->ImageMap();
if (!map_element)
return nullptr;
// Don't allow images that are actually children of a map, as this could lead
// to an infinite loop, where the descendant image points to the ancestor map,
// yet the descendant image is being returned here as an ancestor.
if (Traversal<HTMLMapElement>::FirstAncestor(*image))
return nullptr;
// The image has an associated <map> and does not have a <map> ancestor.
return map_element;
}
// static
AXObject* AXObject::ComputeNonARIAParent(AXObjectCacheImpl& cache,
Node* current_node,
LayoutObject* current_layout_obj) {
DCHECK(current_node || current_layout_obj)
<< "Can't compute parent without a backing Node "
"or LayoutObject.";
// If no node, use the layout parent.
if (!current_node) {
// If no DOM node, this is an anonymous layout object.
DCHECK(current_layout_obj->IsAnonymous());
// In accessibility, this only occurs for descendants of pseudo elements.
DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant(
*current_layout_obj))
<< "Attempt to get AX parent for irrelevant anonymous layout object: "
<< current_layout_obj;
LayoutObject* parent_layout_obj = current_layout_obj->Parent();
if (!parent_layout_obj)
return nullptr;
Node* parent_node = parent_layout_obj->GetNode();
if (!CanComputeAsNaturalParent(parent_node))
return nullptr;
if (AXObject* ax_parent = cache.GetOrCreate(parent_layout_obj)) {
DCHECK(!ax_parent->IsDetached());
DCHECK(ax_parent->ShouldUseLayoutObjectTraversalForChildren())
<< "Do not compute a parent that cannot have this as a child.";
return ax_parent->CanHaveChildren() ? ax_parent : nullptr;
}
return nullptr;
}
DCHECK(current_node->isConnected())
<< "Should not call ComputeParent() with disconnected node: "
<< current_node;
// A WebArea's parent should be the page popup owner, if any, otherwise null.
if (auto* document = DynamicTo<Document>(current_node)) {
LocalFrame* frame = document->GetFrame();
DCHECK(frame);
return cache.GetOrCreate(frame->PagePopupOwner());
}
// For <option> in <select size=1>, return the popup.
if (AXObjectCacheImpl::UseAXMenuList()) {
if (auto* option = DynamicTo<HTMLOptionElement>(current_node)) {
if (AXObject* ax_select =
AXMenuListOption::ComputeParentAXMenuPopupFor(cache, option)) {
return ax_select;
}
}
}
Node* parent_node = GetParentNodeForComputeParent(current_node);
if (!parent_node) {
// This occurs when a DOM child isn't visited by LayoutTreeBuilderTraversal,
// such as an element child of a <textarea>, which only supports plain text.
return nullptr;
}
// When the flag to use AXMenuList in on, a menu list is only allowed to
// parent an AXMenuListPopup, which is added as a child on creation. No other
// children are allowed, and nullptr is returned for anything else where the
// parent would be AXMenuList.
if (AXObjectCacheImpl::ShouldCreateAXMenuListFor(
parent_node->GetLayoutObject())) {
return nullptr;
}
if (!CanComputeAsNaturalParent(parent_node))
return nullptr;
if (AXObject* ax_parent = cache.GetOrCreate(parent_node)) {
DCHECK(!ax_parent->IsDetached());
// If the parent can't have children, then return null so that the caller
// knows that it is not a relevant natural parent, as it is a leaf.
return ax_parent->CanHaveChildren() ? ax_parent : nullptr;
}
// Could not create AXObject for |parent_node|, therefore there is no relevant
// natural parent. For example, the AXObject that would have been created
// would have been a descendant of a leaf, or otherwise an illegal child of a
// specialized object.
return nullptr;
}
#if DCHECK_IS_ON()
void AXObject::EnsureCorrectParentComputation() {
if (!parent_)
return;
DCHECK(!parent_->IsDetached());
DCHECK(parent_->CanHaveChildren());
// Don't check the computed parent if the cached parent is a mock object.
// It is expected that a computed parent could never be a mock object,
// which has no backing DOM node or layout object, and therefore cannot be
// found by traversing DOM/layout ancestors.
if (parent_->IsMockObject())
return;
// Cannot compute a parent for an object that has no backing node or layout
// object to start from.
if (!GetNode() || !GetLayoutObject())
return;
// Don't check the computed parent if the cached parent is an image:
// <area> children's location in the DOM and HTML hierarchy does not match.
// TODO(aleventhal) Try to remove this rule, it may be unnecessary now.
if (parent_->RoleValue() == ax::mojom::blink::Role::kImage)
return;
// TODO(aleventhal) Different in test fast/css/first-letter-removed-added.html
// when run with --force-renderer-accessibility.
if (GetNode() && GetNode()->IsPseudoElement())
return;
// Verify that the algorithm in ComputeParent() provides same results as
// parents that init their children with themselves as the parent.
// Inconsistency indicates a problem could potentially exist where a child's
// parent does not include the child in its children.
#if DCHECK_IS_ON()
AXObject* computed_parent = ComputeParent();
DCHECK(computed_parent) << "Computed parent was null for " << this
<< ", expected " << parent_;
DCHECK_EQ(computed_parent, parent_)
<< "\n**** ComputeParent should have provided the same result as "
"the known parent.\n**** Computed parent layout object was "
<< computed_parent->GetLayoutObject()
<< "\n**** Actual parent's layout object was "
<< parent_->GetLayoutObject() << "\n**** Child was " << this;
#endif
}
#endif
const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute(
AOMStringProperty property) const {
Element* element = GetElement();
if (!element)
return g_null_atom;
return AccessibleNode::GetPropertyOrARIAAttribute(element, property);
}
Element* AXObject::GetAOMPropertyOrARIAAttribute(
AOMRelationProperty property) const {
Element* element = GetElement();
if (!element)
return nullptr;
return AccessibleNode::GetPropertyOrARIAAttribute(element, property);
}
bool AXObject::HasAOMProperty(AOMRelationListProperty property,
HeapVector<Member<Element>>& result) const {
Element* element = GetElement();
if (!element)
return false;
return AccessibleNode::GetProperty(element, property, result);
}
bool AXObject::HasAOMPropertyOrARIAAttribute(
AOMRelationListProperty property,
HeapVector<Member<Element>>& result) const {
Element* element = GetElement();
if (!element)
return false;
return AccessibleNode::GetPropertyOrARIAAttribute(element, property, result);
}
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMBooleanProperty property,
bool& result) const {
Element* element = GetElement();
if (!element)
return false;
bool is_null = true;
result =
AccessibleNode::GetPropertyOrARIAAttribute(element, property, is_null);
return !is_null;
}
bool AXObject::AOMPropertyOrARIAAttributeIsTrue(
AOMBooleanProperty property) const {
bool result;
if (HasAOMPropertyOrARIAAttribute(property, result))
return result;
return false;
}
bool AXObject::AOMPropertyOrARIAAttributeIsFalse(
AOMBooleanProperty property) const {
bool result;
if (HasAOMPropertyOrARIAAttribute(property, result))
return !result;
return false;
}
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMUIntProperty property,
uint32_t& result) const {
Element* element = GetElement();
if (!element)
return false;
bool is_null = true;
result =
AccessibleNode::GetPropertyOrARIAAttribute(element, property, is_null);
return !is_null;
}
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMIntProperty property,
int32_t& result) const {
Element* element = GetElement();
if (!element)
return false;
bool is_null = true;
result =
AccessibleNode::GetPropertyOrARIAAttribute(element, property, is_null);
return !is_null;
}
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMFloatProperty property,
float& result) const {
Element* element = GetElement();
if (!element)
return false;
bool is_null = true;
result =
AccessibleNode::GetPropertyOrARIAAttribute(element, property, is_null);
return !is_null;
}
bool AXObject::HasAOMPropertyOrARIAAttribute(AOMStringProperty property,
AtomicString& result) const {
Element* element = GetElement();
if (!element)
return false;
result = AccessibleNode::GetPropertyOrARIAAttribute(element, property);
return !result.IsNull();
}
AccessibleNode* AXObject::GetAccessibleNode() const {
Element* element = GetElement();
if (!element)
return nullptr;
return element->ExistingAccessibleNode();
}
void AXObject::Serialize(ui::AXNodeData* node_data,
ui::AXMode accessibility_mode) {
node_data->role = RoleValue();
node_data->id = AXObjectID();
DCHECK(!IsDetached()) << "Do not serialize detached nodes: "
<< ToString(true, true);
DCHECK(AccessibilityIsIncludedInTree())
<< "Do not serialize unincluded nodes: " << ToString(true, true);
// Serialize a few things that we need even for ignored nodes.
bool is_focusable = CanSetFocusAttribute();
if (is_focusable)
node_data->AddState(ax::mojom::blink::State::kFocusable);
bool is_visible = IsVisible();
if (!is_visible)
node_data->AddState(ax::mojom::blink::State::kInvisible);
if (is_visible || is_focusable) {
// If the author applied the ARIA "textbox" role on something that is not
// (currently) editable, this may be read-only rich-text object. Or it
// might just be bad authoring. Either way, we want to expose its
// descendants, especially the interactive ones which might gain focus.
bool is_non_atomic_textfield_root = IsARIATextField();
// Preserve continuity in subtrees of richly editable content by including
// richlyEditable state even if ignored.
if (IsEditable()) {
node_data->AddState(ax::mojom::blink::State::kEditable);
if (!is_non_atomic_textfield_root)
is_non_atomic_textfield_root = IsEditableRoot();
if (IsRichlyEditable())
node_data->AddState(ax::mojom::blink::State::kRichlyEditable);
}
if (is_non_atomic_textfield_root) {
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kNonAtomicTextFieldRoot, true);
}
}
if (accessibility_mode.has_mode(ui::AXMode::kHTML))
SerializeHTMLTagAndClass(node_data); // Used for test readability.
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader))
SerializeColorAttributes(node_data); // Blends using all nodes' values.
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader) ||
accessibility_mode.has_mode(ui::AXMode::kPDF)) {
SerializeLangAttribute(node_data); // Propagates using all nodes' values.
}
if (AccessibilityIsIgnored()) {
node_data->AddState(ax::mojom::blink::State::kIgnored);
// Early return for ignored, unfocusable nodes, avoiding unnecessary work.
if (!is_focusable)
return;
}
SerializeUnignoredAttributes(node_data, accessibility_mode);
}
// Attributes that don't need to be serialized on ignored nodes.
void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data,
ui::AXMode accessibility_mode) {
AccessibilityExpanded expanded = IsExpanded();
if (expanded) {
if (expanded == kExpandedCollapsed)
node_data->AddState(ax::mojom::blink::State::kCollapsed);
else if (expanded == kExpandedExpanded)
node_data->AddState(ax::mojom::blink::State::kExpanded);
}
if (HasPopup() != ax::mojom::blink::HasPopup::kFalse)
node_data->SetHasPopup(HasPopup());
else if (RoleValue() == ax::mojom::blink::Role::kPopUpButton)
node_data->SetHasPopup(ax::mojom::blink::HasPopup::kMenu);
if (IsAutofillAvailable())
node_data->AddState(ax::mojom::blink::State::kAutofillAvailable);
if (IsDefault())
node_data->AddState(ax::mojom::blink::State::kDefault);
// aria-grabbed is deprecated in WAI-ARIA 1.1.
if (IsGrabbed() != kGrabbedStateUndefined) {
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kGrabbed,
IsGrabbed() == kGrabbedStateTrue);
}
if (IsHovered())
node_data->AddState(ax::mojom::blink::State::kHovered);
if (IsLinked())
node_data->AddState(ax::mojom::blink::State::kLinked);
if (IsMultiline())
node_data->AddState(ax::mojom::blink::State::kMultiline);
if (IsMultiSelectable())
node_data->AddState(ax::mojom::blink::State::kMultiselectable);
if (IsPasswordField())
node_data->AddState(ax::mojom::blink::State::kProtected);
if (IsRequired())
node_data->AddState(ax::mojom::blink::State::kRequired);
if (IsSelected() != blink::kSelectedStateUndefined) {
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kSelected,
IsSelected() == blink::kSelectedStateTrue);
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kSelectedFromFocus,
IsSelectedFromFocus());
}
if (IsNotUserSelectable()) {
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kNotUserSelectableStyle, true);
}
if (IsVisited())
node_data->AddState(ax::mojom::blink::State::kVisited);
if (Orientation() == kAccessibilityOrientationVertical)
node_data->AddState(ax::mojom::blink::State::kVertical);
else if (Orientation() == blink::kAccessibilityOrientationHorizontal)
node_data->AddState(ax::mojom::blink::State::kHorizontal);
if (GetTextAlign() != ax::mojom::blink::TextAlign::kNone) {
node_data->SetTextAlign(GetTextAlign());
}
if (GetTextIndent() != 0.0f) {
node_data->AddFloatAttribute(ax::mojom::blink::FloatAttribute::kTextIndent,
GetTextIndent());
}
// If this is an HTMLFrameOwnerElement (such as an iframe), we may need
// to embed the ID of the child frame.
if (auto* html_frame_owner_element =
DynamicTo<HTMLFrameOwnerElement>(GetElement())) {
if (Frame* child_frame = html_frame_owner_element->ContentFrame()) {
absl::optional<base::UnguessableToken> child_token =
child_frame->GetEmbeddingToken();
if (child_token && !(IsDetached() || ChildCountIncludingIgnored())) {
ui::AXTreeID child_tree_id =
ui::AXTreeID::FromToken(child_token.value());
node_data->AddChildTreeId(child_tree_id);
}
}
}
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader) ||
accessibility_mode.has_mode(ui::AXMode::kPDF)) {
// The DOMNodeID from Blink. Currently only populated when using
// the accessibility tree for PDF exporting. Warning, this is totally
// unrelated to the accessibility node ID, or the ID attribute for an
// HTML element - it's an ID used to uniquely identify nodes in Blink.
int dom_node_id = GetDOMNodeId();
if (dom_node_id) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kDOMNodeId,
dom_node_id);
}
// Heading level.
if (ui::IsHeading(RoleValue()) && HeadingLevel()) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kHierarchicalLevel, HeadingLevel());
}
SerializeListAttributes(node_data);
SerializeTableAttributes(node_data);
}
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader)) {
// Whether it has ARIA attributes at all.
if (HasAriaAttribute()) {
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kHasAriaAttribute, true);
}
}
if (accessibility_mode.has_mode(ui::AXMode::kPDF)) {
// Return early. None of the following attributes are needed for PDFs.
return;
}
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kValue,
SlowGetValueForControlIncludingContentEditable().Utf8());
switch (Restriction()) {
case AXRestriction::kRestrictionReadOnly:
node_data->SetRestriction(ax::mojom::blink::Restriction::kReadOnly);
break;
case AXRestriction::kRestrictionDisabled:
node_data->SetRestriction(ax::mojom::blink::Restriction::kDisabled);
break;
case AXRestriction::kRestrictionNone:
if (CanSetValueAttribute())
node_data->AddAction(ax::mojom::blink::Action::kSetValue);
break;
}
if (!Url().IsEmpty()) {
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kUrl,
Url().GetString().Utf8());
}
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader)) {
SerializeMarkerAttributes(node_data);
SerializeStyleAttributes(node_data);
}
SerializeSparseAttributes(node_data);
if (Element* element = GetElement()) {
if (IsAtomicTextField()) {
// Selection offsets are only used for plain text controls, (input of a
// text field type, and textarea). Rich editable areas, such as
// contenteditables, use AXTreeData.
//
// TODO(nektar): Remove kTextSelStart and kTextSelEnd from the renderer.
const auto ax_selection =
AXSelection::FromCurrentSelection(ToTextControl(*element));
int start = ax_selection.Base().IsTextPosition()
? ax_selection.Base().TextOffset()
: ax_selection.Base().ChildIndex();
int end = ax_selection.Extent().IsTextPosition()
? ax_selection.Extent().TextOffset()
: ax_selection.Extent().ChildIndex();
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextSelStart,
start);
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextSelEnd,
end);
}
}
if (IsScrollableContainer())
SerializeScrollAttributes(node_data);
SerializeChooserPopupAttributes(node_data);
if (GetElement()) {
SerializeElementAttributes(node_data);
if (accessibility_mode.has_mode(ui::AXMode::kHTML)) {
SerializeHTMLAttributes(node_data);
}
}
}
void AXObject::SerializeTableAttributes(ui::AXNodeData* node_data) {
if (ui::IsTableLike(RoleValue())) {
int aria_colcount = AriaColumnCount();
if (aria_colcount) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kAriaColumnCount, aria_colcount);
}
int aria_rowcount = AriaRowCount();
if (aria_rowcount) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kAriaRowCount,
aria_rowcount);
}
}
if (ui::IsTableRow(RoleValue())) {
AXObject* header = HeaderObject();
if (header && !header->IsDetached()) {
// TODO(accessibility): these should be computed by ui::AXTableInfo and
// removed here.
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTableRowHeaderId,
header->AXObjectID());
}
}
if (ui::IsCellOrTableHeader(RoleValue())) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTableCellColumnSpan, ColumnSpan());
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTableCellRowSpan, RowSpan());
}
if (ui::IsCellOrTableHeader(RoleValue()) || ui::IsTableRow(RoleValue())) {
// aria-rowindex and aria-colindex are supported on cells, headers and
// rows.
int aria_rowindex = AriaRowIndex();
if (aria_rowindex) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kAriaCellRowIndex, aria_rowindex);
}
int aria_colindex = AriaColumnIndex();
if (aria_colindex) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kAriaCellColumnIndex, aria_colindex);
}
}
if (ui::IsTableHeader(RoleValue()) &&
GetSortDirection() != ax::mojom::blink::SortDirection::kNone) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kSortDirection,
static_cast<int32_t>(GetSortDirection()));
}
}
void AXObject::SerializeScrollAttributes(ui::AXNodeData* node_data) {
// Only mark as scrollable if user has actual scrollbars to use.
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kScrollable,
IsUserScrollable());
// Provide x,y scroll info if scrollable in any way (programmatically or via
// user).
const gfx::Point& scroll_offset = GetScrollOffset();
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollX,
scroll_offset.x());
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollY,
scroll_offset.y());
const gfx::Point& min_scroll_offset = MinimumScrollOffset();
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollXMin,
min_scroll_offset.x());
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollYMin,
min_scroll_offset.y());
const gfx::Point& max_scroll_offset = MaximumScrollOffset();
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollXMax,
max_scroll_offset.x());
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kScrollYMax,
max_scroll_offset.y());
}
const AtomicString& AXObject::GetRoleAttributeStringForObjectAttribute() {
// All ARIA roles are exposed in xml-roles.
if (const AtomicString& role_str =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole)) {
return role_str;
}
// Landmarks are the only native roles exposed in xml-roles, matching Firefox.
if (ui::IsLandmark(RoleValue()))
return ARIARoleName(RoleValue());
return g_null_atom;
}
void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) {
Element* element = GetElement();
if (!element)
return;
if (const AtomicString& class_name = element->GetClassAttribute()) {
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kClassName,
class_name.Utf8());
}
// Expose StringAttribute::kRole, which is used for the xml-roles object
// attribute. Prefer the raw ARIA role attribute value, otherwise, the ARIA
// equivalent role is used, if it is a role that is exposed in xml-roles.
const AtomicString& role_str = GetRoleAttributeStringForObjectAttribute();
if (role_str) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kRole, role_str.Utf8());
}
}
void AXObject::SerializeHTMLTagAndClass(ui::AXNodeData* node_data) {
Element* element = GetElement();
if (!element) {
if (ui::IsPlatformDocument(RoleValue())) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kHtmlTag, "#document");
}
return;
}
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kHtmlTag,
element->tagName().LowerASCII().Utf8());
if (const AtomicString& class_name = element->GetClassAttribute()) {
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kClassName,
class_name.Utf8());
}
}
void AXObject::SerializeHTMLAttributes(ui::AXNodeData* node_data) {
Element* element = GetElement();
DCHECK(element);
for (const Attribute& attr : element->Attributes()) {
std::string name = attr.LocalName().LowerASCII().Utf8();
if (name == "class") { // class already in kClassName
continue;
}
std::string value = attr.Value().Utf8();
node_data->html_attributes.push_back(std::make_pair(name, value));
}
// TODO(nektar): Turn off kHTMLAccessibilityMode for automation and Mac
// and remove ifdef.
#if defined(OS_WIN) || BUILDFLAG(IS_CHROMEOS_ASH)
if (node_data->role == ax::mojom::blink::Role::kMath &&
element->innerHTML().length()) {
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kInnerHtml,
element->innerHTML().Utf8());
}
#endif
}
void AXObject::SerializeColorAttributes(ui::AXNodeData* node_data) {
// Text attributes.
if (RGBA32 bg_color = BackgroundColor()) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kBackgroundColor,
bg_color);
}
if (RGBA32 color = GetColor())
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kColor, color);
}
void AXObject::SerializeStyleAttributes(ui::AXNodeData* node_data) {
// Only serialize font family if there is one, and it is different from the
// parent. Use the value from computed style first since that is a fast lookup
// and comparison, and serialize the user-friendly name at points in the tree
// where the font family changes between parent/child.
const AtomicString& computed_family = ComputedFontFamily();
if (computed_family.length()) {
AXObject* parent = ParentObjectUnignored();
if (!parent || parent->ComputedFontFamily() != computed_family) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kFontFamily,
FontFamilyForSerialization().Utf8());
}
}
// Font size is in pixels.
if (FontSize()) {
node_data->AddFloatAttribute(ax::mojom::blink::FloatAttribute::kFontSize,
FontSize());
}
if (FontWeight()) {
node_data->AddFloatAttribute(ax::mojom::blink::FloatAttribute::kFontWeight,
FontWeight());
}
if (RoleValue() == ax::mojom::blink::Role::kListItem &&
GetListStyle() != ax::mojom::blink::ListStyle::kNone) {
node_data->SetListStyle(GetListStyle());
}
if (GetTextDirection() != ax::mojom::blink::WritingDirection::kNone) {
node_data->SetTextDirection(GetTextDirection());
}
if (GetTextPosition() != ax::mojom::blink::TextPosition::kNone) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextPosition,
static_cast<int32_t>(GetTextPosition()));
}
int32_t text_style = 0;
ax::mojom::blink::TextDecorationStyle text_overline_style;
ax::mojom::blink::TextDecorationStyle text_strikethrough_style;
ax::mojom::blink::TextDecorationStyle text_underline_style;
GetTextStyleAndTextDecorationStyle(&text_style, &text_overline_style,
&text_strikethrough_style,
&text_underline_style);
if (text_style) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextStyle,
text_style);
}
if (text_overline_style != ax::mojom::blink::TextDecorationStyle::kNone) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTextOverlineStyle,
static_cast<int32_t>(text_overline_style));
}
if (text_strikethrough_style !=
ax::mojom::blink::TextDecorationStyle::kNone) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTextStrikethroughStyle,
static_cast<int32_t>(text_strikethrough_style));
}
if (text_underline_style != ax::mojom::blink::TextDecorationStyle::kNone) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kTextUnderlineStyle,
static_cast<int32_t>(text_underline_style));
}
}
void AXObject::SerializeSparseAttributes(ui::AXNodeData* node_data) {
if (IsVirtualObject()) {
AccessibleNode* accessible_node = GetAccessibleNode();
if (accessible_node) {
AXNodeDataAOMPropertyClient property_client(*ax_object_cache_,
*node_data);
accessible_node->GetAllAOMProperties(&property_client);
}
}
Element* element = GetElement();
if (!element)
return;
AXSparseAttributeSetterMap& setter_map = GetAXSparseAttributeSetterMap();
AttributeCollection attributes = element->AttributesWithoutUpdate();
HashSet<QualifiedName> set_attributes;
for (const Attribute& attr : attributes) {
set_attributes.insert(attr.GetName());
AXSparseSetterFunc callback;
auto it = setter_map.find(attr.GetName());
if (it == setter_map.end())
continue;
it->value.Run(this, node_data, attr.Value());
}
if (!element->DidAttachInternals())
return;
const auto& internals_attributes =
element->EnsureElementInternals().GetAttributes();
for (const QualifiedName& attr : internals_attributes.Keys()) {
auto it = setter_map.find(attr);
if (set_attributes.Contains(attr) || it == setter_map.end())
continue;
it->value.Run(this, node_data, internals_attributes.at(attr));
}
}
void AXObject::SerializeLangAttribute(ui::AXNodeData* node_data) {
AXObject* parent = ParentObject();
if (Language().length()) {
// TODO(chrishall): should we still trim redundant languages off here?
if (!parent || parent->Language() != Language()) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kLanguage,
Language().Utf8());
}
}
}
void AXObject::SerializeListAttributes(ui::AXNodeData* node_data) {
if (SetSize()) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kSetSize,
SetSize());
}
if (PosInSet()) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kPosInSet,
PosInSet());
}
}
void AXObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const {
// Implemented in subclasses.
}
void AXObject::SerializeChooserPopupAttributes(ui::AXNodeData* node_data) {
AXObject* chooser_popup = ChooserPopup();
if (!chooser_popup)
return;
int32_t chooser_popup_id = chooser_popup->AXObjectID();
auto controls_ids = node_data->GetIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds);
controls_ids.push_back(chooser_popup_id);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds, controls_ids);
}
void AXObject::TruncateAndAddStringAttribute(
ui::AXNodeData* dst,
ax::mojom::blink::StringAttribute attribute,
const std::string& value,
uint32_t max_len) const {
if (value.size() > max_len) {
std::string truncated;
base::TruncateUTF8ToByteSize(value, max_len, &truncated);
dst->AddStringAttribute(attribute, truncated);
} else {
dst->AddStringAttribute(attribute, value);
}
}
bool AXObject::IsAXNodeObject() const {
return false;
}
bool AXObject::IsAXLayoutObject() const {
return false;
}
bool AXObject::IsAXInlineTextBox() const {
return false;
}
bool AXObject::IsList() const {
return ui::IsList(RoleValue());
}
bool AXObject::IsAXListBox() const {
return false;
}
bool AXObject::IsAXListBoxOption() const {
return false;
}
bool AXObject::IsMenuList() const {
return false;
}
bool AXObject::IsMenuListOption() const {
return false;
}
bool AXObject::IsMenuListPopup() const {
return false;
}
bool AXObject::IsMockObject() const {
return false;
}
bool AXObject::IsProgressIndicator() const {
return false;
}
bool AXObject::IsAXRadioInput() const {
return false;
}
bool AXObject::IsSlider() const {
return false;
}
bool AXObject::IsValidationMessage() const {
return false;
}
bool AXObject::IsVirtualObject() const {
return false;
}
ax::mojom::blink::Role AXObject::RoleValue() const {
return role_;
}
bool AXObject::IsARIATextField() const {
if (IsAtomicTextField())
return false; // Native role supercedes the ARIA one.
return AriaRoleAttribute() == ax::mojom::blink::Role::kTextField ||
AriaRoleAttribute() == ax::mojom::blink::Role::kSearchBox ||
AriaRoleAttribute() == ax::mojom::blink::Role::kTextFieldWithComboBox;
}
bool AXObject::IsButton() const {
return ui::IsButton(RoleValue());
}
bool AXObject::IsCanvas() const {
return RoleValue() == ax::mojom::blink::Role::kCanvas;
}
bool AXObject::IsColorWell() const {
return RoleValue() == ax::mojom::blink::Role::kColorWell;
}
bool AXObject::IsControl() const {
return ui::IsControl(RoleValue());
}
bool AXObject::IsDefault() const {
return false;
}
bool AXObject::IsFieldset() const {
return false;
}
bool AXObject::IsHeading() const {
return ui::IsHeading(RoleValue());
}
bool AXObject::IsImage() const {
// Canvas is not currently included so that it is not exposed unless there is
// a label, fallback content or something to make it accessible. This decision
// may be revisited at a later date.
return ui::IsImage(RoleValue()) &&
RoleValue() != ax::mojom::blink::Role::kCanvas;
}
bool AXObject::IsInputImage() const {
return false;
}
bool AXObject::IsLink() const {
return ui::IsLink(RoleValue());
}
bool AXObject::IsImageMapLink() const {
return false;
}
bool AXObject::IsMenu() const {
return RoleValue() == ax::mojom::blink::Role::kMenu;
}
bool AXObject::IsCheckable() const {
switch (RoleValue()) {
case ax::mojom::blink::Role::kCheckBox:
case ax::mojom::blink::Role::kMenuItemCheckBox:
case ax::mojom::blink::Role::kMenuItemRadio:
case ax::mojom::blink::Role::kRadioButton:
case ax::mojom::blink::Role::kSwitch:
case ax::mojom::blink::Role::kToggleButton:
return true;
case ax::mojom::blink::Role::kTreeItem:
case ax::mojom::blink::Role::kListBoxOption:
case ax::mojom::blink::Role::kMenuListOption:
return AriaCheckedIsPresent();
default:
return false;
}
}
// Why this is here instead of AXNodeObject:
// Because an AXMenuListOption (<option>) can
// have an ARIA role of menuitemcheckbox/menuitemradio
// yet does not inherit from AXNodeObject
ax::mojom::blink::CheckedState AXObject::CheckedState() const {
if (!IsCheckable())
return ax::mojom::blink::CheckedState::kNone;
// Try ARIA checked/pressed state
const ax::mojom::blink::Role role = RoleValue();
const auto prop = role == ax::mojom::blink::Role::kToggleButton
? AOMStringProperty::kPressed
: AOMStringProperty::kChecked;
const AtomicString& checked_attribute = GetAOMPropertyOrARIAAttribute(prop);
if (checked_attribute) {
if (EqualIgnoringASCIICase(checked_attribute, "mixed")) {
// Only checkable role that doesn't support mixed is the switch.
if (role != ax::mojom::blink::Role::kSwitch)
return ax::mojom::blink::CheckedState::kMixed;
}
// Anything other than "false" should be treated as "true".
return EqualIgnoringASCIICase(checked_attribute, "false")
? ax::mojom::blink::CheckedState::kFalse
: ax::mojom::blink::CheckedState::kTrue;
}
// Native checked state
if (role != ax::mojom::blink::Role::kToggleButton) {
const Node* node = GetNode();
if (!node)
return ax::mojom::blink::CheckedState::kNone;
// Expose native checkbox mixed state as accessibility mixed state. However,
// do not expose native radio mixed state as accessibility mixed state.
// This would confuse the JAWS screen reader, which reports a mixed radio as
// both checked and partially checked, but a native mixed native radio
// button sinply means no radio buttons have been checked in the group yet.
if (IsNativeCheckboxInMixedState(node))
return ax::mojom::blink::CheckedState::kMixed;
auto* html_input_element = DynamicTo<HTMLInputElement>(node);
if (html_input_element && html_input_element->ShouldAppearChecked()) {
return ax::mojom::blink::CheckedState::kTrue;
}
}
return ax::mojom::blink::CheckedState::kFalse;
}
String AXObject::GetValueForControl() const {
return String();
}
String AXObject::SlowGetValueForControlIncludingContentEditable() const {
return String();
}
bool AXObject::IsNativeCheckboxInMixedState(const Node* node) {
const auto* input = DynamicTo<HTMLInputElement>(node);
if (!input)
return false;
const auto inputType = input->type();
if (inputType != input_type_names::kCheckbox)
return false;
return input->ShouldAppearIndeterminate();
}
bool AXObject::IsMenuRelated() const {
return ui::IsMenuRelated(RoleValue());
}
bool AXObject::IsMeter() const {
return RoleValue() == ax::mojom::blink::Role::kMeter;
}
bool AXObject::IsNativeImage() const {
return false;
}
bool AXObject::IsNativeSpinButton() const {
return false;
}
bool AXObject::IsAtomicTextField() const {
return blink::IsTextControl(GetNode());
}
bool AXObject::IsNonAtomicTextField() const {
// Consivably, an <input type=text> or a <textarea> might also have the
// contenteditable attribute applied. In such cases, the <input> or <textarea>
// tags should supercede.
if (IsAtomicTextField())
return false;
return HasContentEditableAttributeSet() || IsARIATextField();
}
bool AXObject::IsPasswordField() const {
auto* input_element = DynamicTo<HTMLInputElement>(GetNode());
return input_element && input_element->type() == input_type_names::kPassword;
}
bool AXObject::IsPasswordFieldAndShouldHideValue() const {
if (!IsPasswordField())
return false;
const Settings* settings = GetDocument()->GetSettings();
return settings && !settings->GetAccessibilityPasswordValuesEnabled();
}
bool AXObject::IsPresentational() const {
return ui::IsPresentational(RoleValue());
}
bool AXObject::IsTextObject() const {
// Objects with |ax::mojom::blink::Role::kLineBreak| are HTML <br> elements
// and are not backed by DOM text nodes. We can't mark them as text objects
// for that reason.
switch (RoleValue()) {
case ax::mojom::blink::Role::kInlineTextBox:
case ax::mojom::blink::Role::kStaticText:
return true;
default:
return false;
}
}
bool AXObject::IsRangeValueSupported() const {
if (RoleValue() == ax::mojom::blink::Role::kSplitter) {
// According to the ARIA spec, role="separator" acts as a splitter only
// when focusable, and supports a range only in that case.
return CanSetFocusAttribute();
}
return ui::IsRangeValueSupported(RoleValue());
}
bool AXObject::IsScrollbar() const {
return RoleValue() == ax::mojom::blink::Role::kScrollBar;
}
bool AXObject::IsNativeSlider() const {
return false;
}
bool AXObject::IsSpinButton() const {
return RoleValue() == ax::mojom::blink::Role::kSpinButton;
}
bool AXObject::IsTabItem() const {
return RoleValue() == ax::mojom::blink::Role::kTab;
}
bool AXObject::IsTextField() const {
if (IsDetached())
return false;
return IsAtomicTextField() || IsNonAtomicTextField();
}
bool AXObject::IsAutofillAvailable() const {
return false;
}
bool AXObject::IsClickable() const {
return ui::IsClickable(RoleValue());
}
AccessibilityExpanded AXObject::IsExpanded() const {
return kExpandedUndefined;
}
bool AXObject::IsFocused() const {
return false;
}
AccessibilityGrabbedState AXObject::IsGrabbed() const {
return kGrabbedStateUndefined;
}
bool AXObject::IsHovered() const {
return false;
}
bool AXObject::IsLineBreakingObject() const {
// Not all AXObjects have an associated node or layout object. They could be
// virtual accessibility nodes, for example.
//
// We assume that most images on the Web are inline.
return !IsImage() && ui::IsStructure(RoleValue());
}
bool AXObject::IsLinked() const {
return false;
}
bool AXObject::IsLoaded() const {
return false;
}
bool AXObject::IsMultiSelectable() const {
return false;
}
bool AXObject::IsOffScreen() const {
return false;
}
bool AXObject::IsRequired() const {
return false;
}
AccessibilitySelectedState AXObject::IsSelected() const {
return kSelectedStateUndefined;
}
bool AXObject::IsSelectedFromFocusSupported() const {
return false;
}
bool AXObject::IsSelectedFromFocus() const {
return false;
}
bool AXObject::IsSelectedOptionActive() const {
return false;
}
bool AXObject::IsNotUserSelectable() const {
return false;
}
bool AXObject::IsVisited() const {
return false;
}
bool AXObject::AccessibilityIsIgnored() const {
UpdateCachedAttributeValuesIfNeeded();
return cached_is_ignored_;
}
bool AXObject::AccessibilityIsIgnoredButIncludedInTree() const {
UpdateCachedAttributeValuesIfNeeded();
return cached_is_ignored_but_included_in_tree_;
}
// AccessibilityIsIncludedInTree should be true for all nodes that should be
// included in the tree, even if they are ignored
bool AXObject::AccessibilityIsIncludedInTree() const {
return !AccessibilityIsIgnored() || AccessibilityIsIgnoredButIncludedInTree();
}
void AXObject::UpdateCachedAttributeValuesIfNeeded(
bool notify_parent_of_ignored_changes) const {
if (IsDetached()) {
cached_is_ignored_ = true;
cached_is_ignored_but_included_in_tree_ = false;
return;
}
AXObjectCacheImpl& cache = AXObjectCache();
if (cache.ModificationCount() == last_modification_count_)
return;
last_modification_count_ = cache.ModificationCount();
#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString()
DCHECK(!is_updating_cached_values_)
<< "Reentering UpdateCachedAttributeValuesIfNeeded() on same node: "
<< GetNode();
base::AutoReset<bool> reentrancy_protector(&is_updating_cached_values_, true);
DCHECK(!GetDocument() || GetDocument()->Lifecycle().GetState() >=
DocumentLifecycle::kAfterPerformLayout)
<< "Unclean document at lifecycle "
<< GetDocument()->Lifecycle().ToString();
#endif // DCHECK_IS_ON()
if (IsMissingParent())
RepairMissingParent();
cached_is_hidden_via_style = ComputeIsHiddenViaStyle();
// Decisions in what subtree descendants are included (each descendant's
// cached children_) depends on the ARIA hidden state. When it changes,
// the entire subtree needs to recompute descendants.
// In addition, the below computations for is_ignored_but_included_in_tree is
// dependent on having the correct new cached value.
bool is_inert_or_aria_hidden = ComputeIsInertOrAriaHidden();
if (cached_is_inert_or_aria_hidden_ != is_inert_or_aria_hidden) {
// Update children if not already dirty (e.g. during Init() time.
SetNeedsToUpdateChildren();
cached_is_inert_or_aria_hidden_ = is_inert_or_aria_hidden;
}
cached_is_descendant_of_disabled_node_ = ComputeIsDescendantOfDisabledNode();
bool is_ignored = ComputeAccessibilityIsIgnored();
bool is_ignored_but_included_in_tree =
is_ignored && ComputeAccessibilityIsIgnoredButIncludedInTree();
bool included_in_tree_changed = false;
// If the child's "included in tree" state changes, we will be notifying the
// parent to recompute it's children.
// Exceptions:
// - Caller passes in |notify_parent_of_ignored_changes = false| -- this
// occurs when this is a new child, or when a parent is in the middle of
// adding this child, and doing this would be redundant.
// - Inline text boxes: their "included in tree" state is entirely dependent
// on their static text parent.
if (notify_parent_of_ignored_changes &&
RoleValue() != ax::mojom::blink::Role::kInlineTextBox) {
bool is_included_in_tree = !is_ignored || is_ignored_but_included_in_tree;
if (is_included_in_tree != LastKnownIsIncludedInTreeValue())
included_in_tree_changed = true;
}
// Presence of inline text children depends on ignored state.
if (is_ignored != LastKnownIsIgnoredValue() &&
ui::CanHaveInlineTextBoxChildren(RoleValue())) {
// Update children if not already dirty (e.g. during Init() time.
SetNeedsToUpdateChildren();
}
cached_is_ignored_ = is_ignored;
cached_is_ignored_but_included_in_tree_ = is_ignored_but_included_in_tree;
// Compute live region root, which can be from any ARIA live value, including
// "off", or from an automatic ARIA live value, e.g. from role="status".
// TODO(dmazzoni): remove this const_cast.
AtomicString aria_live;
if (GetNode() && IsA<Document>(GetNode())) {
// The document root is never a live region root.
cached_live_region_root_ = nullptr;
} else if (RoleValue() == ax::mojom::blink::Role::kInlineTextBox) {
// Inline text boxes do not need live region properties.
cached_live_region_root_ = nullptr;
} else if (parent_) {
// Is a live region root if this or an ancestor is a live region.
cached_live_region_root_ = IsLiveRegionRoot() ? const_cast<AXObject*>(this)
: parent_->LiveRegionRoot();
}
cached_aria_column_index_ = ComputeAriaColumnIndex();
cached_aria_row_index_ = ComputeAriaRowIndex();
if (included_in_tree_changed) {
if (AXObject* parent = CachedParentObject()) {
// TODO(aleventhal) Reenable DCHECK. It fails on PDF tests.
// DCHECK(!ax_object_cache_->IsFrozen())
// << "Attempting to change children on an ancestor is dangerous during "
// "serialization, because the ancestor may have already been "
// "visited. Reaching this line indicates that AXObjectCacheImpl did "
// "not handle a signal and call ChilldrenChanged() earlier."
// << "\nChild: " << ToString(true)
// << "\nParent: " << parent->ToString(true);
// Defers a ChildrenChanged() on the first included ancestor.
// Must defer it, otherwise it can cause reentry into
// UpdateCachedAttributeValuesIfNeeded() on |this|.
AXObjectCache().ChildrenChangedOnAncestorOf(const_cast<AXObject*>(this));
}
}
if (GetLayoutObject() && GetLayoutObject()->IsText()) {
cached_local_bounding_box_rect_for_accessibility_ =
GetLayoutObject()->LocalBoundingBoxRectForAccessibility();
}
}
bool AXObject::AccessibilityIsIgnoredByDefault(
IgnoredReasons* ignored_reasons) const {
return DefaultObjectInclusion(ignored_reasons) == kIgnoreObject;
}
AXObjectInclusion AXObject::DefaultObjectInclusion(
IgnoredReasons* ignored_reasons) const {
if (IsInertOrAriaHidden()) {
// Keep focusable elements that are aria-hidden in tree, so that they can
// still fire events such as focus and value changes.
if (!CanSetFocusAttribute()) {
if (ignored_reasons)
ComputeIsInertOrAriaHidden(ignored_reasons);
return kIgnoreObject;
}
}
return kDefaultBehavior;
}
bool AXObject::IsInertOrAriaHidden() const {
UpdateCachedAttributeValuesIfNeeded();
return cached_is_inert_or_aria_hidden_;
}
bool AXObject::IsAriaHidden() const {
return IsInertOrAriaHidden() && AriaHiddenRoot();
}
bool AXObject::ComputeIsInertOrAriaHidden(
IgnoredReasons* ignored_reasons) const {
if (GetNode()) {
if (GetNode()->IsInert()) {
if (ignored_reasons) {
HTMLDialogElement* dialog = GetActiveDialogElement(GetNode());
if (dialog) {
AXObject* dialog_object = AXObjectCache().GetOrCreate(dialog);
if (dialog_object) {
ignored_reasons->push_back(
IgnoredReason(kAXActiveModalDialog, dialog_object));
} else {
ignored_reasons->push_back(IgnoredReason(kAXInertElement));
}
} else {
const AXObject* inert_root_el = InertRoot();
if (inert_root_el == this) {
ignored_reasons->push_back(IgnoredReason(kAXInertElement));
} else {
ignored_reasons->push_back(
IgnoredReason(kAXInertSubtree, inert_root_el));
}
}
}
return true;
} else if (IsBlockedByAriaModalDialog(ignored_reasons)) {
return true;
}
} else {
AXObject* parent = ParentObject();
if (parent && parent->IsInertOrAriaHidden()) {
if (ignored_reasons)
parent->ComputeIsInertOrAriaHidden(ignored_reasons);
return true;
}
}
const AXObject* hidden_root = AriaHiddenRoot();
if (hidden_root) {
if (ignored_reasons) {
if (hidden_root == this) {
ignored_reasons->push_back(IgnoredReason(kAXAriaHiddenElement));
} else {
ignored_reasons->push_back(
IgnoredReason(kAXAriaHiddenSubtree, hidden_root));
}
}
return true;
}
return false;
}
bool AXObject::IsModal() const {
if (RoleValue() != ax::mojom::blink::Role::kDialog &&
RoleValue() != ax::mojom::blink::Role::kAlertDialog)
return false;
bool modal = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kModal, modal)) {
return modal;
}
if (GetNode() && IsA<HTMLDialogElement>(*GetNode()))
return To<Element>(GetNode())->IsInTopLayer();
return false;
}
bool AXObject::IsBlockedByAriaModalDialog(
IgnoredReasons* ignored_reasons) const {
AXObject* active_aria_modal_dialog =
AXObjectCache().GetActiveAriaModalDialog();
// On platforms that don't require manual pruning of the accessibility tree,
// the active aria modal dialog should never be set, so has no effect.
if (!active_aria_modal_dialog)
return false;
if (this == active_aria_modal_dialog ||
IsDescendantOf(*active_aria_modal_dialog))
return false;
if (ignored_reasons) {
ignored_reasons->push_back(
IgnoredReason(kAXAriaModalDialog, active_aria_modal_dialog));
}
return true;
}
bool AXObject::IsVisible() const {
return !IsInertOrAriaHidden() && !IsHiddenViaStyle();
}
const AXObject* AXObject::AriaHiddenRoot() const {
for (const AXObject* object = this; object; object = object->ParentObject()) {
if (object->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden))
return object;
}
return nullptr;
}
const AXObject* AXObject::InertRoot() const {
const AXObject* object = this;
if (!RuntimeEnabledFeatures::InertAttributeEnabled())
return nullptr;
while (object && !object->IsAXNodeObject())
object = object->ParentObject();
Node* node = object->GetNode();
auto* element = DynamicTo<Element>(node);
if (!element)
element = FlatTreeTraversal::ParentElement(*node);
while (element) {
if (element->FastHasAttribute(html_names::kInertAttr))
return AXObjectCache().GetOrCreate(element);
element = FlatTreeTraversal::ParentElement(*element);
}
return nullptr;
}
bool AXObject::DispatchEventToAOMEventListeners(Event& event) {
HeapVector<Member<AccessibleNode>> event_path;
for (AXObject* ancestor = this; ancestor;
ancestor = ancestor->ParentObject()) {
AccessibleNode* ancestor_accessible_node = ancestor->GetAccessibleNode();
if (!ancestor_accessible_node)
continue;
if (!ancestor_accessible_node->HasEventListeners(event.type()))
continue;
event_path.push_back(ancestor_accessible_node);
}
// Short-circuit: if there are no AccessibleNodes attached anywhere
// in the ancestry of this node, exit.
if (!event_path.size())
return false;
// Check if the user has granted permission for this domain to use
// AOM event listeners yet. This may trigger an infobar, but we shouldn't
// block, so whatever decision the user makes will apply to the next
// event received after that.
//
// Note that we only ask the user about this permission the first
// time an event is received that actually would have triggered an
// event listener. However, if the user grants this permission, it
// persists for this origin from then on.
if (!AXObjectCache().CanCallAOMEventListeners()) {
AXObjectCache().RequestAOMEventListenerPermission();
return false;
}
// Since we now know the AOM is being used in this document, get the
// AccessibleNode for the target element and create it if necessary -
// otherwise we wouldn't be able to set the event target. However note
// that if it didn't previously exist it won't be part of the event path.
AccessibleNode* target = GetAccessibleNode();
if (!target) {
if (Element* element = GetElement())
target = element->accessibleNode();
}
if (!target)
return false;
event.SetTarget(target);
// Capturing phase.
event.SetEventPhase(Event::kCapturingPhase);
for (int i = static_cast<int>(event_path.size()) - 1; i >= 0; i--) {
// Don't call capturing event listeners on the target. Note that
// the target may not necessarily be in the event path which is why
// we check here.
if (event_path[i] == target)
break;
event.SetCurrentTarget(event_path[i]);
event_path[i]->FireEventListeners(event);
if (event.PropagationStopped())
return true;
}
// Targeting phase.
event.SetEventPhase(Event::kAtTarget);
event.SetCurrentTarget(event_path[0]);
event_path[0]->FireEventListeners(event);
if (event.PropagationStopped())
return true;
// Bubbling phase.
event.SetEventPhase(Event::kBubblingPhase);
for (wtf_size_t i = 1; i < event_path.size(); i++) {
event.SetCurrentTarget(event_path[i]);
event_path[i]->FireEventListeners(event);
if (event.PropagationStopped())
return true;
}
if (event.defaultPrevented())
return true;
return false;
}
bool AXObject::IsDescendantOfDisabledNode() const {
UpdateCachedAttributeValuesIfNeeded();
return cached_is_descendant_of_disabled_node_;
}
bool AXObject::ComputeIsDescendantOfDisabledNode() const {
if (IsA<Document>(GetNode()))
return false;
bool disabled = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kDisabled, disabled))
return disabled;
if (AXObject* parent = ParentObject())
return parent->IsDescendantOfDisabledNode();
return false;
}
bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const {
if (RuntimeEnabledFeatures::AccessibilityExposeIgnoredNodesEnabled())
return true;
if (AXObjectCache().IsAriaOwned(this) || HasARIAOwns(GetElement())) {
// Always include an aria-owned object. It must be a child of the
// element with aria-owns.
return true;
}
const Node* node = GetNode();
if (!node) {
if (GetLayoutObject()) {
// All AXObjects created for anonymous layout objects are included.
// See IsLayoutObjectRelevantForAccessibility() in
// ax_object_cache_impl.cc.
// - Visible content, such as text, images and quotes (can't have
// children).
// - Any containers inside of pseudo-elements.
DCHECK(GetLayoutObject()->IsAnonymous())
<< "Object has layout object but no node and is not anonymous: "
<< GetLayoutObject();
} else {
// Include ignored mock objects, virtual objects and inline text boxes.
DCHECK(IsMockObject() || IsVirtualObject() ||
RoleValue() == ax::mojom::blink::Role::kInlineTextBox)
<< "Nodeless, layout-less object found with role " << RoleValue();
}
// By including all of these objects in the tree, it is ensured that
// ClearChildren() will be able to find these children and detach them
// from their parent.
return true;
}
// Allow the browser side ax tree to access "visibility: [hidden|collapse]"
// and "display: none" nodes. This is useful for APIs that return the node
// referenced by aria-labeledby and aria-describedby.
// An element must have an id attribute or it cannot be referenced by
// aria-labelledby or aria-describedby.
if (RuntimeEnabledFeatures::AccessibilityExposeDisplayNoneEnabled()) {
if (Element* element = GetElement()) {
if (element->FastHasAttribute(html_names::kIdAttr) &&
IsHiddenViaStyle()) {
return true;
}
}
} else if (GetLayoutObject()) {
if (GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible)
return true;
}
// Allow the browser side ax tree to access "aria-hidden" nodes.
// This is useful for APIs that return the node referenced by
// aria-labeledby and aria-describedby.
if (GetLayoutObject() && IsAriaHidden())
return true;
// Custom elements and their children are included in the tree.
// <slot>s and their children are included in the tree.
// Also children of <label> elements, for accname calculation purposes.
// This checks to see whether this is a child of one of those.
if (Node* parent_node = LayoutTreeBuilderTraversal::Parent(*node)) {
if (parent_node->IsCustomElement() || IsA<HTMLSlotElement>(parent_node))
return true;
// <span>s are ignored because they are considered uninteresting. Do not add
// them back inside labels.
if (IsA<HTMLLabelElement>(parent_node) && !IsA<HTMLSpanElement>(node))
return true;
// Simplify AXNodeObject::AddImageMapChildren() -- it will only need to deal
// with included children.
if (IsA<HTMLMapElement>(parent_node))
return true;
}
// The ignored state of media controls can change without a layout update.
// Keep them in the tree at all times so that the serializer isn't
// accidentally working with unincluded nodes, which is not allowed.
if (node->IsInUserAgentShadowRoot() &&
IsA<HTMLMediaElement>(node->OwnerShadowHost())) {
return true;
}
Element* element = GetElement();
if (!element)
return false;
// Custom elements and their children are included in the tree.
if (element->IsCustomElement())
return true;
// <slot>s and their children are included in the tree.
// Detailed explanation:
// <slot> elements are placeholders marking locations in a shadow tree where
// users of a web component can insert their own custom nodes. Inserted nodes
// (also known as distributed nodes) become children of their respective slots
// in the accessibility tree. In other words, the accessibility tree mirrors
// the flattened DOM tree or the layout tree, not the original DOM tree.
// Distributed nodes still maintain their parent relations and computed style
// information with their original location in the DOM. Therefore, we need to
// ensure that in the accessibility tree no remnant information from the
// unflattened DOM tree remains, such as the cached parent.
if (IsA<HTMLSlotElement>(element))
return true;
// Include all pseudo element content. Any anonymous subtree is included
// from above, in the condition where there is no node.
if (element->IsPseudoElement())
return true;
// Include all parents of ::before/::after/::marker pseudo elements to help
// ClearChildren() find all children, and assist naming computation.
// It is unnecessary to include a rule for other types of pseudo elements:
// Specifically, ::first-letter/::backdrop are not visited by
// LayoutTreeBuilderTraversal, and cannot be in the tree, therefore do not add
// a special rule to include their parents.
if (element->GetPseudoElement(kPseudoIdBefore) ||
element->GetPseudoElement(kPseudoIdAfter) ||
element->GetPseudoElement(kPseudoIdMarker)) {
return true;
}
// Use a flag to control whether or not the <html> element is included
// in the accessibility tree. Either way it's always marked as "ignored",
// but eventually we want to always include it in the tree to simplify
// some logic.
if (IsA<HTMLHtmlElement>(element))
return RuntimeEnabledFeatures::AccessibilityExposeHTMLElementEnabled();
// Keep the internal accessibility tree consistent for videos which lack
// a player and also inner text.
if (RoleValue() == ax::mojom::blink::Role::kVideo ||
RoleValue() == ax::mojom::blink::Role::kAudio) {
return true;
}
// Always pass through Line Breaking objects, this is necessary to
// detect paragraph edges, which are defined as hard-line breaks.
if (IsLineBreakingObject())
return true;
// Ruby annotations (i.e. <rt> elements) need to be included because they are
// used for calculating an accessible description for the ruby. We explicitly
// exclude from the tree any <rp> elements, even though they also have the
// kRubyAnnotation role, because such elements provide fallback content for
// browsers that do not support ruby. Hence, their contents should not be
// included in the accessible description, unless another condition in this
// method decides to keep them in the tree for some reason.
if (element->HasTagName(html_names::kRtTag))
return true;
// Preserve SVG grouping elements.
if (IsA<SVGGElement>(element))
return true;
// Keep table-related elements in the tree, because it's too easy for them
// to in and out of being ignored based on their ancestry, as their role
// can depend on several levels up in the hierarchy.
if (IsA<HTMLTableElement>(element) || IsA<HTMLTableSectionElement>(element) ||
IsA<HTMLTableRowElement>(element) || IsA<HTMLTableCellElement>(element)) {
return true;
}
// Ensure clean teardown of AXMenuList.
if (auto* option = DynamicTo<HTMLOptionElement>(element)) {
if (option->OwnerSelectElement())
return true;
}
// Preserve nodes with language attributes.
if (HasAttribute(html_names::kLangAttr))
return true;
return false;
}
const AXObject* AXObject::GetAtomicTextFieldAncestor(
int max_levels_to_check) const {
if (IsAtomicTextField())
return this;
if (max_levels_to_check == 0)
return nullptr;