blob: 9c0d892fd23d89d5885f3b62422bd9403742e26c [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 "config.h"
#include "modules/accessibility/AXNodeObject.h"
#include "core/InputTypeNames.h"
#include "core/dom/NodeTraversal.h"
#include "core/dom/Text.h"
#include "core/dom/shadow/ComposedTreeTraversal.h"
#include "core/html/HTMLDListElement.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/parser/HTMLParserIdioms.h"
#include "core/html/shadow/MediaControlElements.h"
#include "core/layout/LayoutBlockFlow.h"
#include "core/layout/LayoutObject.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "platform/UserGestureIndicator.h"
#include "platform/text/PlatformLocale.h"
#include "wtf/text/StringBuilder.h"
namespace blink {
using namespace HTMLNames;
AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& axObjectCache)
: AXObject(axObjectCache)
, m_ariaRole(UnknownRole)
, m_childrenDirty(false)
#if ENABLE(ASSERT)
, m_initialized(false)
#endif
, m_node(node)
{
}
AXNodeObject* AXNodeObject::create(Node* node, AXObjectCacheImpl& axObjectCache)
{
return new AXNodeObject(node, axObjectCache);
}
AXNodeObject::~AXNodeObject()
{
ASSERT(!m_node);
}
// This function implements the ARIA accessible name as described by the Mozilla
// ARIA Implementer's Guide.
static String accessibleNameForNode(Node* node)
{
if (!node)
return String();
if (node->isTextNode())
return toText(node)->data();
if (isHTMLInputElement(*node))
return toHTMLInputElement(*node).value();
if (node->isHTMLElement()) {
const AtomicString& alt = toHTMLElement(node)->getAttribute(altAttr);
if (!alt.isEmpty())
return alt;
const AtomicString& title = toHTMLElement(node)->getAttribute(titleAttr);
if (!title.isEmpty())
return title;
}
return String();
}
String AXNodeObject::accessibilityDescriptionForElements(WillBeHeapVector<RawPtrWillBeMember<Element>> &elements) const
{
StringBuilder builder;
unsigned size = elements.size();
for (unsigned i = 0; i < size; ++i) {
Element* idElement = elements[i];
builder.append(accessibleNameForNode(idElement));
for (Node& n : NodeTraversal::descendantsOf(*idElement))
builder.append(accessibleNameForNode(&n));
if (i != size - 1)
builder.append(' ');
}
return builder.toString();
}
void AXNodeObject::alterSliderValue(bool increase)
{
if (roleValue() != SliderRole)
return;
float value = valueForRange();
float step = stepValueForRange();
value += increase ? step : -step;
setValue(String::number(value));
axObjectCache().postNotification(node(), AXObjectCacheImpl::AXValueChanged);
}
String AXNodeObject::ariaAccessibilityDescription() const
{
String ariaLabelledby = ariaLabelledbyAttribute();
if (!ariaLabelledby.isEmpty())
return ariaLabelledby;
const AtomicString& ariaLabel = getAttribute(aria_labelAttr);
if (!ariaLabel.isEmpty())
return ariaLabel;
return String();
}
void AXNodeObject::ariaLabelledbyElements(WillBeHeapVector<RawPtrWillBeMember<Element>>& elements) const
{
elementsFromAttribute(elements, aria_labelledbyAttr);
if (!elements.size())
elementsFromAttribute(elements, aria_labeledbyAttr);
}
bool AXNodeObject::computeAccessibilityIsIgnored(IgnoredReasons* ignoredReasons) const
{
#if ENABLE(ASSERT)
// Double-check that an AXObject is never accessed before
// it's been initialized.
ASSERT(m_initialized);
#endif
// If this element is within a parent that cannot have children, it should not be exposed.
if (isDescendantOfLeafNode()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXAncestorIsLeafNode, leafNodeAncestor()));
return true;
}
// Ignore labels that are already referenced by a control's title UI element.
AXObject* controlObject = correspondingControlForLabelElement();
if (controlObject && !controlObject->deprecatedExposesTitleUIElement() && controlObject->isCheckboxOrRadio()) {
if (ignoredReasons) {
HTMLLabelElement* label = labelElementContainer();
if (label && !label->isSameNode(node())) {
AXObject* labelAXObject = axObjectCache().getOrCreate(label);
ignoredReasons->append(IgnoredReason(AXLabelContainer, labelAXObject));
}
ignoredReasons->append(IgnoredReason(AXLabelFor, controlObject));
}
return true;
}
Element* element = node()->isElementNode() ? toElement(node()) : node()->parentElement();
if (!layoutObject()
&& (!element || !element->isInCanvasSubtree())
&& !equalIgnoringCase(getAttribute(aria_hiddenAttr), "false")) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXNotRendered));
return true;
}
if (m_role == UnknownRole) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXUninteresting));
return true;
}
return false;
}
static bool isListElement(Node* node)
{
return isHTMLUListElement(*node) || isHTMLOListElement(*node) || isHTMLDListElement(*node);
}
static bool isPresentationalInTable(AXObject* parent, HTMLElement* currentElement)
{
if (!currentElement)
return false;
Node* parentNode = parent->node();
if (!parentNode || !parentNode->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(*currentElement) && isHTMLTableRowElement(*parentNode))
return parent->hasInheritedPresentationalRole();
if (isHTMLTableRowElement(*currentElement) && isHTMLTableSectionElement(toHTMLElement(*parentNode))) {
// Because TableSections have ignored role, presentation should be checked with its parent node
AXObject* tableObject = parent->parentObject();
Node* tableNode = tableObject ? tableObject->node() : 0;
return isHTMLTableElement(tableNode) && tableObject->hasInheritedPresentationalRole();
}
return false;
}
static bool isRequiredOwnedElement(AXObject* parent, AccessibilityRole currentRole, HTMLElement* currentElement)
{
Node* parentNode = parent->node();
if (!parentNode || !parentNode->isHTMLElement())
return false;
if (currentRole == ListItemRole)
return isListElement(parentNode);
if (currentRole == ListMarkerRole)
return isHTMLLIElement(*parentNode);
if (currentRole == MenuItemCheckBoxRole || currentRole == MenuItemRole || currentRole == MenuItemRadioRole)
return isHTMLMenuElement(*parentNode);
if (!currentElement)
return false;
if (isHTMLTableCellElement(*currentElement))
return isHTMLTableRowElement(*parentNode);
if (isHTMLTableRowElement(*currentElement))
return isHTMLTableSectionElement(toHTMLElement(*parentNode));
// In case of ListboxRole and it's 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() != UnknownRole)
return 0;
AXObject* parent = parentObject();
if (!parent)
return 0;
HTMLElement* element = nullptr;
if (node() && node()->isHTMLElement())
element = toHTMLElement(node());
if (!parent->hasInheritedPresentationalRole()) {
if (!layoutObject() || !layoutObject()->isBoxModelObject())
return 0;
LayoutBoxModelObject* cssBox = toLayoutBoxModelObject(layoutObject());
if (!cssBox->isTableCell() && !cssBox->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;
}
bool AXNodeObject::isDescendantOfElementType(const HTMLQualifiedName& tagName) const
{
if (!node())
return false;
for (Element* parent = node()->parentElement(); parent; parent = parent->parentElement()) {
if (parent->hasTagName(tagName))
return true;
}
return false;
}
AccessibilityRole AXNodeObject::determineAccessibilityRoleUtil()
{
if (!node())
return UnknownRole;
// HTMLAnchorElement sets isLink only when it has hrefAttr.
// We assume that it is also LinkRole if it has event listners even though it doesn't have hrefAttr.
if (node()->isLink() || (isHTMLAnchorElement(*node()) && isClickable()))
return LinkRole;
if (isHTMLButtonElement(*node()))
return buttonRoleType();
if (isHTMLDetailsElement(*node()))
return DetailsRole;
if (isHTMLSummaryElement(*node())) {
ContainerNode* parent = ComposedTreeTraversal::parent(*node());
if (parent && isHTMLDetailsElement(parent))
return DisclosureTriangleRole;
return UnknownRole;
}
if (isHTMLInputElement(*node())) {
HTMLInputElement& input = toHTMLInputElement(*node());
const AtomicString& type = input.type();
if (input.dataList())
return ComboBoxRole;
if (type == InputTypeNames::button) {
if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())) || (parentObject() && parentObject()->roleValue() == MenuRole))
return MenuItemRole;
return buttonRoleType();
}
if (type == InputTypeNames::checkbox) {
if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())) || (parentObject() && parentObject()->roleValue() == MenuRole))
return MenuItemCheckBoxRole;
return CheckBoxRole;
}
if (type == InputTypeNames::date)
return DateRole;
if (type == InputTypeNames::datetime
|| type == InputTypeNames::datetime_local
|| type == InputTypeNames::month
|| type == InputTypeNames::week)
return DateTimeRole;
if (type == InputTypeNames::file)
return ButtonRole;
if (type == InputTypeNames::radio) {
if ((node()->parentNode() && isHTMLMenuElement(node()->parentNode())) || (parentObject() && parentObject()->roleValue() == MenuRole))
return MenuItemRadioRole;
return RadioButtonRole;
}
if (type == InputTypeNames::number)
return SpinButtonRole;
if (input.isTextButton())
return buttonRoleType();
if (type == InputTypeNames::range)
return SliderRole;
if (type == InputTypeNames::color)
return ColorWellRole;
if (type == InputTypeNames::time)
return InputTimeRole;
return TextFieldRole;
}
if (isHTMLSelectElement(*node())) {
HTMLSelectElement& selectElement = toHTMLSelectElement(*node());
return selectElement.multiple() ? ListBoxRole : PopUpButtonRole;
}
if (isHTMLTextAreaElement(*node()))
return TextFieldRole;
if (headingLevel())
return HeadingRole;
if (isHTMLDivElement(*node()))
return DivRole;
if (isHTMLMeterElement(*node()))
return MeterRole;
if (isHTMLOutputElement(*node()))
return StatusRole;
if (isHTMLParagraphElement(*node()))
return ParagraphRole;
if (isHTMLLabelElement(*node()))
return LabelRole;
if (isHTMLLegendElement(*node()))
return LegendRole;
if (isHTMLRubyElement(*node()))
return RubyRole;
if (isHTMLDListElement(*node()))
return DescriptionListRole;
if (node()->hasTagName(ddTag))
return DescriptionListDetailRole;
if (node()->hasTagName(dtTag))
return DescriptionListTermRole;
if (node()->nodeName() == "math")
return MathRole;
if (node()->hasTagName(rpTag) || node()->hasTagName(rtTag))
return AnnotationRole;
if (isHTMLFormElement(*node()))
return FormRole;
if (node()->hasTagName(articleTag))
return ArticleRole;
if (node()->hasTagName(mainTag))
return MainRole;
if (node()->hasTagName(markTag))
return MarkRole;
if (node()->hasTagName(navTag))
return NavigationRole;
if (node()->hasTagName(asideTag))
return ComplementaryRole;
if (node()->hasTagName(preTag))
return PreRole;
if (node()->hasTagName(sectionTag))
return RegionRole;
if (node()->hasTagName(addressTag))
return ContentInfoRole;
if (isHTMLDialogElement(*node()))
return DialogRole;
// The HTML element should not be exposed as an element. That's what the LayoutView element does.
if (isHTMLHtmlElement(*node()))
return IgnoredRole;
if (isHTMLIFrameElement(*node())) {
const AtomicString& ariaRole = getAttribute(roleAttr);
if (ariaRole == "none" || ariaRole == "presentation")
return IframePresentationalRole;
return IframeRole;
}
// 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
if (node()->hasTagName(headerTag) && !isDescendantOfElementType(articleTag) && !isDescendantOfElementType(sectionTag))
return BannerRole;
if (node()->hasTagName(footerTag) && !isDescendantOfElementType(articleTag) && !isDescendantOfElementType(sectionTag))
return FooterRole;
if (node()->hasTagName(blockquoteTag))
return BlockquoteRole;
if (node()->hasTagName(captionTag))
return CaptionRole;
if (node()->hasTagName(figcaptionTag))
return FigcaptionRole;
if (node()->hasTagName(figureTag))
return FigureRole;
if (node()->nodeName() == "TIME")
return TimeRole;
if (isEmbeddedObject())
return EmbeddedObjectRole;
if (isHTMLHRElement(*node()))
return SplitterRole;
return UnknownRole;
}
AccessibilityRole AXNodeObject::determineAccessibilityRole()
{
if (!node())
return UnknownRole;
if ((m_ariaRole = determineAriaRoleAttribute()) != UnknownRole)
return m_ariaRole;
if (node()->isTextNode())
return StaticTextRole;
AccessibilityRole role = determineAccessibilityRoleUtil();
if (role != UnknownRole)
return role;
if (node()->isElementNode()) {
Element* element = toElement(node());
if (element->isInCanvasSubtree() && element->isFocusable())
return GroupRole;
}
return UnknownRole;
}
AccessibilityRole AXNodeObject::determineAriaRoleAttribute() const
{
const AtomicString& ariaRole = getAttribute(roleAttr);
if (ariaRole.isNull() || ariaRole.isEmpty())
return UnknownRole;
AccessibilityRole role = ariaRoleToWebCoreRole(ariaRole);
// ARIA states if an item can get focus, it should not be presentational.
if ((role == NoneRole || role == PresentationalRole) && canSetFocusAttribute())
return UnknownRole;
if (role == ButtonRole)
role = buttonRoleType();
role = remapAriaRoleDueToParent(role);
if (role)
return role;
return UnknownRole;
}
void AXNodeObject::tokenVectorFromAttribute(Vector<String>& tokens, const QualifiedName& attribute) const
{
Node* node = this->node();
if (!node || !node->isElementNode())
return;
String attributeValue = getAttribute(attribute).string();
if (attributeValue.isEmpty())
return;
attributeValue.simplifyWhiteSpace();
attributeValue.split(' ', tokens);
}
void AXNodeObject::elementsFromAttribute(WillBeHeapVector<RawPtrWillBeMember<Element>>& elements, const QualifiedName& attribute) const
{
Vector<String> ids;
tokenVectorFromAttribute(ids, attribute);
if (ids.isEmpty())
return;
TreeScope& scope = node()->treeScope();
for (const auto& id : ids) {
if (Element* idElement = scope.getElementById(AtomicString(id)))
elements.append(idElement);
}
}
void AXNodeObject::accessibilityChildrenFromAttribute(QualifiedName attr, AccessibilityChildrenVector& children) const
{
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
elementsFromAttribute(elements, attr);
AXObjectCacheImpl& cache = axObjectCache();
for (const auto& element : elements) {
if (AXObject* child = cache.getOrCreate(element))
children.append(child);
}
}
// This only returns true if this is the element that actually has the
// contentEditable attribute set, unlike node->hasEditableStyle() which will
// also return true if an ancestor is editable.
bool AXNodeObject::hasContentEditableAttributeSet() const
{
const AtomicString& contentEditableValue = getAttribute(contenteditableAttr);
if (contentEditableValue.isNull())
return false;
// Both "true" (case-insensitive) and the empty string count as true.
return contentEditableValue.isEmpty() || equalIgnoringCase(contentEditableValue, "true");
}
bool AXNodeObject::isTextControl() const
{
if (hasContentEditableAttributeSet())
return true;
switch (roleValue()) {
case TextFieldRole:
case ComboBoxRole:
case SearchBoxRole:
case SpinButtonRole:
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 (m_ariaRole != UnknownRole)
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() == WebAreaRole)
return false;
if (isHTMLBodyElement(node()))
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() == SVGRootRole)
return false;
return true;
}
HTMLLabelElement* AXNodeObject::labelForElement(const Element* element) const
{
if (!element->isHTMLElement() || !toHTMLElement(element)->isLabelable())
return 0;
const AtomicString& id = element->getIdAttribute();
if (!id.isEmpty()) {
if (HTMLLabelElement* label = element->treeScope().labelElementForId(id))
return label;
}
return Traversal<HTMLLabelElement>::firstAncestor(*element);
}
AXObject* AXNodeObject::menuButtonForMenu() const
{
Element* menuItem = menuItemElementForMenu();
if (menuItem) {
// ARIA just has generic menu items. AppKit needs to know if this is a top level items like MenuBarButton or MenuBarItem
AXObject* menuItemAX = axObjectCache().getOrCreate(menuItem);
if (menuItemAX && menuItemAX->isMenuButton())
return menuItemAX;
}
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& siblingAriaRole = sibling->getAttribute(roleAttr);
if (equalIgnoringCase(siblingAriaRole, role))
return sibling;
}
return 0;
}
Element* AXNodeObject::menuItemElementForMenu() const
{
if (ariaRoleAttribute() != MenuRole)
return 0;
return siblingWithAriaRole("menuitem", node());
}
Element* AXNodeObject::mouseButtonListener() const
{
Node* node = this->node();
if (!node)
return 0;
// check if our parent is a mouse button listener
if (!node->isElementNode())
node = node->parentElement();
if (!node)
return 0;
// FIXME: Do the continuation search like anchorElement does
for (Element* element = toElement(node); element; element = element->parentElement()) {
if (element->getAttributeEventListener(EventTypeNames::click) || element->getAttributeEventListener(EventTypeNames::mousedown) || element->getAttributeEventListener(EventTypeNames::mouseup))
return element;
}
return 0;
}
AccessibilityRole AXNodeObject::remapAriaRoleDueToParent(AccessibilityRole role) const
{
// Some objects change their role based on their parent.
// However, asking for the unignoredParent calls accessibilityIsIgnored(), which can trigger a loop.
// While inside the call stack of creating an element, we need to avoid accessibilityIsIgnored().
// https://bugs.webkit.org/show_bug.cgi?id=65174
if (role != ListBoxOptionRole && role != MenuItemRole)
return role;
for (AXObject* parent = parentObject(); parent && !parent->accessibilityIsIgnored(); parent = parent->parentObject()) {
AccessibilityRole parentAriaRole = parent->ariaRoleAttribute();
// Selects and listboxes both have options as child roles, but they map to different roles within WebCore.
if (role == ListBoxOptionRole && parentAriaRole == MenuRole)
return MenuItemRole;
// An aria "menuitem" may map to MenuButton or MenuItem depending on its parent.
if (role == MenuItemRole && parentAriaRole == GroupRole)
return MenuButtonRole;
// If the parent had a different role, then we don't need to continue searching up the chain.
if (parentAriaRole)
break;
}
return role;
}
void AXNodeObject::init()
{
#if ENABLE(ASSERT)
ASSERT(!m_initialized);
m_initialized = true;
#endif
m_role = determineAccessibilityRole();
}
void AXNodeObject::detach()
{
AXObject::detach();
m_node = nullptr;
}
bool AXNodeObject::isAnchor() const
{
return !isNativeImage() && isLink();
}
bool AXNodeObject::isControl() const
{
Node* node = this->node();
if (!node)
return false;
return ((node->isElementNode() && toElement(node)->isFormControlElement())
|| AXObject::isARIAControl(ariaRoleAttribute()));
}
bool AXNodeObject::isControllingVideoElement() const
{
Node* node = this->node();
if (!node)
return true;
return isHTMLVideoElement(toParentMediaElement(node));
}
bool AXNodeObject::isEmbeddedObject() const
{
return isHTMLPlugInElement(node());
}
bool AXNodeObject::isFieldset() const
{
return isHTMLFieldSetElement(node());
}
bool AXNodeObject::isHeading() const
{
return roleValue() == HeadingRole;
}
bool AXNodeObject::isHovered() const
{
Node* node = this->node();
if (!node)
return false;
return node->hovered();
}
bool AXNodeObject::isImage() const
{
return roleValue() == ImageRole;
}
bool AXNodeObject::isImageButton() const
{
return isNativeImage() && isButton();
}
bool AXNodeObject::isInputImage() const
{
Node* node = this->node();
if (roleValue() == ButtonRole && isHTMLInputElement(node))
return toHTMLInputElement(*node).type() == InputTypeNames::image;
return false;
}
bool AXNodeObject::isLink() const
{
return roleValue() == LinkRole;
}
bool AXNodeObject::isMenu() const
{
return roleValue() == MenuRole;
}
bool AXNodeObject::isMenuButton() const
{
return roleValue() == MenuButtonRole;
}
bool AXNodeObject::isMeter() const
{
return roleValue() == MeterRole;
}
bool AXNodeObject::isMultiSelectable() const
{
const AtomicString& ariaMultiSelectable = getAttribute(aria_multiselectableAttr);
if (equalIgnoringCase(ariaMultiSelectable, "true"))
return true;
if (equalIgnoringCase(ariaMultiSelectable, "false"))
return false;
return isHTMLSelectElement(node()) && toHTMLSelectElement(*node()).multiple();
}
bool AXNodeObject::isNativeCheckboxOrRadio() const
{
Node* node = this->node();
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->node();
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->node();
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->node();
if (!isHTMLInputElement(node))
return false;
AccessibilityRole ariaRole = ariaRoleAttribute();
if (ariaRole != TextFieldRole && ariaRole != UnknownRole)
return false;
return toHTMLInputElement(node)->type() == InputTypeNames::password;
}
bool AXNodeObject::isProgressIndicator() const
{
return roleValue() == ProgressIndicatorRole;
}
bool AXNodeObject::isSlider() const
{
return roleValue() == SliderRole;
}
bool AXNodeObject::isNativeSlider() const
{
Node* node = this->node();
if (!node)
return false;
if (!isHTMLInputElement(node))
return false;
return toHTMLInputElement(node)->type() == InputTypeNames::range;
}
bool AXNodeObject::isChecked() const
{
Node* node = this->node();
if (!node)
return false;
// First test for native checkedness semantics
if (isHTMLInputElement(*node))
return toHTMLInputElement(*node).shouldAppearChecked();
// Else, if this is an ARIA role checkbox or radio or menuitemcheckbox
// or menuitemradio or switch, respect the aria-checked attribute
switch (ariaRoleAttribute()) {
case CheckBoxRole:
case MenuItemCheckBoxRole:
case MenuItemRadioRole:
case RadioButtonRole:
case SwitchRole:
if (equalIgnoringCase(getAttribute(aria_checkedAttr), "true"))
return true;
return false;
default:
break;
}
// Otherwise it's not checked
return false;
}
bool AXNodeObject::isClickable() const
{
if (node()) {
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::isEnabled() const
{
if (isDescendantOfDisabledNode())
return false;
Node* node = this->node();
if (!node || !node->isElementNode())
return true;
return !toElement(node)->isDisabledFormControl();
}
AccessibilityExpanded AXNodeObject::isExpanded() const
{
if (node() && isHTMLSummaryElement(*node())) {
if (node()->parentNode() && isHTMLDetailsElement(node()->parentNode()))
return toElement(node()->parentNode())->hasAttribute(openAttr) ? ExpandedExpanded : ExpandedCollapsed;
}
const AtomicString& expanded = getAttribute(aria_expandedAttr);
if (equalIgnoringCase(expanded, "true"))
return ExpandedExpanded;
if (equalIgnoringCase(expanded, "false"))
return ExpandedCollapsed;
return ExpandedUndefined;
}
bool AXNodeObject::isIndeterminate() const
{
Node* node = this->node();
if (!isHTMLInputElement(node))
return false;
return toHTMLInputElement(node)->shouldAppearIndeterminate();
}
bool AXNodeObject::isPressed() const
{
if (!isButton())
return false;
Node* node = this->node();
if (!node)
return false;
// ARIA button with aria-pressed not undefined, then check for aria-pressed attribute rather than node()->active()
if (ariaRoleAttribute() == ToggleButtonRole) {
if (equalIgnoringCase(getAttribute(aria_pressedAttr), "true")
|| equalIgnoringCase(getAttribute(aria_pressedAttr), "mixed"))
return true;
return false;
}
return node->active();
}
bool AXNodeObject::isReadOnly() const
{
Node* node = this->node();
if (!node)
return true;
if (isHTMLTextAreaElement(*node))
return toHTMLTextAreaElement(*node).isReadOnly();
if (isHTMLInputElement(*node)) {
HTMLInputElement& input = toHTMLInputElement(*node);
if (input.isTextField())
return input.isReadOnly();
}
return !node->hasEditableStyle();
}
bool AXNodeObject::isRequired() const
{
Node* n = this->node();
if (n && (n->isElementNode() && toElement(n)->isFormControlElement()) && hasAttribute(requiredAttr))
return toHTMLFormControlElement(n)->isRequired();
if (equalIgnoringCase(getAttribute(aria_requiredAttr), "true"))
return true;
return false;
}
bool AXNodeObject::canSetFocusAttribute() const
{
Node* node = this->node();
if (!node)
return false;
if (isWebArea())
return true;
// NOTE: It would be more accurate to ask the document whether setFocusedNode() would
// do anything. For example, setFocusedNode() will do nothing if the current focused
// node will not relinquish the focus.
if (!node)
return false;
if (isDisabledFormControl(node))
return false;
return node->isElementNode() && toElement(node)->supportsFocus();
}
bool AXNodeObject::canSetValueAttribute() const
{
if (equalIgnoringCase(getAttribute(aria_readonlyAttr), "true"))
return false;
if (isProgressIndicator() || isSlider())
return true;
if (isTextControl() && !isNativeTextControl())
return true;
// Any node could be contenteditable, so isReadOnly should be relied upon
// for this information for all elements.
return !isReadOnly();
}
bool AXNodeObject::canvasHasFallbackContent() const
{
Node* node = this->node();
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);
}
bool AXNodeObject::deprecatedExposesTitleUIElement() const
{
if (!isControl())
return false;
// If this control is ignored (because it's invisible),
// then the label needs to be exposed so it can be visible to accessibility.
if (accessibilityIsIgnored())
return true;
// ARIA: section 2A, bullet #3 says if aria-labelledby or aria-label appears, it should
// override the "label" element association.
bool hasTextAlternative = (!ariaLabelledbyAttribute().isEmpty() || !getAttribute(aria_labelAttr).isEmpty());
// Checkboxes and radio buttons use the text of their title ui element as their own AXTitle.
// This code controls whether the title ui element should appear in the AX tree (usually, no).
// It should appear if the control already has a label (which will be used as the AXTitle instead).
if (isCheckboxOrRadio())
return hasTextAlternative;
// When controls have their own descriptions, the title element should be ignored.
if (hasTextAlternative)
return false;
return true;
}
int AXNodeObject::headingLevel() const
{
// headings can be in block flow and non-block flow
Node* node = this->node();
if (!node)
return 0;
if (roleValue() == HeadingRole && hasAttribute(aria_levelAttr)) {
int level = getAttribute(aria_levelAttr).toInt();
if (level >= 1 && level <= 9)
return level;
}
if (!node->isHTMLElement())
return 0;
HTMLElement& element = toHTMLElement(*node);
if (element.hasTagName(h1Tag))
return 1;
if (element.hasTagName(h2Tag))
return 2;
if (element.hasTagName(h3Tag))
return 3;
if (element.hasTagName(h4Tag))
return 4;
if (element.hasTagName(h5Tag))
return 5;
if (element.hasTagName(h6Tag))
return 6;
return 0;
}
unsigned AXNodeObject::hierarchicalLevel() const
{
Node* node = this->node();
if (!node || !node->isElementNode())
return 0;
Element* element = toElement(node);
String ariaLevel = element->getAttribute(aria_levelAttr);
if (!ariaLevel.isEmpty())
return ariaLevel.toInt();
// Only tree item will calculate its level through the DOM currently.
if (roleValue() != TreeItemRole)
return 0;
// Hierarchy leveling starts at 1, to match the aria-level spec.
// We measure tree hierarchy by the number of groups that the item is within.
unsigned level = 1;
for (AXObject* parent = parentObject(); parent; parent = parent->parentObject()) {
AccessibilityRole parentRole = parent->roleValue();
if (parentRole == GroupRole)
level++;
else if (parentRole == TreeRole)
break;
}
return level;
}
String AXNodeObject::ariaAutoComplete() const
{
if (roleValue() != ComboBoxRole)
return String();
const AtomicString& ariaAutoComplete = getAttribute(aria_autocompleteAttr).lower();
if (ariaAutoComplete == "inline" || ariaAutoComplete == "list"
|| ariaAutoComplete == "both")
return ariaAutoComplete;
return String();
}
String AXNodeObject::deprecatedPlaceholder() const
{
String placeholder;
if (node()) {
if (isHTMLInputElement(*node())) {
HTMLInputElement* inputElement = toHTMLInputElement(node());
placeholder = inputElement->strippedPlaceholder();
} else if (isHTMLTextAreaElement(*node())) {
HTMLTextAreaElement* textAreaElement = toHTMLTextAreaElement(node());
placeholder = textAreaElement->strippedPlaceholder();
}
}
return placeholder;
}
AccessibilityOrientation AXNodeObject::orientation() const
{
const AtomicString& ariaOrientation = getAttribute(aria_orientationAttr);
AccessibilityOrientation orientation = AccessibilityOrientationUndefined;
if (equalIgnoringCase(ariaOrientation, "horizontal"))
orientation = AccessibilityOrientationHorizontal;
else if (equalIgnoringCase(ariaOrientation, "vertical"))
orientation = AccessibilityOrientationVertical;
switch (roleValue()) {
case ComboBoxRole:
case ListBoxRole:
case MenuRole:
case ScrollBarRole:
case TreeRole:
if (orientation == AccessibilityOrientationUndefined)
orientation = AccessibilityOrientationVertical;
return orientation;
case MenuBarRole:
case SliderRole:
case SplitterRole:
case TabListRole:
case ToolbarRole:
if (orientation == AccessibilityOrientationUndefined)
orientation = AccessibilityOrientationHorizontal;
return orientation;
case RadioGroupRole:
case TreeGridRole:
// TODO(nektar): Fix bug 532670 and remove table role.
case TableRole:
return orientation;
default:
return AXObject::orientation();
}
}
String AXNodeObject::text() const
{
// If this is a user defined static text, use the accessible name computation.
if (ariaRoleAttribute() == StaticTextRole)
return ariaAccessibilityDescription();
if (!isTextControl())
return String();
Node* node = this->node();
if (!node)
return String();
if (isNativeTextControl() && (isHTMLTextAreaElement(*node) || isHTMLInputElement(*node)))
return toHTMLTextFormControlElement(*node).value();
if (!node->isElementNode())
return String();
return toElement(node)->innerText();
}
AXObject* AXNodeObject::deprecatedTitleUIElement() const
{
if (!node() || !node()->isElementNode())
return 0;
if (isFieldset())
return axObjectCache().getOrCreate(toHTMLFieldSetElement(node())->legend());
HTMLLabelElement* label = labelForElement(toElement(node()));
if (label)
return axObjectCache().getOrCreate(label);
return 0;
}
AccessibilityButtonState AXNodeObject::checkboxOrRadioValue() const
{
if (isNativeCheckboxOrRadio())
return isChecked() ? ButtonStateOn : ButtonStateOff;
return AXObject::checkboxOrRadioValue();
}
RGBA32 AXNodeObject::colorValue() const
{
if (!isHTMLInputElement(node()) || !isColorWell())
return AXObject::colorValue();
HTMLInputElement* input = toHTMLInputElement(node());
const AtomicString& type = input->getAttribute(typeAttr);
if (!equalIgnoringCase(type, "color"))
return AXObject::colorValue();
// HTMLInputElement::value always returns a string parseable by Color.
Color color;
bool success = color.setFromString(input->value());
ASSERT_UNUSED(success, success);
return color.rgb();
}
InvalidState AXNodeObject::invalidState() const
{
if (hasAttribute(aria_invalidAttr)) {
const AtomicString& attributeValue = getAttribute(aria_invalidAttr);
if (equalIgnoringCase(attributeValue, "false"))
return InvalidStateFalse;
if (equalIgnoringCase(attributeValue, "true"))
return InvalidStateTrue;
if (equalIgnoringCase(attributeValue, "spelling"))
return InvalidStateSpelling;
if (equalIgnoringCase(attributeValue, "grammar"))
return InvalidStateGrammar;
// A yet unknown value.
if (!attributeValue.isEmpty())
return InvalidStateOther;
}
if (node() && node()->isElementNode()
&& toElement(node())->isFormControlElement()) {
HTMLFormControlElement* element = toHTMLFormControlElement(node());
WillBeHeapVector<RefPtrWillBeMember<HTMLFormControlElement>>
invalidControls;
bool isInvalid = !element->checkValidity(
&invalidControls, CheckValidityDispatchNoEvent);
return isInvalid ? InvalidStateTrue : InvalidStateFalse;
}
return InvalidStateUndefined;
}
int AXNodeObject::posInSet() const
{
if (supportsSetSizeAndPosInSet()) {
if (hasAttribute(aria_posinsetAttr))
return getAttribute(aria_posinsetAttr).toInt();
return AXObject::indexInParent() + 1;
}
return 0;
}
int AXNodeObject::setSize() const
{
if (supportsSetSizeAndPosInSet()) {
if (hasAttribute(aria_setsizeAttr))
return getAttribute(aria_setsizeAttr).toInt();
if (parentObject()) {
const auto& siblings = parentObject()->children();
return siblings.size();
}
}
return 0;
}
String AXNodeObject::ariaInvalidValue() const
{
if (invalidState() == InvalidStateOther)
return getAttribute(aria_invalidAttr);
return String();
}
String AXNodeObject::valueDescription() const
{
if (!supportsRangeValue())
return String();
return getAttribute(aria_valuetextAttr).string();
}
float AXNodeObject::valueForRange() const
{
if (hasAttribute(aria_valuenowAttr))
return getAttribute(aria_valuenowAttr).toFloat();
if (isNativeSlider())
return toHTMLInputElement(*node()).valueAsNumber();
if (isHTMLMeterElement(node()))
return toHTMLMeterElement(*node()).value();
return 0.0;
}
float AXNodeObject::maxValueForRange() const
{
if (hasAttribute(aria_valuemaxAttr))
return getAttribute(aria_valuemaxAttr).toFloat();
if (isNativeSlider())
return toHTMLInputElement(*node()).maximum();
if (isHTMLMeterElement(node()))
return toHTMLMeterElement(*node()).max();
return 0.0;
}
float AXNodeObject::minValueForRange() const
{
if (hasAttribute(aria_valueminAttr))
return getAttribute(aria_valueminAttr).toFloat();
if (isNativeSlider())
return toHTMLInputElement(*node()).minimum();
if (isHTMLMeterElement(node()))
return toHTMLMeterElement(*node()).min();
return 0.0;
}
float AXNodeObject::stepValueForRange() const
{
if (!isNativeSlider())
return 0.0;
Decimal step = toHTMLInputElement(*node()).createStepRange(RejectAny).step();
return step.toString().toFloat();
}
String AXNodeObject::stringValue() const
{
Node* node = this->node();
if (!node)
return String();
if (ariaRoleAttribute() == StaticTextRole) {
String staticText = text();
if (!staticText.length())
staticText = deprecatedTextUnderElement(TextUnderElementAll);
return staticText;
}
if (node->isTextNode())
return deprecatedTextUnderElement(TextUnderElementAll);
if (isHTMLSelectElement(*node)) {
HTMLSelectElement& selectElement = toHTMLSelectElement(*node);
int selectedIndex = selectElement.selectedIndex();
const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& listItems = selectElement.listItems();
if (selectedIndex >= 0 && static_cast<size_t>(selectedIndex) < listItems.size()) {
const AtomicString& overriddenDescription = listItems[selectedIndex]->fastGetAttribute(aria_labelAttr);
if (!overriddenDescription.isNull())
return overriddenDescription;
}
if (!selectElement.multiple())
return selectElement.value();
return String();
}
if (isNativeTextControl())
return text();
// FIXME: We might need to implement a value here for more types
// FIXME: It would be better not to advertise a value at all for the types for which we don't implement one;
// this would require subclassing or making accessibilityAttributeNames do something other than return a
// single static array.
return String();
}
String AXNodeObject::ariaDescribedByAttribute() const
{
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
elementsFromAttribute(elements, aria_describedbyAttr);
return accessibilityDescriptionForElements(elements);
}
String AXNodeObject::ariaLabelledbyAttribute() const
{
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
ariaLabelledbyElements(elements);
return accessibilityDescriptionForElements(elements);
}
AccessibilityRole AXNodeObject::ariaRoleAttribute() const
{
return m_ariaRole;
}
// When building the textUnderElement for an object, determine whether or not
// we should include the inner text of this given descendant object or skip it.
static bool shouldUseAccessibilityObjectInnerText(AXObject* obj)
{
// Consider this hypothetical example:
// <div tabindex=0>
// <h2>
// Table of contents
// </h2>
// <a href="#start">Jump to start of book</a>
// <ul>
// <li><a href="#1">Chapter 1</a></li>
// <li><a href="#1">Chapter 2</a></li>
// </ul>
// </div>
//
// The goal is to return a reasonable title for the outer container div, because
// it's focusable - but without making its title be the full inner text, which is
// quite long. As a heuristic, skip links, controls, and elements that are usually
// containers with lots of children.
// Skip hidden children
if (obj->isInertOrAriaHidden())
return false;
// If something doesn't expose any children, then we can always take the inner text content.
// This is what we want when someone puts an <a> inside a <button> for example.
if (obj->isDescendantOfLeafNode())
return true;
// Skip focusable children, so we don't include the text of links and controls.
if (obj->canSetFocusAttribute())
return false;
// Skip big container elements like lists, tables, etc.
if (obj->isList() || obj->isAXTable() || obj->isTree() || obj->isCanvas())
return false;
return true;
}
// 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* blockFlow = toLayoutBlockFlow(current);
if (!blockFlow->inlineBoxWrapper())
return blockFlow;
}
current = current->parent();
}
ASSERT_NOT_REACHED();
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;
}
String AXNodeObject::deprecatedTextUnderElement(TextUnderElementMode mode) const
{
Node* node = this->node();
if (node && node->isTextNode())
return toText(node)->wholeText();
StringBuilder builder;
AXObject* previous = nullptr;
for (AXObject* child = firstChild(); child; child = child->nextSibling()) {
if (!shouldUseAccessibilityObjectInnerText(child))
continue;
if (child->isAXNodeObject()) {
HeapVector<Member<AccessibilityText>> textOrder;
toAXNodeObject(child)->deprecatedAlternativeText(textOrder);
if (textOrder.size() > 0) {
builder.append(textOrder[0]->text());
if (mode == TextUnderElementAny)
break;
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 && builder.length() && !isHTMLSpace(builder[builder.length() - 1])) {
if (!isInSameNonInlineBlockFlow(child->layoutObject(), previous->layoutObject()))
builder.append(' ');
}
builder.append(child->deprecatedTextUnderElement(mode));
previous = child;
if (mode == TextUnderElementAny && !builder.isEmpty())
break;
}
return builder.toString();
}
AXObject* AXNodeObject::findChildWithTagName(const HTMLQualifiedName& tagName) const
{
for (AXObject* child = firstChild(); child; child = child->nextSibling()) {
Node* childNode = child->node();
if (childNode && childNode->hasTagName(tagName))
return child;
}
return 0;
}
String AXNodeObject::deprecatedAccessibilityDescription() const
{
// Static text should not have a description, it should only have a stringValue.
if (roleValue() == StaticTextRole)
return String();
String ariaDescription = ariaAccessibilityDescription();
if (!ariaDescription.isEmpty())
return ariaDescription;
if (isImage() || isInputImage() || isNativeImage() || isCanvas()) {
// Images should use alt as long as the attribute is present, even if empty.
// Otherwise, it should fallback to other methods, like the title attribute.
const AtomicString& alt = getAttribute(altAttr);
if (!alt.isNull())
return alt;
}
// An element's descriptive text is comprised of deprecatedTitle() (what's visible on the screen) and deprecatedAccessibilityDescription() (other descriptive text).
// Both are used to generate what a screen reader speaks.
// If this point is reached (i.e. there's no accessibilityDescription) and there's no deprecatedTitle(), we should fallback to using the title attribute.
// The title attribute is normally used as help text (because it is a tooltip), but if there is nothing else available, this should be used (according to ARIA).
if (deprecatedTitle(TextUnderElementAny).isEmpty())
return getAttribute(titleAttr);
if (roleValue() == FigureRole) {
AXObject* figcaption = findChildWithTagName(figcaptionTag);
if (figcaption)
return figcaption->deprecatedAccessibilityDescription();
}
return String();
}
String AXNodeObject::deprecatedTitle(TextUnderElementMode mode) const
{
Node* node = this->node();
if (!node)
return String();
bool isInputElement = isHTMLInputElement(*node);
if (isInputElement) {
HTMLInputElement& input = toHTMLInputElement(*node);
if (input.isTextButton())
return input.valueWithDefault();
}
if (isInputElement || AXObject::isARIAInput(ariaRoleAttribute()) || isControl()) {
HTMLLabelElement* label = labelForElement(toElement(node));
if (label && !deprecatedExposesTitleUIElement())
return label->innerText();
}
// If this node isn't laid out, there's no inner text we can extract from a select element.
if (!isAXLayoutObject() && isHTMLSelectElement(*node))
return String();
switch (roleValue()) {
case PopUpButtonRole:
// Native popup buttons should not use their button children's text as a title. That value is retrieved through stringValue().
if (isHTMLSelectElement(*node))
return String();
case ButtonRole:
case ToggleButtonRole:
case CheckBoxRole:
case LineBreakRole:
case ListBoxOptionRole:
case ListItemRole:
case MenuButtonRole:
case MenuItemRole:
case MenuItemCheckBoxRole:
case MenuItemRadioRole:
case RadioButtonRole:
case SwitchRole:
case TabRole:
return deprecatedTextUnderElement(mode);
// SVGRoots should not use the text under itself as a title. That could include the text of objects like <text>.
case SVGRootRole:
return String();
case FigureRole: {
AXObject* figcaption = findChildWithTagName(figcaptionTag);
if (figcaption)
return figcaption->deprecatedTextUnderElement();
}
default:
break;
}
if (isHeading() || isLink())
return deprecatedTextUnderElement(mode);
// If it's focusable but it's not content editable or a known control type, then it will appear to
// the user as a single atomic object, so we should use its text as the default title.
if (isGenericFocusableElement())
return deprecatedTextUnderElement(mode);
return String();
}
String AXNodeObject::deprecatedHelpText() const
{
Node* node = this->node();
if (!node)
return String();
const AtomicString& ariaHelp = getAttribute(aria_helpAttr);
if (!ariaHelp.isEmpty())
return ariaHelp;
String describedBy = ariaDescribedByAttribute();
if (!describedBy.isEmpty())
return describedBy;
String description = deprecatedAccessibilityDescription();
for (Node* curr = node; curr; curr = curr->parentNode()) {
if (curr->isHTMLElement()) {
const AtomicString& summary = toElement(curr)->getAttribute(summaryAttr);
if (!summary.isEmpty())
return summary;
// The title attribute should be used as help text unless it is already being used as descriptive text.
const AtomicString& title = toElement(curr)->getAttribute(titleAttr);
if (!title.isEmpty() && description != title)
return title;
}
// Only take help text from an ancestor element if its a group or an unknown role. If help was
// added to those kinds of elements, it is likely it was meant for a child element.
AXObject* axObj = axObjectCache().getOrCreate(curr);
if (axObj) {
AccessibilityRole role = axObj->roleValue();
if (role != GroupRole && role != UnknownRole)
break;
}
}
return String();
}
String AXNodeObject::computedName() const
{
String title = this->deprecatedTitle(TextUnderElementAll);
String titleUIText;
if (title.isEmpty()) {
AXObject* titleUIElement = this->deprecatedTitleUIElement();
if (titleUIElement) {
titleUIText = titleUIElement->deprecatedTextUnderElement();
if (!titleUIText.isEmpty())
return titleUIText;
}
}
String description = deprecatedAccessibilityDescription();
if (!description.isEmpty())
return description;
if (!title.isEmpty())
return title;
String placeholder;
if (isHTMLInputElement(node())) {
HTMLInputElement* element = toHTMLInputElement(node());
placeholder = element->strippedPlaceholder();
if (!placeholder.isEmpty())
return placeholder;
}
return String();
}
//
// New AX name calculation.
//
String AXNodeObject::textAlternative(bool recursive, bool inAriaLabelledByTraversal, AXObjectSet& visited, AXNameFrom& nameFrom, AXObjectVector* nameObjects, NameSources* nameSources) const
{
// If nameSources is non-null, nameObjects is used in filling it in, so it must be non-null as well.
if (nameSources)
ASSERT(nameObjects);
bool alreadyVisited = visited.contains(this);
bool foundTextAlternative = false;
visited.add(this);
String textAlternative;
if (!node() && !layoutObject())
return String();
// Step 2A from: http://www.w3.org/TR/accname-aam-1.1
if (!recursive && layoutObject()
&& layoutObject()->style()->visibility() != VISIBLE
&& !equalIgnoringCase(getAttribute(aria_hiddenAttr), "false")) {
return String();
}
// Step 2B from: http://www.w3.org/TR/accname-aam-1.1
if (!inAriaLabelledByTraversal && !alreadyVisited) {
const QualifiedName& attr = hasAttribute(aria_labeledbyAttr) && !hasAttribute(aria_labelledbyAttr) ? aria_labeledbyAttr : aria_labelledbyAttr;
nameFrom = AXNameFromRelatedElement;
if (nameSources) {
nameSources->append(NameSource(foundTextAlternative, attr));
nameSources->last().type = nameFrom;
}
const AtomicString& ariaLabelledby = getAttribute(attr);
if (!ariaLabelledby.isNull()) {
if (nameSources)
nameSources->last().attributeValue = ariaLabelledby;
textAlternative = textFromAriaLabelledby(visited, nameObjects);
if (!textAlternative.isNull()) {
if (nameSources) {
NameSource& source = nameSources->last();
source.type = nameFrom;
source.nameObjects = *nameObjects;
source.text = textAlternative;
foundTextAlternative = true;
} else {
return textAlternative;
}
} else if (nameSources) {
nameSources->last().invalid = true;
}
}
}
// Step 2C from: http://www.w3.org/TR/accname-aam-1.1
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(foundTextAlternative, aria_labelAttr));
nameSources->last().type = nameFrom;
}
const AtomicString& ariaLabel = getAttribute(aria_labelAttr);
if (!ariaLabel.isEmpty()) {
textAlternative = ariaLabel;
if (nameSources) {
NameSource& source = nameSources->last();
source.text = textAlternative;
source.attributeValue = ariaLabel;
foundTextAlternative = true;
} else {
return textAlternative;
}
}
// Step 2D from: http://www.w3.org/TR/accname-aam-1.1
textAlternative = nativeTextAlternative(visited, nameFrom, nameObjects, nameSources, &foundTextAlternative);
if (!textAlternative.isNull() && !nameSources)
return textAlternative;
// Step 2E from: http://www.w3.org/TR/accname-aam-1.1
if (recursive && !inAriaLabelledByTraversal && isControl()) {
// No need to set any name source info in a recursive call.
if (roleValue() == TextFieldRole || roleValue() == ComboBoxRole)
return text();
if (isRange()) {
const AtomicString& ariaValuetext = getAttribute(aria_valuetextAttr);
if (!ariaValuetext.isNull())
return ariaValuetext.string();
return String::number(valueForRange());
}
}
// Step 2F / 2G from: http://www.w3.org/TR/accname-aam-1.1
if (recursive || nameFromContents()) {
nameFrom = AXNameFromContents;
if (nameSources) {
nameSources->append(NameSource(foundTextAlternative));
nameSources->last().type = nameFrom;
}
Node* node = this->node();
if (node && node->isTextNode())
textAlternative = toText(node)->wholeText();
else
textAlternative = textFromDescendants(visited);
if (!textAlternative.isEmpty()) {
if (nameSources) {
foundTextAlternative = true;
nameSources->last().text = textAlternative;
} else {
return textAlternative;
}
}
}
// Step 2H from: http://www.w3.org/TR/accname-aam-1.1
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(foundTextAlternative, titleAttr));
nameSources->last().type = nameFrom;
}
const AtomicString& title = getAttribute(titleAttr);
if (!title.isEmpty()) {
textAlternative = title;
if (nameSources) {
foundTextAlternative = true;
nameSources->last().text = textAlternative;
} else {
return textAlternative;
}
}
nameFrom = AXNameFromUninitialized;
if (foundTextAlternative) {
for (size_t i = 0; i < nameSources->size(); ++i) {
if (!(*nameSources)[i].text.isNull() && !(*nameSources)[i].superseded) {
NameSource& nameSource = (*nameSources)[i];
nameFrom = nameSource.type;
if (!nameSource.nameObjects.isEmpty())
*nameObjects = nameSource.nameObjects;
return nameSource.text;
}
}
}
return String();
}
String AXNodeObject::textFromDescendants(AXObjectSet& visited) const
{
StringBuilder accumulatedText;
AXObject* previous = nullptr;
for (AXObject* child = firstChild(); child; child = child->nextSibling()) {
// 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 && accumulatedText.length() && !isHTMLSpace(accumulatedText[accumulatedText.length() - 1])) {
if (!isInSameNonInlineBlockFlow(child->layoutObject(), previous->layoutObject()))
accumulatedText.append(' ');
}
String result = recursiveTextAlternative(*child, false, visited);
accumulatedText.append(result);
previous = child;
}
return accumulatedText.toString();
}
String AXNodeObject::textFromElements(bool inAriaLabelledbyTraversal, AXObjectSet& visited, WillBeHeapVector<RawPtrWillBeMember<Element>>& elements, AXObjectVector* nameObjects) const
{
StringBuilder accumulatedText;
bool foundValidElement = false;
AXObjectVector localNameObjects;
for (const auto& element : elements) {
AXObject* axElement = axObjectCache().getOrCreate(element);
if (axElement) {
foundValidElement = true;
localNameObjects.append(axElement);
String result = recursiveTextAlternative(*axElement, inAriaLabelledbyTraversal, visited);
if (!result.isEmpty()) {
if (!accumulatedText.isEmpty())
accumulatedText.append(" ");
accumulatedText.append(result);
}
}
}
if (!foundValidElement)
return String();
if (nameObjects)
*nameObjects = localNameObjects;
return accumulatedText.toString();
}
String AXNodeObject::textFromAriaLabelledby(AXObjectSet& visited, AXObjectVector* nameObjects) const
{
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
ariaLabelledbyElements(elements);
return textFromElements(true, visited, elements, nameObjects);
}
LayoutRect AXNodeObject::elementRect() const
{
// First check if it has a custom rect, for example if this element is tied to a canvas path.
if (!m_explicitElementRect.isEmpty())
return m_explicitElementRect;
// FIXME: If there are a lot of elements in the canvas, it will be inefficient.
// We can avoid the inefficient calculations by using AXComputedObjectAttributeCache.
if (node()->parentElement()->isInCanvasSubtree()) {
LayoutRect rect;
for (Node& child : NodeTraversal::childrenOf(*node())) {
if (child.isHTMLElement()) {
if (AXObject* obj = axObjectCache().get(&child)) {
if (rect.isEmpty())
rect = obj->elementRect();
else
rect.unite(obj->elementRect());
}
}
}
if (!rect.isEmpty())
return rect;
}
// 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.
LayoutRect boundingBox;
for (AXObject* positionProvider = parentObject(); positionProvider; positionProvider = positionProvider->parentObject()) {
if (positionProvider->isAXLayoutObject()) {
LayoutRect parentRect = positionProvider->elementRect();
boundingBox.setSize(LayoutSize(parentRect.width(), LayoutUnit(std::min(10.0f, parentRect.height().toFloat()))));
boundingBox.setLocation(parentRect.location());
break;
}
}
return boundingBox;
}
static Node* getParentNodeForComputeParent(Node* node)
{
if (!node)
return nullptr;
Node* parentNode = nullptr;
// Skip over <optgroup> and consider the <select> the immediate parent of an <option>.
if (isHTMLOptionElement(node))
parentNode = toHTMLOptionElement(node)->ownerSelectElement();
if (!parentNode)
parentNode = node->parentNode();
return parentNode;
}
AXObject* AXNodeObject::computeParent() const
{
if (Node* parentNode = getParentNodeForComputeParent(node()))
return axObjectCache().getOrCreate(parentNode);
return nullptr;
}
AXObject* AXNodeObject::computeParentIfExists() const
{
if (Node* parentNode = getParentNodeForComputeParent(node()))
return axObjectCache().get(parentNode);
return nullptr;
}
AXObject* AXNodeObject::firstChild() const
{
if (!node())
return 0;
Node* firstChild = node()->firstChild();
if (!firstChild)
return 0;
return axObjectCache().getOrCreate(firstChild);
}
AXObject* AXNodeObject::nextSibling() const
{
if (!node())
return 0;
Node* nextSibling = node()->nextSibling();
if (!nextSibling)
return 0;
return axObjectCache().getOrCreate(nextSibling);
}
void AXNodeObject::addChildren()
{
// If the need to add more children in addition to existing children arises,
// childrenChanged should have been called, leaving the object with no children.
ASSERT(!m_haveChildren);
if (!m_node)
return;
m_haveChildren = true;
// The only time we add children from the DOM tree to a node with a layoutObject is when it's a canvas.
if (layoutObject() && !isHTMLCanvasElement(*m_node))
return;
HeapVector<Member<AXObject>> ownedChildren;
computeAriaOwnsChildren(ownedChildren);
for (Node& child : NodeTraversal::childrenOf(*m_node)) {
AXObject* childObj = axObjectCache().getOrCreate(&child);
if (!axObjectCache().isAriaOwned(childObj))
addChild(childObj);
}
for (const auto& ownedChild : ownedChildren)
addChild(ownedChild);
for (const auto& child : m_children)
child->setParent(this);
}
void AXNodeObject::addChild(AXObject* child)
{
insertChild(child, m_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)
m_children.insert(index + i, children[i]);
} else {
ASSERT(child->parentObject() == this);
m_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 (!node() && !isAXLayoutObject())
return false;
// Elements that should not have children
switch (roleValue()) {
case ImageRole:
case ButtonRole:
case PopUpButtonRole:
case CheckBoxRole:
case RadioButtonRole:
case SwitchRole:
case TabRole:
case ToggleButtonRole:
case ListBoxOptionRole:
case ScrollBarRole:
return false;
case StaticTextRole:
if (!axObjectCache().inlineTextBoxAccessibilityEnabled())
return false;
default:
return true;
}
}
Element* AXNodeObject::actionElement() const
{
Node* node = this->node();
if (!node)
return 0;
if (isHTMLInputElement(*node)) {
HTMLInputElement& input = toHTMLInputElement(*node);
if (!input.isDisabledFormControl() && (isCheckboxOrRadio() || input.isTextButton() || input.type() == InputTypeNames::file))
return &input;
} else if (isHTMLButtonElement(*node)) {
return toElement(node);
}
if (AXObject::isARIAInput(ariaRoleAttribute()))
return toElement(node);
if (isImageButton())
return toElement(node);
if (isHTMLSelectElement(*node))
return toElement(node);
switch (roleValue()) {
case ButtonRole:
case PopUpButtonRole:
case ToggleButtonRole:
case TabRole:
case MenuItemRole:
case MenuItemCheckBoxRole:
case MenuItemRadioRole:
case ListItemRole:
return toElement(node);
default:
break;
}
Element* elt = anchorElement();
if (!elt)
elt = mouseButtonListener();
return elt;
}
Element* AXNodeObject::anchorElement() const
{
Node* node = this->node();
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->layoutObject() && cache.getOrCreate(node->layoutObject())->isAnchor()))
return toElement(node);
}
return 0;
}
Document* AXNodeObject::document() const
{
if (!node())
return 0;
return &node()->document();
}
void AXNodeObject::setNode(Node* node)
{
m_node = node;
}
AXObject* AXNodeObject::correspondingControlForLabelElement() const
{
HTMLLabelElement* labelElement = labelElementContainer();
if (!labelElement)
return 0;
HTMLElement* correspondingControl = labelElement->control();
if (!correspondingControl)
return 0;
// Make sure the corresponding control isn't a descendant of this label
// that's in the middle of being destroyed.
if (correspondingControl->layoutObject() && !correspondingControl->layoutObject()->parent())
return 0;
return axObjectCache().getOrCreate(correspondingControl);
}
HTMLLabelElement* AXNodeObject::labelElementContainer() const
{
if (!node())
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(*node());
}
void AXNodeObject::setFocused(bool on)
{
if (!canSetFocusAttribute())
return;
Document* document = this->document();
if (!on) {
document->setFocusedElement(nullptr);
} else {
Node* node = this->node();
if (node && node->isElementNode()) {
// If this node is already the currently focused node, then calling focus() won't do anything.
// That is a problem when focus is removed from the webpage to chrome, and then returns.
// In these cases, we need to do what keyboard and mouse focus do, which is reset focus first.
if (document->focusedElement() == node)
document->setFocusedElement(nullptr);
toElement(node)->focus();
} else {
document->setFocusedElement(nullptr);
}
}
}
void AXNodeObject::increment()
{
UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
alterSliderValue(true);
}
void AXNodeObject::decrement()
{
UserGestureIndicator gestureIndicator(DefinitelyProcessingNewUserGesture);
alterSliderValue(false);
}
void AXNodeObject::childrenChanged()
{
// This method is meant as a quick way of marking a portion of the accessibility tree dirty.
if (!node() && !layoutObject())
return;
// If this is not part of the accessibility tree because an ancestor
// has only presentational children, invalidate this object's children but
// skip sending a notification and skip walking up the ancestors.
if (ancestorForWhichThisIsAPresentationalChild()) {
setNeedsToUpdateChildren();
return;
}
axObjectCache().postNotification(this, AXObjectCacheImpl::AXChildrenChanged);
// 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::AXLiveRegionChanged);
// 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::AXValueChanged);
}
}
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::AXSelectedTextChanged);
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* parentNode = node(); parentNode; parentNode = parentNode->parentNode()) {
AXObject* parent = cache.get(parentNode);
if (!parent)
continue;
if (parent->isLiveRegion())
cache.postNotification(parentNode, AXObjectCacheImpl::AXLiveRegionChanged);
// 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(parentNode, AXObjectCacheImpl::AXValueChanged);
}
}
void AXNodeObject::updateAccessibilityRole()
{
bool ignoredStatus = accessibilityIsIgnored();
m_role = determineAccessibilityRole();
// The AX hierarchy only needs to be updated if the ignored status of an element has changed.
if (ignoredStatus != accessibilityIsIgnored())
childrenChanged();
}
void AXNodeObject::computeAriaOwnsChildren(HeapVector<Member<AXObject>>& ownedChildren)
{
if (!hasAttribute(aria_ownsAttr))
return;
Vector<String> idVector;
tokenVectorFromAttribute(idVector, aria_ownsAttr);
axObjectCache().updateAriaOwns(this, idVector, ownedChildren);
}
String AXNodeObject::deprecatedAlternativeTextForWebArea() const
{
// The WebArea description should follow this order:
// aria-label on the <html>
// title on the <html>
// <title> inside the <head> (of it was set through JS)
// name on the <html>
// For iframes:
// aria-label on the <iframe>
// title on the <iframe>
// name on the <iframe>
Document* document = this->document();
if (!document)
return String();
// Check if the HTML element has an aria-label for the webpage.
if (Element* documentElement = document->documentElement()) {
const AtomicString& ariaLabel = documentElement->getAttribute(aria_labelAttr);
if (!ariaLabel.isEmpty())
return ariaLabel;
}
if (HTMLFrameOwnerElement* owner = document->ownerElement()) {
if (isHTMLFrameElementBase(*owner)) {
const AtomicString& title = owner->getAttribute(titleAttr);
if (!title.isEmpty())
return title;
}
return owner->getNameAttribute();
}
String documentTitle = document->title();
if (!documentTitle.isEmpty())
return documentTitle;
if (HTMLElement* body = document->body())
return body->getNameAttribute();
return String();
}
void AXNodeObject::deprecatedAlternativeText(HeapVector<Member<AccessibilityText>>& textOrder) const
{
if (isWebArea()) {
String webAreaText = deprecatedAlternativeTextForWebArea();
if (!webAreaText.isEmpty())
textOrder.append(AccessibilityText::create(webAreaText, AlternativeText));
return;
}
deprecatedAriaLabelledbyText(textOrder);
const AtomicString& ariaLabel = getAttribute(aria_labelAttr);
if (!ariaLabel.isEmpty())
textOrder.append(AccessibilityText::create(ariaLabel, AlternativeText));
if (isImage() || isInputImage() || isNativeImage() || isCanvas()) {
// Images should use alt as long as the attribute is present, even if empty.
// Otherwise, it should fallback to other methods, like the title attribute.
const AtomicString& alt = getAttribute(altAttr);
if (!alt.isNull())
textOrder.append(AccessibilityText::create(alt, AlternativeText));
}
}
void AXNodeObject::deprecatedAriaLabelledbyText(HeapVector<Member<AccessibilityText>>& textOrder) const
{
String ariaLabelledby = ariaLabelledbyAttribute();
if (!ariaLabelledby.isEmpty()) {
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
ariaLabelledbyElements(elements);
for (const auto& element : elements) {
AXObject* axElement = axObjectCache().getOrCreate(element);
textOrder.append(AccessibilityText::create(ariaLabelledby, AlternativeText, axElement));
}
}
}
// Based on http://rawgit.com/w3c/aria/master/html-aam/html-aam.html#accessible-name-and-description-calculation
String AXNodeObject::nativeTextAlternative(AXObjectSet& visited, AXNameFrom& nameFrom, AXObjectVector* nameObjects, NameSources* nameSources, bool* foundTextAlternative) const
{
if (!node())
return String();
// If nameSources is non-null, nameObjects is used in filling it in, so it must be non-null as well.
if (nameSources)
ASSERT(nameObjects);
String textAlternative;
AXObjectVector localNameObjects;
const HTMLInputElement* inputElement = nullptr;
if (isHTMLInputElement(node()))
inputElement = toHTMLInputElement(node());
// 5.1/5.5 Text inputs, Other labelable Elements
HTMLElement* htmlElement = nullptr;
if (node()->isHTMLElement())
htmlElement = toHTMLElement(node());
if (htmlElement && htmlElement->isLabelable()) {
// label
nameFrom = AXNameFromRelatedElement;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative));
nameSources->last().type = nameFrom;
nameSources->last().nativeSource = AXTextFromNativeHTMLLabel;
}
HTMLLabelElement* label = labelForElement(htmlElement);
if (label) {
AXObject* labelAXObject = axObjectCache().getOrCreate(label);
// Avoid an infinite loop for label wrapped
if (labelAXObject && !visited.contains(labelAXObject)) {
if (nameObjects) {
localNameObjects.append(labelAXObject);
*nameObjects = localNameObjects;
localNameObjects.clear();
}
textAlternative = recursiveTextAlternative(*labelAXObject, false, visited);
if (nameSources) {
NameSource& source = nameSources->last();
source.nameObjects = *nameObjects;
source.text = textAlternative;
if (label->getAttribute(forAttr).isNull())
source.nativeSource = AXTextFromNativeHTMLLabelWrapped;
else
source.nativeSource = AXTextFromNativeHTMLLabelFor;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
}
}
// 5.2 input type="button", input type="submit" and input type="reset"
if (inputElement && inputElement->isTextButton()) {
// value attribue
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, valueAttr));
nameSources->last().type = nameFrom;
}
String value = inputElement->value();
if (!value.isNull()) {
textAlternative = value;
if (nameSources) {
NameSource& source = nameSources->last();
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
return textAlternative;
}
// 5.3 input type="image"
if (inputElement && inputElement->getAttribute(typeAttr) == InputTypeNames::image) {
// alt attr
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, altAttr));
nameSources->last().type = nameFrom;
}
const AtomicString& alt = inputElement->getAttribute(altAttr);
if (!alt.isNull()) {
textAlternative = alt;
if (nameSources) {
NameSource& source = nameSources->last();
source.attributeValue = alt;
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
// value attr
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, valueAttr));
nameSources->last().type = nameFrom;
}
nameFrom = AXNameFromAttribute;
String value = inputElement->value();
if (!value.isNull()) {
textAlternative = value;
if (nameSources) {
NameSource& source = nameSources->last();
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
// localised default value ("Submit")
nameFrom = AXNameFromAttribute;
textAlternative = inputElement->locale().queryString(WebLocalizedString::SubmitButtonDefaultLabel);
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, typeAttr));
NameSource& source = nameSources->last();
source.attributeValue = inputElement->getAttribute(typeAttr);
source.type = nameFrom;
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
return textAlternative;
}
// 5.1 Text inputs - step 3 (placeholder attribute)
if (htmlElement && htmlElement->isTextFormControl()) {
nameFrom = AXNameFromPlaceholder;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, placeholderAttr));
NameSource& source = nameSources->last();
source.type = nameFrom;
}
HTMLElement* element = toHTMLElement(node());
const AtomicString& placeholder = element->fastGetAttribute(placeholderAttr);
if (!placeholder.isEmpty()) {
textAlternative = placeholder;
if (nameSources) {
NameSource& source = nameSources->last();
source.text = textAlternative;
source.attributeValue = placeholder;
} else {
return textAlternative;
}
}
return textAlternative;
}
// 5.7 figure and figcaption Elements
if (node()->hasTagName(figureTag)) {
// figcaption
nameFrom = AXNameFromRelatedElement;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative));
nameSources->last().type = nameFrom;
nameSources->last().nativeSource = AXTextFromNativeHTMLFigcaption;
}
Element* figcaption = nullptr;
for (Element& element : ElementTraversal::descendantsOf(*(node()))) {
if (element.hasTagName(figcaptionTag)) {
figcaption = &element;
break;
}
}
if (figcaption) {
AXObject* figcaptionAXObject = axObjectCache().getOrCreate(figcaption);
if (figcaptionAXObject) {
if (nameObjects) {
localNameObjects.append(figcaptionAXObject);
*nameObjects = localNameObjects;
localNameObjects.clear();
}
textAlternative = recursiveTextAlternative(*figcaptionAXObject, false, visited);
if (nameSources) {
NameSource& source = nameSources->last();
source.nameObjects = *nameObjects;
source.text = textAlternative;
} else {
return textAlternative;
}
}
}
return textAlternative;
}
// 5.8 img Element
if (isHTMLImageElement(node())) {
// alt
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative, altAttr));
nameSources->last().type = nameFrom;
}
const AtomicString& alt = getAttribute(altAttr);
if (!alt.isNull()) {
textAlternative = alt;
if (nameSources) {
NameSource& source = nameSources->last();
source.attributeValue = alt;
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
return textAlternative;
}
// 5.9 table Element
if (isHTMLTableElement(node())) {
HTMLTableElement* tableElement = toHTMLTableElement(node());
// caption
nameFrom = AXNameFromRelatedElement;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative));
nameSources->last().type = nameFrom;
nameSources->last().nativeSource = AXTextFromNativeHTMLTableCaption;
}
HTMLTableCaptionElement* caption = tableElement->caption();
if (caption) {
AXObject* captionAXObject = axObjectCache().getOrCreate(caption);
if (captionAXObject) {
if (nameObjects) {
localNameObjects.append(captionAXObject);
*nameObjects = localNameObjects;
localNameObjects.clear();
}
textAlternative = recursiveTextAlternative(*captionAXObject, false, visited);
if (nameSources) {
NameSource& source = nameSources->last();
source.nameObjects = *nameObjects;
source.text = textAlternative;
} else {
return textAlternative;
}
}
}
// summary
nameFrom = AXNameFromAttribute;
if (nameSources) {
nameSources->append(NameSource(*foundTextAlternative));
nameSources->last().type = nameFrom;
}
const AtomicString& summary = getAttribute(summaryAttr);
if (!summary.isNull()) {
textAlternative = summary;
if (nameSources) {
NameSource& source = nameSources->last();
source.attributeValue = summary;
source.text = textAlternative;
*foundTextAlternative = true;
} else {
return textAlternative;
}
}
return textAlternative;
}
return textAlternative;
}
DEFINE_TRACE(AXNodeObject)
{
visitor->trace(m_node);
AXObject::trace(visitor);
}
} // namespace blink