/* * Copyright (C) 2012, Google 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_node_object.h" #include #include "third_party/blink/renderer/core/aom/accessible_node.h" #include "third_party/blink/renderer/core/dom/element.h" #include "third_party/blink/renderer/core/dom/flat_tree_traversal.h" #include "third_party/blink/renderer/core/dom/node_traversal.h" #include "third_party/blink/renderer/core/dom/qualified_name.h" #include "third_party/blink/renderer/core/dom/shadow_root.h" #include "third_party/blink/renderer/core/dom/text.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/markers/document_marker_controller.h" #include "third_party/blink/renderer/core/editing/position.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/html/canvas/html_canvas_element.h" #include "third_party/blink/renderer/core/html/forms/html_field_set_element.h" #include "third_party/blink/renderer/core/html/forms/html_input_element.h" #include "third_party/blink/renderer/core/html/forms/html_label_element.h" #include "third_party/blink/renderer/core/html/forms/html_legend_element.h" #include "third_party/blink/renderer/core/html/forms/html_select_element.h" #include "third_party/blink/renderer/core/html/forms/html_text_area_element.h" #include "third_party/blink/renderer/core/html/forms/labels_node_list.h" #include "third_party/blink/renderer/core/html/forms/radio_input_type.h" #include "third_party/blink/renderer/core/html/forms/text_control_element.h" #include "third_party/blink/renderer/core/html/html_anchor_element.h" #include "third_party/blink/renderer/core/html/html_div_element.h" #include "third_party/blink/renderer/core/html/html_dlist_element.h" #include "third_party/blink/renderer/core/html/html_frame_element_base.h" #include "third_party/blink/renderer/core/html/html_image_element.h" #include "third_party/blink/renderer/core/html/html_meter_element.h" #include "third_party/blink/renderer/core/html/html_plugin_element.h" #include "third_party/blink/renderer/core/html/html_table_caption_element.h" #include "third_party/blink/renderer/core/html/html_table_cell_element.h" #include "third_party/blink/renderer/core/html/html_table_element.h" #include "third_party/blink/renderer/core/html/html_table_row_element.h" #include "third_party/blink/renderer/core/html/html_table_section_element.h" #include "third_party/blink/renderer/core/html/media/html_media_element.h" #include "third_party/blink/renderer/core/html/parser/html_parser_idioms.h" #include "third_party/blink/renderer/core/input_type_names.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/svg/svg_element.h" #include "third_party/blink/renderer/modules/accessibility/ax_object_cache_impl.h" #include "third_party/blink/renderer/modules/accessibility/ax_position.h" #include "third_party/blink/renderer/modules/accessibility/ax_range.h" #include "third_party/blink/renderer/modules/media_controls/elements/media_control_elements_helper.h" #include "third_party/blink/renderer/platform/text/platform_locale.h" #include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" namespace blink { using namespace html_names; // In ARIA 1.1, default value of aria-level was changed to 2. const int kDefaultHeadingLevel = 2; AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache) : AXObject(ax_object_cache), children_dirty_(false), native_role_(ax::mojom::Role::kUnknown), node_(node) {} AXNodeObject* AXNodeObject::Create(Node* node, AXObjectCacheImpl& ax_object_cache) { return MakeGarbageCollected(node, ax_object_cache); } AXNodeObject::~AXNodeObject() { DCHECK(!node_); } void AXNodeObject::AlterSliderOrSpinButtonValue(bool increase) { if (!IsSlider() && !IsSpinButton()) return; float value; if (!ValueForRange(&value)) return; float step; StepValueForRange(&step); value += increase ? step : -step; OnNativeSetValueAction(String::Number(value)); AXObjectCache().PostNotification(GetNode(), ax::mojom::Event::kValueChanged); } AXObject* AXNodeObject::ActiveDescendant() { Element* element = GetElement(); if (!element) return nullptr; Element* descendant = GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant); if (!descendant) return nullptr; AXObject* ax_descendant = AXObjectCache().GetOrCreate(descendant); return ax_descendant; } bool AXNodeObject::ComputeAccessibilityIsIgnored( IgnoredReasons* ignored_reasons) const { #if DCHECK_IS_ON() // Double-check that an AXObject is never accessed before // it's been initialized. DCHECK(initialized_); #endif // If this element is within a parent that cannot have children, it should not // be exposed. if (IsDescendantOfLeafNode()) { if (ignored_reasons) ignored_reasons->push_back( IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor())); return true; } // Ignore labels that are already referenced by a control. AXObject* control_object = CorrespondingControlForLabelElement(); HTMLLabelElement* label = LabelElementContainer(); if (control_object && control_object->IsCheckboxOrRadio() && control_object->NameFromLabelElement() && AccessibleNode::GetPropertyOrARIAAttribute( label, AOMStringProperty::kRole) == g_null_atom) { if (ignored_reasons) { if (label && label != GetNode()) { AXObject* label_ax_object = AXObjectCache().GetOrCreate(label); ignored_reasons->push_back( IgnoredReason(kAXLabelContainer, label_ax_object)); } ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_object)); } return true; } Element* element = GetNode()->IsElementNode() ? ToElement(GetNode()) : GetNode()->parentElement(); if (!GetLayoutObject() && (!element || !element->IsInCanvasSubtree()) && !AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden)) { if (ignored_reasons) ignored_reasons->push_back(IgnoredReason(kAXNotRendered)); return true; } if (role_ == ax::mojom::Role::kUnknown) { if (ignored_reasons) ignored_reasons->push_back(IgnoredReason(kAXUninteresting)); return true; } return false; } static bool IsListElement(Node* node) { return IsHTMLUListElement(*node) || IsHTMLOListElement(*node) || IsHTMLDListElement(*node); } static bool IsRequiredOwnedElement(AXObject* parent, ax::mojom::Role current_role, HTMLElement* current_element) { Node* parent_node = parent->GetNode(); if (!parent_node || !parent_node->IsHTMLElement()) return false; if (current_role == ax::mojom::Role::kListItem) return IsListElement(parent_node); if (current_role == ax::mojom::Role::kListMarker) return IsHTMLLIElement(*parent_node); if (current_role == ax::mojom::Role::kMenuItemCheckBox || current_role == ax::mojom::Role::kMenuItem || current_role == ax::mojom::Role::kMenuItemRadio) return IsHTMLMenuElement(*parent_node); if (!current_element) return false; if (IsHTMLTableCellElement(*current_element)) return IsHTMLTableRowElement(*parent_node); if (IsHTMLTableRowElement(*current_element)) return IsHTMLTableSectionElement(ToHTMLElement(*parent_node)); // In case of ListboxRole and its child, ListBoxOptionRole, inheritance of // presentation role is handled in AXListBoxOption because ListBoxOption Role // doesn't have any child. // If it's just ignored because of presentation, we can't see any AX tree // related to ListBoxOption. return false; } const AXObject* AXNodeObject::InheritsPresentationalRoleFrom() const { // ARIA states if an item can get focus, it should not be presentational. if (CanSetFocusAttribute()) return nullptr; if (IsPresentational()) return this; // http://www.w3.org/TR/wai-aria/complete#presentation // ARIA spec says that the user agent MUST apply an inherited role of // presentation // to any owned elements that do not have an explicit role defined. if (AriaRoleAttribute() != ax::mojom::Role::kUnknown) return nullptr; AXObject* parent = ParentObject(); if (!parent) return nullptr; HTMLElement* element = nullptr; if (GetNode() && GetNode()->IsHTMLElement()) element = ToHTMLElement(GetNode()); if (!parent->HasInheritedPresentationalRole()) return nullptr; // ARIA spec says that when a parent object is presentational and this object // is a required owned element of that parent, then this object is also // presentational. if (IsRequiredOwnedElement(parent, RoleValue(), element)) return parent; return nullptr; } // There should only be one banner/contentInfo per page. If header/footer are // being used within an article, aside, nave, section, blockquote, details, // fieldset, figure, td, or main, then it should not be exposed as whole // page's banner/contentInfo. static HashSet& GetLandmarkRolesNotAllowed() { DEFINE_STATIC_LOCAL(HashSet, landmark_roles_not_allowed, ()); if (landmark_roles_not_allowed.IsEmpty()) { landmark_roles_not_allowed.insert(kArticleTag); landmark_roles_not_allowed.insert(kAsideTag); landmark_roles_not_allowed.insert(kNavTag); landmark_roles_not_allowed.insert(kSectionTag); landmark_roles_not_allowed.insert(kBlockquoteTag); landmark_roles_not_allowed.insert(kDetailsTag); landmark_roles_not_allowed.insert(kFieldsetTag); landmark_roles_not_allowed.insert(kFigureTag); landmark_roles_not_allowed.insert(kTdTag); landmark_roles_not_allowed.insert(kMainTag); } return landmark_roles_not_allowed; } bool AXNodeObject::IsDescendantOfElementType( HashSet& tag_names) const { if (!GetNode()) return false; for (Element* parent = GetNode()->parentElement(); parent; parent = parent->parentElement()) { if (tag_names.Contains(parent->TagQName())) return true; } return false; } // TODO(accessibility) Needs a new name as it does check ARIA, including // checking the @role for an iframe, and @aria-haspopup/aria-pressed via // ButtonType(). // TODO(accessibility) This value is cached in native_role_ so it needs to // be recached if anything it depends on change, such as IsClickable(), // DataList(), aria-pressed, the parent's tag, role on an iframe, etc. ax::mojom::Role AXNodeObject::NativeRoleIgnoringAria() const { if (!GetNode()) return ax::mojom::Role::kUnknown; // |HTMLAnchorElement| sets isLink only when it has kHrefAttr. if (GetNode()->IsLink()) return ax::mojom::Role::kLink; if (IsHTMLAnchorElement(*GetNode())) { // We assume that an anchor element is LinkRole if it has event listners // even though it doesn't have kHrefAttr. if (IsClickable()) return ax::mojom::Role::kLink; return ax::mojom::Role::kAnchor; } if (IsHTMLButtonElement(*GetNode())) return ButtonRoleType(); if (IsHTMLDetailsElement(*GetNode())) return ax::mojom::Role::kDetails; if (IsHTMLSummaryElement(*GetNode())) { ContainerNode* parent = FlatTreeTraversal::Parent(*GetNode()); if (parent && IsHTMLSlotElement(parent)) parent = FlatTreeTraversal::Parent(*parent); if (parent && IsHTMLDetailsElement(parent)) return ax::mojom::Role::kDisclosureTriangle; return ax::mojom::Role::kUnknown; } if (const auto* input = ToHTMLInputElementOrNull(*GetNode())) { const AtomicString& type = input->type(); if (input->DataList()) return ax::mojom::Role::kTextFieldWithComboBox; if (type == input_type_names::kButton) { if ((GetNode()->parentNode() && IsHTMLMenuElement(GetNode()->parentNode())) || (ParentObject() && ParentObject()->RoleValue() == ax::mojom::Role::kMenu)) return ax::mojom::Role::kMenuItem; return ButtonRoleType(); } if (type == input_type_names::kCheckbox) { if ((GetNode()->parentNode() && IsHTMLMenuElement(GetNode()->parentNode())) || (ParentObject() && ParentObject()->RoleValue() == ax::mojom::Role::kMenu)) return ax::mojom::Role::kMenuItemCheckBox; return ax::mojom::Role::kCheckBox; } if (type == input_type_names::kDate) return ax::mojom::Role::kDate; if (type == input_type_names::kDatetime || type == input_type_names::kDatetimeLocal || type == input_type_names::kMonth || type == input_type_names::kWeek) return ax::mojom::Role::kDateTime; if (type == input_type_names::kFile) return ax::mojom::Role::kButton; if (type == input_type_names::kRadio) { if ((GetNode()->parentNode() && IsHTMLMenuElement(GetNode()->parentNode())) || (ParentObject() && ParentObject()->RoleValue() == ax::mojom::Role::kMenu)) return ax::mojom::Role::kMenuItemRadio; return ax::mojom::Role::kRadioButton; } if (type == input_type_names::kNumber) return ax::mojom::Role::kSpinButton; if (input->IsTextButton()) return ButtonRoleType(); if (type == input_type_names::kRange) return ax::mojom::Role::kSlider; if (type == input_type_names::kColor) return ax::mojom::Role::kColorWell; if (type == input_type_names::kTime) return ax::mojom::Role::kInputTime; return ax::mojom::Role::kTextField; } if (auto* select_element = ToHTMLSelectElementOrNull(*GetNode())) { return select_element->IsMultiple() ? ax::mojom::Role::kListBox : ax::mojom::Role::kPopUpButton; } if (auto* option = ToHTMLOptionElementOrNull(*GetNode())) { HTMLSelectElement* select_element = option->OwnerSelectElement(); return !select_element || select_element->IsMultiple() ? ax::mojom::Role::kListBoxOption : ax::mojom::Role::kMenuListOption; } if (IsHTMLTextAreaElement(*GetNode())) return ax::mojom::Role::kTextField; if (HeadingLevel()) return ax::mojom::Role::kHeading; if (IsHTMLDivElement(*GetNode())) return ax::mojom::Role::kGenericContainer; if (IsHTMLMeterElement(*GetNode())) return ax::mojom::Role::kMeter; if (IsHTMLProgressElement(*GetNode())) return ax::mojom::Role::kProgressIndicator; if (IsHTMLOutputElement(*GetNode())) return ax::mojom::Role::kStatus; if (IsHTMLParagraphElement(*GetNode())) return ax::mojom::Role::kParagraph; if (IsHTMLLabelElement(*GetNode())) return ax::mojom::Role::kLabelText; if (IsHTMLLegendElement(*GetNode())) return ax::mojom::Role::kLegend; if (IsHTMLRubyElement(*GetNode())) return ax::mojom::Role::kRuby; if (IsHTMLDListElement(*GetNode())) return ax::mojom::Role::kDescriptionList; if (IsHTMLAudioElement(*GetNode())) return ax::mojom::Role::kAudio; if (IsHTMLVideoElement(*GetNode())) return ax::mojom::Role::kVideo; if (GetNode()->HasTagName(kDdTag)) return ax::mojom::Role::kDescriptionListDetail; if (GetNode()->HasTagName(kDtTag)) return ax::mojom::Role::kDescriptionListTerm; if (GetNode()->nodeName() == "math") return ax::mojom::Role::kMath; if (GetNode()->HasTagName(kRpTag) || GetNode()->HasTagName(kRtTag)) return ax::mojom::Role::kAnnotation; if (IsHTMLFormElement(*GetNode())) return ax::mojom::Role::kForm; if (GetNode()->HasTagName(kAbbrTag)) return ax::mojom::Role::kAbbr; if (GetNode()->HasTagName(kArticleTag)) return ax::mojom::Role::kArticle; if (GetNode()->HasTagName(kDelTag)) return ax::mojom::Role::kContentDeletion; if (GetNode()->HasTagName(kInsTag)) return ax::mojom::Role::kContentInsertion; if (GetNode()->HasTagName(kMainTag)) return ax::mojom::Role::kMain; if (GetNode()->HasTagName(kMarkTag)) return ax::mojom::Role::kMark; if (GetNode()->HasTagName(kNavTag)) return ax::mojom::Role::kNavigation; if (GetNode()->HasTagName(kAsideTag)) return ax::mojom::Role::kComplementary; if (GetNode()->HasTagName(kPreTag)) return ax::mojom::Role::kPre; if (GetNode()->HasTagName(kSectionTag)) return ax::mojom::Role::kRegion; // TODO(accessibility): http://crbug.com/873118 if (GetNode()->HasTagName(kAddressTag)) return ax::mojom::Role::kContentInfo; if (IsHTMLDialogElement(*GetNode())) return ax::mojom::Role::kDialog; // The HTML element should not be exposed as an element. That's what the // LayoutView element does. if (IsHTMLHtmlElement(*GetNode())) return ax::mojom::Role::kIgnored; // Treat