| /* |
| * 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 "SkMatrix44.h" |
| #include "third_party/blink/public/platform/web_scroll_into_view_params.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/dom/element_traversal.h" |
| #include "third_party/blink/renderer/core/dom/user_gesture_indicator.h" |
| #include "third_party/blink/renderer/core/editing/editing_utilities.h" |
| #include "third_party/blink/renderer/core/editing/visible_position.h" |
| #include "third_party/blink/renderer/core/editing/visible_units.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/html/forms/html_input_element.h" |
| #include "third_party/blink/renderer/core/html/html_dialog_element.h" |
| #include "third_party/blink/renderer/core/html/html_frame_owner_element.h" |
| #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h" |
| #include "third_party/blink/renderer/core/input/context_menu_allowed_scope.h" |
| #include "third_party/blink/renderer/core/input/event_handler.h" |
| #include "third_party/blink/renderer/core/input_type_names.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_box_model_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/page/chrome_client.h" |
| #include "third_party/blink/renderer/core/page/page.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_sparse_attribute_setter.h" |
| #include "third_party/blink/renderer/platform/language.h" |
| #include "third_party/blink/renderer/platform/scroll/scroll_alignment.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/not_found.h" |
| #include "third_party/blink/renderer/platform/wtf/std_lib_extras.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| |
| using blink::WebLocalizedString; |
| |
| namespace blink { |
| |
| using namespace html_names; |
| |
| namespace { |
| |
| struct RoleHashTraits : HashTraits<ax::mojom::Role> { |
| static const bool kEmptyValueIsZero = true; |
| static ax::mojom::Role EmptyValue() { return ax::mojom::Role::kUnknown; } |
| }; |
| |
| using ARIARoleMap = HashMap<String, |
| ax::mojom::Role, |
| CaseFoldingHash, |
| HashTraits<String>, |
| RoleHashTraits>; |
| |
| struct RoleEntry { |
| const char* aria_role; |
| ax::mojom::Role webcore_role; |
| }; |
| |
| // Mapping of ARIA role name to internal role name. |
| const RoleEntry kRoles[] = { |
| {"alert", ax::mojom::Role::kAlert}, |
| {"alertdialog", ax::mojom::Role::kAlertDialog}, |
| {"application", ax::mojom::Role::kApplication}, |
| {"article", ax::mojom::Role::kArticle}, |
| {"banner", ax::mojom::Role::kBanner}, |
| {"blockquote", ax::mojom::Role::kBlockquote}, |
| {"button", ax::mojom::Role::kButton}, |
| {"caption", ax::mojom::Role::kCaption}, |
| {"cell", ax::mojom::Role::kCell}, |
| {"checkbox", ax::mojom::Role::kCheckBox}, |
| {"columnheader", ax::mojom::Role::kColumnHeader}, |
| {"combobox", ax::mojom::Role::kComboBoxGrouping}, |
| {"complementary", ax::mojom::Role::kComplementary}, |
| {"contentinfo", ax::mojom::Role::kContentInfo}, |
| {"definition", ax::mojom::Role::kDefinition}, |
| {"dialog", ax::mojom::Role::kDialog}, |
| {"directory", ax::mojom::Role::kDirectory}, |
| // ------------------------------------------------- |
| // DPub Roles: |
| // www.w3.org/TR/dpub-aam-1.0/#mapping_role_table |
| {"doc-abstract", ax::mojom::Role::kDocAbstract}, |
| {"doc-acknowledgments", ax::mojom::Role::kDocAcknowledgments}, |
| {"doc-afterword", ax::mojom::Role::kDocAfterword}, |
| {"doc-appendix", ax::mojom::Role::kDocAppendix}, |
| {"doc-backlink", ax::mojom::Role::kDocBackLink}, |
| {"doc-biblioentry", ax::mojom::Role::kDocBiblioEntry}, |
| {"doc-bibliography", ax::mojom::Role::kDocBibliography}, |
| {"doc-biblioref", ax::mojom::Role::kDocBiblioRef}, |
| {"doc-chapter", ax::mojom::Role::kDocChapter}, |
| {"doc-colophon", ax::mojom::Role::kDocColophon}, |
| {"doc-conclusion", ax::mojom::Role::kDocConclusion}, |
| {"doc-cover", ax::mojom::Role::kDocCover}, |
| {"doc-credit", ax::mojom::Role::kDocCredit}, |
| {"doc-credits", ax::mojom::Role::kDocCredits}, |
| {"doc-dedication", ax::mojom::Role::kDocDedication}, |
| {"doc-endnote", ax::mojom::Role::kDocEndnote}, |
| {"doc-endnotes", ax::mojom::Role::kDocEndnotes}, |
| {"doc-epigraph", ax::mojom::Role::kDocEpigraph}, |
| {"doc-epilogue", ax::mojom::Role::kDocEpilogue}, |
| {"doc-errata", ax::mojom::Role::kDocErrata}, |
| {"doc-example", ax::mojom::Role::kDocExample}, |
| {"doc-footnote", ax::mojom::Role::kDocFootnote}, |
| {"doc-foreword", ax::mojom::Role::kDocForeword}, |
| {"doc-glossary", ax::mojom::Role::kDocGlossary}, |
| {"doc-glossref", ax::mojom::Role::kDocGlossRef}, |
| {"doc-index", ax::mojom::Role::kDocIndex}, |
| {"doc-introduction", ax::mojom::Role::kDocIntroduction}, |
| {"doc-noteref", ax::mojom::Role::kDocNoteRef}, |
| {"doc-notice", ax::mojom::Role::kDocNotice}, |
| {"doc-pagebreak", ax::mojom::Role::kDocPageBreak}, |
| {"doc-pagelist", ax::mojom::Role::kDocPageList}, |
| {"doc-part", ax::mojom::Role::kDocPart}, |
| {"doc-preface", ax::mojom::Role::kDocPreface}, |
| {"doc-prologue", ax::mojom::Role::kDocPrologue}, |
| {"doc-pullquote", ax::mojom::Role::kDocPullquote}, |
| {"doc-qna", ax::mojom::Role::kDocQna}, |
| {"doc-subtitle", ax::mojom::Role::kDocSubtitle}, |
| {"doc-tip", ax::mojom::Role::kDocTip}, |
| {"doc-toc", ax::mojom::Role::kDocToc}, |
| // End DPub roles. |
| // ------------------------------------------------- |
| {"document", ax::mojom::Role::kDocument}, |
| {"feed", ax::mojom::Role::kFeed}, |
| {"figure", ax::mojom::Role::kFigure}, |
| {"form", ax::mojom::Role::kForm}, |
| // ------------------------------------------------- |
| // ARIA Graphics module roles: |
| // https://rawgit.com/w3c/graphics-aam/master/ |
| {"graphics-document", ax::mojom::Role::kGraphicsDocument}, |
| {"graphics-object", ax::mojom::Role::kGraphicsObject}, |
| {"graphics-symbol", ax::mojom::Role::kGraphicsSymbol}, |
| // End ARIA Graphics module roles. |
| // ------------------------------------------------- |
| {"grid", ax::mojom::Role::kGrid}, |
| {"gridcell", ax::mojom::Role::kCell}, |
| {"group", ax::mojom::Role::kGroup}, |
| {"heading", ax::mojom::Role::kHeading}, |
| {"img", ax::mojom::Role::kImage}, |
| {"link", ax::mojom::Role::kLink}, |
| {"list", ax::mojom::Role::kList}, |
| {"listbox", ax::mojom::Role::kListBox}, |
| {"listitem", ax::mojom::Role::kListItem}, |
| {"log", ax::mojom::Role::kLog}, |
| {"main", ax::mojom::Role::kMain}, |
| {"marquee", ax::mojom::Role::kMarquee}, |
| {"math", ax::mojom::Role::kMath}, |
| {"menu", ax::mojom::Role::kMenu}, |
| {"menubar", ax::mojom::Role::kMenuBar}, |
| {"menuitem", ax::mojom::Role::kMenuItem}, |
| {"menuitemcheckbox", ax::mojom::Role::kMenuItemCheckBox}, |
| {"menuitemradio", ax::mojom::Role::kMenuItemRadio}, |
| {"navigation", ax::mojom::Role::kNavigation}, |
| {"none", ax::mojom::Role::kNone}, |
| {"note", ax::mojom::Role::kNote}, |
| {"option", ax::mojom::Role::kListBoxOption}, |
| {"paragraph", ax::mojom::Role::kParagraph}, |
| {"presentation", ax::mojom::Role::kPresentational}, |
| {"progressbar", ax::mojom::Role::kProgressIndicator}, |
| {"radio", ax::mojom::Role::kRadioButton}, |
| {"radiogroup", ax::mojom::Role::kRadioGroup}, |
| // TODO(accessibility) region should only be mapped |
| // if name present. See http://crbug.com/840819. |
| {"region", ax::mojom::Role::kRegion}, |
| {"row", ax::mojom::Role::kRow}, |
| {"rowheader", ax::mojom::Role::kRowHeader}, |
| {"scrollbar", ax::mojom::Role::kScrollBar}, |
| {"search", ax::mojom::Role::kSearch}, |
| {"searchbox", ax::mojom::Role::kSearchBox}, |
| {"separator", ax::mojom::Role::kSplitter}, |
| {"slider", ax::mojom::Role::kSlider}, |
| {"spinbutton", ax::mojom::Role::kSpinButton}, |
| {"status", ax::mojom::Role::kStatus}, |
| {"switch", ax::mojom::Role::kSwitch}, |
| {"tab", ax::mojom::Role::kTab}, |
| {"table", ax::mojom::Role::kTable}, |
| {"tablist", ax::mojom::Role::kTabList}, |
| {"tabpanel", ax::mojom::Role::kTabPanel}, |
| {"term", ax::mojom::Role::kTerm}, |
| {"text", ax::mojom::Role::kStaticText}, |
| {"textbox", ax::mojom::Role::kTextField}, |
| {"timer", ax::mojom::Role::kTimer}, |
| {"toolbar", ax::mojom::Role::kToolbar}, |
| {"tooltip", ax::mojom::Role::kTooltip}, |
| {"tree", ax::mojom::Role::kTree}, |
| {"treegrid", ax::mojom::Role::kTreeGrid}, |
| {"treeitem", ax::mojom::Role::kTreeItem}}; |
| |
| struct InternalRoleEntry { |
| ax::mojom::Role webcore_role; |
| const char* internal_role_name; |
| }; |
| |
| const InternalRoleEntry kInternalRoles[] = { |
| {ax::mojom::Role::kNone, "None"}, |
| {ax::mojom::Role::kAbbr, "Abbr"}, |
| {ax::mojom::Role::kAlertDialog, "AlertDialog"}, |
| {ax::mojom::Role::kAlert, "Alert"}, |
| {ax::mojom::Role::kAnchor, "Anchor"}, |
| {ax::mojom::Role::kAnnotation, "Annotation"}, |
| {ax::mojom::Role::kApplication, "Application"}, |
| {ax::mojom::Role::kArticle, "Article"}, |
| {ax::mojom::Role::kAudio, "Audio"}, |
| {ax::mojom::Role::kBanner, "Banner"}, |
| {ax::mojom::Role::kBlockquote, "Blockquote"}, |
| {ax::mojom::Role::kButton, "Button"}, |
| {ax::mojom::Role::kCanvas, "Canvas"}, |
| {ax::mojom::Role::kCaption, "Caption"}, |
| {ax::mojom::Role::kCaret, "Caret"}, |
| {ax::mojom::Role::kCell, "Cell"}, |
| {ax::mojom::Role::kCheckBox, "CheckBox"}, |
| {ax::mojom::Role::kClient, "Client"}, |
| {ax::mojom::Role::kColorWell, "ColorWell"}, |
| {ax::mojom::Role::kColumnHeader, "ColumnHeader"}, |
| {ax::mojom::Role::kColumn, "Column"}, |
| {ax::mojom::Role::kComboBoxGrouping, "ComboBox"}, |
| {ax::mojom::Role::kComboBoxMenuButton, "ComboBox"}, |
| {ax::mojom::Role::kComplementary, "Complementary"}, |
| {ax::mojom::Role::kContentDeletion, "ContentDeletion"}, |
| {ax::mojom::Role::kContentInsertion, "ContentInsertion"}, |
| {ax::mojom::Role::kContentInfo, "ContentInfo"}, |
| {ax::mojom::Role::kDate, "Date"}, |
| {ax::mojom::Role::kDateTime, "DateTime"}, |
| {ax::mojom::Role::kDefinition, "Definition"}, |
| {ax::mojom::Role::kDescriptionListDetail, "DescriptionListDetail"}, |
| {ax::mojom::Role::kDescriptionList, "DescriptionList"}, |
| {ax::mojom::Role::kDescriptionListTerm, "DescriptionListTerm"}, |
| {ax::mojom::Role::kDesktop, "Desktop"}, |
| {ax::mojom::Role::kDetails, "Details"}, |
| {ax::mojom::Role::kDialog, "Dialog"}, |
| {ax::mojom::Role::kDirectory, "Directory"}, |
| {ax::mojom::Role::kDisclosureTriangle, "DisclosureTriangle"}, |
| // -------------------------------------------------------------- |
| // DPub Roles: |
| // https://www.w3.org/TR/dpub-aam-1.0/#mapping_role_table |
| {ax::mojom::Role::kDocAbstract, "DocAbstract"}, |
| {ax::mojom::Role::kDocAcknowledgments, "DocAcknowledgments"}, |
| {ax::mojom::Role::kDocAfterword, "DocAfterword"}, |
| {ax::mojom::Role::kDocAppendix, "DocAppendix"}, |
| {ax::mojom::Role::kDocBackLink, "DocBackLink"}, |
| {ax::mojom::Role::kDocBiblioEntry, "DocBiblioentry"}, |
| {ax::mojom::Role::kDocBibliography, "DocBibliography"}, |
| {ax::mojom::Role::kDocBiblioRef, "DocBiblioref"}, |
| {ax::mojom::Role::kDocChapter, "DocChapter"}, |
| {ax::mojom::Role::kDocColophon, "DocColophon"}, |
| {ax::mojom::Role::kDocConclusion, "DocConclusion"}, |
| {ax::mojom::Role::kDocCover, "DocCover"}, |
| {ax::mojom::Role::kDocCredit, "DocCredit"}, |
| {ax::mojom::Role::kDocCredits, "DocCredits"}, |
| {ax::mojom::Role::kDocDedication, "DocDedication"}, |
| {ax::mojom::Role::kDocEndnote, "DocEndnote"}, |
| {ax::mojom::Role::kDocEndnotes, "DocEndnotes"}, |
| {ax::mojom::Role::kDocEpigraph, "DocEpigraph"}, |
| {ax::mojom::Role::kDocEpilogue, "DocEpilogue"}, |
| {ax::mojom::Role::kDocErrata, "DocErrata"}, |
| {ax::mojom::Role::kDocExample, "DocExample"}, |
| {ax::mojom::Role::kDocFootnote, "DocFootnote"}, |
| {ax::mojom::Role::kDocForeword, "DocForeword"}, |
| {ax::mojom::Role::kDocGlossary, "DocGlossary"}, |
| {ax::mojom::Role::kDocGlossRef, "DocGlossref"}, |
| {ax::mojom::Role::kDocIndex, "DocIndex"}, |
| {ax::mojom::Role::kDocIntroduction, "DocIntroduction"}, |
| {ax::mojom::Role::kDocNoteRef, "DocNoteref"}, |
| {ax::mojom::Role::kDocNotice, "DocNotice"}, |
| {ax::mojom::Role::kDocPageBreak, "DocPagebreak"}, |
| {ax::mojom::Role::kDocPageList, "DocPagelist"}, |
| {ax::mojom::Role::kDocPart, "DocPart"}, |
| {ax::mojom::Role::kDocPreface, "DocPreface"}, |
| {ax::mojom::Role::kDocPrologue, "DocPrologue"}, |
| {ax::mojom::Role::kDocPullquote, "DocPullquote"}, |
| {ax::mojom::Role::kDocQna, "DocQna"}, |
| {ax::mojom::Role::kDocSubtitle, "DocSubtitle"}, |
| {ax::mojom::Role::kDocTip, "DocTip"}, |
| {ax::mojom::Role::kDocToc, "DocToc"}, |
| // End DPub roles. |
| // -------------------------------------------------------------- |
| {ax::mojom::Role::kDocument, "Document"}, |
| {ax::mojom::Role::kEmbeddedObject, "EmbeddedObject"}, |
| {ax::mojom::Role::kFeed, "feed"}, |
| {ax::mojom::Role::kFigcaption, "Figcaption"}, |
| {ax::mojom::Role::kFigure, "Figure"}, |
| {ax::mojom::Role::kFooter, "Footer"}, |
| {ax::mojom::Role::kForm, "Form"}, |
| {ax::mojom::Role::kGenericContainer, "GenericContainer"}, |
| // -------------------------------------------------------------- |
| // ARIA Graphics module roles: |
| // https://rawgit.com/w3c/graphics-aam/master/#mapping_role_table |
| {ax::mojom::Role::kGraphicsDocument, "GraphicsDocument"}, |
| {ax::mojom::Role::kGraphicsObject, "GraphicsObject"}, |
| {ax::mojom::Role::kGraphicsSymbol, "GraphicsSymbol"}, |
| // End ARIA Graphics module roles. |
| // -------------------------------------------------------------- |
| {ax::mojom::Role::kGrid, "Grid"}, |
| {ax::mojom::Role::kGroup, "Group"}, |
| {ax::mojom::Role::kHeading, "Heading"}, |
| {ax::mojom::Role::kIframePresentational, "IframePresentational"}, |
| {ax::mojom::Role::kIframe, "Iframe"}, |
| {ax::mojom::Role::kIgnored, "Ignored"}, |
| {ax::mojom::Role::kImageMap, "ImageMap"}, |
| {ax::mojom::Role::kImage, "Image"}, |
| {ax::mojom::Role::kInlineTextBox, "InlineTextBox"}, |
| {ax::mojom::Role::kInputTime, "InputTime"}, |
| {ax::mojom::Role::kKeyboard, "Keyboard"}, |
| {ax::mojom::Role::kLabelText, "Label"}, |
| {ax::mojom::Role::kLayoutTable, "LayoutTable"}, |
| {ax::mojom::Role::kLayoutTableCell, "LayoutCellTable"}, |
| {ax::mojom::Role::kLayoutTableColumn, "LayoutColumnTable"}, |
| {ax::mojom::Role::kLayoutTableRow, "LayoutRowTable"}, |
| {ax::mojom::Role::kLegend, "Legend"}, |
| {ax::mojom::Role::kLink, "Link"}, |
| {ax::mojom::Role::kLineBreak, "LineBreak"}, |
| {ax::mojom::Role::kListBoxOption, "ListBoxOption"}, |
| {ax::mojom::Role::kListBox, "ListBox"}, |
| {ax::mojom::Role::kListItem, "ListItem"}, |
| {ax::mojom::Role::kListMarker, "ListMarker"}, |
| {ax::mojom::Role::kList, "List"}, |
| {ax::mojom::Role::kLog, "Log"}, |
| {ax::mojom::Role::kMain, "Main"}, |
| {ax::mojom::Role::kMark, "Mark"}, |
| {ax::mojom::Role::kMarquee, "Marquee"}, |
| {ax::mojom::Role::kMath, "Math"}, |
| {ax::mojom::Role::kMenuBar, "MenuBar"}, |
| {ax::mojom::Role::kMenuButton, "MenuButton"}, |
| {ax::mojom::Role::kMenuItem, "MenuItem"}, |
| {ax::mojom::Role::kMenuItemCheckBox, "MenuItemCheckBox"}, |
| {ax::mojom::Role::kMenuItemRadio, "MenuItemRadio"}, |
| {ax::mojom::Role::kMenuListOption, "MenuListOption"}, |
| {ax::mojom::Role::kMenuListPopup, "MenuListPopup"}, |
| {ax::mojom::Role::kMenu, "Menu"}, |
| {ax::mojom::Role::kMeter, "Meter"}, |
| {ax::mojom::Role::kNavigation, "Navigation"}, |
| {ax::mojom::Role::kNote, "Note"}, |
| {ax::mojom::Role::kPane, "Pane"}, |
| {ax::mojom::Role::kParagraph, "Paragraph"}, |
| {ax::mojom::Role::kPopUpButton, "PopUpButton"}, |
| {ax::mojom::Role::kPre, "Pre"}, |
| {ax::mojom::Role::kPresentational, "Presentational"}, |
| {ax::mojom::Role::kProgressIndicator, "ProgressIndicator"}, |
| {ax::mojom::Role::kRadioButton, "RadioButton"}, |
| {ax::mojom::Role::kRadioGroup, "RadioGroup"}, |
| {ax::mojom::Role::kRegion, "Region"}, |
| {ax::mojom::Role::kRootWebArea, "WebArea"}, |
| {ax::mojom::Role::kRowHeader, "RowHeader"}, |
| {ax::mojom::Role::kRow, "Row"}, |
| {ax::mojom::Role::kRuby, "Ruby"}, |
| {ax::mojom::Role::kSvgRoot, "SVGRoot"}, |
| {ax::mojom::Role::kScrollBar, "ScrollBar"}, |
| {ax::mojom::Role::kScrollView, "ScrollView"}, |
| {ax::mojom::Role::kSearch, "Search"}, |
| {ax::mojom::Role::kSearchBox, "SearchBox"}, |
| {ax::mojom::Role::kSlider, "Slider"}, |
| {ax::mojom::Role::kSliderThumb, "SliderThumb"}, |
| {ax::mojom::Role::kSpinButton, "SpinButton"}, |
| {ax::mojom::Role::kSplitter, "Splitter"}, |
| {ax::mojom::Role::kStaticText, "StaticText"}, |
| {ax::mojom::Role::kStatus, "Status"}, |
| {ax::mojom::Role::kSwitch, "Switch"}, |
| {ax::mojom::Role::kTabList, "TabList"}, |
| {ax::mojom::Role::kTabPanel, "TabPanel"}, |
| {ax::mojom::Role::kTab, "Tab"}, |
| {ax::mojom::Role::kTableHeaderContainer, "TableHeaderContainer"}, |
| {ax::mojom::Role::kTable, "Table"}, |
| {ax::mojom::Role::kTerm, "Term"}, |
| {ax::mojom::Role::kTextField, "TextField"}, |
| {ax::mojom::Role::kTextFieldWithComboBox, "ComboBox"}, |
| {ax::mojom::Role::kTime, "Time"}, |
| {ax::mojom::Role::kTimer, "Timer"}, |
| {ax::mojom::Role::kTitleBar, "TitleBar"}, |
| {ax::mojom::Role::kToggleButton, "ToggleButton"}, |
| {ax::mojom::Role::kToolbar, "Toolbar"}, |
| {ax::mojom::Role::kTreeGrid, "TreeGrid"}, |
| {ax::mojom::Role::kTreeItem, "TreeItem"}, |
| {ax::mojom::Role::kTree, "Tree"}, |
| {ax::mojom::Role::kTooltip, "UserInterfaceTooltip"}, |
| {ax::mojom::Role::kUnknown, "Unknown"}, |
| {ax::mojom::Role::kVideo, "Video"}, |
| {ax::mojom::Role::kWebArea, "WebArea"}, |
| {ax::mojom::Role::kWebView, "WebView"}, |
| {ax::mojom::Role::kWindow, "Window"}}; |
| |
| static_assert(base::size(kInternalRoles) == |
| static_cast<size_t>(ax::mojom::Role::kMaxValue) + 1, |
| "Not all internal roles have an entry in internalRoles array"); |
| |
| // Roles which we need to map in the other direction |
| const RoleEntry kReverseRoles[] = { |
| {"button", ax::mojom::Role::kToggleButton}, |
| {"combobox", ax::mojom::Role::kPopUpButton}, |
| {"contentinfo", ax::mojom::Role::kFooter}, |
| {"menuitem", ax::mojom::Role::kMenuButton}, |
| {"menuitem", ax::mojom::Role::kMenuListOption}, |
| {"progressbar", ax::mojom::Role::kMeter}, |
| {"textbox", ax::mojom::Role::kTextField}, |
| {"combobox", ax::mojom::Role::kComboBoxMenuButton}, |
| {"combobox", ax::mojom::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); |
| |
| // Grids "ignore" their non-row children during computation of children. |
| role_map->Set(String("rowgroup"), ax::mojom::Role::kIgnored); |
| |
| return role_map; |
| } |
| |
| static Vector<AtomicString>* CreateRoleNameVector() { |
| Vector<AtomicString>* role_name_vector = |
| new Vector<AtomicString>(base::size(kInternalRoles)); |
| for (size_t i = 0; i < base::size(kInternalRoles); i++) |
| (*role_name_vector)[i] = g_null_atom; |
| |
| for (size_t i = 0; i < base::size(kRoles); ++i) { |
| (*role_name_vector)[static_cast<size_t>(kRoles[i].webcore_role)] = |
| AtomicString(kRoles[i].aria_role); |
| } |
| |
| for (size_t i = 0; i < base::size(kReverseRoles); ++i) { |
| (*role_name_vector)[static_cast<size_t>(kReverseRoles[i].webcore_role)] = |
| AtomicString(kReverseRoles[i].aria_role); |
| } |
| |
| return role_name_vector; |
| } |
| |
| static Vector<AtomicString>* CreateInternalRoleNameVector() { |
| Vector<AtomicString>* internal_role_name_vector = |
| new Vector<AtomicString>(base::size(kInternalRoles)); |
| for (size_t i = 0; i < base::size(kInternalRoles); i++) { |
| (*internal_role_name_vector)[static_cast<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(); |
| } |
| |
| } // namespace |
| |
| unsigned AXObject::number_of_live_ax_objects_ = 0; |
| |
| AXObject::AXObject(AXObjectCacheImpl& ax_object_cache) |
| : id_(0), |
| have_children_(false), |
| role_(ax::mojom::Role::kUnknown), |
| aria_role_(ax::mojom::Role::kUnknown), |
| last_known_is_ignored_value_(kDefaultBehavior), |
| explicit_container_id_(0), |
| parent_(nullptr), |
| last_modification_count_(-1), |
| cached_is_ignored_(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), |
| ax_object_cache_(&ax_object_cache) { |
| ++number_of_live_ax_objects_; |
| } |
| |
| AXObject::~AXObject() { |
| DCHECK(IsDetached()); |
| --number_of_live_ax_objects_; |
| } |
| |
| void AXObject::Init() { |
| role_ = DetermineAccessibilityRole(); |
| } |
| |
| void AXObject::Detach() { |
| // Clear any children and call detachFromParent on them so that |
| // no children are left with dangling pointers to their parent. |
| ClearChildren(); |
| |
| ax_object_cache_ = nullptr; |
| } |
| |
| bool AXObject::IsDetached() const { |
| return !ax_object_cache_; |
| } |
| |
| const AtomicString& AXObject::GetAOMPropertyOrARIAAttribute( |
| AOMStringProperty property) const { |
| Element* element = this->GetElement(); |
| if (!element) |
| return g_null_atom; |
| |
| return AccessibleNode::GetPropertyOrARIAAttribute(element, property); |
| } |
| |
| Element* AXObject::GetAOMPropertyOrARIAAttribute( |
| AOMRelationProperty property) const { |
| Element* element = this->GetElement(); |
| if (!element) |
| return nullptr; |
| |
| return AccessibleNode::GetPropertyOrARIAAttribute(element, property); |
| } |
| |
| bool AXObject::HasAOMProperty(AOMRelationListProperty property, |
| HeapVector<Member<Element>>& result) const { |
| Element* element = this->GetElement(); |
| if (!element) |
| return false; |
| |
| return AccessibleNode::GetProperty(element, property, result); |
| } |
| |
| bool AXObject::HasAOMPropertyOrARIAAttribute( |
| AOMRelationListProperty property, |
| HeapVector<Member<Element>>& result) const { |
| Element* element = this->GetElement(); |
| if (!element) |
| return false; |
| |
| return AccessibleNode::GetPropertyOrARIAAttribute(element, property, result); |
| } |
| |
| bool AXObject::HasAOMPropertyOrARIAAttribute(AOMBooleanProperty property, |
| bool& result) const { |
| Element* element = this->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 = this->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 = this->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 = this->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 = this->GetElement(); |
| if (!element) |
| return false; |
| |
| result = AccessibleNode::GetPropertyOrARIAAttribute(element, property); |
| return !result.IsNull(); |
| } |
| |
| AccessibleNode* AXObject::GetAccessibleNode() const { |
| Element* element = GetElement(); |
| if (!element) |
| return nullptr; |
| |
| return element->ExistingAccessibleNode(); |
| } |
| |
| void AXObject::GetSparseAXAttributes( |
| AXSparseAttributeClient& sparse_attribute_client) const { |
| AXSparseAttributeAOMPropertyClient property_client(*ax_object_cache_, |
| sparse_attribute_client); |
| HashSet<QualifiedName> shadowed_aria_attributes; |
| |
| AccessibleNode* accessible_node = GetAccessibleNode(); |
| if (accessible_node) { |
| accessible_node->GetAllAOMProperties(&property_client, |
| shadowed_aria_attributes); |
| } |
| |
| Element* element = GetElement(); |
| if (!element) |
| return; |
| |
| AXSparseAttributeSetterMap& ax_sparse_attribute_setter_map = |
| GetSparseAttributeSetterMap(); |
| AttributeCollection attributes = element->AttributesWithoutUpdate(); |
| for (const Attribute& attr : attributes) { |
| if (shadowed_aria_attributes.Contains(attr.GetName())) |
| continue; |
| |
| AXSparseAttributeSetter* setter = |
| ax_sparse_attribute_setter_map.at(attr.GetName()); |
| if (setter) |
| setter->Run(*this, sparse_attribute_client, attr.Value()); |
| } |
| } |
| |
| bool AXObject::IsARIATextControl() const { |
| return AriaRoleAttribute() == ax::mojom::Role::kTextField || |
| AriaRoleAttribute() == ax::mojom::Role::kSearchBox || |
| AriaRoleAttribute() == ax::mojom::Role::kTextFieldWithComboBox; |
| } |
| |
| bool AXObject::IsButton() const { |
| ax::mojom::Role role = RoleValue(); |
| |
| return role == ax::mojom::Role::kButton || |
| role == ax::mojom::Role::kPopUpButton || |
| role == ax::mojom::Role::kToggleButton; |
| } |
| |
| bool AXObject::IsCheckable() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kCheckBox: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kRadioButton: |
| case ax::mojom::Role::kSwitch: |
| case ax::mojom::Role::kToggleButton: |
| return true; |
| case ax::mojom::Role::kTreeItem: |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kMenuListOption: |
| return AriaCheckedIsPresent(); |
| default: |
| return false; |
| } |
| } |
| |
| // Why this is here instead of AXNodeObject: |
| // Because an AXMenuListOption (<option>) can |
| // have an ARIA role of menuitemcheckbox/menuitemradio |
| // yet does not inherit from AXNodeObject |
| ax::mojom::CheckedState AXObject::CheckedState() const { |
| if (!IsCheckable()) |
| return ax::mojom::CheckedState::kNone; |
| |
| // Try ARIA checked/pressed state |
| const ax::mojom::Role role = RoleValue(); |
| const auto prop = role == ax::mojom::Role::kToggleButton |
| ? AOMStringProperty::kPressed |
| : AOMStringProperty::kChecked; |
| const AtomicString& checked_attribute = GetAOMPropertyOrARIAAttribute(prop); |
| if (checked_attribute) { |
| if (EqualIgnoringASCIICase(checked_attribute, "mixed")) { |
| // Only checkable role that doesn't support mixed is the switch. |
| if (role != ax::mojom::Role::kSwitch) |
| return ax::mojom::CheckedState::kMixed; |
| } |
| |
| // Anything other than "false" should be treated as "true". |
| return EqualIgnoringASCIICase(checked_attribute, "false") |
| ? ax::mojom::CheckedState::kFalse |
| : ax::mojom::CheckedState::kTrue; |
| } |
| |
| // Native checked state |
| if (role != ax::mojom::Role::kToggleButton) { |
| const Node* node = this->GetNode(); |
| if (!node) |
| return ax::mojom::CheckedState::kNone; |
| |
| // Expose native checkbox mixed state as accessibility mixed state. However, |
| // do not expose native radio mixed state as accessibility mixed state. |
| // This would confuse the JAWS screen reader, which reports a mixed radio as |
| // both checked and partially checked, but a native mixed native radio |
| // button sinply means no radio buttons have been checked in the group yet. |
| if (IsNativeCheckboxInMixedState(node)) |
| return ax::mojom::CheckedState::kMixed; |
| |
| if (IsHTMLInputElement(*node) && |
| ToHTMLInputElement(*node).ShouldAppearChecked()) { |
| return ax::mojom::CheckedState::kTrue; |
| } |
| } |
| |
| return ax::mojom::CheckedState::kFalse; |
| } |
| |
| bool AXObject::IsNativeCheckboxInMixedState(const Node* node) { |
| if (!IsHTMLInputElement(node)) |
| return false; |
| |
| const HTMLInputElement* input = ToHTMLInputElement(node); |
| const auto inputType = input->type(); |
| if (inputType != input_type_names::kCheckbox) |
| return false; |
| return input->ShouldAppearIndeterminate(); |
| } |
| |
| bool AXObject::IsLandmarkRelated() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kApplication: |
| case ax::mojom::Role::kArticle: |
| case ax::mojom::Role::kBanner: |
| case ax::mojom::Role::kComplementary: |
| case ax::mojom::Role::kContentInfo: |
| case ax::mojom::Role::kDocAcknowledgments: |
| case ax::mojom::Role::kDocAfterword: |
| case ax::mojom::Role::kDocAppendix: |
| case ax::mojom::Role::kDocBibliography: |
| case ax::mojom::Role::kDocChapter: |
| case ax::mojom::Role::kDocConclusion: |
| case ax::mojom::Role::kDocCredits: |
| case ax::mojom::Role::kDocEndnotes: |
| case ax::mojom::Role::kDocEpilogue: |
| case ax::mojom::Role::kDocErrata: |
| case ax::mojom::Role::kDocForeword: |
| case ax::mojom::Role::kDocGlossary: |
| case ax::mojom::Role::kDocIntroduction: |
| case ax::mojom::Role::kDocPart: |
| case ax::mojom::Role::kDocPreface: |
| case ax::mojom::Role::kDocPrologue: |
| case ax::mojom::Role::kDocToc: |
| case ax::mojom::Role::kFooter: |
| case ax::mojom::Role::kForm: |
| case ax::mojom::Role::kMain: |
| case ax::mojom::Role::kNavigation: |
| case ax::mojom::Role::kRegion: |
| case ax::mojom::Role::kSearch: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::IsMenuRelated() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kMenu: |
| case ax::mojom::Role::kMenuBar: |
| case ax::mojom::Role::kMenuButton: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::IsPasswordFieldAndShouldHideValue() const { |
| Settings* settings = GetDocument()->GetSettings(); |
| if (!settings || settings->GetAccessibilityPasswordValuesEnabled()) |
| return false; |
| |
| return IsPasswordField(); |
| } |
| |
| bool AXObject::IsTextObject() const { |
| // Objects with |ax::mojom::Role::kLineBreak| are HTML <br> elements and are |
| // not backed by DOM text nodes. We can't mark them as text objects for that |
| // reason. |
| switch (RoleValue()) { |
| case ax::mojom::Role::kInlineTextBox: |
| case ax::mojom::Role::kStaticText: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::IsClickable() const { |
| if (IsButton() || IsLink() || IsTextControl()) |
| return true; |
| |
| // TODO(dmazzoni): Ensure that ax::mojom::Role::kColorWell and |
| // ax::mojom::Role::kSpinButton are correctly handled here via their |
| // constituent parts. |
| switch (RoleValue()) { |
| case ax::mojom::Role::kCheckBox: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kDisclosureTriangle: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuListOption: |
| case ax::mojom::Role::kRadioButton: |
| case ax::mojom::Role::kSwitch: |
| case ax::mojom::Role::kTab: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::AccessibilityIsIgnored() const { |
| Node* node = GetNode(); |
| if (!node) { |
| AXObject* parent = this->ParentObject(); |
| while (!node && parent) { |
| node = parent->GetNode(); |
| parent = parent->ParentObject(); |
| } |
| } |
| |
| if (node) |
| node->UpdateDistributionForFlatTreeTraversal(); |
| |
| // TODO(aboxhall): Instead of this, propagate inert down through frames |
| Document* document = GetDocument(); |
| while (document && document->LocalOwner()) { |
| document->LocalOwner()->UpdateDistributionForFlatTreeTraversal(); |
| document = document->LocalOwner()->ownerDocument(); |
| } |
| |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_is_ignored_; |
| } |
| |
| void AXObject::UpdateCachedAttributeValuesIfNeeded() const { |
| if (IsDetached()) |
| return; |
| |
| AXObjectCacheImpl& cache = AXObjectCache(); |
| |
| if (cache.ModificationCount() == last_modification_count_) |
| return; |
| |
| last_modification_count_ = cache.ModificationCount(); |
| cached_background_color_ = ComputeBackgroundColor(); |
| cached_is_inert_or_aria_hidden_ = ComputeIsInertOrAriaHidden(); |
| cached_is_descendant_of_leaf_node_ = !!LeafNodeAncestor(); |
| cached_is_descendant_of_disabled_node_ = !!DisabledAncestor(); |
| cached_has_inherited_presentational_role_ = |
| !!InheritsPresentationalRoleFrom(); |
| cached_is_ignored_ = ComputeAccessibilityIsIgnored(); |
| cached_is_editable_root_ = ComputeIsEditableRoot(); |
| // TODO(dmazzoni): remove this const_cast. |
| cached_live_region_root_ = |
| IsLiveRegion() |
| ? const_cast<AXObject*>(this) |
| : (ParentObjectIfExists() ? ParentObjectIfExists()->LiveRegionRoot() |
| : nullptr); |
| cached_aria_column_index_ = ComputeAriaColumnIndex(); |
| cached_aria_row_index_ = ComputeAriaRowIndex(); |
| if (cached_is_ignored_ != LastKnownIsIgnoredValue()) { |
| last_known_is_ignored_value_ = |
| cached_is_ignored_ ? kIgnoreObject : kIncludeObject; |
| |
| AXObject* parent = ParentObjectIfExists(); |
| if (parent) |
| parent->ChildrenChanged(); |
| } |
| |
| if (GetLayoutObject() && GetLayoutObject()->IsText()) { |
| cached_local_bounding_box_rect_for_accessibility_ = |
| GetLayoutObject()->LocalBoundingBoxRectForAccessibility(); |
| } |
| } |
| |
| bool AXObject::AccessibilityIsIgnoredByDefault( |
| IgnoredReasons* ignored_reasons) const { |
| return DefaultObjectInclusion(ignored_reasons) == kIgnoreObject; |
| } |
| |
| AXObjectInclusion AXObject::AccessibilityPlatformIncludesObject() const { |
| if (IsMenuListPopup() || IsMenuListOption()) |
| return kIncludeObject; |
| |
| return kDefaultBehavior; |
| } |
| |
| AXObjectInclusion AXObject::DefaultObjectInclusion( |
| IgnoredReasons* ignored_reasons) const { |
| if (IsInertOrAriaHidden()) { |
| // Keep focusable elements that are aria-hidden in tree, so that they can |
| // still fire events such as focus and value changes. |
| const Element* elem = GetElement(); |
| if (!elem || !elem->SupportsFocus() || elem->IsInert()) { |
| if (ignored_reasons) |
| ComputeIsInertOrAriaHidden(ignored_reasons); |
| return kIgnoreObject; |
| } |
| } |
| |
| return AccessibilityPlatformIncludesObject(); |
| } |
| |
| bool AXObject::IsInertOrAriaHidden() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_is_inert_or_aria_hidden_; |
| } |
| |
| bool AXObject::ComputeIsInertOrAriaHidden( |
| IgnoredReasons* ignored_reasons) const { |
| if (GetNode()) { |
| if (GetNode()->IsInert()) { |
| if (ignored_reasons) { |
| HTMLDialogElement* dialog = GetActiveDialogElement(GetNode()); |
| if (dialog) { |
| AXObject* dialog_object = AXObjectCache().GetOrCreate(dialog); |
| if (dialog_object) { |
| ignored_reasons->push_back( |
| IgnoredReason(kAXActiveModalDialog, dialog_object)); |
| } else { |
| ignored_reasons->push_back(IgnoredReason(kAXInertElement)); |
| } |
| } else { |
| const AXObject* inert_root_el = InertRoot(); |
| if (inert_root_el == this) { |
| ignored_reasons->push_back(IgnoredReason(kAXInertElement)); |
| } else { |
| ignored_reasons->push_back( |
| IgnoredReason(kAXInertSubtree, inert_root_el)); |
| } |
| } |
| } |
| return true; |
| } |
| } else { |
| AXObject* parent = ParentObject(); |
| if (parent && parent->IsInertOrAriaHidden()) { |
| if (ignored_reasons) |
| parent->ComputeIsInertOrAriaHidden(ignored_reasons); |
| return true; |
| } |
| } |
| |
| const AXObject* hidden_root = AriaHiddenRoot(); |
| if (hidden_root) { |
| if (ignored_reasons) { |
| if (hidden_root == this) { |
| ignored_reasons->push_back(IgnoredReason(kAXAriaHiddenElement)); |
| } else { |
| ignored_reasons->push_back( |
| IgnoredReason(kAXAriaHiddenSubtree, hidden_root)); |
| } |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool AXObject::IsVisible() const { |
| return !IsInertOrAriaHidden(); |
| } |
| |
| 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)) |
| return object; |
| } |
| |
| return nullptr; |
| } |
| |
| const AXObject* AXObject::InertRoot() const { |
| const AXObject* object = this; |
| if (!RuntimeEnabledFeatures::InertAttributeEnabled()) |
| return nullptr; |
| |
| while (object && !object->IsAXNodeObject()) |
| object = object->ParentObject(); |
| Node* node = object->GetNode(); |
| Element* element = node->IsElementNode() |
| ? ToElement(node) |
| : FlatTreeTraversal::ParentElement(*node); |
| while (element) { |
| if (element->hasAttribute(kInertAttr)) |
| return AXObjectCache().GetOrCreate(element); |
| element = FlatTreeTraversal::ParentElement(*element); |
| } |
| |
| return nullptr; |
| } |
| |
| bool AXObject::DispatchEventToAOMEventListeners(Event& event) { |
| HeapVector<Member<AccessibleNode>> event_path; |
| for (AXObject* ancestor = this; ancestor; |
| ancestor = ancestor->ParentObject()) { |
| AccessibleNode* ancestor_accessible_node = ancestor->GetAccessibleNode(); |
| if (!ancestor_accessible_node) |
| continue; |
| |
| if (!ancestor_accessible_node->HasEventListeners(event.type())) |
| continue; |
| |
| event_path.push_back(ancestor_accessible_node); |
| } |
| |
| // Short-circuit: if there are no AccessibleNodes attached anywhere |
| // in the ancestry of this node, exit. |
| if (!event_path.size()) |
| return false; |
| |
| // Check if the user has granted permission for this domain to use |
| // AOM event listeners yet. This may trigger an infobar, but we shouldn't |
| // block, so whatever decision the user makes will apply to the next |
| // event received after that. |
| // |
| // Note that we only ask the user about this permission the first |
| // time an event is received that actually would have triggered an |
| // event listener. However, if the user grants this permission, it |
| // persists for this origin from then on. |
| if (!AXObjectCache().CanCallAOMEventListeners()) { |
| AXObjectCache().RequestAOMEventListenerPermission(); |
| return false; |
| } |
| |
| // Since we now know the AOM is being used in this document, get the |
| // AccessibleNode for the target element and create it if necessary - |
| // otherwise we wouldn't be able to set the event target. However note |
| // that if it didn't previously exist it won't be part of the event path. |
| AccessibleNode* target = GetAccessibleNode(); |
| if (!target) { |
| if (Element* element = GetElement()) |
| target = element->accessibleNode(); |
| } |
| if (!target) |
| return false; |
| event.SetTarget(target); |
| |
| // Capturing phase. |
| event.SetEventPhase(Event::kCapturingPhase); |
| for (int i = static_cast<int>(event_path.size()) - 1; i >= 0; i--) { |
| // Don't call capturing event listeners on the target. Note that |
| // the target may not necessarily be in the event path which is why |
| // we check here. |
| if (event_path[i] == target) |
| break; |
| |
| event.SetCurrentTarget(event_path[i]); |
| event_path[i]->FireEventListeners(event); |
| if (event.PropagationStopped()) |
| return true; |
| } |
| |
| // Targeting phase. |
| event.SetEventPhase(Event::kAtTarget); |
| event.SetCurrentTarget(event_path[0]); |
| event_path[0]->FireEventListeners(event); |
| if (event.PropagationStopped()) |
| return true; |
| |
| // Bubbling phase. |
| event.SetEventPhase(Event::kBubblingPhase); |
| for (wtf_size_t i = 1; i < event_path.size(); i++) { |
| event.SetCurrentTarget(event_path[i]); |
| event_path[i]->FireEventListeners(event); |
| if (event.PropagationStopped()) |
| return true; |
| } |
| |
| if (event.defaultPrevented()) |
| return true; |
| |
| return false; |
| } |
| |
| bool AXObject::IsDescendantOfDisabledNode() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_is_descendant_of_disabled_node_; |
| } |
| |
| const AXObject* AXObject::DisabledAncestor() const { |
| bool disabled = false; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kDisabled, disabled)) { |
| if (disabled) |
| return this; |
| return nullptr; |
| } |
| |
| if (AXObject* parent = ParentObject()) |
| return parent->DisabledAncestor(); |
| |
| return nullptr; |
| } |
| |
| const AXObject* AXObject::DatetimeAncestor(int max_levels_to_check) const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kDateTime: |
| case ax::mojom::Role::kDate: |
| case ax::mojom::Role::kInputTime: |
| case ax::mojom::Role::kTime: |
| return this; |
| default: |
| break; |
| } |
| |
| if (max_levels_to_check == 0) |
| return nullptr; |
| |
| if (AXObject* parent = ParentObject()) |
| return parent->DatetimeAncestor(max_levels_to_check - 1); |
| |
| return nullptr; |
| } |
| |
| bool AXObject::LastKnownIsIgnoredValue() const { |
| if (last_known_is_ignored_value_ == kDefaultBehavior) { |
| last_known_is_ignored_value_ = |
| AccessibilityIsIgnored() ? kIgnoreObject : kIncludeObject; |
| } |
| |
| return last_known_is_ignored_value_ == kIgnoreObject; |
| } |
| |
| void AXObject::SetLastKnownIsIgnoredValue(bool is_ignored) { |
| last_known_is_ignored_value_ = is_ignored ? kIgnoreObject : kIncludeObject; |
| } |
| |
| bool AXObject::HasInheritedPresentationalRole() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_has_inherited_presentational_role_; |
| } |
| |
| bool AXObject::CanReceiveAccessibilityFocus() const { |
| const Element* elem = GetElement(); |
| if (!elem) |
| return false; |
| |
| // Focusable, and not forwarding the focus somewhere else |
| if (elem->IsFocusable() && |
| !GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant)) |
| return true; |
| |
| // aria-activedescendant focus |
| return elem->FastHasAttribute(kIdAttr) && CanBeActiveDescendant(); |
| } |
| |
| bool AXObject::CanSetValueAttribute() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kColorWell: |
| case ax::mojom::Role::kDate: |
| case ax::mojom::Role::kDateTime: |
| case ax::mojom::Role::kScrollBar: |
| case ax::mojom::Role::kSlider: |
| case ax::mojom::Role::kSpinButton: |
| case ax::mojom::Role::kSplitter: |
| case ax::mojom::Role::kTextField: |
| case ax::mojom::Role::kTextFieldWithComboBox: |
| case ax::mojom::Role::kTime: |
| case ax::mojom::Role::kSearchBox: |
| return Restriction() == kNone; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| bool AXObject::CanSetFocusAttribute() const { |
| Node* node = GetNode(); |
| if (!node) |
| return false; |
| |
| if (IsWebArea()) |
| return true; |
| |
| // Children of elements with an aria-activedescendant attribute should be |
| // focusable if they have a (non-presentational) ARIA role. |
| if (!IsPresentational() && AriaRoleAttribute() != ax::mojom::Role::kUnknown && |
| CanBeActiveDescendant()) { |
| return true; |
| } |
| |
| // NOTE: It would be more accurate to ask the document whether |
| // setFocusedNode() would do anything. For example, setFocusedNode() will do |
| // nothing if the current focused node will not relinquish the focus. |
| if (IsDisabledFormControl(node)) |
| return false; |
| |
| // Check for options here because AXListBoxOption and AXMenuListOption |
| // don't help when the <option> is canvas fallback, and because |
| // a common case for aria-owns from a textbox that points to a list |
| // does not change the hierarchy (textboxes don't support children) |
| if (RoleValue() == ax::mojom::Role::kListBoxOption || |
| RoleValue() == ax::mojom::Role::kMenuListOption) |
| return true; |
| |
| return node->IsElementNode() && ToElement(node)->SupportsFocus(); |
| } |
| |
| // From ARIA 1.1. |
| // 1. The value of aria-activedescendant refers to an element that is either a |
| // descendant of the element with DOM focus or is a logical descendant as |
| // indicated by the aria-owns attribute. 2. The element with DOM focus is a |
| // textbox with aria-controls referring to an element that supports |
| // aria-activedescendant, and the value of aria-activedescendant specified for |
| // the textbox refers to either a descendant of the element controlled by the |
| // textbox or is a logical descendant of that controlled element as indicated by |
| // the aria-owns attribute. |
| bool AXObject::CanBeActiveDescendant() const { |
| return IsARIAControlledByTextboxWithActiveDescendant() || |
| AncestorExposesActiveDescendant(); |
| } |
| |
| bool AXObject::IsARIAControlledByTextboxWithActiveDescendant() const { |
| // This situation should mostly arise when using an active descendant on a |
| // textbox inside an ARIA 1.1 combo box widget, which points to the selected |
| // option in a list. In such situations, the active descendant is useful only |
| // when the textbox is focused. Therefore, we don't currently need to keep |
| // track of all aria-controls relationships. |
| const AXObject* focused_object = AXObjectCache().FocusedObject(); |
| if (!focused_object || !focused_object->IsTextControl()) |
| return false; |
| |
| if (!focused_object->GetAOMPropertyOrARIAAttribute( |
| AOMRelationProperty::kActiveDescendant)) { |
| return false; |
| } |
| |
| HeapVector<Member<Element>> controlled_by_elements; |
| if (!focused_object->HasAOMPropertyOrARIAAttribute( |
| AOMRelationListProperty::kControls, controlled_by_elements)) { |
| return false; |
| } |
| |
| for (const auto& controlled_by_element : controlled_by_elements) { |
| const AXObject* controlled_by_object = |
| AXObjectCache().GetOrCreate(controlled_by_element); |
| if (!controlled_by_object) |
| continue; |
| |
| const AXObject* object = this; |
| while (object && object != controlled_by_object) |
| object = object->ParentObjectUnignored(); |
| if (object) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool AXObject::AncestorExposesActiveDescendant() const { |
| const AXObject* parent = ParentObjectUnignored(); |
| if (!parent) |
| return false; |
| |
| if (parent->GetAOMPropertyOrARIAAttribute( |
| AOMRelationProperty::kActiveDescendant)) { |
| return true; |
| } |
| |
| return parent->AncestorExposesActiveDescendant(); |
| } |
| |
| bool AXObject::HasIndirectChildren() const { |
| return IsTableCol() || RoleValue() == ax::mojom::Role::kTableHeaderContainer; |
| } |
| |
| bool AXObject::CanSetSelectedAttribute() const { |
| // Sub-widget elements can be selected if not disabled (native or ARIA) |
| return IsSubWidget() && Restriction() != kDisabled; |
| } |
| |
| bool AXObject::IsSubWidget() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kCell: |
| case ax::mojom::Role::kColumnHeader: |
| case ax::mojom::Role::kRowHeader: |
| case ax::mojom::Role::kColumn: |
| case ax::mojom::Role::kRow: { |
| // If it has an explicit ARIA role, it's a subwidget. |
| // |
| // Reasoning: |
| // Static table cells are not selectable, but ARIA grid cells |
| // and rows definitely are according to the spec. To support |
| // ARIA 1.0, it's sufficient to just check if there's any |
| // ARIA role at all, because if so then it must be a grid-related |
| // role so it must be selectable. |
| // |
| // TODO: an ARIA 1.1+ role of "cell", or a role of "row" inside |
| // an ARIA 1.1 role of "table", should not be selectable. We may |
| // need to create separate role enums for grid cells vs table cells |
| // to implement this. |
| if (AriaRoleAttribute() != ax::mojom::Role::kUnknown) |
| return true; |
| |
| // Otherwise it's only a subwidget if it's in a grid or treegrid, |
| // not in a table. |
| AXObject* parent = ParentObjectUnignored(); |
| while (parent && !parent->IsTableLikeRole()) |
| parent = parent->ParentObjectUnignored(); |
| if (parent && (parent->RoleValue() == ax::mojom::Role::kGrid || |
| parent->RoleValue() == ax::mojom::Role::kTreeGrid)) |
| return true; |
| return false; |
| } |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kMenuListOption: |
| case ax::mojom::Role::kTab: |
| case ax::mojom::Role::kTreeItem: |
| return true; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| bool AXObject::SupportsARIASetSizeAndPosInSet() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kArticle: |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kListItem: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuListOption: |
| case ax::mojom::Role::kRadioButton: |
| case ax::mojom::Role::kRow: |
| case ax::mojom::Role::kTab: |
| case ax::mojom::Role::kTreeItem: |
| return true; |
| default: |
| break; |
| } |
| |
| return false; |
| } |
| |
| // Simplify whitespace, but preserve a single leading and trailing whitespace |
| // character if it's present. |
| // static |
| String AXObject::CollapseWhitespace(const String& str) { |
| StringBuilder result; |
| if (!str.IsEmpty() && IsHTMLSpace<UChar>(str[0])) |
| result.Append(' '); |
| result.Append(str.SimplifyWhiteSpace(IsHTMLSpace<UChar>)); |
| if (!str.IsEmpty() && IsHTMLSpace<UChar>(str[str.length() - 1])) |
| result.Append(' '); |
| return result.ToString(); |
| } |
| |
| String AXObject::ComputedName() const { |
| ax::mojom::NameFrom name_from; |
| AXObject::AXObjectVector name_objects; |
| return GetName(name_from, &name_objects); |
| } |
| |
| String AXObject::GetName(ax::mojom::NameFrom& name_from, |
| AXObject::AXObjectVector* name_objects) const { |
| HeapHashSet<Member<const AXObject>> visited; |
| AXRelatedObjectVector related_objects; |
| String text = TextAlternative(false, false, visited, name_from, |
| &related_objects, nullptr); |
| |
| ax::mojom::Role role = RoleValue(); |
| if (!GetNode() || |
| (!IsHTMLBRElement(GetNode()) && role != ax::mojom::Role::kStaticText && |
| role != ax::mojom::Role::kInlineTextBox)) |
| text = CollapseWhitespace(text); |
| |
| if (name_objects) { |
| name_objects->clear(); |
| for (NameSourceRelatedObject* related_object : related_objects) |
| name_objects->push_back(related_object->object); |
| } |
| |
| return text; |
| } |
| |
| String AXObject::GetName(NameSources* name_sources) const { |
| AXObjectSet visited; |
| ax::mojom::NameFrom tmp_name_from; |
| AXRelatedObjectVector tmp_related_objects; |
| String text = TextAlternative(false, false, visited, tmp_name_from, |
| &tmp_related_objects, name_sources); |
| text = text.SimplifyWhiteSpace(IsHTMLSpace<UChar>); |
| return text; |
| } |
| |
| String AXObject::RecursiveTextAlternative(const AXObject& ax_obj, |
| bool in_aria_labelled_by_traversal, |
| AXObjectSet& visited) { |
| ax::mojom::NameFrom tmp_name_from; |
| return RecursiveTextAlternative(ax_obj, in_aria_labelled_by_traversal, |
| visited, tmp_name_from); |
| } |
| |
| String AXObject::RecursiveTextAlternative(const AXObject& ax_obj, |
| bool in_aria_labelled_by_traversal, |
| AXObjectSet& visited, |
| ax::mojom::NameFrom& name_from) { |
| if (visited.Contains(&ax_obj) && !in_aria_labelled_by_traversal) |
| return String(); |
| |
| return ax_obj.TextAlternative(true, in_aria_labelled_by_traversal, visited, |
| name_from, nullptr, nullptr); |
| } |
| |
| bool AXObject::IsHiddenForTextAlternativeCalculation() const { |
| if (AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden)) |
| return false; |
| |
| if (GetLayoutObject()) |
| return GetLayoutObject()->Style()->Visibility() != EVisibility::kVisible; |
| |
| // This is an obscure corner case: if a node has no LayoutObject, that means |
| // it's not rendered, but we still may be exploring it as part of a text |
| // alternative calculation, for example if it was explicitly referenced by |
| // aria-labelledby. So we need to explicitly call the style resolver to check |
| // whether it's invisible or display:none, rather than relying on the style |
| // cached in the LayoutObject. |
| Document* document = GetDocument(); |
| if (!document || !document->GetFrame()) |
| return false; |
| if (Node* node = GetNode()) { |
| if (node->isConnected() && node->IsElementNode()) { |
| scoped_refptr<ComputedStyle> style = |
| document->EnsureStyleResolver().StyleForElement(ToElement(node)); |
| return style->Display() == EDisplay::kNone || |
| style->Visibility() != EVisibility::kVisible; |
| } |
| } |
| return false; |
| } |
| |
| String AXObject::AriaTextAlternative(bool recursive, |
| bool in_aria_labelled_by_traversal, |
| AXObjectSet& visited, |
| ax::mojom::NameFrom& name_from, |
| AXRelatedObjectVector* related_objects, |
| NameSources* name_sources, |
| bool* found_text_alternative) const { |
| String text_alternative; |
| bool already_visited = visited.Contains(this); |
| visited.insert(this); |
| |
| // Step 2A from: http://www.w3.org/TR/accname-aam-1.1 |
| // If you change this logic, update AXNodeObject::nameFromLabelElement, too. |
| if (!in_aria_labelled_by_traversal && |
| IsHiddenForTextAlternativeCalculation()) { |
| *found_text_alternative = true; |
| return String(); |
| } |
| |
| // Step 2B from: http://www.w3.org/TR/accname-aam-1.1 |
| // If you change this logic, update AXNodeObject::nameFromLabelElement, too. |
| if (!in_aria_labelled_by_traversal && !already_visited) { |
| name_from = ax::mojom::NameFrom::kRelatedElement; |
| |
| // Check AOM property first. |
| HeapVector<Member<Element>> elements; |
| if (HasAOMProperty(AOMRelationListProperty::kLabeledBy, elements)) { |
| if (name_sources) { |
| name_sources->push_back( |
| NameSource(*found_text_alternative, kAriaLabelledbyAttr)); |
| name_sources->back().type = name_from; |
| } |
| |
| // Operate on a copy of |visited| so that if |nameSources| is not null, |
| // the set of visited objects is preserved unmodified for future |
| // calculations. |
| AXObjectSet visited_copy = visited; |
| text_alternative = |
| TextFromElements(true, visited_copy, elements, related_objects); |
| if (!text_alternative.IsNull()) { |
| if (name_sources) { |
| NameSource& source = name_sources->back(); |
| source.type = name_from; |
| source.related_objects = *related_objects; |
| source.text = text_alternative; |
| *found_text_alternative = true; |
| } else { |
| *found_text_alternative = true; |
| return text_alternative; |
| } |
| } else if (name_sources) { |
| name_sources->back().invalid = true; |
| } |
| } else { |
| // Now check ARIA attribute |
| const QualifiedName& attr = |
| HasAttribute(kAriaLabeledbyAttr) && !HasAttribute(kAriaLabelledbyAttr) |
| ? kAriaLabeledbyAttr |
| : kAriaLabelledbyAttr; |
| |
| if (name_sources) { |
| name_sources->push_back(NameSource(*found_text_alternative, attr)); |
| name_sources->back().type = name_from; |
| } |
| |
| const AtomicString& aria_labelledby = GetAttribute(attr); |
| if (!aria_labelledby.IsNull()) { |
| if (name_sources) |
| name_sources->back().attribute_value = aria_labelledby; |
| |
| // Operate on a copy of |visited| so that if |nameSources| is not null, |
| // the set of visited objects is preserved unmodified for future |
| // calculations. |
| AXObjectSet visited_copy = visited; |
| Vector<String> ids; |
| text_alternative = |
| TextFromAriaLabelledby(visited_copy, related_objects, ids); |
| if (!ids.IsEmpty()) |
| AXObjectCache().UpdateReverseRelations(this, ids); |
| if (!text_alternative.IsNull()) { |
| if (name_sources) { |
| NameSource& source = name_sources->back(); |
| source.type = name_from; |
| source.related_objects = *related_objects; |
| source.text = text_alternative; |
| *found_text_alternative = true; |
| } else { |
| *found_text_alternative = true; |
| return text_alternative; |
| } |
| } else if (name_sources) { |
| name_sources->back().invalid = true; |
| } |
| } |
| } |
| } |
| |
| // Step 2C from: http://www.w3.org/TR/accname-aam-1.1 |
| // If you change this logic, update AXNodeObject::nameFromLabelElement, too. |
| name_from = ax::mojom::NameFrom::kAttribute; |
| if (name_sources) { |
| name_sources->push_back( |
| NameSource(*found_text_alternative, kAriaLabelAttr)); |
| name_sources->back().type = name_from; |
| } |
| const AtomicString& aria_label = |
| GetAOMPropertyOrARIAAttribute(AOMStringProperty::kLabel); |
| if (!aria_label.IsEmpty()) { |
| text_alternative = aria_label; |
| |
| if (name_sources) { |
| NameSource& source = name_sources->back(); |
| source.text = text_alternative; |
| source.attribute_value = aria_label; |
| *found_text_alternative = true; |
| } else { |
| *found_text_alternative = true; |
| return text_alternative; |
| } |
| } |
| |
| return text_alternative; |
| } |
| |
| String AXObject::TextFromElements( |
| bool in_aria_labelledby_traversal, |
| AXObjectSet& visited, |
| HeapVector<Member<Element>>& elements, |
| AXRelatedObjectVector* related_objects) const { |
| StringBuilder accumulated_text; |
| bool found_valid_element = false; |
| AXRelatedObjectVector local_related_objects; |
| |
| for (const auto& element : elements) { |
| AXObject* ax_element = AXObjectCache().GetOrCreate(element); |
| if (ax_element) { |
| found_valid_element = true; |
| |
| String result = RecursiveTextAlternative( |
| *ax_element, in_aria_labelledby_traversal, visited); |
| local_related_objects.push_back( |
| new NameSourceRelatedObject(ax_element, result)); |
| if (!result.IsEmpty()) { |
| if (!accumulated_text.IsEmpty()) |
| accumulated_text.Append(' '); |
| accumulated_text.Append(result); |
| } |
| } |
| } |
| if (!found_valid_element) |
| return String(); |
| if (related_objects) |
| *related_objects = local_related_objects; |
| return accumulated_text.ToString(); |
| } |
| |
| void AXObject::TokenVectorFromAttribute(Vector<String>& tokens, |
| const QualifiedName& attribute) const { |
| Node* node = this->GetNode(); |
| if (!node || !node->IsElementNode()) |
| return; |
| |
| String attribute_value = GetAttribute(attribute).GetString(); |
| if (attribute_value.IsEmpty()) |
| return; |
| |
| attribute_value = attribute_value.SimplifyWhiteSpace(); |
| attribute_value.Split(' ', tokens); |
| } |
| |
| void AXObject::ElementsFromAttribute(HeapVector<Member<Element>>& elements, |
| const QualifiedName& attribute, |
| Vector<String>& ids) const { |
| TokenVectorFromAttribute(ids, attribute); |
| if (ids.IsEmpty()) |
| return; |
| |
| TreeScope& scope = GetNode()->GetTreeScope(); |
| for (const auto& id : ids) { |
| if (Element* id_element = scope.getElementById(AtomicString(id))) |
| elements.push_back(id_element); |
| } |
| } |
| |
| void AXObject::AriaLabelledbyElementVector( |
| HeapVector<Member<Element>>& elements, |
| Vector<String>& ids) const { |
| // Try both spellings, but prefer aria-labelledby, which is the official spec. |
| ElementsFromAttribute(elements, kAriaLabelledbyAttr, ids); |
| if (!ids.size()) |
| ElementsFromAttribute(elements, kAriaLabeledbyAttr, ids); |
| } |
| |
| String AXObject::TextFromAriaLabelledby(AXObjectSet& visited, |
| AXRelatedObjectVector* related_objects, |
| Vector<String>& ids) const { |
| HeapVector<Member<Element>> elements; |
| AriaLabelledbyElementVector(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, kAriaDescribedbyAttr, ids); |
| return TextFromElements(true, visited, elements, related_objects); |
| } |
| |
| RGBA32 AXObject::BackgroundColor() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_background_color_; |
| } |
| |
| AccessibilityOrientation AXObject::Orientation() const { |
| // In ARIA 1.1, the default value for aria-orientation changed from |
| // horizontal to undefined. |
| return kAccessibilityOrientationUndefined; |
| } |
| |
| void AXObject::Markers(Vector<DocumentMarker::MarkerType>&, |
| Vector<AXRange>&) const {} |
| |
| void AXObject::TextCharacterOffsets(Vector<int>&) const {} |
| |
| void AXObject::GetWordBoundaries(Vector<AXRange>&) const {} |
| |
| ax::mojom::DefaultActionVerb AXObject::Action() const { |
| Element* action_element = ActionElement(); |
| if (!action_element) |
| return ax::mojom::DefaultActionVerb::kNone; |
| |
| // TODO(dmazzoni): Ensure that combo box text field is handled here. |
| if (IsTextControl()) |
| return ax::mojom::DefaultActionVerb::kActivate; |
| |
| if (IsCheckable()) { |
| return CheckedState() != ax::mojom::CheckedState::kTrue |
| ? ax::mojom::DefaultActionVerb::kCheck |
| : ax::mojom::DefaultActionVerb::kUncheck; |
| } |
| |
| switch (RoleValue()) { |
| case ax::mojom::Role::kButton: |
| case ax::mojom::Role::kDisclosureTriangle: |
| case ax::mojom::Role::kToggleButton: |
| return ax::mojom::DefaultActionVerb::kPress; |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuListOption: |
| return ax::mojom::DefaultActionVerb::kSelect; |
| case ax::mojom::Role::kLink: |
| return ax::mojom::DefaultActionVerb::kJump; |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kPopUpButton: |
| return ax::mojom::DefaultActionVerb::kOpen; |
| default: |
| if (action_element == GetNode()) |
| return ax::mojom::DefaultActionVerb::kClick; |
| return ax::mojom::DefaultActionVerb::kClickAncestor; |
| } |
| } |
| |
| bool AXObject::AriaPressedIsPresent() const { |
| AtomicString result; |
| return HasAOMPropertyOrARIAAttribute(AOMStringProperty::kPressed, result); |
| } |
| |
| bool AXObject::AriaCheckedIsPresent() const { |
| AtomicString result; |
| return HasAOMPropertyOrARIAAttribute(AOMStringProperty::kChecked, result); |
| } |
| |
| bool AXObject::SupportsARIAExpanded() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kAlertDialog: |
| case ax::mojom::Role::kAlert: |
| case ax::mojom::Role::kArticle: |
| case ax::mojom::Role::kBanner: |
| case ax::mojom::Role::kButton: |
| case ax::mojom::Role::kCell: |
| case ax::mojom::Role::kColumnHeader: |
| case ax::mojom::Role::kComboBoxGrouping: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kComplementary: |
| case ax::mojom::Role::kContentInfo: |
| case ax::mojom::Role::kDefinition: |
| case ax::mojom::Role::kDialog: |
| case ax::mojom::Role::kDirectory: |
| case ax::mojom::Role::kDisclosureTriangle: |
| case ax::mojom::Role::kDocument: |
| case ax::mojom::Role::kFeed: |
| case ax::mojom::Role::kFigure: |
| case ax::mojom::Role::kForm: |
| case ax::mojom::Role::kGrid: |
| case ax::mojom::Role::kGroup: |
| case ax::mojom::Role::kHeading: |
| case ax::mojom::Role::kImage: |
| case ax::mojom::Role::kLayoutTable: |
| case ax::mojom::Role::kList: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kListItem: |
| case ax::mojom::Role::kLink: |
| case ax::mojom::Role::kLog: |
| case ax::mojom::Role::kMain: |
| case ax::mojom::Role::kMarquee: |
| case ax::mojom::Role::kMath: |
| case ax::mojom::Role::kMenu: |
| case ax::mojom::Role::kMenuBar: |
| case ax::mojom::Role::kMenuButton: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kNavigation: |
| case ax::mojom::Role::kNote: |
| case ax::mojom::Role::kProgressIndicator: |
| case ax::mojom::Role::kRadioGroup: |
| case ax::mojom::Role::kRegion: |
| case ax::mojom::Role::kRow: |
| case ax::mojom::Role::kRowHeader: |
| case ax::mojom::Role::kSearch: |
| case ax::mojom::Role::kStatus: |
| case ax::mojom::Role::kTab: |
| case ax::mojom::Role::kTable: |
| case ax::mojom::Role::kTabPanel: |
| case ax::mojom::Role::kTerm: |
| case ax::mojom::Role::kTextFieldWithComboBox: |
| case ax::mojom::Role::kTimer: |
| case ax::mojom::Role::kToolbar: |
| case ax::mojom::Role::kTooltip: |
| case ax::mojom::Role::kTree: |
| case ax::mojom::Role::kTreeGrid: |
| case ax::mojom::Role::kTreeItem: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::HasGlobalARIAAttribute() const { |
| if (!GetElement()) |
| return false; |
| |
| AttributeCollection attributes = GetElement()->AttributesWithoutUpdate(); |
| for (const Attribute& attr : attributes) { |
| // Attributes cache their uppercase names. |
| auto name = attr.GetName().LocalNameUpper(); |
| if (!name.StartsWith("ARIA")) |
| continue; |
| 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::SupportsRangeValue() const { |
| return IsProgressIndicator() || IsMeter() || IsSlider() || IsScrollbar() || |
| IsSpinButton() || IsMoveableSplitter(); |
| } |
| |
| int AXObject::IndexInParent() const { |
| if (!ParentObjectUnignored()) |
| return 0; |
| |
| const AXObjectVector& siblings = ParentObjectUnignored()->Children(); |
| size_t index = siblings.Find(this); |
| return (index == kNotFound) ? 0 : static_cast<int>(index); |
| } |
| |
| bool AXObject::IsLiveRegion() const { |
| const AtomicString& live_region = LiveRegionStatus(); |
| return !live_region.IsEmpty() && !EqualIgnoringASCIICase(live_region, "off"); |
| } |
| |
| AXRestriction AXObject::Restriction() const { |
| // According to ARIA, all elements of the base markup can be disabled. |
| // According to CORE-AAM, any focusable descendant of aria-disabled |
| // ancestor is also disabled. |
| bool is_disabled; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kDisabled, |
| is_disabled)) { |
| // Has aria-disabled, overrides native markup determining disabled. |
| if (is_disabled) |
| return kDisabled; |
| } else if (CanSetFocusAttribute() && IsDescendantOfDisabledNode()) { |
| // No aria-disabled, but other markup says it's disabled. |
| return kDisabled; |
| } |
| |
| // Check aria-readonly if supported by current role. |
| bool is_read_only; |
| if (SupportsARIAReadOnly() && |
| HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kReadOnly, |
| is_read_only)) { |
| // ARIA overrides other readonly state markup. |
| return is_read_only ? kReadOnly : kNone; |
| } |
| |
| // This is a node that is not readonly and not disabled. |
| return kNone; |
| } |
| |
| ax::mojom::Role AXObject::DetermineAccessibilityRole() { |
| aria_role_ = DetermineAriaRoleAttribute(); |
| return aria_role_; |
| } |
| |
| ax::mojom::Role AXObject::AriaRoleAttribute() const { |
| return aria_role_; |
| } |
| |
| ax::mojom::Role AXObject::DetermineAriaRoleAttribute() const { |
| const AtomicString& aria_role = |
| GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole); |
| if (aria_role.IsNull() || aria_role.IsEmpty()) |
| return ax::mojom::Role::kUnknown; |
| |
| ax::mojom::Role role = AriaRoleToWebCoreRole(aria_role); |
| |
| // ARIA states if an item can get focus, it should not be presentational. |
| // It also states user agents should ignore the presentational role if |
| // the element has global ARIA states and properties. |
| if ((role == ax::mojom::Role::kNone || |
| role == ax::mojom::Role::kPresentational) && |
| (CanSetFocusAttribute() || HasGlobalARIAAttribute())) |
| return ax::mojom::Role::kUnknown; |
| |
| if (role == ax::mojom::Role::kButton) |
| role = ButtonRoleType(); |
| |
| role = RemapAriaRoleDueToParent(role); |
| |
| // Distinguish between different uses of the "combobox" role: |
| // |
| // ax::mojom::Role::kComboBoxGrouping: |
| // <div role="combobox"><input></div> |
| // ax::mojom::Role::kTextFieldWithComboBox: |
| // <input role="combobox"> |
| // ax::mojom::Role::kComboBoxMenuButton: |
| // <div tabindex=0 role="combobox">Select</div> |
| if (role == ax::mojom::Role::kComboBoxGrouping) { |
| if (IsNativeTextControl()) |
| role = ax::mojom::Role::kTextFieldWithComboBox; |
| else if (GetElement() && GetElement()->SupportsFocus()) |
| role = ax::mojom::Role::kComboBoxMenuButton; |
| } |
| |
| if (role != ax::mojom::Role::kUnknown) |
| return role; |
| |
| return ax::mojom::Role::kUnknown; |
| } |
| |
| ax::mojom::Role AXObject::RemapAriaRoleDueToParent(ax::mojom::Role role) const { |
| // Some objects change their role based on their parent. |
| // However, asking for the unignoredParent calls accessibilityIsIgnored(), |
| // which can trigger a loop. While inside the call stack of creating an |
| // element, we need to avoid accessibilityIsIgnored(). |
| // https://bugs.webkit.org/show_bug.cgi?id=65174 |
| |
| if (role != ax::mojom::Role::kListBoxOption && |
| role != ax::mojom::Role::kMenuItem) |
| return role; |
| |
| for (AXObject* parent = ParentObject(); |
| parent && !parent->AccessibilityIsIgnored(); |
| parent = parent->ParentObject()) { |
| ax::mojom::Role parent_aria_role = parent->AriaRoleAttribute(); |
| |
| // Selects and listboxes both have options as child roles, but they map to |
| // different roles within WebCore. |
| if (role == ax::mojom::Role::kListBoxOption && |
| parent_aria_role == ax::mojom::Role::kMenu) |
| return ax::mojom::Role::kMenuItem; |
| // An aria "menuitem" may map to MenuButton or MenuItem depending on its |
| // parent. |
| if (role == ax::mojom::Role::kMenuItem && |
| parent_aria_role == ax::mojom::Role::kGroup) |
| return ax::mojom::Role::kMenuButton; |
| |
| // If the parent had a different role, then we don't need to continue |
| // searching up the chain. |
| if (parent_aria_role != ax::mojom::Role::kUnknown) |
| break; |
| } |
| |
| return role; |
| } |
| |
| bool AXObject::IsEditableRoot() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_is_editable_root_; |
| } |
| |
| AXObject* AXObject::LiveRegionRoot() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_live_region_root_; |
| } |
| |
| bool AXObject::LiveRegionAtomic() const { |
| bool atomic = false; |
| if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kAtomic, atomic)) |
| return atomic; |
| |
| // ARIA roles "alert" and "status" should have an implicit aria-atomic value |
| // of true. |
| return RoleValue() == ax::mojom::Role::kAlert || |
| RoleValue() == ax::mojom::Role::kStatus; |
| } |
| |
| const AtomicString& AXObject::ContainerLiveRegionStatus() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_live_region_root_ ? cached_live_region_root_->LiveRegionStatus() |
| : g_null_atom; |
| } |
| |
| const AtomicString& AXObject::ContainerLiveRegionRelevant() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_live_region_root_ |
| ? cached_live_region_root_->LiveRegionRelevant() |
| : g_null_atom; |
| } |
| |
| bool AXObject::ContainerLiveRegionAtomic() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_live_region_root_ && |
| cached_live_region_root_->LiveRegionAtomic(); |
| } |
| |
| bool AXObject::ContainerLiveRegionBusy() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_live_region_root_ && |
| cached_live_region_root_->AOMPropertyOrARIAAttributeIsTrue( |
| AOMBooleanProperty::kBusy); |
| } |
| |
| AXObject* AXObject::ElementAccessibilityHitTest(const IntPoint& point) const { |
| // Check if there are any mock elements that need to be handled. |
| for (const auto& child : children_) { |
| if (child->IsMockObject() && |
| child->GetBoundsInFrameCoordinates().Contains(point)) |
| return child->ElementAccessibilityHitTest(point); |
| } |
| |
| return const_cast<AXObject*>(this); |
| } |
| |
| AXObject::AncestorsIterator AXObject::AncestorsBegin() { |
| AXObject* parent = ParentObjectUnignored(); |
| if (parent) |
| return AXObject::AncestorsIterator(*parent); |
| return AncestorsEnd(); |
| } |
| |
| AXObject::AncestorsIterator AXObject::AncestorsEnd() { |
| return AXObject::AncestorsIterator(); |
| } |
| |
| AXObject::InOrderTraversalIterator AXObject::GetInOrderTraversalIterator() { |
| return InOrderTraversalIterator(*this); |
| } |
| |
| int AXObject::ChildCount() const { |
| return HasIndirectChildren() ? 0 : static_cast<int>(Children().size()); |
| } |
| |
| const AXObject::AXObjectVector& AXObject::Children() const { |
| return const_cast<AXObject*>(this)->Children(); |
| } |
| |
| const AXObject::AXObjectVector& AXObject::Children() { |
| UpdateChildrenIfNecessary(); |
| |
| return children_; |
| } |
| |
| AXObject* AXObject::FirstChild() const { |
| return ChildCount() ? *Children().begin() : nullptr; |
| } |
| |
| AXObject* AXObject::LastChild() const { |
| return ChildCount() ? *(Children().end() - 1) : nullptr; |
| } |
| |
| AXObject* AXObject::DeepestFirstChild() const { |
| if (!ChildCount()) |
| return nullptr; |
| |
| AXObject* deepest_child = FirstChild(); |
| while (deepest_child->ChildCount()) |
| deepest_child = deepest_child->FirstChild(); |
| |
| return deepest_child; |
| } |
| |
| AXObject* AXObject::DeepestLastChild() const { |
| if (!ChildCount()) |
| return nullptr; |
| |
| AXObject* deepest_child = LastChild(); |
| while (deepest_child->ChildCount()) |
| deepest_child = deepest_child->LastChild(); |
| |
| return deepest_child; |
| } |
| |
| bool AXObject::IsAncestorOf(const AXObject& descendant) const { |
| return descendant.IsDescendantOf(*this); |
| } |
| |
| bool AXObject::IsDescendantOf(const AXObject& ancestor) const { |
| const AXObject* parent = ParentObject(); |
| while (parent && parent != &ancestor) |
| parent = parent->ParentObject(); |
| return !!parent; |
| } |
| |
| AXObject* AXObject::NextSibling() const { |
| AXObject* parent = ParentObjectUnignored(); |
| if (!parent) |
| return nullptr; |
| |
| if (AccessibilityIsIgnored()) |
| NOTREACHED() << "We don't support finding siblings for ignored objects."; |
| |
| if (IndexInParent() < parent->ChildCount() - 1) |
| return *(parent->Children().begin() + IndexInParent() + 1); |
| |
| return nullptr; |
| } |
| |
| AXObject* AXObject::PreviousSibling() const { |
| AXObject* parent = ParentObjectUnignored(); |
| if (!parent) |
| return nullptr; |
| |
| if (AccessibilityIsIgnored()) |
| NOTREACHED() << "We don't support finding siblings for ignored objects."; |
| |
| if (IndexInParent() > 0) |
| return *(parent->Children().begin() + IndexInParent() - 1); |
| |
| return nullptr; |
| } |
| |
| AXObject* AXObject::NextInTreeObject(bool can_wrap_to_first_element) const { |
| // We don't support finding the next sibling for an ignored object, so we |
| // return the next sibling of the deepest unignored ancestor, which is the |
| // next best thing that doesn't violate next-in-order semantics. |
| if (!AccessibilityIsIgnored()) { |
| if (ChildCount()) |
| return FirstChild(); |
| |
| if (NextSibling()) |
| return NextSibling(); |
| } |
| |
| AXObject* current_object = const_cast<AXObject*>(this); |
| while (current_object->ParentObjectUnignored()) { |
| current_object = current_object->ParentObjectUnignored(); |
| AXObject* sibling = current_object->NextSibling(); |
| if (sibling) |
| return sibling; |
| } |
| |
| return can_wrap_to_first_element ? current_object : nullptr; |
| } |
| |
| AXObject* AXObject::PreviousInTreeObject(bool can_wrap_to_last_element) const { |
| // We don't support finding the previous sibling for an ignored object, so we |
| // return the deepest unignored ancestor instead, which is the next best thing |
| // that doesn't violate previous-in-order semantics. |
| AXObject* sibling = AccessibilityIsIgnored() ? nullptr : PreviousSibling(); |
| if (!sibling) { |
| if (ParentObjectUnignored()) |
| return ParentObjectUnignored(); |
| return can_wrap_to_last_element ? DeepestLastChild() : nullptr; |
| } |
| |
| if (sibling->ChildCount()) |
| return sibling->DeepestLastChild(); |
| |
| return sibling; |
| } |
| |
| AXObject* AXObject::ParentObject() const { |
| if (IsDetached()) |
| return nullptr; |
| |
| if (parent_) |
| return parent_; |
| |
| if (AXObjectCache().IsAriaOwned(this)) |
| return AXObjectCache().GetAriaOwnedParent(this); |
| |
| return ComputeParent(); |
| } |
| |
| AXObject* AXObject::ParentObjectIfExists() const { |
| if (IsDetached()) |
| return nullptr; |
| |
| if (parent_) |
| return parent_; |
| |
| return ComputeParentIfExists(); |
| } |
| |
| AXObject* AXObject::ParentObjectUnignored() const { |
| AXObject* parent; |
| for (parent = ParentObject(); parent && parent->AccessibilityIsIgnored(); |
| parent = parent->ParentObject()) { |
| } |
| |
| return parent; |
| } |
| |
| // Container widgets are those that a user tabs into and arrows around |
| // sub-widgets |
| bool AXObject::IsContainerWidget() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kComboBoxGrouping: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kGrid: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kMenuBar: |
| case ax::mojom::Role::kMenu: |
| case ax::mojom::Role::kRadioGroup: |
| case ax::mojom::Role::kSpinButton: |
| case ax::mojom::Role::kTabList: |
| case ax::mojom::Role::kToolbar: |
| case ax::mojom::Role::kTreeGrid: |
| case ax::mojom::Role::kTree: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| AXObject* AXObject::ContainerWidget() const { |
| AXObject* ancestor = ParentObjectUnignored(); |
| while (ancestor && !ancestor->IsContainerWidget()) |
| ancestor = ancestor->ParentObjectUnignored(); |
| |
| return ancestor; |
| } |
| |
| void AXObject::UpdateChildrenIfNecessary() { |
| if (!HasChildren()) |
| AddChildren(); |
| } |
| |
| void AXObject::ClearChildren() { |
| // Detach all weak pointers from objects to their parents. |
| for (const auto& child : children_) |
| child->DetachFromParent(); |
| |
| children_.clear(); |
| have_children_ = false; |
| } |
| |
| void AXObject::AddAccessibleNodeChildren() { |
| Element* element = GetElement(); |
| if (!element) |
| return; |
| |
| AccessibleNode* accessible_node = element->ExistingAccessibleNode(); |
| if (!accessible_node) |
| return; |
| |
| for (const auto& child : accessible_node->GetChildren()) |
| children_.push_back(AXObjectCache().GetOrCreate(child)); |
| } |
| |
| Element* AXObject::GetElement() const { |
| Node* node = GetNode(); |
| return node && node->IsElementNode() ? ToElement(node) : nullptr; |
| } |
| |
| Document* AXObject::GetDocument() const { |
| LocalFrameView* frame_view = DocumentFrameView(); |
| if (!frame_view) |
| return nullptr; |
| |
| return frame_view->GetFrame().GetDocument(); |
| } |
| |
| LocalFrameView* AXObject::DocumentFrameView() const { |
| const AXObject* object = this; |
| while (object && !object->IsAXLayoutObject()) |
| object = object->ParentObject(); |
| |
| if (!object) |
| return nullptr; |
| |
| return object->DocumentFrameView(); |
| } |
| |
| AtomicString AXObject::Language() const { |
| // This method is used when the style engine is either not available on this |
| // object, e.g. for canvas fallback content, or is unable to determine the |
| // document's language. We use the following signals to detect the element's |
| // language, in decreasing priority: |
| // 1. The [language of a node] as defined in HTML, if known. |
| // 2. The list of languages the browser sends in the [Accept-Language] header. |
| // 3. The browser's default language. |
| |
| const AtomicString& lang = GetAttribute(kLangAttr); |
| if (!lang.IsEmpty()) |
| return lang; |
| |
| AXObject* parent = ParentObject(); |
| if (parent) |
| return parent->Language(); |
| |
| const Document* document = GetDocument(); |
| if (document) { |
| // Fall back to the first content language specified in the meta tag. |
| // This is not part of what the HTML5 Standard suggests but it still appears |
| // to be necessary. |
| if (document->ContentLanguage()) { |
| const String content_languages = document->ContentLanguage(); |
| Vector<String> languages; |
| content_languages.Split(',', languages); |
| if (!languages.IsEmpty()) |
| return AtomicString(languages[0].StripWhiteSpace()); |
| } |
| |
| if (document->GetPage()) { |
| // Use the first accept language preference if present. |
| const String accept_languages = |
| document->GetPage()->GetChromeClient().AcceptLanguages(); |
| Vector<String> languages; |
| accept_languages.Split(',', languages); |
| if (!languages.IsEmpty()) |
| return AtomicString(languages[0].StripWhiteSpace()); |
| } |
| } |
| |
| // As a last resort, return the default language of the browser's UI. |
| AtomicString default_language = DefaultLanguage(); |
| return default_language; |
| } |
| |
| bool AXObject::HasAttribute(const QualifiedName& attribute) const { |
| if (Element* element = GetElement()) |
| return element->FastHasAttribute(attribute); |
| return false; |
| } |
| |
| const AtomicString& AXObject::GetAttribute( |
| const QualifiedName& attribute) const { |
| if (Element* element = GetElement()) |
| return element->FastGetAttribute(attribute); |
| return g_null_atom; |
| } |
| |
| // |
| // Scrollable containers. |
| // |
| |
| bool AXObject::IsScrollableContainer() const { |
| return !!GetScrollableAreaIfScrollable(); |
| } |
| |
| IntPoint AXObject::GetScrollOffset() const { |
| ScrollableArea* area = GetScrollableAreaIfScrollable(); |
| if (!area) |
| return IntPoint(); |
| |
| return IntPoint(area->ScrollOffsetInt().Width(), |
| area->ScrollOffsetInt().Height()); |
| } |
| |
| IntPoint AXObject::MinimumScrollOffset() const { |
| ScrollableArea* area = GetScrollableAreaIfScrollable(); |
| if (!area) |
| return IntPoint(); |
| |
| return IntPoint(area->MinimumScrollOffsetInt().Width(), |
| area->MinimumScrollOffsetInt().Height()); |
| } |
| |
| IntPoint AXObject::MaximumScrollOffset() const { |
| ScrollableArea* area = GetScrollableAreaIfScrollable(); |
| if (!area) |
| return IntPoint(); |
| |
| return IntPoint(area->MaximumScrollOffsetInt().Width(), |
| area->MaximumScrollOffsetInt().Height()); |
| } |
| |
| void AXObject::SetScrollOffset(const IntPoint& offset) const { |
| ScrollableArea* area = GetScrollableAreaIfScrollable(); |
| if (!area) |
| return; |
| |
| // TODO(bokan): This should potentially be a UserScroll. |
| area->SetScrollOffset(ScrollOffset(offset.X(), offset.Y()), |
| kProgrammaticScroll); |
| } |
| |
| bool AXObject::IsTableLikeRole() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kLayoutTable: |
| case ax::mojom::Role::kTable: |
| case ax::mojom::Role::kGrid: |
| case ax::mojom::Role::kTreeGrid: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::IsTableRowLikeRole() const { |
| return RoleValue() == ax::mojom::Role::kRow || |
| RoleValue() == ax::mojom::Role::kLayoutTableRow; |
| } |
| |
| bool AXObject::IsTableCellLikeRole() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kLayoutTableCell: |
| case ax::mojom::Role::kCell: |
| case ax::mojom::Role::kColumnHeader: |
| case ax::mojom::Role::kRowHeader: |
| return true; |
| default: |
| return false; |
| } |
| } |
| |
| unsigned AXObject::ColumnCount() const { |
| if (!IsTableLikeRole()) |
| return 0; |
| |
| unsigned max_column_count = 0; |
| for (const auto& row : TableRowChildren()) { |
| unsigned column_count = row->TableCellChildren().size(); |
| max_column_count = std::max(column_count, max_column_count); |
| } |
| |
| return max_column_count; |
| } |
| |
| unsigned AXObject::RowCount() const { |
| if (!IsTableLikeRole()) |
| return 0; |
| |
| return TableRowChildren().size(); |
| } |
| |
| void AXObject::ColumnHeaders(AXObjectVector& headers) const { |
| if (!IsTableLikeRole()) |
| return; |
| |
| for (const auto& row : TableRowChildren()) { |
| for (const auto& cell : row->TableCellChildren()) { |
| if (cell->RoleValue() == ax::mojom::Role::kColumnHeader) |
| headers.push_back(cell); |
| } |
| } |
| } |
| |
| void AXObject::RowHeaders(AXObjectVector& headers) const { |
| if (!IsTableLikeRole()) |
| return; |
| |
| for (const auto& row : TableRowChildren()) { |
| for (const auto& cell : row->TableCellChildren()) { |
| if (cell->RoleValue() == ax::mojom::Role::kRowHeader) |
| headers.push_back(cell); |
| } |
| } |
| } |
| |
| AXObject* AXObject::CellForColumnAndRow(unsigned target_column_index, |
| unsigned target_row_index) const { |
| if (!IsTableLikeRole()) |
| return nullptr; |
| |
| // Note that this code is only triggered if this is not a LayoutTable, |
| // i.e. it's an ARIA grid/table. |
| // |
| // TODO(dmazzoni): delete this code or rename it "for testing only" |
| // since it's only needed for Blink layout tests and not for production. |
| unsigned row_index = 0; |
| for (const auto& row : TableRowChildren()) { |
| unsigned column_index = 0; |
| for (const auto& cell : row->TableCellChildren()) { |
| if (target_column_index == column_index && target_row_index == row_index) |
| return cell; |
| column_index++; |
| } |
| row_index++; |
| } |
| |
| return nullptr; |
| } |
| |
| int AXObject::AriaColumnCount() const { |
| if (!IsTableLikeRole()) |
| return 0; |
| |
| int32_t col_count; |
| if (!HasAOMPropertyOrARIAAttribute(AOMIntProperty::kColCount, col_count)) |
| return 0; |
| |
| if (col_count > static_cast<int>(ColumnCount())) |
| return col_count; |
| |
| // Spec says that if all of the columns are present in the DOM, it |
| // is not necessary to set this attribute as the user agent can |
| // automatically calculate the total number of columns. |
| // It returns 0 in order not to set this attribute. |
| if (col_count == static_cast<int>(ColumnCount()) || col_count != -1) |
| return 0; |
| |
| return -1; |
| } |
| |
| int AXObject::AriaRowCount() const { |
| if (!IsTableLikeRole()) |
| return 0; |
| |
| int32_t row_count; |
| if (!HasAOMPropertyOrARIAAttribute(AOMIntProperty::kRowCount, row_count)) |
| return 0; |
| |
| if (row_count > static_cast<int>(RowCount())) |
| return row_count; |
| |
| // Spec says that if all of the rows are present in the DOM, it is |
| // not necessary to set this attribute as the user agent can |
| // automatically calculate the total number of rows. |
| // It returns 0 in order not to set this attribute. |
| if (row_count == (int)RowCount() || row_count != -1) |
| return 0; |
| |
| // In the spec, -1 explicitly means an unknown number of rows. |
| return -1; |
| } |
| |
| unsigned AXObject::ColumnIndex() const { |
| if (!IsTableCellLikeRole()) |
| return 0; |
| |
| const AXObject* row = TableRowParent(); |
| if (!row) |
| return 0; |
| |
| unsigned column_index = 0; |
| for (const auto& child : row->TableCellChildren()) { |
| if (child == this) |
| break; |
| column_index++; |
| } |
| return column_index; |
| } |
| |
| unsigned AXObject::RowIndex() const { |
| const AXObject* row = nullptr; |
| if (IsTableRowLikeRole()) |
| row = this; |
| else if (IsTableCellLikeRole()) |
| row = TableRowParent(); |
| |
| if (!row) |
| return 0; |
| |
| const AXObject* table = row->TableParent(); |
| if (!table) |
| return 0; |
| |
| unsigned row_index = 0; |
| for (const auto& child : table->TableRowChildren()) { |
| if (child == row) |
| break; |
| if (!child->IsTableRowLikeRole()) |
| continue; |
| row_index++; |
| } |
| return row_index; |
| } |
| |
| unsigned AXObject::ColumnSpan() const { |
| return IsTableCellLikeRole() ? 1 : 0; |
| } |
| |
| unsigned AXObject::RowSpan() const { |
| return IsTableCellLikeRole() ? 1 : 0; |
| } |
| |
| unsigned AXObject::AriaColumnIndex() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_aria_column_index_; |
| } |
| |
| unsigned AXObject::AriaRowIndex() const { |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_aria_row_index_; |
| } |
| |
| unsigned AXObject::ComputeAriaColumnIndex() const { |
| if (!IsTableCellLikeRole()) |
| return 0; |
| |
| // First see if it has an ARIA column index explicitly set. |
| uint32_t col_index; |
| if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kColIndex, col_index) && |
| col_index >= 1) { |
| return col_index; |
| } |
| |
| // Get the previous sibling. |
| // TODO(dmazzoni): this code depends on the DOM; move this code out of Blink |
| // and make it more general. |
| AXObject* previous = nullptr; |
| if (GetNode()) { |
| Node* previousNode = ElementTraversal::PreviousSibling(*GetNode()); |
| previous = AXObjectCache().GetOrCreate(previousNode); |
| } |
| |
| // It has a previous sibling, so if that cell has a column index, this one's |
| // index is one greater. |
| if (previous) { |
| col_index = previous->AriaColumnIndex(); |
| if (col_index) |
| return col_index + 1; |
| return 0; |
| } |
| |
| // No previous cell, so check the row to see if it sets a column index. |
| const AXObject* row = TableRowParent(); |
| if (!row) |
| return 0; |
| if (row->HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kColIndex, |
| col_index)) { |
| return col_index; |
| } |
| |
| // Otherwise there's no ARIA column index. |
| return 0; |
| } |
| |
| unsigned AXObject::ComputeAriaRowIndex() const { |
| if (!IsTableCellLikeRole() && !IsTableRowLikeRole()) |
| return 0; |
| |
| // First check if there's an ARIA row index explicitly set. |
| uint32_t row_index; |
| if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kRowIndex, row_index) && |
| row_index >= 1) { |
| return row_index; |
| } |
| |
| // If this is a cell, return the ARIA row index of the containing row. |
| if (IsTableCellLikeRole()) { |
| const AXObject* row = TableRowParent(); |
| if (row) |
| return row->AriaRowIndex(); |
| return 0; |
| } |
| |
| // Otherwise, this is a row. Find the previous sibling row. |
| // TODO(dmazzoni): this code depends on the DOM; move this code out of Blink |
| // and make it more general. |
| if (!GetNode()) |
| return 0; |
| Node* previousNode = ElementTraversal::PreviousSibling(*GetNode()); |
| AXObject* previous = AXObjectCache().GetOrCreate(previousNode); |
| if (!previous || !previous->IsTableRowLikeRole()) |
| return 0; |
| |
| // If the previous row has an ARIA row index, this one is the same index |
| // plus one. |
| row_index = previous->AriaRowIndex(); |
| if (row_index) |
| return row_index + 1; |
| |
| // Otherwise there's no ARIA row index. |
| return 0; |
| } |
| |
| AXObject::AXObjectVector AXObject::TableRowChildren() const { |
| AXObjectVector result; |
| for (const auto& child : Children()) { |
| if (child->IsTableRowLikeRole()) |
| result.push_back(child); |
| else if (child->RoleValue() == ax::mojom::Role::kGenericContainer) |
| result.AppendVector(child->TableRowChildren()); |
| } |
| return result; |
| } |
| |
| AXObject::AXObjectVector AXObject::TableCellChildren() const { |
| AXObjectVector result; |
| for (const auto& child : Children()) { |
| if (child->IsTableCellLikeRole()) |
| result.push_back(child); |
| else if (child->RoleValue() == ax::mojom::Role::kGenericContainer) |
| result.AppendVector(child->TableCellChildren()); |
| } |
| return result; |
| } |
| |
| const AXObject* AXObject::TableRowParent() const { |
| const AXObject* row = ParentObjectUnignored(); |
| while (row && !row->IsTableRowLikeRole() && |
| row->RoleValue() == ax::mojom::Role::kGenericContainer) |
| row = row->ParentObjectUnignored(); |
| return row; |
| } |
| |
| const AXObject* AXObject::TableParent() const { |
| const AXObject* table = ParentObjectUnignored(); |
| while (table && !table->IsTableLikeRole() && |
| table->RoleValue() == ax::mojom::Role::kGenericContainer) |
| table = table->ParentObjectUnignored(); |
| return table; |
| } |
| |
| void AXObject::GetRelativeBounds(AXObject** out_container, |
| FloatRect& out_bounds_in_container, |
| SkMatrix44& out_container_transform, |
| bool* clips_children) const { |
| *out_container = nullptr; |
| out_bounds_in_container = FloatRect(); |
| out_container_transform.setIdentity(); |
| |
| // First check if it has explicit bounds, for example if this element is tied |
| // to a canvas path. When explicit coordinates are provided, the ID of the |
| // explicit container element that the coordinates are relative to must be |
| // provided too. |
| if (!explicit_element_rect_.IsEmpty()) { |
| *out_container = AXObjectCache().ObjectFromAXID(explicit_container_id_); |
| if (*out_container) { |
| out_bounds_in_container = FloatRect(explicit_element_rect_); |
| return; |
| } |
| } |
| |
| LayoutObject* layout_object = LayoutObjectForRelativeBounds(); |
| if (!layout_object) |
| return; |
| |
| if (clips_children) { |
| if (IsWebArea()) |
| *clips_children = true; |
| else |
| *clips_children = layout_object->HasOverflowClip(); |
| } |
| |
| if (IsWebArea()) { |
| if (layout_object->GetFrame()->View()) { |
| out_bounds_in_container.SetSize( |
| FloatSize(layout_object->GetFrame()->View()->Size())); |
| } |
| return; |
| } |
| |
| // First compute the container. The container must be an ancestor in the |
| // accessibility tree, and its LayoutObject must be an ancestor in the layout |
| // tree. Get the first such ancestor that's either scrollable or has a paint |
| // layer. |
| AXObject* container = ParentObjectUnignored(); |
| LayoutObject* container_layout_object = nullptr; |
| if (layout_object->IsFixedPositioned()) { |
| // If it's a fixed position element, the container should simply be the |
| // root web area. |
| container = AXObjectCache().GetOrCreate(GetDocument()); |
| } else { |
| while (container) { |
| container_layout_object = container->GetLayoutObject(); |
| if (container_layout_object && container_layout_object->IsBox() && |
| layout_object->IsDescendantOf(container_layout_object)) { |
| if (container->IsScrollableContainer() || |
| container_layout_object->HasLayer()) { |
| if (layout_object->IsAbsolutePositioned()) { |
| // If it's absolutely positioned, the container must be the |
| // nearest positioned container, or the root. |
| if (container->IsWebArea()) |
| break; |
| if (container_layout_object->IsPositioned()) |
| break; |
| } else { |
| break; |
| } |
| } |
| } |
| |
| container = container->ParentObjectUnignored(); |
| } |
| } |
| |
| if (!container) |
| return; |
| *out_container = container; |
| out_bounds_in_container = |
| layout_object->LocalBoundingBoxRectForAccessibility(); |
| |
| // Frames need to take their border and padding into account so the |
| // child element's computed position will be correct. |
| if (layout_object->IsBox() && layout_object->GetNode() && |
| layout_object->GetNode()->IsFrameOwnerElement()) { |
| out_bounds_in_container = |
| FloatRect(ToLayoutBox(layout_object)->PhysicalContentBoxRect()); |
| } |
| |
| // If the container has a scroll offset, subtract that out because we want our |
| // bounds to be relative to the *unscrolled* position of the container object. |
| if (auto* scrollable_area = container->GetScrollableAreaIfScrollable()) |
| out_bounds_in_container.Move(scrollable_area->GetScrollOffset()); |
| |
| // Compute the transform between the container's coordinate space and this |
| // object. |
| TransformationMatrix transform = layout_object->LocalToAncestorTransform( |
| ToLayoutBoxModelObject(container_layout_object)); |
| |
| // If the transform is just a simple translation, apply that to the |
| // bounding box, but if it's a non-trivial transformation like a rotation, |
| // scaling, etc. then return the full matrix instead. |
| if (transform.IsIdentityOr2DTranslation()) { |
| out_bounds_in_container.Move(transform.To2DTranslation()); |
| } else { |
| out_container_transform = TransformationMatrix::ToSkMatrix44(transform); |
| } |
| } |
| |
| FloatRect AXObject::LocalBoundingBoxRectForAccessibility() { |
| if (!GetLayoutObject()) |
| return FloatRect(); |
| DCHECK(GetLayoutObject()->IsText()); |
| UpdateCachedAttributeValuesIfNeeded(); |
| return cached_local_bounding_box_rect_for_accessibility_; |
| } |
| |
| LayoutRect AXObject::GetBoundsInFrameCoordinates() const { |
| AXObject* container = nullptr; |
| FloatRect bounds; |
| SkMatrix44 transform; |
| GetRelativeBounds(&container, bounds, transform); |
| FloatRect computed_bounds(0, 0, bounds.Width(), bounds.Height()); |
| while (container && container != this) { |
| computed_bounds.Move(bounds.X(), bounds.Y()); |
| if (!container->IsWebArea()) { |
| computed_bounds.Move(-container->GetScrollOffset().X(), |
| -container->GetScrollOffset().Y()); |
| } |
| if (!transform.isIdentity()) { |
| TransformationMatrix transformation_matrix(transform); |
| transformation_matrix.MapRect(computed_bounds); |
| } |
| container->GetRelativeBounds(&container, bounds, transform); |
| } |
| return LayoutRect(computed_bounds); |
| } |
| |
| // |
| // Modify or take an action on an object. |
| // |
| |
| bool AXObject::RequestDecrementAction() { |
| Event* event = |
| Event::CreateCancelable(event_type_names::kAccessibledecrement); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeDecrementAction(); |
| } |
| |
| bool AXObject::RequestClickAction() { |
| Event* event = Event::CreateCancelable(event_type_names::kAccessibleclick); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeClickAction(); |
| } |
| |
| bool AXObject::OnNativeClickAction() { |
| Document* document = GetDocument(); |
| if (!document) |
| return false; |
| |
| std::unique_ptr<UserGestureIndicator> gesture_indicator = |
| LocalFrame::NotifyUserActivation(document->GetFrame(), |
| UserGestureToken::kNewGesture); |
| |
| Element* element = GetElement(); |
| if (!element && GetNode()) |
| element = GetNode()->parentElement(); |
| |
| if (element) { |
| element->AccessKeyAction(true); |
| return true; |
| } |
| |
| if (CanSetFocusAttribute()) |
| return OnNativeFocusAction(); |
| |
| return false; |
| } |
| |
| bool AXObject::RequestFocusAction() { |
| Event* event = Event::CreateCancelable(event_type_names::kAccessiblefocus); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeFocusAction(); |
| } |
| |
| bool AXObject::RequestIncrementAction() { |
| Event* event = |
| Event::CreateCancelable(event_type_names::kAccessibleincrement); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeIncrementAction(); |
| } |
| |
| bool AXObject::RequestScrollToGlobalPointAction(const IntPoint& point) { |
| return OnNativeScrollToGlobalPointAction(point); |
| } |
| |
| bool AXObject::RequestScrollToMakeVisibleAction() { |
| Event* event = |
| Event::CreateCancelable(event_type_names::kAccessiblescrollintoview); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeScrollToMakeVisibleAction(); |
| } |
| |
| bool AXObject::RequestScrollToMakeVisibleWithSubFocusAction( |
| const IntRect& subfocus) { |
| return OnNativeScrollToMakeVisibleWithSubFocusAction(subfocus); |
| } |
| |
| bool AXObject::RequestSetSelectedAction(bool selected) { |
| return OnNativeSetSelectedAction(selected); |
| } |
| |
| bool AXObject::RequestSetSelectionAction(const AXSelection& selection) { |
| return OnNativeSetSelectionAction(selection); |
| } |
| |
| bool AXObject::RequestSetSequentialFocusNavigationStartingPointAction() { |
| return OnNativeSetSequentialFocusNavigationStartingPointAction(); |
| } |
| |
| bool AXObject::RequestSetValueAction(const String& value) { |
| return OnNativeSetValueAction(value); |
| } |
| |
| bool AXObject::RequestShowContextMenuAction() { |
| Event* event = |
| Event::CreateCancelable(event_type_names::kAccessiblecontextmenu); |
| if (DispatchEventToAOMEventListeners(*event)) |
| return true; |
| |
| return OnNativeShowContextMenuAction(); |
| } |
| |
| bool AXObject::InternalClearAccessibilityFocusAction() { |
| return false; |
| } |
| |
| bool AXObject::InternalSetAccessibilityFocusAction() { |
| return false; |
| } |
| |
| bool AXObject::OnNativeScrollToMakeVisibleAction() const { |
| Node* node = GetNode(); |
| LayoutObject* layout_object = node ? node->GetLayoutObject() : nullptr; |
| if (!layout_object || !node->isConnected()) |
| return false; |
| LayoutRect target_rect(layout_object->AbsoluteBoundingBoxRect()); |
| layout_object->ScrollRectToVisible( |
| target_rect, |
| WebScrollIntoViewParams(ScrollAlignment::kAlignCenterIfNeeded, |
| ScrollAlignment::kAlignCenterIfNeeded, |
| kProgrammaticScroll, false, kScrollBehaviorAuto)); |
| AXObjectCache().PostNotification( |
| AXObjectCache().GetOrCreate(GetDocument()->GetLayoutView()), |
| ax::mojom::Event::kLocationChanged); |
| return true; |
| } |
| |
| bool AXObject::OnNativeScrollToMakeVisibleWithSubFocusAction( |
| const IntRect& rect) const { |
| Node* node = GetNode(); |
| LayoutObject* layout_object = node ? node->GetLayoutObject() : nullptr; |
| if (!layout_object || !node->isConnected()) |
| return false; |
| LayoutRect target_rect( |
| layout_object->LocalToAbsoluteQuad(FloatQuad(FloatRect(rect))) |
| .BoundingBox()); |
| // TODO(szager): This scroll alignment is intended to preserve existing |
| // behavior to the extent possible, but it's not clear that this behavior is |
| // well-spec'ed or optimal. In particular, it favors centering things in |
| // the visible viewport rather than snapping them to the closest edge, which |
| // is the default behavior of element.scrollIntoView. |
| ScrollAlignment scroll_alignment = { |
| kScrollAlignmentNoScroll, kScrollAlignmentCenter, kScrollAlignmentCenter}; |
| layout_object->ScrollRectToVisible( |
| target_rect, |
| WebScrollIntoViewParams(scroll_alignment, scroll_alignment, |
| kProgrammaticScroll, false, kScrollBehaviorAuto)); |
| AXObjectCache().PostNotification( |
| AXObjectCache().GetOrCreate(GetDocument()->GetLayoutView()), |
| ax::mojom::Event::kLocationChanged); |
| return true; |
| } |
| |
| bool AXObject::OnNativeScrollToGlobalPointAction( |
| const IntPoint& global_point) const { |
| Node* node = GetNode(); |
| LayoutObject* layout_object = node ? node->GetLayoutObject() : nullptr; |
| if (!layout_object || !node->isConnected()) |
| return false; |
| LayoutRect target_rect(layout_object->AbsoluteBoundingBoxRect()); |
| target_rect.MoveBy(-global_point); |
| layout_object->ScrollRectToVisible( |
| target_rect, |
| WebScrollIntoViewParams(ScrollAlignment::kAlignLeftAlways, |
| ScrollAlignment::kAlignTopAlways, |
| kProgrammaticScroll, false, kScrollBehaviorAuto)); |
| AXObjectCache().PostNotification( |
| AXObjectCache().GetOrCreate(GetDocument()->GetLayoutView()), |
| ax::mojom::Event::kLocationChanged); |
| return true; |
| } |
| |
| bool AXObject::OnNativeSetSequentialFocusNavigationStartingPointAction() { |
| // Call it on the nearest ancestor that overrides this with a specific |
| // implementation. |
| if (ParentObject()) { |
| return ParentObject() |
| ->OnNativeSetSequentialFocusNavigationStartingPointAction(); |
| } |
| return false; |
| } |
| |
| bool AXObject::OnNativeDecrementAction() { |
| return false; |
| } |
| |
| bool AXObject::OnNativeFocusAction() { |
| return false; |
| } |
| |
| bool AXObject::OnNativeIncrementAction() { |
| return false; |
| } |
| |
| bool AXObject::OnNativeSetValueAction(const String&) { |
| return false; |
| } |
| |
| bool AXObject::OnNativeSetSelectedAction(bool) { |
| return false; |
| } |
| |
| bool AXObject::OnNativeSetSelectionAction(const AXSelection& selection) { |
| return false; |
| } |
| |
| bool AXObject::OnNativeShowContextMenuAction() { |
| Element* element = GetElement(); |
| if (!element) |
| element = ParentObject() ? ParentObject()->GetElement() : nullptr; |
| if (!element) |
| return false; |
| |
| Document* document = GetDocument(); |
| if (!document || !document->GetFrame()) |
| return false; |
| |
| ContextMenuAllowedScope scope; |
| document->GetFrame()->GetEventHandler().ShowNonLocatedContextMenu(element); |
| return true; |
| } |
| |
| void AXObject::SelectionChanged() { |
| if (AXObject* parent = ParentObjectIfExists()) |
| parent->SelectionChanged(); |
| } |
| |
| int AXObject::LineForPosition(const VisiblePosition& position) const { |
| if (position.IsNull() || !GetNode()) |
| return -1; |
| |
| // If the position is not in the same editable region as this AX object, |
| // return -1. |
| Node* container_node = position.DeepEquivalent().ComputeContainerNode(); |
| if (!container_node->IsShadowIncludingInclusiveAncestorOf(GetNode()) && |
| !GetNode()->IsShadowIncludingInclusiveAncestorOf(container_node)) |
| return -1; |
| |
| int line_count = -1; |
| VisiblePosition current_position = position; |
| VisiblePosition previous_position; |
| |
| // Move up until we get to the top. |
| // FIXME: This only takes us to the top of the rootEditableElement, not the |
| // top of the top document. |
| do { |
| previous_position = current_position; |
| current_position = PreviousLinePosition(current_position, LayoutUnit(), |
| kHasEditableAXRole); |
| ++line_count; |
| } while (current_position.IsNotNull() && |
| !InSameLine(current_position, previous_position)); |
| |
| return line_count; |
| } |
| |
| // static |
| bool AXObject::IsARIAControl(ax::mojom::Role aria_role) { |
| return IsARIAInput(aria_role) || aria_role == ax::mojom::Role::kButton || |
| aria_role == ax::mojom::Role::kComboBoxMenuButton || |
| aria_role == ax::mojom::Role::kSlider; |
| } |
| |
| // static |
| bool AXObject::IsARIAInput(ax::mojom::Role aria_role) { |
| return aria_role == ax::mojom::Role::kRadioButton || |
| aria_role == ax::mojom::Role::kCheckBox || |
| aria_role == ax::mojom::Role::kTextField || |
| aria_role == ax::mojom::Role::kSwitch || |
| aria_role == ax::mojom::Role::kSearchBox || |
| aria_role == ax::mojom::Role::kTextFieldWithComboBox; |
| } |
| |
| ax::mojom::Role AXObject::AriaRoleToWebCoreRole(const String& value) { |
| DCHECK(!value.IsEmpty()); |
| |
| static const ARIARoleMap* role_map = CreateARIARoleMap(); |
| |
| Vector<String> role_vector; |
| value.Split(' ', role_vector); |
| ax::mojom::Role role = ax::mojom::Role::kUnknown; |
| for (const auto& child : role_vector) { |
| role = role_map->at(child); |
| if (role != ax::mojom::Role::kUnknown) |
| return role; |
| } |
| |
| return role; |
| } |
| |
| bool AXObject::NameFromSelectedOption(bool recursive) const { |
| switch (RoleValue()) { |
| // Step 2E from: http://www.w3.org/TR/accname-aam-1.1 |
| case ax::mojom::Role::kComboBoxGrouping: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kPopUpButton: |
| return recursive; |
| default: |
| return false; |
| } |
| } |
| |
| bool AXObject::NameFromContents(bool recursive) const { |
| // ARIA 1.1, section 5.2.7.5. |
| bool result = false; |
| |
| switch (RoleValue()) { |
| // ----- NameFrom: contents ------------------------- |
| // Get their own name from contents, or contribute to ancestors |
| case ax::mojom::Role::kAnchor: |
| case ax::mojom::Role::kButton: |
| case ax::mojom::Role::kCell: |
| case ax::mojom::Role::kCheckBox: |
| case ax::mojom::Role::kColumnHeader: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kDocBackLink: |
| case ax::mojom::Role::kDocBiblioRef: |
| case ax::mojom::Role::kDocNoteRef: |
| case ax::mojom::Role::kDocGlossRef: |
| case ax::mojom::Role::kDisclosureTriangle: |
| case ax::mojom::Role::kHeading: |
| case ax::mojom::Role::kLayoutTableCell: |
| case ax::mojom::Role::kLineBreak: |
| case ax::mojom::Role::kLink: |
| case ax::mojom::Role::kListBoxOption: |
| case ax::mojom::Role::kMenuButton: |
| case ax::mojom::Role::kMenuItem: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kMenuListOption: |
| case ax::mojom::Role::kPopUpButton: |
| case ax::mojom::Role::kRadioButton: |
| case ax::mojom::Role::kRowHeader: |
| case ax::mojom::Role::kStaticText: |
| case ax::mojom::Role::kSwitch: |
| case ax::mojom::Role::kTab: |
| case ax::mojom::Role::kToggleButton: |
| case ax::mojom::Role::kTreeItem: |
| case ax::mojom::Role::kTooltip: |
| result = true; |
| break; |
| |
| // ----- No name from contents ------------------------- |
| // These never have or contribute a name from contents, as they are |
| // containers for many subobjects. Superset of nameFrom:author ARIA roles. |
| case ax::mojom::Role::kAlert: |
| case ax::mojom::Role::kAlertDialog: |
| case ax::mojom::Role::kApplication: |
| case ax::mojom::Role::kAudio: |
| case ax::mojom::Role::kArticle: |
| case ax::mojom::Role::kBanner: |
| case ax::mojom::Role::kBlockquote: |
| case ax::mojom::Role::kCaret: |
| case ax::mojom::Role::kClient: |
| case ax::mojom::Role::kColorWell: |
| case ax::mojom::Role::kColumn: |
| case ax::mojom::Role::kComboBoxGrouping: |
| case ax::mojom::Role::kComplementary: |
| case ax::mojom::Role::kContentInfo: |
| case ax::mojom::Role::kDate: |
| case ax::mojom::Role::kDateTime: |
| case ax::mojom::Role::kDefinition: |
| case ax::mojom::Role::kDesktop: |
| case ax::mojom::Role::kDialog: |
| case ax::mojom::Role::kDirectory: |
| case ax::mojom::Role::kDocCover: |
| case ax::mojom::Role::kDocBiblioEntry: |
| case ax::mojom::Role::kDocEndnote: |
| case ax::mojom::Role::kDocFootnote: |
| case ax::mojom::Role::kDocPageBreak: |
| case ax::mojom::Role::kDocAbstract: |
| case ax::mojom::Role::kDocAcknowledgments: |
| case ax::mojom::Role::kDocAfterword: |
| case ax::mojom::Role::kDocAppendix: |
| case ax::mojom::Role::kDocBibliography: |
| case ax::mojom::Role::kDocChapter: |
| case ax::mojom::Role::kDocColophon: |
| case ax::mojom::Role::kDocConclusion: |
| case ax::mojom::Role::kDocCredit: |
| case ax::mojom::Role::kDocCredits: |
| case ax::mojom::Role::kDocDedication: |
| case ax::mojom::Role::kDocEndnotes: |
| case ax::mojom::Role::kDocEpigraph: |
| case ax::mojom::Role::kDocEpilogue: |
| case ax::mojom::Role::kDocErrata: |
| case ax::mojom::Role::kDocExample: |
| case ax::mojom::Role::kDocForeword: |
| case ax::mojom::Role::kDocGlossary: |
| case ax::mojom::Role::kDocIndex: |
| case ax::mojom::Role::kDocIntroduction: |
| case ax::mojom::Role::kDocNotice: |
| case ax::mojom::Role::kDocPageList: |
| case ax::mojom::Role::kDocPart: |
| case ax::mojom::Role::kDocPreface: |
| case ax::mojom::Role::kDocPrologue: |
| case ax::mojom::Role::kDocPullquote: |
| case ax::mojom::Role::kDocQna: |
| case ax::mojom::Role::kDocSubtitle: |
| case ax::mojom::Role::kDocTip: |
| case ax::mojom::Role::kDocToc: |
| case ax::mojom::Role::kDocument: |
| case ax::mojom::Role::kEmbeddedObject: |
| case ax::mojom::Role::kFeed: |
| case ax::mojom::Role::kFigure: |
| case ax::mojom::Role::kForm: |
| case ax::mojom::Role::kGraphicsDocument: |
| case ax::mojom::Role::kGraphicsObject: |
| case ax::mojom::Role::kGraphicsSymbol: |
| case ax::mojom::Role::kGrid: |
| case ax::mojom::Role::kGroup: |
| case ax::mojom::Role::kIframePresentational: |
| case ax::mojom::Role::kIframe: |
| case ax::mojom::Role::kImage: |
| case ax::mojom::Role::kInputTime: |
| case ax::mojom::Role::kKeyboard: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kLog: |
| case ax::mojom::Role::kMain: |
| case ax::mojom::Role::kMarquee: |
| case ax::mojom::Role::kMath: |
| case ax::mojom::Role::kMenuListPopup: |
| case ax::mojom::Role::kMenu: |
| case ax::mojom::Role::kMenuBar: |
| case ax::mojom::Role::kMeter: |
| case ax::mojom::Role::kNavigation: |
| case ax::mojom::Role::kNote: |
| case ax::mojom::Role::kPane: |
| case ax::mojom::Role::kProgressIndicator: |
| case ax::mojom::Role::kRadioGroup: |
| case ax::mojom::Role::kRootWebArea: |
| case ax::mojom::Role::kScrollBar: |
| case ax::mojom::Role::kScrollView: |
| case ax::mojom::Role::kSearch: |
| case ax::mojom::Role::kSearchBox: |
| case ax::mojom::Role::kSplitter: |
| case ax::mojom::Role::kSlider: |
| case ax::mojom::Role::kSpinButton: |
| case ax::mojom::Role::kStatus: |
| case ax::mojom::Role::kSliderThumb: |
| case ax::mojom::Role::kSvgRoot: |
| case ax::mojom::Role::kTable: |
| case ax::mojom::Role::kTableHeaderContainer: |
| case ax::mojom::Role::kTabList: |
| case ax::mojom::Role::kTabPanel: |
| case ax::mojom::Role::kTerm: |
| case ax::mojom::Role::kTextField: |
| case ax::mojom::Role::kTextFieldWithComboBox: |
| case ax::mojom::Role::kTitleBar: |
| case ax::mojom::Role::kTime: |
| case ax::mojom::Role::kTimer: |
| case ax::mojom::Role::kToolbar: |
| case ax::mojom::Role::kTree: |
| case ax::mojom::Role::kTreeGrid: |
| case ax::mojom::Role::kVideo: |
| case ax::mojom::Role::kWebArea: |
| case ax::mojom::Role::kWebView: |
| result = false; |
| break; |
| |
| // ----- Conditional: contribute to ancestor only, unless focusable ------- |
| // Some objects can contribute their contents to ancestor names, but |
| // only have their own name if they are focusable |
| case ax::mojom::Role::kAbbr: |
| case ax::mojom::Role::kAnnotation: |
| case ax::mojom::Role::kCanvas: |
| case ax::mojom::Role::kCaption: |
| case ax::mojom::Role::kContentDeletion: |
| case ax::mojom::Role::kContentInsertion: |
| case ax::mojom::Role::kDescriptionListDetail: |
| case ax::mojom::Role::kDescriptionList: |
| case ax::mojom::Role::kDescriptionListTerm: |
| case ax::mojom::Role::kDetails: |
| case ax::mojom::Role::kFigcaption: |
| case ax::mojom::Role::kFooter: |
| case ax::mojom::Role::kGenericContainer: |
| case ax::mojom::Role::kIgnored: |
| case ax::mojom::Role::kImageMap: |
| case ax::mojom::Role::kInlineTextBox: |
| case ax::mojom::Role::kLabelText: |
| case ax::mojom::Role::kLayoutTable: |
| case ax::mojom::Role::kLayoutTableColumn: |
| case ax::mojom::Role::kLayoutTableRow: |
| case ax::mojom::Role::kLegend: |
| case ax::mojom::Role::kList: |
| case ax::mojom::Role::kListItem: |
| case ax::mojom::Role::kListMarker: |
| case ax::mojom::Role::kMark: |
| case ax::mojom::Role::kNone: |
| case ax::mojom::Role::kParagraph: |
| case ax::mojom::Role::kPre: |
| case ax::mojom::Role::kPresentational: |
| case ax::mojom::Role::kRegion: |
| // Spec says we should always expose the name on rows, |
| // but for performance reasons we only do it |
| // if the row might receive focus |
| case ax::mojom::Role::kRow: |
| case ax::mojom::Role::kRuby: |
| result = recursive || (CanReceiveAccessibilityFocus() && !IsEditable()); |
| break; |
| |
| case ax::mojom::Role::kUnknown: |
| case ax::mojom::Role::kMaxValue: |
| LOG(ERROR) << "ax::mojom::Role::kUnknown for " << GetNode(); |
| NOTREACHED(); |
| break; |
| } |
| |
| return result; |
| } |
| |
| bool AXObject::SupportsARIAReadOnly() const { |
| switch (RoleValue()) { |
| case ax::mojom::Role::kCell: |
| case ax::mojom::Role::kCheckBox: |
| case ax::mojom::Role::kColorWell: |
| case ax::mojom::Role::kColumnHeader: |
| case ax::mojom::Role::kComboBoxGrouping: |
| case ax::mojom::Role::kComboBoxMenuButton: |
| case ax::mojom::Role::kDate: |
| case ax::mojom::Role::kDateTime: |
| case ax::mojom::Role::kGrid: |
| case ax::mojom::Role::kInputTime: |
| case ax::mojom::Role::kListBox: |
| case ax::mojom::Role::kMenuButton: |
| case ax::mojom::Role::kMenuItemCheckBox: |
| case ax::mojom::Role::kMenuItemRadio: |
| case ax::mojom::Role::kPopUpButton: |
| case ax::mojom::Role::kRadioGroup: |
| case ax::mojom::Role::kRowHeader: |
| case ax::mojom::Role::kSearchBox: |
| case ax::mojom::Role::kSlider: |
| case ax::mojom::Role::kSpinButton: |
| case ax::mojom::Role::kSwitch: |
| case ax::mojom::Role::kTextField: |
| case ax::mojom::Role::kTextFieldWithComboBox: |
| case ax::mojom::Role::kToggleButton: |
| case ax::mojom::Role::kTreeGrid: |
| return true; |
| default: |
| break; |
| } |
| return false; |
| } |
| |
| ax::mojom::Role AXObject::ButtonRoleType() const { |
| // If aria-pressed is present, then it should be exposed as a toggle button. |
| // http://www.w3.org/TR/wai-aria/states_and_properties#aria-pressed |
| if (AriaPressedIsPresent()) |
| return ax::mojom::Role::kToggleButton; |
| if (HasPopup() != ax::mojom::HasPopup::kFalse) |
| return ax::mojom::Role::kPopUpButton; |
| // We don't contemplate RadioButtonRole, as it depends on the input |
| // type. |
| |
| return ax::mojom::Role::kButton; |
| } |
| |
| // static |
| const AtomicString& AXObject::RoleName(ax::mojom::Role role) { |
| static const Vector<AtomicString>* role_name_vector = CreateRoleNameVector(); |
| |
| return role_name_vector->at(static_cast<size_t>(role)); |
| } |
| |
| // static |
| const AtomicString& AXObject::InternalRoleName(ax::mojom::Role role) { |
| static const Vector<AtomicString>* internal_role_name_vector = |
| CreateInternalRoleNameVector(); |
| |
| return internal_role_name_vector->at(static_cast<size_t>(role)); |
| } |
| |
| // static |
| const AXObject* AXObject::LowestCommonAncestor(const AXObject& first, |
| const AXObject& second, |
| int* index_in_ancestor1, |
| int* index_in_ancestor2) { |
| *index_in_ancestor1 = -1; |
| *index_in_ancestor2 = -1; |
| |
| if (first.IsDetached() || second.IsDetached()) |
| return nullptr; |
| |
| if (first == second) |
| return &first; |
| |
| HeapVector<Member<const AXObject>> ancestors1; |
| ancestors1.push_back(&first); |
| while (ancestors1.back()) |
| ancestors1.push_back(ancestors1.back()->ParentObjectUnignored()); |
| |
| HeapVector<Member<const AXObject>> ancestors2; |
| ancestors2.push_back(&second); |
| while (ancestors2.back()) |
| ancestors2.push_back(ancestors2.back()->ParentObjectUnignored()); |
| |
| const AXObject* common_ancestor = nullptr; |
| while (!ancestors1.IsEmpty() && !ancestors2.IsEmpty() && |
| ancestors1.back() == ancestors2.back()) { |
| common_ancestor = ancestors1.back(); |
| ancestors1.pop_back(); |
| ancestors2.pop_back(); |
| } |
| |
| if (common_ancestor) { |
| if (!ancestors1.IsEmpty()) |
| *index_in_ancestor1 = ancestors1.back()->IndexInParent(); |
| if (!ancestors2.IsEmpty()) |
| *index_in_ancestor2 = ancestors2.back()->IndexInParent(); |
| } |
| |
| return common_ancestor; |
| } |
| |
| String AXObject::ToString() const { |
| return AXObject::InternalRoleName(RoleValue()) |
| .GetString() |
| .EncodeForDebugging() + |
| ": " + ComputedName().EncodeForDebugging(); |
| } |
| |
| VisiblePosition AXObject::VisiblePositionForIndex(int) const { |
| return VisiblePosition(); |
| } |
| |
| bool operator==(const AXObject& first, const AXObject& second) { |
| if (first.IsDetached() || second.IsDetached()) |
| return false; |
| if (&first == &second) { |
| DCHECK_EQ(first.AXObjectID(), second.AXObjectID()); |
| return true; |
| } |
| return false; |
| } |
| |
| bool operator!=(const AXObject& first, const AXObject& second) { |
| return !(first == second); |
| } |
| |
| bool operator<(const AXObject& first, const AXObject& second) { |
| if (first.IsDetached() || second.IsDetached()) |
| return false; |
| |
| int index_in_ancestor1, index_in_ancestor2; |
| const AXObject* ancestor = AXObject::LowestCommonAncestor( |
| first, second, &index_in_ancestor1, &index_in_ancestor2); |
| DCHECK_GE(index_in_ancestor1, -1); |
| DCHECK_GE(index_in_ancestor2, -1); |
| if (!ancestor) |
| return false; |
| return index_in_ancestor1 < index_in_ancestor2; |
| } |
| |
| bool operator<=(const AXObject& first, const AXObject& second) { |
| return first == second || first < second; |
| } |
| |
| bool operator>(const AXObject& first, const AXObject& second) { |
| if (first.IsDetached() || second.IsDetached()) |
| return false; |
| |
| int index_in_ancestor1, index_in_ancestor2; |
| const AXObject* ancestor = AXObject::LowestCommonAncestor( |
| first, second, &index_in_ancestor1, &index_in_ancestor2); |
| DCHECK_GE(index_in_ancestor1, -1); |
| DCHECK_GE(index_in_ancestor2, -1); |
| if (!ancestor) |
| return false; |
| return index_in_ancestor1 > index_in_ancestor2; |
| } |
| |
| bool operator>=(const AXObject& first, const AXObject& second) { |
| return first == second || first > second; |
| } |
| |
| std::ostream& operator<<(std::ostream& stream, const AXObject& obj) { |
| return stream << obj.ToString().Utf8().data(); |
| } |
| |
| void AXObject::Trace(blink::Visitor* visitor) { |
| visitor->Trace(children_); |
| visitor->Trace(parent_); |
| visitor->Trace(cached_live_region_root_); |
| visitor->Trace(ax_object_cache_); |
| } |
| |
| } // namespace blink |