| /* |
| * Copyright (C) 2012, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "modules/accessibility/AXNodeObject.h" |
| |
| #include "core/InputTypeNames.h" |
| #include "core/dom/AccessibleNode.h" |
| #include "core/dom/DocumentUserGestureToken.h" |
| #include "core/dom/Element.h" |
| #include "core/dom/NodeTraversal.h" |
| #include "core/dom/QualifiedName.h" |
| #include "core/dom/Text.h" |
| #include "core/dom/shadow/FlatTreeTraversal.h" |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/markers/DocumentMarkerController.h" |
| #include "core/frame/FrameView.h" |
| #include "core/html/HTMLAnchorElement.h" |
| #include "core/html/HTMLDListElement.h" |
| #include "core/html/HTMLDivElement.h" |
| #include "core/html/HTMLFieldSetElement.h" |
| #include "core/html/HTMLFrameElementBase.h" |
| #include "core/html/HTMLImageElement.h" |
| #include "core/html/HTMLInputElement.h" |
| #include "core/html/HTMLLabelElement.h" |
| #include "core/html/HTMLLegendElement.h" |
| #include "core/html/HTMLMediaElement.h" |
| #include "core/html/HTMLMeterElement.h" |
| #include "core/html/HTMLPlugInElement.h" |
| #include "core/html/HTMLSelectElement.h" |
| #include "core/html/HTMLTableCaptionElement.h" |
| #include "core/html/HTMLTableCellElement.h" |
| #include "core/html/HTMLTableElement.h" |
| #include "core/html/HTMLTableRowElement.h" |
| #include "core/html/HTMLTableSectionElement.h" |
| #include "core/html/HTMLTextAreaElement.h" |
| #include "core/html/LabelsNodeList.h" |
| #include "core/html/TextControlElement.h" |
| #include "core/html/forms/RadioInputType.h" |
| #include "core/html/parser/HTMLParserIdioms.h" |
| #include "core/html/shadow/MediaControlElements.h" |
| #include "core/layout/LayoutBlockFlow.h" |
| #include "core/layout/LayoutObject.h" |
| #include "core/svg/SVGElement.h" |
| #include "modules/accessibility/AXObjectCacheImpl.h" |
| #include "platform/UserGestureIndicator.h" |
| #include "platform/text/PlatformLocale.h" |
| #include "platform/weborigin/KURL.h" |
| #include "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) : m_attribute(attribute) {} |
| |
| private: |
| AXBoolAttribute m_attribute; |
| |
| void run(const AXObject& obj, |
| AXSparseAttributeClient& attributeMap, |
| const AtomicString& value) override { |
| attributeMap.addBoolAttribute(m_attribute, |
| equalIgnoringCase(value, "true")); |
| } |
| }; |
| |
| class StringAttributeSetter : public SparseAttributeSetter { |
| public: |
| StringAttributeSetter(AXStringAttribute attribute) : m_attribute(attribute) {} |
| |
| private: |
| AXStringAttribute m_attribute; |
| |
| void run(const AXObject& obj, |
| AXSparseAttributeClient& attributeMap, |
| const AtomicString& value) override { |
| attributeMap.addStringAttribute(m_attribute, value); |
| } |
| }; |
| |
| class ObjectAttributeSetter : public SparseAttributeSetter { |
| public: |
| ObjectAttributeSetter(AXObjectAttribute attribute) : m_attribute(attribute) {} |
| |
| private: |
| AXObjectAttribute m_attribute; |
| |
| void run(const AXObject& obj, |
| AXSparseAttributeClient& attributeMap, |
| const AtomicString& value) override { |
| if (value.isNull() || value.isEmpty()) |
| return; |
| |
| Node* node = obj.getNode(); |
| if (!node || !node->isElementNode()) |
| return; |
| Element* target = toElement(node)->treeScope().getElementById(value); |
| if (!target) |
| return; |
| AXObject* axTarget = obj.axObjectCache().getOrCreate(target); |
| if (axTarget) |
| attributeMap.addObjectAttribute(m_attribute, *axTarget); |
| } |
| }; |
| |
| class ObjectVectorAttributeSetter : public SparseAttributeSetter { |
| public: |
| ObjectVectorAttributeSetter(AXObjectVectorAttribute attribute) |
| : m_attribute(attribute) {} |
| |
| private: |
| AXObjectVectorAttribute m_attribute; |
| |
| void run(const AXObject& obj, |
| AXSparseAttributeClient& attributeMap, |
| const AtomicString& value) override { |
| Node* node = obj.getNode(); |
| if (!node || !node->isElementNode()) |
| return; |
| |
| String attributeValue = value.getString(); |
| if (attributeValue.isEmpty()) |
| return; |
| |
| attributeValue.simplifyWhiteSpace(); |
| Vector<String> ids; |
| attributeValue.split(' ', ids); |
| if (ids.isEmpty()) |
| return; |
| |
| HeapVector<Member<AXObject>> objects; |
| TreeScope& scope = node->treeScope(); |
| for (const auto& id : ids) { |
| if (Element* idElement = scope.getElementById(AtomicString(id))) { |
| AXObject* axIdElement = obj.axObjectCache().getOrCreate(idElement); |
| if (axIdElement && !axIdElement->accessibilityIsIgnored()) |
| objects.push_back(axIdElement); |
| } |
| } |
| |
| attributeMap.addObjectVectorAttribute(m_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, axSparseAttributeSetterMap, |
| ()); |
| if (axSparseAttributeSetterMap.isEmpty()) { |
| axSparseAttributeSetterMap.set( |
| aria_activedescendantAttr, |
| new ObjectAttributeSetter(AXObjectAttribute::AriaActiveDescendant)); |
| axSparseAttributeSetterMap.set( |
| aria_controlsAttr, |
| new ObjectVectorAttributeSetter(AXObjectVectorAttribute::AriaControls)); |
| axSparseAttributeSetterMap.set( |
| aria_flowtoAttr, |
| new ObjectVectorAttributeSetter(AXObjectVectorAttribute::AriaFlowTo)); |
| axSparseAttributeSetterMap.set( |
| aria_detailsAttr, |
| new ObjectVectorAttributeSetter(AXObjectVectorAttribute::AriaDetails)); |
| axSparseAttributeSetterMap.set( |
| aria_errormessageAttr, |
| new ObjectAttributeSetter(AXObjectAttribute::AriaErrorMessage)); |
| axSparseAttributeSetterMap.set( |
| aria_keyshortcutsAttr, |
| new StringAttributeSetter(AXStringAttribute::AriaKeyShortcuts)); |
| axSparseAttributeSetterMap.set( |
| aria_roledescriptionAttr, |
| new StringAttributeSetter(AXStringAttribute::AriaRoleDescription)); |
| } |
| return axSparseAttributeSetterMap; |
| } |
| |
| AXNodeObject::AXNodeObject(Node* node, AXObjectCacheImpl& axObjectCache) |
| : AXObject(axObjectCache), |
| m_ariaRole(UnknownRole), |
| m_childrenDirty(false), |
| m_node(node) { |
| } |
| |
| AXNodeObject* AXNodeObject::create(Node* node, |
| AXObjectCacheImpl& axObjectCache) { |
| return new AXNodeObject(node, axObjectCache); |
| } |
| |
| AXNodeObject::~AXNodeObject() { |
| ASSERT(!m_node); |
| } |
| |
| 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(getNode(), |
| AXObjectCacheImpl::AXValueChanged); |
| } |
| |
| AXObject* AXNodeObject::activeDescendant() { |
| if (!m_node || !m_node->isElementNode()) |
| return nullptr; |
| |
| const AtomicString& activeDescendantAttr = |
| getAttribute(aria_activedescendantAttr); |
| if (activeDescendantAttr.isNull() || activeDescendantAttr.isEmpty()) |
| return nullptr; |
| |
| Element* element = toElement(getNode()); |
| Element* descendant = |
| element->treeScope().getElementById(activeDescendantAttr); |
| if (!descendant) |
| return nullptr; |
| |
| AXObject* axDescendant = axObjectCache().getOrCreate(descendant); |
| return axDescendant; |
| } |
| |
| bool AXNodeObject::computeAccessibilityIsIgnored( |
| IgnoredReasons* ignoredReasons) const { |
| #if DCHECK_IS_ON() |
| // 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->push_back( |
| IgnoredReason(AXAncestorIsLeafNode, leafNodeAncestor())); |
| return true; |
| } |
| |
| // Ignore labels that are already referenced by a control. |
| AXObject* controlObject = correspondingControlForLabelElement(); |
| if (controlObject && controlObject->isCheckboxOrRadio() && |
| controlObject->nameFromLabelElement()) { |
| if (ignoredReasons) { |
| HTMLLabelElement* label = labelElementContainer(); |
| if (label && label != getNode()) { |
| AXObject* labelAXObject = axObjectCache().getOrCreate(label); |
| ignoredReasons->push_back( |
| IgnoredReason(AXLabelContainer, labelAXObject)); |
| } |
| |
| ignoredReasons->push_back(IgnoredReason(AXLabelFor, controlObject)); |
| } |
| return true; |
| } |
| |
| Element* element = getNode()->isElementNode() ? toElement(getNode()) |
| : getNode()->parentElement(); |
| if (!getLayoutObject() && (!element || !element->isInCanvasSubtree()) && |
| !equalIgnoringCase(getAttribute(aria_hiddenAttr), "false")) { |
| if (ignoredReasons) |
| ignoredReasons->push_back(IgnoredReason(AXNotRendered)); |
| return true; |
| } |
| |
| if (m_role == UnknownRole) { |
| if (ignoredReasons) |
| ignoredReasons->push_back(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->getNode(); |
| 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->getNode() : 0; |
| return isHTMLTableElement(tableNode) && |
| tableObject->hasInheritedPresentationalRole(); |
| } |
| return false; |
| } |
| |
| static bool isRequiredOwnedElement(AXObject* parent, |
| AccessibilityRole currentRole, |
| HTMLElement* currentElement) { |
| Node* parentNode = parent->getNode(); |
| 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 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() != UnknownRole) |
| 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* cssBox = toLayoutBoxModelObject(getLayoutObject()); |
| 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; |
| } |
| |
| // 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>, landmarkRolesNotAllowed, ()); |
| if (landmarkRolesNotAllowed.isEmpty()) { |
| landmarkRolesNotAllowed.insert(articleTag); |
| landmarkRolesNotAllowed.insert(asideTag); |
| landmarkRolesNotAllowed.insert(navTag); |
| landmarkRolesNotAllowed.insert(sectionTag); |
| landmarkRolesNotAllowed.insert(blockquoteTag); |
| landmarkRolesNotAllowed.insert(detailsTag); |
| landmarkRolesNotAllowed.insert(fieldsetTag); |
| landmarkRolesNotAllowed.insert(figureTag); |
| landmarkRolesNotAllowed.insert(tdTag); |
| landmarkRolesNotAllowed.insert(mainTag); |
| } |
| return landmarkRolesNotAllowed; |
| } |
| |
| bool AXNodeObject::isDescendantOfElementType( |
| HashSet<QualifiedName>& tagNames) const { |
| if (!getNode()) |
| return false; |
| |
| for (Element* parent = getNode()->parentElement(); parent; |
| parent = parent->parentElement()) { |
| if (tagNames.contains(parent->tagQName())) |
| return true; |
| } |
| return false; |
| } |
| |
| AccessibilityRole AXNodeObject::nativeAccessibilityRoleIgnoringAria() const { |
| if (!getNode()) |
| return UnknownRole; |
| |
| // |HTMLAnchorElement| sets isLink only when it has hrefAttr. |
| if (getNode()->isLink()) |
| return LinkRole; |
| |
| 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 LinkRole; |
| return AnchorRole; |
| } |
| |
| if (isHTMLButtonElement(*getNode())) |
| return buttonRoleType(); |
| |
| if (isHTMLDetailsElement(*getNode())) |
| return DetailsRole; |
| |
| if (isHTMLSummaryElement(*getNode())) { |
| ContainerNode* parent = FlatTreeTraversal::parent(*getNode()); |
| if (parent && isHTMLDetailsElement(parent)) |
| return DisclosureTriangleRole; |
| return UnknownRole; |
| } |
| |
| if (isHTMLInputElement(*getNode())) { |
| HTMLInputElement& input = toHTMLInputElement(*getNode()); |
| const AtomicString& type = input.type(); |
| if (input.dataList()) |
| return ComboBoxRole; |
| if (type == InputTypeNames::button) { |
| if ((getNode()->parentNode() && |
| isHTMLMenuElement(getNode()->parentNode())) || |
| (parentObject() && parentObject()->roleValue() == MenuRole)) |
| return MenuItemRole; |
| return buttonRoleType(); |
| } |
| if (type == InputTypeNames::checkbox) { |
| if ((getNode()->parentNode() && |
| isHTMLMenuElement(getNode()->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 ((getNode()->parentNode() && |
| isHTMLMenuElement(getNode()->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(*getNode())) { |
| HTMLSelectElement& selectElement = toHTMLSelectElement(*getNode()); |
| return selectElement.isMultiple() ? ListBoxRole : PopUpButtonRole; |
| } |
| |
| if (isHTMLTextAreaElement(*getNode())) |
| return TextFieldRole; |
| |
| if (headingLevel()) |
| return HeadingRole; |
| |
| if (isHTMLDivElement(*getNode())) |
| return DivRole; |
| |
| if (isHTMLMeterElement(*getNode())) |
| return MeterRole; |
| |
| if (isHTMLOutputElement(*getNode())) |
| return StatusRole; |
| |
| if (isHTMLParagraphElement(*getNode())) |
| return ParagraphRole; |
| |
| if (isHTMLLabelElement(*getNode())) |
| return LabelRole; |
| |
| if (isHTMLLegendElement(*getNode())) |
| return LegendRole; |
| |
| if (isHTMLRubyElement(*getNode())) |
| return RubyRole; |
| |
| if (isHTMLDListElement(*getNode())) |
| return DescriptionListRole; |
| |
| if (isHTMLAudioElement(*getNode())) |
| return AudioRole; |
| if (isHTMLVideoElement(*getNode())) |
| return VideoRole; |
| |
| if (getNode()->hasTagName(ddTag)) |
| return DescriptionListDetailRole; |
| |
| if (getNode()->hasTagName(dtTag)) |
| return DescriptionListTermRole; |
| |
| if (getNode()->nodeName() == "math") |
| return MathRole; |
| |
| if (getNode()->hasTagName(rpTag) || getNode()->hasTagName(rtTag)) |
| return AnnotationRole; |
| |
| if (isHTMLFormElement(*getNode())) |
| return FormRole; |
| |
| if (getNode()->hasTagName(abbrTag)) |
| return AbbrRole; |
| |
| if (getNode()->hasTagName(articleTag)) |
| return ArticleRole; |
| |
| if (getNode()->hasTagName(mainTag)) |
| return MainRole; |
| |
| if (getNode()->hasTagName(markTag)) |
| return MarkRole; |
| |
| if (getNode()->hasTagName(navTag)) |
| return NavigationRole; |
| |
| if (getNode()->hasTagName(asideTag)) |
| return ComplementaryRole; |
| |
| if (getNode()->hasTagName(preTag)) |
| return PreRole; |
| |
| if (getNode()->hasTagName(sectionTag)) |
| return RegionRole; |
| |
| if (getNode()->hasTagName(addressTag)) |
| return ContentInfoRole; |
| |
| if (isHTMLDialogElement(*getNode())) |
| return DialogRole; |
| |
| // The HTML element should not be exposed as an element. That's what the |
| // LayoutView element does. |
| if (isHTMLHtmlElement(*getNode())) |
| return IgnoredRole; |
| |
| if (isHTMLIFrameElement(*getNode())) { |
| const AtomicString& ariaRole = |
| getAOMPropertyOrARIAAttribute(AOMStringProperty::kRole); |
| 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 but as a group role. |
| if (getNode()->hasTagName(headerTag)) { |
| if (isDescendantOfElementType(getLandmarkRolesNotAllowed())) |
| return GroupRole; |
| return BannerRole; |
| } |
| |
| if (getNode()->hasTagName(footerTag)) { |
| if (isDescendantOfElementType(getLandmarkRolesNotAllowed())) |
| return GroupRole; |
| return FooterRole; |
| } |
| |
| if (getNode()->hasTagName(blockquoteTag)) |
| return BlockquoteRole; |
| |
| if (getNode()->hasTagName(captionTag)) |
| return CaptionRole; |
| |
| if (getNode()->hasTagName(figcaptionTag)) |
| return FigcaptionRole; |
| |
| if (getNode()->hasTagName(figureTag)) |
| return FigureRole; |
| |
| if (getNode()->nodeName() == "TIME") |
| return TimeRole; |
| |
| if (isEmbeddedObject()) |
| return EmbeddedObjectRole; |
| |
| if (isHTMLHRElement(*getNode())) |
| return SplitterRole; |
| |
| if (isFieldset()) |
| return GroupRole; |
| |
| return UnknownRole; |
| } |
| |
| AccessibilityRole AXNodeObject::determineAccessibilityRole() { |
| if (!getNode()) |
| return UnknownRole; |
| |
| if ((m_ariaRole = determineAriaRoleAttribute()) != UnknownRole) |
| return m_ariaRole; |
| if (getNode()->isTextNode()) |
| return StaticTextRole; |
| |
| AccessibilityRole role = nativeAccessibilityRoleIgnoringAria(); |
| if (role != UnknownRole) |
| return role; |
| if (getNode()->isElementNode()) { |
| Element* element = toElement(getNode()); |
| // A generic element with tabIndex explicitly set gets GroupRole. |
| // The layout checks for focusability aren't critical here; a false |
| // positive would be harmless. |
| if (element->isInCanvasSubtree() && element->supportsFocus()) |
| return GroupRole; |
| } |
| return UnknownRole; |
| } |
| |
| AccessibilityRole AXNodeObject::determineAriaRoleAttribute() const { |
| const AtomicString& ariaRole = |
| getAOMPropertyOrARIAAttribute(AOMStringProperty::kRole); |
| 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::accessibilityChildrenFromAttribute( |
| QualifiedName attr, |
| AXObject::AXObjectVector& children) const { |
| HeapVector<Member<Element>> elements; |
| elementsFromAttribute(elements, attr); |
| |
| 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() && attr != aria_labelledbyAttr && |
| attr != aria_labeledbyAttr && attr != aria_describedbyAttr) { |
| continue; |
| } |
| children.push_back(child); |
| } |
| } |
| } |
| |
| // This only returns true if this is the element that actually has the |
| // contentEditable attribute set, unlike node->hasEditableStyle() which will |
| // also return true if an ancestor is editable. |
| bool AXNodeObject::hasContentEditableAttributeSet() const { |
| const AtomicString& 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(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() == SVGRootRole) |
| return false; |
| |
| return true; |
| } |
| |
| 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", getNode()); |
| } |
| |
| Element* AXNodeObject::mouseButtonListener() const { |
| Node* node = this->getNode(); |
| if (!node) |
| return 0; |
| |
| if (!node->isElementNode()) |
| node = node->parentElement(); |
| |
| if (!node) |
| return 0; |
| |
| for (Element* element = toElement(node); element; |
| element = element->parentElement()) { |
| // It's a pretty common practice to put click listeners on the body or |
| // document, but that's almost never what the user wants when clicking on an |
| // accessible element. |
| if (isHTMLBodyElement(element)) |
| break; |
| |
| if (element->hasEventListeners(EventTypeNames::click) || |
| element->hasEventListeners(EventTypeNames::mousedown) || |
| element->hasEventListeners(EventTypeNames::mouseup) || |
| element->hasEventListeners(EventTypeNames::DOMActivate)) |
| return element; |
| } |
| |
| return 0; |
| } |
| |
| AccessibilityRole AXNodeObject::remapAriaRoleDueToParent( |
| AccessibilityRole role) const { |
| // Some objects change their role based on their parent. |
| // However, asking for the unignoredParent calls accessibilityIsIgnored(), |
| // which can trigger a loop. While inside the call stack of creating an |
| // element, we need to avoid accessibilityIsIgnored(). |
| // https://bugs.webkit.org/show_bug.cgi?id=65174 |
| |
| if (role != 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 DCHECK_IS_ON() |
| ASSERT(!m_initialized); |
| m_initialized = true; |
| #endif |
| m_role = determineAccessibilityRole(); |
| } |
| |
| void AXNodeObject::detach() { |
| AXObject::detach(); |
| m_node = nullptr; |
| } |
| |
| void AXNodeObject::getSparseAXAttributes( |
| AXSparseAttributeClient& sparseAttributeClient) const { |
| Node* node = this->getNode(); |
| if (!node || !node->isElementNode()) |
| return; |
| |
| AXSparseAttributeSetterMap& axSparseAttributeSetterMap = |
| getSparseAttributeSetterMap(); |
| AttributeCollection attributes = toElement(node)->attributesWithoutUpdate(); |
| for (const Attribute& attr : attributes) { |
| SparseAttributeSetter* setter = axSparseAttributeSetterMap.at(attr.name()); |
| if (setter) |
| setter->run(*this, sparseAttributeClient, 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(toParentMediaElement(node)); |
| } |
| |
| bool AXNodeObject::isEmbeddedObject() const { |
| return isHTMLPlugInElement(getNode()); |
| } |
| |
| bool AXNodeObject::isFieldset() const { |
| return isHTMLFieldSetElement(getNode()); |
| } |
| |
| bool AXNodeObject::isHeading() const { |
| return roleValue() == HeadingRole; |
| } |
| |
| bool AXNodeObject::isHovered() const { |
| if (Node* node = this->getNode()) |
| return node->isHovered(); |
| return false; |
| } |
| |
| bool AXNodeObject::isImage() const { |
| return roleValue() == ImageRole; |
| } |
| |
| bool AXNodeObject::isImageButton() const { |
| return isNativeImage() && isButton(); |
| } |
| |
| bool AXNodeObject::isInputImage() const { |
| Node* node = this->getNode(); |
| if (roleValue() == ButtonRole && isHTMLInputElement(node)) |
| return toHTMLInputElement(*node).type() == InputTypeNames::image; |
| |
| return false; |
| } |
| |
| bool AXNodeObject::isLink() const { |
| return roleValue() == LinkRole; |
| } |
| |
| // 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 (!m_node || !m_node->isElementNode()) |
| return false; |
| Element* element = toElement(m_node); |
| // We exclude elements that are in the shadow DOM. |
| if (element->containingShadowRoot()) |
| return false; |
| |
| if (isHTMLAnchorElement(element)) { |
| HTMLAnchorElement* htmlElement = toHTMLAnchorElement(element); |
| return htmlElement->hasName() || htmlElement->hasID(); |
| } |
| |
| if (element->hasID() && (isLandmarkRelated() || isHTMLDivElement(element))) |
| return true; |
| return false; |
| } |
| |
| 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(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 ariaRole = ariaRoleAttribute(); |
| if (ariaRole != TextFieldRole && ariaRole != UnknownRole) |
| return false; |
| |
| return toHTMLInputElement(node)->type() == InputTypeNames::password; |
| } |
| |
| bool AXNodeObject::isProgressIndicator() const { |
| return roleValue() == ProgressIndicatorRole; |
| } |
| |
| bool AXNodeObject::isRichlyEditable() const { |
| return hasContentEditableAttributeSet(); |
| } |
| |
| bool AXNodeObject::isSlider() const { |
| return roleValue() == SliderRole; |
| } |
| |
| 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::isChecked() const { |
| Node* node = this->getNode(); |
| 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 (getNode()) { |
| if (getNode()->isElementNode() && |
| toElement(getNode())->isDisabledFormControl()) |
| return false; |
| |
| // Note: we can't call getNode()->willRespondToMouseClickEvents() because |
| // that triggers a style recalc and can delete this. |
| if (getNode()->hasEventListeners(EventTypeNames::mouseup) || |
| getNode()->hasEventListeners(EventTypeNames::mousedown) || |
| getNode()->hasEventListeners(EventTypeNames::click) || |
| getNode()->hasEventListeners(EventTypeNames::DOMActivate)) |
| return true; |
| } |
| |
| return AXObject::isClickable(); |
| } |
| |
| bool AXNodeObject::isEnabled() const { |
| if (isDescendantOfDisabledNode()) |
| return false; |
| |
| Node* node = this->getNode(); |
| if (!node || !node->isElementNode()) |
| return true; |
| |
| return !toElement(node)->isDisabledFormControl(); |
| } |
| |
| AccessibilityExpanded AXNodeObject::isExpanded() const { |
| if (getNode() && isHTMLSummaryElement(*getNode())) { |
| if (getNode()->parentNode() && |
| isHTMLDetailsElement(getNode()->parentNode())) |
| return toElement(getNode()->parentNode())->hasAttribute(openAttr) |
| ? ExpandedExpanded |
| : ExpandedCollapsed; |
| } |
| |
| const AtomicString& expanded = getAttribute(aria_expandedAttr); |
| if (equalIgnoringCase(expanded, "true")) |
| return ExpandedExpanded; |
| if (equalIgnoringCase(expanded, "false")) |
| return ExpandedCollapsed; |
| |
| return ExpandedUndefined; |
| } |
| |
| bool AXNodeObject::isModal() const { |
| if (roleValue() != DialogRole && roleValue() != AlertDialogRole) |
| return false; |
| |
| if (hasAttribute(aria_modalAttr)) { |
| const AtomicString& modal = getAttribute(aria_modalAttr); |
| if (equalIgnoringCase(modal, "true")) |
| return true; |
| if (equalIgnoringCase(modal, "false")) |
| return false; |
| } |
| |
| if (getNode() && isHTMLDialogElement(*getNode())) |
| return toElement(getNode())->isInTopLayer(); |
| |
| return false; |
| } |
| |
| bool AXNodeObject::isPressed() const { |
| if (!isButton()) |
| return false; |
| |
| Node* node = this->getNode(); |
| if (!node) |
| return false; |
| |
| // ARIA button with aria-pressed not undefined, then check for aria-pressed |
| // attribute rather than getNode()->active() |
| if (ariaRoleAttribute() == ToggleButtonRole) { |
| if (equalIgnoringCase(getAttribute(aria_pressedAttr), "true") || |
| equalIgnoringCase(getAttribute(aria_pressedAttr), "mixed")) |
| return true; |
| return false; |
| } |
| |
| return node->isActive(); |
| } |
| |
| bool AXNodeObject::isReadOnly() const { |
| Node* node = this->getNode(); |
| if (!node) |
| return true; |
| |
| if (isHTMLTextAreaElement(*node)) |
| return toHTMLTextAreaElement(*node).isReadOnly(); |
| |
| if (isHTMLInputElement(*node)) { |
| HTMLInputElement& input = toHTMLInputElement(*node); |
| if (input.isTextField()) |
| return input.isReadOnly(); |
| } |
| |
| return !hasEditableStyle(*node); |
| } |
| |
| bool AXNodeObject::isRequired() const { |
| Node* n = this->getNode(); |
| if (n && (n->isElementNode() && toElement(n)->isFormControlElement()) && |
| hasAttribute(requiredAttr)) |
| return toHTMLFormControlElement(n)->isRequired(); |
| |
| if (equalIgnoringCase(getAttribute(aria_requiredAttr), "true")) |
| return true; |
| |
| return false; |
| } |
| |
| bool AXNodeObject::canSetFocusAttribute() const { |
| Node* node = getNode(); |
| if (!node) |
| return false; |
| |
| if (isWebArea()) |
| return true; |
| |
| // Children of elements with an aria-activedescendant attribute should be |
| // focusable if they have a (non-presentational) ARIA role. |
| if (!isPresentational() && ariaRoleAttribute() != UnknownRole && |
| ancestorExposesActiveDescendant()) |
| return true; |
| |
| // NOTE: It would be more accurate to ask the document whether |
| // setFocusedNode() would do anything. For example, setFocusedNode() will do |
| // nothing if the current focused node will not relinquish the focus. |
| if (isDisabledFormControl(node)) |
| return false; |
| |
| return node->isElementNode() && toElement(node)->supportsFocus(); |
| } |
| |
| bool AXNodeObject::canSetValueAttribute() const { |
| if (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::canSetSelectedAttribute() const { |
| // ARIA list box options can be selected if they are children of an element |
| // with an aria-activedescendant attribute. |
| if (ariaRoleAttribute() == ListBoxOptionRole && |
| ancestorExposesActiveDescendant()) |
| return true; |
| return AXObject::canSetSelectedAttribute(); |
| } |
| |
| bool AXNodeObject::canvasHasFallbackContent() const { |
| Node* node = this->getNode(); |
| if (!isHTMLCanvasElement(node)) |
| return false; |
| |
| // If it has any children that are elements, we'll assume it might be fallback |
| // content. If it has no children or its only children are not elements |
| // (e.g. just text nodes), it doesn't have fallback content. |
| return ElementTraversal::firstChild(*node); |
| } |
| |
| int AXNodeObject::headingLevel() const { |
| // headings can be in block flow and non-block flow |
| Node* node = this->getNode(); |
| if (!node) |
| return 0; |
| |
| if (roleValue() == HeadingRole) { |
| String levelStr = getAttribute(aria_levelAttr); |
| if (!levelStr.isEmpty()) { |
| int level = levelStr.toInt(); |
| if (level >= 1 && level <= 9) |
| return level; |
| return 1; |
| } |
| } |
| |
| if (!node->isHTMLElement()) |
| return 0; |
| |
| HTMLElement& element = toHTMLElement(*node); |
| if (element.hasTagName(h1Tag)) |
| return 1; |
| |
| if (element.hasTagName(h2Tag)) |
| return 2; |
| |
| if (element.hasTagName(h3Tag)) |
| return 3; |
| |
| if (element.hasTagName(h4Tag)) |
| return 4; |
| |
| if (element.hasTagName(h5Tag)) |
| return 5; |
| |
| if (element.hasTagName(h6Tag)) |
| return 6; |
| |
| return 0; |
| } |
| |
| unsigned AXNodeObject::hierarchicalLevel() const { |
| Node* node = this->getNode(); |
| if (!node || !node->isElementNode()) |
| return 0; |
| |
| Element* element = toElement(node); |
| String levelStr = element->getAttribute(aria_levelAttr); |
| if (!levelStr.isEmpty()) { |
| int level = levelStr.toInt(); |
| if (level > 0) |
| return level; |
| return 1; |
| } |
| |
| // 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(); |
| } |
| |
| void AXNodeObject::markers(Vector<DocumentMarker::MarkerType>& markerTypes, |
| Vector<AXRange>& markerRanges) const { |
| if (!getNode() || !getDocument() || !getDocument()->view()) |
| return; |
| |
| DocumentMarkerController& markerController = getDocument()->markers(); |
| DocumentMarkerVector markers = markerController.markersFor(getNode()); |
| for (size_t i = 0; i < markers.size(); ++i) { |
| DocumentMarker* marker = markers[i]; |
| switch (marker->type()) { |
| case DocumentMarker::Spelling: |
| case DocumentMarker::Grammar: |
| case DocumentMarker::TextMatch: |
| markerTypes.push_back(marker->type()); |
| markerRanges.push_back( |
| AXRange(marker->startOffset(), marker->endOffset())); |
| break; |
| case DocumentMarker::Composition: |
| // No need for accessibility to know about these marker types. |
| break; |
| } |
| } |
| } |
| |
| AXObject* AXNodeObject::inPageLinkTarget() const { |
| if (!m_node || !isHTMLAnchorElement(m_node) || !getDocument()) |
| return AXObject::inPageLinkTarget(); |
| |
| HTMLAnchorElement* anchor = toHTMLAnchorElement(m_node); |
| DCHECK(anchor); |
| KURL linkURL = anchor->href(); |
| if (!linkURL.isValid()) |
| return AXObject::inPageLinkTarget(); |
| String fragment = linkURL.fragmentIdentifier(); |
| if (fragment.isEmpty()) |
| return AXObject::inPageLinkTarget(); |
| |
| KURL documentURL = getDocument()->url(); |
| if (!documentURL.isValid() || |
| !equalIgnoringFragmentIdentifier(documentURL, linkURL)) { |
| return AXObject::inPageLinkTarget(); |
| } |
| |
| TreeScope& treeScope = anchor->treeScope(); |
| Element* target = treeScope.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& 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(); |
| } |
| } |
| |
| AXObject::AXObjectVector AXNodeObject::radioButtonsInGroup() const { |
| AXObjectVector radioButtons; |
| if (!m_node || roleValue() != RadioButtonRole) |
| return radioButtons; |
| |
| if (isHTMLInputElement(m_node)) { |
| HTMLInputElement* radioButton = toHTMLInputElement(m_node); |
| HeapVector<Member<HTMLInputElement>> htmlRadioButtons = |
| findAllRadioButtonsWithSameName(radioButton); |
| for (size_t i = 0; i < htmlRadioButtons.size(); ++i) { |
| AXObject* axRadioButton = |
| axObjectCache().getOrCreate(htmlRadioButtons[i]); |
| if (axRadioButton) |
| radioButtons.push_back(axRadioButton); |
| } |
| return radioButtons; |
| } |
| |
| // If the immediate parent is a radio group, return all its children that are |
| // radio buttons. |
| AXObject* parent = parentObject(); |
| if (parent && parent->roleValue() == RadioGroupRole) { |
| for (size_t i = 0; i < parent->children().size(); ++i) { |
| AXObject* child = parent->children()[i]; |
| DCHECK(child); |
| if (child->roleValue() == RadioButtonRole && |
| !child->accessibilityIsIgnored()) { |
| radioButtons.push_back(child); |
| } |
| } |
| } |
| |
| return radioButtons; |
| } |
| |
| // static |
| HeapVector<Member<HTMLInputElement>> |
| AXNodeObject::findAllRadioButtonsWithSameName(HTMLInputElement* radioButton) { |
| HeapVector<Member<HTMLInputElement>> allRadioButtons; |
| if (!radioButton || radioButton->type() != InputTypeNames::radio) |
| return allRadioButtons; |
| |
| constexpr bool kTraverseForward = true; |
| constexpr bool kTraverseBackward = false; |
| HTMLInputElement* firstRadioButton = radioButton; |
| do { |
| radioButton = RadioInputType::nextRadioButtonInGroup(firstRadioButton, |
| kTraverseBackward); |
| if (radioButton) |
| firstRadioButton = radioButton; |
| } while (radioButton); |
| |
| HTMLInputElement* nextRadioButton = firstRadioButton; |
| do { |
| allRadioButtons.push_back(nextRadioButton); |
| nextRadioButton = RadioInputType::nextRadioButtonInGroup(nextRadioButton, |
| kTraverseForward); |
| } while (nextRadioButton); |
| return allRadioButtons; |
| } |
| |
| String AXNodeObject::text() const { |
| // If this is a user defined static text, use the accessible name computation. |
| if (ariaRoleAttribute() == StaticTextRole) |
| return computedName(); |
| |
| if (!isTextControl()) |
| return String(); |
| |
| Node* node = this->getNode(); |
| if (!node) |
| return String(); |
| |
| if (isNativeTextControl() && |
| (isHTMLTextAreaElement(*node) || isHTMLInputElement(*node))) |
| return toTextControlElement(*node).value(); |
| |
| if (!node->isElementNode()) |
| return String(); |
| |
| return toElement(node)->innerText(); |
| } |
| |
| AccessibilityButtonState AXNodeObject::checkboxOrRadioValue() const { |
| if (isNativeCheckboxInMixedState()) |
| return ButtonStateMixed; |
| |
| if (isNativeCheckboxOrRadio()) |
| return isChecked() ? ButtonStateOn : ButtonStateOff; |
| |
| return AXObject::checkboxOrRadioValue(); |
| } |
| |
| RGBA32 AXNodeObject::colorValue() const { |
| if (!isHTMLInputElement(getNode()) || !isColorWell()) |
| return AXObject::colorValue(); |
| |
| HTMLInputElement* input = toHTMLInputElement(getNode()); |
| 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()); |
| DCHECK(success); |
| return color.rgb(); |
| } |
| |
| AriaCurrentState AXNodeObject::ariaCurrentState() const { |
| if (!hasAttribute(aria_currentAttr)) |
| return AXObject::ariaCurrentState(); |
| |
| const AtomicString& attributeValue = getAttribute(aria_currentAttr); |
| if (attributeValue.isEmpty() || equalIgnoringCase(attributeValue, "false")) |
| return AriaCurrentStateFalse; |
| if (equalIgnoringCase(attributeValue, "true")) |
| return AriaCurrentStateTrue; |
| if (equalIgnoringCase(attributeValue, "page")) |
| return AriaCurrentStatePage; |
| if (equalIgnoringCase(attributeValue, "step")) |
| return AriaCurrentStateStep; |
| if (equalIgnoringCase(attributeValue, "location")) |
| return AriaCurrentStateLocation; |
| if (equalIgnoringCase(attributeValue, "date")) |
| return AriaCurrentStateDate; |
| if (equalIgnoringCase(attributeValue, "time")) |
| return AriaCurrentStateTime; |
| // An unknown value should return true. |
| if (!attributeValue.isEmpty()) |
| return AriaCurrentStateTrue; |
| |
| return AXObject::ariaCurrentState(); |
| } |
| |
| InvalidState AXNodeObject::getInvalidState() 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 (getNode() && getNode()->isElementNode() && |
| toElement(getNode())->isFormControlElement()) { |
| HTMLFormControlElement* element = toHTMLFormControlElement(getNode()); |
| HeapVector<Member<HTMLFormControlElement>> invalidControls; |
| bool isInvalid = |
| !element->checkValidity(&invalidControls, CheckValidityDispatchNoEvent); |
| return isInvalid ? InvalidStateTrue : InvalidStateFalse; |
| } |
| |
| return AXObject::getInvalidState(); |
| } |
| |
| int AXNodeObject::posInSet() const { |
| if (supportsSetSizeAndPosInSet()) { |
| String posInSetStr = getAttribute(aria_posinsetAttr); |
| if (!posInSetStr.isEmpty()) { |
| int posInSet = posInSetStr.toInt(); |
| if (posInSet > 0) |
| return posInSet; |
| return 1; |
| } |
| |
| return AXObject::indexInParent() + 1; |
| } |
| |
| return 0; |
| } |
| |
| int AXNodeObject::setSize() const { |
| if (supportsSetSizeAndPosInSet()) { |
| String setSizeStr = getAttribute(aria_setsizeAttr); |
| if (!setSizeStr.isEmpty()) { |
| int setSize = setSizeStr.toInt(); |
| if (setSize > 0) |
| return setSize; |
| return 1; |
| } |
| |
| if (parentObject()) { |
| const auto& siblings = parentObject()->children(); |
| return siblings.size(); |
| } |
| } |
| |
| return 0; |
| } |
| |
| String AXNodeObject::ariaInvalidValue() const { |
| if (getInvalidState() == InvalidStateOther) |
| return getAttribute(aria_invalidAttr); |
| |
| return String(); |
| } |
| |
| String AXNodeObject::valueDescription() const { |
| if (!supportsRangeValue()) |
| return String(); |
| |
| return getAttribute(aria_valuetextAttr).getString(); |
| } |
| |
| float AXNodeObject::valueForRange() const { |
| if (hasAttribute(aria_valuenowAttr)) |
| return getAttribute(aria_valuenowAttr).toFloat(); |
| |
| if (isNativeSlider()) |
| return toHTMLInputElement(*getNode()).valueAsNumber(); |
| |
| if (isHTMLMeterElement(getNode())) |
| return toHTMLMeterElement(*getNode()).value(); |
| |
| return 0.0; |
| } |
| |
| float AXNodeObject::maxValueForRange() const { |
| if (hasAttribute(aria_valuemaxAttr)) |
| return getAttribute(aria_valuemaxAttr).toFloat(); |
| |
| if (isNativeSlider()) |
| return toHTMLInputElement(*getNode()).maximum(); |
| |
| if (isHTMLMeterElement(getNode())) |
| return toHTMLMeterElement(*getNode()).max(); |
| |
| return 0.0; |
| } |
| |
| float AXNodeObject::minValueForRange() const { |
| if (hasAttribute(aria_valueminAttr)) |
| return getAttribute(aria_valueminAttr).toFloat(); |
| |
| if (isNativeSlider()) |
| return toHTMLInputElement(*getNode()).minimum(); |
| |
| if (isHTMLMeterElement(getNode())) |
| return toHTMLMeterElement(*getNode()).min(); |
| |
| return 0.0; |
| } |
| |
| float AXNodeObject::stepValueForRange() const { |
| if (!isNativeSlider()) |
| return 0.0; |
| |
| Decimal step = |
| toHTMLInputElement(*getNode()).createStepRange(RejectAny).step(); |
| return step.toString().toFloat(); |
| } |
| |
| String AXNodeObject::stringValue() const { |
| Node* node = this->getNode(); |
| if (!node) |
| return String(); |
| |
| if (isHTMLSelectElement(*node)) { |
| HTMLSelectElement& selectElement = toHTMLSelectElement(*node); |
| int selectedIndex = selectElement.selectedIndex(); |
| const HeapVector<Member<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.isMultiple()) |
| return selectElement.value(); |
| return String(); |
| } |
| |
| if (isNativeTextControl()) |
| return text(); |
| |
| // 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 m_ariaRole; |
| } |
| |
| // 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; |
| } |
| |
| bool AXNodeObject::isNativeCheckboxInMixedState() const { |
| if (!isHTMLInputElement(m_node)) |
| return false; |
| |
| HTMLInputElement* input = toHTMLInputElement(m_node); |
| return input->type() == InputTypeNames::checkbox && |
| input->shouldAppearIndeterminate(); |
| } |
| |
| // |
| // New AX name calculation. |
| // |
| |
| String AXNodeObject::textAlternative(bool recursive, |
| bool inAriaLabelledByTraversal, |
| AXObjectSet& visited, |
| AXNameFrom& nameFrom, |
| AXRelatedObjectVector* relatedObjects, |
| NameSources* nameSources) const { |
| // If nameSources is non-null, relatedObjects is used in filling it in, so it |
| // must be non-null as well. |
| if (nameSources) |
| ASSERT(relatedObjects); |
| |
| bool foundTextAlternative = false; |
| |
| if (!getNode() && !getLayoutObject()) |
| return String(); |
| |
| String textAlternative = ariaTextAlternative( |
| recursive, inAriaLabelledByTraversal, visited, nameFrom, relatedObjects, |
| nameSources, &foundTextAlternative); |
| if (foundTextAlternative && !nameSources) |
| return textAlternative; |
| |
| // Step 2E from: http://www.w3.org/TR/accname-aam-1.1 |
| if (recursive && !inAriaLabelledByTraversal && isControl() && !isButton()) { |
| // No need to set any name source info in a recursive call. |
| if (isTextControl()) |
| return text(); |
| |
| if (isRange()) { |
| const AtomicString& ariaValuetext = getAttribute(aria_valuetextAttr); |
| if (!ariaValuetext.isNull()) |
| return ariaValuetext.getString(); |
| return String::number(valueForRange()); |
| } |
| |
| return stringValue(); |
| } |
| |
| // Step 2D from: http://www.w3.org/TR/accname-aam-1.1 |
| textAlternative = nativeTextAlternative(visited, nameFrom, relatedObjects, |
| nameSources, &foundTextAlternative); |
| if (!textAlternative.isEmpty() && !nameSources) |
| return textAlternative; |
| |
| // Step 2F / 2G from: http://www.w3.org/TR/accname-aam-1.1 |
| if (recursive || nameFromContents()) { |
| nameFrom = AXNameFromContents; |
| if (nameSources) { |
| nameSources->push_back(NameSource(foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| } |
| |
| Node* node = this->getNode(); |
| if (node && node->isTextNode()) |
| textAlternative = toText(node)->wholeText(); |
| else if (isHTMLBRElement(node)) |
| textAlternative = String("\n"); |
| else |
| textAlternative = textFromDescendants(visited, false); |
| |
| if (!textAlternative.isEmpty()) { |
| if (nameSources) { |
| foundTextAlternative = true; |
| nameSources->back().text = textAlternative; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| |
| // Step 2H from: http://www.w3.org/TR/accname-aam-1.1 |
| nameFrom = AXNameFromTitle; |
| if (nameSources) { |
| nameSources->push_back(NameSource(foundTextAlternative, titleAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| const AtomicString& title = getAttribute(titleAttr); |
| if (!title.isEmpty()) { |
| textAlternative = title; |
| if (nameSources) { |
| foundTextAlternative = true; |
| nameSources->back().text = textAlternative; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| nameFrom = AXNameFromUninitialized; |
| |
| if (nameSources && 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.relatedObjects.isEmpty()) |
| *relatedObjects = nameSource.relatedObjects; |
| return nameSource.text; |
| } |
| } |
| } |
| |
| return String(); |
| } |
| |
| String AXNodeObject::textFromDescendants(AXObjectSet& visited, |
| bool recursive) const { |
| if (!canHaveChildren() && recursive) |
| return String(); |
| |
| StringBuilder accumulatedText; |
| AXObject* previous = nullptr; |
| |
| AXObjectVector children; |
| |
| HeapVector<Member<AXObject>> ownedChildren; |
| computeAriaOwnsChildren(ownedChildren); |
| for (AXObject* obj = rawFirstChild(); obj; obj = obj->rawNextSibling()) { |
| if (!axObjectCache().isAriaOwned(obj)) |
| children.push_back(obj); |
| } |
| for (const auto& ownedChild : ownedChildren) |
| children.push_back(ownedChild); |
| |
| 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 (equalIgnoringCase(child->getAttribute(aria_hiddenAttr), "true")) |
| continue; |
| |
| // If we're going between two layoutObjects that are in separate |
| // LayoutBoxes, add whitespace if it wasn't there already. Intuitively if |
| // you have <span>Hello</span><span>World</span>, those are part of the same |
| // LayoutBox so we should return "HelloWorld", but given |
| // <div>Hello</div><div>World</div> the strings are in separate boxes so we |
| // should return "Hello World". |
| if (previous && accumulatedText.length() && |
| !isHTMLSpace(accumulatedText[accumulatedText.length() - 1])) { |
| if (!isInSameNonInlineBlockFlow(child->getLayoutObject(), |
| previous->getLayoutObject())) |
| accumulatedText.append(' '); |
| } |
| |
| String result; |
| if (child->isPresentational()) |
| result = child->textFromDescendants(visited, true); |
| else |
| result = recursiveTextAlternative(*child, false, visited); |
| accumulatedText.append(result); |
| previous = child; |
| } |
| |
| return accumulatedText.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& ariaLabel = |
| getAOMPropertyOrARIAAttribute(AOMStringProperty::kLabel); |
| if (!ariaLabel.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* htmlElement = nullptr; |
| if (getNode()->isHTMLElement()) |
| htmlElement = toHTMLElement(getNode()); |
| if (htmlElement && isLabelableElement(htmlElement)) { |
| if (toLabelableElement(htmlElement)->labels() && |
| toLabelableElement(htmlElement)->labels()->length() > 0) |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool AXNodeObject::nameFromContents() const { |
| Node* node = getNode(); |
| if (!node || !node->isElementNode()) |
| return AXObject::nameFromContents(); |
| // AXObject::nameFromContents determines whether an element should take its |
| // name from its descendant contents based on role. However, <select> is a |
| // special case, as unlike a typical pop-up button it contains its own pop-up |
| // menu's contents, which should not be used as the name. |
| if (isHTMLSelectElement(node)) |
| return false; |
| return AXObject::nameFromContents(); |
| } |
| |
| void AXNodeObject::getRelativeBounds(AXObject** outContainer, |
| FloatRect& outBoundsInContainer, |
| SkMatrix44& outContainerTransform) const { |
| if (layoutObjectForRelativeBounds()) { |
| AXObject::getRelativeBounds(outContainer, outBoundsInContainer, |
| outContainerTransform); |
| return; |
| } |
| |
| *outContainer = nullptr; |
| outBoundsInContainer = FloatRect(); |
| outContainerTransform.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 (!m_explicitElementRect.isEmpty()) { |
| *outContainer = axObjectCache().objectFromAXID(m_explicitContainerID); |
| if (*outContainer) { |
| outBoundsInContainer = FloatRect(m_explicitElementRect); |
| 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, outContainerTransform); |
| if (container) { |
| *outContainer = container; |
| rects.push_back(bounds); |
| } |
| } |
| } |
| } |
| |
| if (*outContainer) { |
| outBoundsInContainer = 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* positionProvider = parentObject(); positionProvider; |
| positionProvider = positionProvider->parentObject()) { |
| if (positionProvider->isAXLayoutObject()) { |
| positionProvider->getRelativeBounds(outContainer, outBoundsInContainer, |
| outContainerTransform); |
| if (*outContainer) |
| outBoundsInContainer.setSize( |
| FloatSize(outBoundsInContainer.width(), |
| std::min(10.0f, outBoundsInContainer.height()))); |
| break; |
| } |
| } |
| } |
| |
| 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 { |
| ASSERT(!isDetached()); |
| if (Node* parentNode = getParentNodeForComputeParent(getNode())) |
| return axObjectCache().getOrCreate(parentNode); |
| |
| return nullptr; |
| } |
| |
| AXObject* AXNodeObject::computeParentIfExists() const { |
| if (Node* parentNode = getParentNodeForComputeParent(getNode())) |
| return axObjectCache().get(parentNode); |
| |
| return nullptr; |
| } |
| |
| AXObject* AXNodeObject::rawFirstChild() const { |
| if (!getNode()) |
| return 0; |
| |
| Node* firstChild = getNode()->firstChild(); |
| |
| if (!firstChild) |
| return 0; |
| |
| return axObjectCache().getOrCreate(firstChild); |
| } |
| |
| AXObject* AXNodeObject::rawNextSibling() const { |
| if (!getNode()) |
| return 0; |
| |
| Node* nextSibling = getNode()->nextSibling(); |
| if (!nextSibling) |
| return 0; |
| |
| return axObjectCache().getOrCreate(nextSibling); |
| } |
| |
| void AXNodeObject::addChildren() { |
| ASSERT(!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. |
| 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 (getLayoutObject() && !isHTMLCanvasElement(*m_node)) |
| return; |
| |
| HeapVector<Member<AXObject>> ownedChildren; |
| computeAriaOwnsChildren(ownedChildren); |
| |
| for (Node& child : NodeTraversal::childrenOf(*m_node)) { |
| AXObject* childObj = axObjectCache().getOrCreate(&child); |
| if (childObj && !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 (!getNode() && !isAXLayoutObject()) |
| return false; |
| |
| if (getNode() && isHTMLMapElement(getNode())) |
| return false; |
| |
| AccessibilityRole role = roleValue(); |
| |
| // If an element has an ARIA role of presentation, we need to consider the |
| // native role when deciding whether it can have children or not - otherwise |
| // giving something a role of presentation could expose inner implementation |
| // details. |
| if (isPresentational()) |
| role = nativeAccessibilityRoleIgnoringAria(); |
| |
| switch (role) { |
| case 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->getNode(); |
| if (!node) |
| return 0; |
| |
| if (isHTMLInputElement(*node)) { |
| HTMLInputElement& input = toHTMLInputElement(*node); |
| if (!input.isDisabledFormControl() && |
| (isCheckboxOrRadio() || input.isTextButton() || |
| input.type() == InputTypeNames::file)) |
| return &input; |
| } else if (isHTMLButtonElement(*node)) { |
| return toElement(node); |
| } |
| |
| if (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: |
| return toElement(node); |
| default: |
| break; |
| } |
| |
| Element* anchor = anchorElement(); |
| Element* clickElement = mouseButtonListener(); |
| if (!anchor || (clickElement && clickElement->isDescendantOf(anchor))) |
| return clickElement; |
| 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->layoutObject() && |
| cache.getOrCreate(node->layoutObject())->isAnchor())) |
| return toElement(node); |
| } |
| |
| return 0; |
| } |
| |
| Document* AXNodeObject::getDocument() const { |
| if (!getNode()) |
| return 0; |
| return &getNode()->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 (!getNode()) |
| return 0; |
| |
| // the control element should not be considered part of the label |
| if (isControl()) |
| return 0; |
| |
| // the link element should not be considered part of the label |
| if (isLink()) |
| return 0; |
| |
| // find if this has a ancestor that is a label |
| return Traversal<HTMLLabelElement>::firstAncestorOrSelf(*getNode()); |
| } |
| |
| void AXNodeObject::setFocused(bool on) { |
| if (!canSetFocusAttribute()) |
| return; |
| |
| Document* document = this->getDocument(); |
| if (!on) { |
| document->clearFocusedElement(); |
| } else { |
| Node* node = this->getNode(); |
| if (node && node->isElementNode()) { |
| // If this node is already the currently focused node, then calling |
| // focus() won't do anything. That is a problem when focus is removed |
| // from the webpage to chrome, and then returns. In these cases, we need |
| // to do what keyboard and mouse focus do, which is reset focus first. |
| if (document->focusedElement() == node) |
| document->clearFocusedElement(); |
| |
| toElement(node)->focus(); |
| } else { |
| document->clearFocusedElement(); |
| } |
| } |
| } |
| |
| void AXNodeObject::increment() { |
| UserGestureIndicator gestureIndicator(DocumentUserGestureToken::create( |
| getDocument(), UserGestureToken::NewGesture)); |
| alterSliderValue(true); |
| } |
| |
| void AXNodeObject::decrement() { |
| UserGestureIndicator gestureIndicator(DocumentUserGestureToken::create( |
| getDocument(), UserGestureToken::NewGesture)); |
| alterSliderValue(false); |
| } |
| |
| void AXNodeObject::setSequentialFocusNavigationStartingPoint() { |
| if (!getNode()) |
| return; |
| |
| getNode()->document().clearFocusedElement(); |
| getNode()->document().setSequentialFocusNavigationStartingPoint(getNode()); |
| } |
| |
| void AXNodeObject::childrenChanged() { |
| // This method is meant as a quick way of marking a portion of the |
| // accessibility tree dirty. |
| if (!getNode() && !getLayoutObject()) |
| return; |
| |
| // If this is not part of the accessibility tree because an ancestor |
| // has only presentational children, invalidate this object's children but |
| // skip sending a notification and skip walking up the ancestors. |
| if (ancestorForWhichThisIsAPresentationalChild()) { |
| setNeedsToUpdateChildren(); |
| return; |
| } |
| |
| axObjectCache().postNotification(this, AXObjectCacheImpl::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); |
| if (getDocument()) { |
| AXObject* documentObject = axObjectCache().getOrCreate(getDocument()); |
| axObjectCache().postNotification( |
| documentObject, AXObjectCacheImpl::AXDocumentSelectionChanged); |
| } |
| } 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 = getNode(); 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) const { |
| if (!hasAttribute(aria_ownsAttr)) |
| return; |
| |
| Vector<String> idVector; |
| if (canHaveChildren() && !isNativeTextControl() && |
| !hasContentEditableAttributeSet()) |
| tokenVectorFromAttribute(idVector, aria_ownsAttr); |
| |
| axObjectCache().updateAriaOwns(this, idVector, ownedChildren); |
| } |
| |
| // 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, |
| AXRelatedObjectVector* relatedObjects, |
| NameSources* nameSources, |
| bool* foundTextAlternative) 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 (nameSources) |
| ASSERT(relatedObjects); |
| |
| String textAlternative; |
| AXRelatedObjectVector localRelatedObjects; |
| |
| const HTMLInputElement* inputElement = nullptr; |
| if (isHTMLInputElement(getNode())) |
| inputElement = toHTMLInputElement(getNode()); |
| |
| // 5.1/5.5 Text inputs, Other labelable Elements |
| // If you change this logic, update AXNodeObject::nameFromLabelElement, too. |
| HTMLElement* htmlElement = nullptr; |
| if (getNode()->isHTMLElement()) |
| htmlElement = toHTMLElement(getNode()); |
| |
| if (htmlElement && htmlElement->isLabelable()) { |
| nameFrom = AXNameFromRelatedElement; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLLabel; |
| } |
| |
| LabelsNodeList* labels = toLabelableElement(htmlElement)->labels(); |
| if (labels && labels->length() > 0) { |
| HeapVector<Member<Element>> labelElements; |
| for (unsigned labelIndex = 0; labelIndex < labels->length(); |
| ++labelIndex) { |
| Element* label = labels->item(labelIndex); |
| if (nameSources) { |
| if (!label->getAttribute(forAttr).isEmpty() && |
| label->getAttribute(forAttr) == htmlElement->getIdAttribute()) { |
| nameSources->back().nativeSource = AXTextFromNativeHTMLLabelFor; |
| } else { |
| nameSources->back().nativeSource = AXTextFromNativeHTMLLabelWrapped; |
| } |
| } |
| labelElements.push_back(label); |
| } |
| |
| textAlternative = |
| textFromElements(false, visited, labelElements, relatedObjects); |
| if (!textAlternative.isNull()) { |
| *foundTextAlternative = true; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = textAlternative; |
| } else { |
| return textAlternative; |
| } |
| } else if (nameSources) { |
| nameSources->back().invalid = true; |
| } |
| } |
| } |
| |
| // 5.2 input type="button", input type="submit" and input type="reset" |
| if (inputElement && inputElement->isTextButton()) { |
| // value attribue |
| nameFrom = AXNameFromValue; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative, valueAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| String value = inputElement->value(); |
| if (!value.isNull()) { |
| textAlternative = value; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| // 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 defaultLabel = inputElement->valueOrDefaultLabel(); |
| if (value.isNull() && !defaultLabel.isNull()) { |
| // default label |
| nameFrom = AXNameFromContents; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| } |
| textAlternative = defaultLabel; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| 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->push_back(NameSource(*foundTextAlternative, altAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| const AtomicString& alt = inputElement->getAttribute(altAttr); |
| if (!alt.isNull()) { |
| textAlternative = alt; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.attributeValue = alt; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| // value attr |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative, valueAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| nameFrom = AXNameFromAttribute; |
| String value = inputElement->value(); |
| if (!value.isNull()) { |
| textAlternative = value; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| // localised default value ("Submit") |
| nameFrom = AXNameFromValue; |
| textAlternative = inputElement->locale().queryString( |
| WebLocalizedString::SubmitButtonDefaultLabel); |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative, typeAttr)); |
| NameSource& source = nameSources->back(); |
| 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->isTextControl()) { |
| nameFrom = AXNameFromPlaceholder; |
| if (nameSources) { |
| nameSources->push_back( |
| NameSource(*foundTextAlternative, placeholderAttr)); |
| NameSource& source = nameSources->back(); |
| source.type = nameFrom; |
| } |
| const String placeholder = placeholderFromNativeAttribute(); |
| if (!placeholder.isEmpty()) { |
| textAlternative = placeholder; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| source.attributeValue = htmlElement->fastGetAttribute(placeholderAttr); |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| // Also check for aria-placeholder. |
| nameFrom = AXNameFromPlaceholder; |
| if (nameSources) { |
| nameSources->push_back( |
| NameSource(*foundTextAlternative, aria_placeholderAttr)); |
| NameSource& source = nameSources->back(); |
| source.type = nameFrom; |
| } |
| const AtomicString& ariaPlaceholder = |
| htmlElement->fastGetAttribute(aria_placeholderAttr); |
| if (!ariaPlaceholder.isEmpty()) { |
| textAlternative = ariaPlaceholder; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| source.attributeValue = ariaPlaceholder; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| return textAlternative; |
| } |
| |
| // 5.7 figure and figcaption Elements |
| if (getNode()->hasTagName(figureTag)) { |
| // figcaption |
| nameFrom = AXNameFromRelatedElement; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLFigcaption; |
| } |
| Element* figcaption = nullptr; |
| for (Element& element : ElementTraversal::descendantsOf(*(getNode()))) { |
| if (element.hasTagName(figcaptionTag)) { |
| figcaption = &element; |
| break; |
| } |
| } |
| if (figcaption) { |
| AXObject* figcaptionAXObject = axObjectCache().getOrCreate(figcaption); |
| if (figcaptionAXObject) { |
| textAlternative = |
| recursiveTextAlternative(*figcaptionAXObject, false, visited); |
| |
| if (relatedObjects) { |
| localRelatedObjects.push_back( |
| new NameSourceRelatedObject(figcaptionAXObject, textAlternative)); |
| *relatedObjects = localRelatedObjects; |
| localRelatedObjects.clear(); |
| } |
| |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| return textAlternative; |
| } |
| |
| // 5.8 img or area Element |
| if (isHTMLImageElement(getNode()) || isHTMLAreaElement(getNode()) || |
| (getLayoutObject() && getLayoutObject()->isSVGImage())) { |
| // alt |
| nameFrom = AXNameFromAttribute; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative, altAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| const AtomicString& alt = getAttribute(altAttr); |
| if (!alt.isNull()) { |
| textAlternative = alt; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.attributeValue = alt; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| return textAlternative; |
| } |
| |
| // 5.9 table Element |
| if (isHTMLTableElement(getNode())) { |
| HTMLTableElement* tableElement = toHTMLTableElement(getNode()); |
| |
| // caption |
| nameFrom = AXNameFromCaption; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLTableCaption; |
| } |
| HTMLTableCaptionElement* caption = tableElement->caption(); |
| if (caption) { |
| AXObject* captionAXObject = axObjectCache().getOrCreate(caption); |
| if (captionAXObject) { |
| textAlternative = |
| recursiveTextAlternative(*captionAXObject, false, visited); |
| if (relatedObjects) { |
| localRelatedObjects.push_back( |
| new NameSourceRelatedObject(captionAXObject, textAlternative)); |
| *relatedObjects = localRelatedObjects; |
| localRelatedObjects.clear(); |
| } |
| |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| |
| // summary |
| nameFrom = AXNameFromAttribute; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative, summaryAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| const AtomicString& summary = getAttribute(summaryAttr); |
| if (!summary.isNull()) { |
| textAlternative = summary; |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.attributeValue = summary; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| |
| return textAlternative; |
| } |
| |
| // Per SVG AAM 1.0's modifications to 2D of this algorithm. |
| if (getNode()->isSVGElement()) { |
| nameFrom = AXNameFromRelatedElement; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLTitleElement; |
| } |
| ASSERT(getNode()->isContainerNode()); |
| Element* title = ElementTraversal::firstChild( |
| toContainerNode(*(getNode())), HasTagName(SVGNames::titleTag)); |
| |
| if (title) { |
| AXObject* titleAXObject = axObjectCache().getOrCreate(title); |
| if (titleAXObject && !visited.contains(titleAXObject)) { |
| textAlternative = |
| recursiveTextAlternative(*titleAXObject, false, visited); |
| if (relatedObjects) { |
| localRelatedObjects.push_back( |
| new NameSourceRelatedObject(titleAXObject, textAlternative)); |
| *relatedObjects = localRelatedObjects; |
| localRelatedObjects.clear(); |
| } |
| } |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| source.relatedObjects = *relatedObjects; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| |
| // Fieldset / legend. |
| if (isHTMLFieldSetElement(getNode())) { |
| nameFrom = AXNameFromRelatedElement; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLLegend; |
| } |
| HTMLElement* legend = toHTMLFieldSetElement(getNode())->legend(); |
| if (legend) { |
| AXObject* legendAXObject = axObjectCache().getOrCreate(legend); |
| // Avoid an infinite loop |
| if (legendAXObject && !visited.contains(legendAXObject)) { |
| textAlternative = |
| recursiveTextAlternative(*legendAXObject, false, visited); |
| |
| if (relatedObjects) { |
| localRelatedObjects.push_back( |
| new NameSourceRelatedObject(legendAXObject, textAlternative)); |
| *relatedObjects = localRelatedObjects; |
| localRelatedObjects.clear(); |
| } |
| |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| } |
| |
| // Document. |
| if (isWebArea()) { |
| Document* document = this->getDocument(); |
| if (document) { |
| nameFrom = AXNameFromAttribute; |
| if (nameSources) { |
| nameSources->push_back( |
| NameSource(foundTextAlternative, aria_labelAttr)); |
| nameSources->back().type = nameFrom; |
| } |
| if (Element* documentElement = document->documentElement()) { |
| const AtomicString& ariaLabel = AccessibleNode::getProperty( |
| documentElement, AOMStringProperty::kLabel); |
| if (!ariaLabel.isEmpty()) { |
| textAlternative = ariaLabel; |
| |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.text = textAlternative; |
| source.attributeValue = ariaLabel; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| |
| nameFrom = AXNameFromRelatedElement; |
| if (nameSources) { |
| nameSources->push_back(NameSource(*foundTextAlternative)); |
| nameSources->back().type = nameFrom; |
| nameSources->back().nativeSource = AXTextFromNativeHTMLTitleElement; |
| } |
| |
| textAlternative = document->title(); |
| |
| Element* titleElement = document->titleElement(); |
| AXObject* titleAXObject = axObjectCache().getOrCreate(titleElement); |
| if (titleAXObject) { |
| if (relatedObjects) { |
| localRelatedObjects.push_back( |
| new NameSourceRelatedObject(titleAXObject, textAlternative)); |
| *relatedObjects = localRelatedObjects; |
| localRelatedObjects.clear(); |
| } |
| |
| if (nameSources) { |
| NameSource& source = nameSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = textAlternative; |
| *foundTextAlternative = true; |
| } else { |
| return textAlternative; |
| } |
| } |
| } |
| } |
| |
| return textAlternative; |
| } |
| |
| String AXNodeObject::description(AXNameFrom nameFrom, |
| AXDescriptionFrom& descriptionFrom, |
| AXObjectVector* descriptionObjects) const { |
| AXRelatedObjectVector relatedObjects; |
| String result = |
| description(nameFrom, descriptionFrom, nullptr, &relatedObjects); |
| if (descriptionObjects) { |
| descriptionObjects->clear(); |
| for (size_t i = 0; i < relatedObjects.size(); i++) |
| descriptionObjects->push_back(relatedObjects[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 nameFrom, |
| AXDescriptionFrom& descriptionFrom, |
| DescriptionSources* descriptionSources, |
| AXRelatedObjectVector* relatedObjects) const { |
| // If descriptionSources is non-null, relatedObjects is used in filling it in, |
| // so it must be non-null as well. |
| if (descriptionSources) |
| ASSERT(relatedObjects); |
| |
| if (!getNode()) |
| return String(); |
| |
| String description; |
| bool foundDescription = false; |
| |
| descriptionFrom = AXDescriptionFromRelatedElement; |
| if (descriptionSources) { |
| descriptionSources->push_back( |
| DescriptionSource(foundDescription, aria_describedbyAttr)); |
| descriptionSources->back().type = descriptionFrom; |
| } |
| |
| // aria-describedby overrides any other accessible description, from: |
| // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html |
| const AtomicString& ariaDescribedby = getAttribute(aria_describedbyAttr); |
| if (!ariaDescribedby.isNull()) { |
| if (descriptionSources) |
| descriptionSources->back().attributeValue = ariaDescribedby; |
| |
| description = textFromAriaDescribedby(relatedObjects); |
| |
| if (!description.isNull()) { |
| if (descriptionSources) { |
| DescriptionSource& source = descriptionSources->back(); |
| source.type = descriptionFrom; |
| source.relatedObjects = *relatedObjects; |
| source.text = description; |
| foundDescription = true; |
| } else { |
| return description; |
| } |
| } else if (descriptionSources) { |
| descriptionSources->back().invalid = true; |
| } |
| } |
| |
| const HTMLInputElement* inputElement = nullptr; |
| if (isHTMLInputElement(getNode())) |
| inputElement = toHTMLInputElement(getNode()); |
| |
| // value, 5.2.2 from: http://rawgit.com/w3c/aria/master/html-aam/html-aam.html |
| if (nameFrom != AXNameFromValue && inputElement && |
| inputElement->isTextButton()) { |
| descriptionFrom = AXDescriptionFromAttribute; |
| if (descriptionSources) { |
| descriptionSources->push_back( |
| DescriptionSource(foundDescription, valueAttr)); |
| descriptionSources->back().type = descriptionFrom; |
| } |
| String value = inputElement->value(); |
| if (!value.isNull()) { |
| description = value; |
| if (descriptionSources) { |
| DescriptionSource& source = descriptionSources->back(); |
| source.text = description; |
| foundDescription = true; |
| } else { |
| return description; |
| } |
| } |
| } |
| |
| // table caption, 5.9.2 from: |
| // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html |
| if (nameFrom != AXNameFromCaption && isHTMLTableElement(getNode())) { |
| HTMLTableElement* tableElement = toHTMLTableElement(getNode()); |
| |
| descriptionFrom = AXDescriptionFromRelatedElement; |
| if (descriptionSources) { |
| descriptionSources->push_back(DescriptionSource(foundDescription)); |
| descriptionSources->back().type = descriptionFrom; |
| descriptionSources->back().nativeSource = |
| AXTextFromNativeHTMLTableCaption; |
| } |
| HTMLTableCaptionElement* caption = tableElement->caption(); |
| if (caption) { |
| AXObject* captionAXObject = axObjectCache().getOrCreate(caption); |
| if (captionAXObject) { |
| AXObjectSet visited; |
| description = |
| recursiveTextAlternative(*captionAXObject, false, visited); |
| if (relatedObjects) |
| relatedObjects->push_back( |
| new NameSourceRelatedObject(captionAXObject, description)); |
| |
| if (descriptionSources) { |
| DescriptionSource& source = descriptionSources->back(); |
| source.relatedObjects = *relatedObjects; |
| source.text = description; |
| foundDescription = true; |
| } else { |
| return description; |
| } |
| } |
| } |
| } |
| |
| // summary, 5.6.2 from: |
| // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html |
| if (nameFrom != AXNameFromContents && isHTMLSummaryElement(getNode())) { |
| descriptionFrom = AXDescriptionFromContents; |
| if (descriptionSources) { |
| descriptionSources->push_back(DescriptionSource(foundDescription)); |
| descriptionSources->back().type = descriptionFrom; |
| } |
| |
| AXObjectSet visited; |
| description = textFromDescendants(visited, false); |
| |
| if (!description.isEmpty()) { |
| if (descriptionSources) { |
| foundDescription = true; |
| descriptionSources->back().text = description; |
| } else { |
| return description; |
| } |
| } |
| } |
| |
| // title attribute, from: |
| // http://rawgit.com/w3c/aria/master/html-aam/html-aam.html |
| if (nameFrom != AXNameFromTitle) { |
| descriptionFrom = AXDescriptionFromAttribute; |
| if (descriptionSources) { |
| descriptionSources->push_back( |
| DescriptionSource(foundDescription, titleAttr)); |
| descriptionSources->back().type = descriptionFrom; |
| } |
| const AtomicString& title = getAttribute(titleAttr); |
| if (!title.isEmpty()) { |
| description = title; |
| if (descriptionSources) { |
| foundDescription = true; |
| descriptionSources->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. |
| descriptionFrom = AXDescriptionFromAttribute; |
| if (descriptionSources) { |
| descriptionSources->push_back( |
| DescriptionSource(foundDescription, aria_helpAttr)); |
| descriptionSources->back().type = descriptionFrom; |
| } |
| const AtomicString& help = getAttribute(aria_helpAttr); |
| if (!help.isEmpty()) { |
| description = help; |
| if (descriptionSources) { |
| foundDescription = true; |
| descriptionSources->back().text = description; |
| } else { |
| return description; |
| } |
| } |
| |
| descriptionFrom = AXDescriptionFromUninitialized; |
| |
| if (foundDescription) { |
| for (size_t i = 0; i < descriptionSources->size(); ++i) { |
| if (!(*descriptionSources)[i].text.isNull() && |
| !(*descriptionSources)[i].superseded) { |
| DescriptionSource& descriptionSource = (*descriptionSources)[i]; |
| descriptionFrom = descriptionSource.type; |
| if (!descriptionSource.relatedObjects.isEmpty()) |
| *relatedObjects = descriptionSource.relatedObjects; |
| return descriptionSource.text; |
| } |
| } |
| } |
| |
| return String(); |
| } |
| |
| String AXNodeObject::placeholder(AXNameFrom nameFrom) const { |
| if (nameFrom == AXNameFromPlaceholder) |
| return String(); |
| |
| Node* node = getNode(); |
| if (!node || !node->isHTMLElement()) |
| return String(); |
| |
| String nativePlaceholder = placeholderFromNativeAttribute(); |
| if (!nativePlaceholder.isEmpty()) |
| return nativePlaceholder; |
| |
| const AtomicString& ariaPlaceholder = |
| toHTMLElement(node)->fastGetAttribute(aria_placeholderAttr); |
| if (!ariaPlaceholder.isEmpty()) |
| return ariaPlaceholder; |
| |
| return String(); |
| } |
| |
| String AXNodeObject::placeholderFromNativeAttribute() const { |
| Node* node = getNode(); |
| if (!node || !isTextControlElement(node)) |
| return String(); |
| return toTextControlElement(node)->strippedPlaceholder(); |
| } |
| |
| DEFINE_TRACE(AXNodeObject) { |
| visitor->trace(m_node); |
| AXObject::trace(visitor); |
| } |
| |
| } // namespace blink |