/* * Copyright (C) 2008 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 "AXObjectCache.h" #include "AccessibilityImageMapLink.h" #include "AccessibilityListBox.h" #include "AccessibilitySVGRoot.h" #include "AccessibilitySpinButton.h" #include "AccessibilityTable.h" #include "CachedImage.h" #include "ElementIterator.h" #include "FloatRect.h" #include "Frame.h" #include "FrameLoader.h" #include "FrameSelection.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 "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 "ProgressTracker.h" #include "RenderButton.h" #include "RenderFieldset.h" #include "RenderFileUploadControl.h" #include "RenderHTMLCanvas.h" #include "RenderImage.h" #include "RenderInline.h" #include "RenderIterator.h" #include "RenderLayer.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 "htmlediting.h" #include #include #include namespace WebCore { using namespace HTMLNames; AccessibilityRenderObject::AccessibilityRenderObject(RenderObject* renderer) : AccessibilityNodeObject(renderer->node()) , m_renderer(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::detach(AccessibilityDetachmentType detachmentType, AXObjectCache* cache) { AccessibilityNodeObject::detach(detachmentType, cache); detachRemoteSVGRoot(); #ifndef NDEBUG if (m_renderer) m_renderer->setHasAXObject(false); #endif m_renderer = nullptr; } RenderBoxModelObject* AccessibilityRenderObject::renderBoxModelObject() const { if (!is(m_renderer)) return nullptr; return downcast(m_renderer); } void AccessibilityRenderObject::setRenderer(RenderObject* renderer) { m_renderer = 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() && firstChild && firstChild->isInlineElementContinuation()) 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(); RenderBoxModelObject* previous; for (auto* current = &downcast(renderer); current; ) { previous = current; if (RenderObject* newLastChild = current->lastChild()) lastChild = newLastChild; if (is(*current)) { current = downcast(*current).inlineElementContinuation(); ASSERT_UNUSED(previous, current || !downcast(*previous).continuation()); } else current = downcast(*current).inlineElementContinuation(); } 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(); return axObjectCache()->getOrCreate(firstChild); } AccessibilityObject* AccessibilityRenderObject::lastChild() const { if (!m_renderer) return nullptr; RenderObject* lastChild = lastChildConsideringContinuation(*m_renderer); if (!lastChild && !canHaveChildren()) return AccessibilityNodeObject::lastChild(); return axObjectCache()->getOrCreate(lastChild); } static inline RenderInline* startOfContinuations(RenderObject& renderer) { if (renderer.isInlineElementContinuation() && is(renderer.node()->renderer())) return downcast(renderer.node()->renderer()); // Blocks with a previous continuation always have a next continuation if (is(renderer) && downcast(renderer).inlineElementContinuation()) return downcast(downcast(renderer).inlineElementContinuation()->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; if (is(*current)) { current = downcast(*current).inlineElementContinuation(); ASSERT(current || !downcast(*previous).continuation()); } else current = downcast(*current).inlineElementContinuation(); } 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 = downcast(*currentContainer).continuation(); } else if (is(*currentContainer)) { if (currentContainer == child) return previous; previous = currentContainer; currentContainer = downcast(*currentContainer).inlineElementContinuation(); } } ASSERT_NOT_REACHED(); return nullptr; } static inline bool firstChildIsInlineContinuation(RenderElement& renderer) { RenderObject* child = renderer.firstChild(); return child && child->isInlineElementContinuation(); } 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, m_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; return axObjectCache()->getOrCreate(previousSibling); } static inline bool lastChildHasContinuation(RenderElement& renderer) { RenderObject* child = renderer.lastChild(); return child && isInlineWithContinuation(*child); } AccessibilityObject* AccessibilityRenderObject::nextSibling() const { if (!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).inlineElementContinuation())) 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 && 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; return axObjectCache()->getOrCreate(nextSibling); } static RenderBoxModelObject* nextContinuation(RenderObject& renderer) { if (is(renderer) && !renderer.isReplaced()) return downcast(renderer).continuation(); if (is(renderer)) return downcast(renderer).inlineElementContinuation(); 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() == MenuBarRole) return axObjectCache()->getOrCreate(m_renderer->parent()); // menuButton and its corresponding menu are DOM siblings, but Accessibility needs them to be parent/child if (ariaRoleAttribute() == MenuRole) { 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() == UnknownRole; } 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->absoluteClippedOverflowRect()); // 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 = m_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 AtomicString& ariaHelp = getAttribute(aria_helpAttr); if (!ariaHelp.isEmpty()) return ariaHelp; String describedBy = ariaDescribedByAttribute(); if (!describedBy.isEmpty()) return describedBy; String description = accessibilityDescription(); for (RenderObject* ancestor = m_renderer; ancestor; ancestor = ancestor->parent()) { if (is(ancestor->node())) { HTMLElement& element = downcast(*ancestor->node()); 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 = 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(ancestor); if (axObj) { AccessibilityRole role = axObj->roleValue(); if (role != GroupRole && role != UnknownRole) 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 ASCIILiteral("\n"); bool isRenderText = is(*m_renderer); 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. bool shouldIncludeAllChildren = mode.childrenInclusion == AccessibilityTextUnderElementMode::TextUnderElementModeIncludeAllChildren; if (isRenderText || shouldIncludeAllChildren) { // If possible, use a text iterator to get the text, so that whitespace // is handled consistently. Document* nodeDocument = nullptr; RefPtr textRange; if (Node* node = m_renderer->node()) { nodeDocument = &node->document(); textRange = rangeOfContents(*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 && lastChildRenderer) { ASSERT(firstChildRenderer->node()); ASSERT(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. Node* firstNodeInBlock = firstChildRenderer->node(); Position startPosition = positionInParentBeforeNode(firstNodeInBlock); Position endPosition = positionInParentAfterNode(lastChildRenderer->node()); nodeDocument = &firstNodeInBlock->document(); textRange = Range::create(*nodeDocument, startPosition, endPosition); } } if (nodeDocument && textRange) { if (Frame* frame = nodeDocument->frame()) { // catch stale WebCoreAXObject (see ) if (frame->document() != nodeDocument) return String(); // The tree should be stable before looking through the children of a non-Render Text object. // Otherwise, further uses of TextIterator will force a layout update, potentially altering // the accessibility tree and causing crashes in the loop that computes the result text. ASSERT((isRenderText || !shouldIncludeAllChildren) || (!nodeDocument->renderView()->layoutState() && !nodeDocument->childNeedsStyleRecalc())); return plainText(textRange.get(), 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 (ariaRoleAttribute() == StaticTextRole) { 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 AtomicString& overriddenDescription = listItems[selectedIndex]->attributeWithoutSynchronization(aria_labelAttr); if (!overriddenDescription.isNull()) return overriddenDescription; } return downcast(*m_renderer).text(); } if (is(*m_renderer)) return downcast(*m_renderer).text(); if (isWebArea()) return String(); if (isTextControl()) return text(); #if PLATFORM(IOS) 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(); } 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 = m_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(m_renderer); } Path AccessibilityRenderObject::elementPath() const { if (is(m_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::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()[0]->clickPoint(); // 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(); VisibleSelection visSelection = selection(); VisiblePositionRange range = VisiblePositionRange(visSelection.visibleStart(), visSelection.visibleEnd()); IntRect bounds = boundsForVisiblePositionRange(range); #if PLATFORM(COCOA) bounds.setLocation(m_renderer->view().frameView().screenToContents(bounds.location())); #endif return IntPoint(bounds.x() + (bounds.width() / 2), bounds.y() - (bounds.height() / 2)); } AccessibilityObject* AccessibilityRenderObject::internalLinkElement() const { Element* element = anchorElement(); // Right now, we do not support ARIA links as internal link elements if (!is(element)) return nullptr; HTMLAnchorElement& anchor = downcast(*element); URL linkURL = anchor.href(); String fragmentIdentifier = linkURL.fragmentIdentifier(); if (fragmentIdentifier.isEmpty()) return nullptr; // check if URL is the same as current URL URL documentURL = m_renderer->document().url(); if (!equalIgnoringFragmentIdentifier(documentURL, linkURL)) return nullptr; Node* 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); } ESpeak AccessibilityRenderObject::speakProperty() const { if (!m_renderer) return AccessibilityObject::speakProperty(); return m_renderer->style().speak(); } void AccessibilityRenderObject::addRadioButtonGroupChildren(AccessibilityObject* parent, AccessibilityChildrenVector& linkedUIElements) const { for (const auto& child : parent->children()) { if (child->roleValue() == RadioButtonRole) linkedUIElements.append(child); else addRadioButtonGroupChildren(child.get(), linkedUIElements); } } void AccessibilityRenderObject::addRadioButtonGroupMembers(AccessibilityChildrenVector& linkedUIElements) const { if (roleValue() != RadioButtonRole) return; Node* node = this->node(); if (is(node)) { HTMLInputElement& input = downcast(*node); for (auto& radioSibling : input.radioButtonGroup()) { if (AccessibilityObject* object = axObjectCache()->getOrCreate(radioSibling)) 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() == RadioGroupRole) 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() == RadioButtonRole) 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::ariaHasPopup() const { return elementAttributeValue(aria_haspopupAttr); } bool AccessibilityRenderObject::supportsARIADropping() const { const AtomicString& dropEffect = getAttribute(aria_dropeffectAttr); return !dropEffect.isEmpty(); } bool AccessibilityRenderObject::supportsARIADragging() const { const AtomicString& grabbed = getAttribute(aria_grabbedAttr); return equalLettersIgnoringASCIICase(grabbed, "true") || equalLettersIgnoringASCIICase(grabbed, "false"); } bool AccessibilityRenderObject::isARIAGrabbed() { return elementAttributeValue(aria_grabbedAttr); } void AccessibilityRenderObject::determineARIADropEffects(Vector& effects) { const AtomicString& dropEffects = getAttribute(aria_dropeffectAttr); if (dropEffects.isEmpty()) { effects.clear(); return; } String dropEffectsString = dropEffects.string(); dropEffectsString.replace('\n', ' '); dropEffectsString.split(' ', effects); } bool AccessibilityRenderObject::exposesTitleUIElement() const { if (!isControl() && !isFigure()) 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