| /* |
| * 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: |
|