diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-09-03 13:32:17 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2021-10-01 14:31:55 +0200 |
commit | 21ba0c5d4bf8fba15dddd97cd693bad2358b77fd (patch) | |
tree | 91be119f694044dfc1ff9fdc054459e925de9df0 /chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc | |
parent | 03c549e0392f92c02536d3f86d5e1d8dfa3435ac (diff) | |
download | qtwebengine-chromium-21ba0c5d4bf8fba15dddd97cd693bad2358b77fd.tar.gz |
BASELINE: Update Chromium to 92.0.4515.166
Change-Id: I42a050486714e9e54fc271f2a8939223a02ae364
Diffstat (limited to 'chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc | 1386 |
1 files changed, 729 insertions, 657 deletions
diff --git a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc index d09d11af243..fa0a78a291f 100644 --- a/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc +++ b/chromium/third_party/blink/renderer/modules/accessibility/ax_object.cc @@ -29,6 +29,7 @@ #include "third_party/blink/renderer/modules/accessibility/ax_object.h" #include <algorithm> +#include <ostream> #include "base/strings/string_util.h" #include "build/build_config.h" #include "third_party/blink/public/common/input/web_menu_source_type.h" @@ -41,6 +42,7 @@ #include "third_party/blink/renderer/core/dom/events/simulated_click_options.h" #include "third_party/blink/renderer/core/dom/focus_params.h" #include "third_party/blink/renderer/core/dom/node_computed_style.h" +#include "third_party/blink/renderer/core/editing/editing_utilities.h" #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/frame/settings.h" @@ -48,11 +50,14 @@ #include "third_party/blink/renderer/core/html/custom/element_internals.h" #include "third_party/blink/renderer/core/html/forms/html_input_element.h" #include "third_party/blink/renderer/core/html/forms/html_select_element.h" +#include "third_party/blink/renderer/core/html/forms/html_text_area_element.h" #include "third_party/blink/renderer/core/html/forms/text_control_element.h" #include "third_party/blink/renderer/core/html/html_dialog_element.h" +#include "third_party/blink/renderer/core/html/html_element.h" #include "third_party/blink/renderer/core/html/html_frame_owner_element.h" #include "third_party/blink/renderer/core/html/html_head_element.h" #include "third_party/blink/renderer/core/html/html_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" @@ -73,6 +78,7 @@ #include "third_party/blink/renderer/core/svg/svg_element.h" #include "third_party/blink/renderer/core/svg/svg_g_element.h" #include "third_party/blink/renderer/core/svg/svg_style_element.h" +#include "third_party/blink/renderer/modules/accessibility/ax_image_map_link.h" #include "third_party/blink/renderer/modules/accessibility/ax_menu_list.h" #include "third_party/blink/renderer/modules/accessibility/ax_menu_list_option.h" #include "third_party/blink/renderer/modules/accessibility/ax_menu_list_popup.h" @@ -109,8 +115,6 @@ String IgnoredReasonName(AXIgnoredReason reason) { return "activeModalDialog"; case kAXAriaModalDialog: return "activeAriaModalDialog"; - case kAXAncestorIsLeafNode: - return "ancestorIsLeafNode"; case kAXAriaHiddenElement: return "ariaHiddenElement"; case kAXAriaHiddenSubtree: @@ -123,8 +127,6 @@ String IgnoredReasonName(AXIgnoredReason reason) { return "inertElement"; case kAXInertSubtree: return "inertSubtree"; - case kAXInheritsPresentation: - return "inheritsPresentation"; case kAXLabelContainer: return "labelContainer"; case kAXLabelFor: @@ -159,7 +161,15 @@ String GetIgnoredReasonsDebugString(AXObject::IgnoredReasons& reasons) { #endif -String GetElementString(Element* element) { +String GetNodeString(Node* node) { + if (node->IsTextNode()) { + String string_builder = "\""; + string_builder = string_builder + node->nodeValue(); + string_builder = string_builder + "\""; + return string_builder; + } + + Element* element = DynamicTo<Element>(node); if (!element) return "<null>"; @@ -183,14 +193,18 @@ Node* GetParentNodeForComputeParent(Node* node) { if (!node) return nullptr; - // Prefer LayoutTreeBuilderTraversal::Parent(), which handles pseudo content. - Node* parent = LayoutTreeBuilderTraversal::Parent(*node); - if (parent) - return parent; - - // Unfortunately, LayoutTreeBuilderTraversal::Parent() can return nullptr for - // a text node, such as inside a text area. Fall back on DOM parentNode(). - return node->parentNode(); + // 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. + // TODO(aleventhal) Remove this method / inline, if proven to be this simple. + return LayoutTreeBuilderTraversal::Parent(*node); } #if DCHECK_IS_ON() @@ -202,7 +216,6 @@ bool IsValidRole(ax::mojom::blink::Role role) { case ax::mojom::blink::Role::kColumn: case ax::mojom::blink::Role::kDesktop: case ax::mojom::blink::Role::kKeyboard: - case ax::mojom::blink::Role::kIgnored: case ax::mojom::blink::Role::kImeCandidate: case ax::mojom::blink::Role::kListGrid: case ax::mojom::blink::Role::kPane: @@ -227,6 +240,9 @@ struct RoleHashTraits : HashTraits<ax::mojom::blink::Role> { } }; +constexpr wtf_size_t kNumRoles = + static_cast<wtf_size_t>(ax::mojom::blink::Role::kMaxValue) + 1; + using ARIARoleMap = HashMap<String, ax::mojom::blink::Role, CaseFoldingHash, @@ -234,12 +250,16 @@ using ARIARoleMap = HashMap<String, RoleHashTraits>; struct RoleEntry { - const char* aria_role; - ax::mojom::blink::Role webcore_role; + const char* role_name; + ax::mojom::blink::Role role; }; // Mapping of ARIA role name to internal role name. -const RoleEntry kRoles[] = { +// 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}, @@ -342,16 +362,17 @@ const RoleEntry kRoles[] = { {"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}, - {"presentation", ax::mojom::blink::Role::kPresentational}, {"progressbar", ax::mojom::blink::Role::kProgressIndicator}, {"radio", ax::mojom::blink::Role::kRadioButton}, {"radiogroup", ax::mojom::blink::Role::kRadioGroup}, - // TODO(accessibility) region should only be mapped - // if name present. See http://crbug.com/840819. {"region", ax::mojom::blink::Role::kRegion}, {"row", ax::mojom::blink::Role::kRow}, {"rowgroup", ax::mojom::blink::Role::kRowGroup}, @@ -371,7 +392,6 @@ const RoleEntry kRoles[] = { {"tablist", ax::mojom::blink::Role::kTabList}, {"tabpanel", ax::mojom::blink::Role::kTabPanel}, {"term", ax::mojom::blink::Role::kTerm}, - {"text", ax::mojom::blink::Role::kStaticText}, {"textbox", ax::mojom::blink::Role::kTextField}, {"time", ax::mojom::blink::Role::kTime}, {"timer", ax::mojom::blink::Role::kTimer}, @@ -381,316 +401,49 @@ const RoleEntry kRoles[] = { {"treegrid", ax::mojom::blink::Role::kTreeGrid}, {"treeitem", ax::mojom::blink::Role::kTreeItem}}; -struct InternalRoleEntry { - ax::mojom::blink::Role webcore_role; - const char* internal_role_name; -}; - -const InternalRoleEntry kInternalRoles[] = { - {ax::mojom::blink::Role::kNone, "None"}, - {ax::mojom::blink::Role::kAbbr, "Abbr"}, - {ax::mojom::blink::Role::kAlertDialog, "AlertDialog"}, - {ax::mojom::blink::Role::kAlert, "Alert"}, - {ax::mojom::blink::Role::kAnchor, "Anchor"}, - {ax::mojom::blink::Role::kComment, "Comment"}, - {ax::mojom::blink::Role::kApplication, "Application"}, - {ax::mojom::blink::Role::kArticle, "Article"}, - {ax::mojom::blink::Role::kAudio, "Audio"}, - {ax::mojom::blink::Role::kBanner, "Banner"}, - {ax::mojom::blink::Role::kBlockquote, "Blockquote"}, - {ax::mojom::blink::Role::kButton, "Button"}, - {ax::mojom::blink::Role::kCanvas, "Canvas"}, - {ax::mojom::blink::Role::kCaption, "Caption"}, - {ax::mojom::blink::Role::kCaret, "Caret"}, - {ax::mojom::blink::Role::kCell, "Cell"}, - {ax::mojom::blink::Role::kCheckBox, "CheckBox"}, - {ax::mojom::blink::Role::kClient, "Client"}, - {ax::mojom::blink::Role::kCode, "Code"}, - {ax::mojom::blink::Role::kColorWell, "ColorWell"}, - {ax::mojom::blink::Role::kColumnHeader, "ColumnHeader"}, - {ax::mojom::blink::Role::kColumn, "Column"}, - {ax::mojom::blink::Role::kComboBoxGrouping, "ComboBox"}, - {ax::mojom::blink::Role::kComboBoxMenuButton, "ComboBox"}, - {ax::mojom::blink::Role::kComplementary, "Complementary"}, - {ax::mojom::blink::Role::kContentDeletion, "ContentDeletion"}, - {ax::mojom::blink::Role::kContentInsertion, "ContentInsertion"}, - {ax::mojom::blink::Role::kContentInfo, "ContentInfo"}, - {ax::mojom::blink::Role::kDate, "Date"}, - {ax::mojom::blink::Role::kDateTime, "DateTime"}, - {ax::mojom::blink::Role::kDefinition, "Definition"}, - {ax::mojom::blink::Role::kDescriptionListDetail, "DescriptionListDetail"}, - {ax::mojom::blink::Role::kDescriptionList, "DescriptionList"}, - {ax::mojom::blink::Role::kDescriptionListTerm, "DescriptionListTerm"}, - {ax::mojom::blink::Role::kDesktop, "Desktop"}, - {ax::mojom::blink::Role::kDetails, "Details"}, - {ax::mojom::blink::Role::kDialog, "Dialog"}, - {ax::mojom::blink::Role::kDirectory, "Directory"}, - {ax::mojom::blink::Role::kDisclosureTriangle, "DisclosureTriangle"}, - // -------------------------------------------------------------- - // DPub Roles: - // https://www.w3.org/TR/dpub-aam-1.0/#mapping_role_table - {ax::mojom::blink::Role::kDocAbstract, "DocAbstract"}, - {ax::mojom::blink::Role::kDocAcknowledgments, "DocAcknowledgments"}, - {ax::mojom::blink::Role::kDocAfterword, "DocAfterword"}, - {ax::mojom::blink::Role::kDocAppendix, "DocAppendix"}, - {ax::mojom::blink::Role::kDocBackLink, "DocBackLink"}, - {ax::mojom::blink::Role::kDocBiblioEntry, "DocBiblioentry"}, - {ax::mojom::blink::Role::kDocBibliography, "DocBibliography"}, - {ax::mojom::blink::Role::kDocBiblioRef, "DocBiblioref"}, - {ax::mojom::blink::Role::kDocChapter, "DocChapter"}, - {ax::mojom::blink::Role::kDocColophon, "DocColophon"}, - {ax::mojom::blink::Role::kDocConclusion, "DocConclusion"}, - {ax::mojom::blink::Role::kDocCover, "DocCover"}, - {ax::mojom::blink::Role::kDocCredit, "DocCredit"}, - {ax::mojom::blink::Role::kDocCredits, "DocCredits"}, - {ax::mojom::blink::Role::kDocDedication, "DocDedication"}, - {ax::mojom::blink::Role::kDocEndnote, "DocEndnote"}, - {ax::mojom::blink::Role::kDocEndnotes, "DocEndnotes"}, - {ax::mojom::blink::Role::kDocEpigraph, "DocEpigraph"}, - {ax::mojom::blink::Role::kDocEpilogue, "DocEpilogue"}, - {ax::mojom::blink::Role::kDocErrata, "DocErrata"}, - {ax::mojom::blink::Role::kDocExample, "DocExample"}, - {ax::mojom::blink::Role::kDocFootnote, "DocFootnote"}, - {ax::mojom::blink::Role::kDocForeword, "DocForeword"}, - {ax::mojom::blink::Role::kDocGlossary, "DocGlossary"}, - {ax::mojom::blink::Role::kDocGlossRef, "DocGlossref"}, - {ax::mojom::blink::Role::kDocIndex, "DocIndex"}, - {ax::mojom::blink::Role::kDocIntroduction, "DocIntroduction"}, - {ax::mojom::blink::Role::kDocNoteRef, "DocNoteref"}, - {ax::mojom::blink::Role::kDocNotice, "DocNotice"}, - {ax::mojom::blink::Role::kDocPageBreak, "DocPagebreak"}, - {ax::mojom::blink::Role::kDocPageFooter, "DocPageFooter"}, - {ax::mojom::blink::Role::kDocPageHeader, "DocPageHeader"}, - {ax::mojom::blink::Role::kDocPageList, "DocPagelist"}, - {ax::mojom::blink::Role::kDocPart, "DocPart"}, - {ax::mojom::blink::Role::kDocPreface, "DocPreface"}, - {ax::mojom::blink::Role::kDocPrologue, "DocPrologue"}, - {ax::mojom::blink::Role::kDocPullquote, "DocPullquote"}, - {ax::mojom::blink::Role::kDocQna, "DocQna"}, - {ax::mojom::blink::Role::kDocSubtitle, "DocSubtitle"}, - {ax::mojom::blink::Role::kDocTip, "DocTip"}, - {ax::mojom::blink::Role::kDocToc, "DocToc"}, - // End DPub roles. - // -------------------------------------------------------------- - {ax::mojom::blink::Role::kDocument, "Document"}, - {ax::mojom::blink::Role::kEmbeddedObject, "EmbeddedObject"}, - {ax::mojom::blink::Role::kEmphasis, "Emphasis"}, - {ax::mojom::blink::Role::kFeed, "feed"}, - {ax::mojom::blink::Role::kFigcaption, "Figcaption"}, - {ax::mojom::blink::Role::kFigure, "Figure"}, - {ax::mojom::blink::Role::kFooter, "Footer"}, - {ax::mojom::blink::Role::kFooterAsNonLandmark, "FooterAsNonLandmark"}, - {ax::mojom::blink::Role::kForm, "Form"}, - {ax::mojom::blink::Role::kGenericContainer, "GenericContainer"}, - // -------------------------------------------------------------- - // ARIA Graphics module roles: - // https://rawgit.com/w3c/graphics-aam/master/#mapping_role_table - {ax::mojom::blink::Role::kGraphicsDocument, "GraphicsDocument"}, - {ax::mojom::blink::Role::kGraphicsObject, "GraphicsObject"}, - {ax::mojom::blink::Role::kGraphicsSymbol, "GraphicsSymbol"}, - // End ARIA Graphics module roles. - // -------------------------------------------------------------- - {ax::mojom::blink::Role::kGrid, "Grid"}, - {ax::mojom::blink::Role::kGroup, "Group"}, - {ax::mojom::blink::Role::kHeader, "Header"}, - {ax::mojom::blink::Role::kHeaderAsNonLandmark, "HeaderAsNonLandmark"}, - {ax::mojom::blink::Role::kHeading, "Heading"}, - {ax::mojom::blink::Role::kIframePresentational, "IframePresentational"}, - {ax::mojom::blink::Role::kIframe, "Iframe"}, - {ax::mojom::blink::Role::kIgnored, "Ignored"}, - {ax::mojom::blink::Role::kImage, "Image"}, - {ax::mojom::blink::Role::kImeCandidate, "ImeCandidate"}, - {ax::mojom::blink::Role::kInlineTextBox, "InlineTextBox"}, - {ax::mojom::blink::Role::kInputTime, "InputTime"}, - {ax::mojom::blink::Role::kKeyboard, "Keyboard"}, - {ax::mojom::blink::Role::kLabelText, "Label"}, - {ax::mojom::blink::Role::kLayoutTable, "LayoutTable"}, - {ax::mojom::blink::Role::kLayoutTableCell, "LayoutCellTable"}, - {ax::mojom::blink::Role::kLayoutTableRow, "LayoutRowTable"}, - {ax::mojom::blink::Role::kLegend, "Legend"}, - {ax::mojom::blink::Role::kLink, "Link"}, - {ax::mojom::blink::Role::kLineBreak, "LineBreak"}, - {ax::mojom::blink::Role::kListBox, "ListBox"}, - {ax::mojom::blink::Role::kListBoxOption, "ListBoxOption"}, - {ax::mojom::blink::Role::kListGrid, "ListGrid"}, - {ax::mojom::blink::Role::kListItem, "ListItem"}, - {ax::mojom::blink::Role::kListMarker, "ListMarker"}, - {ax::mojom::blink::Role::kList, "List"}, - {ax::mojom::blink::Role::kLog, "Log"}, - {ax::mojom::blink::Role::kMain, "Main"}, - {ax::mojom::blink::Role::kMark, "Mark"}, - {ax::mojom::blink::Role::kMarquee, "Marquee"}, - {ax::mojom::blink::Role::kMath, "Math"}, - {ax::mojom::blink::Role::kMenuBar, "MenuBar"}, - {ax::mojom::blink::Role::kMenuItem, "MenuItem"}, - {ax::mojom::blink::Role::kMenuItemCheckBox, "MenuItemCheckBox"}, - {ax::mojom::blink::Role::kMenuItemRadio, "MenuItemRadio"}, - {ax::mojom::blink::Role::kMenuListOption, "MenuListOption"}, - {ax::mojom::blink::Role::kMenuListPopup, "MenuListPopup"}, - {ax::mojom::blink::Role::kMenu, "Menu"}, - {ax::mojom::blink::Role::kMeter, "Meter"}, - {ax::mojom::blink::Role::kNavigation, "Navigation"}, - {ax::mojom::blink::Role::kNote, "Note"}, - {ax::mojom::blink::Role::kPane, "Pane"}, - {ax::mojom::blink::Role::kParagraph, "Paragraph"}, - {ax::mojom::blink::Role::kPdfActionableHighlight, "PdfActionableHighlight"}, - {ax::mojom::blink::Role::kPdfRoot, "PdfRoot"}, - {ax::mojom::blink::Role::kPluginObject, "PluginObject"}, - {ax::mojom::blink::Role::kPopUpButton, "PopUpButton"}, - {ax::mojom::blink::Role::kPortal, "Portal"}, - {ax::mojom::blink::Role::kPre, "Pre"}, - {ax::mojom::blink::Role::kPresentational, "Presentational"}, - {ax::mojom::blink::Role::kProgressIndicator, "ProgressIndicator"}, - {ax::mojom::blink::Role::kRadioButton, "RadioButton"}, - {ax::mojom::blink::Role::kRadioGroup, "RadioGroup"}, - {ax::mojom::blink::Role::kRegion, "Region"}, - {ax::mojom::blink::Role::kRootWebArea, "WebArea"}, - {ax::mojom::blink::Role::kRow, "Row"}, - {ax::mojom::blink::Role::kRowGroup, "RowGroup"}, - {ax::mojom::blink::Role::kRowHeader, "RowHeader"}, - {ax::mojom::blink::Role::kRuby, "Ruby"}, - {ax::mojom::blink::Role::kRubyAnnotation, "RubyAnnotation"}, - {ax::mojom::blink::Role::kSection, "Section"}, - {ax::mojom::blink::Role::kSvgRoot, "SVGRoot"}, - {ax::mojom::blink::Role::kScrollBar, "ScrollBar"}, - {ax::mojom::blink::Role::kScrollView, "ScrollView"}, - {ax::mojom::blink::Role::kSearch, "Search"}, - {ax::mojom::blink::Role::kSearchBox, "SearchBox"}, - {ax::mojom::blink::Role::kSlider, "Slider"}, - {ax::mojom::blink::Role::kSpinButton, "SpinButton"}, - {ax::mojom::blink::Role::kSplitter, "Splitter"}, - {ax::mojom::blink::Role::kStaticText, "StaticText"}, - {ax::mojom::blink::Role::kStatus, "Status"}, - {ax::mojom::blink::Role::kStrong, "Strong"}, - {ax::mojom::blink::Role::kSuggestion, "Suggestion"}, - {ax::mojom::blink::Role::kSwitch, "Switch"}, - {ax::mojom::blink::Role::kTab, "Tab"}, - {ax::mojom::blink::Role::kTabList, "TabList"}, - {ax::mojom::blink::Role::kTabPanel, "TabPanel"}, - {ax::mojom::blink::Role::kTable, "Table"}, - {ax::mojom::blink::Role::kTableHeaderContainer, "TableHeaderContainer"}, - {ax::mojom::blink::Role::kTerm, "Term"}, - {ax::mojom::blink::Role::kTextField, "TextField"}, - {ax::mojom::blink::Role::kTextFieldWithComboBox, "ComboBox"}, - {ax::mojom::blink::Role::kTime, "Time"}, - {ax::mojom::blink::Role::kTimer, "Timer"}, - {ax::mojom::blink::Role::kTitleBar, "TitleBar"}, - {ax::mojom::blink::Role::kToggleButton, "ToggleButton"}, - {ax::mojom::blink::Role::kToolbar, "Toolbar"}, - {ax::mojom::blink::Role::kTreeGrid, "TreeGrid"}, - {ax::mojom::blink::Role::kTreeItem, "TreeItem"}, - {ax::mojom::blink::Role::kTree, "Tree"}, - {ax::mojom::blink::Role::kTooltip, "UserInterfaceTooltip"}, - {ax::mojom::blink::Role::kUnknown, "Unknown"}, - {ax::mojom::blink::Role::kVideo, "Video"}, - {ax::mojom::blink::Role::kWebView, "WebView"}, - {ax::mojom::blink::Role::kWindow, "Window"}}; - -static_assert(base::size(kInternalRoles) == - static_cast<size_t>(ax::mojom::blink::Role::kMaxValue) + 1, - "Not all internal roles have an entry in internalRoles array"); - -// Roles which we need to map in the other direction +// More friendly names for debugging. These are roles which don't map from +// the ARIA role name to the internal role when building the tree, but when +// debugging, we want to show the ARIA role name, since it is close in meaning. const RoleEntry kReverseRoles[] = { {"banner", ax::mojom::blink::Role::kHeader}, {"button", ax::mojom::blink::Role::kToggleButton}, {"combobox", ax::mojom::blink::Role::kPopUpButton}, {"contentinfo", ax::mojom::blink::Role::kFooter}, {"menuitem", ax::mojom::blink::Role::kMenuListOption}, - {"progressbar", ax::mojom::blink::Role::kMeter}, - {"region", ax::mojom::blink::Role::kSection}, - {"textbox", ax::mojom::blink::Role::kTextField}, {"combobox", ax::mojom::blink::Role::kComboBoxMenuButton}, {"combobox", ax::mojom::blink::Role::kTextFieldWithComboBox}}; static ARIARoleMap* CreateARIARoleMap() { ARIARoleMap* role_map = new ARIARoleMap; - for (size_t i = 0; i < base::size(kRoles); ++i) - role_map->Set(String(kRoles[i].aria_role), kRoles[i].webcore_role); + for (auto aria_role : kAriaRoles) + role_map->Set(String(aria_role.role_name), aria_role.role); return role_map; } -static Vector<AtomicString>* CreateRoleNameVector() { - Vector<AtomicString>* role_name_vector = - new Vector<AtomicString>(base::size(kInternalRoles)); - for (wtf_size_t i = 0; i < base::size(kInternalRoles); i++) - (*role_name_vector)[i] = g_null_atom; +// 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 (wtf_size_t i = 0; i < base::size(kRoles); ++i) { - (*role_name_vector)[static_cast<wtf_size_t>(kRoles[i].webcore_role)] = - AtomicString(kRoles[i].aria_role); + for (auto aria_role : kAriaRoles) { + (*role_name_vector)[static_cast<wtf_size_t>(aria_role.role)] = + AtomicString(aria_role.role_name); } - for (wtf_size_t i = 0; i < base::size(kReverseRoles); ++i) { - (*role_name_vector)[static_cast<wtf_size_t>( - kReverseRoles[i].webcore_role)] = - AtomicString(kReverseRoles[i].aria_role); + 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; } -static Vector<AtomicString>* CreateInternalRoleNameVector() { - Vector<AtomicString>* internal_role_name_vector = - new Vector<AtomicString>(base::size(kInternalRoles)); - for (wtf_size_t i = 0; i < base::size(kInternalRoles); i++) { - (*internal_role_name_vector)[static_cast<wtf_size_t>( - kInternalRoles[i].webcore_role)] = - AtomicString(kInternalRoles[i].internal_role_name); - } - - return internal_role_name_vector; -} - HTMLDialogElement* GetActiveDialogElement(Node* node) { return node->GetDocument().ActiveModalDialog(); } -// TODO(dmazzoni): replace this with a call to RoleName(). -std::string GetEquivalentAriaRoleString(const ax::mojom::blink::Role role) { - switch (role) { - case ax::mojom::blink::Role::kArticle: - return "article"; - case ax::mojom::blink::Role::kBanner: - return "banner"; - case ax::mojom::blink::Role::kButton: - return "button"; - case ax::mojom::blink::Role::kComplementary: - return "complementary"; - case ax::mojom::blink::Role::kFigure: - return "figure"; - case ax::mojom::blink::Role::kFooter: - return "contentinfo"; - case ax::mojom::blink::Role::kHeader: - return "banner"; - case ax::mojom::blink::Role::kHeading: - return "heading"; - case ax::mojom::blink::Role::kImage: - return "img"; - case ax::mojom::blink::Role::kMain: - return "main"; - case ax::mojom::blink::Role::kNavigation: - return "navigation"; - case ax::mojom::blink::Role::kRadioButton: - return "radio"; - case ax::mojom::blink::Role::kRegion: - return "region"; - case ax::mojom::blink::Role::kSection: - // A <section> element uses the 'region' ARIA role mapping. - return "region"; - case ax::mojom::blink::Role::kSlider: - return "slider"; - case ax::mojom::blink::Role::kTime: - return "time"; - default: - break; - } - - return std::string(); -} - } // namespace int32_t ToAXMarkerType(DocumentMarker::MarkerType marker_type) { @@ -720,6 +473,8 @@ int32_t ToAXMarkerType(DocumentMarker::MarkerType marker_type) { return static_cast<int32_t>(result); } +// static +bool AXObject::is_loading_inline_boxes_ = false; unsigned AXObject::number_of_live_ax_objects_ = 0; AXObject::AXObject(AXObjectCacheImpl& ax_object_cache) @@ -731,10 +486,7 @@ AXObject::AXObject(AXObjectCacheImpl& ax_object_cache) cached_is_ignored_(false), cached_is_ignored_but_included_in_tree_(false), cached_is_inert_or_aria_hidden_(false), - cached_is_descendant_of_leaf_node_(false), cached_is_descendant_of_disabled_node_(false), - cached_has_inherited_presentational_role_(false), - cached_is_editable_root_(false), cached_live_region_root_(nullptr), cached_aria_column_index_(0), cached_aria_row_index_(0), @@ -747,9 +499,12 @@ AXObject::~AXObject() { --number_of_live_ax_objects_; } -void AXObject::Init(AXObject* parent_if_known) { +void AXObject::Init(AXObject* parent) { #if DCHECK_IS_ON() - DCHECK(!parent_); + DCHECK(!parent_) << "Should not already have a cached parent:" + << "\n* Child = " << GetNode() << " / " << GetLayoutObject() + << "\n* Parent = " << parent_->ToString(true, true) + << "\n* Equal to passed-in parent? " << (parent == parent_); DCHECK(!is_initializing_); base::AutoReset<bool> reentrancy_protector(&is_initializing_, true); #endif // DCHECK_IS_ON() @@ -766,24 +521,42 @@ void AXObject::Init(AXObject* parent_if_known) { // Determine the parent as soon as possible. // Every AXObject must have a parent unless it's the root. - SetParent(parent_if_known ? parent_if_known : ComputeParent()); - DCHECK(parent_ || IsA<Document>(GetNode())) + SetParent(parent); + DCHECK(parent_ || IsRoot()) << "The following node should have a parent: " << GetNode(); - SetNeedsToUpdateChildren(); // Should be called after role_ is set. + // The parent cannot have children. This object must be destroyed. + DCHECK(!parent_ || parent_->CanHaveChildren()) + << "Tried to set a parent that cannot have children:" + << "\n* Parent = " << parent_->ToString(true, true) + << "\n* Child = " << ToString(true, true); + + // This is one after the role_ is computed, because the role is used to + // determine whether an AXObject can have children. + children_dirty_ = CanHaveChildren(); + + // Ensure that the aria-owns relationship is set before attempting + // to update cached attribute values. + if (GetNode()) + AXObjectCache().MaybeNewRelationTarget(*GetNode(), this); + UpdateCachedAttributeValuesIfNeeded(false); } void AXObject::Detach() { -#if DCHECK_IS_ON() - // 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. + // 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; } - DCHECK(!is_adding_children_) << ToString(true, true); + +#if DCHECK_IS_ON() DCHECK(ax_object_cache_); DCHECK(!ax_object_cache_->IsFrozen()) << "Do not detach children while the tree is frozen, in order to avoid " @@ -791,6 +564,15 @@ void AXObject::Detach() { "accessibility properties."; #endif +#if defined(AX_FAIL_FAST_BUILD) + SANITIZER_CHECK(!is_adding_children_) << ToString(true, true); +#endif + + CHECK(!is_loading_inline_boxes_) + << "Should not be attempting to detach object while in the middle of " + "recursively loading inline text boxes: " + << ToString(true, true); + // Clear any children and call DetachFromParent() on them so that // no children are left with dangling pointers to their parent. ClearChildren(); @@ -804,12 +586,35 @@ bool AXObject::IsDetached() const { return !ax_object_cache_; } -void AXObject::SetParent(AXObject* new_parent) { - DCHECK(new_parent || IsA<Document>(GetNode())) - << "Parent cannot be null, except at the root, was null at " << GetNode() - << " " << GetLayoutObject(); +bool AXObject::IsRoot() const { + return GetNode() && GetNode() == &AXObjectCache().GetDocument(); +} +void AXObject::SetParent(AXObject* new_parent) const { #if DCHECK_IS_ON() + if (!new_parent && !IsRoot()) { + std::ostringstream message; + message << "Parent cannot be null, except at the root. " + "Parent chain from DOM, starting at |this|:"; + int count = 0; + for (Node* node = GetNode(); node; + node = GetParentNodeForComputeParent(node)) { + message << "\n" + << (++count) << ". " << node + << "\n LayoutObject=" << node->GetLayoutObject(); + if (AXObject* obj = AXObjectCache().Get(node)) + message << "\n " << obj->ToString(true, true); + } + NOTREACHED() << message.str(); + } + + if (new_parent) { + DCHECK(!new_parent->IsDetached()) + << "Cannot set parent to a detached object:" + << "\n* Child: " << ToString(true, true) + << "\n* New parent: " << new_parent->ToString(true, true); + } + // Check to ensure that if the parent is changing from a previous parent, // that |this| is not still a child of that one. // This is similar to the IsParentUnignoredOf() check in @@ -832,99 +637,177 @@ void AXObject::SetParent(AXObject* new_parent) { parent_ = new_parent; } +bool AXObject::IsMissingParent() const { + if (!parent_) + return !IsRoot(); + + if (parent_->IsDetached()) + return true; + + return false; +} + +void AXObject::RepairMissingParent() const { + DCHECK(IsMissingParent()); + + SetParent(ComputeParent()); +} + // In many cases, ComputeParent() is not called, because the parent adding -// the child will pass itself into AXObject::Init() via parent_if_known. +// 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 { - DCHECK(!IsDetached()); +#if defined(AX_FAIL_FAST_BUILD) + SANITIZER_CHECK(!IsDetached()); - DCHECK(!parent_ || parent_->IsDetached()) - << "Should use cached parent unless it's detached, and should not " - "attempt to recompute it, occurred on " - << GetNode(); + SANITIZER_CHECK(!IsVirtualObject()) + << "A virtual object must have a parent, and cannot exist without one. " + "The parent is set when the object is constructed."; - if (!GetNode() && !GetLayoutObject()) { - NOTREACHED() << "Can't compute parent on AXObjects without a backing Node " - "or LayoutObject. Objects without those must set the " - "parent in Init(), |this| = " - << RoleValue(); - return nullptr; - } + SANITIZER_CHECK(!IsMockObject()) + << "A mock object must have a parent, and cannot exist without one. " + "The parent is set when the object is constructed."; - return ComputeParentImpl(); -} + SANITIZER_CHECK(GetNode() || GetLayoutObject()) + << "Can't compute parent on AXObjects without a backing Node " + "or LayoutObject. Objects without those must set the " + "parent in Init(), |this| = " + << RoleValue(); +#endif -AXObject* AXObject::ComputeParentImpl() const { - DCHECK(!IsDetached()); + AXObject* ax_parent = + AXObjectCache().IsAriaOwned(this) + ? AXObjectCache().GetAriaOwnedParent(this) + : ComputeNonARIAParent(AXObjectCache(), GetNode(), GetLayoutObject()); - if (AXObjectCache().IsAriaOwned(this)) - return AXObjectCache().GetAriaOwnedParent(this); + CHECK(!ax_parent || !ax_parent->IsDetached()) + << "Computed parent should never be detached:" + << "\n* Child: " << GetNode() + << "\n* Parent: " << ax_parent->ToString(true, true); - Node* current_node = GetNode(); + return ax_parent; +} - // A WebArea's parent should be the page popup owner, if any, otherwise null. - if (IsA<Document>(current_node)) { - LocalFrame* frame = GetLayoutObject()->GetFrame(); - return AXObjectCache().GetOrCreate(frame->PagePopupOwner()); +// static +bool AXObject::CanComputeAsNaturalParent(Node* node) { + // A <select> menulist that will use AXMenuList is not allowed. + if (AXObjectCacheImpl::UseAXMenuList()) { + if (auto* select = DynamicTo<HTMLSelectElement>(node)) { + if (select->UsesMenuList()) + return false; + } } - if (IsVirtualObject()) { - NOTREACHED() - << "A virtual object must have a parent, and cannot exist without one. " - "The parent is set when the object is constructed."; - return nullptr; + // A <br> can only support AXInlineTextBox children, which is never the result + // of a parent computation (the parent of the children is set at Init()). + if (IsA<HTMLBRElement>(node)) + return false; + + // Image map parent-child relationships (from image to area) must be retrieved + // manually via AXImageMapLink::GetAXObjectForImageMap(). + if (IsA<HTMLMapElement>(node) || IsA<HTMLAreaElement>(node) || + IsA<HTMLImageElement>(node)) { + return false; } - // If no node, or a pseudo element, use the layout parent. + return true; +} + +// static +AXObject* AXObject::ComputeNonARIAParent(AXObjectCacheImpl& cache, + Node* current_node, + LayoutObject* current_layout_obj) { + DCHECK(current_node || current_layout_obj) + << "Can't compute parent without a backing Node " + "or LayoutObject."; + + // If no node, use the layout parent. if (!current_node) { - LayoutObject* current_layout_obj = GetLayoutObject(); - if (!current_layout_obj) { - NOTREACHED() - << "Can't compute parent on AXObjects without a backing Node " - "or LayoutObject. Objects without those must set the " - "parent in Init(), |this| = " - << RoleValue(); - return nullptr; - } - // If no DOM node and no parent, this must be an anonymous layout object. + // If no DOM node, this is an anonymous layout object. DCHECK(current_layout_obj->IsAnonymous()); + // In accessibility, this only occurs for descendants of pseudo elements. + DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant( + *current_layout_obj)) + << "Attempt to get AX parent for irrelevant anonymous layout object: " + << current_layout_obj; LayoutObject* parent_layout_obj = current_layout_obj->Parent(); if (!parent_layout_obj) return nullptr; - if (AXObject* ax_parent = AXObjectCache().GetOrCreate(parent_layout_obj)) { - DCHECK(!ax_parent->IsDetached()); - return ax_parent; - } - // Switch to using DOM nodes. The only cases that should occur do not have - // chains of multiple parents without DOM nodes. Node* parent_node = parent_layout_obj->GetNode(); - DCHECK(parent_node) << "Computing an accessible parent from the layout " - "parent did not yield an accessible object nor a " - "DOM node to walk up from, current_layout_obj = " - << current_layout_obj; - if (!parent_node) + if (!CanComputeAsNaturalParent(parent_node)) return nullptr; + if (AXObject* ax_parent = cache.GetOrCreate(parent_layout_obj)) { + DCHECK(!ax_parent->IsDetached()); + DCHECK(ax_parent->ShouldUseLayoutObjectTraversalForChildren()) + << "Do not compute a parent that cannot have this as a child."; + return ax_parent->CanHaveChildren() ? ax_parent : nullptr; + } + return nullptr; } DCHECK(current_node->isConnected()) << "Should not call ComputeParent() with disconnected node: " << current_node; - while (true) { - current_node = GetParentNodeForComputeParent(current_node); - if (!current_node) - break; - AXObject* ax_parent = AXObjectCache().GetOrCreate(current_node); - if (ax_parent) { - DCHECK(!ax_parent->IsDetached()); - return ax_parent; + // A WebArea's parent should be the page popup owner, if any, otherwise null. + if (auto* document = DynamicTo<Document>(current_node)) { + LocalFrame* frame = document->GetFrame(); + DCHECK(frame); + return cache.GetOrCreate(frame->PagePopupOwner()); + } + + // For <option> in <select size=1>, return the popup. + if (AXObjectCacheImpl::UseAXMenuList()) { + if (auto* option = DynamicTo<HTMLOptionElement>(current_node)) { + if (AXObject* ax_select = + AXMenuListOption::ComputeParentAXMenuPopupFor(cache, option)) { + return ax_select; + } + } + } + + // For <area>, return the image it is a child link of. + if (IsA<HTMLAreaElement>(current_node)) { + if (AXObject* ax_image = + AXImageMapLink::GetAXObjectForImageMap(cache, current_node)) { + return ax_image; } } + Node* parent_node = GetParentNodeForComputeParent(current_node); + if (!parent_node) { + // This occurs when a DOM child isn't visited by LayoutTreeBuilderTraversal, + // such as an element child of a <textarea>, which only supports plain text. + return nullptr; + } + + // When the flag to use AXMenuList in on, a menu list is only allowed to + // parent an AXMenuListPopup, which is added as a child on creation. No other + // children are allowed, and nullptr is returned for anything else where the + // parent would be AXMenuList. + if (AXObjectCacheImpl::ShouldCreateAXMenuListFor( + parent_node->GetLayoutObject())) { + return nullptr; + } + + if (!CanComputeAsNaturalParent(parent_node)) + return nullptr; + + if (AXObject* ax_parent = cache.GetOrCreate(parent_node)) { + DCHECK(!ax_parent->IsDetached()); + // If the parent can't have children, then return null so that the caller + // knows that it is not a relevant natural parent, as it is a leaf. + return ax_parent->CanHaveChildren() ? ax_parent : nullptr; + } + + // Could not create AXObject for |parent_node|, therefore there is no relevant + // natural parent. For example, the AXObject that would have been created + // would have been a descendant of a leaf, or otherwise an illegal child of a + // specialized object. return nullptr; } @@ -935,6 +818,8 @@ void AXObject::EnsureCorrectParentComputation() { DCHECK(!parent_->IsDetached()); + DCHECK(parent_->CanHaveChildren()); + // Don't check the computed parent if the cached parent is a mock object. // It is expected that a computed parent could never be a mock object, // which has no backing DOM node or layout object, and therefore cannot be @@ -958,24 +843,28 @@ void AXObject::EnsureCorrectParentComputation() { if (GetNode() && GetNode()->IsPseudoElement()) return; - // Verify that the algorithm in ComputeParentImpl() provides same results as - // parents that init their children with themselves as the parent_if_known. - // Inconsistency indicates a problem could potentially exist where a child's - // parent does not include the child in its children. - DCHECK(ComputeParentImpl()) - << "Computed parent was null for " << this << ", expected " << parent_; - DCHECK_EQ(ComputeParentImpl(), parent_) + // Verify that the algorithm in ComputeParent() provides same results as + // parents that init their children with themselves as the parent. + // Inconsistency indicates a problem could potentially exist where a child's + // parent does not include the child in its children. +#if DCHECK_IS_ON() + AXObject* computed_parent = ComputeParent(); + + DCHECK(computed_parent) << "Computed parent was null for " << this + << ", expected " << parent_; + DCHECK_EQ(computed_parent, parent_) << "\n**** ComputeParent should have provided the same result as " "the known parent.\n**** Computed parent layout object was " - << ComputeParentImpl()->GetLayoutObject() + << computed_parent->GetLayoutObject() << "\n**** Actual parent's layout object was " << parent_->GetLayoutObject() << "\n**** Child was " << this; +#endif } #endif const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute( AOMStringProperty property) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return g_null_atom; @@ -984,7 +873,7 @@ const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute( Element* AXObject::GetAOMPropertyOrARIAAttribute( AOMRelationProperty property) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return nullptr; @@ -993,7 +882,7 @@ Element* AXObject::GetAOMPropertyOrARIAAttribute( bool AXObject::HasAOMProperty(AOMRelationListProperty property, HeapVector<Member<Element>>& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1003,7 +892,7 @@ bool AXObject::HasAOMProperty(AOMRelationListProperty property, bool AXObject::HasAOMPropertyOrARIAAttribute( AOMRelationListProperty property, HeapVector<Member<Element>>& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1012,7 +901,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute( bool AXObject::HasAOMPropertyOrARIAAttribute(AOMBooleanProperty property, bool& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1040,7 +929,7 @@ bool AXObject::AOMPropertyOrARIAAttributeIsFalse( bool AXObject::HasAOMPropertyOrARIAAttribute(AOMUIntProperty property, uint32_t& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1052,7 +941,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMUIntProperty property, bool AXObject::HasAOMPropertyOrARIAAttribute(AOMIntProperty property, int32_t& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1064,7 +953,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMIntProperty property, bool AXObject::HasAOMPropertyOrARIAAttribute(AOMFloatProperty property, float& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1076,7 +965,7 @@ bool AXObject::HasAOMPropertyOrARIAAttribute(AOMFloatProperty property, bool AXObject::HasAOMPropertyOrARIAAttribute(AOMStringProperty property, AtomicString& result) const { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return false; @@ -1118,7 +1007,7 @@ void AXObject::Serialize(ui::AXNodeData* node_data, node_data->AddState(ax::mojom::blink::State::kEditable); if (IsEditableRoot()) { node_data->AddBoolAttribute( - ax::mojom::blink::BoolAttribute::kEditableRoot, true); + ax::mojom::blink::BoolAttribute::kContentEditableRoot, true); } if (IsRichlyEditable()) node_data->AddState(ax::mojom::blink::State::kRichlyEditable); @@ -1227,7 +1116,7 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data, if (auto* html_frame_owner_element = DynamicTo<HTMLFrameOwnerElement>(GetElement())) { if (Frame* child_frame = html_frame_owner_element->ContentFrame()) { - base::Optional<base::UnguessableToken> child_token = + absl::optional<base::UnguessableToken> child_token = child_frame->GetEmbeddingToken(); if (child_token && !(IsDetached() || ChildCountIncludingIgnored())) { ui::AXTreeID child_tree_id = @@ -1259,6 +1148,14 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data, SerializeTableAttributes(node_data); } + if (accessibility_mode.has_mode(ui::AXMode::kScreenReader)) { + // Whether it has ARIA attributes at all. + if (HasAriaAttribute()) { + node_data->AddBoolAttribute( + ax::mojom::blink::BoolAttribute::kHasAriaAttribute, true); + } + } + if (accessibility_mode.has_mode(ui::AXMode::kPDF)) { // Return early. None of the following attributes are needed for PDFs. return; @@ -1295,21 +1192,7 @@ void AXObject::SerializeUnignoredAttributes(ui::AXNodeData* node_data, SerializeSparseAttributes(node_data); if (Element* element = GetElement()) { - if (const AtomicString& aria_role = - GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole)) { - TruncateAndAddStringAttribute(node_data, - ax::mojom::blink::StringAttribute::kRole, - aria_role.Utf8()); - } else { - std::string role_str = GetEquivalentAriaRoleString(RoleValue()); - if (!role_str.empty()) { - TruncateAndAddStringAttribute(node_data, - ax::mojom::blink::StringAttribute::kRole, - GetEquivalentAriaRoleString(RoleValue())); - } - } - - if (IsNativeTextField()) { + 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. @@ -1424,7 +1307,7 @@ void AXObject::SerializeScrollAttributes(ui::AXNodeData* node_data) { } void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) { - Element* element = this->GetElement(); + Element* element = GetElement(); if (!element) return; @@ -1440,10 +1323,11 @@ void AXObject::SerializeElementAttributes(ui::AXNodeData* node_data) { TruncateAndAddStringAttribute( node_data, ax::mojom::blink::StringAttribute::kRole, aria_role.Utf8()); } else { - std::string role = GetEquivalentAriaRoleString(RoleValue()); - if (!role.empty()) { - TruncateAndAddStringAttribute( - node_data, ax::mojom::blink::StringAttribute::kRole, role); + const AtomicString& role_str = GetEquivalentAriaRoleName(RoleValue()); + if (role_str != g_null_atom) { + TruncateAndAddStringAttribute(node_data, + ax::mojom::blink::StringAttribute::kRole, + role_str.Ascii()); } } } @@ -1740,7 +1624,7 @@ bool AXObject::IsAnchor() const { } bool AXObject::IsARIATextField() const { - if (IsNativeTextField()) + if (IsAtomicTextField()) return false; // Native role supercedes the ARIA one. return AriaRoleAttribute() == ax::mojom::blink::Role::kTextField || AriaRoleAttribute() == ax::mojom::blink::Role::kSearchBox || @@ -1755,19 +1639,6 @@ bool AXObject::IsCanvas() const { return RoleValue() == ax::mojom::blink::Role::kCanvas; } -bool AXObject::IsCheckboxOrRadio() 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: - return true; - default: - break; - } - return false; -} - bool AXObject::IsColorWell() const { return RoleValue() == ax::mojom::blink::Role::kColorWell; } @@ -1863,7 +1734,7 @@ ax::mojom::blink::CheckedState AXObject::CheckedState() const { // Native checked state if (role != ax::mojom::blink::Role::kToggleButton) { - const Node* node = this->GetNode(); + const Node* node = GetNode(); if (!node) return ax::mojom::blink::CheckedState::kNone; @@ -1957,15 +1828,15 @@ bool AXObject::IsNativeSpinButton() const { return false; } -bool AXObject::IsNativeTextField() const { +bool AXObject::IsAtomicTextField() const { return blink::IsTextControl(GetNode()); } -bool AXObject::IsNonNativeTextField() const { +bool AXObject::IsNonAtomicTextField() const { // Consivably, an <input type=text> or a <textarea> might also have the // contenteditable attribute applied. In such cases, the <input> or <textarea> // tags should supercede. - if (IsNativeTextField()) + if (IsAtomicTextField()) return false; return HasContentEditableAttributeSet() || IsARIATextField(); } @@ -2027,7 +1898,7 @@ bool AXObject::IsTabItem() const { bool AXObject::IsTextField() const { if (IsDetached()) return false; - return IsNativeTextField() || IsNonNativeTextField(); + return IsAtomicTextField() || IsNonAtomicTextField(); } bool AXObject::IsAutofillAvailable() const { @@ -2140,12 +2011,10 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded( DocumentLifecycle::kAfterPerformLayout) << "Unclean document at lifecycle " << GetDocument()->Lifecycle().ToString(); -#endif +#endif // DCHECK_IS_ON() - // TODO(accessibility) Every AXObject must have a parent except the root. - // Sometimes the parent is detached and a new parent isn't yet reattached. - if (!parent_) - parent_ = ComputeParent(); + if (IsMissingParent()) + RepairMissingParent(); cached_is_hidden_via_style = ComputeIsHiddenViaStyle(); @@ -2160,10 +2029,7 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded( SetNeedsToUpdateChildren(); cached_is_inert_or_aria_hidden_ = is_inert_or_aria_hidden; } - cached_is_descendant_of_leaf_node_ = !!LeafNodeAncestor(); cached_is_descendant_of_disabled_node_ = !!DisabledAncestor(); - cached_has_inherited_presentational_role_ = - !!InheritsPresentationalRoleFrom(); bool is_ignored = ComputeAccessibilityIsIgnored(); bool is_ignored_but_included_in_tree = @@ -2178,7 +2044,8 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded( DisplayLockUtilities::NearestLockedExclusiveAncestor(*GetNode())) { DCHECK(!cached_is_ignored_but_included_in_tree_) << "Display locked text should not be included in the tree (subject to " - "future rule change)"; + "future rule change): " + << ToString(true, true); } #endif bool included_in_tree_changed = false; @@ -2207,7 +2074,6 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded( cached_is_ignored_ = is_ignored; cached_is_ignored_but_included_in_tree_ = is_ignored_but_included_in_tree; - cached_is_editable_root_ = ComputeIsEditableRoot(); // Compute live region root, which can be from any ARIA live value, including // "off", or from an automatic ARIA live value, e.g. from role="status". // TODO(dmazzoni): remove this const_cast. @@ -2236,9 +2102,10 @@ void AXObject::UpdateCachedAttributeValuesIfNeeded( // "not handle a signal and call ChilldrenChanged() earlier." // << "\nChild: " << ToString(true) // << "\nParent: " << parent->ToString(true); - // Defer this ChildrenChanged(), otherwise it can cause reentry into + // Defers a ChildrenChanged() on the first included ancestor. + // Must defer it, otherwise it can cause reentry into // UpdateCachedAttributeValuesIfNeeded() on |this|. - AXObjectCache().ChildrenChanged(parent); + AXObjectCache().ChildrenChangedOnAncestorOf(const_cast<AXObject*>(this)); } } @@ -2371,22 +2238,6 @@ bool AXObject::IsVisible() const { return !IsInertOrAriaHidden() && !IsHiddenViaStyle(); } -bool AXObject::IsDescendantOfLeafNode() const { - UpdateCachedAttributeValuesIfNeeded(); - return cached_is_descendant_of_leaf_node_; -} - -AXObject* AXObject::LeafNodeAncestor() const { - if (AXObject* parent = ParentObject()) { - if (!parent->CanHaveChildren()) - return parent; - - return parent->LeafNodeAncestor(); - } - - return nullptr; -} - const AXObject* AXObject::AriaHiddenRoot() const { for (const AXObject* object = this; object; object = object->ParentObject()) { if (object->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden)) @@ -2554,28 +2405,81 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const { return true; } - // Include all pseudo element content. Any anonymous subtree is included - // from above, in the condition where there is no node. - if (node->IsPseudoElement()) + // Allow the browser side ax tree to access "visibility: [hidden|collapse]" + // and "display: none" nodes. This is useful for APIs that return the node + // referenced by aria-labeledby and aria-describedby. + // An element must have an id attribute or it cannot be referenced by + // aria-labelledby or aria-describedby. + if (RuntimeEnabledFeatures::AccessibilityExposeDisplayNoneEnabled()) { + if (Element* element = GetElement()) { + if (element->FastHasAttribute(html_names::kIdAttr) && + IsHiddenViaStyle()) { + return true; + } + } + } else if (GetLayoutObject()) { + if (GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible) + return true; + } + + // Allow the browser side ax tree to access "aria-hidden" nodes. + // This is useful for APIs that return the node referenced by + // aria-labeledby and aria-describedby. + if (GetLayoutObject() && IsAriaHidden()) return true; + // Custom elements and their children are included in the tree. // <slot>s and their children are included in the tree. - // TODO(accessibility) Consider including all shadow content; however, this - // can actually be a lot of nodes inside of a web component, e.g. svg. - if (IsA<HTMLSlotElement>(node)) + // This checks to see if this a child one of those. + if (Node* parent_node = LayoutTreeBuilderTraversal::Parent(*node)) { + if (parent_node->IsCustomElement() || IsA<HTMLSlotElement>(parent_node)) + return true; + } + + Element* element = GetElement(); + if (!element) + return false; + + // Custom elements and their children are included in the tree. + if (element->IsCustomElement()) + return true; + + // <slot>s and their children are included in the tree. + // Detailed explanation: + // <slot> elements are placeholders marking locations in a shadow tree where + // users of a web component can insert their own custom nodes. Inserted nodes + // (also known as distributed nodes) become children of their respective slots + // in the accessibility tree. In other words, the accessibility tree mirrors + // the flattened DOM tree or the layout tree, not the original DOM tree. + // Distributed nodes still maintain their parent relations and computed style + // information with their original location in the DOM. Therefore, we need to + // ensure that in the accessibility tree no remnant information from the + // unflattened DOM tree remains, such as the cached parent. + if (IsA<HTMLSlotElement>(element)) return true; - if (CachedParentObject() && - IsA<HTMLSlotElement>(CachedParentObject()->GetNode())) + + // Include all pseudo element content. Any anonymous subtree is included + // from above, in the condition where there is no node. + if (element->IsPseudoElement()) return true; - if (GetElement() && GetElement()->IsCustomElement()) + // Include all parents of ::before/::after/::marker pseudo elements to help + // ClearChildren() find all children, and assist naming computation. + // It is unnecessary to include a rule for other types of pseudo elements: + // Specifically, ::first-letter/::backdrop are not visited by + // LayoutTreeBuilderTraversal, and cannot be in the tree, therefore do not add + // a special rule to include their parents. + if (element->GetPseudoElement(kPseudoIdBefore) || + element->GetPseudoElement(kPseudoIdAfter) || + element->GetPseudoElement(kPseudoIdMarker)) { return true; + } // Use a flag to control whether or not the <html> element is included // in the accessibility tree. Either way it's always marked as "ignored", // but eventually we want to always include it in the tree to simplify // some logic. - if (IsA<HTMLHtmlElement>(node)) + if (IsA<HTMLHtmlElement>(element)) return RuntimeEnabledFeatures::AccessibilityExposeHTMLElementEnabled(); // Keep the internal accessibility tree consistent for videos which lack @@ -2590,41 +2494,24 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const { if (IsLineBreakingObject()) return true; - // Allow the browser side ax tree to access "visibility: [hidden|collapse]" - // and "display: none" nodes. This is useful for APIs that return the node - // referenced by aria-labeledby and aria-describedby. - // An element must have an id attribute or it cannot be referenced by - // aria-labelledby or aria-describedby. - if (RuntimeEnabledFeatures::AccessibilityExposeDisplayNoneEnabled()) { - if (Element* element = GetElement()) { - if (element->FastHasAttribute(html_names::kIdAttr) && - IsHiddenViaStyle()) { - return true; - } - } - } else if (GetLayoutObject()) { - if (GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible) - return true; - } - - // Allow the browser side ax tree to access "aria-hidden" nodes. - // This is useful for APIs that return the node referenced by - // aria-labeledby and aria-describedby. - if (GetLayoutObject() && IsAriaHidden()) - return true; - // Preserve SVG grouping elements. - if (IsA<SVGGElement>(node)) + if (IsA<SVGGElement>(element)) return true; // Keep table-related elements in the tree, because it's too easy for them // to in and out of being ignored based on their ancestry, as their role // can depend on several levels up in the hierarchy. - if (IsA<HTMLTableElement>(node) || IsA<HTMLTableSectionElement>(node) || - IsA<HTMLTableRowElement>(node) || IsA<HTMLTableCellElement>(node)) { + if (IsA<HTMLTableElement>(element) || IsA<HTMLTableSectionElement>(element) || + IsA<HTMLTableRowElement>(element) || IsA<HTMLTableCellElement>(element)) { return true; } + // Ensure clean teardown of AXMenuList. + if (auto* option = DynamicTo<HTMLOptionElement>(element)) { + if (option->OwnerSelectElement()) + return true; + } + // Preserve nodes with language attributes. if (HasAttribute(html_names::kLangAttr)) return true; @@ -2632,16 +2519,16 @@ bool AXObject::ComputeAccessibilityIsIgnoredButIncludedInTree() const { return false; } -const AXObject* AXObject::GetNativeTextControlAncestor( +const AXObject* AXObject::GetAtomicTextFieldAncestor( int max_levels_to_check) const { - if (IsNativeTextField()) + if (IsAtomicTextField()) return this; if (max_levels_to_check == 0) return nullptr; if (AXObject* parent = ParentObject()) - return parent->GetNativeTextControlAncestor(max_levels_to_check - 1); + return parent->GetAtomicTextFieldAncestor(max_levels_to_check - 1); return nullptr; } @@ -2667,10 +2554,15 @@ const AXObject* AXObject::DatetimeAncestor(int max_levels_to_check) const { } bool AXObject::LastKnownIsIgnoredValue() const { + DCHECK(cached_is_ignored_ || !IsDetached()) + << "A detached object should always indicate that it is ignored so that " + "it won't ever accidentally be included in the tree."; return cached_is_ignored_; } bool AXObject::LastKnownIsIgnoredButIncludedInTreeValue() const { + DCHECK(!cached_is_ignored_but_included_in_tree_ || !IsDetached()) + << "A detached object should never be included in the tree."; return cached_is_ignored_but_included_in_tree_; } @@ -2685,11 +2577,6 @@ ax::mojom::blink::Role AXObject::DetermineAccessibilityRole() { return NativeRoleIgnoringAria(); } -bool AXObject::HasInheritedPresentationalRole() const { - UpdateCachedAttributeValuesIfNeeded(); - return cached_has_inherited_presentational_role_; -} - bool AXObject::CanSetValueAttribute() const { switch (RoleValue()) { case ax::mojom::blink::Role::kColorWell: @@ -3163,7 +3050,7 @@ String AXObject::AriaTextAlternative(bool recursive, if (element) { HeapVector<Member<Element>> elements_from_attribute; Vector<String> ids; - ElementsFromAttribute(elements_from_attribute, attr, ids); + ElementsFromAttribute(element, elements_from_attribute, attr, ids); const AtomicString& aria_labelledby = GetAttribute(attr); @@ -3257,13 +3144,14 @@ String AXObject::TextFromElements( return accumulated_text.ToString(); } -void AXObject::TokenVectorFromAttribute(Vector<String>& tokens, - const QualifiedName& attribute) const { - Node* node = this->GetNode(); - if (!node || !node->IsElementNode()) +// static +void AXObject::TokenVectorFromAttribute(Element* element, + Vector<String>& tokens, + const QualifiedName& attribute) { + if (!element) return; - String attribute_value = GetAttribute(attribute).GetString(); + String attribute_value = element->FastGetAttribute(attribute).GetString(); if (attribute_value.IsEmpty()) return; @@ -3271,47 +3159,84 @@ void AXObject::TokenVectorFromAttribute(Vector<String>& tokens, attribute_value.Split(' ', tokens); } -void AXObject::ElementsFromAttribute(HeapVector<Member<Element>>& elements, +// static +bool AXObject::ElementsFromAttribute(Element* from, + HeapVector<Member<Element>>& elements, const QualifiedName& attribute, - Vector<String>& ids) const { + Vector<String>& ids) { + if (!from) + return false; + // We compute the attr-associated elements, which are either explicitly set // element references set via the IDL, or computed from the content attribute. - TokenVectorFromAttribute(ids, attribute); - Element* element = GetElement(); - if (!element) - return; + TokenVectorFromAttribute(from, ids, attribute); - base::Optional<HeapVector<Member<Element>>> attr_associated_elements = - element->GetElementArrayAttribute(attribute); + absl::optional<HeapVector<Member<Element>>> attr_associated_elements = + from->GetElementArrayAttribute(attribute); if (!attr_associated_elements) - return; + return false; for (const auto& element : attr_associated_elements.value()) elements.push_back(element); + + return elements.size(); } -void AXObject::AriaLabelledbyElementVector( +// static +bool AXObject::AriaLabelledbyElementVector( + Element* from, HeapVector<Member<Element>>& elements, - Vector<String>& ids) const { + Vector<String>& ids) { // Try both spellings, but prefer aria-labelledby, which is the official spec. - ElementsFromAttribute(elements, html_names::kAriaLabelledbyAttr, ids); - if (!ids.size()) - ElementsFromAttribute(elements, html_names::kAriaLabeledbyAttr, ids); + if (ElementsFromAttribute(from, elements, html_names::kAriaLabelledbyAttr, + ids)) { + return true; + } + + return ElementsFromAttribute(from, elements, html_names::kAriaLabeledbyAttr, + ids); +} + +// static +bool AXObject::IsNameFromAriaAttribute(Element* element) { + // TODO(accessibility) Make this work for virtual nodes. + + if (!element) + return false; + + HeapVector<Member<Element>> elements_from_attribute; + Vector<String> ids; + if (AriaLabelledbyElementVector(element, elements_from_attribute, ids)) + return true; + + const AtomicString& aria_label = AccessibleNode::GetPropertyOrARIAAttribute( + element, AOMStringProperty::kLabel); + if (!aria_label.IsEmpty()) + return true; + + return false; +} + +bool AXObject::IsNameFromAuthorAttribute() const { + return IsNameFromAriaAttribute(GetElement()) || + HasAttribute(html_names::kTitleAttr); } String AXObject::TextFromAriaLabelledby(AXObjectSet& visited, AXRelatedObjectVector* related_objects, Vector<String>& ids) const { HeapVector<Member<Element>> elements; - AriaLabelledbyElementVector(elements, ids); + AriaLabelledbyElementVector(GetElement(), elements, ids); return TextFromElements(true, visited, elements, related_objects); } String AXObject::TextFromAriaDescribedby(AXRelatedObjectVector* related_objects, Vector<String>& ids) const { AXObjectSet visited; + HeapVector<Member<Element>> elements; - ElementsFromAttribute(elements, html_names::kAriaDescribedbyAttr, ids); + ElementsFromAttribute(GetElement(), elements, + html_names::kAriaDescribedbyAttr, ids); return TextFromElements(true, visited, elements, related_objects); } @@ -3321,7 +3246,12 @@ AccessibilityOrientation AXObject::Orientation() const { return kAccessibilityOrientationUndefined; } -void AXObject::LoadInlineTextBoxes() {} +void AXObject::LoadInlineTextBoxes() { + base::AutoReset<bool> reentrancy_protector(&is_loading_inline_boxes_, true); + LoadInlineTextBoxesRecursive(); +} + +void AXObject::LoadInlineTextBoxesRecursive() {} AXObject* AXObject::NextOnLine() const { return nullptr; @@ -3331,7 +3261,7 @@ AXObject* AXObject::PreviousOnLine() const { return nullptr; } -base::Optional<const DocumentMarker::MarkerType> +absl::optional<const DocumentMarker::MarkerType> AXObject::GetAriaSpellingOrGrammarMarker() const { AtomicString aria_invalid_value; const AncestorsIterator iter = std::find_if( @@ -3343,12 +3273,12 @@ AXObject::GetAriaSpellingOrGrammarMarker() const { }); if (iter == UnignoredAncestorsEnd()) - return base::nullopt; + return absl::nullopt; if (EqualIgnoringASCIICase(aria_invalid_value, "spelling")) return DocumentMarker::kSpelling; if (EqualIgnoringASCIICase(aria_invalid_value, "grammar")) return DocumentMarker::kGrammar; - return base::nullopt; + return absl::nullopt; } void AXObject::TextCharacterOffsets(Vector<int>&) const {} @@ -3357,7 +3287,7 @@ void AXObject::GetWordBoundaries(Vector<int>& word_starts, Vector<int>& word_ends) const {} int AXObject::TextLength() const { - if (IsNativeTextField()) + if (IsAtomicTextField()) return GetValueForControl().length(); return 0; } @@ -3461,76 +3391,60 @@ bool AXObject::SupportsARIAExpanded() const { } } -bool IsGlobalARIAAttribute(const AtomicString& name) { - if (!name.StartsWith("ARIA")) - return false; - if (name.StartsWith("ARIA-ATOMIC")) - return true; - if (name.StartsWith("ARIA-BUSY")) - return true; - if (name.StartsWith("ARIA-CONTROLS")) - return true; - if (name.StartsWith("ARIA-CURRENT")) - return true; - if (name.StartsWith("ARIA-DESCRIBEDBY")) - return true; - if (name.StartsWith("ARIA-DETAILS")) - return true; - if (name.StartsWith("ARIA-DISABLED")) - return true; - if (name.StartsWith("ARIA-DROPEFFECT")) - return true; - if (name.StartsWith("ARIA-ERRORMESSAGE")) - return true; - if (name.StartsWith("ARIA-FLOWTO")) - return true; - if (name.StartsWith("ARIA-GRABBED")) - return true; - if (name.StartsWith("ARIA-HASPOPUP")) - return true; - if (name.StartsWith("ARIA-HIDDEN")) - return true; - if (name.StartsWith("ARIA-INVALID")) - return true; - if (name.StartsWith("ARIA-KEYSHORTCUTS")) - return true; - if (name.StartsWith("ARIA-LABEL")) - return true; - if (name.StartsWith("ARIA-LABELEDBY")) - return true; - if (name.StartsWith("ARIA-LABELLEDBY")) - return true; - if (name.StartsWith("ARIA-LIVE")) - return true; - if (name.StartsWith("ARIA-OWNS")) - return true; - if (name.StartsWith("ARIA-RELEVANT")) - return true; - if (name.StartsWith("ARIA-ROLEDESCRIPTION")) - return true; - return false; -} - -bool AXObject::HasGlobalARIAAttribute() const { +bool DoesUndoRolePresentation(const AtomicString& name) { + // This is the list of global ARIA properties that force + // role="presentation"/"none" to be exposed, and does not contain ARIA + // properties who's global status is being deprecated. + // clang-format off + DEFINE_STATIC_LOCAL( + HashSet<AtomicString>, aria_global_properties, + ({ + "ARIA-ATOMIC", + // TODO(accessibility/ARIA 1.3) Add (and test in aria-global.html) + // "ARIA-BRAILLEROLEDESCRIPTION", + "ARIA-BUSY", + "ARIA-CONTROLS", + "ARIA-CURRENT", + "ARIA-DESCRIBEDBY", + "ARIA-DESCRIPTION", + "ARIA-DETAILS", + "ARIA-DROPEFFECT", + "ARIA-FLOWTO", + "ARIA-GRABBED", + "ARIA-HIDDEN", // For aria-hidden=false. + "ARIA-KEYSHORTCUTS", + "ARIA-LIVE", + "ARIA-OWNS", + "ARIA-RELEVANT", + "ARIA-ROLEDESCRIPTION" + })); + // clang-format on + + return aria_global_properties.Contains(name); +} + +bool AXObject::HasAriaAttribute(bool does_undo_role_presentation) const { auto* element = GetElement(); if (!element) return false; + // A role is considered an ARIA attribute. + if (!does_undo_role_presentation && + AriaRoleAttribute() != ax::mojom::blink::Role::kUnknown) { + return true; + } + + // Check for any attribute that begins with "aria-". AttributeCollection attributes = element->AttributesWithoutUpdate(); for (const Attribute& attr : attributes) { // Attributes cache their uppercase names. auto name = attr.GetName().LocalNameUpper(); - if (IsGlobalARIAAttribute(name)) - return true; - } - if (!element->DidAttachInternals()) - return false; - const auto& internals_attributes = - element->EnsureElementInternals().GetAttributes(); - for (const QualifiedName& attr : internals_attributes.Keys()) { - if (IsGlobalARIAAttribute(attr.LocalNameUpper())) - return true; + if (name.StartsWith("ARIA-")) { + if (!does_undo_role_presentation || DoesUndoRolePresentation(name)) + return true; + } } + return false; } @@ -3637,13 +3551,28 @@ ax::mojom::blink::Role AXObject::AriaRoleAttribute() const { return ax::mojom::blink::Role::kUnknown; } -ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const { +ax::mojom::blink::Role AXObject::RawAriaRole() const { const AtomicString& aria_role = GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole); if (aria_role.IsNull() || aria_role.IsEmpty()) return ax::mojom::blink::Role::kUnknown; + return AriaRoleStringToRoleEnum(aria_role); +} - ax::mojom::blink::Role role = AriaRoleToWebCoreRole(aria_role); +ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const { + ax::mojom::blink::Role role = RawAriaRole(); + + if (role == ax::mojom::blink::Role::kRegion && !IsNameFromAuthorAttribute() && + !HasAttribute(html_names::kAriaRoledescriptionAttr)) { + // Nameless ARIA regions fall back on the native element's role. + // We only check aria-label/aria-labelledby because those are the only + // allowed ways to name an ARIA region. + // TODO(accessibility) The aria-roledescription logic is required, otherwise + // ChromeVox will ignore the aria-roledescription. It only speaks the role + // description on certain roles, and ignores it on the generic role. + // See also https://github.com/w3c/aria/issues/1463. + return ax::mojom::blink::Role::kUnknown; + } // ARIA states if an item can get focus, it should not be presentational. // It also states user agents should ignore the presentational role if @@ -3652,9 +3581,13 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const { if (IsA<HTMLIFrameElement>(*GetNode()) || IsA<HTMLFrameElement>(*GetNode())) return ax::mojom::blink::Role::kIframePresentational; if ((GetElement() && GetElement()->SupportsFocus()) || - HasGlobalARIAAttribute()) { - // If we return an unknown role, then the native HTML role would be used - // instead. + HasAriaAttribute(true /* does_undo_role_presentation */)) { + // Must be exposed with a role if focusable or has a global ARIA property + // that is allowed in this context. See + // https://w3c.github.io/aria/#presentation for more information about the + // conditions upon which elements with role="none"/"presentation" must be + // included in the tree. Return Role::kUnknown, so that the native HTML + // role is used instead. return ax::mojom::blink::Role::kUnknown; } } @@ -3671,7 +3604,7 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const { // ax::mojom::blink::Role::kComboBoxMenuButton: // <div tabindex=0 role="combobox">Select</div> if (role == ax::mojom::blink::Role::kComboBoxGrouping) { - if (IsNativeTextField()) + if (IsAtomicTextField()) role = ax::mojom::blink::Role::kTextFieldWithComboBox; else if (GetElement() && GetElement()->SupportsFocus()) role = ax::mojom::blink::Role::kComboBoxMenuButton; @@ -3680,9 +3613,71 @@ ax::mojom::blink::Role AXObject::DetermineAriaRoleAttribute() const { return role; } +ax::mojom::blink::HasPopup AXObject::HasPopup() const { + return ax::mojom::blink::HasPopup::kFalse; +} + +bool AXObject::IsEditable() const { + const Node* node = GetNode(); + if (IsDetached() || !node) + return false; +#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString() + DCHECK(GetDocument()); + DCHECK_GE(GetDocument()->Lifecycle().GetState(), + DocumentLifecycle::kStyleClean) + << "Unclean document style at lifecycle state " + << GetDocument()->Lifecycle().ToString(); +#endif // DCHECK_IS_ON() + + if (HasEditableStyle(*node)) + return true; + + // For the purposes of accessibility, atomic text fields i.e. input and + // textarea are editable because the user can potentially enter text in them. + if (IsAtomicTextField()) + return true; + + return false; +} + bool AXObject::IsEditableRoot() const { - UpdateCachedAttributeValuesIfNeeded(); - return cached_is_editable_root_; + return false; +} + +bool AXObject::HasContentEditableAttributeSet() const { + return false; +} + +bool AXObject::IsMultiline() const { + if (IsDetached() || !GetNode() || !IsTextField()) + return false; + + bool is_multiline = false; + if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiline, + is_multiline)) { + return is_multiline; + } + + return IsA<HTMLTextAreaElement>(*GetNode()) || + HasContentEditableAttributeSet(); +} + +bool AXObject::IsRichlyEditable() const { + const Node* node = GetNode(); + if (IsDetached() || !node) + return false; +#if DCHECK_IS_ON() // Required in order to get Lifecycle().ToString() + DCHECK(GetDocument()); + DCHECK_GE(GetDocument()->Lifecycle().GetState(), + DocumentLifecycle::kStyleClean) + << "Unclean document style at lifecycle state " + << GetDocument()->Lifecycle().ToString(); +#endif // DCHECK_IS_ON() + + if (HasRichlyEditableStyle(*node)) + return true; + + return false; } AXObject* AXObject::LiveRegionRoot() const { @@ -4106,13 +4101,11 @@ AXObject* AXObject::ParentObject() const { // detached, but the children still exist. One example of this is when // a <select size="1"> changes to <select size="2">, where the // Role::kMenuListPopup is detached. - if (!parent_) { + if (IsMissingParent()) { DCHECK(!IsVirtualObject()) << "A virtual object must have a parent, and cannot exist without one. " "The parent is set when the object is constructed."; - parent_ = ComputeParent(); - DCHECK(parent_ || IsA<Document>(GetNode())) - << "The following node should have a parent: " << GetNode(); + RepairMissingParent(); } return parent_; @@ -4151,23 +4144,30 @@ AXObject* AXObject::ContainerWidget() const { return ancestor; } -// Only use layout object traversal for pseudo elements and their descendants. +// Determine which traversal approach is used to get children of an object. bool AXObject::ShouldUseLayoutObjectTraversalForChildren() const { - if (!GetLayoutObject()) - return false; + // There are two types of traversal used to find AXObjects: + // 1. LayoutTreeBuilderTraversal, which takes FlatTreeTraversal and adds + // pseudo elements on top of that. This is the usual case. However, while this + // can add pseudo elements it cannot add important content descendants such as + // text and images. For this, LayoutObject traversal (#2) is required. + // 2. LayoutObject traversal, which just uses the children of a LayoutObject. + + // Therefore, if the object is a pseudo element or pseudo element descendant, + // use LayoutObject traversal (#2) to find the children. + if (GetNode() && GetNode()->IsPseudoElement()) + return true; // If no node, this is an anonymous layout object. The only way this can be // reached is inside a pseudo element subtree. - if (!GetNode()) { + if (!GetNode() && GetLayoutObject()) { DCHECK(GetLayoutObject()->IsAnonymous()); - DCHECK(AXObjectCacheImpl::IsPseudoElementDescendant(*GetLayoutObject())); + DCHECK(AXObjectCacheImpl::IsRelevantPseudoElementDescendant( + *GetLayoutObject())); return true; } - // The only other case for using layout builder traversal is for a pseudo - // element, such as ::before. Pseudo element child text and images are not - // visibited by LayoutBuilderTraversal. - return GetNode()->IsPseudoElement(); + return false; } void AXObject::UpdateChildrenIfNecessary() { @@ -4230,15 +4230,22 @@ void AXObject::ClearChildren() const { // AccessibilityExposeIgnoredNodes(). // Loop through AXObject children. - -#if DCHECK_IS_ON() - DCHECK(!is_adding_children_) - << "Should not be attempting to clear children while in the middle of " - "adding children on parent: " +#if defined(AX_FAIL_FAST_BUILD) + CHECK(!is_adding_children_) + << "Should not attempt to simultaneously add and clear children on: " << ToString(true, true); #endif + CHECK(!is_loading_inline_boxes_) << "Should not attempt to clear children " + "while loading inline text boxes: " + << ToString(true, true); + for (const auto& child : children_) { + // Check parent first, as the child might be several levels down if there + // are unincluded nodes in between, in which case the cached parent will + // also be a descendant (unlike children_, parent_ does not skip levels). + // Another case where the parent is not the same is when the child has been + // reparented using aria-owns. if (child->CachedParentObject() == this) child->DetachFromParent(); } @@ -4248,6 +4255,12 @@ void AXObject::ClearChildren() const { if (!GetNode()) return; + if (GetDocument()->IsFlatTreeTraversalForbidden()) { + // Cannot use layout tree builder traversal now, will have to rely on + // RepairParent() at a later point. + return; + } + // <slot> content is always included in the tree, so there is no need to // iterate through the nodes. This also protects us against slot use "after // poison", where attempts to access assigned nodes triggers a DCHECK. @@ -4262,24 +4275,37 @@ void AXObject::ClearChildren() const { // information with their original location in the DOM. Therefore, we need to // ensure that in the accessibility tree no remnant information from the // unflattened DOM tree remains, such as the cached parent. - if (IsA<HTMLSlotElement>(GetNode())) + + // TODO(crbug.com/1209216): Figure out why removing this causes a + // use-after-poison and possibly replace it with a better check. + HTMLSlotElement* slot = DynamicTo<HTMLSlotElement>(GetNode()); + if (slot && slot->SupportsAssignment()) return; + // Detach children that were not cleared from first loop. + // These must have been an unincluded node who's parent is this, + // although it may now be included since the children were last updated. for (Node* child_node = LayoutTreeBuilderTraversal::FirstChild(*GetNode()); child_node; child_node = LayoutTreeBuilderTraversal::NextSibling(*child_node)) { + // Get the child object that should be detached from this parent. AXObject* ax_child_from_node = AXObjectCache().Get(child_node); if (ax_child_from_node && ax_child_from_node->CachedParentObject() == this) { - // Child was not cleared from first loop. - // It must have been an unincluded node who's parent is this, - // although it may now be included since the children were last updated. // Check current parent first. It may be owned by another node. ax_child_from_node->DetachFromParent(); } } } +Node* AXObject::GetNode() const { + return nullptr; +} + +LayoutObject* AXObject::GetLayoutObject() const { + return nullptr; +} + Element* AXObject::GetElement() const { return DynamicTo<Element>(GetNode()); } @@ -4976,11 +5002,11 @@ bool AXObject::RequestShowContextMenuAction() { return OnNativeShowContextMenuAction(); } -bool AXObject::InternalClearAccessibilityFocusAction() { +bool AXObject::InternalSetAccessibilityFocusAction() { return false; } -bool AXObject::InternalSetAccessibilityFocusAction() { +bool AXObject::InternalClearAccessibilityFocusAction() { return false; } @@ -5137,15 +5163,15 @@ bool AXObject::HasARIAOwns(Element* element) { const AtomicString& aria_owns = element->FastGetAttribute(html_names::kAriaOwnsAttr); - // TODO: do we need to check !AriaOwnsElements.empty() ? Is that fundamentally - // different from HasExplicitlySetAttrAssociatedElements()? And is an element - // even necessary in the case of virtual nodes? + // TODO(accessibility): do we need to check !AriaOwnsElements.empty() ? Is + // that fundamentally different from HasExplicitlySetAttrAssociatedElements()? + // And is an element even necessary in the case of virtual nodes? return !aria_owns.IsEmpty() || element->HasExplicitlySetAttrAssociatedElements( html_names::kAriaOwnsAttr); } -ax::mojom::blink::Role AXObject::AriaRoleToWebCoreRole(const String& value) { +ax::mojom::blink::Role AXObject::AriaRoleStringToRoleEnum(const String& value) { DCHECK(!value.IsEmpty()); static const ARIARoleMap* role_map = CreateARIARoleMap(); @@ -5345,7 +5371,6 @@ bool AXObject::SupportsNameFromContents(bool recursive) const { case ax::mojom::blink::Role::kNone: case ax::mojom::blink::Role::kParagraph: case ax::mojom::blink::Role::kPre: - case ax::mojom::blink::Role::kPresentational: case ax::mojom::blink::Role::kRegion: // Spec says we should always expose the name on rows, // but for performance reasons we only do it @@ -5416,7 +5441,6 @@ bool AXObject::SupportsNameFromContents(bool recursive) const { case ax::mojom::blink::Role::kColumn: case ax::mojom::blink::Role::kDesktop: case ax::mojom::blink::Role::kKeyboard: - case ax::mojom::blink::Role::kIgnored: case ax::mojom::blink::Role::kImeCandidate: case ax::mojom::blink::Role::kListGrid: case ax::mojom::blink::Role::kPane: @@ -5465,18 +5489,62 @@ ax::mojom::blink::Role AXObject::ButtonRoleType() const { } // static -const AtomicString& AXObject::RoleName(ax::mojom::blink::Role role) { - static const Vector<AtomicString>* role_name_vector = CreateRoleNameVector(); +const AtomicString& AXObject::ARIARoleName(ax::mojom::blink::Role role) { + static const Vector<AtomicString>* aria_role_name_vector = + CreateARIARoleNameVector(); - return role_name_vector->at(static_cast<wtf_size_t>(role)); + return aria_role_name_vector->at(static_cast<wtf_size_t>(role)); } // static -const AtomicString& AXObject::InternalRoleName(ax::mojom::blink::Role role) { - static const Vector<AtomicString>* internal_role_name_vector = - CreateInternalRoleNameVector(); +const AtomicString& AXObject::GetEquivalentAriaRoleName( + const ax::mojom::blink::Role role) { + // TODO(accessibilty) Why are some roles listed here and not others? + switch (role) { + case ax::mojom::blink::Role::kArticle: + case ax::mojom::blink::Role::kBanner: + case ax::mojom::blink::Role::kButton: + case ax::mojom::blink::Role::kComplementary: + case ax::mojom::blink::Role::kFigure: + case ax::mojom::blink::Role::kFooter: + case ax::mojom::blink::Role::kHeader: + case ax::mojom::blink::Role::kHeading: + case ax::mojom::blink::Role::kImage: + case ax::mojom::blink::Role::kMain: + case ax::mojom::blink::Role::kNavigation: + case ax::mojom::blink::Role::kRadioButton: + case ax::mojom::blink::Role::kRegion: + case ax::mojom::blink::Role::kSlider: + case ax::mojom::blink::Role::kTime: + return ARIARoleName(role); + default: + return g_null_atom; + } +} + +const String AXObject::InternalRoleName(ax::mojom::blink::Role role) { + std::ostringstream role_name; + role_name << role; + // Convert from std::ostringstream to std::string, while removing "k" prefix. + // For example, kStaticText becomes StaticText. + // Many conversions, but this isn't used in performance-sensitive code. + std::string role_name_std = role_name.str().substr(1, std::string::npos); + String role_name_wtf_string = role_name_std.c_str(); + return role_name_wtf_string; +} + +// static +const String AXObject::RoleName(ax::mojom::blink::Role role, + bool* is_internal) { + if (is_internal) + *is_internal = false; + if (const auto& role_name = ARIARoleName(role)) + return role_name.GetString(); + + if (is_internal) + *is_internal = true; - return internal_role_name_vector->at(static_cast<wtf_size_t>(role)); + return InternalRoleName(role); } // static @@ -5525,8 +5593,7 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const { // Build a friendly name for debugging the object. // If verbose, build a longer name name in the form of: // CheckBox axid#28 <input.someClass#cbox1> name="checkbox" - String string_builder = - AXObject::InternalRoleName(RoleValue()).GetString().EncodeForDebugging(); + String string_builder = InternalRoleName(RoleValue()).EncodeForDebugging(); if (IsDetached()) string_builder = string_builder + " (detached)"; @@ -5534,8 +5601,8 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const { if (verbose) { string_builder = string_builder + " axid#" + String::Number(AXObjectID()); // Add useful HTML element info, like <div.myClass#myId>. - if (GetElement()) - string_builder = string_builder + " " + GetElementString(GetElement()); + if (GetNode()) + string_builder = string_builder + " " + GetNodeString(GetNode()); // Add properties of interest that often contribute to errors: if (HasARIAOwns(GetElement())) { @@ -5588,23 +5655,28 @@ String AXObject::ToString(bool verbose, bool cached_values_only) const { string_builder = string_builder + " isDisplayLocked"; } } - if (const AXObject* aria_hidden_root = AriaHiddenRoot()) { - string_builder = string_builder + " ariaHiddenRoot"; - if (aria_hidden_root != this) { - string_builder = - string_builder + GetElementString(aria_hidden_root->GetElement()); + if (cached_values_only) { + if (cached_is_inert_or_aria_hidden_ && GetNode() && !GetNode()->IsInert()) + string_builder = string_builder + " ariaHidden"; + } else { + if (const AXObject* aria_hidden_root = AriaHiddenRoot()) { + string_builder = string_builder + " ariaHiddenRoot"; + if (aria_hidden_root != this) { + string_builder = + string_builder + GetNodeString(aria_hidden_root->GetNode()); + } } } - if (GetDocument() && GetDocument()->Lifecycle().GetState() < - DocumentLifecycle::kLayoutClean) { - string_builder = string_builder + " styleInfoUnavailable"; - } else if (IsHiddenViaStyle()) { + if (cached_values_only ? cached_is_hidden_via_style : IsHiddenViaStyle()) string_builder = string_builder + " isHiddenViaCSS"; - } if (GetNode() && GetNode()->IsInert()) string_builder = string_builder + " isInert"; - if (NeedsToUpdateChildren()) + if (NeedsToUpdateChildren()) { string_builder = string_builder + " needsToUpdateChildren"; + } else if (!children_.IsEmpty()) { + string_builder = string_builder + " #children="; + string_builder = string_builder + String::Number(children_.size()); + } if (!GetLayoutObject()) string_builder = string_builder + " missingLayout"; |