blob: 0c4ce360e94d291df760c2f3b1db6fa98676cf97 [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/auto_reset.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/input/web_keyboard_event.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/public/mojom/scroll/scroll_into_view_params.mojom-blink.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/accessibility/axid.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/element.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/dom/shadow_root.h"
#include "third_party/blink/renderer/core/dom/slot_assignment_engine.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/events/keyboard_event.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/frame/visual_viewport.h"
#include "third_party/blink/renderer/core/fullscreen/fullscreen.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/fenced_frame/html_fenced_frame_element.h"
#include "third_party/blink/renderer/core/html/forms/html_form_control_element.h"
#include "third_party/blink/renderer/core/html/forms/html_input_element.h"
#include "third_party/blink/renderer/core/html/forms/html_opt_group_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/html/shadow/shadow_element_names.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/scroll/scroll_into_view_util.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/aria_notification.h"
#include "third_party/blink/renderer/modules/accessibility/ax_enums.h"
#if DCHECK_IS_ON()
#include "third_party/blink/renderer/modules/accessibility/ax_debug_utils.h"
#endif
#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h"
#include "third_party/blink/renderer/modules/accessibility/ax_inline_text_box.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/keyboard_codes.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_enums.mojom-blink-forward.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/accessibility/ax_role_properties.h"
#include "ui/accessibility/ax_tree_id.h"
#include "ui/accessibility/ax_tree_source.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/geometry/transform.h"
namespace blink {
using mojom::blink::FormControlType;
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 kAXActiveFullscreenElement:
return "activeFullscreenElement";
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 kAXHiddenByChildTree:
return "hiddenByChildTree";
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 To<Document>(node)->IsLoadCompleted() ? "#document"
: "#document (loading)";
}
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 + ">";
}
#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::kPreDeprecated:
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
constexpr wtf_size_t kNumRoles =
static_cast<wtf_size_t>(ax::mojom::blink::Role::kMaxValue) + 1;
using ARIARoleMap =
HashMap<String, ax::mojom::blink::Role, CaseFoldingHashTraits<String>>;
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},
// role="image" is listed after role="img" to treat the synonym img
// as a computed name image
{"image", 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},
{"subscript", ax::mojom::blink::Role::kSubscript},
{"suggestion", ax::mojom::blink::Role::kSuggestion},
{"superscript", ax::mojom::blink::Role::kSuperscript},
{"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, and for WPT tests.
// These are roles which map from the ARIA role name to the internal role when
// building the tree, but in DevTools or testing, we want to show the ARIA
// role name, since that is the publicly visible concept.
const RoleEntry kReverseRoles[] = {
{"banner", ax::mojom::blink::Role::kHeader},
{"generic", ax::mojom::blink::Role::kHeaderAsNonLandmark},
{"button", ax::mojom::blink::Role::kToggleButton},
{"button", ax::mojom::blink::Role::kPopUpButton},
{"contentinfo", ax::mojom::blink::Role::kFooter},
{"option", ax::mojom::blink::Role::kMenuListOption},
{"option", ax::mojom::blink::Role::kListBoxOption},
{"group", ax::mojom::blink::Role::kDetails},
{"combobox", ax::mojom::blink::Role::kComboBoxMenuButton},
{"combobox", ax::mojom::blink::Role::kComboBoxSelect},
{"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;
}
void AddIntListAttributeFromObjects(ax::mojom::blink::IntListAttribute attr,
const AXObject::AXObjectVector& objects,
ui::AXNodeData* node_data) {
DCHECK(node_data);
std::vector<int32_t> ids;
for (const auto& obj : objects) {
if (!obj->AccessibilityIsIgnored())
ids.push_back(obj->AXObjectID());
}
if (!ids.empty())
node_data->AddIntListAttribute(attr, ids);
}
// Max length for attributes such as aria-label.
static constexpr uint32_t kMaxStringAttributeLength = 10000;
// Max length for a static text name.
// Length of War and Peace (http://www.gutenberg.org/files/2600/2600-0.txt).
static constexpr uint32_t kMaxStaticTextLength = 3227574;
std::string TruncateString(const String& str,
uint32_t max_len = kMaxStringAttributeLength) {
auto str_utf8 = str.Utf8(kStrictUTF8Conversion);
if (str_utf8.size() > max_len) {
std::string truncated;
base::TruncateUTF8ToByteSize(str_utf8, max_len, &truncated);
return truncated;
}
return str_utf8;
}
void TruncateAndAddStringAttribute(
ui::AXNodeData* dst,
ax::mojom::blink::StringAttribute attribute,
const String& value,
uint32_t max_len = kMaxStringAttributeLength) {
if (!value.empty()) {
dst->AddStringAttribute(attribute, TruncateString(value, max_len));
}
}
void AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute attr,
const Vector<int> offsets,
ui::AXNodeData* node_data) {
std::vector<int32_t> offset_values;
for (int offset : offsets)
offset_values.push_back(static_cast<int32_t>(offset));
DCHECK(node_data);
if (!offset_values.empty())
node_data->AddIntListAttribute(attr, offset_values);
}
} // 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;
case DocumentMarker::kCustomHighlight:
result = ax::mojom::blink::MarkerType::kHighlight;
break;
default:
result = ax::mojom::blink::MarkerType::kNone;
break;
}
return static_cast<int32_t>(result);
}
int32_t ToAXHighlightType(const AtomicString& highlight_type) {
DEFINE_STATIC_LOCAL(const AtomicString, type_highlight, ("highlight"));
DEFINE_STATIC_LOCAL(const AtomicString, type_spelling_error,
("spelling-error"));
DEFINE_STATIC_LOCAL(const AtomicString, type_grammar_error,
("grammar-error"));
ax::mojom::blink::HighlightType result =
ax::mojom::blink::HighlightType::kNone;
if (highlight_type == type_highlight)
result = ax::mojom::blink::HighlightType::kHighlight;
else if (highlight_type == type_spelling_error)
result = ax::mojom::blink::HighlightType::kSpellingError;
else if (highlight_type == type_grammar_error)
result = ax::mojom::blink::HighlightType::kGrammarError;
// Check that |highlight_type| is one of the static AtomicStrings defined
// above or "none", so if there are more HighlightTypes added, they should
// also be taken into account in this function.
DCHECK(result != ax::mojom::blink::HighlightType::kNone ||
highlight_type == "none");
return static_cast<int32_t>(result);
}
const AXObject* FindAncestorWithAriaHidden(const AXObject* start) {
for (const AXObject* object = start;
object && !IsA<Document>(object->GetNode());
object = object->ParentObject()) {
if (object->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden))
return object;
}
return nullptr;
}
// 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),
cached_live_region_root_(nullptr),
ax_object_cache_(&ax_object_cache) {
++number_of_live_ax_objects_;
}
AXObject::~AXObject() {
DCHECK(IsDetached());
--number_of_live_ax_objects_;
}
void AXObject::SetHasDirtyDescendants(bool dirty) const {
CHECK(!dirty || LastKnownIsIncludedInTreeValue())
<< "Only included nodes can be marked as having dirty descendants: "
<< ToString(true, true);
has_dirty_descendants_ = dirty;
}
void AXObject::SetAncestorsHaveDirtyDescendants() const {
CHECK(!IsDetached());
CHECK(!AXObjectCache().HasBeenDisposed());
if (AXObjectCache().IsFrozen()) {
// TODO(accessibility): Restore as CHECK(), remove early return.
DCHECK(false) << "Attempt to update frozen tree: " << ToString(true, true);
return;
}
CHECK(!AXObjectCache().UpdatingTree());
// Set the dirty bit for the root AX object when created. For all other
// objects, this is set by a descendant needing to be updated, and
// AXObjectCacheImpl::UpdateTreeIfNeeded will therefore process an object
// if its parent has has_dirty_descendants_ set. The root, however, has no
// parent, so there is no parent to mark in order to cause the root to update
// itself. Therefore this bit serves a second purpose of determining
// whether AXObjectCacheImpl::UpdateTreeIfNeeded needs to update the root
// object.
if (IsRoot()) {
// Need at least the root object to be flagged in order for
// UpdateTreeIfNeeded() to do anything.
SetHasDirtyDescendants(true);
return;
}
if (AXObjectCache().EntireDocumentIsDirty()) {
// No need to walk parent chain when marking the entire document dirty,
// as every node will have the bit set. In addition, attempting to repair
// the parent chain while marking everything dirty is actually against
// the point, because all child-parent relationships will be rebuilt
// from the top down.
if (LastKnownIsIncludedInTreeValue()) {
SetHasDirtyDescendants(true);
}
return;
}
const AXObject* ancestor = this;
while (true) {
ancestor = ancestor->CachedParentObject();
if (!ancestor) {
break;
}
DCHECK(!ancestor->IsDetached());
// We need to to continue setting bits through AX objects for which
// LastKnownIsIncludedInTreeValue is false, since those objects are omitted
// from the generated tree. However, don't set the bit on unincluded
// objects, during the clearing phase in
// AXObjectCacheImpl::UpdateTreeIfNeeded(), only included nodes are
// visited.
if (!ancestor->LastKnownIsIncludedInTreeValue()) {
continue;
}
if (ancestor->has_dirty_descendants_) {
break;
}
ancestor->SetHasDirtyDescendants(true);
}
#if DCHECK_IS_ON()
// Walk up the tree looking for dirty bits that failed to be set. If any
// are found, this is a bug.
if (!AXObjectCache().UpdatingTree()) {
bool fail = false;
for (auto* obj = CachedParentObject(); obj;
obj = obj->CachedParentObject()) {
if (obj->LastKnownIsIncludedInTreeValue() &&
!obj->has_dirty_descendants_) {
fail = true;
break;
}
}
DCHECK(!fail) << "Failed to set dirty bits on some ancestors:\n"
<< ParentChainToStringHelper(this);
}
#endif
}
void AXObject::Init(AXObject* parent) {
CHECK(!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_);
// Every AXObject must have a parent unless it's the root.
CHECK(parent || IsRoot())
<< "The following node should have a parent: " << GetNode();
CHECK(!AXObjectCache().IsFrozen());
#if DCHECK_IS_ON()
CHECK(!is_initializing_);
base::AutoReset<bool> reentrancy_protector(&is_initializing_, true);
#endif // DCHECK_IS_ON()
// Set the parent as soon as possible, so that we can use it in computations
// for the role and cached value. We will set it again at the end of the
// method using SetParent(), to ensure all of the normal code paths for
// setting the parent are followed.
parent_ = parent;
// 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();
HTMLOptGroupElement* optgroup = DynamicTo<HTMLOptGroupElement>(GetNode());
if (optgroup && optgroup->OwnerSelectElement()) {
// We do not currently create accessible objects for an <optgroup> inside of
// a <select size=1>.
// TODO(accessibility) Remove this once we refactor HTML <select> to use
// the shadow DOM and AXNodeObject instead of AXMenuList* classes.
DCHECK(!optgroup->OwnerSelectElement()->UsesMenuList());
}
#endif // DCHECK_IS_ON()
// 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);
children_dirty_ = true;
UpdateCachedAttributeValuesIfNeeded(false);
DCHECK(GetDocument()) << "All AXObjects must have a document: "
<< ToString(true, true);
// Set the parent again, this time via SetParent(), so that all related checks
// and calls occur now that we have the role and updated cached values.
SetParent(parent_);
}
void AXObject::Detach() {
#if DCHECK_IS_ON()
DCHECK(!is_updating_cached_values_)
<< "Don't detach in the middle of updating cached values: "
<< ToString(true, true);
#endif
// 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 defined(AX_FAIL_FAST_BUILD)
SANITIZER_CHECK(ax_object_cache_);
SANITIZER_CHECK(!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.";
SANITIZER_CHECK(!is_adding_children_) << ToString(true, true);
#endif
#if !defined(NDEBUG)
// Facilitates debugging of detached objects by providing info on what it was.
if (!ax_object_cache_->HasBeenDisposed()) {
detached_object_debug_info_ = 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;
child_cached_values_need_update_ = false;
cached_values_need_update_ = false;
has_dirty_descendants_ = false;
id_ = 0;
}
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."
<< "\nThis: " << ToString(true, true)
<< "\nDOM parent chain , starting at |this->GetNode()|:";
int count = 0;
for (Node* node = GetNode(); node;
node = GetParentNodeForComputeParent(AXObjectCache(), node)) {
message << "\n"
<< (++count) << ". " << node
<< "\n LayoutObject=" << node->GetLayoutObject();
if (AXObject* obj = AXObjectCache().Get(node))
message << "\n " << obj->ToString(true, true);
if (!node->isConnected()) {
break;
}
}
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);
DCHECK(!IsAXInlineTextBox() ||
ui::CanHaveInlineTextBoxChildren(new_parent->RoleValue()))
<< "Unexpected parent of inline text box: " << new_parent->RoleValue();
}
// 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;
if (AXObjectCache().UpdatingTree()) {
// If updating tree, tell the newly included parent to iterate through
// all of its children to look for the has dirty descendants flag.
// However, we do not set the flag on higher ancestors since
// they have already been walked by the tree update loop.
if (AXObject* ax_included_parent = ParentObjectIncludedInTree()) {
ax_included_parent->SetHasDirtyDescendants(true);
}
} else {
SetAncestorsHaveDirtyDescendants();
}
}
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.
bool is_missing = !IsRoot();
DUMP_WILL_BE_CHECK(!is_missing || !AXObjectCache().IsFrozen())
<< "Should not have missing parent in frozen tree: "
<< ToString(true, true);
return is_missing;
}
if (parent_->IsDetached()) {
CHECK(!AXObjectCache().IsFrozen())
<< "Should not have detached parent in frozen tree: "
<< ToString(true, true);
return true;
}
return false;
}
// 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 {
AXObject* ax_parent = ComputeParentOrNull();
CHECK(!ax_parent || !ax_parent->IsDetached())
<< "Computed parent should never be detached:" << "\n* Child: "
<< ToString(true, true)
<< "\n* Parent: " << ax_parent->ToString(true, true);
return ax_parent;
}
// Same as ComputeParent, but without the extra check for valid parent in the
// end. This is for use in RestoreParentOrPrune.
AXObject* AXObject::ComputeParentOrNull() const {
CHECK(!IsDetached());
if (IsMockObject()) {
const AXMenuListPopup* popup = To<AXMenuListPopup>(this);
return popup->owner();
}
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();
AXObject* ax_parent = nullptr;
if (IsAXInlineTextBox()) {
NOTREACHED()
<< "AXInlineTextBox box tried to compute a new parent, but they are "
"not allowed to exist even temporarily without a parent, as their "
"existence depends on the parent text object. Parent text = "
<< (AXObjectCache().Get(GetNode())
? AXObjectCache().Get(GetNode())->ToString(true, true)
: "");
} else 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());
}
return ax_parent;
}
// static
Node* AXObject::GetParentNodeForComputeParent(AXObjectCacheImpl& cache,
Node* node) {
if (!node || !node->isConnected()) {
return nullptr;
}
// A document's parent should be the page popup owner, if any, otherwise null.
if (auto* document = DynamicTo<Document>(node)) {
LocalFrame* frame = document->GetFrame();
DCHECK(frame);
Node* popup_owner = frame->PagePopupOwner();
if (!popup_owner) {
return nullptr;
}
// TODO(accessibility) Remove this rule once we stop using AXMenuList*.
if (IsA<HTMLSelectElement>(popup_owner) &&
AXObjectCacheImpl::ShouldCreateAXMenuListFor(
popup_owner->GetLayoutObject())) {
return nullptr;
}
return popup_owner;
}
// 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);
if (!parent) {
return nullptr;
}
// Descendants of pseudo elements must only be created by walking the tree via
// AXNodeObject::AddChildren(), which already knows the parent. Therefore, the
// parent must not be computed. This helps avoid situations with certain
// elements where there is asymmetry between what considers this a child vs
// what the this considers its parent. An example of this kind of situation is
// a ::first-letter within a ::before.
if (node->GetLayoutObject() && node->GetLayoutObject()->Parent() &&
node->GetLayoutObject()->Parent()->IsPseudoElement()) {
return nullptr;
}
HTMLMapElement* map_element = DynamicTo<HTMLMapElement>(parent);
if (map_element) {
// 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;
}
return CanComputeAsNaturalParent(parent) ? parent : nullptr;
}
// static
bool AXObject::CanComputeAsNaturalParent(Node* node) {
if (IsA<Document>(node)) {
return true;
}
DCHECK(IsA<Element>(node)) << "Expected element: " << node;
// 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 false is returned for anything else where the
// parent would be AXMenuList.
if (AXObjectCacheImpl::ShouldCreateAXMenuListFor(node->GetLayoutObject())) {
return false;
}
// An image cannot be the natural DOM parent of another AXObject, it can only
// have <area> children, which are from another part of the DOM tree.
if (IsA<HTMLImageElement>(node)) {
return false;
}
return CanHaveChildren(*To<Element>(node));
}
// static
bool AXObject::CanHaveChildren(Element& element) {
// Image map parent-child relationships work as follows:
// - The image is the parent
// - The DOM children of the associated <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>(element)) {
return false;
}
if (IsA<HTMLImageElement>(element)) {
return GetMapForImage(&element);
}
// Placeholder gets exposed as an attribute on the input accessibility node,
// so there's no need to add its text children. Placeholder text is a separate
// node that gets removed when it disappears, so this will only be present if
// the placeholder is visible.
if (Element* host = element.OwnerShadowHost()) {
if (auto* ancestor_input = DynamicTo<TextControlElement>(host)) {
if (ancestor_input->PlaceholderElement() == &element) {
// |element| is a placeholder.
return false;
}
}
}
if (IsA<HTMLBRElement>(element)) {
// Normally, a <br> is allowed to have a single inline text box child.
// However, a <br> element that has DOM children can occur only if a script
// adds the children, and Blink will not render those children. This is an
// obscure edge case that should only occur during fuzzing, but to maintain
// tree consistency and prevent DCHECKs, AXObjects for <br> elements are not
// allowed to have children if there are any DOM children at all.
return !element.hasChildren();
}
if (IsA<HTMLHRElement>(element)) {
return false;
}
if (auto* input = DynamicTo<HTMLInputElement>(&element)) {
// False for checkbox, radio and range.
return !input->IsCheckable() &&
input->FormControlType() != FormControlType::kInputRange;
}
if (IsA<HTMLOptionElement>(element)) {
return false;
}
if (IsA<HTMLProgressElement>(element)) {
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;
// 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) {
if (!current_node) {
return nullptr;
}
// 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(cache, current_node);
// If the tree is not currently mutable, then new AXObjects cannot be created.
// Return the AXObject for the parent only if it is already part of the tree.
if (!cache.IsProcessingDeferredEvents()) {
return cache.Get(parent_node);
}
// Get the existing AXObject for the parent or create one if necessary.
// Will not create an object if no valid parent node is found. This occurs
// when a DOM child isn't visited by LayoutTreeBuilderTraversal, such as an
// element child of a <textarea>, which only supports plain text.
// TODO(acessibility) Covert to NOTREACHED(), and eventually try to remove
// parent repairs and this method entirely, as AXObjects will only be created
// by building from the top down.
return cache.GetOrCreate(parent_node);
}
#if DCHECK_IS_ON()
std::string AXObject::GetAXTreeForThis() const {
return TreeToStringWithMarkedObjectHelper(AXObjectCache().Root(), this);
}
void AXObject::ShowAXTreeForThis() const {
DLOG(INFO) << "\n" << GetAXTreeForThis();
}
#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();
}
namespace {
void SerializeAriaNotificationAttributes(const AriaNotification& notification,
ui::AXNodeData* node_data) {
DCHECK(node_data);
node_data->AddStringListAttribute(
ax::mojom::blink::StringListAttribute::kAriaNotificationAnnouncements,
{TruncateString(notification.Announcement())});
node_data->AddStringListAttribute(
ax::mojom::blink::StringListAttribute::kAriaNotificationIds,
{TruncateString(notification.NotificationId())});
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kAriaNotificationInterruptProperties,
{static_cast<int32_t>(notification.Interrupt())});
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kAriaNotificationPriorityProperties,
{static_cast<int32_t>(notification.Priority())});
}
} // namespace
void AXObject::Serialize(ui::AXNodeData* node_data,
ui::AXMode accessibility_mode) {
// Reduce redundant ancestor chain walking for display lock computations.
auto memoization_scope =
DisplayLockUtilities::CreateLockCheckMemoizationScope();
node_data->role = ComputeFinalRoleForSerialization();
node_data->id = AXObjectID();
PreSerializationConsistencyCheck();
// 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 a 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::kPDFPrinting)) {
SerializeLangAttribute(node_data); // Propagates using all nodes' values.
}
// Always try to serialize child tree ids.
SerializeChildTreeID(node_data);
if (!accessibility_mode.has_mode(ui::AXMode::kPDFPrinting)) {
SerializeBoundingBoxAttributes(*node_data);
}
// Return early. The following attributes are unnecessary for ignored nodes.
// Exception: focusable ignored nodes are fully serialized, so that reasonable
// verbalizations can be made if they actually receive focus.
if (AccessibilityIsIgnored()) {
node_data->AddState(ax::mojom::blink::State::kIgnored);
// Early return for ignored, unfocusable nodes, avoiding unnecessary work.
if (!is_focusable) {
// The name is important for exposing the selection around ignored nodes.
// TODO(accessibility) Remove this and still pass this
// content_browsertest:
// All/DumpAccessibilityTreeTest.AccessibilityIgnoredSelection/blink
if (RoleValue() == ax::mojom::blink::Role::kStaticText)
SerializeNameAndDescriptionAttributes(accessibility_mode, node_data);
return;
}
}
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader))
SerializeScreenReaderAttributes(node_data);
SerializeUnignoredAttributes(node_data, accessibility_mode);
if (accessibility_mode.has_mode(ui::AXMode::kPDFPrinting)) {
SerializeNameAndDescriptionAttributes(accessibility_mode, node_data);
// Return early. None of the following attributes are needed for PDFs.
return;
}
SerializeNameAndDescriptionAttributes(accessibility_mode, node_data);
if (!accessibility_mode.has_mode(ui::AXMode::kScreenReader))
return;
if (LiveRegionRoot())
SerializeLiveRegionAttributes(node_data);
SerializeOtherScreenReaderAttributes(node_data);
for (const auto& notification :
AXObjectCache().RetrieveAriaNotifications(this)) {
SerializeAriaNotificationAttributes(notification, node_data);
}
// Return early. The following attributes are unnecessary for ignored nodes.
// Exception: focusable ignored nodes are fully serialized, so that reasonable
// verbalizations can be made if they actually receive focus.
if (AccessibilityIsIgnored() &&
!node_data->HasState(ax::mojom::blink::State::kFocusable)) {
return;
}
}
void AXObject::SerializeBoundingBoxAttributes(ui::AXNodeData& dst) const {
bool clips_children = false;
PopulateAXRelativeBounds(dst.relative_bounds, &clips_children);
if (clips_children) {
dst.AddBoolAttribute(ax::mojom::blink::BoolAttribute::kClipsChildren, true);
}
if (IsLineBreakingObject()) {
dst.AddBoolAttribute(ax::mojom::blink::BoolAttribute::kIsLineBreakingObject,
true);
}
AXObjectCache().SetCachedBoundingBox(AXObjectID(), dst.relative_bounds);
}
static bool AXShouldIncludePageScaleFactorInRoot() {
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_MAC)
return true;
#else
return false;
#endif
}
void AXObject::PopulateAXRelativeBounds(ui::AXRelativeBounds& bounds,
bool* clips_children) const {
AXObject* offset_container;
gfx::RectF bounds_in_container;
gfx::Transform container_transform;
GetRelativeBounds(&offset_container, bounds_in_container, container_transform,
clips_children);
bounds.bounds = bounds_in_container;
if (offset_container && !offset_container->IsDetached())
bounds.offset_container_id = offset_container->AXObjectID();
if (AXShouldIncludePageScaleFactorInRoot() && IsRoot()) {
const Page* page = GetDocument()->GetPage();
container_transform.Scale(page->PageScaleFactor(), page->PageScaleFactor());
container_transform.Translate(
-page->GetVisualViewport().VisibleRect().origin().OffsetFromOrigin());
}
if (!container_transform.IsIdentity())
bounds.transform = std::make_unique<gfx::Transform>(container_transform);
}
void AXObject::SerializeActionAttributes(ui::AXNodeData* node_data) {
if (CanSetValueAttribute())
node_data->AddAction(ax::mojom::blink::Action::kSetValue);
if (IsSlider()) {
node_data->AddAction(ax::mojom::blink::Action::kDecrement);
node_data->AddAction(ax::mojom::blink::Action::kIncrement);
}
if (IsUserScrollable()) {
node_data->AddAction(ax::mojom::blink::Action::kScrollUp);
node_data->AddAction(ax::mojom::blink::Action::kScrollDown);
node_data->AddAction(ax::mojom::blink::Action::kScrollLeft);
node_data->AddAction(ax::mojom::blink::Action::kScrollRight);
node_data->AddAction(ax::mojom::blink::Action::kScrollForward);
node_data->AddAction(ax::mojom::blink::Action::kScrollBackward);
}
}
void AXObject::SerializeChildTreeID(ui::AXNodeData* node_data) {
// If a child tree has explicitly been stitched at this object via the
// `ax::mojom::blink::Action::kStitchChildTree`, then override any child trees
// coming from HTML.
if (child_tree_id_) {
node_data->AddChildTreeId(*child_tree_id_);
return;
}
// If this is an HTMLFrameOwnerElement (such as an iframe), we may need to
// embed the ID of the child frame.
if (!IsEmbeddingElement()) {
// TODO(crbug.com/1342603) Determine why these are firing in the wild and,
// once fixed, turn into a DCHECK.
SANITIZER_CHECK(!IsFrame(GetNode()))
<< "If this is an iframe, it should also be a child tree owner: "
<< ToString(true, true);
return;
}
// Do not attach hidden child trees.
if (!IsVisible()) {
return;
}
auto* html_frame_owner_element = To<HTMLFrameOwnerElement>(GetElement());
Frame* child_frame = html_frame_owner_element->ContentFrame();
if (!child_frame) {
// TODO(crbug.com/1342603) Determine why these are firing in the wild and,
// once fixed, turn into a DCHECK.
SANITIZER_CHECK(IsDisabled()) << ToString(true, true);
return;
}
std::optional<base::UnguessableToken> child_token =
child_frame->GetEmbeddingToken();
if (!child_token)
return; // No child token means that the connection isn't ready yet.
DCHECK_EQ(ChildCountIncludingIgnored(), 0)
<< "Children won't exist until the trees are stitched together in the "
"browser process. A failure means that a child node was incorrectly "
"considered relevant by AXObjectCacheImpl."
<< "\n* Parent: " << ToString(true)
<< "\n* Frame owner: " << IsA<HTMLFrameOwnerElement>(GetNode())
<< "\n* Element src: " << GetAttribute(html_names::kSrcAttr)
<< "\n* First child: " << FirstChildIncludingIgnored()->ToString(true);
ui::AXTreeID child_tree_id = ui::AXTreeID::FromToken(child_token.value());
node_data->AddChildTreeId(child_tree_id);
}
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::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::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);
}
// 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(node_data);
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kRole, role_str);
}
void AXObject::SerializeHTMLTagAndClass(ui::AXNodeData* node_data) {
Element* element = GetElement();
if (!element) {
if (IsA<Document>(GetNode())) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kHtmlTag, "#document");
}
return;
}
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kHtmlTag,
element->tagName().LowerASCII());
if (const AtomicString& class_name = element->GetClassAttribute()) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kClassName, class_name);
}
}
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 BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
if (node_data->role == ax::mojom::blink::Role::kMath ||
node_data->role == ax::mojom::blink::Role::kMathMLMath) {
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kInnerHtml,
element->innerHTML(), kMaxStaticTextLength);
}
#endif
}
void AXObject::SerializeInlineTextBoxAttributes(
ui::AXNodeData* node_data) const {
DCHECK_EQ(ax::mojom::blink::Role::kInlineTextBox, node_data->role);
Vector<int> character_offsets;
TextCharacterOffsets(character_offsets);
AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute::kCharacterOffsets, character_offsets,
node_data);
Vector<int> word_starts;
Vector<int> word_ends;
GetWordBoundaries(word_starts, word_ends);
AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute::kWordStarts, word_starts, node_data);
AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute::kWordEnds, word_ends, node_data);
}
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());
}
}
}
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::SerializeListMarkerAttributes(ui::AXNodeData* node_data) const {
DCHECK_EQ(ax::mojom::blink::Role::kListMarker, node_data->role);
Vector<int> word_starts;
Vector<int> word_ends;
GetWordBoundaries(word_starts, word_ends);
AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute::kWordStarts, word_starts, node_data);
AddIntListAttributeFromOffsetVector(
ax::mojom::blink::IntListAttribute::kWordEnds, word_ends, node_data);
}
void AXObject::SerializeLiveRegionAttributes(ui::AXNodeData* node_data) const {
DCHECK(LiveRegionRoot());
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kLiveAtomic,
LiveRegionAtomic());
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kLiveStatus,
LiveRegionStatus());
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kLiveRelevant,
LiveRegionRelevant());
// If we are not at the root of an atomic live region.
if (ContainerLiveRegionAtomic() && !LiveRegionRoot()->IsDetached() &&
!LiveRegionAtomic()) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kMemberOfId,
LiveRegionRoot()->AXObjectID());
}
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kContainerLiveAtomic,
ContainerLiveRegionAtomic());
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kContainerLiveBusy,
ContainerLiveRegionBusy());
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kContainerLiveStatus,
ContainerLiveRegionStatus());
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kContainerLiveRelevant,
ContainerLiveRegionRelevant());
}
void AXObject::SerializeNameAndDescriptionAttributes(
ui::AXMode accessibility_mode,
ui::AXNodeData* node_data) const {
ax::mojom::blink::NameFrom name_from;
AXObjectVector name_objects;
String name = GetName(name_from, &name_objects);
if (name_from == ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty) {
node_data->AddStringAttribute(ax::mojom::blink::StringAttribute::kName,
std::string());
node_data->SetNameFrom(
ax::mojom::blink::NameFrom::kAttributeExplicitlyEmpty);
} else if (!name.empty()) {
int max_length = node_data->role == ax::mojom::blink::Role::kStaticText
? kMaxStaticTextLength
: kMaxStringAttributeLength;
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kName, name, max_length);
node_data->SetNameFrom(name_from);
AddIntListAttributeFromObjects(
ax::mojom::blink::IntListAttribute::kLabelledbyIds, name_objects,
node_data);
}
ax::mojom::blink::DescriptionFrom description_from;
AXObjectVector description_objects;
String description =
Description(name_from, description_from, &description_objects);
if (!description.empty()) {
DCHECK(description_from != ax::mojom::blink::DescriptionFrom::kNone);
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kDescription,
description);
node_data->SetDescriptionFrom(description_from);
AddIntListAttributeFromObjects(
ax::mojom::blink::IntListAttribute::kDescribedbyIds,
description_objects, node_data);
}
String title = Title(name_from);
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kTooltip, title);
if (!accessibility_mode.has_mode(ui::AXMode::kScreenReader))
return;
String placeholder = Placeholder(name_from);
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kPlaceholder, placeholder);
}
void AXObject::SerializeScreenReaderAttributes(ui::AXNodeData* node_data) {
if (ui::IsText(RoleValue())) {
// Don't serialize these attributes on text, where it is uninteresting.
return;
}
String display_style;
if (Element* element = GetElement()) {
if (const ComputedStyle* computed_style = element->GetComputedStyle()) {
display_style = CSSProperty::Get(CSSPropertyID::kDisplay)
.CSSValueFromComputedStyle(
*computed_style, /* layout_object */ nullptr,
/* allow_visited_style */ false,
CSSValuePhase::kComputedValue)
->CssText();
if (!display_style.empty()) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kDisplay,
display_style);
}
}
// Whether it has ARIA attributes at all.
if (HasAriaAttribute()) {
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kHasAriaAttribute, true);
}
}
if (KeyboardShortcut().length() &&
!node_data->HasStringAttribute(
ax::mojom::blink::StringAttribute::kKeyShortcuts)) {
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kKeyShortcuts,
KeyboardShortcut());
}
if (AXObject* active_descendant = ActiveDescendant()) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kActivedescendantId,
active_descendant->AXObjectID());
}
}
String AXObject::KeyboardShortcut() const {
const AtomicString& access_key = AccessKey();
if (access_key.IsNull())
return String();
DEFINE_STATIC_LOCAL(String, modifier_string, ());
if (modifier_string.IsNull()) {
unsigned modifiers = KeyboardEventManager::kAccessKeyModifiers;
// Follow the same order as Mozilla MSAA implementation:
// Ctrl+Alt+Shift+Meta+key. MSDN states that keyboard shortcut strings
// should not be localized and defines the separator as "+".
StringBuilder modifier_string_builder;
if (modifiers & WebInputEvent::kControlKey)
modifier_string_builder.Append("Ctrl+");
if (modifiers & WebInputEvent::kAltKey)
modifier_string_builder.Append("Alt+");
if (modifiers & WebInputEvent::kShiftKey)
modifier_string_builder.Append("Shift+");
if (modifiers & WebInputEvent::kMetaKey)
modifier_string_builder.Append("Win+");
modifier_string = modifier_string_builder.ToString();
}
return String(modifier_string + access_key);
}
void AXObject::SerializeOtherScreenReaderAttributes(
ui::AXNodeData* node_data) const {
DCHECK_NE(node_data->role, ax::mojom::blink::Role::kUnknown);
if (IsA<Document>(GetNode())) {
// The busy attribute is only relevant for actual Documents, not popups.
if (RoleValue() == ax::mojom::blink::Role::kRootWebArea && !IsLoaded()) {
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kBusy, true);
}
if (AXObject* parent = ParentObject()) {
DCHECK(parent->ChooserPopup() == this)
<< "ChooserPopup missing for: " << parent->ToString(true);
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kPopupForId,
parent->AXObjectID());
}
}
if (node_data->role == ax::mojom::blink::Role::kColorWell) {
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kColorValue,
ColorValue());
}
if (node_data->role == ax::mojom::blink::Role::kLink) {
AXObject* target = InPageLinkTarget();
if (target) {
int32_t target_id = target->AXObjectID();
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kInPageLinkTargetId, target_id);
}
// `ax::mojom::blink::StringAttribute::kLinkTarget` is only valid on <a> and
// <area> elements. <area> elements should link to something in order to be
// considered, see `AXImageMap::Role()`.
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kLinkTarget,
EffectiveTarget());
}
if (node_data->role == ax::mojom::blink::Role::kRadioButton) {
AddIntListAttributeFromObjects(
ax::mojom::blink::IntListAttribute::kRadioGroupIds,
RadioButtonsInGroup(), node_data);
}
if (GetAriaCurrentState() != ax::mojom::blink::AriaCurrentState::kNone) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kAriaCurrentState,
static_cast<int32_t>(GetAriaCurrentState()));
}
if (GetInvalidState() != ax::mojom::blink::InvalidState::kNone)
node_data->SetInvalidState(GetInvalidState());
if (CheckedState() != ax::mojom::blink::CheckedState::kNone) {
node_data->SetCheckedState(CheckedState());
}
if (node_data->role == ax::mojom::blink::Role::kInlineTextBox) {
SerializeInlineTextBoxAttributes(node_data);
}
if (node_data->role == ax::mojom::blink::Role::kListMarker) {
SerializeListMarkerAttributes(node_data);
}
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kAccessKey, AccessKey());
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kAutoComplete,
AutoComplete());
if (Action() != ax::mojom::blink::DefaultActionVerb::kNone) {
node_data->SetDefaultActionVerb(Action());
}
if (AXObject* next_on_line = NextOnLine()) {
CHECK(!next_on_line->IsDetached());
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kNextOnLineId,
next_on_line->AXObjectID());
}
if (AXObject* prev_on_line = PreviousOnLine()) {
CHECK(!prev_on_line->IsDetached());
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kPreviousOnLineId,
prev_on_line->AXObjectID());
}
AXObjectVector error_messages = ErrorMessage();
if (error_messages.size() > 0) {
AddIntListAttributeFromObjects(
ax::mojom::blink::IntListAttribute::kErrormessageIds, error_messages,
node_data);
}
if (ui::SupportsHierarchicalLevel(node_data->role) && HierarchicalLevel()) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kHierarchicalLevel,
HierarchicalLevel());
}
if (CanvasHasFallbackContent()) {
node_data->AddBoolAttribute(
ax::mojom::blink::BoolAttribute::kCanvasHasFallback, true);
}
if (IsRangeValueSupported()) {
float value;
if (ValueForRange(&value)) {
node_data->AddFloatAttribute(
ax::mojom::blink::FloatAttribute::kValueForRange, value);
}
float max_value;
if (MaxValueForRange(&max_value)) {
node_data->AddFloatAttribute(
ax::mojom::blink::FloatAttribute::kMaxValueForRange, max_value);
}
float min_value;
if (MinValueForRange(&min_value)) {
node_data->AddFloatAttribute(
ax::mojom::blink::FloatAttribute::kMinValueForRange, min_value);
}
float step_value;
if (StepValueForRange(&step_value)) {
node_data->AddFloatAttribute(
ax::mojom::blink::FloatAttribute::kStepValueForRange, step_value);
}
}
if (ui::IsDialog(node_data->role)) {
node_data->AddBoolAttribute(ax::mojom::blink::BoolAttribute::kModal,
IsModal());
}
}
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).
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());
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());
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());
}
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::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());
}
}
// 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::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()));
}
}
// 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);
}
ax::mojom::blink::Role role = RoleValue();
if (HasPopup() != ax::mojom::blink::HasPopup::kFalse) {
node_data->SetHasPopup(HasPopup());
} else if (role == ax::mojom::blink::Role::kPopUpButton ||
role == ax::mojom::blink::Role::kComboBoxSelect) {
node_data->SetHasPopup(ax::mojom::blink::HasPopup::kMenu);
} else if (ui::IsComboBox(role)) {
node_data->SetHasPopup(ax::mojom::blink::HasPopup::kListbox);
}
if (IsPopup() != ax::mojom::blink::IsPopup::kNone) {
node_data->SetIsPopup(IsPopup());
}
if (IsAutofillAvailable())
node_data->AddState(ax::mojom::blink::State::kAutofillAvailable);
if (IsDefault())
node_data->AddState(ax::mojom::blink::State::kDefault);
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 (accessibility_mode.has_mode(ui::AXMode::kScreenReader) ||
accessibility_mode.has_mode(ui::AXMode::kPDFPrinting)) {
// 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(role) && HeadingLevel()) {
node_data->AddIntAttribute(
ax::mojom::blink::IntAttribute::kHierarchicalLevel, HeadingLevel());
}
SerializeListAttributes(node_data);
SerializeTableAttributes(node_data);
}
if (accessibility_mode.has_mode(ui::AXMode::kPDFPrinting)) {
// Return early. None of the following attributes are needed for PDFs.
return;
}
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:
SerializeActionAttributes(node_data);
break;
}
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kUrl, Url().GetString());
if (accessibility_mode.has_mode(ui::AXMode::kScreenReader)) {
SerializeMarkerAttributes(node_data);
SerializeStyleAttributes(node_data);
}
SerializeSparseAttributes(node_data);
if (Element* element = GetElement()) {
// Do not send the value attribute for non-atomic text fields in order to
// improve the performance of the cross-process communication with the
// browser process, and since it can be easily computed in that process.
TruncateAndAddStringAttribute(node_data,
ax::mojom::blink::StringAttribute::kValue,
GetValueForControl());
if (IsA<HTMLInputElement>(element)) {
String type = element->getAttribute(html_names::kTypeAttr);
if (type.empty()) {
type = "text";
}
TruncateAndAddStringAttribute(
node_data, ax::mojom::blink::StringAttribute::kInputType, type);
}
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.Anchor().IsTextPosition()
? ax_selection.Anchor().TextOffset()
: ax_selection.Anchor().ChildIndex();
int end = ax_selection.Focus().IsTextPosition()
? ax_selection.Focus().TextOffset()
: ax_selection.Focus().ChildIndex();
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextSelStart,
start);
node_data->AddIntAttribute(ax::mojom::blink::IntAttribute::kTextSelEnd,
end);
}
}
SerializeComputedDetailsRelation(node_data);
// Try to get an aria-controls listbox for an <input role="combobox">.
if (!node_data->HasIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds)) {
if (AXObject* listbox = GetControlsListboxForTextfieldCombobox()) {
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kControlsIds,
{static_cast<int32_t>(listbox->AXObjectID())});
}
}
if (IsScrollableContainer())
SerializeScrollAttributes(node_data);
SerializeChooserPopupAttributes(node_data);
if (GetElement()) {
SerializeElementAttributes(node_data);
if (accessibility_mode.has_mode(ui::AXMode::kHTML)) {
SerializeHTMLAttributes(node_data);
}
}
SerializeImageDataAttributes(node_data);
SerializeTextInsertionDeletionOffsetAttributes(node_data);
}
void AXObject::SerializeComputedDetailsRelation(
ui::AXNodeData* node_data) const {
// aria-details was used -- it may have set a relation, unless the attribute
// value did not point to valid elements (e.g aria-details=""). Whether it
// actually set the relation or not, the author's intent in using the
// aria-details attribute is understood to mean that no automatic relation
// should be set.
if (HasAttribute(html_names::kAriaDetailsAttr)) {
return;
}
// Add details relation to <figure>, pointing at <figcaption>.
if (node_data->role == ax::mojom::blink::Role::kFigure) {
AXObject* fig_caption = GetChildFigcaption();
if (fig_caption) {
std::vector<int32_t> ids;
ids.push_back(GetChildFigcaption()->AXObjectID());
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds, ids);
return;
}
}
// Add aria-details for a popover invoker.
// TODO(https://crbug.com/1426607) Support this for non-plain hint popovers.
if (AXObject* popover = GetTargetPopoverForInvoker()) {
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kDetailsIds,
{static_cast<int32_t>(popover->AXObjectID())});
}
}
bool AXObject::IsPlainContent() const {
if (!ui::IsPlainContentElement(role_)) {
return false;
}
for (const auto& child : ChildrenIncludingIgnored()) {
if (!child->IsPlainContent()) {
return false;
}
}
return true;
}
// Popover invoking elements should have details relationships with their
// target popover, when that popover is a) open, and b) not the next element
// in the DOM (depth first search order).
AXObject* AXObject::GetTargetPopoverForInvoker() const {
auto* form_element = DynamicTo<HTMLFormControlElement>(GetElement());
if (!form_element) {
return nullptr;
}
HTMLElement* target_popover = form_element->popoverTargetElement().popover;
if (!target_popover || !target_popover->popoverOpen()) {
return nullptr;
}
if (ElementTraversal::NextSkippingChildren(*form_element) == target_popover) {
// The next element is already the popover.
return nullptr;
}
return AXObjectCache().Get(target_popover);
}
// Try to get an aria-controls for an <input role="combobox">, because it
// helps identify focusable options in the listbox using activedescendant
// detection, even though the focus is on the textbox and not on the listbox
// ancestor.
AXObject* AXObject::GetControlsListboxForTextfieldCombobox() {
// Only perform work for textfields.
if (!ui::IsTextField(RoleValue()))
return nullptr;
// Object is ignored for some reason, most likely hidden.
if (AccessibilityIsIgnored()) {
return nullptr;
}
// Authors used to be told to use aria-owns to point from the textfield to the
// listbox. However, the aria-owns on a textfield must be ignored for its
// normal purpose because a textfield cannot have children. This code allows
// the textfield's invalid aria-owns to be remapped to aria-controls.
DCHECK(GetElement());
HeapVector<Member<Element>> owned_elements;
AXObject* listbox_candidate = nullptr;
if (ElementsFromAttribute(GetElement(), owned_elements,
html_names::kAriaOwnsAttr) &&
owned_elements.size() > 0) {
DCHECK(owned_elements[0]);
listbox_candidate = AXObjectCache().Get(owned_elements[0]);
}
// Combobox grouping <div role="combobox"><input><div role="listbox"></div>.
if (!listbox_candidate && RoleValue() == ax::mojom::blink::Role::kTextField &&
ParentObject()->RoleValue() ==
ax::mojom::blink::Role::kComboBoxGrouping) {
listbox_candidate = UnignoredNextSibling();
}
// Heuristic: try the next sibling, but we are very strict about this in
// order to avoid false positives such as an <input> followed by a
// <select>.
if (!listbox_candidate &&
RoleValue() == ax::mojom::blink::Role::kTextFieldWithComboBox) {
// Require an aria-activedescendant on the <input>.
if (!GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant))
return nullptr;
listbox_candidate = UnignoredNextSibling();
if (!listbox_candidate)
return nullptr;
// Require that the next sibling is not a <select>.
if (IsA<HTMLSelectElement>(listbox_candidate->GetNode()))
return nullptr;
// Require an ARIA role on the next sibling.
if (!ui::IsComboBoxContainer(listbox_candidate->AriaRoleAttribute())) {
return nullptr;
}
// Naming a listbox within a composite combobox widget is not part of a
// known/used pattern. If it has a name, it's an indicator that it's
// probably a separate listbox widget.
if (!listbox_candidate->ComputedName().empty())
return nullptr;
}
if (!listbox_candidate ||
!ui::IsComboBoxContainer(listbox_candidate->RoleValue())) {
return nullptr;
}
return listbox_candidate;
}
const AtomicString& AXObject::GetRoleAttributeStringForObjectAttribute(
ui::AXNodeData* node_data) {
// All ARIA roles are exposed in xml-roles.
if (const AtomicString& role_str =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole)) {
return role_str;
}
ax::mojom::blink::Role landmark_role = node_data->role;
if (landmark_role == ax::mojom::blink::Role::kFooter) {
// - Treat <footer> as "contentinfo" in xml-roles object attribute.
landmark_role = ax::mojom::blink::Role::kContentInfo;
} else if (landmark_role == ax::mojom::blink::Role::kHeader) {
// - Treat <header> as "banner" in xml-roles object attribute.
landmark_role = ax::mojom::blink::Role::kBanner;
} else if (!ui::IsLandmark(node_data->role)) {
// Landmarks are the only roles exposed in xml-roles, matching Firefox.
return g_null_atom;
}
return ARIARoleName(landmark_role);
}
void AXObject::SerializeMarkerAttributes(ui::AXNodeData* node_data) const {
// Implemented in subclasses.
}
void AXObject::SerializeImageDataAttributes(ui::AXNodeData* node_data) const {
if (AXObjectID() != AXObjectCache().image_data_node_id()) {
return;
}
// In general, string attributes should be truncated using
// TruncateAndAddStringAttribute, but ImageDataUrl contains a data url
// representing an image, so add it directly using AddStringAttribute.
node_data->AddStringAttribute(
ax::mojom::blink::StringAttribute::kImageDataUrl,
ImageDataUrl(AXObjectCache().max_image_data_size()).Utf8());
}
void AXObject::SerializeTextInsertionDeletionOffsetAttributes(
ui::AXNodeData* node_data) const {
if (!IsEditable()) {
return;
}
WTF::Vector<TextChangedOperation>* offsets =
AXObjectCache().GetFromTextOperationInNodeIdMap(AXObjectID());
if (!offsets) {
return;
}
std::vector<int> start_offsets;
std::vector<int> end_offsets;
std::vector<int> start_anchor_ids;
std::vector<int> end_anchor_ids;
std::vector<int> operations_ints;
start_offsets.reserve(offsets->size());
end_offsets.reserve(offsets->size());
start_anchor_ids.reserve(offsets->size());
end_anchor_ids.reserve(offsets->size());
operations_ints.reserve(offsets->size());
for (auto operation : *offsets) {
start_offsets.push_back(operation.start);
end_offsets.push_back(operation.end);
start_anchor_ids.push_back(operation.start_anchor_id);
end_anchor_ids.push_back(operation.end_anchor_id);
operations_ints.push_back(static_cast<int>(operation.op));
}
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kTextOperationStartOffsets,
start_offsets);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kTextOperationEndOffsets,
end_offsets);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kTextOperationStartAnchorIds,
start_anchor_ids);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kTextOperationEndAnchorIds,
end_anchor_ids);
node_data->AddIntListAttribute(
ax::mojom::blink::IntListAttribute::kTextOperations, operations_ints);
AXObjectCache().ClearTextOperationInNodeIdMap();
}
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::ComputeFinalRoleForSerialization() const {
// An SVG with no accessible children should be exposed as an image rather
// than a document. See https://github.com/w3c/svg-aam/issues/12.
// We do this check here for performance purposes: When
// AXLayoutObject::RoleFromLayoutObjectOrNode is called, that node's
// accessible children have not been calculated. Rather than force calculation
// there, wait until we have the full tree.
if (role_ == ax::mojom::blink::Role::kSvgRoot && !UnignoredChildCount()) {
return ax::mojom::blink::Role::kImage;
}
// DPUB ARIA 1.1 deprecated doc-biblioentry and doc-endnote, but it's still
// possible to create these internal roles / platform mappings with a listitem
// (native or ARIA) inside of a doc-bibliography or doc-endnotes section.
if (role_ == ax::mojom::blink::Role::kListItem) {
AXObject* ancestor = CachedParentObject();
if (ancestor && ancestor->RoleValue() == ax::mojom::blink::Role::kList) {
// Go up to the root, or next list, checking to see if the list item is
// inside an endnote or bibliography section. If it is, remap the role.
// The remapping does not occur for list items multiple levels deep.
while (true) {
ancestor = ancestor->CachedParentObject();
if (!ancestor)
break;
ax::mojom::blink::Role ancestor_role = ancestor->RoleValue();
if (ancestor_role == ax::mojom::blink::Role::kList)
break;
if (ancestor_role == ax::mojom::blink::Role::kDocBibliography)
return ax::mojom::blink::Role::kDocBiblioEntry;
if (ancestor_role == ax::mojom::blink::Role::kDocEndnotes)
return ax::mojom::blink::Role::kDocEndnote;
}
}
}
if (role_ == ax::mojom::blink::Role::kHeader) {
if (IsDescendantOfLandmarkDisallowedElement()) {
return ax::mojom::blink::Role::kHeaderAsNonLandmark;
}
}
if (role_ == ax::mojom::blink::Role::kFooter) {
if (IsDescendantOfLandmarkDisallowedElement()) {
return ax::mojom::blink::Role::kFooterAsNonLandmark;
}
}
// An <aside> element should not be considered a landmark region
// if it is a child of a landmark disallowed element, UNLESS it has
// an accessible name.
if (role_ == ax::mojom::blink::Role::kComplementary) {
if (IsDescendantOfLandmarkDisallowedElement() &&
!IsNameFromAuthorAttribute()) {
return ax::mojom::blink::Role::kGenericContainer;
}
}
// TODO(accessibility): Consider moving the image vs. image map role logic
// here. Currently it is implemented in AXPlatformNode subclasses and thus
// not available to the InspectorAccessibilityAgent.
return role_;
}
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: