blob: de2a0052b8345088f4cc1c7b572382b19f847a4c [file] [log] [blame]
/*
* 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 "AccessibilityLabel.h"
#include "AccessibilityListBox.h"
#include "AccessibilitySVGRoot.h"
#include "AccessibilitySpinButton.h"
#include "AccessibilityTable.h"
#include "CachedImage.h"
#include "Editing.h"
#include "Editor.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 "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 "ProgressTracker.h"
#include "RenderButton.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 <wtf/NeverDestroyed.h>
#include <wtf/StdLibExtras.h>
#include <wtf/unicode/CharacterNames.h>
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> 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<RenderBoxModelObject>(renderer()))
return nullptr;
return downcast<RenderBoxModelObject>(renderer());
}
void AccessibilityRenderObject::setRenderer(RenderObject* renderer)
{
m_renderer = makeWeakPtr(renderer);
setNode(renderer->node());
}
static inline bool isInlineWithContinuation(RenderObject& object)
{
return is<RenderInline>(object) && downcast<RenderInline>(object).continuation();
}
static inline RenderObject* firstChildInContinuation(RenderInline& renderer)
{
auto* continuation = renderer.continuation();
while (continuation) {
if (is<RenderBlock>(*continuation))
return continuation;
if (RenderObject* child = continuation->firstChild())
return child;
continuation = downcast<RenderInline>(*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<RenderInline>(firstChild) && downcast<RenderInline>(*firstChild).isContinuation())
firstChild = nullptr;
if (!firstChild && isInlineWithContinuation(renderer))
firstChild = firstChildInContinuation(downcast<RenderInline>(renderer));
return firstChild;
}
static inline RenderObject* lastChildConsideringContinuation(RenderObject& renderer)
{
if (!is<RenderInline>(renderer) && !is<RenderBlock>(renderer))
return &renderer;
RenderObject* lastChild = downcast<RenderBoxModelObject>(renderer).lastChild();
for (auto* current = &downcast<RenderBoxModelObject>(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 <meter> 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 (!is<RenderElement>(renderer))
return nullptr;
auto& renderElement = downcast<RenderElement>(renderer);
if (is<RenderInline>(renderElement) && renderElement.isContinuation() && is<RenderInline>(renderElement.element()->renderer()))
return downcast<RenderInline>(renderer.node()->renderer());
// Blocks with a previous continuation always have a next continuation
if (is<RenderBlock>(renderElement) && downcast<RenderBlock>(renderElement).inlineContinuation())
return downcast<RenderInline>(downcast<RenderBlock>(renderElement).inlineContinuation()->element()->renderer());
return nullptr;
}
static inline RenderObject* endOfContinuations(RenderObject& renderer)
{
if (!is<RenderInline>(renderer) && !is<RenderBlock>(renderer))
return &renderer;
auto* previous = &downcast<RenderBoxModelObject>(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<RenderInline>(*currentContainer)) {
auto* current = currentContainer->firstChild();
while (current) {
if (current == child)
return previous;
previous = current;
current = current->nextSibling();
}
currentContainer = currentContainer->continuation();
} else if (is<RenderBlock>(*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<RenderInline>(child) && downcast<RenderInline>(*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<RenderBox>(*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<RenderBlock>(*m_renderer))) {
RenderBlock& renderBlock = downcast<RenderBlock>(*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<RenderInline>(*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<RenderBlock>(*m_renderer) && (inlineContinuation = downcast<RenderBlock>(*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<RenderBlock>(*m_renderer))) {
RenderElement* lastParent = endOfContinuations(*downcast<RenderBlock>(*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<RenderInline>(*m_renderer->parent()).continuation();
// Case 5a: continuation is a block - in this case the block itself is the next sibling.
if (is<RenderBlock>(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;
// Make sure next sibling has the same parent.
AccessibilityObject* nextObj = axObjectCache()->getOrCreate(nextSibling);
if (nextObj && nextObj->parentObject() != this->parentObject())
return nullptr;
return nextObj;
}
static RenderBoxModelObject* nextContinuation(RenderObject& renderer)
{
if (is<RenderInline>(renderer) && !renderer.isReplaced())
return downcast<RenderInline>(renderer).continuation();
if (is<RenderBlock>(renderer))
return downcast<RenderBlock>(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<RenderBlock>(*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<RenderInline>(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<HTMLInputElement>(m_renderer->node())) {
HTMLInputElement& input = downcast<HTMLInputElement>(*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 = renderer(); currentRenderer && !currentRenderer->node(); currentRenderer = currentRenderer->parent()) {
if (currentRenderer->isAnonymousBlock()) {
if (RenderObject* continuation = downcast<RenderBlock>(*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<HTMLAnchorElement>(*node) || (node->renderer() && cache->getOrCreate(node->renderer())->isLink()))
return downcast<Element>(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 = renderer(); ancestor; ancestor = ancestor->parent()) {
if (is<HTMLElement>(ancestor->node())) {
HTMLElement& element = downcast<HTMLElement>(*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.
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<RenderFileUploadControl>(*m_renderer))
return downcast<RenderFileUploadControl>(*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<RenderText>(*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;
RefPtr<Range> 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 && 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.
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 <rdar://problem/3960196>)
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.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<RenderText>(*m_renderer)) {
RenderText& renderTextObject = downcast<RenderText>(*m_renderer);
if (is<RenderTextFragment>(renderTextObject)) {
RenderTextFragment& renderTextFragment = downcast<RenderTextFragment>(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<RenderText>(*m_renderer))
return textUnderElement();
if (is<RenderMenuList>(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<HTMLSelectElement>(*m_renderer->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]->attributeWithoutSynchronization(aria_labelAttr);
if (!overriddenDescription.isNull())
return overriddenDescription;
}
return downcast<RenderMenuList>(*m_renderer).text();
}
if (is<RenderListMarker>(*m_renderer))
return downcast<RenderListMarker>(*m_renderer).text();
if (isWebArea())
return String();
if (isTextControl())
return text();
#if PLATFORM(IOS_FAMILY)
if (isInputTypePopupButton())
return textUnderElement();
#endif
if (is<RenderFileUploadControl>(*m_renderer))
return downcast<RenderFileUploadControl>(*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<RenderText>(*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<HTMLLabelElement>(*parentNode))
return downcast<HTMLLabelElement>(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<FloatQuad> quads;
bool isSVGRoot = false;
if (obj->isSVGRoot())
isSVGRoot = true;
if (is<RenderText>(*obj))
quads = downcast<RenderText>(*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<Element>(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<RenderSVGShape>(renderer());
}
Path AccessibilityRenderObject::elementPath() const
{
if (is<RenderSVGShape>(renderer()) && downcast<RenderSVGShape>(*m_renderer).hasPath()) {
Path path = downcast<RenderSVGShape>(*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<RenderSVGRoot>(*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 (RefPtr<Range> range = elementRange()) {
VisiblePosition start = range->startPosition();
VisiblePosition end = nextVisiblePosition(start);
if (start.isNull() || !range->contains(end))
return AccessibilityObject::clickPoint();
RefPtr<Range> charRange = makeRange(start, end);
IntRect rect = boundsForRange(charRange);
return { rect.x() + rect.width() / 2, rect.y() + rect.height() / 2 };
}
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.
AccessibilityChildrenVector children = this->children();
if (isHeading() && children.size() == 1)
return children[0]->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();
VisibleSelection visSelection = selection();
VisiblePositionRange range = VisiblePositionRange(visSelection.visibleStart(), visSelection.visibleEnd());
IntRect bounds = boundsForVisiblePositionRange(range);
return { 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<HTMLAnchorElement>(element))
return nullptr;
HTMLAnchorElement& anchor = downcast<HTMLAnchorElement>(*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);
}
OptionSet<SpeakAs> AccessibilityRenderObject::speakAsProperty() const
{
if (!m_renderer)
return AccessibilityObject::speakAsProperty();
return m_renderer->style().speakAs();
}
void AccessibilityRenderObject::addRadioButtonGroupChildren(AccessibilityObject* 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<HTMLInputElement>(node)) {
HTMLInputElement& input = downcast<HTMLInputElement>(*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() == 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 !equalLettersIgnoringASCIICase(hasPopupValue(), "false");
}
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);
}
Vector<String> AccessibilityRenderObject::determineARIADropEffects()
{
const AtomicString& dropEffects = getAttribute(aria_dropeffectAttr);
if (dropEffects.isEmpty()) {
return { };
}
String dropEffectsString = dropEffects.string();
dropEffectsString.replace('\n', ' ');
return dropEffectsString.split(' ');
}
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 <label> element has aria-label or aria-labelledby on it, we shouldn't expose it as the
// titleUIElement, otherwise its inner text will be announced by a screenreader.
if (isLabelable()) {
if (HTMLLabelElement* label = labelForElement(downcast<Element>(node()))) {
if (!label->attributeWithoutSynchronization(aria_labelAttr).isEmpty())
return false;
if (AccessibilityObject* labelObject = axObjectCache()->getOrCreate(label)) {
if (!labelObject->ariaLabeledByAttribute().isEmpty())
return false;
// To simplify instances where the labeling element includes widget descendants
// which it does not label.
if (is<AccessibilityLabel>(*labelObject)
&& downcast<AccessibilityLabel>(*labelObject).containsUnrelatedControls())
return false;
}
}
}
return true;
}
#if ENABLE(APPLE_PAY)
String AccessibilityRenderObject::applePayButtonDescription() const
{
switch (applePayButtonType()) {
case ApplePayButtonType::Plain:
return AXApplePayPlainLabel();
case ApplePayButtonType::Buy:
return AXApplePayBuyLabel();
case ApplePayButtonType::SetUp:
return AXApplePaySetupLabel();
case ApplePayButtonType::Donate:
return AXApplePayDonateLabel();
#if ENABLE(APPLE_PAY_SESSION_V4)
case ApplePayButtonType::CheckOut:
return AXApplePayCheckOutLabel();
case ApplePayButtonType::Book:
return AXApplePayBookLabel();
case ApplePayButtonType::Subscribe:
return AXApplePaySubscribeLabel();
#endif
}
}
#endif
void AccessibilityRenderObject::titleElementText(Vector<AccessibilityText>& textOrder) const
{
#if ENABLE(APPLE_PAY)
if (isApplePayButton()) {
textOrder.append(AccessibilityText(applePayButtonDescription(), AccessibilityTextSource::Alternative));
return;
}
#endif
AccessibilityNodeObject::titleElementText(textOrder);
}
AccessibilityObject* AccessibilityRenderObject::titleUIElement() const
{
if (!m_renderer)
return nullptr;
// if isFieldset is true, the renderer is guaranteed to be a RenderFieldset
if (isFieldset())
return axObjectCache()->getOrCreate(downcast<RenderBlock>(*m_renderer).findFieldsetLegend(RenderBlock::FieldsetIncludeFloatingOrOutOfFlow));
if (isFigureElement())
return captionForFigure();
Node* node = m_renderer->node();
if (!is<Element>(node))
return nullptr;
HTMLLabelElement* label = labelForElement(downcast<Element>(node));
if (label && label->renderer())
return axObjectCache()->getOrCreate(label);
return nullptr;
}
bool AccessibilityRenderObject::isAllowedChildOfTree() const
{
// Determine if this is in a tree. If so, we apply special behavior to make it work like an AXOutline.
AccessibilityObject* axObj = parentObject();
bool isInTree = false;
bool isTreeItemDescendant = false;
while (axObj) {
if (axObj->roleValue() == AccessibilityRole::TreeItem)
isTreeItemDescendant = true;
if (axObj->isTree()) {
isInTree = true;
break;
}
axObj = axObj->parentObject();
}
// If the object is in a tree, only tree items should be exposed (and the children of tree items).
if (isInTree) {
AccessibilityRole role = roleValue();
if (role != AccessibilityRole::TreeItem && role != AccessibilityRole::StaticText && !isTreeItemDescendant)
return false;
}
return true;
}
static AccessibilityObjectInclusion objectInclusionFromAltText(const String& altText)
{
// Don't ignore an image that has an alt tag.
if (!altText.isAllSpecialCharacters<isHTMLSpace>())
return AccessibilityObjectInclusion::IncludeObject;
// The informal standard is to ignore images with zero-length alt strings.
if (!altText.isNull())
return AccessibilityObjectInclusion::IgnoreObject;
return AccessibilityObjectInclusion::DefaultBehavior;
}
AccessibilityObjectInclusion AccessibilityRenderObject::defaultObjectInclusion() const
{
// The following cases can apply to any element that's a subclass of AccessibilityRenderObject.
if (!m_renderer)
return AccessibilityObjectInclusion::IgnoreObject;
if (m_renderer->style().visibility() != Visibility::Visible) {
// aria-hidden is meant to override visibility as the determinant in AX hierarchy inclusion.
if (equalLettersIgnoringASCIICase(getAttribute(aria_hiddenAttr), "false"))
return AccessibilityObjectInclusion::DefaultBehavior;
return AccessibilityObjectInclusion::IgnoreObject;
}
return AccessibilityObject::defaultObjectInclusion();
}
static bool webAreaIsPresentational(RenderObject* renderer)
{
if (!renderer || !is<RenderView>(*renderer))
return false;
if (auto ownerElement = renderer->document().ownerElement())
return nodeHasPresentationRole(ownerElement);
return false;
}
bool AccessibilityRenderObject::computeAccessibilityIsIgnored() const
{
#ifndef NDEBUG
ASSERT(m_initialized);
#endif
if (!m_renderer)
return true;
// Check first if any of the common reasons cause this element to be ignored.
// Then process other use cases that need to be applied to all the various roles
// that AccessibilityRenderObjects take on.
AccessibilityObjectInclusion decision = defaultObjectInclusion();
if (decision == AccessibilityObjectInclusion::IncludeObject)
return false;
if (decision == AccessibilityObjectInclusion::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() == AccessibilityRole::Ignored)
return true;
if (roleValue() == AccessibilityRole::Presentational || inheritsPresentationalRole())
return true;
// WebAreas should be ignored if their iframe container is marked as presentational.
if (webAreaIsPresentational(renderer()))
return true;
// An ARIA tree can only have tree items and static text as children.
if (!isAllowedChildOfTree())
return true;
// Allow the platform to decide if the attachment is ignored or not.
if (isAttachment())
return accessibilityIgnoreAttachment();
// ignore popup menu items because AppKit does
if (m_renderer && ancestorsOfType<RenderMenuList>(*m_renderer).first())
return true;
// https://webkit.org/b/161276 Getting the controlObject might cause the m_renderer to be nullptr.
if (!m_renderer)
return true;
if (m_renderer->isBR())
return true;
if (is<RenderText>(*m_renderer)) {
// static text beneath MenuItems and MenuButtons are just reported along with the menu item, so it's ignored on an individual level
AccessibilityObject* parent = parentObjectUnignored();
if (parent && (parent->isMenuItem() || parent->ariaRoleAttribute() == AccessibilityRole::MenuButton))
return true;
auto& renderText = downcast<RenderText>(*m_renderer);
if (!renderText.hasRenderedText())
return true;
if (renderText.parent()->isFirstLetter())
return true;
// static text beneath TextControls is reported along with the text control text so it's ignored.
for (AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent->roleValue() == AccessibilityRole::TextField)
return true;
}
// Walking up the parent chain might reset the m_renderer.
if (!m_renderer)
return true;
// The alt attribute may be set on a text fragment through CSS, which should be honored.
if (is<RenderTextFragment>(renderText)) {
AccessibilityObjectInclusion altTextInclusion = objectInclusionFromAltText(downcast<RenderTextFragment>(renderText).altText());
if (altTextInclusion == AccessibilityObjectInclusion::IgnoreObject)
return true;
if (altTextInclusion == AccessibilityObjectInclusion::IncludeObject)
return false;
}
// text elements that are just empty whitespace should not be returned
return renderText.text().isAllSpecialCharacters<isHTMLSpace>();
}
if (isHeading())
return false;
if (isLink())
return false;
if (isLandmark())
return false;
// all controls are accessible
if (isControl())
return false;
if (isFigureElement())
return false;
switch (roleValue()) {
case AccessibilityRole::Audio:
case AccessibilityRole::DescriptionListTerm:
case AccessibilityRole::DescriptionListDetail:
case AccessibilityRole::Details:
case AccessibilityRole::DocumentArticle:
case AccessibilityRole::Footer:
case AccessibilityRole::LandmarkRegion:
case AccessibilityRole::ListItem:
case AccessibilityRole::Time:
case AccessibilityRole::Video:
return false;
default:
break;
}
if (ariaRoleAttribute() != AccessibilityRole::Unknown)
return false;
if (roleValue() == AccessibilityRole::HorizontalRule)
return false;
// don't ignore labels, because they serve as TitleUIElements
Node* node = m_renderer->node();
if (is<HTMLLabelElement>(node))
return false;
// Anything that is content editable should not be ignored.
// However, one cannot just call node->hasEditableStyle() since that will ask if its parents
// are also editable. Only the top level content editable region should be exposed.
if (hasContentEditableAttributeSet())
return false;
// if this element has aria attributes on it, it should not be ignored.
if (supportsARIAAttributes())
return false;
#if ENABLE(MATHML)
// First check if this is a special case within the math tree that needs to be ignored.
if (isIgnoredElementWithinMathTree())
return true;
// Otherwise all other math elements are in the tree.
if (isMathElement())
return false;
#endif
if (is<RenderBlockFlow>(*m_renderer) && m_renderer->childrenInline() && !canSetFocusAttribute())
return !downcast<RenderBlockFlow>(*m_renderer).hasLines() && !mouseButtonListener();
// ignore images seemingly used as spacers
if (isImage()) {
// If the image can take focus, it should not be ignored, lest the user not be able to interact with something important.
if (canSetFocusAttribute())
return false;
// First check the RenderImage's altText (which can be set through a style sheet, or come from the Element).
// However, if this is not a native image, fallback to the attribute on the Element.
AccessibilityObjectInclusion altTextInclusion = AccessibilityObjectInclusion::DefaultBehavior;
bool isRenderImage = is<RenderImage>(renderer());
if (isRenderImage)
altTextInclusion = objectInclusionFromAltText(downcast<RenderImage>(*m_renderer).altText());
else
altTextInclusion = objectInclusionFromAltText(getAttribute(altAttr).string());
if (altTextInclusion == AccessibilityObjectInclusion::IgnoreObject)
return true;
if (altTextInclusion == AccessibilityObjectInclusion::IncludeObject)
return false;
// If an image has a title attribute on it, accessibility should be lenient and allow it to appear in the hierarchy (according to WAI-ARIA).
if (!getAttribute(titleAttr).isEmpty())
return false;
if (isRenderImage) {
// check for one-dimensional image
RenderImage& image = downcast<RenderImage>(*m_renderer);
if (image.height() <= 1 || image.width() <= 1)
return true;
// check whether rendered image was stretched from one-dimensional file image
if (image.cachedImage()) {
LayoutSize imageSize = image.cachedImage()->imageSizeForRenderer(&image, image.view().zoomFactor());
return imageSize.height() <= 1 || imageSize.width() <= 1;
}
}
return false;
}
if (isCanvas()) {
if (canvasHasFallbackContent())
return false;
if (is<RenderBox>(*m_renderer)) {
auto& canvasBox = downcast<RenderBox>(*m_renderer);
if (canvasBox.height() <= 1 || canvasBox.width() <= 1)
return true;
}
// Otherwise fall through; use presence of help text, title, or description to decide.
}
if (m_renderer->isListMarker()) {
AccessibilityObject* parent = parentObjectUnignored();
return parent && !parent->isListItem();
}
if (isWebArea())
return false;
#if ENABLE(METER_ELEMENT)
// The render tree of meter includes a RenderBlock (meter) and a RenderMeter (div).
// We expose the latter and thus should ignore the former. However, if the author
// includes a title attribute on the element, hasAttributesRequiredForInclusion()
// will return true, potentially resulting in a redundant accessible object.
if (is<HTMLMeterElement>(node))
return true;
#endif
// Using the presence of an accessible name to decide an element's visibility is not
// as definitive as previous checks, so this should remain as one of the last.
if (hasAttributesRequiredForInclusion())
return false;
// Don't ignore generic focusable elements like <div tabindex=0>
// unless they're completely empty, with no children.
if (isGenericFocusableElement() && node->firstChild())
return false;
// <span> tags are inline tags and not meant to convey information if they have no other aria
// information on them. If we don't ignore them, they may emit signals expected to come from
// their parent. In addition, because included spans are AccessibilityRole::Group objects, and AccessibilityRole::Group
// objects are often containers with meaningful information, the inclusion of a span can have
// the side effect of causing the immediate parent accessible to be ignored. This is especially
// problematic for platforms which have distinct roles for textual block elements.
if (node && node->hasTagName(spanTag))
return true;
// Other non-ignored host language elements
if (node && node->hasTagName(dfnTag))
return false;
if (isStyleFormatGroup())
return false;
// Make sure that ruby containers are not ignored.
if (m_renderer->isRubyRun() || m_renderer->isRubyBlock() || m_renderer->isRubyInline())
return false;
// Find out if this element is inside of a label element.
// If so, it may be ignored because it's the label for a checkbox or radio button.
AccessibilityObject* controlObject = correspondingControlForLabelElement();
if (controlObject && !controlObject->exposesTitleUIElement() && controlObject->isCheckboxOrRadio())
return true;
// By default, objects should be ignored so that the AX hierarchy is not
// filled with unnecessary items.
return true;
}
bool AccessibilityRenderObject::isLoaded() const
{
return !m_renderer->document().parser();
}
double AccessibilityRenderObject::estimatedLoadingProgress() const
{
if (!m_renderer)
return 0;
if (isLoaded())
return 1.0;
return m_renderer->page().progress().estimatedProgress();
}
int AccessibilityRenderObject::layoutCount() const
{
if (!m_renderer || !is<RenderView>(*m_renderer))
return 0;
return downcast<RenderView>(*m_renderer).frameView().layoutContext().layoutCount();
}
String AccessibilityRenderObject::text() const
{
if (isPasswordField())
return passwordFieldValue();
return AccessibilityNodeObject::text();
}
int AccessibilityRenderObject::textLength() const
{
ASSERT(isTextControl());
if (isPasswordField())
return passwordFieldValue().length();
return text().length();
}
PlainTextRange AccessibilityRenderObject::documentBasedSelectedTextRange() const
{
Node* node = m_renderer->node();
if (!node)
return PlainTextRange();
VisibleSelection visibleSelection = selection();
RefPtr<Range> currentSelectionRange = visibleSelection.toNormalizedRange();
if (!currentSelectionRange)
return PlainTextRange();
// FIXME: The reason this does the correct thing when the selection is in the
// shadow tree of an input element is that we get an exception below, and we
// choose to interpret all exceptions as "does not intersect". Seems likely
// that does not handle all cases correctly.
auto intersectsResult = currentSelectionRange->intersectsNode(*node);
if (!intersectsResult.hasException() && !intersectsResult.releaseReturnValue())
return PlainTextRange();
int start = indexForVisiblePosition(visibleSelection.start());
int end = indexForVisiblePosition(visibleSelection.end());
return PlainTextRange(start, end - start);
}
String AccessibilityRenderObject::selectedText() const
{
ASSERT(isTextControl());
if (isPasswordField())
return String(); // need to return something distinct from empty string
if (isNativeTextControl()) {
HTMLTextFormControlElement& textControl = downcast<RenderTextControl>(*m_renderer).textFormControlElement();
return textControl.selectedText();
}
return doAXStringForRange(documentBasedSelectedTextRange());
}
const AtomicString& AccessibilityRenderObject::accessKey() const
{
Node* node = m_renderer->node();
if (!is<Element>(node))
return nullAtom();
return downcast<Element>(*node).attributeWithoutSynchronization(accesskeyAttr);
}
VisibleSelection AccessibilityRenderObject::selection() const
{
return m_renderer->frame().selection().selection();
}
PlainTextRange AccessibilityRenderObject::selectedTextRange() const
{
ASSERT(isTextControl());
if (isPasswordField())
return PlainTextRange();
AccessibilityRole ariaRole = ariaRoleAttribute();
// Use the text control native range if it's a native object and it has no ARIA role (or has a text based ARIA role).
if (isNativeTextControl() && (ariaRole == AccessibilityRole::Unknown || isARIATextControl())) {
HTMLTextFormControlElement& textControl = downcast<RenderTextControl>(*m_renderer).textFormControlElement();
return PlainTextRange(textControl.selectionStart(), textControl.selectionEnd() - textControl.selectionStart());
}
return documentBasedSelectedTextRange();
}
static void setTextSelectionIntent(AXObjectCache* cache, AXTextStateChangeType type)
{
if (!cache)
return;
AXTextStateChangeIntent intent(type, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, false });
cache->setTextSelectionIntent(intent);
cache->setIsSynchronizingSelection(true);
}
static void clearTextSelectionIntent(AXObjectCache* cache)
{
if (!cache)
return;
cache->setTextSelectionIntent(AXTextStateChangeIntent());
cache->setIsSynchronizingSelection(false);
}
void AccessibilityRenderObject::setSelectedTextRange(const PlainTextRange& range)
{
setTextSelectionIntent(axObjectCache(), range.length ? AXTextStateChangeTypeSelectionExtend : AXTextStateChangeTypeSelectionMove);
if (isNativeTextControl()) {
HTMLTextFormControlElement& textControl = downcast<RenderTextControl>(*m_renderer).textFormControlElement();
textControl.setSelectionRange(range.start, range.start + range.length);
} else {
ASSERT(node());
VisiblePosition start = visiblePositionForIndexUsingCharacterIterator(*node(), range.start);
VisiblePosition end = visiblePositionForIndexUsingCharacterIterator(*node(), range.start + range.length);
m_renderer->frame().selection().setSelection(VisibleSelection(start, end), FrameSelection::defaultSetSelectionOptions(UserTriggered));
}
clearTextSelectionIntent(axObjectCache());
}
URL AccessibilityRenderObject::url() const
{
if (isLink() && is<HTMLAnchorElement>(*m_renderer->node())) {
if (HTMLAnchorElement* anchor = downcast<HTMLAnchorElement>(anchorElement()))
return anchor->href();
}
if (isWebArea())
return m_renderer->document().url();
if (isImage() && is<HTMLImageElement>(m_renderer->node()))
return downcast<HTMLImageElement>(*m_renderer->node()).src();
if (isInputImage())
return downcast<HTMLInputElement>(*m_renderer->node()).src();
return URL();
}
bool AccessibilityRenderObject::isUnvisited() const
{
if (!m_renderer)
return true;
// FIXME: Is it a privacy violation to expose unvisited information to accessibility APIs?
return m_renderer->style().isLink() && m_renderer->style().insideLink() == InsideLink::InsideUnvisited;
}
bool AccessibilityRenderObject::isVisited() const
{
if (!m_renderer)
return false;
// FIXME: Is it a privacy violation to expose visited information to accessibility APIs?
return m_renderer->style().isLink() && m_renderer->style().insideLink() == InsideLink::InsideVisited;
}
void AccessibilityRenderObject::setElementAttributeValue(const QualifiedName& attributeName, bool value)
{
if (!m_renderer)
return;
Node* node = m_renderer->node();
if (!is<Element>(node))
return;
downcast<Element>(*node).setAttribute(attributeName, (value) ? "true" : "false");
}
bool AccessibilityRenderObject::elementAttributeValue(const QualifiedName& attributeName) const
{
if (!m_renderer)
return false;
return equalLettersIgnoringASCIICase(getAttribute(attributeName), "true");
}
bool AccessibilityRenderObject::isSelected() const
{
if (!m_renderer)
return false;
if (!m_renderer->node())
return false;
if (equalLettersIgnoringASCIICase(getAttribute(aria_selectedAttr), "true"))
return true;
if (isTabItem() && isTabItemSelected())
return true;
// Menu items are considered selectable by assistive technologies
if (isMenuItem())
return isFocused() || parentObjectUnignored()->activeDescendant() == this;
return false;
}
bool AccessibilityRenderObject::isTabItemSelected() const
{
if (!isTabItem() || !m_renderer)
return false;
Node* node = m_renderer->node();
if (!node || !node->isElementNode())
return false;
// The ARIA spec says a tab item can also be selected if it is aria-labeled by a tabpanel
// that has keyboard focus inside of it, or if a tabpanel in its aria-controls list has KB
// focus inside of it.
AccessibilityObject* focusedElement = static_cast<AccessibilityObject*>(focusedUIElement());
if (!focusedElement)
return false;
Vector<Element*> elements;
elementsFromAttribute(elements, aria_controlsAttr);
AXObjectCache* cache = axObjectCache();
if (!cache)
return false;
for (const auto& element : elements) {
AccessibilityObject* tabPanel = cache->getOrCreate(element);
// A tab item should only control tab panels.
if (!tabPanel || tabPanel->roleValue() != AccessibilityRole::TabPanel)
continue;
AccessibilityObject* checkFocusElement = focusedElement;
// Check if the focused element is a descendant of the element controlled by the tab item.
while (checkFocusElement) {
if (tabPanel == checkFocusElement)
return true;
checkFocusElement = checkFocusElement->parentObject();
}
}
return false;
}
bool AccessibilityRenderObject::isFocused() const
{
if (!m_renderer)
return false;
Document& document = m_renderer->document();
Element* focusedElement = document.focusedElement();
if (!focusedElement)
return false;
// A web area is represented by the Document node in the DOM tree, which isn't focusable.
// Check instead if the frame's selection controller is focused
if (focusedElement == m_renderer->node()
|| (roleValue() == AccessibilityRole::WebArea && document.frame()->selection().isFocusedAndActive()))
return true;
return false;
}
void AccessibilityRenderObject::setFocused(bool on)
{
if (!canSetFocusAttribute())
return;
Document* document = this->document();
Node* node = this->node();
if (!on || !is<Element>(node)) {
document->setFocusedElement(nullptr);
return;
}
// When a node is told to set focus, that can cause it to be deallocated, which means that doing
// anything else inside this object will crash. To fix this, we added a RefPtr to protect this object
// long enough for duration.
RefPtr<AccessibilityObject> protectedThis(this);
// If this node is already the currently focused node, then calling focus() won't do anything.
// That is a problem when focus is removed from the webpage to chrome, and then returns.
// In these cases, we need to do what keyboard and mouse focus do, which is reset focus first.
if (document->focusedElement() == node)
document->setFocusedElement(nullptr);
// If we return from setFocusedElement and our element has been removed from a tree, axObjectCache() may be null.
if (AXObjectCache* cache = axObjectCache()) {
cache->setIsSynchronizingSelection(true);
downcast<Element>(*node).focus();
cache->setIsSynchronizingSelection(false);
}
}
void AccessibilityRenderObject::setSelectedRows(AccessibilityChildrenVector& selectedRows)
{
// Setting selected only makes sense in trees and tables (and tree-tables).
AccessibilityRole role = roleValue();
if (role != AccessibilityRole::Tree && role != AccessibilityRole::TreeGrid && role != AccessibilityRole::Table && role != AccessibilityRole::Grid)
return;
bool isMulti = isMultiSelectable();
unsigned count = selectedRows.size();
if (count > 1 && !isMulti)
count = 1;
for (const auto& selectedRow : selectedRows)
selectedRow->setSelected(true);
}
void AccessibilityRenderObject::setValue(const String& string)
{
if (!m_renderer || !is<Element>(m_renderer->node()))
return;
Element& element = downcast<Element>(*m_renderer->node());
RenderObject& renderer = *m_renderer;
// We should use the editor's insertText to mimic typing into the field.
// Also only do this when the field is in editing mode.
if (Frame* frame = renderer.document().frame()) {
Editor& editor = frame->editor();
if (element.shouldUseInputMethod()) {
editor.clearText();
editor.insertText(string, nullptr);
return;
}
}
// FIXME: Do we want to do anything here for ARIA textboxes?
if (renderer.isTextField() && is<HTMLInputElement>(element))
downcast<HTMLInputElement>(element).setValue(string);
else if (renderer.isTextArea() && is<HTMLTextAreaElement>(element))
downcast<HTMLTextAreaElement>(element).setValue(string);
}
bool AccessibilityRenderObject::supportsARIAOwns() const
{
if (!m_renderer)
return false;
const AtomicString& ariaOwns = getAttribute(aria_ownsAttr);
return !ariaOwns.isEmpty();
}
RenderView* AccessibilityRenderObject::topRenderer() const
{
Document* topDoc = topDocument();
if (!topDoc)
return nullptr;
return topDoc->renderView();
}
Document* AccessibilityRenderObject::document() const
{
if (!m_renderer)
return nullptr;
return &m_renderer->document();
}
Widget* AccessibilityRenderObject::widget() const
{
if (!m_renderer || !is<RenderWidget>(*m_renderer))
return nullptr;
return downcast<RenderWidget>(*m_renderer).widget();
}
AccessibilityObject* AccessibilityRenderObject::accessibilityParentForImageMap(HTMLMapElement* map) const
{
// find an image that is using this map
if (!map)
return nullptr;
HTMLImageElement* imageElement = map->imageElement();
if (!imageElement)
return nullptr;
if (AXObjectCache* cache = axObjectCache())
return cache->getOrCreate(imageElement);
return nullptr;
}
void AccessibilityRenderObject::getDocumentLinks(AccessibilityChildrenVector& result)
{
Document& document = m_renderer->document();
Ref<HTMLCollection> links = document.links();
for (unsigned i = 0; auto* current = links->item(i); ++i) {
if (auto* renderer = current->renderer()) {
RefPtr<AccessibilityObject> axObject = document.axObjectCache()->getOrCreate(renderer);
ASSERT(axObject);
if (!axObject->accessibilityIsIgnored() && axObject->isLink())
result.append(axObject);
} else {
auto* parent = current->parentNode();
if (is<HTMLAreaElement>(*current) && is<HTMLMapElement>(parent)) {
auto& areaObject = downcast<AccessibilityImageMapLink>(*axObjectCache()->getOrCreate(AccessibilityRole::ImageMapLink));
HTMLMapElement& map = downcast<HTMLMapElement>(*parent);
areaObject.setHTMLAreaElement(downcast<HTMLAreaElement>(current));
areaObject.setHTMLMapElement(&map);
areaObject.setParent(accessibilityParentForImageMap(&map));
result.append(&areaObject);
}
}
}
}
FrameView* AccessibilityRenderObject::documentFrameView() const
{
if (!m_renderer)
return nullptr;
// this is the RenderObject's Document's Frame's FrameView
return &m_renderer->view().frameView();
}
Widget* AccessibilityRenderObject::widgetForAttachmentView() const
{
if (!isAttachment())
return nullptr;
return downcast<RenderWidget>(*m_renderer).widget();
}
// This function is like a cross-platform version of - (WebCoreTextMarkerRange*)textMarkerRange. It returns
// a Range that we can convert to a WebCoreTextMarkerRange in the Obj-C file
VisiblePositionRange AccessibilityRenderObject::visiblePositionRange() const
{
if (!m_renderer)
return VisiblePositionRange();
// construct VisiblePositions for start and end
Node* node = m_renderer->node();
if (!node)
return VisiblePositionRange();
VisiblePosition startPos = firstPositionInOrBeforeNode(node);
VisiblePosition endPos = lastPositionInOrAfterNode(node);
// the VisiblePositions are equal for nodes like buttons, so adjust for that
// FIXME: Really? [button, 0] and [button, 1] are distinct (before and after the button)
// I expect this code is only hit for things like empty divs? In which case I don't think
// the behavior is correct here -- eseidel
if (startPos == endPos) {
endPos = endPos.next();
if (endPos.isNull())
endPos = startPos;
}
return VisiblePositionRange(startPos, endPos);
}
VisiblePositionRange AccessibilityRenderObject::visiblePositionRangeForLine(unsigned lineCount) const
{
if (!lineCount || !m_renderer)
return VisiblePositionRange();
// iterate over the lines
// FIXME: this is wrong when lineNumber is lineCount+1, because nextLinePosition takes you to the
// last offset of the last line
VisiblePosition visiblePos = m_renderer->view().positionForPoint(IntPoint(), nullptr);
VisiblePosition savedVisiblePos;
while (--lineCount) {
savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
if (visiblePos.isNull() || visiblePos == savedVisiblePos)
return VisiblePositionRange();
}
// make a caret selection for the marker position, then extend it to the line
// NOTE: ignores results of sel.modify because it returns false when
// starting at an empty line. The resulting selection in that case
// will be a caret at visiblePos.
FrameSelection selection;
selection.setSelection(VisibleSelection(visiblePos));
selection.modify(FrameSelection::AlterationExtend, DirectionRight, LineBoundary);
return VisiblePositionRange(selection.selection().visibleStart(), selection.selection().visibleEnd());
}
VisiblePosition AccessibilityRenderObject::visiblePositionForIndex(int index) const
{
if (!m_renderer)
return VisiblePosition();
if (isNativeTextControl())
return downcast<RenderTextControl>(*m_renderer).textFormControlElement().visiblePositionForIndex(index);
if (!allowsTextRanges() && !is<RenderText>(*m_renderer))
return VisiblePosition();
Node* node = m_renderer->node();
if (!node)
return VisiblePosition();
return visiblePositionForIndexUsingCharacterIterator(*node, index);
}
int AccessibilityRenderObject::indexForVisiblePosition(const VisiblePosition& position) const
{
if (isNativeTextControl())
return downcast<RenderTextControl>(*m_renderer).textFormControlElement().indexForVisiblePosition(position);
if (!isTextControl())
return 0;
Node* node = m_renderer->node();
if (!node)
return 0;
Position indexPosition = position.deepEquivalent();
if (indexPosition.isNull() || highestEditableRoot(indexPosition, HasEditableAXRole) != node)
return 0;
#if PLATFORM(GTK)
// We need to consider replaced elements for GTK, as they will be
// presented with the 'object replacement character' (0xFFFC).
bool forSelectionPreservation = true;
#else
bool forSelectionPreservation = false;
#endif
return WebCore::indexForVisiblePosition(*node, position, forSelectionPreservation);
}
Element* AccessibilityRenderObject::rootEditableElementForPosition(const Position& position) const
{
// Find the root editable or pseudo-editable (i.e. having an editable ARIA role) element.
Element* result = nullptr;
Element* rootEditableElement = position.rootEditableElement();
for (Element* e = position.element(); e && e != rootEditableElement; e = e->parentElement()) {
if (nodeIsTextControl(e))
result = e;
if (e->hasTagName(bodyTag))
break;
}
if (result)
return result;
return rootEditableElement;
}
bool AccessibilityRenderObject::nodeIsTextControl(const Node* node) const
{
if (!node)
return false;
if (AXObjectCache* cache = axObjectCache()) {
if (AccessibilityObject* axObjectForNode = cache->getOrCreate(const_cast<Node*>(node)))
return axObjectForNode->isTextControl();
}
return false;
}
IntRect AccessibilityRenderObject::boundsForRects(LayoutRect const& rect1, LayoutRect const& rect2, RefPtr<Range> const& dataRange)
{
LayoutRect ourRect = rect1;
ourRect.unite(rect2);
// if the rectangle spans lines and contains multiple text chars, use the range's bounding box intead
if (rect1.maxY() != rect2.maxY()) {
LayoutRect boundingBox = dataRange->absoluteBoundingBox();
String rangeString = plainText(dataRange.get());
if (rangeString.length() > 1 && !boundingBox.isEmpty())
ourRect = boundingBox;
}
return snappedIntRect(ourRect);
}
IntRect AccessibilityRenderObject::boundsForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
{
if (visiblePositionRange.isNull())
return IntRect();
// Create a mutable VisiblePositionRange.
VisiblePositionRange range(visiblePositionRange);
LayoutRect rect1 = range.start.absoluteCaretBounds();
LayoutRect rect2 = range.end.absoluteCaretBounds();
// readjust for position at the edge of a line. This is to exclude line rect that doesn't need to be accounted in the range bounds
if (rect2.y() != rect1.y()) {
VisiblePosition endOfFirstLine = endOfLine(range.start);
if (range.start == endOfFirstLine) {
range.start.setAffinity(DOWNSTREAM);
rect1 = range.start.absoluteCaretBounds();
}
if (range.end == endOfFirstLine) {
range.end.setAffinity(UPSTREAM);
rect2 = range.end.absoluteCaretBounds();
}
}
RefPtr<Range> dataRange = makeRange(range.start, range.end);
return boundsForRects(rect1, rect2, dataRange);
}
IntRect AccessibilityRenderObject::boundsForRange(const RefPtr<Range> range) const
{
if (!range)
return IntRect();
AXObjectCache* cache = this->axObjectCache();
if (!cache)
return IntRect();
CharacterOffset start = cache->startOrEndCharacterOffsetForRange(range, true);
CharacterOffset end = cache->startOrEndCharacterOffsetForRange(range, false);
LayoutRect rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
LayoutRect rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
// readjust for position at the edge of a line. This is to exclude line rect that doesn't need to be accounted in the range bounds.
if (rect2.y() != rect1.y()) {
CharacterOffset endOfFirstLine = cache->endCharacterOffsetOfLine(start);
if (start.isEqual(endOfFirstLine)) {
start = cache->nextCharacterOffset(start, false);
rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
}
if (end.isEqual(endOfFirstLine)) {
end = cache->previousCharacterOffset(end, false);
rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
}
}
return boundsForRects(rect1, rect2, range);
}
bool AccessibilityRenderObject::isVisiblePositionRangeInDifferentDocument(const VisiblePositionRange& range) const
{
if (range.start.isNull() || range.end.isNull())
return false;
VisibleSelection newSelection = VisibleSelection(range.start, range.end);
if (Document* newSelectionDocument = newSelection.base().document()) {
if (RefPtr<Frame> newSelectionFrame = newSelectionDocument->frame()) {
Frame* frame = this->frame();
if (!frame || (newSelectionFrame != frame && newSelectionDocument != frame->document()))
return true;
}
}
return false;
}
void AccessibilityRenderObject::setSelectedVisiblePositionRange(const VisiblePositionRange& range) const
{
if (range.start.isNull() || range.end.isNull())
return;
// In WebKit1, when the top web area sets the selection to be an input element in an iframe, the caret will disappear.
// FrameSelection::setSelectionWithoutUpdatingAppearance is setting the selection on the new frame in this case, and causing this behavior.
if (isWebArea() && parentObject() && parentObject()->isAttachment()) {
if (isVisiblePositionRangeInDifferentDocument(range))
return;
}
// make selection and tell the document to use it. if it's zero length, then move to that position
if (range.start == range.end) {
setTextSelectionIntent(axObjectCache(), AXTextStateChangeTypeSelectionMove);
m_renderer->frame().selection().moveTo(range.start, UserTriggered);
clearTextSelectionIntent(axObjectCache());
}
else {
setTextSelectionIntent(axObjectCache(), AXTextStateChangeTypeSelectionExtend);
VisibleSelection newSelection = VisibleSelection(range.start, range.end);
m_renderer->frame().selection().setSelection(newSelection, FrameSelection::defaultSetSelectionOptions());
clearTextSelectionIntent(axObjectCache());
}
}
VisiblePosition AccessibilityRenderObject::visiblePositionForPoint(const IntPoint& point) const
{
if (!m_renderer)
return VisiblePosition();
// convert absolute point to view coordinates
RenderView* renderView = topRenderer();
if (!renderView)
return VisiblePosition();
#if PLATFORM(COCOA)
FrameView* frameView = &renderView->frameView();
#endif
Node* innerNode = nullptr;
// locate the node containing the point
LayoutPoint pointResult;
while (1) {
LayoutPoint ourpoint;
#if PLATFORM(MAC)
ourpoint = frameView->screenToContents(point);
#else
ourpoint = point;
#endif
HitTestRequest request(HitTestRequest::ReadOnly |
HitTestRequest::Active);
HitTestResult result(ourpoint);
renderView->hitTest(request, result);
innerNode = result.innerNode();
if (!innerNode)
return VisiblePosition();
RenderObject* renderer = innerNode->renderer();
if (!renderer)
return VisiblePosition();
pointResult = result.localPoint();
// done if hit something other than a widget
if (!is<RenderWidget>(*renderer))
break;
// descend into widget (FRAME, IFRAME, OBJECT...)
Widget* widget = downcast<RenderWidget>(*renderer).widget();
if (!is<FrameView>(widget))
break;
Frame& frame = downcast<FrameView>(*widget).frame();
renderView = frame.document()->renderView();
#if PLATFORM(COCOA)
frameView = downcast<FrameView>(widget);
#endif
}
return innerNode->renderer()->positionForPoint(pointResult, nullptr);
}
// NOTE: Consider providing this utility method as AX API
VisiblePosition AccessibilityRenderObject::visiblePositionForIndex(unsigned indexValue, bool lastIndexOK) const
{
if (!isTextControl())
return VisiblePosition();
// lastIndexOK specifies whether the position after the last character is acceptable
if (indexValue >= text().length()) {
if (!lastIndexOK || indexValue > text().length())
return VisiblePosition();
}
VisiblePosition position = visiblePositionForIndex(indexValue);
position.setAffinity(DOWNSTREAM);
return position;
}
// NOTE: Consider providing this utility method as AX API
int AccessibilityRenderObject::index(const VisiblePosition& position) const
{
if (position.isNull() || !isTextControl())
return -1;
if (renderObjectContainsPosition(renderer(), position.deepEquivalent()))
return indexForVisiblePosition(position);
return -1;
}
void AccessibilityRenderObject::lineBreaks(Vector<int>& lineBreaks) const
{
if (!isTextControl())
return;
VisiblePosition visiblePos = visiblePositionForIndex(0);
VisiblePosition savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
while (!visiblePos.isNull() && visiblePos != savedVisiblePos) {
lineBreaks.append(indexForVisiblePosition(visiblePos));
savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
}
}
// Given a line number, the range of characters of the text associated with this accessibility
// object that contains the line number.
PlainTextRange AccessibilityRenderObject::doAXRangeForLine(unsigned lineNumber) const
{
if (!isTextControl())
return PlainTextRange();
// iterate to the specified line
VisiblePosition visiblePos = visiblePositionForIndex(0);
VisiblePosition savedVisiblePos;
for (unsigned lineCount = lineNumber; lineCount; lineCount -= 1) {
savedVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0);
if (visiblePos.isNull() || visiblePos == savedVisiblePos)
return PlainTextRange();
}
// Get the end of the line based on the starting position.
VisiblePosition endPosition = endOfLine(visiblePos);
int index1 = indexForVisiblePosition(visiblePos);
int index2 = indexForVisiblePosition(endPosition);
// add one to the end index for a line break not caused by soft line wrap (to match AppKit)
if (endPosition.affinity() == DOWNSTREAM && endPosition.next().isNotNull())
index2 += 1;
// return nil rather than an zero-length range (to match AppKit)
if (index1 == index2)
return PlainTextRange();
return PlainTextRange(index1, index2 - index1);
}
// The composed character range in the text associated with this accessibility object that
// is specified by the given index value. This parameterized attribute returns the complete
// range of characters (including surrogate pairs of multi-byte glyphs) at the given index.
PlainTextRange AccessibilityRenderObject::doAXRangeForIndex(unsigned index) const
{
if (!isTextControl())
return PlainTextRange();
String elementText = text();
if (!elementText.length() || index > elementText.length() - 1)
return PlainTextRange();
return PlainTextRange(index, 1);
}
// A substring of the text associated with this accessibility object that is
// specified by the given character range.
String AccessibilityRenderObject::doAXStringForRange(const PlainTextRange& range) const
{
if (!range.length)
return String();
if (!isTextControl())
return String();
String elementText = isPasswordField() ? passwordFieldValue() : text();
return elementText.substring(range.start, range.length);
}
// The bounding rectangle of the text associated with this accessibility object that is
// specified by the given range. This is the bounding rectangle a sighted user would see
// on the display screen, in pixels.
IntRect AccessibilityRenderObject::doAXBoundsForRange(const PlainTextRange& range) const
{
if (allowsTextRanges())
return boundsForVisiblePositionRange(visiblePositionRangeForRange(range));
return IntRect();
}
IntRect AccessibilityRenderObject::doAXBoundsForRangeUsingCharacterOffset(const PlainTextRange& range) const
{
if (allowsTextRanges())
return boundsForRange(rangeForPlainTextRange(range));
return IntRect();
}
AccessibilityObject* AccessibilityRenderObject::accessibilityImageMapHitTest(HTMLAreaElement* area, const IntPoint& point) const
{
if (!area)
return nullptr;
AccessibilityObject* parent = nullptr;
for (Element* mapParent = area->parentElement(); mapParent; mapParent = mapParent->parentElement()) {
if (is<HTMLMapElement>(*mapParent)) {
parent = accessibilityParentForImageMap(downcast<HTMLMapElement>(mapParent));
break;
}
}
if (!parent)
return nullptr;
for (const auto& child : parent->children()) {
if (child->elementRect().contains(point))
return child.get();
}
return nullptr;
}
AccessibilityObjectInterface* AccessibilityRenderObject::remoteSVGElementHitTest(const IntPoint& point) const
{
AccessibilityObject* remote = remoteSVGRootElement(Create);
if (!remote)
return nullptr;
IntSize offset = point - roundedIntPoint(boundingBoxRect().location());
return remote->accessibilityHitTest(IntPoint(offset));
}
AccessibilityObjectInterface* AccessibilityRenderObject::elementAccessibilityHitTest(const IntPoint& point) const
{
if (isSVGImage())
return remoteSVGElementHitTest(point);
return AccessibilityObject::elementAccessibilityHitTest(point);
}
static bool shouldUseShadowHostForHitTesting(Node* shadowHost)
{
// We need to allow automation of mouse events on video tags.
return shadowHost && !shadowHost->hasTagName(videoTag);
}
AccessibilityObjectInterface* AccessibilityRenderObject::accessibilityHitTest(const IntPoint& point) const
{
if (!m_renderer || !m_renderer->hasLayer())
return nullptr;
m_renderer->document().updateLayout();
RenderLayer* layer = downcast<RenderBox>(*m_renderer).layer();
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AccessibilityHitTest);
HitTestResult hitTestResult = HitTestResult(point);
layer->hitTest(request, hitTestResult);
Node* node = hitTestResult.innerNode();
if (!node)
return nullptr;
Node* shadowAncestorNode = node->shadowHost();
if (shouldUseShadowHostForHitTesting(shadowAncestorNode))
node = shadowAncestorNode;
ASSERT(node);
if (is<HTMLAreaElement>(*node))
return accessibilityImageMapHitTest(downcast<HTMLAreaElement>(node), point);
if (is<HTMLOptionElement>(*node))
node = downcast<HTMLOptionElement>(*node).ownerSelectElement();
RenderObject* obj = node->renderer();
if (!obj)
return nullptr;
AccessibilityObject* result = obj->document().axObjectCache()->getOrCreate(obj);
result->updateChildrenIfNecessary();
// Allow the element to perform any hit-testing it might need to do to reach non-render children.
result = static_cast<AccessibilityObject*>(result->elementAccessibilityHitTest(point));
if (result && result->accessibilityIsIgnored()) {
// If this element is the label of a control, a hit test should return the control.
AccessibilityObject* controlObject = result->correspondingControlForLabelElement();
if (controlObject && !controlObject->exposesTitleUIElement())
return controlObject;
result = result->parentObjectUnignored();
}
return result;
}
bool AccessibilityRenderObject::shouldNotifyActiveDescendant() const
{
#if PLATFORM(GTK)
// According to the Core AAM spec, ATK expects object:state-changed:focused notifications
// whenever the active descendant changes.
return true;
#endif
// We want to notify that the combo box has changed its active descendant,
// but we do not want to change the focus, because focus should remain with the combo box.
if (isComboBox())
return true;
return shouldFocusActiveDescendant();
}
bool AccessibilityRenderObject::shouldFocusActiveDescendant() const
{
switch (ariaRoleAttribute()) {
case AccessibilityRole::ApplicationGroup:
case AccessibilityRole::ListBox:
case AccessibilityRole::Menu:
case AccessibilityRole::MenuBar:
case AccessibilityRole::RadioGroup:
case AccessibilityRole::Row:
case AccessibilityRole::PopUpButton:
case AccessibilityRole::Meter:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::Toolbar:
case AccessibilityRole::Outline:
case AccessibilityRole::Tree:
case AccessibilityRole::Grid:
/* FIXME: replace these with actual roles when they are added to AccessibilityRole
composite
alert
alertdialog
status
timer
*/
return true;
default:
return false;
}
}
AccessibilityObject* AccessibilityRenderObject::activeDescendant() const
{
if (!m_renderer)
return nullptr;
const AtomicString& activeDescendantAttrStr = getAttribute(aria_activedescendantAttr);
if (activeDescendantAttrStr.isNull() || activeDescendantAttrStr.isEmpty())
return nullptr;
Element* element = this->element();
if (!element)
return nullptr;
Element* target = element->treeScope().getElementById(activeDescendantAttrStr);
if (!target)
return nullptr;
if (AXObjectCache* cache = axObjectCache()) {
AccessibilityObject* obj = cache->getOrCreate(target);
if (obj && obj->isAccessibilityRenderObject())
// an activedescendant is only useful if it has a renderer, because that's what's needed to post the notification
return obj;
}
return nullptr;
}
void AccessibilityRenderObject::handleAriaExpandedChanged()
{
// This object might be deleted under the call to the parentObject() method.
auto protectedThis = makeRef(*this);
// Find if a parent of this object should handle aria-expanded changes.
AccessibilityObject* containerParent = this->parentObject();
while (containerParent) {
bool foundParent = false;
switch (containerParent->roleValue()) {
case AccessibilityRole::Tree:
case AccessibilityRole::TreeGrid:
case AccessibilityRole::Grid:
case AccessibilityRole::Table:
case AccessibilityRole::Browser:
foundParent = true;
break;
default:
break;
}
if (foundParent)
break;
containerParent = containerParent->parentObject();
}
// Post that the row count changed.
AXObjectCache* cache = axObjectCache();
if (!cache)
return;
if (containerParent)
cache->postNotification(containerParent, document(), AXObjectCache::AXRowCountChanged);
// Post that the specific row either collapsed or expanded.
if (roleValue() == AccessibilityRole::Row || roleValue() == AccessibilityRole::TreeItem)
cache->postNotification(this, document(), isExpanded() ? AXObjectCache::AXRowExpanded : AXObjectCache::AXRowCollapsed);
else
cache->postNotification(this, document(), AXObjectCache::AXExpandedChanged);
}
RenderObject* AccessibilityRenderObject::targetElementForActiveDescendant(const QualifiedName& attributeName, AccessibilityObject* activeDescendant) const
{
AccessibilityObject::AccessibilityChildrenVector elements;
ariaElementsFromAttribute(elements, attributeName);
for (const auto& element : elements) {
if (activeDescendant->isDescendantOfObject(element.get()))
return element->renderer();
}
return nullptr;
}
void AccessibilityRenderObject::handleActiveDescendantChanged()
{
Element* element = downcast<Element>(renderer()->node());
if (!element)
return;
if (!renderer()->frame().selection().isFocusedAndActive() || renderer()->document().focusedElement() != element)
return;
auto* activeDescendant = this->activeDescendant();
if (activeDescendant && shouldNotifyActiveDescendant()) {
auto* targetRenderer = renderer();
#if PLATFORM(COCOA)
// If the combobox's activeDescendant is inside another object, the target element should be that parent.
if (isComboBox()) {
if (auto* ariaOwner = targetElementForActiveDescendant(aria_ownsAttr, activeDescendant))
targetRenderer = ariaOwner;
else if (auto* ariaController = targetElementForActiveDescendant(aria_controlsAttr, activeDescendant))
targetRenderer = ariaController;
}
#endif
renderer()->document().axObjectCache()->postNotification(targetRenderer, AXObjectCache::AXActiveDescendantChanged);
}
}
AccessibilityObject* AccessibilityRenderObject::correspondingControlForLabelElement() const
{
HTMLLabelElement* labelElement = labelElementContainer();
if (!labelElement)
return nullptr;
auto correspondingControl = labelElement->control();
if (!correspondingControl)
return nullptr;
// Make sure the corresponding control isn't a descendant of this label that's in the middle of being destroyed.
if (correspondingControl->renderer() && !correspondingControl->renderer()->parent())
return nullptr;
return axObjectCache()->getOrCreate(correspondingControl.get());
}
AccessibilityObject* AccessibilityRenderObject::correspondingLabelForControlElement() const
{
if (!m_renderer)
return nullptr;
// ARIA: section 2A, bullet #3 says if aria-labeledby or aria-label appears, it should
// override the "label" element association.
if (hasTextAlternative())
return nullptr;
Node* node = m_renderer->node();
if (is<HTMLElement>(node)) {
if (HTMLLabelElement* label = labelForElement(downcast<HTMLElement>(node)))
return axObjectCache()->getOrCreate(label);
}
return nullptr;
}
bool AccessibilityRenderObject::renderObjectIsObservable(RenderObject& renderer) const
{
// AX clients will listen for AXValueChange on a text control.
if (is<RenderTextControl>(renderer))
return true;
// AX clients will listen for AXSelectedChildrenChanged on listboxes.
Node* node = renderer.node();
if (!node)
return false;
if (nodeHasRole(node, "listbox") || (is<RenderBoxModelObject>(renderer) && downcast<RenderBoxModelObject>(renderer).isListBox()))
return true;
// Textboxes should send out notifications.
if (nodeHasRole(node, "textbox") || (is<Element>(*node) && contentEditableAttributeIsEnabled(downcast<Element>(node))))
return true;
return false;
}
AccessibilityObject* AccessibilityRenderObject::observableObject() const
{
// Find the object going up the parent chain that is used in accessibility to monitor certain notifications.
for (RenderObject* renderer = this->renderer(); renderer && renderer->node(); renderer = renderer->parent()) {
if (renderObjectIsObservable(*renderer)) {
if (AXObjectCache* cache = axObjectCache())
return cache->getOrCreate(renderer);
}
}
return nullptr;
}
bool AccessibilityRenderObject::isDescendantOfElementType(const HashSet<QualifiedName>& tagNames) const
{
for (auto& ancestor : ancestorsOfType<RenderElement>(*m_renderer)) {
if (ancestor.element() && tagNames.contains(ancestor.element()->tagQName()))
return true;
}
return false;
}
bool AccessibilityRenderObject::isDescendantOfElementType(const QualifiedName& tagName) const
{
for (auto& ancestor : ancestorsOfType<RenderElement>(*m_renderer)) {
if (ancestor.element() && ancestor.element()->hasTagName(tagName))
return true;
}
return false;
}
String AccessibilityRenderObject::expandedTextValue() const
{
if (AccessibilityObject* parent = parentObject()) {
if (parent->hasTagName(abbrTag) || parent->hasTagName(acronymTag))
return parent->getAttribute(titleAttr);
}
return String();
}
bool AccessibilityRenderObject::supportsExpandedTextValue() const
{
if (roleValue() == AccessibilityRole::StaticText) {
if (AccessibilityObject* parent = parentObject())
return parent->hasTagName(abbrTag) || parent->hasTagName(acronymTag);
}
return false;
}
AccessibilityRole AccessibilityRenderObject::determineAccessibilityRole()
{
if (!m_renderer)
return AccessibilityRole::Unknown;
#if ENABLE(APPLE_PAY)
if (isApplePayButton())
return AccessibilityRole::Button;
#endif
// Sometimes we need to ignore the attribute role. Like if a tree is malformed,
// we want to ignore the treeitem's attribute role.
if ((m_ariaRole = determineAriaRoleAttribute()) != AccessibilityRole::Unknown && !shouldIgnoreAttributeRole())
return m_ariaRole;
Node* node = m_renderer->node();
RenderBoxModelObject* cssBox = renderBoxModelObject();
if (node && node->isLink())
return AccessibilityRole::WebCoreLink;
if (node && is<HTMLImageElement>(*node) && downcast<HTMLImageElement>(*node).hasAttributeWithoutSynchronization(usemapAttr))
return AccessibilityRole::ImageMap;
if ((cssBox && cssBox->isListItem()) || (node && node->hasTagName(liTag)))
return AccessibilityRole::ListItem;
if (m_renderer->isListMarker())
return AccessibilityRole::ListMarker;
if (node && node->hasTagName(buttonTag))
return buttonRoleType();
if (node && node->hasTagName(legendTag))
return AccessibilityRole::Legend;
if (m_renderer->isText())
return AccessibilityRole::StaticText;
if (cssBox && cssBox->isImage()) {
if (is<HTMLInputElement>(node))
return hasPopup() ? AccessibilityRole::PopUpButton : AccessibilityRole::Button;
if (isSVGImage())
return AccessibilityRole::SVGRoot;
return AccessibilityRole::Image;
}
if (node && node->hasTagName(canvasTag))
return AccessibilityRole::Canvas;
if (cssBox && cssBox->isRenderView())
return AccessibilityRole::WebArea;
if (cssBox && cssBox->isTextField()) {
if (is<HTMLInputElement>(node))
return downcast<HTMLInputElement>(*node).isSearchField() ? AccessibilityRole::SearchField : AccessibilityRole::TextField;
}
if (cssBox && cssBox->isTextArea())
return AccessibilityRole::TextArea;
if (is<HTMLInputElement>(node)) {
HTMLInputElement& input = downcast<HTMLInputElement>(*node);
if (input.isCheckbox())
return AccessibilityRole::CheckBox;
if (input.isRadioButton())
return AccessibilityRole::RadioButton;
if (input.isTextButton())
return buttonRoleType();
// On iOS, the date field and time field are popup buttons. On other platforms they are text fields.
#if PLATFORM(IOS_FAMILY)
if (input.isDateField() || input.isTimeField())
return AccessibilityRole::PopUpButton;
#endif
#if ENABLE(INPUT_TYPE_COLOR)
if (input.isColorControl())
return AccessibilityRole::ColorWell;
#endif
}
if (hasContentEditableAttributeSet())
return AccessibilityRole::TextArea;
if (isFileUploadButton())
return AccessibilityRole::Button;
if (cssBox && cssBox->isMenuList())
return AccessibilityRole::PopUpButton;
if (headingLevel())
return AccessibilityRole::Heading;
if (m_renderer->isSVGRoot())
return AccessibilityRole::SVGRoot;
if (isStyleFormatGroup())
return is<RenderInline>(*m_renderer) ? AccessibilityRole::Inline : AccessibilityRole::TextGroup;
if (node && node->hasTagName(ddTag))
return AccessibilityRole::DescriptionListDetail;
if (node && node->hasTagName(dtTag))
return AccessibilityRole::DescriptionListTerm;
if (node && node->hasTagName(dlTag))
return AccessibilityRole::DescriptionList;
if (node && node->hasTagName(fieldsetTag))
return AccessibilityRole::Group;
if (node && node->hasTagName(figureTag))
return AccessibilityRole::Figure;
// Check for Ruby elements
if (m_renderer->isRubyText())
return AccessibilityRole::RubyText;
if (m_renderer->isRubyBase())
return AccessibilityRole::RubyBase;
if (m_renderer->isRubyRun())
return AccessibilityRole::RubyRun;
if (m_renderer->isRubyBlock())
return AccessibilityRole::RubyBlock;
if (m_renderer->isRubyInline())
return AccessibilityRole::RubyInline;
// This return value is what will be used if AccessibilityTableCell determines
// the cell should not be treated as a cell (e.g. because it is a layout table.
if (is<RenderTableCell>(renderer()))
return AccessibilityRole::TextGroup;
// Table sections should be ignored.
if (m_renderer->isTableSection())
return AccessibilityRole::Ignored;
if (m_renderer->isHR())
return AccessibilityRole::HorizontalRule;
if (node && node->hasTagName(pTag))
return AccessibilityRole::Paragraph;
if (is<HTMLLabelElement>(node))
return AccessibilityRole::Label;
if (node && node->hasTagName(dfnTag))
return AccessibilityRole::Definition;
if (node && node->hasTagName(divTag))
return AccessibilityRole::Div;
if (is<HTMLFormElement>(node))
return AccessibilityRole::Form;
if (node && node->hasTagName(articleTag))
return AccessibilityRole::DocumentArticle;
if (node && node->hasTagName(mainTag))
return AccessibilityRole::LandmarkMain;
if (node && node->hasTagName(navTag))
return AccessibilityRole::LandmarkNavigation;
if (node && node->hasTagName(asideTag))
return AccessibilityRole::LandmarkComplementary;
// The default role attribute value for the section element, region, became a landmark in ARIA 1.1.
// The HTML AAM spec says it is "strongly recommended" that ATs only convey and provide navigation
// for section elements which have names.
if (node && node->hasTagName(sectionTag))
return hasAttribute(aria_labelAttr) || hasAttribute(aria_labelledbyAttr) ? AccessibilityRole::LandmarkRegion : AccessibilityRole::TextGroup;
if (node && node->hasTagName(addressTag))
return AccessibilityRole::LandmarkContentInfo;
if (node && node->hasTagName(blockquoteTag))
return AccessibilityRole::Blockquote;
if (node && node->hasTagName(captionTag))
return AccessibilityRole::Caption;
if (node && node->hasTagName(markTag))
return AccessibilityRole::Mark;
if (node && node->hasTagName(preTag))
return AccessibilityRole::Pre;
if (is<HTMLDetailsElement>(node))
return AccessibilityRole::Details;
if (is<HTMLSummaryElement>(node))
return AccessibilityRole::Summary;
// http://rawgit.com/w3c/aria/master/html-aam/html-aam.html
// Output elements should be mapped to status role.
if (isOutput())
return AccessibilityRole::ApplicationStatus;
#if ENABLE(VIDEO)
if (is<HTMLVideoElement>(node))
return AccessibilityRole::Video;
if (is<HTMLAudioElement>(node))
return AccessibilityRole::Audio;
#endif
// The HTML element should not be exposed as an element. That's what the RenderView element does.
if (node && node->hasTagName(htmlTag))
return AccessibilityRole::Ignored;
// There should only be one banner/contentInfo per page. If header/footer are being used within an article or section
// then it should not be exposed as whole page's banner/contentInfo
if (node && node->hasTagName(headerTag) && !isDescendantOfElementType({ articleTag, sectionTag }))
return AccessibilityRole::LandmarkBanner;
// http://webkit.org/b/190138 Footers should become contentInfo's if scoped to body (and consequently become a landmark).
// It should remain a footer if scoped to main, sectioning elements (article, section) or root sectioning element (blockquote, details, dialog, fieldset, figure, td).
if (node && node->hasTagName(footerTag)) {
if (!isDescendantOfElementType({ articleTag, sectionTag, mainTag, blockquoteTag, detailsTag, fieldsetTag, figureTag, tdTag }))
return AccessibilityRole::LandmarkContentInfo;
return AccessibilityRole::Footer;
}
// menu tags with toolbar type should have Toolbar role.
if (node && node->hasTagName(menuTag) && equalLettersIgnoringASCIICase(getAttribute(typeAttr), "toolbar"))
return AccessibilityRole::Toolbar;
if (node && node->hasTagName(timeTag))
return AccessibilityRole::Time;
// If the element does not have role, but it has ARIA attributes, or accepts tab focus, accessibility should fallback to exposing it as a group.
if (supportsARIAAttributes() || canSetFocusAttribute())
return AccessibilityRole::Group;
if (m_renderer->isRenderBlockFlow())
return m_renderer->isAnonymousBlock() ? AccessibilityRole::TextGroup : AccessibilityRole::Group;
// InlineRole is the final fallback before assigning AccessibilityRole::Unknown to an object. It makes it
// possible to distinguish truly unknown objects from non-focusable inline text elements
// which have an event handler or attribute suggesting possible inclusion by the platform.
if (is<RenderInline>(*m_renderer)
&& (hasAttributesRequiredForInclusion()
|| (node && node->hasEventListeners())
|| (supportsDatetimeAttribute() && !getAttribute(datetimeAttr).isEmpty())))
return AccessibilityRole::Inline;
return AccessibilityRole::Unknown;
}
AccessibilityOrientation AccessibilityRenderObject::orientation() const
{
const AtomicString& ariaOrientation = getAttribute(aria_orientationAttr);
if (equalLettersIgnoringASCIICase(ariaOrientation, "horizontal"))
return AccessibilityOrientation::Horizontal;
if (equalLettersIgnoringASCIICase(ariaOrientation, "vertical"))
return AccessibilityOrientation::Vertical;
if (equalLettersIgnoringASCIICase(ariaOrientation, "undefined"))
return AccessibilityOrientation::Undefined;
// In ARIA 1.1, the implicit value of aria-orientation changed from horizontal
// to undefined on all roles that don't have their own role-specific values. In
// addition, the implicit value of combobox became undefined.
if (isComboBox() || isRadioGroup() || isTreeGrid())
return AccessibilityOrientation::Undefined;
if (isScrollbar() || isListBox() || isMenu() || isTree())
return AccessibilityOrientation::Vertical;
if (isMenuBar() || isSplitter() || isTabList() || isToolbar() || isSlider())
return AccessibilityOrientation::Horizontal;
return AccessibilityObject::orientation();
}
bool AccessibilityRenderObject::inheritsPresentationalRole() const
{
// ARIA states if an item can get focus, it should not be presentational.
if (canSetFocusAttribute())
return false;
// ARIA spec says that when a parent object is presentational, and it has required child elements,
// those child elements are also presentational. For example, <li> becomes presentational from <ul>.
// http://www.w3.org/WAI/PF/aria/complete#presentation
const Vector<const HTMLQualifiedName*>* parentTags;
switch (roleValue()) {
case AccessibilityRole::ListItem:
case AccessibilityRole::ListMarker: {
static const auto listItemParents = makeNeverDestroyed(Vector<const HTMLQualifiedName*> { &dlTag.get(), &olTag.get(), &ulTag.get() });
parentTags = &listItemParents.get();
break;
}
case AccessibilityRole::GridCell:
case AccessibilityRole::Cell: {
static const auto tableCellParents = makeNeverDestroyed(Vector<const HTMLQualifiedName*> { &tableTag.get() });
parentTags = &tableCellParents.get();
break;
}
default:
// Not all elements need to do the following check, only ones that are required children.
return false;
}
for (auto* parent = parentObject(); parent; parent = parent->parentObject()) {
if (!is<AccessibilityRenderObject>(*parent))
continue;
Node* node = downcast<AccessibilityRenderObject>(*parent).node();
if (!is<Element>(node))
continue;
// If native tag of the parent element matches an acceptable name, then return
// based on its presentational status.
auto& name = downcast<Element>(*node).tagQName();
if (std::any_of(parentTags->begin(), parentTags->end(), [&name] (auto* possibleName) { return *possibleName == name; }))
return parent->roleValue() == AccessibilityRole::Presentational;
}
return false;
}
bool AccessibilityRenderObject::isPresentationalChildOfAriaRole() const
{
// Walk the parent chain looking for a parent that has presentational children
AccessibilityObject* parent;
for (parent = parentObject(); parent && !parent->ariaRoleHasPresentationalChildren(); parent = parent->parentObject())
{ }
return parent;
}
bool AccessibilityRenderObject::ariaRoleHasPresentationalChildren() const
{
switch (m_ariaRole) {
case AccessibilityRole::Button:
case AccessibilityRole::Slider:
case AccessibilityRole::Image:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::SpinButton:
// case SeparatorRole:
return true;
default:
return false;
}
}
bool AccessibilityRenderObject::canSetExpandedAttribute() const
{
if (roleValue() == AccessibilityRole::Details)
return true;
// An object can be expanded if it aria-expanded is true or false.
const AtomicString& expanded = getAttribute(aria_expandedAttr);
if (equalLettersIgnoringASCIICase(expanded, "true") || equalLettersIgnoringASCIICase(expanded, "false"))
return true;
return false;
}
bool AccessibilityRenderObject::canSetTextRangeAttributes() const
{
return isTextControl();
}
void AccessibilityRenderObject::textChanged()
{
// If this element supports ARIA live regions, or is part of a region with an ARIA editable role,
// then notify the AT of changes.
AXObjectCache* cache = axObjectCache();
if (!cache)
return;
for (RenderObject* renderParent = renderer(); renderParent; renderParent = renderParent->parent()) {
AccessibilityObject* parent = cache->get(renderParent);
if (!parent)
continue;
if (parent->supportsLiveRegion())
cache->postLiveRegionChangeNotification(parent);
if (parent->isNonNativeTextControl())
cache->postNotification(renderParent, AXObjectCache::AXValueChanged);
}
}
void AccessibilityRenderObject::clearChildren()
{
AccessibilityObject::clearChildren();
m_childrenDirty = false;
}
void AccessibilityRenderObject::addImageMapChildren()
{
RenderBoxModelObject* cssBox = renderBoxModelObject();
if (!is<RenderImage>(cssBox))
return;
HTMLMapElement* map = downcast<RenderImage>(*cssBox).imageMap();
if (!map)
return;
for (auto& area : descendantsOfType<HTMLAreaElement>(*map)) {
// add an <area> element for this child if it has a link
if (!area.isLink())
continue;
auto& areaObject = downcast<AccessibilityImageMapLink>(*axObjectCache()->getOrCreate(AccessibilityRole::ImageMapLink));
areaObject.setHTMLAreaElement(&area);
areaObject.setHTMLMapElement(map);
areaObject.setParent(this);
if (!areaObject.accessibilityIsIgnored())
m_children.append(&areaObject);
else
axObjectCache()->remove(areaObject.axObjectID());
}
}
void AccessibilityRenderObject::updateChildrenIfNecessary()
{
if (needsToUpdateChildren())
clearChildren();
AccessibilityObject::updateChildrenIfNecessary();
}
void AccessibilityRenderObject::addTextFieldChildren()
{
Node* node = this->node();
if (!is<HTMLInputElement>(node))
return;
HTMLInputElement& input = downcast<HTMLInputElement>(*node);
if (HTMLElement* autoFillElement = input.autoFillButtonElement()) {
if (AccessibilityObject* axAutoFill = axObjectCache()->getOrCreate(autoFillElement))
m_children.append(axAutoFill);
}
HTMLElement* spinButtonElement = input.innerSpinButtonElement();
if (!is<SpinButtonElement>(spinButtonElement))
return;
auto& axSpinButton = downcast<AccessibilitySpinButton>(*axObjectCache()->getOrCreate(AccessibilityRole::SpinButton));
axSpinButton.setSpinButtonElement(downcast<SpinButtonElement>(spinButtonElement));
axSpinButton.setParent(this);
m_children.append(&axSpinButton);
}
bool AccessibilityRenderObject::isSVGImage() const
{
return remoteSVGRootElement(Create);
}
void AccessibilityRenderObject::detachRemoteSVGRoot()
{
if (AccessibilitySVGRoot* root = remoteSVGRootElement(Retrieve))
root->setParent(nullptr);
}
AccessibilitySVGRoot* AccessibilityRenderObject::remoteSVGRootElement(CreationChoice createIfNecessary) const
{
if (!is<RenderImage>(renderer()))
return nullptr;
CachedImage* cachedImage = downcast<RenderImage>(*m_renderer).cachedImage();
if (!cachedImage)
return nullptr;
Image* image = cachedImage->image();
if (!is<SVGImage>(image))
return nullptr;
FrameView* frameView = downcast<SVGImage>(*image).frameView();
if (!frameView)
return nullptr;
Frame& frame = frameView->frame();
Document* document = frame.document();
if (!is<SVGDocument>(document))
return nullptr;
auto rootElement = SVGDocument::rootElement(*document);
if (!rootElement)
return nullptr;
RenderObject* rendererRoot = rootElement->renderer();
if (!rendererRoot)
return nullptr;
AXObjectCache* cache = frame.document()->axObjectCache();
if (!cache)
return nullptr;
AccessibilityObject* rootSVGObject = createIfNecessary == Create ? cache->getOrCreate(rendererRoot) : cache->get(rendererRoot);
// In order to connect the AX hierarchy from the SVG root element from the loaded resource
// the parent must be set, because there's no other way to get back to who created the image.
ASSERT(!createIfNecessary || rootSVGObject);
if (!is<AccessibilitySVGRoot>(rootSVGObject))
return nullptr;
return downcast<AccessibilitySVGRoot>(rootSVGObject);
}
void AccessibilityRenderObject::addRemoteSVGChildren()
{
AccessibilitySVGRoot* root = remoteSVGRootElement(Create);
if (!root)
return;
root->setParent(this);
if (root->accessibilityIsIgnored()) {
for (const auto& child : root->children())
m_children.append(child);
} else
m_children.append(root);
}
void AccessibilityRenderObject::addCanvasChildren()
{
// Add the unrendered canvas children as AX nodes, unless we're not using a canvas renderer
// because JS is disabled for example.
if (!node() || !node()->hasTagName(canvasTag) || (renderer() && !renderer()->isCanvas()))
return;
// If it's a canvas, it won't have rendered children, but it might have accessible fallback content.
// Clear m_haveChildren because AccessibilityNodeObject::addChildren will expect it to be false.
ASSERT(!m_children.size());
m_haveChildren = false;
AccessibilityNodeObject::addChildren();
}
void AccessibilityRenderObject::addAttachmentChildren()
{
if (!isAttachment())
return;
// FrameView's need to be inserted into the AX hierarchy when encountered.
Widget* widget = widgetForAttachmentView();
if (!widget || !widget->isFrameView())
return;
addChild(axObjectCache()->getOrCreate(widget));
}
#if PLATFORM(COCOA)
void AccessibilityRenderObject::updateAttachmentViewParents()
{
// Only the unignored parent should set the attachment parent, because that's what is reflected in the AX
// hierarchy to the client.
if (accessibilityIsIgnored())
return;
for (const auto& child : m_children) {
if (child->isAttachment())
child->overrideAttachmentParent(this);
}
}
#endif
// Hidden children are those that are not rendered or visible, but are specifically marked as aria-hidden=false,
// meaning that they should be exposed to the AX hierarchy.
void AccessibilityRenderObject::addHiddenChildren()
{
Node* node = this->node();
if (!node)
return;
// First do a quick run through to determine if we have any hidden nodes (most often we will not).
// If we do have hidden nodes, we need to determine where to insert them so they match DOM order as close as possible.
bool shouldInsertHiddenNodes = false;
for (Node* child = node->firstChild(); child; child = child->nextSibling()) {
if (!child->renderer() && isNodeAriaVisible(child)) {
shouldInsertHiddenNodes = true;
break;
}
}
if (!shouldInsertHiddenNodes)
return;
// Iterate through all of the children, including those that may have already been added, and
// try to insert hidden nodes in the correct place in the DOM order.
unsigned insertionIndex = 0;
for (Node* child = node->firstChild(); child; child = child->nextSibling()) {
if (child->renderer()) {
// Find out where the last render sibling is located within m_children.
AccessibilityObject* childObject = axObjectCache()->get(child->renderer());
if (childObject && childObject->accessibilityIsIgnored()) {
auto& children = childObject->children();
if (children.size())
childObject = children.last().get();
else
childObject = nullptr;
}
if (childObject)
insertionIndex = m_children.find(childObject) + 1;
continue;
}
if (!isNodeAriaVisible(child))
continue;
unsigned previousSize = m_children.size();
if (insertionIndex > previousSize)
insertionIndex = previousSize;
insertChild(axObjectCache()->getOrCreate(child), insertionIndex);
insertionIndex += (m_children.size() - previousSize);
}
}
void AccessibilityRenderObject::updateRoleAfterChildrenCreation()
{
// If a menu does not have valid menuitem children, it should not be exposed as a menu.
auto role = roleValue();
if (role == AccessibilityRole::Menu) {
// Elements marked as menus must have at least one menu item child.
size_t menuItemCount = 0;
for (const auto& child : children()) {
if (child->isMenuItem()) {
menuItemCount++;
break;
}
}
if (!menuItemCount)
m_role = AccessibilityRole::Group;
}
if (role == AccessibilityRole::SVGRoot && !hasChildren())
m_role = AccessibilityRole::Image;
}
void AccessibilityRenderObject::addChildren()
{
// If the need to add more children in addition to existing children arises,
// childrenChanged should have been called, leaving the object with no children.
ASSERT(!m_haveChildren);
m_haveChildren = true;
if (!canHaveChildren())
return;
for (RefPtr<AccessibilityObject> obj = firstChild(); obj; obj = obj->nextSibling())
addChild(obj.get());
m_subtreeDirty = false;
addHiddenChildren();
addAttachmentChildren();
addImageMapChildren();
addTextFieldChildren();
addCanvasChildren();
addRemoteSVGChildren();
#if PLATFORM(COCOA)
updateAttachmentViewParents();
#endif
updateRoleAfterChildrenCreation();
}
bool AccessibilityRenderObject::canHaveChildren() const
{
if (!m_renderer)
return false;
return AccessibilityNodeObject::canHaveChildren();
}
const String AccessibilityRenderObject::liveRegionStatus() const
{
const AtomicString& liveRegionStatus = getAttribute(aria_liveAttr);
// These roles have implicit live region status.
if (liveRegionStatus.isEmpty())
return defaultLiveRegionStatusForRole(roleValue());
return liveRegionStatus;
}
const String AccessibilityRenderObject::liveRegionRelevant() const
{
static NeverDestroyed<const AtomicString> defaultLiveRegionRelevant("additions text", AtomicString::ConstructFromLiteral);
const AtomicString& relevant = getAttribute(aria_relevantAttr);
// Default aria-relevant = "additions text".
if (relevant.isEmpty())
return "additions text";
return relevant;
}
bool AccessibilityRenderObject::liveRegionAtomic() const
{
const AtomicString& atomic = getAttribute(aria_atomicAttr);
if (equalLettersIgnoringASCIICase(atomic, "true"))
return true;
if (equalLettersIgnoringASCIICase(atomic, "false"))
return false;
// WAI-ARIA "alert" and "status" roles have an implicit aria-atomic value of true.
switch (roleValue()) {
case AccessibilityRole::ApplicationAlert:
case AccessibilityRole::ApplicationStatus:
return true;
default:
return false;
}
}
bool AccessibilityRenderObject::isBusy() const
{
return elementAttributeValue(aria_busyAttr);
}
bool AccessibilityRenderObject::canHaveSelectedChildren() const
{
switch (roleValue()) {
// These roles are containers whose children support aria-selected:
case AccessibilityRole::Grid:
case AccessibilityRole::ListBox:
case AccessibilityRole::TabList:
case AccessibilityRole::Tree:
case AccessibilityRole::TreeGrid:
case AccessibilityRole::List:
// These roles are containers whose children are treated as selected by assistive
// technologies. We can get the "selected" item via aria-activedescendant or the
// focused element.
case AccessibilityRole::Menu:
case AccessibilityRole::MenuBar:
return true;
default:
return false;
}
}
void AccessibilityRenderObject::ariaSelectedRows(AccessibilityChildrenVector& result)
{
// Determine which rows are selected.
bool isMulti = isMultiSelectable();
// Prefer active descendant over aria-selected.
AccessibilityObject* activeDesc = activeDescendant();
if (activeDesc && (activeDesc->isTreeItem() || activeDesc->isTableRow())) {
result.append(activeDesc);
if (!isMulti)
return;
}
// Get all the rows.
auto rowsIteration = [&](auto& rows) {
for (auto& row : rows) {
if (row->isSelected() || row->isActiveDescendantOfFocusedContainer()) {
result.append(row);
if (!isMulti)
break;
}
}
};
if (isTree()) {
AccessibilityChildrenVector allRows;
ariaTreeRows(allRows);
rowsIteration(allRows);
} else if (is<AccessibilityTable>(*this)) {
auto& thisTable = downcast<AccessibilityTable>(*this);
if (thisTable.isExposableThroughAccessibility() && thisTable.supportsSelectedRows())
rowsIteration(thisTable.rows());
}
}
void AccessibilityRenderObject::ariaListboxSelectedChildren(AccessibilityChildrenVector& result)
{
bool isMulti = isMultiSelectable();
for (const auto& child : children()) {
// Every child should have aria-role option, and if so, check for selected attribute/state.
if (child->ariaRoleAttribute() == AccessibilityRole::ListBoxOption && (child->isSelected() || child->isActiveDescendantOfFocusedContainer())) {
result.append(child);
if (!isMulti)
return;
}
}
}
void AccessibilityRenderObject::selectedChildren(AccessibilityChildrenVector& result)
{
ASSERT(result.isEmpty());
if (!canHaveSelectedChildren())
return;
switch (roleValue()) {
case AccessibilityRole::ListBox:
// native list boxes would be AccessibilityListBoxes, so only check for aria list boxes
ariaListboxSelectedChildren(result);
return;
case AccessibilityRole::Grid:
case AccessibilityRole::Tree:
case AccessibilityRole::TreeGrid:
ariaSelectedRows(result);
return;
case AccessibilityRole::TabList:
if (AccessibilityObject* selectedTab = selectedTabItem())
result.append(selectedTab);
return;
case AccessibilityRole::List:
if (auto* selectedListItemChild = selectedListItem())
result.append(selectedListItemChild);
return;
case AccessibilityRole::Menu:
case AccessibilityRole::MenuBar:
if (AccessibilityObject* descendant = activeDescendant()) {
result.append(descendant);
return;
}
if (AccessibilityObject* focusedElement = static_cast<AccessibilityObject*>(focusedUIElement())) {
result.append(focusedElement);
return;
}
return;
default:
ASSERT_NOT_REACHED();
}
}
void AccessibilityRenderObject::ariaListboxVisibleChildren(AccessibilityChildrenVector& result)
{
if (!hasChildren())
addChildren();
for (const auto& child : children()) {
if (child->isOffScreen())
result.append(child);
}
}
void AccessibilityRenderObject::visibleChildren(AccessibilityChildrenVector& result)
{
ASSERT(result.isEmpty());
// only listboxes are asked for their visible children.
if (ariaRoleAttribute() != AccessibilityRole::ListBox) {
// native list boxes would be AccessibilityListBoxes, so only check for aria list boxes
ASSERT_NOT_REACHED();
return;
}
return ariaListboxVisibleChildren(result);
}
void AccessibilityRenderObject::tabChildren(AccessibilityChildrenVector& result)
{
ASSERT(roleValue() == AccessibilityRole::TabList);
for (const auto& child : children()) {
if (child->isTabItem())
result.append(child);
}
}
const String& AccessibilityRenderObject::actionVerb() const
{
#if !PLATFORM(IOS_FAMILY)
// FIXME: Need to add verbs for select elements.
static NeverDestroyed<const String> buttonAction(AXButtonActionVerb());
static NeverDestroyed<const String> textFieldAction(AXTextFieldActionVerb());
static NeverDestroyed<const String> radioButtonAction(AXRadioButtonActionVerb());
static NeverDestroyed<const String> checkedCheckBoxAction(AXUncheckedCheckBoxActionVerb());
static NeverDestroyed<const String> uncheckedCheckBoxAction(AXUncheckedCheckBoxActionVerb());
static NeverDestroyed<const String> linkAction(AXLinkActionVerb());
switch (roleValue()) {
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
return buttonAction;
case AccessibilityRole::TextField:
case AccessibilityRole::TextArea:
return textFieldAction;
case AccessibilityRole::RadioButton:
return radioButtonAction;
case AccessibilityRole::CheckBox:
return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction;
case AccessibilityRole::Link:
case AccessibilityRole::WebCoreLink:
return linkAction;
default:
return nullAtom();
}
#else
return nullAtom();
#endif
}
void AccessibilityRenderObject::setAccessibleName(const AtomicString& name)
{
// Setting the accessible name can store the value in the DOM
if (!m_renderer)
return;
Node* node = nullptr;
// For web areas, set the aria-label on the HTML element.
if (isWebArea())
node = m_renderer->document().documentElement();
else
node = m_renderer->node();
if (is<Element>(node))
downcast<Element>(*node).setAttribute(aria_labelAttr, name);
}
static bool isLinkable(const AccessibilityRenderObject& object)
{
if (!object.renderer())
return false;
// See https://wiki.mozilla.org/Accessibility/AT-Windows-API for the elements
// Mozilla considers linkable.
return object.isLink() || object.isImage() || object.renderer()->isText();
}
String AccessibilityRenderObject::stringValueForMSAA() const
{
if (isLinkable(*this)) {
Element* anchor = anchorElement();
if (is<HTMLAnchorElement>(anchor))
return downcast<HTMLAnchorElement>(*anchor).href();
}
return stringValue();
}
bool AccessibilityRenderObject::isLinked() const
{
if (!isLinkable(*this))
return false;
Element* anchor = anchorElement();
if (!is<HTMLAnchorElement>(anchor))
return false;
return !downcast<HTMLAnchorElement>(*anchor).href().isEmpty();
}
bool AccessibilityRenderObject::hasBoldFont() const
{
if (!m_renderer)
return false;
return isFontWeightBold(m_renderer->style().fontDescription().weight());
}
bool AccessibilityRenderObject::hasItalicFont() const
{
if (!m_renderer)
return false;
return isItalic(m_renderer->style().fontDescription().italic());
}
bool AccessibilityRenderObject::hasPlainText() const
{
if (!m_renderer)
return false;
if (!canHavePlainText())
return false;
const RenderStyle& style = m_renderer->style();
return style.fontDescription().weight() == normalWeightValue()
&& !isItalic(style.fontDescription().italic())
&& style.textDecorationsInEffect().isEmpty();
}
bool AccessibilityRenderObject::hasSameFont(RenderObject* renderer) const
{
if (!m_renderer || !renderer)
return false;
return m_renderer->style().fontDescription().families() == renderer->style().fontDescription().families();
}
#if ENABLE(APPLE_PAY)
bool AccessibilityRenderObject::isApplePayButton() const
{
if (!m_renderer)
return false;
return m_renderer->style().appearance() == ApplePayButtonPart;
}
ApplePayButtonType AccessibilityRenderObject::applePayButtonType() const
{
if (!m_renderer)
return ApplePayButtonType::Plain;
return m_renderer->style().applePayButtonType();
}
#endif
bool AccessibilityRenderObject::hasSameFontColor(RenderObject* renderer) const
{
if (!m_renderer || !renderer)
return false;
return m_renderer->style().visitedDependentColor(CSSPropertyColor) == renderer->style().visitedDependentColor(CSSPropertyColor);
}
bool AccessibilityRenderObject::hasSameStyle(RenderObject* renderer) const
{
if (!m_renderer || !renderer)
return false;
return m_renderer->style() == renderer->style();
}
bool AccessibilityRenderObject::hasUnderline() const
{
if (!m_renderer)
return false;
return m_renderer->style().textDecorationsInEffect().contains(TextDecoration::Underline);
}
String AccessibilityRenderObject::nameForMSAA() const
{
if (m_renderer && m_renderer->isText())
return textUnderElement();
return title();
}
static bool shouldReturnTagNameAsRoleForMSAA(const Element& element)
{
return element.hasTagName(abbrTag) || element.hasTagName(acronymTag)
|| element.hasTagName(blockquoteTag) || element.hasTagName(ddTag)
|| element.hasTagName(dlTag) || element.hasTagName(dtTag)
|| element.hasTagName(formTag) || element.hasTagName(frameTag)
|| element.hasTagName(h1Tag) || element.hasTagName(h2Tag)
|| element.hasTagName(h3Tag) || element.hasTagName(h4Tag)
|| element.hasTagName(h5Tag) || element.hasTagName(h6Tag)
|| element.hasTagName(iframeTag) || element.hasTagName(qTag)
|| element.hasTagName(tbodyTag) || element.hasTagName(tfootTag)
|| element.hasTagName(theadTag);
}
String AccessibilityRenderObject::stringRoleForMSAA() const
{
if (!m_renderer)
return String();
Node* node = m_renderer->node();
if (!is<Element>(node))
return String();
Element& element = downcast<Element>(*node);
if (!shouldReturnTagNameAsRoleForMSAA(element))
return String();
return element.tagName();
}
String AccessibilityRenderObject::positionalDescriptionForMSAA() const
{
// See "positional descriptions",
// https://wiki.mozilla.org/Accessibility/AT-Windows-API
if (isHeading())
return makeString('L', headingLevel());
// FIXME: Add positional descriptions for other elements.
return String();
}
String AccessibilityRenderObject::descriptionForMSAA() const
{
String description = positionalDescriptionForMSAA();
if (!description.isEmpty())
return description;
description = accessibilityDescription();
if (!description.isEmpty()) {
// From the Mozilla MSAA implementation:
// "Signal to screen readers that this description is speakable and is not
// a formatted positional information description. Don't localize the
// 'Description: ' part of this string, it will be parsed out by assistive
// technologies."
return "Description: " + description;
}
return String();
}
static AccessibilityRole msaaRoleForRenderer(const RenderObject* renderer)
{
if (!renderer)
return AccessibilityRole::Unknown;
if (is<RenderText>(*renderer))
return AccessibilityRole::EditableText;
if (is<RenderListItem>(*renderer))
return AccessibilityRole::ListItem;
return AccessibilityRole::Unknown;
}
AccessibilityRole AccessibilityRenderObject::roleValueForMSAA() const
{
if (m_roleForMSAA != AccessibilityRole::Unknown)
return m_roleForMSAA;
m_roleForMSAA = msaaRoleForRenderer(renderer());
if (m_roleForMSAA == AccessibilityRole::Unknown)
m_roleForMSAA = roleValue();
return m_roleForMSAA;
}
String AccessibilityRenderObject::passwordFieldValue() const
{
ASSERT(isPasswordField());
// Look for the RenderText object in the RenderObject tree for this input field.
RenderObject* renderer = node()->renderer();
while (renderer && !is<RenderText>(renderer))
renderer = downcast<RenderElement>(*renderer).firstChild();
if (!is<RenderText>(renderer))
return String();
// Return the text that is actually being rendered in the input field.
return downcast<RenderText>(*renderer).textWithoutConvertingBackslashToYenSymbol();
}
ScrollableArea* AccessibilityRenderObject::getScrollableAreaIfScrollable() const
{
// If the parent is a scroll view, then this object isn't really scrollable, the parent ScrollView should handle the scrolling.
if (parentObject() && parentObject()->isAccessibilityScrollView())
return nullptr;
if (!is<RenderBox>(renderer()))
return nullptr;
auto& box = downcast<RenderBox>(*m_renderer);
if (!box.canBeScrolledAndHasScrollableArea())
return nullptr;
return box.layer();
}
void AccessibilityRenderObject::scrollTo(const IntPoint& point) const
{
if (!is<RenderBox>(renderer()))
return;
auto& box = downcast<RenderBox>(*m_renderer);
if (!box.canBeScrolledAndHasScrollableArea())
return;
// FIXME: is point a ScrollOffset or ScrollPosition? Test in RTL overflow.
box.layer()->scrollToOffset(point);
}
#if ENABLE(MATHML)
bool AccessibilityRenderObject::isIgnoredElementWithinMathTree() const
{
// We ignore anonymous boxes inserted into RenderMathMLBlocks to honor CSS rules.
// See https://www.w3.org/TR/css3-box/#block-level0
return m_renderer && m_renderer->isAnonymous() && m_renderer->parent() && is<RenderMathMLBlock>(m_renderer->parent());
}
#endif
} // namespace WebCore