/* * Copyright (C) 2008-2020 Apple 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 Inc. ("Apple") nor the names of * its contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "config.h" #include "AccessibilityRenderObject.h" #include "AXLogger.h" #include "AXObjectCache.h" #include "AccessibilityImageMapLink.h" #include "AccessibilityLabel.h" #include "AccessibilityListBox.h" #include "AccessibilitySVGRoot.h" #include "AccessibilitySpinButton.h" #include "AccessibilityTable.h" #include "CachedImage.h" #include "DocumentSVG.h" #include "Editing.h" #include "Editor.h" #include "EditorClient.h" #include "ElementIterator.h" #include "EventHandler.h" #include "FloatRect.h" #include "Frame.h" #include "FrameLoader.h" #include "FrameSelection.h" #include "GeometryUtilities.h" #include "HTMLAreaElement.h" #include "HTMLAudioElement.h" #include "HTMLDetailsElement.h" #include "HTMLFormElement.h" #include "HTMLFrameElementBase.h" #include "HTMLImageElement.h" #include "HTMLInputElement.h" #include "HTMLLabelElement.h" #include "HTMLMapElement.h" #include "HTMLMeterElement.h" #include "HTMLNames.h" #include "HTMLOptionElement.h" #include "HTMLOptionsCollection.h" #include "HTMLParserIdioms.h" #include "HTMLSelectElement.h" #include "HTMLSummaryElement.h" #include "HTMLTableElement.h" #include "HTMLTextAreaElement.h" #include "HTMLVideoElement.h" #include "HitTestRequest.h" #include "HitTestResult.h" #include "Image.h" #include "LocalizedStrings.h" #include "NodeList.h" #include "Page.h" #include "PathUtilities.h" #include "ProgressTracker.h" #include "Range.h" #include "RenderButton.h" #include "RenderFileUploadControl.h" #include "RenderHTMLCanvas.h" #include "RenderImage.h" #include "RenderInline.h" #include "RenderIterator.h" #include "RenderLayer.h" #include "RenderLayerScrollableArea.h" #include "RenderLineBreak.h" #include "RenderListBox.h" #include "RenderListItem.h" #include "RenderListMarker.h" #include "RenderMathMLBlock.h" #include "RenderMenuList.h" #include "RenderSVGRoot.h" #include "RenderSVGShape.h" #include "RenderTableCell.h" #include "RenderText.h" #include "RenderTextControl.h" #include "RenderTextControlSingleLine.h" #include "RenderTextFragment.h" #include "RenderTheme.h" #include "RenderView.h" #include "RenderWidget.h" #include "RenderedPosition.h" #include "SVGDocument.h" #include "SVGImage.h" #include "SVGSVGElement.h" #include "Text.h" #include "TextControlInnerElements.h" #include "TextIterator.h" #include "VisibleUnits.h" #include #include #include namespace WebCore { using namespace HTMLNames; AccessibilityRenderObject::AccessibilityRenderObject(RenderObject* renderer) : AccessibilityNodeObject(renderer->node()) , m_renderer(makeWeakPtr(renderer)) { #ifndef NDEBUG m_renderer->setHasAXObject(true); #endif } AccessibilityRenderObject::~AccessibilityRenderObject() { ASSERT(isDetached()); } void AccessibilityRenderObject::init() { AccessibilityNodeObject::init(); } Ref AccessibilityRenderObject::create(RenderObject* renderer) { return adoptRef(*new AccessibilityRenderObject(renderer)); } void AccessibilityRenderObject::detachRemoteParts(AccessibilityDetachmentType detachmentType) { AccessibilityNodeObject::detachRemoteParts(detachmentType); detachRemoteSVGRoot(); #ifndef NDEBUG if (m_renderer) m_renderer->setHasAXObject(false); #endif m_renderer = nullptr; } RenderBoxModelObject* AccessibilityRenderObject::renderBoxModelObject() const { if (!is(renderer())) return nullptr; return downcast(renderer()); } void AccessibilityRenderObject::setRenderer(RenderObject* renderer) { m_renderer = makeWeakPtr(renderer); setNode(renderer->node()); } static inline bool isInlineWithContinuation(RenderObject& object) { return is(object) && downcast(object).continuation(); } static inline RenderObject* firstChildInContinuation(RenderInline& renderer) { auto* continuation = renderer.continuation(); while (continuation) { if (is(*continuation)) return continuation; if (RenderObject* child = continuation->firstChild()) return child; continuation = downcast(*continuation).continuation(); } return nullptr; } static inline RenderObject* firstChildConsideringContinuation(RenderObject& renderer) { RenderObject* firstChild = renderer.firstChildSlow(); // We don't want to include the end of a continuation as the firstChild of the // anonymous parent, because everything has already been linked up via continuation. // CSS first-letter selector is an example of this case. if (renderer.isAnonymous() && is(firstChild) && downcast(*firstChild).isContinuation()) firstChild = nullptr; if (!firstChild && isInlineWithContinuation(renderer)) firstChild = firstChildInContinuation(downcast(renderer)); return firstChild; } static inline RenderObject* lastChildConsideringContinuation(RenderObject& renderer) { if (!is(renderer) && !is(renderer)) return &renderer; RenderObject* lastChild = downcast(renderer).lastChild(); for (auto* current = &downcast(renderer); current; ) { if (RenderObject* newLastChild = current->lastChild()) lastChild = newLastChild; current = current->inlineContinuation(); } return lastChild; } AccessibilityObject* AccessibilityRenderObject::firstChild() const { if (!m_renderer) return nullptr; RenderObject* firstChild = firstChildConsideringContinuation(*m_renderer); // If an object can't have children, then it is using this method to help // calculate some internal property (like its description). // In this case, it should check the Node level for children in case they're // not rendered (like a element). if (!firstChild && !canHaveChildren()) return AccessibilityNodeObject::firstChild(); auto objectCache = axObjectCache(); return objectCache ? objectCache->getOrCreate(firstChild) : nullptr; } AccessibilityObject* AccessibilityRenderObject::lastChild() const { if (!m_renderer) return nullptr; RenderObject* lastChild = lastChildConsideringContinuation(*m_renderer); if (!lastChild && !canHaveChildren()) return AccessibilityNodeObject::lastChild(); auto objectCache = axObjectCache(); return objectCache ? objectCache->getOrCreate(lastChild) : nullptr; } static inline RenderInline* startOfContinuations(RenderObject& renderer) { if (!is(renderer)) return nullptr; auto& renderElement = downcast(renderer); if (is(renderElement) && renderElement.isContinuation() && is(renderElement.element()->renderer())) return downcast(renderer.node()->renderer()); // Blocks with a previous continuation always have a next continuation if (is(renderElement) && downcast(renderElement).inlineContinuation()) return downcast(downcast(renderElement).inlineContinuation()->element()->renderer()); return nullptr; } static inline RenderObject* endOfContinuations(RenderObject& renderer) { if (!is(renderer) && !is(renderer)) return &renderer; auto* previous = &downcast(renderer); for (auto* current = previous; current; ) { previous = current; current = current->inlineContinuation(); } return previous; } static inline RenderObject* childBeforeConsideringContinuations(RenderInline* renderer, RenderObject* child) { RenderObject* previous = nullptr; for (RenderBoxModelObject* currentContainer = renderer; currentContainer; ) { if (is(*currentContainer)) { auto* current = currentContainer->firstChild(); while (current) { if (current == child) return previous; previous = current; current = current->nextSibling(); } currentContainer = currentContainer->continuation(); } else if (is(*currentContainer)) { if (currentContainer == child) return previous; previous = currentContainer; currentContainer = currentContainer->inlineContinuation(); } } ASSERT_NOT_REACHED(); return nullptr; } static inline bool firstChildIsInlineContinuation(RenderElement& renderer) { RenderObject* child = renderer.firstChild(); return is(child) && downcast(*child).isContinuation(); } AccessibilityObject* AccessibilityRenderObject::previousSibling() const { if (!m_renderer) return nullptr; RenderObject* previousSibling = nullptr; // Case 1: The node is a block and is an inline's continuation. In that case, the inline's // last child is our previous sibling (or further back in the continuation chain) RenderInline* startOfConts; if (is(*m_renderer) && (startOfConts = startOfContinuations(*m_renderer))) previousSibling = childBeforeConsideringContinuations(startOfConts, renderer()); // Case 2: Anonymous block parent of the end of a continuation - skip all the way to before // the parent of the start, since everything in between will be linked up via the continuation. else if (m_renderer->isAnonymousBlock() && firstChildIsInlineContinuation(downcast(*m_renderer))) { RenderBlock& renderBlock = downcast(*m_renderer); auto* firstParent = startOfContinuations(*renderBlock.firstChild())->parent(); ASSERT(firstParent); while (firstChildIsInlineContinuation(*firstParent)) firstParent = startOfContinuations(*firstParent->firstChild())->parent(); previousSibling = firstParent->previousSibling(); } // Case 3: The node has an actual previous sibling else if (RenderObject* ps = m_renderer->previousSibling()) previousSibling = ps; // Case 4: This node has no previous siblings, but its parent is an inline, // and is another node's inline continutation. Follow the continuation chain. else if (is(m_renderer->parent()) && (startOfConts = startOfContinuations(*m_renderer->parent()))) previousSibling = childBeforeConsideringContinuations(startOfConts, m_renderer->parent()->firstChild()); if (!previousSibling) return nullptr; if (auto* objectCache = axObjectCache()) return objectCache->getOrCreate(previousSibling); return nullptr; } static inline bool lastChildHasContinuation(RenderElement& renderer) { RenderObject* child = renderer.lastChild(); return child && isInlineWithContinuation(*child); } AccessibilityObject* AccessibilityRenderObject::nextSibling() const { if (!m_renderer || is(*m_renderer)) return nullptr; RenderObject* nextSibling = nullptr; // Case 1: node is a block and has an inline continuation. Next sibling is the inline continuation's // first child. RenderInline* inlineContinuation; if (is(*m_renderer) && (inlineContinuation = downcast(*m_renderer).inlineContinuation())) nextSibling = firstChildConsideringContinuation(*inlineContinuation); // Case 2: Anonymous block parent of the start of a continuation - skip all the way to // after the parent of the end, since everything in between will be linked up via the continuation. else if (m_renderer->isAnonymousBlock() && lastChildHasContinuation(downcast(*m_renderer))) { RenderElement* lastParent = endOfContinuations(*downcast(*m_renderer).lastChild())->parent(); ASSERT(lastParent); while (lastChildHasContinuation(*lastParent)) lastParent = endOfContinuations(*lastParent->lastChild())->parent(); nextSibling = lastParent->nextSibling(); } // Case 3: node has an actual next sibling else if (RenderObject* ns = m_renderer->nextSibling()) nextSibling = ns; // Case 4: node is an inline with a continuation. Next sibling is the next sibling of the end // of the continuation chain. else if (isInlineWithContinuation(*m_renderer)) nextSibling = endOfContinuations(*m_renderer)->nextSibling(); // Case 5: node has no next sibling, and its parent is an inline with a continuation. // Case 5.1: After case 4, (the element was inline w/ continuation but had no sibling), then check it's parent. if (!nextSibling && m_renderer->parent() && isInlineWithContinuation(*m_renderer->parent())) { auto& continuation = *downcast(*m_renderer->parent()).continuation(); // Case 5a: continuation is a block - in this case the block itself is the next sibling. if (is(continuation)) nextSibling = &continuation; // Case 5b: continuation is an inline - in this case the inline's first child is the next sibling else nextSibling = firstChildConsideringContinuation(continuation); // After case 4, there are chances that nextSibling has the same node as the current renderer, // which might lead to adding the same child repeatedly. if (nextSibling && nextSibling->node() == m_renderer->node()) { if (AccessibilityObject* nextObj = axObjectCache()->getOrCreate(nextSibling)) return nextObj->nextSibling(); } } if (!nextSibling) return nullptr; auto* objectCache = axObjectCache(); if (!objectCache) return nullptr; // Make sure next sibling has the same parent. auto* nextObject = objectCache->getOrCreate(nextSibling); if (nextObject && nextObject->parentObject() != this->parentObject()) return nullptr; return nextObject; } static RenderBoxModelObject* nextContinuation(RenderObject& renderer) { if (is(renderer) && !renderer.isReplaced()) return downcast(renderer).continuation(); if (is(renderer)) return downcast(renderer).inlineContinuation(); return nullptr; } RenderObject* AccessibilityRenderObject::renderParentObject() const { if (!m_renderer) return nullptr; RenderElement* parent = m_renderer->parent(); // Case 1: node is a block and is an inline's continuation. Parent // is the start of the continuation chain. RenderInline* startOfConts = nullptr; RenderObject* firstChild = nullptr; if (is(*m_renderer) && (startOfConts = startOfContinuations(*m_renderer))) parent = startOfConts; // Case 2: node's parent is an inline which is some node's continuation; parent is // the earliest node in the continuation chain. else if (is(parent) && (startOfConts = startOfContinuations(*parent))) parent = startOfConts; // Case 3: The first sibling is the beginning of a continuation chain. Find the origin of that continuation. else if (parent && (firstChild = parent->firstChild()) && firstChild->node()) { // Get the node's renderer and follow that continuation chain until the first child is found RenderObject* nodeRenderFirstChild = firstChild->node()->renderer(); while (nodeRenderFirstChild != firstChild) { for (RenderObject* contsTest = nodeRenderFirstChild; contsTest; contsTest = nextContinuation(*contsTest)) { if (contsTest == firstChild) { parent = nodeRenderFirstChild->parent(); break; } } RenderObject* parentFirstChild = parent->firstChild(); if (firstChild == parentFirstChild) break; firstChild = parentFirstChild; if (!firstChild->node()) break; nodeRenderFirstChild = firstChild->node()->renderer(); } } return parent; } AccessibilityObject* AccessibilityRenderObject::parentObjectIfExists() const { AXObjectCache* cache = axObjectCache(); if (!cache) return nullptr; // WebArea's parent should be the scroll view containing it. if (isWebArea()) return cache->get(&m_renderer->view().frameView()); return cache->get(renderParentObject()); } AccessibilityObject* AccessibilityRenderObject::parentObject() const { if (!m_renderer) return nullptr; if (ariaRoleAttribute() == AccessibilityRole::MenuBar) return axObjectCache()->getOrCreate(m_renderer->parent()); // menuButton and its corresponding menu are DOM siblings, but Accessibility needs them to be parent/child if (ariaRoleAttribute() == AccessibilityRole::Menu) { AccessibilityObject* parent = menuButtonForMenu(); if (parent) return parent; } AXObjectCache* cache = axObjectCache(); if (!cache) return nullptr; RenderObject* parentObj = renderParentObject(); if (parentObj) return cache->getOrCreate(parentObj); // WebArea's parent should be the scroll view containing it. if (isWebArea()) return cache->getOrCreate(&m_renderer->view().frameView()); return nullptr; } bool AccessibilityRenderObject::isAttachment() const { RenderBoxModelObject* renderer = renderBoxModelObject(); if (!renderer) return false; // Widgets are the replaced elements that we represent to AX as attachments bool isWidget = renderer->isWidget(); return isWidget && ariaRoleAttribute() == AccessibilityRole::Unknown; } bool AccessibilityRenderObject::isFileUploadButton() const { if (m_renderer && is(m_renderer->node())) { HTMLInputElement& input = downcast(*m_renderer->node()); return input.isFileUpload(); } return false; } bool AccessibilityRenderObject::isOffScreen() const { if (!m_renderer) return true; IntRect contentRect = snappedIntRect(m_renderer->absoluteClippedOverflowRectForSpatialNavigation()); // FIXME: unclear if we need LegacyIOSDocumentVisibleRect. IntRect viewRect = m_renderer->view().frameView().visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); viewRect.intersect(contentRect); return viewRect.isEmpty(); } Element* AccessibilityRenderObject::anchorElement() const { if (!m_renderer) return nullptr; AXObjectCache* cache = axObjectCache(); if (!cache) return nullptr; RenderObject* currentRenderer; // Search up the render tree for a RenderObject with a DOM node. Defer to an earlier continuation, though. for (currentRenderer = renderer(); currentRenderer && !currentRenderer->node(); currentRenderer = currentRenderer->parent()) { if (currentRenderer->isAnonymousBlock()) { if (RenderObject* continuation = downcast(*currentRenderer).continuation()) return cache->getOrCreate(continuation)->anchorElement(); } } // bail if none found if (!currentRenderer) return nullptr; // 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 = currentRenderer->node(); node; node = node->parentNode()) { if (is(*node) || (node->renderer() && cache->getOrCreate(node->renderer())->isLink())) return downcast(node); } return nullptr; } String AccessibilityRenderObject::helpText() const { if (!m_renderer) return String(); const AtomString& ariaHelp = getAttribute(aria_helpAttr); if (!ariaHelp.isEmpty()) return ariaHelp; String describedBy = ariaDescribedByAttribute(); if (!describedBy.isEmpty()) return describedBy; String description = accessibilityDescription(); for (RenderObject* ancestor = renderer(); ancestor; ancestor = ancestor->parent()) { if (is(ancestor->node())) { HTMLElement& element = downcast(*ancestor->node()); const AtomString& 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 AtomString& 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. if (AccessibilityObject* axObj = axObjectCache()->getOrCreate(ancestor)) { if (!axObj->isGroup() && axObj->roleValue() != AccessibilityRole::Unknown) break; } } return String(); } String AccessibilityRenderObject::textUnderElement(AccessibilityTextUnderElementMode mode) const { if (!m_renderer) return String(); if (is(*m_renderer)) return downcast(*m_renderer).buttonValue(); // Reflect when a content author has explicitly marked a line break. if (m_renderer->isBR()) return "\n"_s; if (shouldGetTextFromNode(mode)) return AccessibilityNodeObject::textUnderElement(mode); // We use a text iterator for text objects AND for those cases where we are // explicitly asking for the full text under a given element. if (is(*m_renderer) || mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren) { // If possible, use a text iterator to get the text, so that whitespace // is handled consistently. Document* nodeDocument = nullptr; std::optional textRange; if (Node* node = m_renderer->node()) { nodeDocument = &node->document(); textRange = makeRangeSelectingNodeContents(*node); } else { // For anonymous blocks, we work around not having a direct node to create a range from // defining one based in the two external positions defining the boundaries of the subtree. RenderObject* firstChildRenderer = m_renderer->firstChildSlow(); RenderObject* lastChildRenderer = m_renderer->lastChildSlow(); if (firstChildRenderer && firstChildRenderer->node() && lastChildRenderer && lastChildRenderer->node()) { // We define the start and end positions for the range as the ones right before and after // the first and the last nodes in the DOM tree that is wrapped inside the anonymous block. auto& firstNodeInBlock = *firstChildRenderer->node(); nodeDocument = &firstNodeInBlock.document(); textRange = makeSimpleRange(positionInParentBeforeNode(&firstNodeInBlock), positionInParentAfterNode(lastChildRenderer->node())); } } if (nodeDocument && textRange) { if (Frame* frame = nodeDocument->frame()) { // catch stale WebCoreAXObject (see ) if (frame->document() != nodeDocument) return String(); // Renders referenced by accessibility objects could get destroyed, if TextIterator ends up triggering // style update/layout here. See also AXObjectCache::deferTextChangedIfNeeded(). ASSERT_WITH_SECURITY_IMPLICATION(!nodeDocument->childNeedsStyleRecalc()); ASSERT_WITH_SECURITY_IMPLICATION(!nodeDocument->view()->layoutContext().isInRenderTreeLayout()); return plainText(*textRange, textIteratorBehaviorForTextRange()); } } // Sometimes text fragments don't have Nodes associated with them (like when // CSS content is used to insert text or when a RenderCounter is used.) if (is(*m_renderer)) { RenderText& renderTextObject = downcast(*m_renderer); if (is(renderTextObject)) { RenderTextFragment& renderTextFragment = downcast(renderTextObject); // The alt attribute may be set on a text fragment through CSS, which should be honored. const String& altText = renderTextFragment.altText(); if (!altText.isEmpty()) return altText; return renderTextFragment.contentString(); } return renderTextObject.text(); } } return AccessibilityNodeObject::textUnderElement(mode); } bool AccessibilityRenderObject::shouldGetTextFromNode(AccessibilityTextUnderElementMode mode) const { if (!m_renderer) return false; // AccessibilityRenderObject::textUnderElement() gets the text of anonymous blocks by using // the child nodes to define positions. CSS tables and their anonymous descendants lack // children with nodes. if (m_renderer->isAnonymous() && m_renderer->isTablePart()) return mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren; // AccessibilityRenderObject::textUnderElement() calls rangeOfContents() to create the text // range. rangeOfContents() does not include CSS-generated content. if (m_renderer->isBeforeOrAfterContent()) return true; if (Node* node = m_renderer->node()) { Node* firstChild = node->pseudoAwareFirstChild(); Node* lastChild = node->pseudoAwareLastChild(); if ((firstChild && firstChild->isPseudoElement()) || (lastChild && lastChild->isPseudoElement())) return true; } return false; } Node* AccessibilityRenderObject::node() const { if (!m_renderer) return nullptr; if (m_renderer->isRenderView()) return &m_renderer->document(); return m_renderer->node(); } String AccessibilityRenderObject::stringValue() const { if (!m_renderer) return String(); if (isPasswordField()) return passwordFieldValue(); RenderBoxModelObject* cssBox = renderBoxModelObject(); if (isARIAStaticText()) { String staticText = text(); if (!staticText.length()) staticText = textUnderElement(); return staticText; } if (is(*m_renderer)) return textUnderElement(); if (is(cssBox)) { // RenderMenuList will go straight to the text() of its selected item. // This has to be overridden in the case where the selected item has an ARIA label. HTMLSelectElement& selectElement = downcast(*m_renderer->node()); int selectedIndex = selectElement.selectedIndex(); const Vector& listItems = selectElement.listItems(); if (selectedIndex >= 0 && static_cast(selectedIndex) < listItems.size()) { const AtomString& overriddenDescription = listItems[selectedIndex]->attributeWithoutSynchronization(aria_labelAttr); if (!overriddenDescription.isNull()) return overriddenDescription; } return downcast(*m_renderer).text(); } if (is(*m_renderer)) return downcast(*m_renderer).textWithoutSuffix().toString(); if (isWebArea()) return String(); if (isTextControl()) return text(); #if PLATFORM(IOS_FAMILY) if (isInputTypePopupButton()) return textUnderElement(); #endif if (is(*m_renderer)) return downcast(*m_renderer).fileTextValue(); // FIXME: We might need to implement a value here for more types // FIXME: It would be better not to advertise a value at all for the types for which we don't implement one; // this would require subclassing or making accessibilityAttributeNames do something other than return a // single static array. return String(); } bool AccessibilityRenderObject::canHavePlainText() const { return isARIAStaticText() || is(*m_renderer) || isTextControl(); } HTMLLabelElement* AccessibilityRenderObject::labelElementContainer() const { if (!m_renderer) return nullptr; // the control element should not be considered part of the label if (isControl()) return nullptr; // find if this has a parent that is a label for (Node* parentNode = m_renderer->node(); parentNode; parentNode = parentNode->parentNode()) { if (is(*parentNode)) return downcast(parentNode); } return nullptr; } // The boundingBox for elements within the remote SVG element needs to be offset by its position // within the parent page, otherwise they are in relative coordinates only. void AccessibilityRenderObject::offsetBoundingBoxForRemoteSVGElement(LayoutRect& rect) const { for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { if (parent->isAccessibilitySVGRoot()) { rect.moveBy(parent->parentObject()->boundingBoxRect().location()); break; } } } LayoutRect AccessibilityRenderObject::boundingBoxRect() const { RenderObject* obj = renderer(); if (!obj) return LayoutRect(); if (obj->node()) // If we are a continuation, we want to make sure to use the primary renderer. obj = obj->node()->renderer(); // absoluteFocusRingQuads will query the hierarchy below this element, which for large webpages can be very slow. // For a web area, which will have the most elements of any element, absoluteQuads should be used. // We should also use absoluteQuads for SVG elements, otherwise transforms won't be applied. Vector quads; bool isSVGRoot = false; if (obj->isSVGRoot()) isSVGRoot = true; if (is(*obj)) quads = downcast(*obj).absoluteQuadsClippedToEllipsis(); else if (isWebArea() || isSVGRoot) obj->absoluteQuads(quads); else obj->absoluteFocusRingQuads(quads); LayoutRect result = boundingBoxForQuads(obj, quads); Document* document = this->document(); if (document && document->isSVGDocument()) offsetBoundingBoxForRemoteSVGElement(result); // The size of the web area should be the content size, not the clipped size. if (isWebArea()) result.setSize(obj->view().frameView().contentsSize()); return result; } LayoutRect AccessibilityRenderObject::checkboxOrRadioRect() const { if (!m_renderer) return LayoutRect(); HTMLLabelElement* label = labelForElement(downcast(m_renderer->node())); if (!label || !label->renderer()) return boundingBoxRect(); LayoutRect labelRect = axObjectCache()->getOrCreate(label)->elementRect(); labelRect.unite(boundingBoxRect()); return labelRect; } LayoutRect AccessibilityRenderObject::elementRect() const { // a checkbox or radio button should encompass its label if (isCheckboxOrRadio()) return checkboxOrRadioRect(); return boundingBoxRect(); } bool AccessibilityRenderObject::supportsPath() const { return is(renderer()) || is(renderer()); } Path AccessibilityRenderObject::elementPath() const { if (is(renderer())) { Vector rects; downcast(*m_renderer).absoluteRects(rects, flooredLayoutPoint(m_renderer->localToAbsolute())); // If only 1 rect, don't compute path since the bounding rect will be good enough. if (rects.size() < 2) return Path(); // Compute the path only if this is the last part of a line followed by the beginning of the next line. const auto& style = m_renderer->style(); bool rightToLeftText = style.direction() == TextDirection::RTL; static const int xTolerance = 5; static const int yTolerance = 5; bool needsPath = false; IntRect unionRect = rects[0]; for (size_t i = 1; i < rects.size(); ++i) { needsPath = abs(rects[i].y() - unionRect.maxY()) < yTolerance // This rect is in a new line. && (rightToLeftText ? rects[i].x() - unionRect.x() > xTolerance : unionRect.x() - rects[i].x() > xTolerance); // And this rect is to right/left of all previous rects. if (needsPath) break; unionRect.unite(rects[i]); } if (!needsPath) return Path(); float outlineOffset = style.outlineOffset(); float deviceScaleFactor = m_renderer->document().deviceScaleFactor(); Vector pixelSnappedRects; for (auto rect : rects) { rect.inflate(outlineOffset); pixelSnappedRects.append(snapRectToDevicePixels(rect, deviceScaleFactor)); } return PathUtilities::pathWithShrinkWrappedRects(pixelSnappedRects, 0); } if (is(renderer()) && downcast(*m_renderer).hasPath()) { Path path = downcast(*m_renderer).path(); // The SVG path is in terms of the parent's bounding box. The path needs to be offset to frame coordinates. if (auto svgRoot = ancestorsOfType(*m_renderer).first()) { LayoutPoint parentOffset = axObjectCache()->getOrCreate(&*svgRoot)->elementRect().location(); path.transform(AffineTransform().translate(parentOffset.x(), parentOffset.y())); } return path; } return Path(); } IntPoint AccessibilityRenderObject::linkClickPoint() { ASSERT(isLink()); /* A link bounding rect can contain points that are not part of the link. For instance, a link that starts at the end of a line and finishes at the beginning of the next line will have a bounding rect that includes the entire two lines. In such a case, the middle point of the bounding rect may not belong to the link element and thus may not activate the link. Hence, return the middle point of the first character in the link if exists. */ if (auto range = elementRange()) { auto start = VisiblePosition { makeContainerOffsetPosition(range->start) }; auto end = nextVisiblePosition(start); if (contains(*range, makeBoundaryPoint(end))) return { boundsForRange(*makeSimpleRange(start, end)).center() }; } return AccessibilityObject::clickPoint(); } IntPoint AccessibilityRenderObject::clickPoint() { // Headings are usually much wider than their textual content. If the mid point is used, often it can be wrong. if (isHeading() && children().size() == 1) return children().first()->clickPoint(); if (isLink()) return linkClickPoint(); // use the default position unless this is an editable web area, in which case we use the selection bounds. if (!isWebArea() || !canSetValueAttribute()) return AccessibilityObject::clickPoint(); return boundsForVisiblePositionRange(selection()).center(); } AccessibilityObject* AccessibilityRenderObject::internalLinkElement() const { auto element = anchorElement(); // Right now, we do not support ARIA links as internal link elements if (!is(element)) return nullptr; auto& anchor = downcast(*element); auto linkURL = anchor.href(); auto fragmentIdentifier = linkURL.fragmentIdentifier(); if (fragmentIdentifier.isEmpty()) return nullptr; // check if URL is the same as current URL auto documentURL = m_renderer->document().url(); if (!equalIgnoringFragmentIdentifier(documentURL, linkURL)) return nullptr; auto linkedNode = m_renderer->document().findAnchor(fragmentIdentifier); if (!linkedNode) return nullptr; // The element we find may not be accessible, so find the first accessible object. return firstAccessibleObjectFromNode(linkedNode); } OptionSet AccessibilityRenderObject::speakAsProperty() const { if (!m_renderer) return AccessibilityObject::speakAsProperty(); return m_renderer->style().speakAs(); } void AccessibilityRenderObject::addRadioButtonGroupChildren(AXCoreObject* parent, AccessibilityChildrenVector& linkedUIElements) const { for (const auto& child : parent->children()) { if (child->roleValue() == AccessibilityRole::RadioButton) linkedUIElements.append(child); else addRadioButtonGroupChildren(child.get(), linkedUIElements); } } void AccessibilityRenderObject::addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const { if (roleValue() != AccessibilityRole::RadioButton) return; Node* node = this->node(); if (is(node)) { HTMLInputElement& input = downcast(*node); for (auto& radioSibling : input.radioButtonGroup()) { if (AccessibilityObject* object = axObjectCache()->getOrCreate(radioSibling.ptr())) linkedUIElements.append(object); } } else { // If we didn't find any radio button siblings with the traditional naming, lets search for a radio group role and find its children. for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) { if (parent->roleValue() == AccessibilityRole::RadioGroup) addRadioButtonGroupChildren(parent, linkedUIElements); } } } // linked ui elements could be all the related radio buttons in a group // or an internal anchor connection void AccessibilityRenderObject::linkedUIElements(AccessibilityChildrenVector& linkedUIElements) const { ariaFlowToElements(linkedUIElements); if (isLink()) { AccessibilityObject* linkedAXElement = internalLinkElement(); if (linkedAXElement) linkedUIElements.append(linkedAXElement); } if (roleValue() == AccessibilityRole::RadioButton) addRadioButtonGroupMembers(linkedUIElements); } bool AccessibilityRenderObject::hasTextAlternative() const { // ARIA: section 2A, bullet #3 says if aria-labeledby or aria-label appears, it should // override the "label" element association. return ariaAccessibilityDescription().length(); } bool AccessibilityRenderObject::hasPopup() const { // Return true if this has the aria-haspopup attribute, or if it has an ancestor of type link with the aria-haspopup attribute. return Accessibility::findAncestor(*this, true, [this] (const AccessibilityObject& object) { return (this == &object) ? !equalLettersIgnoringASCIICase(object.popupValue(), "false") : object.isLink() && !equalLettersIgnoringASCIICase(object.popupValue(), "false"); }); } bool AccessibilityRenderObject::supportsDropping() const { return determineDropEffects().size(); } bool AccessibilityRenderObject::supportsDragging() const { const AtomString& grabbed = getAttribute(aria_grabbedAttr); return equalLettersIgnoringASCIICase(grabbed, "true") || equalLettersIgnoringASCIICase(grabbed, "false") || hasAttribute(draggableAttr); } bool AccessibilityRenderObject::isGrabbed() { #if ENABLE(DRAG_SUPPORT) if (mainFrame() && mainFrame()->eventHandler().draggingElement() == element()) return true; #endif return elementAttributeValue(aria_grabbedAttr); } Vector AccessibilityRenderObject::determineDropEffects() const { // Order is aria-dropeffect, dropzone, webkitdropzone const AtomString& dropEffects = getAttribute(aria_dropeffectAttr); if (!dropEffects.isEmpty()) { String dropEffectsString = dropEffects.string(); dropEffectsString.replace('\n', ' '); return dropEffectsString.split(' '); } auto dropzone = getAttribute(dropzoneAttr); if (!dropzone.isEmpty()) return Vector { dropzone }; auto webkitdropzone = getAttribute(webkitdropzoneAttr); if (!webkitdropzone.isEmpty()) return Vector { webkitdropzone }; return { }; } bool AccessibilityRenderObject::exposesTitleUIElement() const { if (!isControl() && !isFigureElement()) return false; // If this control is ignored (because it's invisible), // then the label needs to be exposed so it can be visible to accessibility. if (accessibilityIsIgnored()) return true; // When controls have their own descriptions, the title element should be ignored. if (hasTextAlternative()) return false; // When