diff options
author | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
---|---|---|
committer | Lorry Tar Creator <lorry-tar-importer@lorry> | 2017-06-27 06:07:23 +0000 |
commit | 1bf1084f2b10c3b47fd1a588d85d21ed0eb41d0c (patch) | |
tree | 46dcd36c86e7fbc6e5df36deb463b33e9967a6f7 /Source/WebCore/accessibility/AccessibilityNodeObject.cpp | |
parent | 32761a6cee1d0dee366b885b7b9c777e67885688 (diff) | |
download | WebKitGtk-tarball-master.tar.gz |
webkitgtk-2.16.5HEADwebkitgtk-2.16.5master
Diffstat (limited to 'Source/WebCore/accessibility/AccessibilityNodeObject.cpp')
-rw-r--r-- | Source/WebCore/accessibility/AccessibilityNodeObject.cpp | 856 |
1 files changed, 501 insertions, 355 deletions
diff --git a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp index 8969dba6f..4bce0b09c 100644 --- a/Source/WebCore/accessibility/AccessibilityNodeObject.cpp +++ b/Source/WebCore/accessibility/AccessibilityNodeObject.cpp @@ -10,7 +10,7 @@ * 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 +* 3. Neither the name of Apple 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. * @@ -31,6 +31,7 @@ #include "AXObjectCache.h" #include "AccessibilityImageMapLink.h" +#include "AccessibilityList.h" #include "AccessibilityListBox.h" #include "AccessibilitySpinButton.h" #include "AccessibilityTable.h" @@ -41,40 +42,31 @@ #include "FrameLoader.h" #include "FrameSelection.h" #include "FrameView.h" -#include "HTMLAreaElement.h" +#include "HTMLCanvasElement.h" +#include "HTMLDetailsElement.h" #include "HTMLFieldSetElement.h" #include "HTMLFormElement.h" -#include "HTMLFrameElementBase.h" #include "HTMLImageElement.h" #include "HTMLInputElement.h" #include "HTMLLabelElement.h" #include "HTMLLegendElement.h" -#include "HTMLMapElement.h" #include "HTMLNames.h" -#include "HTMLOptGroupElement.h" -#include "HTMLOptionElement.h" -#include "HTMLOptionsCollection.h" #include "HTMLParserIdioms.h" -#include "HTMLPlugInImageElement.h" #include "HTMLSelectElement.h" #include "HTMLTextAreaElement.h" #include "HTMLTextFormControlElement.h" -#include "HitTestRequest.h" -#include "HitTestResult.h" #include "LabelableElement.h" #include "LocalizedStrings.h" +#include "MathMLElement.h" #include "MathMLNames.h" #include "NodeList.h" #include "NodeTraversal.h" -#include "Page.h" #include "ProgressTracker.h" #include "RenderImage.h" #include "RenderView.h" #include "SVGElement.h" -#include "SVGNames.h" #include "Text.h" #include "TextControlInnerElements.h" -#include "TextIterator.h" #include "UserGestureIndicator.h" #include "VisibleUnits.h" #include "Widget.h" @@ -87,7 +79,7 @@ namespace WebCore { using namespace HTMLNames; -static String accessibleNameForNode(Node*); +static String accessibleNameForNode(Node* node, Node* labelledbyNode = nullptr); AccessibilityNodeObject::AccessibilityNodeObject(Node* node) : AccessibilityObject() @@ -115,16 +107,16 @@ void AccessibilityNodeObject::init() m_role = determineAccessibilityRole(); } -PassRefPtr<AccessibilityNodeObject> AccessibilityNodeObject::create(Node* node) +Ref<AccessibilityNodeObject> AccessibilityNodeObject::create(Node* node) { - return adoptRef(new AccessibilityNodeObject(node)); + return adoptRef(*new AccessibilityNodeObject(node)); } void AccessibilityNodeObject::detach(AccessibilityDetachmentType detachmentType, AXObjectCache* cache) { // AccessibilityObject calls clearChildren. AccessibilityObject::detach(detachmentType, cache); - m_node = 0; + m_node = nullptr; } void AccessibilityNodeObject::childrenChanged() @@ -149,11 +141,14 @@ void AccessibilityNodeObject::childrenChanged() // 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. + // Sometimes this function can be called many times within a short period of time, leading to posting too many AXLiveRegionChanged + // notifications. To fix this, we used a timer to make sure we only post one notification for the children changes within a pre-defined + // time interval. if (parent->supportsARIALiveRegion()) - cache->postNotification(parent, parent->document(), AXObjectCache::AXLiveRegionChanged); + cache->postLiveRegionChangeNotification(parent); // If this element is an ARIA text control, notify the AT of changes. - if (parent->isARIATextControl() && !parent->isNativeTextControl() && !parent->node()->hasEditableStyle()) + if (parent->isNonNativeTextControl()) cache->postNotification(parent, parent->document(), AXObjectCache::AXValueChanged); } } @@ -171,12 +166,12 @@ void AccessibilityNodeObject::updateAccessibilityRole() AccessibilityObject* AccessibilityNodeObject::firstChild() const { if (!node()) - return 0; + return nullptr; Node* firstChild = node()->firstChild(); if (!firstChild) - return 0; + return nullptr; return axObjectCache()->getOrCreate(firstChild); } @@ -184,11 +179,11 @@ AccessibilityObject* AccessibilityNodeObject::firstChild() const AccessibilityObject* AccessibilityNodeObject::lastChild() const { if (!node()) - return 0; + return nullptr; Node* lastChild = node()->lastChild(); if (!lastChild) - return 0; + return nullptr; return axObjectCache()->getOrCreate(lastChild); } @@ -196,11 +191,11 @@ AccessibilityObject* AccessibilityNodeObject::lastChild() const AccessibilityObject* AccessibilityNodeObject::previousSibling() const { if (!node()) - return 0; + return nullptr; Node* previousSibling = node()->previousSibling(); if (!previousSibling) - return 0; + return nullptr; return axObjectCache()->getOrCreate(previousSibling); } @@ -208,11 +203,11 @@ AccessibilityObject* AccessibilityNodeObject::previousSibling() const AccessibilityObject* AccessibilityNodeObject::nextSibling() const { if (!node()) - return 0; + return nullptr; Node* nextSibling = node()->nextSibling(); if (!nextSibling) - return 0; + return nullptr; return axObjectCache()->getOrCreate(nextSibling); } @@ -225,20 +220,23 @@ AccessibilityObject* AccessibilityNodeObject::parentObjectIfExists() const AccessibilityObject* AccessibilityNodeObject::parentObject() const { if (!node()) - return 0; + return nullptr; Node* parentObj = node()->parentNode(); - if (parentObj) - return axObjectCache()->getOrCreate(parentObj); + if (!parentObj) + return nullptr; - return 0; + if (AXObjectCache* cache = axObjectCache()) + return cache->getOrCreate(parentObj); + + return nullptr; } LayoutRect AccessibilityNodeObject::elementRect() const { return boundingBoxRect(); } - + LayoutRect AccessibilityNodeObject::boundingBoxRect() const { // AccessibilityNodeObjects have no mechanism yet to return a size or position. @@ -267,7 +265,7 @@ void AccessibilityNodeObject::setNode(Node* node) Document* AccessibilityNodeObject::document() const { if (!node()) - return 0; + return nullptr; return &node()->document(); } @@ -276,52 +274,52 @@ AccessibilityRole AccessibilityNodeObject::determineAccessibilityRole() if (!node()) return UnknownRole; - m_ariaRole = determineAriaRoleAttribute(); + if ((m_ariaRole = determineAriaRoleAttribute()) != UnknownRole) + return m_ariaRole; - AccessibilityRole ariaRole = ariaRoleAttribute(); - if (ariaRole != UnknownRole) - return ariaRole; - if (node()->isLink()) return WebCoreLinkRole; if (node()->isTextNode()) return StaticTextRole; if (node()->hasTagName(buttonTag)) return buttonRoleType(); - if (isHTMLInputElement(node())) { - HTMLInputElement* input = toHTMLInputElement(node()); - if (input->isCheckbox()) + if (is<HTMLInputElement>(*node())) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node()); + if (input.isCheckbox()) return CheckBoxRole; - if (input->isRadioButton()) + if (input.isRadioButton()) return RadioButtonRole; - if (input->isTextButton()) + if (input.isTextButton()) return buttonRoleType(); - if (input->isRangeControl()) + if (input.isRangeControl()) return SliderRole; - + if (input.isInputTypeHidden()) + return IgnoredRole; + if (input.isSearchField()) + return SearchFieldRole; #if ENABLE(INPUT_TYPE_COLOR) - const AtomicString& type = input->getAttribute(typeAttr); - if (equalIgnoringCase(type, "color")) + if (input.isColorControl()) return ColorWellRole; #endif - return TextFieldRole; } if (node()->hasTagName(selectTag)) { - HTMLSelectElement* selectElement = toHTMLSelectElement(node()); - return selectElement->multiple() ? ListBoxRole : PopUpButtonRole; + HTMLSelectElement& selectElement = downcast<HTMLSelectElement>(*node()); + return selectElement.multiple() ? ListBoxRole : PopUpButtonRole; } - if (isHTMLTextAreaElement(node())) + if (is<HTMLTextAreaElement>(*node())) return TextAreaRole; if (headingLevel()) return HeadingRole; + if (node()->hasTagName(blockquoteTag)) + return BlockquoteRole; if (node()->hasTagName(divTag)) return DivRole; if (node()->hasTagName(pTag)) return ParagraphRole; - if (isHTMLLabelElement(node())) + if (is<HTMLLabelElement>(*node())) return LabelRole; - if (node()->isElementNode() && toElement(node())->isFocusable()) + if (is<Element>(*node()) && downcast<Element>(*node()).isFocusable()) return GroupRole; return UnknownRole; @@ -397,11 +395,8 @@ bool AccessibilityNodeObject::canHaveChildren() const case ListBoxOptionRole: case ScrollBarRole: case ProgressIndicatorRole: + case SwitchRole: return false; - case LegendRole: - if (Element* element = this->element()) - return !ancestorsOfType<HTMLFieldSetElement>(*element).first(); - FALLTHROUGH; default: return true; } @@ -426,20 +421,28 @@ bool AccessibilityNodeObject::computeAccessibilityIsIgnored() const if (!string.length()) return true; } - + + AccessibilityObjectInclusion decision = defaultObjectInclusion(); + if (decision == IncludeObject) + return false; + if (decision == IgnoreObject) + return true; // If this element is within a parent that cannot have children, it should not be exposed. if (isDescendantOfBarrenParent()) return true; + if (roleValue() == IgnoredRole) + return true; + return m_role == UnknownRole; } bool AccessibilityNodeObject::canvasHasFallbackContent() const { Node* node = this->node(); - if (!node || !node->hasTagName(canvasTag)) + if (!is<HTMLCanvasElement>(node)) return false; - Element& canvasElement = toElement(*node); + HTMLCanvasElement& canvasElement = downcast<HTMLCanvasElement>(*node); // 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. @@ -451,23 +454,18 @@ bool AccessibilityNodeObject::isImageButton() const return isNativeImage() && isButton(); } -bool AccessibilityNodeObject::isAnchor() const -{ - return !isNativeImage() && isLink(); -} - bool AccessibilityNodeObject::isNativeTextControl() const { Node* node = this->node(); if (!node) return false; - if (isHTMLTextAreaElement(node)) + if (is<HTMLTextAreaElement>(*node)) return true; - if (isHTMLInputElement(node)) { - HTMLInputElement* input = toHTMLInputElement(node); - return input->isText() || input->isNumberField(); + if (is<HTMLInputElement>(*node)) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + return input.isText() || input.isNumberField(); } return false; @@ -479,12 +477,13 @@ bool AccessibilityNodeObject::isSearchField() const if (!node) return false; - HTMLInputElement* inputElement = node->toInputElement(); - if (!inputElement) + if (roleValue() == SearchFieldRole) + return true; + + if (!is<HTMLInputElement>(*node)) return false; - if (inputElement->isSearchField()) - return true; + auto& inputElement = downcast<HTMLInputElement>(*node); // Some websites don't label their search fields as such. However, they will // use the word "search" in either the form or input type. This won't catch every case, @@ -496,7 +495,7 @@ bool AccessibilityNodeObject::isSearchField() const return true; // Check the form action and the name, which will sometimes be "search". - HTMLFormElement* form = inputElement->form(); + auto* form = inputElement.form(); if (form && (form->name().contains("search", false) || form->action().contains("search", false))) return true; @@ -509,15 +508,15 @@ bool AccessibilityNodeObject::isNativeImage() const if (!node) return false; - if (isHTMLImageElement(node)) + if (is<HTMLImageElement>(*node)) return true; if (node->hasTagName(appletTag) || node->hasTagName(embedTag) || node->hasTagName(objectTag)) return true; - if (isHTMLInputElement(node)) { - HTMLInputElement* input = toHTMLInputElement(node); - return input->isImageButton(); + if (is<HTMLInputElement>(*node)) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + return input.isImageButton(); } return false; @@ -530,29 +529,41 @@ bool AccessibilityNodeObject::isImage() const bool AccessibilityNodeObject::isPasswordField() const { - Node* node = this->node(); - if (!node || !node->isHTMLElement()) + auto* node = this->node(); + if (!is<HTMLInputElement>(node)) return false; if (ariaRoleAttribute() != UnknownRole) return false; - HTMLInputElement* inputElement = node->toInputElement(); - if (!inputElement) - return false; + return downcast<HTMLInputElement>(*node).isPasswordField(); +} - return inputElement->isPasswordField(); +AccessibilityObject* AccessibilityNodeObject::passwordFieldOrContainingPasswordField() +{ + Node* node = this->node(); + if (!node) + return nullptr; + + if (is<HTMLInputElement>(*node) && downcast<HTMLInputElement>(*node).isPasswordField()) + return this; + + auto* element = node->shadowHost(); + if (!is<HTMLInputElement>(element)) + return nullptr; + + if (auto* cache = axObjectCache()) + return cache->getOrCreate(element); + + return nullptr; } bool AccessibilityNodeObject::isInputImage() const { Node* node = this->node(); - if (!node) - return false; - - if (roleValue() == ButtonRole && isHTMLInputElement(node)) { - HTMLInputElement* input = toHTMLInputElement(node); - return input->isImageButton(); + if (is<HTMLInputElement>(node) && roleValue() == ButtonRole) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + return input.isImageButton(); } return false; @@ -613,14 +624,11 @@ bool AccessibilityNodeObject::isMenuItem() const bool AccessibilityNodeObject::isNativeCheckboxOrRadio() const { Node* node = this->node(); - if (!node) + if (!is<HTMLInputElement>(node)) return false; - HTMLInputElement* input = node->toInputElement(); - if (input) - return input->isCheckbox() || input->isRadioButton(); - - return false; + auto& input = downcast<HTMLInputElement>(*node); + return input.isCheckbox() || input.isRadioButton(); } bool AccessibilityNodeObject::isEnabled() const @@ -628,30 +636,25 @@ bool AccessibilityNodeObject::isEnabled() const // ARIA says that the disabled status applies to the current element and all descendant elements. for (AccessibilityObject* object = const_cast<AccessibilityNodeObject*>(this); object; object = object->parentObject()) { const AtomicString& disabledStatus = object->getAttribute(aria_disabledAttr); - if (equalIgnoringCase(disabledStatus, "true")) + if (equalLettersIgnoringASCIICase(disabledStatus, "true")) return false; - if (equalIgnoringCase(disabledStatus, "false")) + if (equalLettersIgnoringASCIICase(disabledStatus, "false")) break; } - + + if (roleValue() == HorizontalRuleRole) + return false; + Node* node = this->node(); - if (!node || !node->isElementNode()) + if (!is<Element>(node)) return true; - return !toElement(node)->isDisabledFormControl(); + return !downcast<Element>(*node).isDisabledFormControl(); } bool AccessibilityNodeObject::isIndeterminate() const { - Node* node = this->node(); - if (!node) - return false; - - HTMLInputElement* inputElement = node->toInputElement(); - if (!inputElement) - return false; - - return inputElement->shouldAppearIndeterminate(); + return equalLettersIgnoringASCIICase(getAttribute(indeterminateAttr), "true"); } bool AccessibilityNodeObject::isPressed() const @@ -663,16 +666,13 @@ bool AccessibilityNodeObject::isPressed() const if (!node) return false; - // If this is an ARIA button, check the aria-pressed attribute rather than node()->active() - if (ariaRoleAttribute() == ButtonRole) { - if (equalIgnoringCase(getAttribute(aria_pressedAttr), "true")) - return true; - return false; - } + // If this is an toggle button, check the aria-pressed attribute rather than node()->active() + if (isToggleButton()) + return equalLettersIgnoringASCIICase(getAttribute(aria_pressedAttr), "true"); - if (!node->isElementNode()) + if (!is<Element>(*node)) return false; - return toElement(node)->active(); + return downcast<Element>(*node).active(); } bool AccessibilityNodeObject::isChecked() const @@ -682,9 +682,8 @@ bool AccessibilityNodeObject::isChecked() const return false; // First test for native checkedness semantics - HTMLInputElement* inputElement = node->toInputElement(); - if (inputElement) - return inputElement->shouldAppearChecked(); + if (is<HTMLInputElement>(*node)) + return downcast<HTMLInputElement>(*node).shouldAppearChecked(); // Else, if this is an ARIA checkbox or radio, respect the aria-checked attribute bool validRole = false; @@ -694,13 +693,14 @@ bool AccessibilityNodeObject::isChecked() const case MenuItemRole: case MenuItemCheckboxRole: case MenuItemRadioRole: + case SwitchRole: validRole = true; break; default: break; } - if (validRole && equalIgnoringCase(getAttribute(aria_checkedAttr), "true")) + if (validRole && equalLettersIgnoringASCIICase(getAttribute(aria_checkedAttr), "true")) return true; return false; @@ -709,53 +709,32 @@ bool AccessibilityNodeObject::isChecked() const bool AccessibilityNodeObject::isHovered() const { Node* node = this->node(); - if (!node) - return false; - - return node->isElementNode() && toElement(node)->hovered(); + return is<Element>(node) && downcast<Element>(*node).hovered(); } bool AccessibilityNodeObject::isMultiSelectable() const { const AtomicString& ariaMultiSelectable = getAttribute(aria_multiselectableAttr); - if (equalIgnoringCase(ariaMultiSelectable, "true")) + if (equalLettersIgnoringASCIICase(ariaMultiSelectable, "true")) return true; - if (equalIgnoringCase(ariaMultiSelectable, "false")) + if (equalLettersIgnoringASCIICase(ariaMultiSelectable, "false")) return false; - return node() && node()->hasTagName(selectTag) && toHTMLSelectElement(node())->multiple(); -} - -bool AccessibilityNodeObject::isReadOnly() const -{ - Node* node = this->node(); - if (!node) - return true; - - if (isHTMLTextAreaElement(node)) - return toHTMLFormControlElement(node)->isReadOnly(); - - if (isHTMLInputElement(node)) { - HTMLInputElement* input = toHTMLInputElement(node); - if (input->isTextField()) - return input->isReadOnly(); - } - - return !node->hasEditableStyle(); + return node() && node()->hasTagName(selectTag) && downcast<HTMLSelectElement>(*node()).multiple(); } bool AccessibilityNodeObject::isRequired() const { // Explicit aria-required values should trump native required attributes. const AtomicString& requiredValue = getAttribute(aria_requiredAttr); - if (equalIgnoringCase(requiredValue, "true")) + if (equalLettersIgnoringASCIICase(requiredValue, "true")) return true; - if (equalIgnoringCase(requiredValue, "false")) + if (equalLettersIgnoringASCIICase(requiredValue, "false")) return false; Node* n = this->node(); - if (n && (n->isElementNode() && toElement(n)->isFormControlElement())) - return toHTMLFormControlElement(n)->isRequired(); + if (is<HTMLFormControlElement>(n)) + return downcast<HTMLFormControlElement>(*n).isRequired(); return false; } @@ -766,9 +745,11 @@ bool AccessibilityNodeObject::supportsRequiredAttribute() const case ButtonRole: return isFileUploadButton(); case CellRole: + case ColumnHeaderRole: case CheckBoxRole: case ComboBoxRole: case GridRole: + case GridCellRole: case IncrementorRole: case ListBoxRole: case PopUpButtonRole: @@ -818,6 +799,11 @@ int AccessibilityNodeObject::headingLevel() const if (node->hasTagName(h6Tag)) return 6; + // The implicit value of aria-level is 2 for the heading role. + // https://www.w3.org/TR/wai-aria-1.1/#heading + if (ariaRoleAttribute() == HeadingRole) + return 2; + return 0; } @@ -831,44 +817,62 @@ String AccessibilityNodeObject::valueDescription() const float AccessibilityNodeObject::valueForRange() const { - if (node() && isHTMLInputElement(node())) { - HTMLInputElement* input = toHTMLInputElement(node()); - if (input->isRangeControl()) - return input->valueAsNumber(); + if (is<HTMLInputElement>(node())) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node()); + if (input.isRangeControl()) + return input.valueAsNumber(); } if (!isRangeControl()) return 0.0f; - return getAttribute(aria_valuenowAttr).toFloat(); + // In ARIA 1.1, the implicit value for aria-valuenow on a spin button is 0. + // For other roles, it is half way between aria-valuemin and aria-valuemax. + auto value = getAttribute(aria_valuenowAttr); + if (!value.isEmpty()) + return value.toFloat(); + + return isSpinButton() ? 0 : (minValueForRange() + maxValueForRange()) / 2; } float AccessibilityNodeObject::maxValueForRange() const { - if (node() && isHTMLInputElement(node())) { - HTMLInputElement* input = toHTMLInputElement(node()); - if (input->isRangeControl()) - return input->maximum(); + if (is<HTMLInputElement>(node())) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node()); + if (input.isRangeControl()) + return input.maximum(); } if (!isRangeControl()) return 0.0f; - return getAttribute(aria_valuemaxAttr).toFloat(); + auto value = getAttribute(aria_valuemaxAttr); + if (!value.isEmpty()) + return value.toFloat(); + + // In ARIA 1.1, the implicit value for aria-valuemax on a spin button + // is that there is no maximum value. For other roles, it is 100. + return isSpinButton() ? std::numeric_limits<float>::max() : 100.0f; } float AccessibilityNodeObject::minValueForRange() const { - if (node() && isHTMLInputElement(node())) { - HTMLInputElement* input = toHTMLInputElement(node()); - if (input->isRangeControl()) - return input->minimum(); + if (is<HTMLInputElement>(node())) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node()); + if (input.isRangeControl()) + return input.minimum(); } if (!isRangeControl()) return 0.0f; - return getAttribute(aria_valueminAttr).toFloat(); + auto value = getAttribute(aria_valueminAttr); + if (!value.isEmpty()) + return value.toFloat(); + + // In ARIA 1.1, the implicit value for aria-valuemin on a spin button + // is that there is no minimum value. For other roles, it is 0. + return isSpinButton() ? -std::numeric_limits<float>::max() : 0.0f; } float AccessibilityNodeObject::stepValueForRange() const @@ -892,8 +896,7 @@ bool AccessibilityNodeObject::isControl() const if (!node) return false; - return ((node->isElementNode() && toElement(node)->isFormControlElement()) - || AccessibilityObject::isARIAControl(ariaRoleAttribute())); + return is<HTMLFormControlElement>(*node) || AccessibilityObject::isARIAControl(ariaRoleAttribute()); } bool AccessibilityNodeObject::isFieldset() const @@ -928,12 +931,13 @@ AccessibilityObject* AccessibilityNodeObject::selectedTabItem() if (!isTabList()) return nullptr; + // FIXME: Is this valid? ARIA tab items support aria-selected; not aria-checked. // Find the child tab item that is selected (ie. the intValue == 1). AccessibilityObject::AccessibilityChildrenVector tabs; tabChildren(tabs); for (const auto& child : children()) { - if (child->isTabItem() && child->isChecked()) + if (child->isTabItem() && (child->isChecked() || child->isSelected())) return child.get(); } return nullptr; @@ -942,7 +946,7 @@ AccessibilityObject* AccessibilityNodeObject::selectedTabItem() AccessibilityButtonState AccessibilityNodeObject::checkboxOrRadioValue() const { if (isNativeCheckboxOrRadio()) - return isChecked() ? ButtonStateOn : ButtonStateOff; + return isIndeterminate() ? ButtonStateMixed : isChecked() ? ButtonStateOn : ButtonStateOff; return AccessibilityObject::checkboxOrRadioValue(); } @@ -951,25 +955,25 @@ Element* AccessibilityNodeObject::anchorElement() const { Node* node = this->node(); if (!node) - return 0; + return nullptr; AXObjectCache* 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->renderer() && cache->getOrCreate(node->renderer())->isAnchor())) - return toElement(node); + if (is<HTMLAnchorElement>(*node) || (node->renderer() && cache->getOrCreate(node->renderer())->isLink())) + return downcast<Element>(node); } - return 0; + return nullptr; } static bool isNodeActionElement(Node* node) { - if (isHTMLInputElement(node)) { - HTMLInputElement* input = toHTMLInputElement(node); - if (!input->isDisabledFormControl() && (input->isRadioButton() || input->isCheckbox() || input->isTextButton() || input->isFileUpload() || input->isImageButton())) + if (is<HTMLInputElement>(*node)) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + if (!input.isDisabledFormControl() && (input.isRadioButton() || input.isCheckbox() || input.isTextButton() || input.isFileUpload() || input.isImageButton())) return true; } else if (node->hasTagName(buttonTag) || node->hasTagName(selectTag)) return true; @@ -980,7 +984,7 @@ static bool isNodeActionElement(Node* node) static Element* nativeActionElement(Node* start) { if (!start) - return 0; + return nullptr; // Do a deep-dive to see if any nodes should be used as the action element. // We have to look at Nodes, since this method should only be called on objects that do not have children (like buttons). @@ -988,25 +992,25 @@ static Element* nativeActionElement(Node* start) for (Node* child = start->firstChild(); child; child = child->nextSibling()) { if (isNodeActionElement(child)) - return toElement(child); + return downcast<Element>(child); if (Element* subChild = nativeActionElement(child)) return subChild; } - return 0; + return nullptr; } Element* AccessibilityNodeObject::actionElement() const { Node* node = this->node(); if (!node) - return 0; + return nullptr; if (isNodeActionElement(node)) - return toElement(node); + return downcast<Element>(node); if (AccessibilityObject::isARIAInput(ariaRoleAttribute())) - return toElement(node); + return downcast<Element>(node); switch (roleValue()) { case ButtonRole: @@ -1020,7 +1024,7 @@ Element* AccessibilityNodeObject::actionElement() const // Check if the author is hiding the real control element inside the ARIA element. if (Element* nativeElement = nativeActionElement(node)) return nativeElement; - return toElement(node); + return downcast<Element>(node); default: break; } @@ -1031,25 +1035,25 @@ Element* AccessibilityNodeObject::actionElement() const return elt; } -Element* AccessibilityNodeObject::mouseButtonListener() const +Element* AccessibilityNodeObject::mouseButtonListener(MouseButtonListenerResultFilter filter) const { Node* node = this->node(); if (!node) - return 0; + return nullptr; // check if our parent is a mouse button listener // FIXME: Do the continuation search like anchorElement does - for (auto& element : elementLineage(node->isElementNode() ? toElement(node) : node->parentElement())) { - // If we've reached the body and this is not a control element, do not expose press action for this element. - // It can cause false positives, where every piece of text is labeled as accepting press actions. - if (element.hasTagName(bodyTag) && isStaticText()) + for (auto& element : elementLineage(is<Element>(*node) ? downcast<Element>(node) : node->parentElement())) { + // If we've reached the body and this is not a control element, do not expose press action for this element unless filter is IncludeBodyElement. + // It can cause false positives, where every piece of text is labeled as accepting press actions. + if (element.hasTagName(bodyTag) && isStaticText() && filter == ExcludeBodyElement) break; if (element.hasEventListeners(eventNames().clickEvent) || element.hasEventListeners(eventNames().mousedownEvent) || element.hasEventListeners(eventNames().mouseupEvent)) return &element; } - return 0; + return nullptr; } bool AccessibilityNodeObject::isDescendantOfBarrenParent() const @@ -1075,13 +1079,13 @@ void AccessibilityNodeObject::alterSliderValue(bool increase) void AccessibilityNodeObject::increment() { - UserGestureIndicator gestureIndicator(DefinitelyProcessingUserGesture); + UserGestureIndicator gestureIndicator(ProcessingUserGesture, document()); alterSliderValue(true); } void AccessibilityNodeObject::decrement() { - UserGestureIndicator gestureIndicator(DefinitelyProcessingUserGesture); + UserGestureIndicator gestureIndicator(ProcessingUserGesture, document()); alterSliderValue(false); } @@ -1154,8 +1158,8 @@ bool AccessibilityNodeObject::isGenericFocusableElement() const HTMLLabelElement* AccessibilityNodeObject::labelForElement(Element* element) const { - if (!element->isHTMLElement() || !toHTMLElement(element)->isLabelable()) - return 0; + if (!is<HTMLElement>(*element) || !downcast<HTMLElement>(*element).isLabelable()) + return nullptr; const AtomicString& id = element->getIdAttribute(); if (!id.isEmpty()) { @@ -1179,15 +1183,16 @@ String AccessibilityNodeObject::ariaAccessibilityDescription() const return String(); } -static Element* siblingWithAriaRole(String role, Node* node) +static Element* siblingWithAriaRole(Node* node, const char* role) { + // FIXME: Either we should add a null check here or change the function to take a reference instead of a pointer. ContainerNode* parent = node->parentNode(); if (!parent) return nullptr; for (auto& sibling : childrenOfType<Element>(*parent)) { - const AtomicString& siblingAriaRole = sibling.fastGetAttribute(roleAttr); - if (equalIgnoringCase(siblingAriaRole, role)) + // FIXME: Should skip sibling that is the same as the node. + if (equalIgnoringASCIICase(sibling.attributeWithoutSynchronization(roleAttr), role)) return &sibling; } @@ -1199,39 +1204,89 @@ Element* AccessibilityNodeObject::menuElementForMenuButton() const if (ariaRoleAttribute() != MenuButtonRole) return nullptr; - return siblingWithAriaRole("menu", node()); + return siblingWithAriaRole(node(), "menu"); } AccessibilityObject* AccessibilityNodeObject::menuForMenuButton() const { - return axObjectCache()->getOrCreate(menuElementForMenuButton()); + if (AXObjectCache* cache = axObjectCache()) + return cache->getOrCreate(menuElementForMenuButton()); + return nullptr; } Element* AccessibilityNodeObject::menuItemElementForMenu() const { if (ariaRoleAttribute() != MenuRole) - return 0; + return nullptr; - return siblingWithAriaRole("menuitem", node()); + return siblingWithAriaRole(node(), "menuitem"); } AccessibilityObject* AccessibilityNodeObject::menuButtonForMenu() const { + AXObjectCache* cache = axObjectCache(); + if (!cache) + return nullptr; + 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 - AccessibilityObject* menuItemAX = axObjectCache()->getOrCreate(menuItem); + AccessibilityObject* menuItemAX = cache->getOrCreate(menuItem); if (menuItemAX && menuItemAX->isMenuButton()) return menuItemAX; } - return 0; + return nullptr; +} + +AccessibilityObject* AccessibilityNodeObject::captionForFigure() const +{ + if (!isFigure()) + return nullptr; + + AXObjectCache* cache = axObjectCache(); + if (!cache) + return nullptr; + + Node* node = this->node(); + for (Node* child = node->firstChild(); child; child = child->nextSibling()) { + if (child->hasTagName(figcaptionTag)) + return cache->getOrCreate(child); + } + return nullptr; } bool AccessibilityNodeObject::usesAltTagForTextComputation() const { return isImage() || isInputImage() || isNativeImage() || isCanvas() || (node() && node()->hasTagName(imgTag)); } + +bool AccessibilityNodeObject::isLabelable() const +{ + Node* node = this->node(); + if (!node) + return false; + + return is<HTMLInputElement>(*node) || AccessibilityObject::isARIAInput(ariaRoleAttribute()) || isControl() || isProgressIndicator() || isMeter(); +} + +String AccessibilityNodeObject::textForLabelElement(Element* element) const +{ + String result = String(); + if (!is<HTMLLabelElement>(*element)) + return result; + + HTMLLabelElement* label = downcast<HTMLLabelElement>(element); + // Check to see if there's aria-labelledby attribute on the label element. + if (AccessibilityObject* labelObject = axObjectCache()->getOrCreate(label)) + result = labelObject->ariaLabeledByAttribute(); + + // Then check for aria-label attribute. + if (result.isEmpty()) + result = label->attributeWithoutSynchronization(aria_labelAttr); + + return !result.isEmpty() ? result : label->innerText(); +} void AccessibilityNodeObject::titleElementText(Vector<AccessibilityText>& textOrder) const { @@ -1239,15 +1294,14 @@ void AccessibilityNodeObject::titleElementText(Vector<AccessibilityText>& textOr if (!node) return; - bool isInputTag = isHTMLInputElement(node); - if (isInputTag || AccessibilityObject::isARIAInput(ariaRoleAttribute()) || isControl()) { - HTMLLabelElement* label = labelForElement(toElement(node)); - if (label) { + if (isLabelable()) { + if (HTMLLabelElement* label = labelForElement(downcast<Element>(node))) { AccessibilityObject* labelObject = axObjectCache()->getOrCreate(label); - String innerText = label->innerText(); + String innerText = textForLabelElement(label); + // Only use the <label> text if there's no ARIA override. if (!innerText.isEmpty() && !ariaAccessibilityDescription()) - textOrder.append(AccessibilityText(innerText, LabelByElementText, labelObject)); + textOrder.append(AccessibilityText(innerText, isMeter() ? AlternativeText : LabelByElementText, labelObject)); return; } } @@ -1273,8 +1327,8 @@ void AccessibilityNodeObject::alternativeText(Vector<AccessibilityText>& textOrd textOrder.append(AccessibilityText(ariaLabel, AlternativeText)); if (usesAltTagForTextComputation()) { - if (renderer() && renderer()->isRenderImage()) { - String renderAltText = toRenderImage(renderer())->altText(); + if (is<RenderImage>(renderer())) { + String renderAltText = downcast<RenderImage>(*renderer()).altText(); // RenderImage will return title as a fallback from altText, but we don't want title here because we consider that in helpText. if (!renderAltText.isEmpty() && renderAltText != getAttribute(titleAttr)) { @@ -1294,17 +1348,22 @@ void AccessibilityNodeObject::alternativeText(Vector<AccessibilityText>& textOrd return; // The fieldset element derives its alternative text from the first associated legend element if one is available. - if (isHTMLFieldSetElement(node)) { - AccessibilityObject* object = axObjectCache()->getOrCreate(toHTMLFieldSetElement(node)->legend()); + if (is<HTMLFieldSetElement>(*node)) { + AccessibilityObject* object = axObjectCache()->getOrCreate(downcast<HTMLFieldSetElement>(*node).legend()); if (object && !object->isHidden()) textOrder.append(AccessibilityText(accessibleNameForNode(object->node()), AlternativeText)); } -#if ENABLE(SVG) - // SVG elements all can have a <svg:title> element inside which should act as the descriptive text. - if (node->isSVGElement()) - textOrder.append(AccessibilityText(toSVGElement(node)->title(), AlternativeText)); -#endif + // The figure element derives its alternative text from the first associated figcaption element if one is available. + if (isFigure()) { + AccessibilityObject* captionForFigure = this->captionForFigure(); + if (captionForFigure && !captionForFigure->isHidden()) + textOrder.append(AccessibilityText(accessibleNameForNode(captionForFigure->node()), AlternativeText)); + } + + // Tree items missing a label are labeled by all child elements. + if (isTreeItem() && ariaLabel.isEmpty() && ariaLabeledByAttribute().isEmpty()) + textOrder.append(AccessibilityText(accessibleNameForNode(node), AlternativeText)); #if ENABLE(MATHML) if (node->isMathMLElement()) @@ -1318,11 +1377,11 @@ void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder) if (!node) return; - bool isInputTag = isHTMLInputElement(node); + bool isInputTag = is<HTMLInputElement>(*node); if (isInputTag) { - HTMLInputElement* input = toHTMLInputElement(node); - if (input->isTextButton()) { - textOrder.append(AccessibilityText(input->valueWithDefault(), VisibleText)); + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + if (input.isTextButton()) { + textOrder.append(AccessibilityText(input.valueWithDefault(), VisibleText)); return; } } @@ -1344,7 +1403,7 @@ void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder) case CheckBoxRole: case ListBoxOptionRole: // MacOS does not expect native <li> elements to expose label information, it only expects leaf node elements to do that. -#if !PLATFORM(MAC) +#if !PLATFORM(COCOA) case ListItemRole: #endif case MenuButtonRole: @@ -1352,8 +1411,8 @@ void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder) case MenuItemCheckboxRole: case MenuItemRadioRole: case RadioButtonRole: + case SwitchRole: case TabRole: - case ProgressIndicatorRole: useTextUnderElement = true; break; default: @@ -1365,6 +1424,9 @@ void AccessibilityNodeObject::visibleText(Vector<AccessibilityText>& textOrder) if (isHeading() || isLink()) useTextUnderElement = true; + if (isOutput()) + useTextUnderElement = true; + if (useTextUnderElement) { AccessibilityTextUnderElementMode mode; @@ -1387,27 +1449,23 @@ void AccessibilityNodeObject::helpText(Vector<AccessibilityText>& textOrder) con String describedBy = ariaDescribedByAttribute(); if (!describedBy.isEmpty()) textOrder.append(AccessibilityText(describedBy, SummaryText)); - - // Add help type text that is derived from ancestors. - for (Node* curr = node(); curr; curr = curr->parentNode()) { - const AtomicString& summary = getAttribute(summaryAttr); - if (!summary.isEmpty()) - textOrder.append(AccessibilityText(summary, SummaryText)); - - // The title attribute should be used as help text unless it is already being used as descriptive text. - const AtomicString& title = getAttribute(titleAttr); - if (!title.isEmpty()) + + // Summary attribute used as help text on tables. + const AtomicString& summary = getAttribute(summaryAttr); + if (!summary.isEmpty()) + textOrder.append(AccessibilityText(summary, SummaryText)); + + // The title attribute should be used as help text unless it is already being used as descriptive text. + // However, when the title attribute is the only text alternative provided, it may be exposed as the + // descriptive text. This is problematic in the case of meters because the HTML spec suggests authors + // can expose units through this attribute. Therefore, if the element is a meter, change its source + // type to HelpText. + const AtomicString& title = getAttribute(titleAttr); + if (!title.isEmpty()) { + if (!isMeter()) textOrder.append(AccessibilityText(title, TitleTagText)); - - // Only take help text from an ancestor element if its a group or an unknown role. If help was - // added to those kinds of elements, it is likely it was meant for a child element. - AccessibilityObject* axObj = axObjectCache()->getOrCreate(curr); - if (!axObj) - return; - - AccessibilityRole role = axObj->roleValue(); - if (role != GroupRole && role != UnknownRole) - break; + else + textOrder.append(AccessibilityText(title, HelpText)); } } @@ -1436,7 +1494,7 @@ void AccessibilityNodeObject::ariaLabeledByText(Vector<AccessibilityText>& textO axElements.append(axElement); } - textOrder.append(AccessibilityText(ariaLabeledBy, AlternativeText, axElements)); + textOrder.append(AccessibilityText(ariaLabeledBy, AlternativeText, WTFMove(axElements))); } } @@ -1458,30 +1516,26 @@ String AccessibilityNodeObject::alternativeTextForWebArea() const // Check if the HTML element has an aria-label for the webpage. if (Element* documentElement = document->documentElement()) { - const AtomicString& ariaLabel = documentElement->getAttribute(aria_labelAttr); + const AtomicString& ariaLabel = documentElement->attributeWithoutSynchronization(aria_labelAttr); if (!ariaLabel.isEmpty()) return ariaLabel; } - Node* owner = document->ownerElement(); - if (owner) { + if (auto* owner = document->ownerElement()) { if (owner->hasTagName(frameTag) || owner->hasTagName(iframeTag)) { - const AtomicString& title = toElement(owner)->getAttribute(titleAttr); + const AtomicString& title = owner->attributeWithoutSynchronization(titleAttr); if (!title.isEmpty()) return title; - return toElement(owner)->getNameAttribute(); } - if (owner->isHTMLElement()) - return toHTMLElement(owner)->getNameAttribute(); + return owner->getNameAttribute(); } String documentTitle = document->title(); if (!documentTitle.isEmpty()) return documentTitle; - owner = document->body(); - if (owner && owner->isHTMLElement()) - return toHTMLElement(owner)->getNameAttribute(); + if (auto* body = document->bodyOrFrameset()) + return body->getNameAttribute(); return String(); } @@ -1503,15 +1557,9 @@ String AccessibilityNodeObject::accessibilityDescription() const if (!alt.isNull()) return alt; } - -#if ENABLE(SVG) - // SVG elements all can have a <svg:title> element inside which should act as the descriptive text. - if (m_node && m_node->isSVGElement()) - return toSVGElement(m_node)->title(); -#endif #if ENABLE(MATHML) - if (m_node && m_node->isMathMLElement()) + if (is<MathMLElement>(m_node)) return getAttribute(MathMLNames::alttextAttr); #endif @@ -1540,21 +1588,22 @@ String AccessibilityNodeObject::helpText() const return describedBy; String description = accessibilityDescription(); - for (Node* curr = node; curr; curr = curr->parentNode()) { - if (curr->isHTMLElement()) { - const AtomicString& summary = toElement(curr)->getAttribute(summaryAttr); + for (Node* ancestor = node; ancestor; ancestor = ancestor->parentNode()) { + if (is<HTMLElement>(*ancestor)) { + HTMLElement& element = downcast<HTMLElement>(*ancestor); + const AtomicString& summary = element.getAttribute(summaryAttr); if (!summary.isEmpty()) return summary; // The title attribute should be used as help text unless it is already being used as descriptive text. - const AtomicString& title = toElement(curr)->getAttribute(titleAttr); + const AtomicString& title = element.getAttribute(titleAttr); if (!title.isEmpty() && description != title) return title; } // Only take help text from an ancestor element if its a group or an unknown role. If help was // added to those kinds of elements, it is likely it was meant for a child element. - AccessibilityObject* axObj = axObjectCache()->getOrCreate(curr); + AccessibilityObject* axObj = axObjectCache()->getOrCreate(ancestor); if (axObj) { AccessibilityRole role = axObj->roleValue(); if (role != GroupRole && role != UnknownRole) @@ -1568,10 +1617,10 @@ String AccessibilityNodeObject::helpText() const unsigned AccessibilityNodeObject::hierarchicalLevel() const { Node* node = this->node(); - if (!node || !node->isElementNode()) + if (!is<Element>(node)) return 0; - Element* element = toElement(node); - String ariaLevel = element->getAttribute(aria_levelAttr); + Element& element = downcast<Element>(*node); + const AtomicString& ariaLevel = element.attributeWithoutSynchronization(aria_levelAttr); if (!ariaLevel.isEmpty()) return ariaLevel.toInt(); @@ -1583,7 +1632,7 @@ unsigned AccessibilityNodeObject::hierarchicalLevel() const // We measure tree hierarchy by the number of groups that the item is within. unsigned level = 1; for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { - AccessibilityRole parentRole = parent->roleValue(); + AccessibilityRole parentRole = parent->ariaRoleAttribute(); if (parentRole == GroupRole) level++; else if (parentRole == TreeRole) @@ -1593,9 +1642,18 @@ unsigned AccessibilityNodeObject::hierarchicalLevel() const return level; } +void AccessibilityNodeObject::setIsExpanded(bool expand) +{ + if (is<HTMLDetailsElement>(node())) { + auto& details = downcast<HTMLDetailsElement>(*node()); + if (expand != details.isOpen()) + details.toggleOpen(); + } +} + // When building the textUnderElement for an object, determine whether or not // we should include the inner text of this given descendant object or skip it. -static bool shouldUseAccessiblityObjectInnerText(AccessibilityObject* obj, AccessibilityTextUnderElementMode mode) +static bool shouldUseAccessibilityObjectInnerText(AccessibilityObject* obj, AccessibilityTextUnderElementMode mode) { // Do not use any heuristic if we are explicitly asking to include all the children. if (mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren) @@ -1618,7 +1676,12 @@ static bool shouldUseAccessiblityObjectInnerText(AccessibilityObject* obj, Acces // quite long. As a heuristic, skip links, controls, and elements that are usually // containers with lots of children. - if (equalIgnoringCase(obj->getAttribute(aria_hiddenAttr), "true")) + // ARIA states that certain elements are not allowed to expose their children content for name calculation. + if (mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeNameFromContentsChildren + && !obj->accessibleNameDerivesFromContent()) + return false; + + if (equalLettersIgnoringASCIICase(obj->getAttribute(aria_hiddenAttr), "true")) return false; // If something doesn't expose any children, then we can always take the inner text content. @@ -1631,13 +1694,19 @@ static bool shouldUseAccessiblityObjectInnerText(AccessibilityObject* obj, Acces return false; // Skip big container elements like lists, tables, etc. - if (obj->isList() || obj->isAccessibilityTable() || obj->isTree() || obj->isCanvas()) + if (is<AccessibilityList>(*obj)) + return false; + + if (is<AccessibilityTable>(*obj) && downcast<AccessibilityTable>(*obj).isExposableThroughAccessibility()) + return false; + + if (obj->isTree() || obj->isCanvas()) return false; return true; } -static bool shouldAddSpaceBeforeAppendingNextElement(StringBuilder& builder, String& childText) +static bool shouldAddSpaceBeforeAppendingNextElement(StringBuilder& builder, const String& childText) { if (!builder.length() || !childText.length()) return false; @@ -1645,41 +1714,46 @@ static bool shouldAddSpaceBeforeAppendingNextElement(StringBuilder& builder, Str // We don't need to add an additional space before or after a line break. return !(isHTMLLineBreak(childText[0]) || isHTMLLineBreak(builder[builder.length() - 1])); } + +static void appendNameToStringBuilder(StringBuilder& builder, const String& text) +{ + if (shouldAddSpaceBeforeAppendingNextElement(builder, text)) + builder.append(' '); + builder.append(text); +} String AccessibilityNodeObject::textUnderElement(AccessibilityTextUnderElementMode mode) const { Node* node = this->node(); - if (node && node->isTextNode()) - return toText(node)->wholeText(); - - // The render tree should be stable before going ahead. Otherwise, further uses of the - // TextIterator will force a layout update, potentially altering the accessibility tree - // and leading to crashes in the loop that computes the result text from the children. - ASSERT(!document()->renderView()->layoutState()); - ASSERT(!document()->childNeedsStyleRecalc()); + if (is<Text>(node)) + return downcast<Text>(*node).wholeText(); StringBuilder builder; for (AccessibilityObject* child = firstChild(); child; child = child->nextSibling()) { - if (!shouldUseAccessiblityObjectInnerText(child, mode)) + if (mode.ignoredChildNode && child->node() == mode.ignoredChildNode) + continue; + + bool shouldDeriveNameFromAuthor = (mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeNameFromContentsChildren && !child->accessibleNameDerivesFromContent()); + if (shouldDeriveNameFromAuthor) { + appendNameToStringBuilder(builder, accessibleNameForNode(child->node())); + continue; + } + + if (!shouldUseAccessibilityObjectInnerText(child, mode)) continue; - if (child->isAccessibilityNodeObject()) { + if (is<AccessibilityNodeObject>(*child)) { Vector<AccessibilityText> textOrder; - toAccessibilityNodeObject(child)->alternativeText(textOrder); + downcast<AccessibilityNodeObject>(*child).alternativeText(textOrder); if (textOrder.size() > 0 && textOrder[0].text.length()) { - if (shouldAddSpaceBeforeAppendingNextElement(builder, textOrder[0].text)) - builder.append(' '); - builder.append(textOrder[0].text); + appendNameToStringBuilder(builder, textOrder[0].text); continue; } } - + String childText = child->textUnderElement(mode); - if (childText.length()) { - if (shouldAddSpaceBeforeAppendingNextElement(builder, childText)) - builder.append(' '); - builder.append(childText); - } + if (childText.length()) + appendNameToStringBuilder(builder, childText); } return builder.toString().stripWhiteSpace().simplifyWhiteSpace(isHTMLSpaceButNotLineBreak); @@ -1691,18 +1765,18 @@ String AccessibilityNodeObject::title() const if (!node) return String(); - bool isInputTag = isHTMLInputElement(node); + bool isInputTag = is<HTMLInputElement>(*node); if (isInputTag) { - HTMLInputElement* input = toHTMLInputElement(node); - if (input->isTextButton()) - return input->valueWithDefault(); + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + if (input.isTextButton()) + return input.valueWithDefault(); } - if (isInputTag || AccessibilityObject::isARIAInput(ariaRoleAttribute()) || isControl()) { - HTMLLabelElement* label = labelForElement(toElement(node)); + if (isLabelable()) { + HTMLLabelElement* label = labelForElement(downcast<Element>(node)); // Use the label text as the title if 1) the title element is NOT an exposed element and 2) there's no ARIA override. if (label && !exposesTitleUIElement() && !ariaAccessibilityDescription().length()) - return label->innerText(); + return textForLabelElement(label); } // If this node isn't rendered, there's no inner text we can extract from a select element. @@ -1725,6 +1799,7 @@ String AccessibilityNodeObject::title() const case MenuItemCheckboxRole: case MenuItemRadioRole: case RadioButtonRole: + case SwitchRole: case TabRole: return textUnderElement(); // SVGRoots should not use the text under itself as a title. That could include the text of objects like <text>. @@ -1759,13 +1834,13 @@ String AccessibilityNodeObject::text() const if (!node) return String(); - if (isNativeTextControl() && (isHTMLTextAreaElement(node) || isHTMLInputElement(node))) - return toHTMLTextFormControlElement(node)->value(); + if (isNativeTextControl() && is<HTMLTextFormControlElement>(*node)) + return downcast<HTMLTextFormControlElement>(*node).value(); if (!node->isElementNode()) return String(); - return toElement(node)->innerText(); + return downcast<Element>(node)->innerText(); } String AccessibilityNodeObject::stringValue() const @@ -1785,16 +1860,16 @@ String AccessibilityNodeObject::stringValue() const return textUnderElement(); if (node->hasTagName(selectTag)) { - HTMLSelectElement* selectElement = toHTMLSelectElement(node); - int selectedIndex = selectElement->selectedIndex(); - const Vector<HTMLElement*> listItems = selectElement->listItems(); + HTMLSelectElement& selectElement = downcast<HTMLSelectElement>(*node); + int selectedIndex = selectElement.selectedIndex(); + const Vector<HTMLElement*>& listItems = selectElement.listItems(); if (selectedIndex >= 0 && static_cast<size_t>(selectedIndex) < listItems.size()) { - const AtomicString& overriddenDescription = listItems[selectedIndex]->fastGetAttribute(aria_labelAttr); + const AtomicString& overriddenDescription = listItems[selectedIndex]->attributeWithoutSynchronization(aria_labelAttr); if (!overriddenDescription.isNull()) return overriddenDescription; } - if (!selectElement->multiple()) - return selectElement->value(); + if (!selectElement.multiple()) + return selectElement.value(); return String(); } @@ -1814,73 +1889,103 @@ void AccessibilityNodeObject::colorValue(int& r, int& g, int& b) const g = 0; b = 0; +#if ENABLE(INPUT_TYPE_COLOR) if (!isColorWell()) return; - if (!node() || !isHTMLInputElement(node())) + if (!is<HTMLInputElement>(node())) return; - HTMLInputElement* input = toHTMLInputElement(node()); - const AtomicString& type = input->getAttribute(typeAttr); - if (!equalIgnoringCase(type, "color")) + auto& input = downcast<HTMLInputElement>(*node()); + if (!input.isColorControl()) return; // HTMLInputElement::value always returns a string parseable by Color(). - Color color(input->value()); + Color color(input.value()); r = color.red(); g = color.green(); b = color.blue(); +#endif } // This function implements the ARIA accessible name as described by the Mozilla // ARIA Implementer's Guide. -static String accessibleNameForNode(Node* node) +static String accessibleNameForNode(Node* node, Node* labelledbyNode) { - if (!node->isHTMLElement()) + ASSERT(node); + if (!is<Element>(node)) return String(); - HTMLElement* element = toHTMLElement(node); - - const AtomicString& ariaLabel = element->fastGetAttribute(aria_labelAttr); + Element& element = downcast<Element>(*node); + const AtomicString& ariaLabel = element.attributeWithoutSynchronization(aria_labelAttr); if (!ariaLabel.isEmpty()) return ariaLabel; - const AtomicString& alt = element->fastGetAttribute(altAttr); + const AtomicString& alt = element.attributeWithoutSynchronization(altAttr); if (!alt.isEmpty()) return alt; - - if (isHTMLInputElement(node)) - return toHTMLInputElement(node)->value(); - + // If the node can be turned into an AX object, we can use standard name computation rules. // If however, the node cannot (because there's no renderer e.g.) fallback to using the basic text underneath. AccessibilityObject* axObject = node->document().axObjectCache()->getOrCreate(node); - String text; - if (axObject) - text = axObject->textUnderElement(); - else if (node->isElementNode()) - text = toElement(node)->innerText(); + if (axObject) { + String valueDescription = axObject->valueDescription(); + if (!valueDescription.isEmpty()) + return valueDescription; + } + + if (is<HTMLInputElement>(*node)) + return downcast<HTMLInputElement>(*node).value(); + String text; + if (axObject) { + if (axObject->accessibleNameDerivesFromContent()) + text = axObject->textUnderElement(AccessibilityTextUnderElementMode(AccessibilityTextUnderElementMode::TextUnderElementModeIncludeNameFromContentsChildren, true, labelledbyNode)); + } else + text = element.innerText(); + if (!text.isEmpty()) return text; - const AtomicString& title = element->fastGetAttribute(titleAttr); + const AtomicString& title = element.attributeWithoutSynchronization(titleAttr); if (!title.isEmpty()) return title; return String(); } +String AccessibilityNodeObject::accessibilityDescriptionForChildren() const +{ + Node* node = this->node(); + if (!node) + return String(); + + AXObjectCache* cache = axObjectCache(); + if (!cache) + return String(); + + StringBuilder builder; + for (Node* child = node->firstChild(); child; child = child->nextSibling()) { + if (!is<Element>(child)) + continue; + + if (AccessibilityObject* axObject = cache->getOrCreate(child)) { + String description = axObject->ariaLabeledByAttribute(); + if (description.isEmpty()) + description = accessibleNameForNode(child); + appendNameToStringBuilder(builder, description); + } + } + + return builder.toString(); +} + String AccessibilityNodeObject::accessibilityDescriptionForElements(Vector<Element*> &elements) const { StringBuilder builder; unsigned size = elements.size(); - for (unsigned i = 0; i < size; ++i) { - if (i) - builder.append(' '); - - builder.append(accessibleNameForNode(elements[i])); - } + for (unsigned i = 0; i < size; ++i) + appendNameToStringBuilder(builder, accessibleNameForNode(elements[i], node())); return builder.toString(); } @@ -1931,18 +2036,66 @@ bool AccessibilityNodeObject::canSetFocusAttribute() const // NOTE: It would be more accurate to ask the document whether setFocusedElement() would // do anything. For example, setFocusedElement() will do nothing if the current focused // node will not relinquish the focus. - if (!node) + if (!is<Element>(node)) return false; - if (!node->isElementNode()) + Element& element = downcast<Element>(*node); + + if (element.isDisabledFormControl()) return false; - Element* element = toElement(node); + return element.supportsFocus(); +} - if (element->isDisabledFormControl()) +bool AccessibilityNodeObject::canSetValueAttribute() const +{ + Node* node = this->node(); + if (!node) return false; - return element->supportsFocus(); + // The host-language readonly attribute trumps aria-readonly. + if (is<HTMLTextAreaElement>(*node)) + return !downcast<HTMLTextAreaElement>(*node).isReadOnly(); + if (is<HTMLInputElement>(*node)) { + HTMLInputElement& input = downcast<HTMLInputElement>(*node); + if (input.isTextField()) + return !input.isReadOnly(); + } + + String readOnly = ariaReadOnlyValue(); + if (!readOnly.isEmpty()) + return readOnly == "true" ? false : true; + + if (isNonNativeTextControl()) + return true; + + if (isMeter()) + return false; + + if (isProgressIndicator() || isSlider()) + return true; + +#if PLATFORM(GTK) + // In ATK, input types which support aria-readonly are treated as having a + // settable value if the user can modify the widget's value or its state. + if (supportsARIAReadOnly() || isRadioButton()) + return true; +#endif + + if (isWebArea()) { + Document* document = this->document(); + if (!document) + return false; + + if (HTMLElement* body = document->bodyOrFrameset()) { + if (body->hasEditableStyle()) + return true; + } + + return document->hasEditableStyle(); + } + + return node->hasEditableStyle(); } AccessibilityRole AccessibilityNodeObject::determineAriaRoleAttribute() const @@ -2008,22 +2161,12 @@ AccessibilityRole AccessibilityNodeObject::remapAriaRoleDueToParent(Accessibilit return role; } -// If you call node->hasEditableStyle() since that will return true if an ancestor is editable. -// This only returns true if this is the element that actually has the contentEditable attribute set. -bool AccessibilityNodeObject::hasContentEditableAttributeSet() const -{ - if (!hasAttribute(contenteditableAttr)) - return false; - const AtomicString& contentEditableValue = getAttribute(contenteditableAttr); - // Both "true" (case-insensitive) and the empty string count as true. - return contentEditableValue.isEmpty() || equalIgnoringCase(contentEditableValue, "true"); -} - bool AccessibilityNodeObject::canSetSelectedAttribute() const { // Elements that can be selected switch (roleValue()) { case CellRole: + case GridCellRole: case RadioButtonRole: case RowHeaderRole: case RowRole: @@ -2032,6 +2175,9 @@ bool AccessibilityNodeObject::canSetSelectedAttribute() const case TreeGridRole: case TreeItemRole: case TreeRole: + case MenuItemCheckboxRole: + case MenuItemRadioRole: + case MenuItemRole: return isEnabled(); default: return false; |