blob: 55b2fbcefa4cb0ad6a877212304485d89e12299d [file] [log] [blame]
/*
* 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 "modules/accessibility/AXNodeObject.h"
#include "core/InputTypeNames.h"
#include "core/dom/AccessibleNode.h"
#include "core/dom/DocumentUserGestureToken.h"
#include "core/dom/Element.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/QualifiedName.h"
#include "core/dom/Text.h"
#include "core/dom/shadow/FlatTreeTraversal.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/frame/FrameView.h"
#include "core/html/HTMLAnchorElement.h"
#include "core/html/HTMLDListElement.h"
#include "core/html/HTMLDivElement.h"
#include "core/html/HTMLFieldSetElement.h"
#include "core/html/HTMLFrameElementBase.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLLabelElement.h"
#include "core/html/HTMLLegendElement.h"
#include "core/html/HTMLMediaElement.h"
#include "core/html/HTMLMeterElement.h"
#include "core/html/HTMLPlugInElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/html/HTMLTableCaptionElement.h"
#include "core/html/HTMLTableCellElement.h"
#include "core/html/HTMLTableElement.h"
#include "core/html/HTMLTableRowElement.h"
#include "core/html/HTMLTableSectionElement.h"
#include "core/html/HTMLTextAreaElement.h"
#include "core/html/LabelsNodeList.h"
#include "core/html/TextControlElement.h"
#include "core/html/forms/RadioInputType.h"
#include "core/html/parser/HTMLParserIdioms.h"
#include "core/html/shadow/MediaControlElementTypes.h"
#include "core/layout/LayoutBlockFlow.h"
#include "core/layout/LayoutObject.h"
#include "core/svg/SVGElement.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "platform/UserGestureIndicator.h"
#include "platform/text/PlatformLocale.h"
#include "platform/weborigin/KURL.h"
#include "platform/wtf/text/StringBuilder.h"
namespace blink {
using namespace HTMLNames;
class SparseAttributeSetter {
USING_FAST_MALLOC(SparseAttributeSetter);
public:
virtual void Run(const AXObjectImpl&,
AXSparseAttributeClient&,
const AtomicString& value) = 0;
};
class BoolAttributeSetter : public SparseAttributeSetter {
public:
BoolAttributeSetter(AXBoolAttribute attribute) : attribute_(attribute) {}
private:
AXBoolAttribute attribute_;
void Run(const AXObjectImpl& obj,
AXSparseAttributeClient& attribute_map,
const AtomicString& value) override {
attribute_map.AddBoolAttribute(attribute_,
EqualIgnoringASCIICase(value, "true"));
}
};
class StringAttributeSetter : public SparseAttributeSetter {
public:
StringAttributeSetter(AXStringAttribute attribute) : attribute_(attribute) {}
private:
AXStringAttribute attribute_;
void Run(const AXObjectImpl& obj,
AXSparseAttributeClient& attribute_map,
const AtomicString& value) override {
attribute_map.AddStringAttribute(attribute_, value);
}
};
class ObjectAttributeSetter : public SparseAttributeSetter {
public:
ObjectAttributeSetter(AXObjectAttribute attribute) : attribute_(attribute) {}
private:
AXObjectAttribute attribute_;
void Run(const AXObjectImpl& obj,
AXSparseAttributeClient& attribute_map,
const AtomicString& value) override {
if (value.IsNull() || value.IsEmpty())
return;
Node* node = obj.GetNode();
if (!node || !node->IsElementNode())
return;
Element* target = ToElement(node)->GetTreeScope().getElementById(value);
if (!target)
return;
AXObjectImpl* ax_target = obj.AxObjectCache().GetOrCreate(target);
if (ax_target)
attribute_map.AddObjectAttribute(attribute_, *ax_target);
}
};
class ObjectVectorAttributeSetter : public SparseAttributeSetter {
public:
ObjectVectorAttributeSetter(AXObjectVectorAttribute attribute)
: attribute_(attribute) {}
private:
AXObjectVectorAttribute attribute_;
void Run(const AXObjectImpl& obj,
AXSparseAttributeClient& attribute_map,
const AtomicString& value) override {
Node* node = obj.GetNode();
if (!node || !node->IsElementNode())
return;
String attribute_value = value.GetString();
if (attribute_value.IsEmpty())
return;
attribute_value.SimplifyWhiteSpace();
Vector<String> ids;
attribute_value.Split(' ', ids);
if (ids.IsEmpty())
return;
HeapVector<Member<AXObjectImpl>> objects;
TreeScope& scope = node->GetTreeScope();
for (const auto& id : ids) {
if (Element* id_element = scope.getElementById(AtomicString(id))) {
AXObjectImpl* ax_id_element =
obj.AxObjectCache().GetOrCreate(id_element);
if (ax_id_element && !ax_id_element->AccessibilityIsIgnored())
objects.push_back(ax_id_element);
}
}
attribute_map.AddObjectVectorAttribute(attribute_, objects);
}
};
using AXSparseAttributeSetterMap =
HashMap<QualifiedName, SparseAttributeSetter*>;
static AXSparseAttributeSetterMap& GetSparseAttributeSetterMap() {
// Use a map from attribute name to properties of that attribute.
// That way we only need to iterate over the list of attributes once,
// rather than calling getAttribute() once for each possible obscure
// accessibility attribute.
DEFINE_STATIC_LOCAL(AXSparseAttributeSetterMap,
ax_sparse_attribute_setter_map, ());
if (ax_sparse_attribute_setter_map.IsEmpty()) {
ax_sparse_attribute_setter_map.Set(
aria_activedescendantAttr,
new ObjectAttributeSetter(AXObjectAttribute::kAriaActiveDescendant));
ax_sparse_attribute_setter_map.Set(
aria_controlsAttr, new ObjectVectorAttributeSetter(
AXObjectVectorAttribute::kAriaControls));
ax_sparse_attribute_setter_map.Set(
aria_flowtoAttr,
new ObjectVectorAttributeSetter(AXObjectVectorAttribute::kAriaFlowTo));
ax_sparse_attribute_setter_map.Set(
aria_detailsAttr,
new ObjectVectorAttributeSetter(AXObjectVectorAttribute::kAriaDetails));
ax_sparse_attribute_setter_map.Set(
aria_errormessageAttr,
new ObjectAttributeSetter(AXObjectAttribute::kAriaErrorMessage));
ax_sparse_attribute_setter_map.Set(
aria_keyshortcutsAttr,
new StringAttributeSetter(AXStringAttribute::kAriaKeyShortcuts));
ax_sparse_attribute_setter_map.Set(
aria_roledescriptionAttr,
new StringAttributeSetter(AXStringAttribute::kAriaRoleDescription));
}
return ax_sparse_attribute_setter_map;
}
AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache)
: AXObjectImpl(ax_object_cache),
aria_role_(kUnknownRole),
children_dirty_(false),
node_(node) {}
AXNodeObject* AXNodeObject::Create(Node* node,
AXObjectCacheImpl& ax_object_cache) {
return new AXNodeObject(node, ax_object_cache);
}
AXNodeObject::~AXNodeObject() {
DCHECK(!node_);
}
void AXNodeObject::AlterSliderValue(bool increase) {
if (RoleValue() != kSliderRole)
return;
float value = ValueForRange();
float step = StepValueForRange();
value += increase ? step : -step;
SetValue(String::Number(value));
AxObjectCache().PostNotification(GetNode(),
AXObjectCacheImpl::kAXValueChanged);
}
AXObjectImpl* AXNodeObject::ActiveDescendant() {
if (!node_ || !node_->IsElementNode())
return nullptr;
const AtomicString& active_descendant_attr =
GetAttribute(aria_activedescendantAttr);
if (active_descendant_attr.IsNull() || active_descendant_attr.IsEmpty())
return nullptr;
Element* element = ToElement(GetNode());
Element* descendant =
element->GetTreeScope().getElementById(active_descendant_attr);
if (!descendant)
return nullptr;
AXObjectImpl* ax_descendant = AxObjectCache().GetOrCreate(descendant);
return ax_descendant;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
#if DCHECK_IS_ON()
// Double-check that an AXObjectImpl 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.
AXObjectImpl* control_object = CorrespondingControlForLabelElement();
if (control_object && control_object->IsCheckboxOrRadio() &&
control_object->NameFromLabelElement()) {
if (ignored_reasons) {
HTMLLabelElement* label = LabelElementContainer();
if (label && label != GetNode()) {
AXObjectImpl* 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()) &&
!EqualIgnoringASCIICase(GetAttribute(aria_hiddenAttr), "false")) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
if (role_ == kUnknownRole) {
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 IsPresentationalInTable(AXObjectImpl* parent,
HTMLElement* current_element) {
if (!current_element)
return false;
Node* parent_node = parent->GetNode();
if (!parent_node || !parent_node->IsHTMLElement())
return false;
// AXTable determines the role as checking isTableXXX.
// If Table has explicit role including presentation, AXTable doesn't assign
// implicit Role to a whole Table. That's why we should check it based on
// node.
// Normal Table Tree is that
// cell(its role)-> tr(tr role)-> tfoot, tbody, thead(ignored role) ->
// table(table role).
// If table has presentation role, it will be like
// cell(group)-> tr(unknown) -> tfoot, tbody, thead(ignored) ->
// table(presentation).
if (IsHTMLTableCellElement(*current_element) &&
isHTMLTableRowElement(*parent_node))
return parent->HasInheritedPresentationalRole();
if (isHTMLTableRowElement(*current_element) &&
IsHTMLTableSectionElement(ToHTMLElement(*parent_node))) {
// Because TableSections have ignored role, presentation should be checked
// with its parent node.
AXObjectImpl* table_object = parent->ParentObject();
Node* table_node = table_object ? table_object->GetNode() : 0;
return isHTMLTableElement(table_node) &&
table_object->HasInheritedPresentationalRole();
}
return false;
}
static bool IsRequiredOwnedElement(AXObjectImpl* parent,
AccessibilityRole current_role,
HTMLElement* current_element) {
Node* parent_node = parent->GetNode();
if (!parent_node || !parent_node->IsHTMLElement())
return false;
if (current_role == kListItemRole)
return IsListElement(parent_node);
if (current_role == kListMarkerRole)
return isHTMLLIElement(*parent_node);
if (current_role == kMenuItemCheckBoxRole || current_role == kMenuItemRole ||
current_role == kMenuItemRadioRole)
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 AXObjectImpl* AXNodeObject::InheritsPresentationalRoleFrom() const {
// ARIA states if an item can get focus, it should not be presentational.
if (CanSetFocusAttribute())
return 0;
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() != kUnknownRole)
return 0;
AXObjectImpl* parent = ParentObject();
if (!parent)
return 0;
HTMLElement* element = nullptr;
if (GetNode() && GetNode()->IsHTMLElement())
element = ToHTMLElement(GetNode());
if (!parent->HasInheritedPresentationalRole()) {
if (!GetLayoutObject() || !GetLayoutObject()->IsBoxModelObject())
return 0;
LayoutBoxModelObject* css_box = ToLayoutBoxModelObject(GetLayoutObject());
if (!css_box->IsTableCell() && !css_box->IsTableRow())
return 0;
if (!IsPresentationalInTable(parent, element))
return 0;
}
// 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 0;
}
// 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<QualifiedName>& GetLandmarkRolesNotAllowed() {
DEFINE_STATIC_LOCAL(HashSet<QualifiedName>, landmark_roles_not_allowed, ());
if (landmark_roles_not_allowed.IsEmpty()) {
landmark_roles_not_allowed.insert(articleTag);
landmark_roles_not_allowed.insert(asideTag);
landmark_roles_not_allowed.insert(navTag);
landmark_roles_not_allowed.insert(sectionTag);
landmark_roles_not_allowed.insert(blockquoteTag);
landmark_roles_not_allowed.insert(detailsTag);
landmark_roles_not_allowed.insert(fieldsetTag);
landmark_roles_not_allowed.insert(figureTag);
landmark_roles_not_allowed.insert(tdTag);
landmark_roles_not_allowed.insert(mainTag);
}
return landmark_roles_not_allowed;
}
bool AXNodeObject::IsDescendantOfElementType(
HashSet<QualifiedName>& 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;
}
AccessibilityRole AXNodeObject::NativeAccessibilityRoleIgnoringAria() const {
if (!GetNode())
return kUnknownRole;
// |HTMLAnchorElement| sets isLink only when it has hrefAttr.
if (GetNode()->IsLink())
return kLinkRole;
if (isHTMLAnchorElement(*GetNode())) {
// We assume that an anchor element is LinkRole if it has event listners
// even though it doesn't have hrefAttr.
if (IsClickable())
return kLinkRole;
return kAnchorRole;
}
if (isHTMLButtonElement(*GetNode()))
return ButtonRoleType();
if (isHTMLDetailsElement(*GetNode()))
return kDetailsRole;
if (isHTMLSummaryElement(*GetNode())) {
ContainerNode* parent = FlatTreeTraversal::Parent(*GetNode());
if (parent && isHTMLDetailsElement(parent))
return kDisclosureTriangleRole;
return kUnknownRole;
}
if (isHTMLInputElement(*GetNode())) {
HTMLInputElement& input = toHTMLInputElement(*GetNode());
const AtomicString& type = input.type();
if (input.DataList())
return kComboBoxRole;
if (type == InputTypeNames::button) {
if ((GetNode()->parentNode() &&
isHTMLMenuElement(GetNode()->parentNode())) ||
(ParentObject() && ParentObject()->RoleValue() == kMenuRole))
return kMenuItemRole;
return ButtonRoleType();
}
if (type == InputTypeNames::checkbox) {
if ((GetNode()->parentNode() &&
isHTMLMenuElement(GetNode()->parentNode())) ||
(ParentObject() && ParentObject()->RoleValue() == kMenuRole))
return kMenuItemCheckBoxRole;
return kCheckBoxRole;
}
if (type == InputTypeNames::date)
return kDateRole;
if (type == InputTypeNames::datetime ||
type == InputTypeNames::datetime_local ||
type == InputTypeNames::month || type == InputTypeNames::week)
return kDateTimeRole;
if (type == InputTypeNames::file)
return kButtonRole;
if (type == InputTypeNames::radio) {
if ((GetNode()->parentNode() &&
isHTMLMenuElement(GetNode()->parentNode())) ||
(ParentObject() && ParentObject()->RoleValue() == kMenuRole))
return kMenuItemRadioRole;
return kRadioButtonRole;
}
if (type == InputTypeNames::number)
return kSpinButtonRole;
if (input.IsTextButton())
return ButtonRoleType();
if (type == InputTypeNames::range)
return kSliderRole;
if (type == InputTypeNames::color)
return kColorWellRole;
if (type == InputTypeNames::time)
return kInputTimeRole;
return kTextFieldRole;
}
if (isHTMLSelectElement(*GetNode())) {
HTMLSelectElement& select_element = toHTMLSelectElement(*GetNode());
return select_element.IsMultiple() ? kListBoxRole : kPopUpButtonRole;
}
if (isHTMLTextAreaElement(*GetNode()))
return kTextFieldRole;
if (HeadingLevel())
return kHeadingRole;
if (isHTMLDivElement(*GetNode()))
return kDivRole;
if (isHTMLMeterElement(*GetNode()))
return kMeterRole;
if (isHTMLOutputElement(*GetNode()))
return kStatusRole;
if (isHTMLParagraphElement(*GetNode()))
return kParagraphRole;
if (isHTMLLabelElement(*GetNode()))
return kLabelRole;
if (isHTMLLegendElement(*GetNode()))
return kLegendRole;
if (isHTMLRubyElement(*GetNode()))
return kRubyRole;
if (isHTMLDListElement(*GetNode()))
return kDescriptionListRole;
if (isHTMLAudioElement(*GetNode()))
return kAudioRole;
if (isHTMLVideoElement(*GetNode()))
return kVideoRole;
if (GetNode()->HasTagName(ddTag))
return kDescriptionListDetailRole;
if (GetNode()->HasTagName(dtTag))
return kDescriptionListTermRole;
if (GetNode()->nodeName() == "math")
return kMathRole;
if (GetNode()->HasTagName(rpTag) || GetNode()->HasTagName(rtTag))
return kAnnotationRole;
if (isHTMLFormElement(*GetNode()))
return kFormRole;
if (GetNode()->HasTagName(abbrTag))
return kAbbrRole;
if (GetNode()->HasTagName(articleTag))
return kArticleRole;
if (GetNode()->HasTagName(mainTag))
return kMainRole;
if (GetNode()->HasTagName(markTag))
return kMarkRole;
if (GetNode()->HasTagName(navTag))
return kNavigationRole;
if (GetNode()->HasTagName(asideTag))
return kComplementaryRole;
if (GetNode()->HasTagName(preTag))
return kPreRole;
if (GetNode()->HasTagName(sectionTag))
return kRegionRole;
if (GetNode()->HasTagName(addressTag))
return kContentInfoRole;
if (isHTMLDialogElement(*GetNode()))
return kDialogRole;
// The HTML element should not be exposed as an element. That's what the
// LayoutView element does.
if (isHTMLHtmlElement(*GetNode()))
return kIgnoredRole;
if (isHTMLIFrameElement(*GetNode())) {
const AtomicString& aria_role =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole);
if (aria_role == "none" || aria_role == "presentation")
return kIframePresentationalRole;
return kIframeRole;
}
// There should only be one banner/contentInfo per page. If header/footer are
// being used within an article or section then it should not be exposed as
// whole page's banner/contentInfo but as a group role.
if (GetNode()->HasTagName(headerTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return kGroupRole;
return kBannerRole;
}
if (GetNode()->HasTagName(footerTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return kGroupRole;
return kFooterRole;
}
if (GetNode()->HasTagName(blockquoteTag))
return kBlockquoteRole;
if (GetNode()->HasTagName(captionTag))
return kCaptionRole;
if (GetNode()->HasTagName(figcaptionTag))
return kFigcaptionRole;
if (GetNode()->HasTagName(figureTag))
return kFigureRole;
if (GetNode()->nodeName() == "TIME")
return kTimeRole;
if (IsEmbeddedObject())
return kEmbeddedObjectRole;
if (isHTMLHRElement(*GetNode()))
return kSplitterRole;
if (IsFieldset())
return kGroupRole;
return kUnknownRole;
}
AccessibilityRole AXNodeObject::DetermineAccessibilityRole() {
if (!GetNode())
return kUnknownRole;
if ((aria_role_ = DetermineAriaRoleAttribute()) != kUnknownRole)
return aria_role_;
if (GetNode()->IsTextNode())
return kStaticTextRole;
AccessibilityRole role = NativeAccessibilityRoleIgnoringAria();
if (role != kUnknownRole)
return role;
if (GetNode()->IsElementNode()) {
Element* element = ToElement(GetNode());
// A generic element with tabIndex explicitly set gets GroupRole.
// The layout checks for focusability aren't critical here; a false
// positive would be harmless.
if (element->IsInCanvasSubtree() && element->SupportsFocus())
return kGroupRole;
}
return kUnknownRole;
}
AccessibilityRole AXNodeObject::DetermineAriaRoleAttribute() const {
const AtomicString& aria_role =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRole);
if (aria_role.IsNull() || aria_role.IsEmpty())
return kUnknownRole;
AccessibilityRole role = AriaRoleToWebCoreRole(aria_role);
// ARIA states if an item can get focus, it should not be presentational.
if ((role == kNoneRole || role == kPresentationalRole) &&
CanSetFocusAttribute())
return kUnknownRole;
if (role == kButtonRole)
role = ButtonRoleType();
role = RemapAriaRoleDueToParent(role);
if (role)
return role;
return kUnknownRole;
}
void AXNodeObject::AccessibilityChildrenFromAttribute(
QualifiedName attr,
AXObjectImpl::AXObjectVector& children) const {
HeapVector<Member<Element>> elements;
ElementsFromAttribute(elements, attr);
AXObjectCacheImpl& cache = AxObjectCache();
for (const auto& element : elements) {
if (AXObjectImpl* child = cache.GetOrCreate(element)) {
// Only aria-labelledby and aria-describedby can target hidden elements.
if (child->AccessibilityIsIgnored() && attr != aria_labelledbyAttr &&
attr != aria_labeledbyAttr && attr != aria_describedbyAttr) {
continue;
}
children.push_back(child);
}
}
}
// This only returns true if this is the element that actually has the
// contentEditable attribute set, unlike node->hasEditableStyle() which will
// also return true if an ancestor is editable.
bool AXNodeObject::HasContentEditableAttributeSet() const {
const AtomicString& content_editable_value =
GetAttribute(contenteditableAttr);
if (content_editable_value.IsNull())
return false;
// Both "true" (case-insensitive) and the empty string count as true.
return content_editable_value.IsEmpty() ||
EqualIgnoringASCIICase(content_editable_value, "true");
}
bool AXNodeObject::IsTextControl() const {
if (HasContentEditableAttributeSet())
return true;
switch (RoleValue()) {
case kTextFieldRole:
case kComboBoxRole:
case kSearchBoxRole:
case kSpinButtonRole:
return true;
default:
return false;
}
}
bool AXNodeObject::IsGenericFocusableElement() const {
if (!CanSetFocusAttribute())
return false;
// If it's a control, it's not generic.
if (IsControl())
return false;
// If it has an aria role, it's not generic.
if (aria_role_ != kUnknownRole)
return false;
// If the content editable attribute is set on this element, that's the reason
// it's focusable, and existing logic should handle this case already - so
// it's not a generic focusable element.
if (HasContentEditableAttributeSet())
return false;
// The web area and body element are both focusable, but existing logic
// handles these cases already, so we don't need to include them here.
if (RoleValue() == kWebAreaRole)
return false;
if (isHTMLBodyElement(GetNode()))
return false;
// An SVG root is focusable by default, but it's probably not interactive, so
// don't include it. It can still be made accessible by giving it an ARIA
// role.
if (RoleValue() == kSVGRootRole)
return false;
return true;
}
AXObjectImpl* AXNodeObject::MenuButtonForMenu() const {
Element* menu_item = MenuItemElementForMenu();
if (menu_item) {
// ARIA just has generic menu items. AppKit needs to know if this is a top
// level items like MenuBarButton or MenuBarItem
AXObjectImpl* menu_item_ax = AxObjectCache().GetOrCreate(menu_item);
if (menu_item_ax && menu_item_ax->IsMenuButton())
return menu_item_ax;
}
return 0;
}
static Element* SiblingWithAriaRole(String role, Node* node) {
Node* parent = node->parentNode();
if (!parent)
return 0;
for (Element* sibling = ElementTraversal::FirstChild(*parent); sibling;
sibling = ElementTraversal::NextSibling(*sibling)) {
const AtomicString& sibling_aria_role =
AccessibleNode::GetProperty(sibling, AOMStringProperty::kRole);
if (EqualIgnoringASCIICase(sibling_aria_role, role))
return sibling;
}
return 0;
}
Element* AXNodeObject::MenuItemElementForMenu() const {
if (AriaRoleAttribute() != kMenuRole)
return 0;
return SiblingWithAriaRole("menuitem", GetNode());
}
Element* AXNodeObject::MouseButtonListener() const {
Node* node = this->GetNode();
if (!node)
return 0;
if (!node->IsElementNode())
node = node->parentElement();
if (!node)
return 0;
for (Element* element = ToElement(node); element;
element = element->parentElement()) {
// It's a pretty common practice to put click listeners on the body or
// document, but that's almost never what the user wants when clicking on an
// accessible element.
if (isHTMLBodyElement(element))
break;
if (element->HasEventListeners(EventTypeNames::click) ||
element->HasEventListeners(EventTypeNames::mousedown) ||
element->HasEventListeners(EventTypeNames::mouseup) ||
element->HasEventListeners(EventTypeNames::DOMActivate))
return element;
}
return 0;
}
AccessibilityRole AXNodeObject::RemapAriaRoleDueToParent(
AccessibilityRole 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 != kListBoxOptionRole && role != kMenuItemRole)
return role;
for (AXObjectImpl* parent = ParentObject();
parent && !parent->AccessibilityIsIgnored();
parent = parent->ParentObject()) {
AccessibilityRole parent_aria_role = parent->AriaRoleAttribute();
// Selects and listboxes both have options as child roles, but they map to
// different roles within WebCore.
if (role == kListBoxOptionRole && parent_aria_role == kMenuRole)
return kMenuItemRole;
// An aria "menuitem" may map to MenuButton or MenuItem depending on its
// parent.
if (role == kMenuItemRole && parent_aria_role == kGroupRole)
return kMenuButtonRole;
// If the parent had a different role, then we don't need to continue
// searching up the chain.
if (parent_aria_role)
break;
}
return role;
}
void AXNodeObject::Init() {
#if DCHECK_IS_ON()
DCHECK(!initialized_);
initialized_ = true;
#endif
role_ = DetermineAccessibilityRole();
}
void AXNodeObject::Detach() {
AXObjectImpl::Detach();
node_ = nullptr;
}
void AXNodeObject::GetSparseAXAttributes(
AXSparseAttributeClient& sparse_attribute_client) const {
Node* node = this->GetNode();
if (!node || !node->IsElementNode())
return;
AXSparseAttributeSetterMap& ax_sparse_attribute_setter_map =
GetSparseAttributeSetterMap();
AttributeCollection attributes = ToElement(node)->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
SparseAttributeSetter* setter =
ax_sparse_attribute_setter_map.at(attr.GetName());
if (setter)
setter->Run(*this, sparse_attribute_client, attr.Value());
}
// TODO(dmazzoni): Efficiently iterate over AccessibleNode properties that are
// set and merge the two loops somehow.
if (ToElement(node)->ExistingAccessibleNode()) {
AtomicString key_shortcuts =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kKeyShortcuts);
if (!key_shortcuts.IsNull()) {
ax_sparse_attribute_setter_map.at(aria_keyshortcutsAttr)
->Run(*this, sparse_attribute_client, key_shortcuts);
}
AtomicString role_description =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kRoleDescription);
if (!role_description.IsNull()) {
ax_sparse_attribute_setter_map.at(aria_roledescriptionAttr)
->Run(*this, sparse_attribute_client, role_description);
}
}
}
bool AXNodeObject::IsAnchor() const {
return !IsNativeImage() && IsLink();
}
bool AXNodeObject::IsControl() const {
Node* node = this->GetNode();
if (!node)
return false;
return ((node->IsElementNode() && ToElement(node)->IsFormControlElement()) ||
AXObjectImpl::IsARIAControl(AriaRoleAttribute()));
}
bool AXNodeObject::IsControllingVideoElement() const {
Node* node = this->GetNode();
if (!node)
return true;
return isHTMLVideoElement(ToParentMediaElement(node));
}
bool AXNodeObject::IsEmbeddedObject() const {
return IsHTMLPlugInElement(GetNode());
}
bool AXNodeObject::IsFieldset() const {
return isHTMLFieldSetElement(GetNode());
}
bool AXNodeObject::IsHeading() const {
return RoleValue() == kHeadingRole;
}
bool AXNodeObject::IsHovered() const {
if (Node* node = this->GetNode())
return node->IsHovered();
return false;
}
bool AXNodeObject::IsImage() const {
return RoleValue() == kImageRole;
}
bool AXNodeObject::IsImageButton() const {
return IsNativeImage() && IsButton();
}
bool AXNodeObject::IsInputImage() const {
Node* node = this->GetNode();
if (RoleValue() == kButtonRole && isHTMLInputElement(node))
return toHTMLInputElement(*node).type() == InputTypeNames::image;
return false;
}
bool AXNodeObject::IsLink() const {
return RoleValue() == kLinkRole;
}
// It is not easily possible to find out if an element is the target of an
// in-page link.
// As a workaround, we check if the element is a sectioning element with an ID,
// or an anchor with a name.
bool AXNodeObject::IsInPageLinkTarget() const {
if (!node_ || !node_->IsElementNode())
return false;
Element* element = ToElement(node_);
// We exclude elements that are in the shadow DOM.
if (element->ContainingShadowRoot())
return false;
if (isHTMLAnchorElement(element)) {
HTMLAnchorElement* html_element = toHTMLAnchorElement(element);
return html_element->HasName() || html_element->HasID();
}
if (element->HasID() && (IsLandmarkRelated() || isHTMLSpanElement(element) ||
isHTMLDivElement(element))) {
return true;
}
return false;
}
bool AXNodeObject::IsMenu() const {
return RoleValue() == kMenuRole;
}
bool AXNodeObject::IsMenuButton() const {
return RoleValue() == kMenuButtonRole;
}
bool AXNodeObject::IsMeter() const {
return RoleValue() == kMeterRole;
}
bool AXNodeObject::IsMultiSelectable() const {
const AtomicString& aria_multi_selectable =
GetAttribute(aria_multiselectableAttr);
if (EqualIgnoringASCIICase(aria_multi_selectable, "true"))
return true;
if (EqualIgnoringASCIICase(aria_multi_selectable, "false"))
return false;
return isHTMLSelectElement(GetNode()) &&
toHTMLSelectElement(*GetNode()).IsMultiple();
}
bool AXNodeObject::IsNativeCheckboxOrRadio() const {
Node* node = this->GetNode();
if (!isHTMLInputElement(node))
return false;
HTMLInputElement* input = toHTMLInputElement(node);
return input->type() == InputTypeNames::checkbox ||
input->type() == InputTypeNames::radio;
}
bool AXNodeObject::IsNativeImage() const {
Node* node = this->GetNode();
if (!node)
return false;
if (isHTMLImageElement(*node))
return true;
if (IsHTMLPlugInElement(*node))
return true;
if (isHTMLInputElement(*node))
return toHTMLInputElement(*node).type() == InputTypeNames::image;
return false;
}
bool AXNodeObject::IsNativeTextControl() const {
Node* node = this->GetNode();
if (!node)
return false;
if (isHTMLTextAreaElement(*node))
return true;
if (isHTMLInputElement(*node))
return toHTMLInputElement(node)->IsTextField();
return false;
}
bool AXNodeObject::IsNonNativeTextControl() const {
if (IsNativeTextControl())
return false;
if (HasContentEditableAttributeSet())
return true;
if (IsARIATextControl())
return true;
return false;
}
bool AXNodeObject::IsPasswordField() const {
Node* node = this->GetNode();
if (!isHTMLInputElement(node))
return false;
AccessibilityRole aria_role = AriaRoleAttribute();
if (aria_role != kTextFieldRole && aria_role != kUnknownRole)
return false;
return toHTMLInputElement(node)->type() == InputTypeNames::password;
}
bool AXNodeObject::IsProgressIndicator() const {
return RoleValue() == kProgressIndicatorRole;
}
bool AXNodeObject::IsRichlyEditable() const {
return HasContentEditableAttributeSet();
}
bool AXNodeObject::IsSlider() const {
return RoleValue() == kSliderRole;
}
bool AXNodeObject::IsNativeSlider() const {
Node* node = this->GetNode();
if (!node)
return false;
if (!isHTMLInputElement(node))
return false;
return toHTMLInputElement(node)->type() == InputTypeNames::range;
}
bool AXNodeObject::IsClickable() const {
if (GetNode()) {
if (GetNode()->IsElementNode() &&
ToElement(GetNode())->IsDisabledFormControl())
return false;
// Note: we can't call getNode()->willRespondToMouseClickEvents() because
// that triggers a style recalc and can delete this.
if (GetNode()->HasEventListeners(EventTypeNames::mouseup) ||
GetNode()->HasEventListeners(EventTypeNames::mousedown) ||
GetNode()->HasEventListeners(EventTypeNames::click) ||
GetNode()->HasEventListeners(EventTypeNames::DOMActivate))
return true;
}
return AXObjectImpl::IsClickable();
}
bool AXNodeObject::IsEnabled() const {
if (IsDescendantOfDisabledNode())
return false;
Node* node = this->GetNode();
if (!node || !node->IsElementNode())
return true;
return !ToElement(node)->IsDisabledFormControl();
}
AccessibilityExpanded AXNodeObject::IsExpanded() const {
if (GetNode() && isHTMLSummaryElement(*GetNode())) {
if (GetNode()->parentNode() &&
isHTMLDetailsElement(GetNode()->parentNode()))
return ToElement(GetNode()->parentNode())->hasAttribute(openAttr)
? kExpandedExpanded
: kExpandedCollapsed;
}
const AtomicString& expanded = GetAttribute(aria_expandedAttr);
if (EqualIgnoringASCIICase(expanded, "true"))
return kExpandedExpanded;
if (EqualIgnoringASCIICase(expanded, "false"))
return kExpandedCollapsed;
return kExpandedUndefined;
}
bool AXNodeObject::IsModal() const {
if (RoleValue() != kDialogRole && RoleValue() != kAlertDialogRole)
return false;
if (HasAttribute(aria_modalAttr)) {
const AtomicString& modal = GetAttribute(aria_modalAttr);
if (EqualIgnoringASCIICase(modal, "true"))
return true;
if (EqualIgnoringASCIICase(modal, "false"))
return false;
}
if (GetNode() && isHTMLDialogElement(*GetNode()))
return ToElement(GetNode())->IsInTopLayer();
return false;
}
bool AXNodeObject::IsPressed() const {
if (!IsButton())
return false;
Node* node = this->GetNode();
if (!node)
return false;
// ARIA button with aria-pressed not undefined, then check for aria-pressed
// attribute rather than getNode()->active()
if (AriaRoleAttribute() == kToggleButtonRole) {
if (EqualIgnoringASCIICase(GetAttribute(aria_pressedAttr), "true") ||
EqualIgnoringASCIICase(GetAttribute(aria_pressedAttr), "mixed"))
return true;
return false;
}
return node->IsActive();
}
bool AXNodeObject::IsReadOnly() const {
Node* node = this->GetNode();
if (!node)
return true;
if (isHTMLTextAreaElement(*node))
return toHTMLTextAreaElement(*node).IsReadOnly();
if (isHTMLInputElement(*node)) {
HTMLInputElement& input = toHTMLInputElement(*node);
if (input.IsTextField())
return input.IsReadOnly();
}
return !HasEditableStyle(*node);
}
bool AXNodeObject::IsRequired() const {
Node* n = this->GetNode();
if (n && (n->IsElementNode() && ToElement(n)->IsFormControlElement()) &&
HasAttribute(requiredAttr))
return ToHTMLFormControlElement(n)->IsRequired();
if (EqualIgnoringASCIICase(GetAttribute(aria_requiredAttr), "true"))
return true;
return false;
}
bool AXNodeObject::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() != kUnknownRole &&
AncestorExposesActiveDescendant())
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;
return node->IsElementNode() && ToElement(node)->SupportsFocus();
}
bool AXNodeObject::CanSetValueAttribute() const {
if (EqualIgnoringASCIICase(GetAttribute(aria_readonlyAttr), "true"))
return false;
if (IsProgressIndicator() || IsSlider())
return true;
if (IsTextControl() && !IsNativeTextControl())
return true;
// Any node could be contenteditable, so isReadOnly should be relied upon
// for this information for all elements.
return !IsReadOnly();
}
bool AXNodeObject::CanSetSelectedAttribute() const {
// ARIA list box options can be selected if they are children of an element
// with an aria-activedescendant attribute.
if (AriaRoleAttribute() == kListBoxOptionRole &&
AncestorExposesActiveDescendant())
return true;
return AXObjectImpl::CanSetSelectedAttribute();
}
bool AXNodeObject::CanvasHasFallbackContent() const {
Node* node = this->GetNode();
if (!isHTMLCanvasElement(node))
return false;
// If it has any children that are elements, we'll assume it might be fallback
// content. If it has no children or its only children are not elements
// (e.g. just text nodes), it doesn't have fallback content.
return ElementTraversal::FirstChild(*node);
}
int AXNodeObject::HeadingLevel() const {
// headings can be in block flow and non-block flow
Node* node = this->GetNode();
if (!node)
return 0;
if (RoleValue() == kHeadingRole) {
String level_str = GetAttribute(aria_levelAttr);
if (!level_str.IsEmpty()) {
int level = level_str.ToInt();
if (level >= 1 && level <= 9)
return level;
return 1;
}
}
if (!node->IsHTMLElement())
return 0;
HTMLElement& element = ToHTMLElement(*node);
if (element.HasTagName(h1Tag))
return 1;
if (element.HasTagName(h2Tag))
return 2;
if (element.HasTagName(h3Tag))
return 3;
if (element.HasTagName(h4Tag))
return 4;
if (element.HasTagName(h5Tag))
return 5;
if (element.HasTagName(h6Tag))
return 6;
return 0;
}
unsigned AXNodeObject::HierarchicalLevel() const {
Node* node = this->GetNode();
if (!node || !node->IsElementNode())
return 0;
Element* element = ToElement(node);
String level_str = element->getAttribute(aria_levelAttr);
if (!level_str.IsEmpty()) {
int level = level_str.ToInt();
if (level > 0)
return level;
return 1;
}
// Only tree item will calculate its level through the DOM currently.
if (RoleValue() != kTreeItemRole)
return 0;
// Hierarchy leveling starts at 1, to match the aria-level spec.
// We measure tree hierarchy by the number of groups that the item is within.
unsigned level = 1;
for (AXObjectImpl* parent = ParentObject(); parent;
parent = parent->ParentObject()) {
AccessibilityRole parent_role = parent->RoleValue();
if (parent_role == kGroupRole)
level++;
else if (parent_role == kTreeRole)
break;
}
return level;
}
String AXNodeObject::AriaAutoComplete() const {
if (RoleValue() != kComboBoxRole)
return String();
const AtomicString& aria_auto_complete =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kAutocomplete)
.DeprecatedLower();
if (aria_auto_complete == "inline" || aria_auto_complete == "list" ||
aria_auto_complete == "both")
return aria_auto_complete;
return String();
}
void AXNodeObject::Markers(Vector<DocumentMarker::MarkerType>& marker_types,
Vector<AXRange>& marker_ranges) const {
if (!GetNode() || !GetDocument() || !GetDocument()->View())
return;
DocumentMarkerController& marker_controller = GetDocument()->Markers();
DocumentMarkerVector markers = marker_controller.MarkersFor(GetNode());
for (size_t i = 0; i < markers.size(); ++i) {
DocumentMarker* marker = markers[i];
switch (marker->GetType()) {
case DocumentMarker::kSpelling:
case DocumentMarker::kGrammar:
case DocumentMarker::kTextMatch:
marker_types.push_back(marker->GetType());
marker_ranges.push_back(
AXRange(marker->StartOffset(), marker->EndOffset()));
break;
case DocumentMarker::kComposition:
// No need for accessibility to know about these marker types.
break;
}
}
}
AXObjectImpl* AXNodeObject::InPageLinkTarget() const {
if (!node_ || !isHTMLAnchorElement(node_) || !GetDocument())
return AXObjectImpl::InPageLinkTarget();
HTMLAnchorElement* anchor = toHTMLAnchorElement(node_);
DCHECK(anchor);
KURL link_url = anchor->Href();
if (!link_url.IsValid())
return AXObjectImpl::InPageLinkTarget();
String fragment = link_url.FragmentIdentifier();
if (fragment.IsEmpty())
return AXObjectImpl::InPageLinkTarget();
KURL document_url = GetDocument()->Url();
if (!document_url.IsValid() ||
!EqualIgnoringFragmentIdentifier(document_url, link_url)) {
return AXObjectImpl::InPageLinkTarget();
}
TreeScope& tree_scope = anchor->GetTreeScope();
Element* target = tree_scope.FindAnchor(fragment);
if (!target)
return AXObjectImpl::InPageLinkTarget();
// If the target is not in the accessibility tree, get the first unignored
// sibling.
return AxObjectCache().FirstAccessibleObjectFromNode(target);
}
AccessibilityOrientation AXNodeObject::Orientation() const {
const AtomicString& aria_orientation =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kOrientation);
AccessibilityOrientation orientation = kAccessibilityOrientationUndefined;
if (EqualIgnoringASCIICase(aria_orientation, "horizontal"))
orientation = kAccessibilityOrientationHorizontal;
else if (EqualIgnoringASCIICase(aria_orientation, "vertical"))
orientation = kAccessibilityOrientationVertical;
switch (RoleValue()) {
case kComboBoxRole:
case kListBoxRole:
case kMenuRole:
case kScrollBarRole:
case kTreeRole:
if (orientation == kAccessibilityOrientationUndefined)
orientation = kAccessibilityOrientationVertical;
return orientation;
case kMenuBarRole:
case kSliderRole:
case kSplitterRole:
case kTabListRole:
case kToolbarRole:
if (orientation == kAccessibilityOrientationUndefined)
orientation = kAccessibilityOrientationHorizontal;
return orientation;
case kRadioGroupRole:
case kTreeGridRole:
return orientation;
default:
return AXObjectImpl::Orientation();
}
}
AXObjectImpl::AXObjectVector AXNodeObject::RadioButtonsInGroup() const {
AXObjectVector radio_buttons;
if (!node_ || RoleValue() != kRadioButtonRole)
return radio_buttons;
if (isHTMLInputElement(node_)) {
HTMLInputElement* radio_button = toHTMLInputElement(node_);
HeapVector<Member<HTMLInputElement>> html_radio_buttons =
FindAllRadioButtonsWithSameName(radio_button);
for (size_t i = 0; i < html_radio_buttons.size(); ++i) {
AXObjectImpl* ax_radio_button =
AxObjectCache().GetOrCreate(html_radio_buttons[i]);
if (ax_radio_button)
radio_buttons.push_back(ax_radio_button);
}
return radio_buttons;
}
// If the immediate parent is a radio group, return all its children that are
// radio buttons.
AXObjectImpl* parent = ParentObject();
if (parent && parent->RoleValue() == kRadioGroupRole) {
for (size_t i = 0; i < parent->Children().size(); ++i) {
AXObjectImpl* child = parent->Children()[i];
DCHECK(child);
if (child->RoleValue() == kRadioButtonRole &&
!child->AccessibilityIsIgnored()) {
radio_buttons.push_back(child);
}
}
}
return radio_buttons;
}
// static
HeapVector<Member<HTMLInputElement>>
AXNodeObject::FindAllRadioButtonsWithSameName(HTMLInputElement* radio_button) {
HeapVector<Member<HTMLInputElement>> all_radio_buttons;
if (!radio_button || radio_button->type() != InputTypeNames::radio)
return all_radio_buttons;
constexpr bool kTraverseForward = true;
constexpr bool kTraverseBackward = false;
HTMLInputElement* first_radio_button = radio_button;
do {
radio_button = RadioInputType::NextRadioButtonInGroup(first_radio_button,
kTraverseBackward);
if (radio_button)
first_radio_button = radio_button;
} while (radio_button);
HTMLInputElement* next_radio_button = first_radio_button;
do {
all_radio_buttons.push_back(next_radio_button);
next_radio_button = RadioInputType::NextRadioButtonInGroup(
next_radio_button, kTraverseForward);
} while (next_radio_button);
return all_radio_buttons;
}
String AXNodeObject::GetText() const {
// If this is a user defined static text, use the accessible name computation.
if (AriaRoleAttribute() == kStaticTextRole)
return ComputedName();
if (!IsTextControl())
return String();
Node* node = this->GetNode();
if (!node)
return String();
if (IsNativeTextControl() &&
(isHTMLTextAreaElement(*node) || isHTMLInputElement(*node)))
return ToTextControlElement(*node).value();
if (!node->IsElementNode())
return String();
return ToElement(node)->innerText();
}
RGBA32 AXNodeObject::ColorValue() const {
if (!isHTMLInputElement(GetNode()) || !IsColorWell())
return AXObjectImpl::ColorValue();
HTMLInputElement* input = toHTMLInputElement(GetNode());
const AtomicString& type = input->getAttribute(typeAttr);
if (!EqualIgnoringASCIICase(type, "color"))
return AXObjectImpl::ColorValue();
// HTMLInputElement::value always returns a string parseable by Color.
Color color;
bool success = color.SetFromString(input->value());
DCHECK(success);
return color.Rgb();
}
AriaCurrentState AXNodeObject::GetAriaCurrentState() const {
const AtomicString& attribute_value =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kCurrent);
if (attribute_value.IsNull())
return kAriaCurrentStateUndefined;
if (attribute_value.IsEmpty() ||
EqualIgnoringASCIICase(attribute_value, "false"))
return kAriaCurrentStateFalse;
if (EqualIgnoringASCIICase(attribute_value, "true"))
return kAriaCurrentStateTrue;
if (EqualIgnoringASCIICase(attribute_value, "page"))
return kAriaCurrentStatePage;
if (EqualIgnoringASCIICase(attribute_value, "step"))
return kAriaCurrentStateStep;
if (EqualIgnoringASCIICase(attribute_value, "location"))
return kAriaCurrentStateLocation;
if (EqualIgnoringASCIICase(attribute_value, "date"))
return kAriaCurrentStateDate;
if (EqualIgnoringASCIICase(attribute_value, "time"))
return kAriaCurrentStateTime;
// An unknown value should return true.
if (!attribute_value.IsEmpty())
return kAriaCurrentStateTrue;
return AXObjectImpl::GetAriaCurrentState();
}
InvalidState AXNodeObject::GetInvalidState() const {
const AtomicString& attribute_value =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kInvalid);
if (EqualIgnoringASCIICase(attribute_value, "false"))
return kInvalidStateFalse;
if (EqualIgnoringASCIICase(attribute_value, "true"))
return kInvalidStateTrue;
if (EqualIgnoringASCIICase(attribute_value, "spelling"))
return kInvalidStateSpelling;
if (EqualIgnoringASCIICase(attribute_value, "grammar"))
return kInvalidStateGrammar;
// A yet unknown value.
if (!attribute_value.IsEmpty())
return kInvalidStateOther;
if (GetNode() && GetNode()->IsElementNode() &&
ToElement(GetNode())->IsFormControlElement()) {
HTMLFormControlElement* element = ToHTMLFormControlElement(GetNode());
HeapVector<Member<HTMLFormControlElement>> invalid_controls;
bool is_invalid = !element->checkValidity(&invalid_controls,
kCheckValidityDispatchNoEvent);
return is_invalid ? kInvalidStateTrue : kInvalidStateFalse;
}
return AXObjectImpl::GetInvalidState();
}
int AXNodeObject::PosInSet() const {
if (SupportsSetSizeAndPosInSet()) {
String pos_in_set_str = GetAttribute(aria_posinsetAttr);
if (!pos_in_set_str.IsEmpty()) {
int pos_in_set = pos_in_set_str.ToInt();
if (pos_in_set > 0)
return pos_in_set;
return 1;
}
return AXObjectImpl::IndexInParent() + 1;
}
return 0;
}
int AXNodeObject::SetSize() const {
if (SupportsSetSizeAndPosInSet()) {
String set_size_str = GetAttribute(aria_setsizeAttr);
if (!set_size_str.IsEmpty()) {
int set_size = set_size_str.ToInt();
if (set_size > 0)
return set_size;
return 1;
}
if (ParentObject()) {
const auto& siblings = ParentObject()->Children();
return siblings.size();
}
}
return 0;
}
String AXNodeObject::AriaInvalidValue() const {
if (GetInvalidState() == kInvalidStateOther)
return GetAOMPropertyOrARIAAttribute(AOMStringProperty::kInvalid);
return String();
}
String AXNodeObject::ValueDescription() const {
if (!SupportsRangeValue())
return String();
return GetAOMPropertyOrARIAAttribute(AOMStringProperty::kValueText)
.GetString();
}
float AXNodeObject::ValueForRange() const {
if (HasAttribute(aria_valuenowAttr))
return GetAttribute(aria_valuenowAttr).ToFloat();
if (IsNativeSlider())
return toHTMLInputElement(*GetNode()).valueAsNumber();
if (isHTMLMeterElement(GetNode()))
return toHTMLMeterElement(*GetNode()).value();
return 0.0;
}
float AXNodeObject::MaxValueForRange() const {
if (HasAttribute(aria_valuemaxAttr))
return GetAttribute(aria_valuemaxAttr).ToFloat();
if (IsNativeSlider())
return toHTMLInputElement(*GetNode()).Maximum();
if (isHTMLMeterElement(GetNode()))
return toHTMLMeterElement(*GetNode()).max();
return 0.0;
}
float AXNodeObject::MinValueForRange() const {
if (HasAttribute(aria_valueminAttr))
return GetAttribute(aria_valueminAttr).ToFloat();
if (IsNativeSlider())
return toHTMLInputElement(*GetNode()).Minimum();
if (isHTMLMeterElement(GetNode()))
return toHTMLMeterElement(*GetNode()).min();
return 0.0;
}
float AXNodeObject::StepValueForRange() const {
if (!IsNativeSlider())
return 0.0;
Decimal step =
toHTMLInputElement(*GetNode()).CreateStepRange(kRejectAny).Step();
return step.ToString().ToFloat();
}
String AXNodeObject::StringValue() const {
Node* node = this->GetNode();
if (!node)
return String();
if (isHTMLSelectElement(*node)) {
HTMLSelectElement& select_element = toHTMLSelectElement(*node);
int selected_index = select_element.selectedIndex();
const HeapVector<Member<HTMLElement>>& list_items =
select_element.GetListItems();
if (selected_index >= 0 &&
static_cast<size_t>(selected_index) < list_items.size()) {
const AtomicString& overridden_description =
list_items[selected_index]->FastGetAttribute(aria_labelAttr);
if (!overridden_description.IsNull())
return overridden_description;
}
if (!select_element.IsMultiple())
return select_element.value();
return String();
}
if (IsNativeTextControl())
return GetText();
// Handle other HTML input elements that aren't text controls, like date and
// time controls, by returning the string value, with the exception of
// checkboxes and radio buttons (which would return "on").
if (isHTMLInputElement(node)) {
HTMLInputElement* input = toHTMLInputElement(node);
if (input->type() != InputTypeNames::checkbox &&
input->type() != InputTypeNames::radio)
return input->value();
}
return String();
}
AccessibilityRole AXNodeObject::AriaRoleAttribute() const {
return aria_role_;
}
// Returns the nearest LayoutBlockFlow ancestor which does not have an
// inlineBoxWrapper - i.e. is not itself an inline object.
static LayoutBlockFlow* NonInlineBlockFlow(LayoutObject* object) {
LayoutObject* current = object;
while (current) {
if (current->IsLayoutBlockFlow()) {
LayoutBlockFlow* block_flow = ToLayoutBlockFlow(current);
if (!block_flow->InlineBoxWrapper())
return block_flow;
}
current = current->Parent();
}
NOTREACHED();
return nullptr;
}
// Returns true if |r1| and |r2| are both non-null, both inline, and are
// contained within the same non-inline LayoutBlockFlow.
static bool IsInSameNonInlineBlockFlow(LayoutObject* r1, LayoutObject* r2) {
if (!r1 || !r2)
return false;
if (!r1->IsInline() || !r2->IsInline())
return false;
LayoutBlockFlow* b1 = NonInlineBlockFlow(r1);
LayoutBlockFlow* b2 = NonInlineBlockFlow(r2);
return b1 && b2 && b1 == b2;
}
//
// New AX name calculation.
//
String AXNodeObject::TextAlternative(bool recursive,
bool in_aria_labelled_by_traversal,
AXObjectSet& visited,
AXNameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources) const {
// If nameSources is non-null, relatedObjects is used in filling it in, so it
// must be non-null as well.
if (name_sources)
DCHECK(related_objects);
bool found_text_alternative = false;
if (!GetNode() && !GetLayoutObject())
return String();
String text_alternative = AriaTextAlternative(
recursive, in_aria_labelled_by_traversal, visited, name_from,
related_objects, name_sources, &found_text_alternative);
if (found_text_alternative && !name_sources)
return text_alternative;
// Step 2E from: http://www.w3.org/TR/accname-aam-1.1
if (recursive && !in_aria_labelled_by_traversal && IsControl() &&
!IsButton()) {
// No need to set any name source info in a recursive call.
if (IsTextControl())
return GetText();
if (IsRange()) {
const AtomicString& aria_valuetext =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kValueText);
if (!aria_valuetext.IsNull())
return aria_valuetext.GetString();
return String::Number(ValueForRange());
}
return StringValue();
}
// Step 2D from: http://www.w3.org/TR/accname-aam-1.1
text_alternative =
NativeTextAlternative(visited, name_from, related_objects, name_sources,
&found_text_alternative);
const bool has_text_alternative =
!text_alternative.IsEmpty() ||
name_from == kAXNameFromAttributeExplicitlyEmpty;
if (has_text_alternative && !name_sources)
return text_alternative;
// Step 2F / 2G from: http://www.w3.org/TR/accname-aam-1.1
if (recursive || NameFromContents()) {
name_from = kAXNameFromContents;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative));
name_sources->back().type = name_from;
}
Node* node = this->GetNode();
if (node && node->IsTextNode())
text_alternative = ToText(node)->wholeText();
else if (isHTMLBRElement(node))
text_alternative = String("\n");
else
text_alternative = TextFromDescendants(visited, false);
if (!text_alternative.IsEmpty()) {
if (name_sources) {
found_text_alternative = true;
name_sources->back().text = text_alternative;
} else {
return text_alternative;
}
}
}
// Step 2H from: http://www.w3.org/TR/accname-aam-1.1
name_from = kAXNameFromTitle;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative, titleAttr));
name_sources->back().type = name_from;
}
const AtomicString& title = GetAttribute(titleAttr);
if (!title.IsEmpty()) {
text_alternative = title;
if (name_sources) {
found_text_alternative = true;
name_sources->back().text = text_alternative;
} else {
return text_alternative;
}
}
name_from = kAXNameFromUninitialized;
if (name_sources && found_text_alternative) {
for (size_t i = 0; i < name_sources->size(); ++i) {
if (!(*name_sources)[i].text.IsNull() && !(*name_sources)[i].superseded) {
NameSource& name_source = (*name_sources)[i];
name_from = name_source.type;
if (!name_source.related_objects.IsEmpty())
*related_objects = name_source.related_objects;
return name_source.text;
}
}
}
return String();
}
String AXNodeObject::TextFromDescendants(AXObjectSet& visited,
bool recursive) const {
if (!CanHaveChildren() && recursive)
return String();
StringBuilder accumulated_text;
AXObjectImpl* previous = nullptr;
AXObjectVector children;
HeapVector<Member<AXObjectImpl>> owned_children;
ComputeAriaOwnsChildren(owned_children);
for (AXObjectImpl* obj = RawFirstChild(); obj; obj = obj->RawNextSibling()) {
if (!AxObjectCache().IsAriaOwned(obj))
children.push_back(obj);
}
for (const auto& owned_child : owned_children)
children.push_back(owned_child);
for (AXObjectImpl* child : children) {
// Don't recurse into children that are explicitly marked as aria-hidden.
// Note that we don't call isInertOrAriaHidden because that would return
// true if any ancestor is hidden, but we need to be able to compute the
// accessible name of object inside hidden subtrees (for example, if
// aria-labelledby points to an object that's hidden).
if (EqualIgnoringASCIICase(child->GetAttribute(aria_hiddenAttr), "true"))
continue;
// If we're going between two layoutObjects that are in separate
// LayoutBoxes, add whitespace if it wasn't there already. Intuitively if
// you have <span>Hello</span><span>World</span>, those are part of the same
// LayoutBox so we should return "HelloWorld", but given
// <div>Hello</div><div>World</div> the strings are in separate boxes so we
// should return "Hello World".
if (previous && accumulated_text.length() &&
!IsHTMLSpace(accumulated_text[accumulated_text.length() - 1])) {
if (!IsInSameNonInlineBlockFlow(child->GetLayoutObject(),
previous->GetLayoutObject()))
accumulated_text.Append(' ');
}
String result;
if (child->IsPresentational())
result = child->TextFromDescendants(visited, true);
else
result = RecursiveTextAlternative(*child, false, visited);
accumulated_text.Append(result);
previous = child;
}
return accumulated_text.ToString();
}
bool AXNodeObject::NameFromLabelElement() const {
// This unfortunately duplicates a bit of logic from textAlternative and
// nativeTextAlternative, but it's necessary because nameFromLabelElement
// needs to be called from computeAccessibilityIsIgnored, which isn't allowed
// to call axObjectCache->getOrCreate.
if (!GetNode() && !GetLayoutObject())
return false;
// Step 2A from: http://www.w3.org/TR/accname-aam-1.1
if (IsHiddenForTextAlternativeCalculation())
return false;
// Step 2B from: http://www.w3.org/TR/accname-aam-1.1
HeapVector<Member<Element>> elements;
AriaLabelledbyElementVector(elements);
if (elements.size() > 0)
return false;
// Step 2C from: http://www.w3.org/TR/accname-aam-1.1
const AtomicString& aria_label =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kLabel);
if (!aria_label.IsEmpty())
return false;
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
// 5.1/5.5 Text inputs, Other labelable Elements
HTMLElement* html_element = nullptr;
if (GetNode()->IsHTMLElement())
html_element = ToHTMLElement(GetNode());
if (html_element && IsLabelableElement(html_element)) {
if (ToLabelableElement(html_element)->labels() &&
ToLabelableElement(html_element)->labels()->length() > 0)
return true;
}
return false;
}
bool AXNodeObject::NameFromContents() const {
Node* node = GetNode();
if (!node || !node->IsElementNode())
return AXObjectImpl::NameFromContents();
// AXObjectImpl::nameFromContents determines whether an element should take
// its name from its descendant contents based on role. However, <select> is a
// special case, as unlike a typical pop-up button it contains its own pop-up
// menu's contents, which should not be used as the name.
if (isHTMLSelectElement(node))
return false;
return AXObjectImpl::NameFromContents();
}
void AXNodeObject::GetRelativeBounds(
AXObjectImpl** out_container,
FloatRect& out_bounds_in_container,
SkMatrix44& out_container_transform) const {
if (LayoutObjectForRelativeBounds()) {
AXObjectImpl::GetRelativeBounds(out_container, out_bounds_in_container,
out_container_transform);
return;
}
*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;
}
}
// If it's in a canvas but doesn't have an explicit rect, get the bounding
// rect of its children.
if (GetNode()->parentElement()->IsInCanvasSubtree()) {
Vector<FloatRect> rects;
for (Node& child : NodeTraversal::ChildrenOf(*GetNode())) {
if (child.IsHTMLElement()) {
if (AXObjectImpl* obj = AxObjectCache().Get(&child)) {
AXObjectImpl* container;
FloatRect bounds;
obj->GetRelativeBounds(&container, bounds, out_container_transform);
if (container) {
*out_container = container;
rects.push_back(bounds);
}
}
}
}
if (*out_container) {
out_bounds_in_container = UnionRect(rects);
return;
}
}
// If this object doesn't have an explicit element rect or computable from its
// children, for now, let's return the position of the ancestor that does have
// a position, and make it the width of that parent, and about the height of a
// line of text, so that it's clear the object is a child of the parent.
for (AXObjectImpl* position_provider = ParentObject(); position_provider;
position_provider = position_provider->ParentObject()) {
if (position_provider->IsAXLayoutObject()) {
position_provider->GetRelativeBounds(
out_container, out_bounds_in_container, out_container_transform);
if (*out_container)
out_bounds_in_container.SetSize(
FloatSize(out_bounds_in_container.Width(),
std::min(10.0f, out_bounds_in_container.Height())));
break;
}
}
}
static Node* GetParentNodeForComputeParent(Node* node) {
if (!node)
return nullptr;
Node* parent_node = nullptr;
// Skip over <optgroup> and consider the <select> the immediate parent of an
// <option>.
if (isHTMLOptionElement(node))
parent_node = toHTMLOptionElement(node)->OwnerSelectElement();
if (!parent_node)
parent_node = node->parentNode();
return parent_node;
}
AXObjectImpl* AXNodeObject::ComputeParent() const {
DCHECK(!IsDetached());
if (Node* parent_node = GetParentNodeForComputeParent(GetNode()))
return AxObjectCache().GetOrCreate(parent_node);
return nullptr;
}
AXObjectImpl* AXNodeObject::ComputeParentIfExists() const {
if (Node* parent_node = GetParentNodeForComputeParent(GetNode()))
return AxObjectCache().Get(parent_node);
return nullptr;
}
AXObjectImpl* AXNodeObject::RawFirstChild() const {
if (!GetNode())
return 0;
Node* first_child = GetNode()->firstChild();
if (!first_child)
return 0;
return AxObjectCache().GetOrCreate(first_child);
}
AXObjectImpl* AXNodeObject::RawNextSibling() const {
if (!GetNode())
return 0;
Node* next_sibling = GetNode()->nextSibling();
if (!next_sibling)
return 0;
return AxObjectCache().GetOrCreate(next_sibling);
}
void AXNodeObject::AddChildren() {
DCHECK(!IsDetached());
// If the need to add more children in addition to existing children arises,
// childrenChanged should have been called, leaving the object with no
// children.
DCHECK(!have_children_);
if (!node_)
return;
have_children_ = true;
// The only time we add children from the DOM tree to a node with a
// layoutObject is when it's a canvas.
if (GetLayoutObject() && !isHTMLCanvasElement(*node_))
return;
HeapVector<Member<AXObjectImpl>> owned_children;
ComputeAriaOwnsChildren(owned_children);
for (Node& child : NodeTraversal::ChildrenOf(*node_)) {
AXObjectImpl* child_obj = AxObjectCache().GetOrCreate(&child);
if (child_obj && !AxObjectCache().IsAriaOwned(child_obj))
AddChild(child_obj);
}
for (const auto& owned_child : owned_children)
AddChild(owned_child);
for (const auto& child : children_)
child->SetParent(this);
}
void AXNodeObject::AddChild(AXObjectImpl* child) {
InsertChild(child, children_.size());
}
void AXNodeObject::InsertChild(AXObjectImpl* child, unsigned index) {
if (!child)
return;
// If the parent is asking for this child's children, then either it's the
// first time (and clearing is a no-op), or its visibility has changed. In the
// latter case, this child may have a stale child cached. This can prevent
// aria-hidden changes from working correctly. Hence, whenever a parent is
// getting children, ensure data is not stale.
child->ClearChildren();
if (child->AccessibilityIsIgnored()) {
const auto& children = child->Children();
size_t length = children.size();
for (size_t i = 0; i < length; ++i)
children_.insert(index + i, children[i]);
} else {
DCHECK_EQ(child->ParentObject(), this);
children_.insert(index, child);
}
}
bool AXNodeObject::CanHaveChildren() const {
// If this is an AXLayoutObject, then it's okay if this object
// doesn't have a node - there are some layoutObjects that don't have
// associated nodes, like scroll areas and css-generated text.
if (!GetNode() && !IsAXLayoutObject())
return false;
if (GetNode() && isHTMLMapElement(GetNode()))
return false;
AccessibilityRole role = RoleValue();
// If an element has an ARIA role of presentation, we need to consider the
// native role when deciding whether it can have children or not - otherwise
// giving something a role of presentation could expose inner implementation
// details.
if (IsPresentational())
role = NativeAccessibilityRoleIgnoringAria();
switch (role) {
case kImageRole:
case kButtonRole:
case kPopUpButtonRole:
case kCheckBoxRole:
case kRadioButtonRole:
case kSwitchRole:
case kTabRole:
case kToggleButtonRole:
case kListBoxOptionRole:
case kScrollBarRole:
return false;
case kStaticTextRole:
if (!AxObjectCache().InlineTextBoxAccessibilityEnabled())
return false;
default:
return true;
}
}
Element* AXNodeObject::ActionElement() const {
Node* node = this->GetNode();
if (!node)
return 0;
if (isHTMLInputElement(*node)) {
HTMLInputElement& input = toHTMLInputElement(*node);
if (!input.IsDisabledFormControl() &&
(IsCheckboxOrRadio() || input.IsTextButton() ||
input.type() == InputTypeNames::file))
return &input;
} else if (isHTMLButtonElement(*node)) {
return ToElement(node);
}
if (AXObjectImpl::IsARIAInput(AriaRoleAttribute()))
return ToElement(node);
if (IsImageButton())
return ToElement(node);
if (isHTMLSelectElement(*node))
return ToElement(node);
switch (RoleValue()) {
case kButtonRole:
case kPopUpButtonRole:
case kToggleButtonRole:
case kTabRole:
case kMenuItemRole:
case kMenuItemCheckBoxRole:
case kMenuItemRadioRole:
return ToElement(node);
default:
break;
}
Element* anchor = AnchorElement();
Element* click_element = MouseButtonListener();
if (!anchor || (click_element && click_element->IsDescendantOf(anchor)))
return click_element;
return anchor;
}
Element* AXNodeObject::AnchorElement() const {
Node* node = this->GetNode();
if (!node)
return 0;
AXObjectCacheImpl& cache = AxObjectCache();
// search up the DOM tree for an anchor element
// NOTE: this assumes that any non-image with an anchor is an
// HTMLAnchorElement
for (; node; node = node->parentNode()) {
if (isHTMLAnchorElement(*node) ||
(node->GetLayoutObject() &&
cache.GetOrCreate(node->GetLayoutObject())->IsAnchor()))
return ToElement(node);
}
return 0;
}
Document* AXNodeObject::GetDocument() const {
if (!GetNode())
return 0;
return &GetNode()->GetDocument();
}
void AXNodeObject::SetNode(Node* node) {
node_ = node;
}
AXObjectImpl* AXNodeObject::CorrespondingControlForLabelElement() const {
HTMLLabelElement* label_element = LabelElementContainer();
if (!label_element)
return 0;
HTMLElement* corresponding_control = label_element->control();
if (!corresponding_control)
return 0;
// Make sure the corresponding control isn't a descendant of this label
// that's in the middle of being destroyed.
if (corresponding_control->GetLayoutObject() &&
!corresponding_control->GetLayoutObject()->Parent())
return 0;
return AxObjectCache().GetOrCreate(corresponding_control);
}
HTMLLabelElement* AXNodeObject::LabelElementContainer() const {
if (!GetNode())
return 0;
// the control element should not be considered part of the label
if (IsControl())
return 0;
// the link element should not be considered part of the label
if (IsLink())
return 0;
// find if this has a ancestor that is a label
return Traversal<HTMLLabelElement>::FirstAncestorOrSelf(*GetNode());
}
void AXNodeObject::SetFocused(bool on) {
if (!CanSetFocusAttribute())
return;
Document* document = this->GetDocument();
if (!on) {
document->ClearFocusedElement();
} else {
Node* node = this->GetNode();
if (node && node->IsElementNode()) {
// If this node is already the currently focused node, then calling
// focus() won't do anything. That is a problem when focus is removed
// from the webpage to chrome, and then returns. In these cases, we need
// to do what keyboard and mouse focus do, which is reset focus first.
if (document->FocusedElement() == node)
document->ClearFocusedElement();
ToElement(node)->focus();
} else {
document->ClearFocusedElement();
}
}
}
void AXNodeObject::Increment() {
UserGestureIndicator gesture_indicator(DocumentUserGestureToken::Create(
GetDocument(), UserGestureToken::kNewGesture));
AlterSliderValue(true);
}
void AXNodeObject::Decrement() {
UserGestureIndicator gesture_indicator(DocumentUserGestureToken::Create(
GetDocument(), UserGestureToken::kNewGesture));
AlterSliderValue(false);
}
void AXNodeObject::SetSequentialFocusNavigationStartingPoint() {
if (!GetNode())
return;
GetNode()->GetDocument().ClearFocusedElement();
GetNode()->GetDocument().SetSequentialFocusNavigationStartingPoint(GetNode());
}
void AXNodeObject::ChildrenChanged() {
// This method is meant as a quick way of marking a portion of the
// accessibility tree dirty.
if (!GetNode() && !GetLayoutObject())
return;
// If this is not part of the accessibility tree because an ancestor
// has only presentational children, invalidate this object's children but
// skip sending a notification and skip walking up the ancestors.
if (AncestorForWhichThisIsAPresentationalChild()) {
SetNeedsToUpdateChildren();
return;
}
AxObjectCache().PostNotification(this, AXObjectCacheImpl::kAXChildrenChanged);
// Go up the accessibility parent chain, but only if the element already
// exists. This method is called during layout, minimal work should be done.
// If AX elements are created now, they could interrogate the layout tree
// while it's in a funky state. At the same time, process ARIA live region
// changes.
for (AXObjectImpl* parent = this; parent;
parent = parent->ParentObjectIfExists()) {
parent->SetNeedsToUpdateChildren();
// These notifications always need to be sent because screenreaders are
// reliant on them to perform. In other words, they need to be sent even
// when the screen reader has not accessed this live region since the last
// update.
// If this element supports ARIA live regions, then notify the AT of
// changes.
if (parent->IsLiveRegion())
AxObjectCache().PostNotification(parent,
AXObjectCacheImpl::kAXLiveRegionChanged);
// If this element is an ARIA text box or content editable, post a "value
// changed" notification on it so that it behaves just like a native input
// element or textarea.
if (IsNonNativeTextControl())
AxObjectCache().PostNotification(parent,
AXObjectCacheImpl::kAXValueChanged);
}
}
void AXNodeObject::SelectionChanged() {
// Post the selected text changed event on the first ancestor that's
// focused (to handle form controls, ARIA text boxes and contentEditable),
// or the web area if the selection is just in the document somewhere.
if (IsFocused() || IsWebArea()) {
AxObjectCache().PostNotification(this,
AXObjectCacheImpl::kAXSelectedTextChanged);
if (GetDocument()) {
AXObjectImpl* document_object =
AxObjectCache().GetOrCreate(GetDocument());
AxObjectCache().PostNotification(
document_object, AXObjectCacheImpl::kAXDocumentSelectionChanged);
}
} else {
AXObjectImpl::SelectionChanged(); // Calls selectionChanged on parent.
}
}
void AXNodeObject::TextChanged() {
// If this element supports ARIA live regions, or is part of a region with an
// ARIA editable role, then notify the AT of changes.
AXObjectCacheImpl& cache = AxObjectCache();
for (Node* parent_node = GetNode(); parent_node;
parent_node = parent_node->parentNode()) {
AXObjectImpl* parent = cache.Get(parent_node);
if (!parent)
continue;
if (parent->IsLiveRegion())
cache.PostNotification(parent_node,
AXObjectCacheImpl::kAXLiveRegionChanged);
// If this element is an ARIA text box or content editable, post a "value
// changed" notification on it so that it behaves just like a native input
// element or textarea.
if (parent->IsNonNativeTextControl())
cache.PostNotification(parent_node, AXObjectCacheImpl::kAXValueChanged);
}
}
void AXNodeObject::UpdateAccessibilityRole() {
role_ = DetermineAccessibilityRole();
bool ignored_status = AccessibilityIsIgnored();
// The AX hierarchy only needs to be updated if the ignored status of an
// element has changed.
if (ignored_status != AccessibilityIsIgnored())
ChildrenChanged();
}
void AXNodeObject::ComputeAriaOwnsChildren(
HeapVector<Member<AXObjectImpl>>& owned_children) const {
if (!HasAttribute(aria_ownsAttr))
return;
Vector<String> id_vector;
if (CanHaveChildren() && !IsNativeTextControl() &&
!HasContentEditableAttributeSet())
TokenVectorFromAttribute(id_vector, aria_ownsAttr);
AxObjectCache().UpdateAriaOwns(this, id_vector, owned_children);
}
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
String AXNodeObject::NativeTextAlternative(
AXObjectSet& visited,
AXNameFrom& name_from,
AXRelatedObjectVector* related_objects,
NameSources* name_sources,
bool* found_text_alternative) const {
if (!GetNode())
return String();
// If nameSources is non-null, relatedObjects is used in filling it in, so it
// must be non-null as well.
if (name_sources)
DCHECK(related_objects);
String text_alternative;
AXRelatedObjectVector local_related_objects;
const HTMLInputElement* input_element = nullptr;
if (isHTMLInputElement(GetNode()))
input_element = toHTMLInputElement(GetNode());
// 5.1/5.5 Text inputs, Other labelable Elements
// If you change this logic, update AXNodeObject::nameFromLabelElement, too.
HTMLElement* html_element = nullptr;
if (GetNode()->IsHTMLElement())
html_element = ToHTMLElement(GetNode());
if (html_element && html_element->IsLabelable()) {
name_from = kAXNameFromRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLabel;
}
LabelsNodeList* labels = ToLabelableElement(html_element)->labels();
if (labels && labels->length() > 0) {
HeapVector<Member<Element>> label_elements;
for (unsigned label_index = 0; label_index < labels->length();
++label_index) {
Element* label = labels->item(label_index);
if (name_sources) {
if (!label->getAttribute(forAttr).IsEmpty() &&
label->getAttribute(forAttr) == html_element->GetIdAttribute()) {
name_sources->back().native_source = kAXTextFromNativeHTMLLabelFor;
} else {
name_sources->back().native_source =
kAXTextFromNativeHTMLLabelWrapped;
}
}
label_elements.push_back(label);
}
text_alternative =
TextFromElements(false, visited, label_elements, related_objects);
if (!text_alternative.IsNull()) {
*found_text_alternative = true;
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
} else {
return text_alternative;
}
} else if (name_sources) {
name_sources->back().invalid = true;
}
}
}
// 5.2 input type="button", input type="submit" and input type="reset"
if (input_element && input_element->IsTextButton()) {
// value attribue
name_from = kAXNameFromValue;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, valueAttr));
name_sources->back().type = name_from;
}
String value = input_element->value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// Get default value if object is not laid out.
// If object is laid out, it will have a layout object for the label.
if (!GetLayoutObject()) {
String default_label = input_element->ValueOrDefaultLabel();
if (value.IsNull() && !default_label.IsNull()) {
// default label
name_from = kAXNameFromContents;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
}
text_alternative = default_label;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
// 5.3 input type="image"
if (input_element &&
input_element->getAttribute(typeAttr) == InputTypeNames::image) {
// alt attr
const AtomicString& alt = input_element->getAttribute(altAttr);
const bool is_empty = alt.IsEmpty() && !alt.IsNull();
name_from =
is_empty ? kAXNameFromAttributeExplicitlyEmpty : kAXNameFromAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, altAttr));
name_sources->back().type = name_from;
}
if (!alt.IsNull()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// value attr
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, valueAttr));
name_sources->back().type = name_from;
}
name_from = kAXNameFromAttribute;
String value = input_element->value();
if (!value.IsNull()) {
text_alternative = value;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// localised default value ("Submit")
name_from = kAXNameFromValue;
text_alternative = input_element->GetLocale().QueryString(
WebLocalizedString::kSubmitButtonDefaultLabel);
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, typeAttr));
NameSource& source = name_sources->back();
source.attribute_value = input_element->getAttribute(typeAttr);
source.type = name_from;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
return text_alternative;
}
// 5.1 Text inputs - step 3 (placeholder attribute)
if (html_element && html_element->IsTextControl()) {
name_from = kAXNameFromPlaceholder;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, placeholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const String placeholder = PlaceholderFromNativeAttribute();
if (!placeholder.IsEmpty()) {
text_alternative = placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value =
html_element->FastGetAttribute(placeholderAttr);
*found_text_alternative = true;
} else {
return text_alternative;
}
}
// Also check for aria-placeholder.
name_from = kAXNameFromPlaceholder;
if (name_sources) {
name_sources->push_back(
NameSource(*found_text_alternative, aria_placeholderAttr));
NameSource& source = name_sources->back();
source.type = name_from;
}
const AtomicString& aria_placeholder =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kPlaceholder);
if (!aria_placeholder.IsEmpty()) {
text_alternative = aria_placeholder;
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.attribute_value = aria_placeholder;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// 5.7 figure and figcaption Elements
if (GetNode()->HasTagName(figureTag)) {
// figcaption
name_from = kAXNameFromRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLFigcaption;
}
Element* figcaption = nullptr;
for (Element& element : ElementTraversal::DescendantsOf(*(GetNode()))) {
if (element.HasTagName(figcaptionTag)) {
figcaption = &element;
break;
}
}
if (figcaption) {
AXObjectImpl* figcaption_ax_object =
AxObjectCache().GetOrCreate(figcaption);
if (figcaption_ax_object) {
text_alternative =
RecursiveTextAlternative(*figcaption_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(new NameSourceRelatedObject(
figcaption_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
return text_alternative;
}
// 5.8 img or area Element
if (isHTMLImageElement(GetNode()) || isHTMLAreaElement(GetNode()) ||
(GetLayoutObject() && GetLayoutObject()->IsSVGImage())) {
// alt
const AtomicString& alt = GetAttribute(altAttr);
const bool is_empty = alt.IsEmpty() && !alt.IsNull();
name_from =
is_empty ? kAXNameFromAttributeExplicitlyEmpty : kAXNameFromAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, altAttr));
name_sources->back().type = name_from;
}
if (!alt.IsNull()) {
text_alternative = alt;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = alt;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// 5.9 table Element
if (isHTMLTableElement(GetNode())) {
HTMLTableElement* table_element = toHTMLTableElement(GetNode());
// caption
name_from = kAXNameFromCaption;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObjectImpl* caption_ax_object = AxObjectCache().GetOrCreate(caption);
if (caption_ax_object) {
text_alternative =
RecursiveTextAlternative(*caption_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
new NameSourceRelatedObject(caption_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
// summary
name_from = kAXNameFromAttribute;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative, summaryAttr));
name_sources->back().type = name_from;
}
const AtomicString& summary = GetAttribute(summaryAttr);
if (!summary.IsNull()) {
text_alternative = summary;
if (name_sources) {
NameSource& source = name_sources->back();
source.attribute_value = summary;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
return text_alternative;
}
// Per SVG AAM 1.0's modifications to 2D of this algorithm.
if (GetNode()->IsSVGElement()) {
name_from = kAXNameFromRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTitleElement;
}
DCHECK(GetNode()->IsContainerNode());
Element* title = ElementTraversal::FirstChild(
ToContainerNode(*(GetNode())), HasTagName(SVGNames::titleTag));
if (title) {
AXObjectImpl* title_ax_object = AxObjectCache().GetOrCreate(title);
if (title_ax_object && !visited.Contains(title_ax_object)) {
text_alternative =
RecursiveTextAlternative(*title_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
new NameSourceRelatedObject(title_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
}
if (name_sources) {
NameSource& source = name_sources->back();
source.text = text_alternative;
source.related_objects = *related_objects;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
// Fieldset / legend.
if (isHTMLFieldSetElement(GetNode())) {
name_from = kAXNameFromRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLLegend;
}
HTMLElement* legend = toHTMLFieldSetElement(GetNode())->Legend();
if (legend) {
AXObjectImpl* legend_ax_object = AxObjectCache().GetOrCreate(legend);
// Avoid an infinite loop
if (legend_ax_object && !visited.Contains(legend_ax_object)) {
text_alternative =
RecursiveTextAlternative(*legend_ax_object, false, visited);
if (related_objects) {
local_related_objects.push_back(
new NameSourceRelatedObject(legend_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
// Document.
if (IsWebArea()) {
Document* document = this->GetDocument();
if (document) {
name_from = kAXNameFromAttribute;
if (name_sources) {
name_sources->push_back(
NameSource(found_text_alternative, aria_labelAttr));
name_sources->back().type = name_from;
}
if (Element* document_element = document->documentElement()) {
const AtomicString& aria_label = AccessibleNode::GetProperty(
document_element, 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 {
return text_alternative;
}
}
}
name_from = kAXNameFromRelatedElement;
if (name_sources) {
name_sources->push_back(NameSource(*found_text_alternative));
name_sources->back().type = name_from;
name_sources->back().native_source = kAXTextFromNativeHTMLTitleElement;
}
text_alternative = document->title();
Element* title_element = document->TitleElement();
AXObjectImpl* title_ax_object =
AxObjectCache().GetOrCreate(title_element);
if (title_ax_object) {
if (related_objects) {
local_related_objects.push_back(
new NameSourceRelatedObject(title_ax_object, text_alternative));
*related_objects = local_related_objects;
local_related_objects.clear();
}
if (name_sources) {
NameSource& source = name_sources->back();
source.related_objects = *related_objects;
source.text = text_alternative;
*found_text_alternative = true;
} else {
return text_alternative;
}
}
}
}
return text_alternative;
}
String AXNodeObject::Description(AXNameFrom name_from,
AXDescriptionFrom& description_from,
AXObjectVector* description_objects) const {
AXRelatedObjectVector related_objects;
String result =
Description(name_from, description_from, nullptr, &related_objects);
if (description_objects) {
description_objects->clear();
for (size_t i = 0; i < related_objects.size(); i++)
description_objects->push_back(related_objects[i]->object);
}
return CollapseWhitespace(result);
}
// Based on
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
String AXNodeObject::Description(AXNameFrom name_from,
AXDescriptionFrom& description_from,
DescriptionSources* description_sources,
AXRelatedObjectVector* related_objects) const {
// If descriptionSources is non-null, relatedObjects is used in filling it in,
// so it must be non-null as well.
if (description_sources)
DCHECK(related_objects);
if (!GetNode())
return String();
String description;
bool found_description = false;
description_from = kAXDescriptionFromRelatedElement;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, aria_describedbyAttr));
description_sources->back().type = description_from;
}
// aria-describedby overrides any other accessible description, from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
const AtomicString& aria_describedby = GetAttribute(aria_describedbyAttr);
if (!aria_describedby.IsNull()) {
if (description_sources)
description_sources->back().attribute_value = aria_describedby;
description = TextFromAriaDescribedby(related_objects);
if (!description.IsNull()) {
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.type = description_from;
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
} else if (description_sources) {
description_sources->back().invalid = true;
}
}
const HTMLInputElement* input_element = nullptr;
if (isHTMLInputElement(GetNode()))
input_element = toHTMLInputElement(GetNode());
// value, 5.2.2 from: http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != kAXNameFromValue && input_element &&
input_element->IsTextButton()) {
description_from = kAXDescriptionFromAttribute;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, valueAttr));
description_sources->back().type = description_from;
}
String value = input_element->value();
if (!value.IsNull()) {
description = value;
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.text = description;
found_description = true;
} else {
return description;
}
}
}
// table caption, 5.9.2 from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != kAXNameFromCaption && isHTMLTableElement(GetNode())) {
HTMLTableElement* table_element = toHTMLTableElement(GetNode());
description_from = kAXDescriptionFromRelatedElement;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
description_sources->back().native_source =
kAXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = table_element->caption();
if (caption) {
AXObjectImpl* caption_ax_object = AxObjectCache().GetOrCreate(caption);
if (caption_ax_object) {
AXObjectSet visited;
description =
RecursiveTextAlternative(*caption_ax_object, false, visited);
if (related_objects)
related_objects->push_back(
new NameSourceRelatedObject(caption_ax_object, description));
if (description_sources) {
DescriptionSource& source = description_sources->back();
source.related_objects = *related_objects;
source.text = description;
found_description = true;
} else {
return description;
}
}
}
}
// summary, 5.6.2 from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != kAXNameFromContents && isHTMLSummaryElement(GetNode())) {
description_from = kAXDescriptionFromContents;
if (description_sources) {
description_sources->push_back(DescriptionSource(found_description));
description_sources->back().type = description_from;
}
AXObjectSet visited;
description = TextFromDescendants(visited, false);
if (!description.IsEmpty()) {
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
// title attribute, from:
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
if (name_from != kAXNameFromTitle) {
description_from = kAXDescriptionFromAttribute;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, titleAttr));
description_sources->back().type = description_from;
}
const AtomicString& title = GetAttribute(titleAttr);
if (!title.IsEmpty()) {
description = title;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
}
// aria-help.
// FIXME: this is not part of the official standard, but it's needed because
// the built-in date/time controls use it.
description_from = kAXDescriptionFromAttribute;
if (description_sources) {
description_sources->push_back(
DescriptionSource(found_description, aria_helpAttr));
description_sources->back().type = description_from;
}
const AtomicString& help = GetAttribute(aria_helpAttr);
if (!help.IsEmpty()) {
description = help;
if (description_sources) {
found_description = true;
description_sources->back().text = description;
} else {
return description;
}
}
description_from = kAXDescriptionFromUninitialized;
if (found_description) {
for (size_t i = 0; i < description_sources->size(); ++i) {
if (!(*description_sources)[i].text.IsNull() &&
!(*description_sources)[i].superseded) {
DescriptionSource& description_source = (*description_sources)[i];
description_from = description_source.type;
if (!description_source.related_objects.IsEmpty())
*related_objects = description_source.related_objects;
return description_source.text;
}
}
}
return String();
}
String AXNodeObject::Placeholder(AXNameFrom name_from) const {
if (name_from == kAXNameFromPlaceholder)
return String();
Node* node = GetNode();
if (!node || !node->IsHTMLElement())
return String();
String native_placeholder = PlaceholderFromNativeAttribute();
if (!native_placeholder.IsEmpty())
return native_placeholder;
const AtomicString& aria_placeholder =
ToHTMLElement(node)->FastGetAttribute(aria_placeholderAttr);
if (!aria_placeholder.IsEmpty())
return aria_placeholder;
return String();
}
String AXNodeObject::PlaceholderFromNativeAttribute() const {
Node* node = GetNode();
if (!node || !IsTextControlElement(node))
return String();
return ToTextControlElement(node)->StrippedPlaceholder();
}
DEFINE_TRACE(AXNodeObject) {
visitor->Trace(node_);
AXObjectImpl::Trace(visitor);
}
} // namespace blink