blob: 48d6a4295b25f44fd85bc1de5a8c17d4cf776546 [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/AccessibleNodeList.h"
#include "core/dom/Element.h"
#include "core/dom/FlatTreeTraversal.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/QualifiedName.h"
#include "core/dom/Text.h"
#include "core/dom/UserGestureIndicator.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/markers/DocumentMarkerController.h"
#include "core/frame/LocalFrameView.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/layout/LayoutBlockFlow.h"
#include "core/layout/LayoutObject.h"
#include "core/svg/SVGElement.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "modules/media_controls/elements/MediaControlElementsHelper.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 AXObject&,
AXSparseAttributeClient&,
const AtomicString& value) = 0;
};
class BoolAttributeSetter : public SparseAttributeSetter {
public:
BoolAttributeSetter(AXBoolAttribute attribute) : attribute_(attribute) {}
private:
AXBoolAttribute attribute_;
void Run(const AXObject& obj,
AXSparseAttributeClient& attribute_map,
const AtomicString& value) override {
// ARIA booleans are true if not "false" and not specifically undefined.
bool is_true = !AccessibleNode::IsUndefinedAttrValue(value) &&
!EqualIgnoringASCIICase(value, "false");
if (is_true) // Not necessary to add if false
attribute_map.AddBoolAttribute(attribute_, true);
}
};
class StringAttributeSetter : public SparseAttributeSetter {
public:
StringAttributeSetter(AXStringAttribute attribute) : attribute_(attribute) {}
private:
AXStringAttribute attribute_;
void Run(const AXObject& 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 AXObject& 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;
AXObject* 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 AXObject& 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<AXObject>> objects;
TreeScope& scope = node->GetTreeScope();
for (const auto& id : ids) {
if (Element* id_element = scope.getElementById(AtomicString(id))) {
AXObject* 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 ObjectAttributeSetter(AXObjectAttribute::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));
ax_sparse_attribute_setter_map.Set(
aria_busyAttr, new BoolAttributeSetter(AXBoolAttribute::kAriaBusy));
}
return ax_sparse_attribute_setter_map;
}
class AXSparseAttributeAOMPropertyClient : public AOMPropertyClient {
public:
AXSparseAttributeAOMPropertyClient(
AXObjectCacheImpl& ax_object_cache,
AXSparseAttributeClient& sparse_attribute_client)
: ax_object_cache_(ax_object_cache),
sparse_attribute_client_(sparse_attribute_client) {}
void AddStringProperty(AOMStringProperty property,
const String& value) override {
AXStringAttribute attribute;
switch (property) {
case AOMStringProperty::kKeyShortcuts:
attribute = AXStringAttribute::kAriaKeyShortcuts;
break;
case AOMStringProperty::kRoleDescription:
attribute = AXStringAttribute::kAriaRoleDescription;
break;
default:
return;
}
sparse_attribute_client_.AddStringAttribute(attribute, value);
}
void AddBooleanProperty(AOMBooleanProperty property, bool value) override {
AXBoolAttribute attribute;
switch (property) {
case AOMBooleanProperty::kBusy:
attribute = AXBoolAttribute::kAriaBusy;
break;
default:
return;
}
sparse_attribute_client_.AddBoolAttribute(attribute, value);
}
void AddIntProperty(AOMIntProperty property, int32_t value) override {}
void AddUIntProperty(AOMUIntProperty property, uint32_t value) override {}
void AddFloatProperty(AOMFloatProperty property, float value) override {}
void AddRelationProperty(AOMRelationProperty property,
const AccessibleNode& value) override {
AXObjectAttribute attribute;
switch (property) {
case AOMRelationProperty::kActiveDescendant:
attribute = AXObjectAttribute::kAriaActiveDescendant;
break;
case AOMRelationProperty::kDetails:
attribute = AXObjectAttribute::kAriaDetails;
break;
case AOMRelationProperty::kErrorMessage:
attribute = AXObjectAttribute::kAriaErrorMessage;
break;
default:
return;
}
Element* target_element = value.element();
AXObject* target_obj = ax_object_cache_->GetOrCreate(target_element);
if (target_element)
sparse_attribute_client_.AddObjectAttribute(attribute, *target_obj);
}
void AddRelationListProperty(AOMRelationListProperty property,
const AccessibleNodeList& relations) override {
AXObjectVectorAttribute attribute;
switch (property) {
case AOMRelationListProperty::kControls:
attribute = AXObjectVectorAttribute::kAriaControls;
break;
case AOMRelationListProperty::kFlowTo:
attribute = AXObjectVectorAttribute::kAriaFlowTo;
break;
default:
return;
}
HeapVector<Member<AXObject>> objects;
for (size_t i = 0; i < relations.length(); ++i) {
AccessibleNode* accessible_node = relations.item(i);
if (accessible_node) {
Element* element = accessible_node->element();
AXObject* ax_element = ax_object_cache_->GetOrCreate(element);
if (ax_element && !ax_element->AccessibilityIsIgnored())
objects.push_back(ax_element);
}
}
sparse_attribute_client_.AddObjectVectorAttribute(attribute, objects);
}
private:
Persistent<AXObjectCacheImpl> ax_object_cache_;
AXSparseAttributeClient& sparse_attribute_client_;
};
AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& ax_object_cache)
: AXObject(ax_object_cache),
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;
OnNativeSetValueAction(String::Number(value));
AxObjectCache().PostNotification(GetNode(),
AXObjectCacheImpl::kAXValueChanged);
}
AXObject* AXNodeObject::ActiveDescendant() {
Element* element = GetElement();
if (!element)
return nullptr;
Element* descendant =
GetAOMPropertyOrARIAAttribute(AOMRelationProperty::kActiveDescendant);
if (!descendant)
return nullptr;
AXObject* ax_descendant = AxObjectCache().GetOrCreate(descendant);
return ax_descendant;
}
bool AXNodeObject::ComputeAccessibilityIsIgnored(
IgnoredReasons* ignored_reasons) const {
#if DCHECK_IS_ON()
// Double-check that an AXObject is never accessed before
// it's been initialized.
DCHECK(initialized_);
#endif
// If this element is within a parent that cannot have children, it should not
// be exposed.
if (IsDescendantOfLeafNode()) {
if (ignored_reasons)
ignored_reasons->push_back(
IgnoredReason(kAXAncestorIsLeafNode, LeafNodeAncestor()));
return true;
}
// Ignore labels that are already referenced by a control.
AXObject* control_object = CorrespondingControlForLabelElement();
if (control_object && control_object->IsCheckboxOrRadio() &&
control_object->NameFromLabelElement()) {
if (ignored_reasons) {
HTMLLabelElement* label = LabelElementContainer();
if (label && label != GetNode()) {
AXObject* label_ax_object = AxObjectCache().GetOrCreate(label);
ignored_reasons->push_back(
IgnoredReason(kAXLabelContainer, label_ax_object));
}
ignored_reasons->push_back(IgnoredReason(kAXLabelFor, control_object));
}
return true;
}
Element* element = GetNode()->IsElementNode() ? ToElement(GetNode())
: GetNode()->parentElement();
if (!GetLayoutObject() && (!element || !element->IsInCanvasSubtree()) &&
!AOMPropertyOrARIAAttributeIsFalse(AOMBooleanProperty::kHidden)) {
if (ignored_reasons)
ignored_reasons->push_back(IgnoredReason(kAXNotRendered));
return true;
}
if (role_ == 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(AXObject* 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.
AXObject* 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(AXObject* 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 AXObject* 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;
AXObject* 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 (isHTMLOptionElement(*GetNode())) {
HTMLSelectElement* select_element =
toHTMLOptionElement(GetNode())->OwnerSelectElement();
return !select_element || select_element->IsMultiple()
? kListBoxOptionRole
: kMenuListOptionRole;
}
if (isHTMLTextAreaElement(*GetNode()))
return kTextFieldRole;
if (HeadingLevel())
return kHeadingRole;
if (isHTMLDivElement(*GetNode()))
return kGenericContainerRole;
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 generic container role.
if (GetNode()->HasTagName(headerTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return kGenericContainerRole;
return kBannerRole;
}
if (GetNode()->HasTagName(footerTag)) {
if (IsDescendantOfElementType(GetLandmarkRolesNotAllowed()))
return kGenericContainerRole;
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();
return role == kUnknownRole ? kGenericContainerRole : role;
}
void AXNodeObject::AccessibilityChildrenFromAOMProperty(
AOMRelationListProperty property,
AXObject::AXObjectVector& children) const {
HeapVector<Member<Element>> elements;
if (!HasAOMPropertyOrARIAAttribute(property, elements))
return;
AXObjectCacheImpl& cache = AxObjectCache();
for (const auto& element : elements) {
if (AXObject* child = cache.GetOrCreate(element)) {
// Only aria-labelledby and aria-describedby can target hidden elements.
if (child->AccessibilityIsIgnored() &&
property != AOMRelationListProperty::kLabeledBy &&
property != AOMRelationListProperty::kDescribedBy) {
continue;
}
children.push_back(child);
}
}
}
bool AXNodeObject::IsMultiline() const {
Node* node = this->GetNode();
if (!node)
return false;
const AccessibilityRole role = RoleValue();
const bool is_edit_box = role == kSearchBoxRole || role == kTextFieldRole;
if (!IsEditable() && !is_edit_box)
return false; // Doesn't support multiline.
// Supports aria-multiline, so check for attribute.
bool is_multiline = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiline,
is_multiline)) {
return is_multiline;
}
// Default for <textarea> is true.
if (isHTMLTextAreaElement(*node))
return true;
// Default for other edit boxes is false, including for ARIA, says CORE-AAM.
if (is_edit_box)
return false;
// If root of contenteditable area and no ARIA role of textbox/searchbox used,
// default to multiline=true which is what the default behavior is.
return HasContentEditableAttributeSet();
}
// 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");
}
// TODO(dmazzoni) Find a more appropriate name or consider returning false
// for everything but a searchbox or textfield, as a combobox and spinbox
// can contain a field but should not be considered edit controls themselves.
// Combo box text fields should return true though.
bool AXNodeObject::IsTextControl() const {
if (HasContentEditableAttributeSet())
return true;
switch (RoleValue()) {
case kTextFieldRole:
case kComboBoxRole:
case kSearchBoxRole:
// TODO(dmazzoni): kSpinButtonRole might need to be removed.
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;
}
AXObject* 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
AXObject* 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::GetPropertyOrARIAAttribute(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 nullptr;
if (!node->IsElementNode())
node = node->parentElement();
if (!node)
return nullptr;
for (Element* element = ToElement(node); element;
element = element->parentElement()) {
if (element->HasEventListeners(EventTypeNames::click) ||
element->HasEventListeners(EventTypeNames::mousedown) ||
element->HasEventListeners(EventTypeNames::mouseup) ||
element->HasEventListeners(EventTypeNames::DOMActivate))
return element;
}
return nullptr;
}
void AXNodeObject::Init() {
#if DCHECK_IS_ON()
DCHECK(!initialized_);
initialized_ = true;
#endif
AXObject::Init();
}
void AXNodeObject::Detach() {
AXObject::Detach();
node_ = nullptr;
}
void AXNodeObject::GetSparseAXAttributes(
AXSparseAttributeClient& sparse_attribute_client) const {
Element* element = GetElement();
if (!element)
return;
AXSparseAttributeAOMPropertyClient property_client(*ax_object_cache_,
sparse_attribute_client);
HashSet<QualifiedName> shadowed_aria_attributes;
AccessibleNode::GetAllAOMProperties(element, &property_client,
shadowed_aria_attributes);
AXSparseAttributeSetterMap& ax_sparse_attribute_setter_map =
GetSparseAttributeSetterMap();
AttributeCollection attributes = element->AttributesWithoutUpdate();
for (const Attribute& attr : attributes) {
if (shadowed_aria_attributes.Contains(attr.GetName()))
continue;
SparseAttributeSetter* setter =
ax_sparse_attribute_setter_map.at(attr.GetName());
if (setter)
setter->Run(*this, sparse_attribute_client, attr.Value());
}
}
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()) ||
AXObject::IsARIAControl(AriaRoleAttribute()));
}
bool AXNodeObject::IsControllingVideoElement() const {
Node* node = this->GetNode();
if (!node)
return true;
return isHTMLVideoElement(
MediaControlElementsHelper::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 {
switch (RoleValue()) {
case kGridRole:
case kTreeGridRole:
case kTreeRole:
case kListBoxRole:
case kTabListRole: {
bool multiselectable = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kMultiselectable,
multiselectable)) {
return multiselectable;
}
}
default:
break;
}
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::IsMoveableSplitter() const {
return RoleValue() == kSplitterRole && CanSetFocusAttribute();
}
bool AXNodeObject::IsClickable() const {
Node* node = GetNode();
if (!node)
return false;
if (node->IsElementNode() && ToElement(node)->IsDisabledFormControl()) {
return false;
}
// Note: we can't call |node->WillRespondToMouseClickEvents()| because that
// triggers a style recalc and can delete this.
if (node->HasEventListeners(EventTypeNames::mouseup) ||
node->HasEventListeners(EventTypeNames::mousedown) ||
node->HasEventListeners(EventTypeNames::click) ||
node->HasEventListeners(EventTypeNames::DOMActivate)) {
return true;
}
return AXObject::IsClickable();
}
bool AXNodeObject::CanSupportAriaReadOnly() const {
switch (RoleValue()) {
case kCellRole:
case kCheckBoxRole:
case kColorWellRole:
case kColumnHeaderRole:
case kComboBoxRole:
case kDateRole:
case kDateTimeRole:
case kGridRole:
case kInputTimeRole:
case kListBoxRole:
case kMenuButtonRole:
case kMenuItemCheckBoxRole:
case kMenuItemRadioRole:
case kPopUpButtonRole:
case kRadioGroupRole:
case kRowHeaderRole:
case kSearchBoxRole:
case kSliderRole:
case kSpinButtonRole:
case kSwitchRole:
case kTextFieldRole:
case kToggleButtonRole:
case kTreeGridRole:
return true;
default:
break;
}
return false;
}
AXRestriction AXNodeObject::Restriction() const {
Element* elem = GetElement();
if (!elem)
return kNone;
// An <optgroup> is not exposed directly in the AX tree.
if (isHTMLOptGroupElement(elem))
return kNone;
// According to ARIA, all elements of the base markup can be disabled.
// According to CORE-AAM, any focusable descendant of aria-disabled
// ancestor is also disabled.
bool is_disabled;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kDisabled,
is_disabled)) {
// Has aria-disabled, overrides native markup determining disabled.
if (is_disabled)
return kDisabled;
} else if (elem->IsDisabledFormControl() ||
(CanSetFocusAttribute() && IsDescendantOfDisabledNode())) {
// No aria-disabled, but other markup says it's disabled.
return kDisabled;
}
// Check aria-readonly if supported by current role.
bool is_read_only;
if (CanSupportAriaReadOnly() &&
HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kReadOnly,
is_read_only)) {
// ARIA overrides other readonly state markup.
return is_read_only ? kReadOnly : kNone;
}
// Only editable fields can be marked @readonly (unlike @aria-readonly).
if (isHTMLTextAreaElement(*elem) && toHTMLTextAreaElement(*elem).IsReadOnly())
return kReadOnly;
if (isHTMLInputElement(*elem)) {
HTMLInputElement& input = toHTMLInputElement(*elem);
if (input.IsTextField() && input.IsReadOnly())
return kReadOnly;
}
// This is a node that is not readonly and not disabled.
return kNone;
}
AccessibilityExpanded AXNodeObject::IsExpanded() const {
if (GetNode() && isHTMLSummaryElement(*GetNode())) {
if (GetNode()->parentNode() &&
isHTMLDetailsElement(GetNode()->parentNode()))
return ToElement(GetNode()->parentNode())->hasAttribute(openAttr)
? kExpandedExpanded
: kExpandedCollapsed;
}
bool expanded = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kExpanded, expanded)) {
return expanded ? kExpandedExpanded : kExpandedCollapsed;
}
return kExpandedUndefined;
}
bool AXNodeObject::IsModal() const {
if (RoleValue() != kDialogRole && RoleValue() != kAlertDialogRole)
return false;
bool modal = false;
if (HasAOMPropertyOrARIAAttribute(AOMBooleanProperty::kModal, modal))
return modal;
if (GetNode() && isHTMLDialogElement(*GetNode()))
return ToElement(GetNode())->IsInTopLayer();
return false;
}
bool AXNodeObject::IsRequired() const {
Node* n = this->GetNode();
if (n && (n->IsElementNode() && ToElement(n)->IsFormControlElement()) &&
HasAttribute(requiredAttr))
return ToHTMLFormControlElement(n)->IsRequired();
if (AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kRequired))
return true;
return false;
}
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) {
uint32_t level;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, level)) {
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 {
Element* element = GetElement();
if (!element)
return 0;
uint32_t level;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kLevel, level)) {
if (level >= 1 && level <= 9)
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.
level = 1;
for (AXObject* 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 (!IsARIATextControl())
return String();
const AtomicString& aria_auto_complete =
GetAOMPropertyOrARIAAttribute(AOMStringProperty::kAutocomplete)
.DeprecatedLower();
// Illegal values must be passed through, according to CORE-AAM.
return aria_auto_complete == "none" ? String() : aria_auto_complete;
}
namespace {
bool MarkerTypeIsUsedForAccessibility(DocumentMarker::MarkerType type) {
return DocumentMarker::MarkerTypes(
DocumentMarker::kSpelling | DocumentMarker::kGrammar |
DocumentMarker::kTextMatch | DocumentMarker::kActiveSuggestion |
DocumentMarker::kSuggestion)
.Contains(type);
}
} // namespace
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];
if (MarkerTypeIsUsedForAccessibility(marker->GetType())) {
marker_types.push_back(marker->GetType());
marker_ranges.push_back(
AXRange(marker->StartOffset(), marker->EndOffset()));
}
}
}
AXObject* AXNodeObject::InPageLinkTarget() const {
if (!node_ || !isHTMLAnchorElement(node_) || !GetDocument())
return AXObject::InPageLinkTarget();
HTMLAnchorElement* anchor = toHTMLAnchorElement(node_);
DCHECK(anchor);
KURL link_url = anchor->Href();
if (!link_url.IsValid())
return AXObject::InPageLinkTarget();
String fragment = link_url.FragmentIdentifier();
if (fragment.IsEmpty())
return AXObject::InPageLinkTarget();
KURL document_url = GetDocument()->Url();
if (!document_url.IsValid() ||
!EqualIgnoringFragmentIdentifier(document_url, link_url)) {
return AXObject::InPageLinkTarget();
}
TreeScope& tree_scope = anchor->GetTreeScope();
Element* target = tree_scope.FindAnchor(fragment);
if (!target)
return AXObject::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 AXObject::Orientation();
}
}
AXObject::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) {
AXObject* 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.
AXObject* parent = ParentObject();
if (parent && parent->RoleValue() == kRadioGroupRole) {
for (size_t i = 0; i < parent->Children().size(); ++i) {
AXObject* 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 (!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 AXObject::ColorValue();
HTMLInputElement* input = toHTMLInputElement(GetNode());
const AtomicString& type = input->getAttribute(typeAttr);
if (!EqualIgnoringASCIICase(type, "color"))
return AXObject::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 AXObject::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 AXObject::GetInvalidState();
}
int AXNodeObject::PosInSet() const {
if (SupportsSetSizeAndPosInSet()) {
uint32_t pos_in_set;
if (HasAOMPropertyOrARIAAttribute(AOMUIntProperty::kPosInSet, pos_in_set))
return pos_in_set;
return AutoPosInSet();
}
return 0;
}
int AXNodeObject::SetSize() const {
if (SupportsSetSizeAndPosInSet()) {
int32_t set_size;
if (HasAOMPropertyOrARIAAttribute(AOMIntProperty::kSetSize, set_size))
return set_size;
return AutoSetSize();
}
return 0;
}
int AXNodeObject::AutoPosInSet() const {
AXObject* parent = ParentObject();
if (!parent)
return 0;
int pos_in_set = 1;
auto siblings = parent->Children();
AccessibilityRole role = RoleValue();
int level = HierarchicalLevel();
int index_in_parent = IndexInParent();
for (int index = index_in_parent - 1; index >= 0; index--) {
const auto sibling = siblings[index];
AccessibilityRole sibling_role = sibling->RoleValue();
if (sibling_role == kSplitterRole)
break; // Set stops at a separator
if (sibling_role != role || sibling->AccessibilityIsIgnored())
continue;
int sibling_level = sibling->HierarchicalLevel();
if (sibling_level < level)
break;
if (sibling_level > level)
continue; // Skip subset
++pos_in_set;
}
return pos_in_set;
}
int AXNodeObject::AutoSetSize() const {
AXObject* parent = ParentObject();
if (!parent)
return 0;
int set_size = AutoPosInSet();
auto siblings = parent->Children();
AccessibilityRole role = RoleValue();
int level = HierarchicalLevel();
int index_in_parent = IndexInParent();
int sibling_count = siblings.size();
for (int index = index_in_parent + 1; index < sibling_count; index++) {
const auto sibling = siblings[index];
AccessibilityRole sibling_role = sibling->RoleValue();
if (sibling_role == kSplitterRole)
break; // Set stops at a separator
if (sibling_role != role || sibling->AccessibilityIsIgnored())
continue;
int sibling_level = sibling->HierarchicalLevel();
if (sibling_level < level)
break;
if (sibling_level > level)
continue; // Skip subset
++set_size;
}
return set_size;
}
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 {
float value_now;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueNow, value_now))
return value_now;
if (IsNativeSlider())
return toHTMLInputElement(*GetNode()).valueAsNumber();
if (isHTMLMeterElement(GetNode()))
return toHTMLMeterElement(*GetNode()).value();
return 0.0;
}
float AXNodeObject::MaxValueForRange() const {
float value_max;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueMax, value_max))
return value_max;
if (IsNativeSlider())
return toHTMLInputElement(*GetNode()).Maximum();
if (isHTMLMeterElement(GetNode()))
return toHTMLMeterElement(*GetNode()).max();
return 0.0;
}
float AXNodeObject::MinValueForRange() const {
float value_min;
if (HasAOMPropertyOrARIAAttribute(AOMFloatProperty::kValueMin, value_min))
return value_min;
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 -- value from control
if (recursive && !in_aria_labelled_by_traversal && CanSetValueAttribute()) {
// 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 (in_aria_labelled_by_traversal || NameFromContents(recursive)) {
Node* node = GetNode();
if (!isHTMLSelectElement(node)) { // Avoid option descendant text
name_from = kAXNameFromContents;
if (name_sources) {
name_sources->push_back(NameSource(found_text_alternative));
name_sources->back().type = name_from;
}
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;
AXObject* previous = nullptr;
AXObjectVector children;
HeapVector<Member<AXObject>> owned_children;
ComputeAriaOwnsChildren(owned_children);
for (AXObject* 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 (AXObject* 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 (child->AOMPropertyOrARIAAttributeIsTrue(AOMBooleanProperty::kHidden))
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;
}
void AXNodeObject::GetRelativeBounds(
AXObject** out_container,
FloatRect& out_bounds_in_container,
SkMatrix44& out_container_transform) const {
if (LayoutObjectForRelativeBounds()) {
AXObject::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 (AXObject* obj = AxObjectCache().Get(&child)) {
AXObject* 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 (AXObject* 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;
}
AXObject* AXNodeObject::ComputeParent() const {
DCHECK(!IsDetached());
if (Node* parent_node = GetParentNodeForComputeParent(GetNode()))
return AxObjectCache().GetOrCreate(parent_node);
return nullptr;
}
AXObject* AXNodeObject::ComputeParentIfExists() const {
if (Node* parent_node = GetParentNodeForComputeParent(GetNode()))
return AxObjectCache().Get(parent_node);
return nullptr;
}
AXObject* AXNodeObject::RawFirstChild() const {
if (!GetNode())
return 0;
Node* first_child = GetNode()->firstChild();
if (!first_child)
return 0;
return AxObjectCache().GetOrCreate(first_child);
}
AXObject* 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<AXObject>> owned_children;
ComputeAriaOwnsChildren(owned_children);
for (Node& child : NodeTraversal::ChildrenOf(*node_)) {
AXObject* 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);
AddAccessibleNodeChildren();
}
void AXNodeObject::AddChild(AXObject* child) {
InsertChild(child, children_.size());
}
void AXNodeObject::InsertChild(AXObject* 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; // Does not have a role, so check here
// Placeholder gets exposed as an attribute on the input accessibility node,
// so there's no need to add its text children.
if (GetElement() && GetElement()->ShadowPseudoId() ==
AtomicString("-webkit-input-placeholder")) {
return false;
}
switch (NativeAccessibilityRoleIgnoringAria()) {
case kButtonRole:
case kCheckBoxRole:
case kImageRole:
case kListBoxOptionRole:
case kMenuButtonRole:
case kMenuListOptionRole:
case kMenuItemRole:
case kMenuItemCheckBoxRole:
case kMenuItemRadioRole:
case kProgressIndicatorRole:
case kRadioButtonRole:
case kScrollBarRole:
// case kSearchBoxRole:
case kSliderRole:
case kSplitterRole:
case kSwitchRole:
case kTabRole:
// case kTextFieldRole:
case kToggleButtonRole:
return false;
case kPopUpButtonRole:
return true;
case kStaticTextRole:
return AxObjectCache().InlineTextBoxAccessibilityEnabled();
default:
break;
}
switch (AriaRoleAttribute()) {
case kImageRole:
return false;
case kButtonRole:
case kCheckBoxRole:
case kListBoxOptionRole:
case kMathRole: // role="math" is flat, unlike <math>
case kMenuButtonRole:
case kMenuListOptionRole:
case kMenuItemRole:
case kMenuItemCheckBoxRole:
case kMenuItemRadioRole:
case kPopUpButtonRole:
case kProgressIndicatorRole:
case kRadioButtonRole:
case kScrollBarRole:
case kSliderRole:
case kSplitterRole:
case kSwitchRole:
case kTabRole:
case kToggleButtonRole: {
// These roles have ChildrenPresentational: true in the ARIA spec.
// We used to remove/prune all descendants of them, but that removed
// useful content if the author didn't follow the spec perfectly, for
// example if they wanted a complex radio button with a textfield child.
// We are now only pruning these if there is a single text child,
// otherwise the subtree is exposed. The ChildrenPresentational rule
// is thus useful for authoring/verification tools but does not break
// complex widget implementations.
Element* element = GetElement();
return element && !element->HasOneTextChild();
}
default:
break;
}
return true;
}
Element* AXNodeObject::ActionElement() const {
Node* node = this->GetNode();
if (!node)
return nullptr;
if (node->IsElementNode() && IsClickable())
return ToElement(node);
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;
}
AXObject* 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());
}
bool AXNodeObject::OnNativeFocusAction() {
if (!CanSetFocusAttribute())
return false;
Document* document = GetDocument();
if (IsWebArea()) {
document->ClearFocusedElement();
return true;
}
Element* element = GetElement();
if (!element) {
document->ClearFocusedElement();
return true;
}
// 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() == element)
document->ClearFocusedElement();
element->focus();
return true;
}
bool AXNodeObject::OnNativeIncrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
std::unique_ptr<UserGestureIndicator> gesture_indicator =
LocalFrame::CreateUserGesture(frame, UserGestureToken::kNewGesture);
AlterSliderValue(true);
return true;
}
bool AXNodeObject::OnNativeDecrementAction() {
LocalFrame* frame = GetDocument() ? GetDocument()->GetFrame() : nullptr;
std::unique_ptr<UserGestureIndicator> gesture_indicator =
LocalFrame::CreateUserGesture(frame, UserGestureToken::kNewGesture);
AlterSliderValue(false);
return true;
}
bool AXNodeObject::OnNativeSetSequentialFocusNavigationStartingPointAction() {
if (!GetNode())
return false;
Document* document = GetDocument();
document->ClearFocusedElement();
document->SetSequentialFocusNavigationStartingPoint(GetNode());
return true;
}
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 node's children are not part of the accessibility tree then
// invalidate the children but skip notification and walking up the ancestors.
// Cases where this happens:
// - an ancestor has only presentational children, or
// - this or an ancestor is a leaf node
// Uses |cached_is_descendant_of_leaf_node_| to avoid updating cached
// attributes for eachc change via | UpdateCachedAttributeValuesIfNeeded()|.
if (!CanHaveChildren() || cached_is_descendant_of_leaf_node_) {
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 (AXObject* 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()) {
AXObject* document_object = AxObjectCache().GetOrCreate(GetDocument());
AxObjectCache().PostNotification(
document_object, AXObjectCacheImpl::kAXDocumentSelectionChanged);
}
} else {
AXObject::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()) {
AXObject* 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<AXObject>>& owned_children) const {
Vector<String> id_vector;
if (!CanHaveChildren() || IsNativeTextControl() ||
HasContentEditableAttributeSet()) {
if (GetNode())
AxObjectCache().UpdateAriaOwns(this, id_vector, owned_children);
return;
}
HeapVector<Member<Element>> elements;
if (HasAOMProperty(AOMRelationListProperty::kOwns, elements)) {
AxObjectCache().UpdateAriaOwns(this, id_vector, owned_children);
for (const auto& element : elements) {
AXObject* ax_element = ax_object_cache_->GetOrCreate(&*element);
if (ax_element && !ax_element->AccessibilityIsIgnored())
owned_children.push_back(ax_element);
}
return;
}
if (!HasAttribute(aria_ownsAttr))
return;
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) {
AXObject* 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) {
AXObject* 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) {
AXObject* 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) {
AXObject* 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::GetPropertyOrARIAAttribute(
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();
AXObject* 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
// AOM version.
HeapVector<Member<Element>> elements;
if (HasAOMProperty(AOMRelationListProperty::kDescribedBy, elements)) {
AXObjectSet visited;
description = TextFromElements(true, visited, elements, 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;
}
}
// 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) {
AXObject* 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_);
AXObject::Trace(visitor);
}
} // namespace blink