blob: 5cde34330ee468bf280abd9c80b92f2fab879a55 [file] [log] [blame] [edit]
/*
* 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 Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "modules/accessibility/AXLayoutObject.h"
#include "bindings/core/v8/ExceptionStatePlaceholder.h"
#include "core/CSSPropertyNames.h"
#include "core/InputTypeNames.h"
#include "core/dom/ElementTraversal.h"
#include "core/dom/Range.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/RenderedPosition.h"
#include "core/editing/TextAffinity.h"
#include "core/editing/VisibleUnits.h"
#include "core/editing/iterators/CharacterIterator.h"
#include "core/editing/iterators/TextIterator.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/html/HTMLImageElement.h"
#include "core/html/HTMLLabelElement.h"
#include "core/html/HTMLOptionElement.h"
#include "core/html/HTMLSelectElement.h"
#include "core/html/HTMLTextAreaElement.h"
#include "core/html/shadow/ShadowElementNames.h"
#include "core/layout/HitTestResult.h"
#include "core/layout/LayoutFieldset.h"
#include "core/layout/LayoutFileUploadControl.h"
#include "core/layout/LayoutHTMLCanvas.h"
#include "core/layout/LayoutImage.h"
#include "core/layout/LayoutInline.h"
#include "core/layout/LayoutListMarker.h"
#include "core/layout/LayoutMenuList.h"
#include "core/layout/LayoutPart.h"
#include "core/layout/LayoutTextControl.h"
#include "core/layout/LayoutTextControlSingleLine.h"
#include "core/layout/LayoutTextFragment.h"
#include "core/layout/LayoutView.h"
#include "core/loader/ProgressTracker.h"
#include "core/page/Page.h"
#include "core/paint/DeprecatedPaintLayer.h"
#include "core/style/ComputedStyleConstants.h"
#include "core/svg/SVGDocumentExtensions.h"
#include "core/svg/SVGSVGElement.h"
#include "core/svg/graphics/SVGImage.h"
#include "modules/accessibility/AXImageMapLink.h"
#include "modules/accessibility/AXInlineTextBox.h"
#include "modules/accessibility/AXObjectCacheImpl.h"
#include "modules/accessibility/AXSVGRoot.h"
#include "modules/accessibility/AXSpinButton.h"
#include "modules/accessibility/AXTable.h"
#include "platform/fonts/FontTraits.h"
#include "platform/text/PlatformLocale.h"
#include "platform/text/TextDirection.h"
#include "wtf/StdLibExtras.h"
using blink::WebLocalizedString;
namespace blink {
using namespace HTMLNames;
static inline LayoutObject* firstChildInContinuation(const LayoutInline& layoutObject)
{
LayoutBoxModelObject* r = layoutObject.continuation();
while (r) {
if (r->isLayoutBlock())
return r;
if (LayoutObject* child = r->slowFirstChild())
return child;
r = toLayoutInline(r)->continuation();
}
return 0;
}
static inline bool isInlineWithContinuation(LayoutObject* object)
{
if (!object->isBoxModelObject())
return false;
LayoutBoxModelObject* layoutObject = toLayoutBoxModelObject(object);
if (!layoutObject->isLayoutInline())
return false;
return toLayoutInline(layoutObject)->continuation();
}
static inline LayoutObject* firstChildConsideringContinuation(LayoutObject* layoutObject)
{
LayoutObject* firstChild = layoutObject->slowFirstChild();
if (!firstChild && isInlineWithContinuation(layoutObject))
firstChild = firstChildInContinuation(toLayoutInline(*layoutObject));
return firstChild;
}
static inline LayoutInline* startOfContinuations(LayoutObject* r)
{
if (r->isInlineElementContinuation()) {
return toLayoutInline(r->node()->layoutObject());
}
// Blocks with a previous continuation always have a next continuation
if (r->isLayoutBlock() && toLayoutBlock(r)->inlineElementContinuation())
return toLayoutInline(toLayoutBlock(r)->inlineElementContinuation()->node()->layoutObject());
return 0;
}
static inline LayoutObject* endOfContinuations(LayoutObject* layoutObject)
{
LayoutObject* prev = layoutObject;
LayoutObject* cur = layoutObject;
if (!cur->isLayoutInline() && !cur->isLayoutBlock())
return layoutObject;
while (cur) {
prev = cur;
if (cur->isLayoutInline()) {
cur = toLayoutInline(cur)->inlineElementContinuation();
ASSERT(cur || !toLayoutInline(prev)->continuation());
} else {
cur = toLayoutBlock(cur)->inlineElementContinuation();
}
}
return prev;
}
static inline bool lastChildHasContinuation(LayoutObject* layoutObject)
{
LayoutObject* lastChild = layoutObject->slowLastChild();
return lastChild && isInlineWithContinuation(lastChild);
}
static LayoutBoxModelObject* nextContinuation(LayoutObject* layoutObject)
{
ASSERT(layoutObject);
if (layoutObject->isLayoutInline() && !layoutObject->isReplaced())
return toLayoutInline(layoutObject)->continuation();
if (layoutObject->isLayoutBlock())
return toLayoutBlock(layoutObject)->inlineElementContinuation();
return 0;
}
AXLayoutObject::AXLayoutObject(LayoutObject* layoutObject, AXObjectCacheImpl& axObjectCache)
: AXNodeObject(layoutObject->node(), axObjectCache)
, m_layoutObject(layoutObject)
, m_cachedElementRectDirty(true)
{
#if ENABLE(ASSERT)
m_layoutObject->setHasAXObject(true);
#endif
}
AXLayoutObject* AXLayoutObject::create(LayoutObject* layoutObject, AXObjectCacheImpl& axObjectCache)
{
return new AXLayoutObject(layoutObject, axObjectCache);
}
AXLayoutObject::~AXLayoutObject()
{
ASSERT(isDetached());
}
LayoutRect AXLayoutObject::elementRect() const
{
if (!m_explicitElementRect.isEmpty())
return m_explicitElementRect;
if (!m_layoutObject)
return LayoutRect();
if (!m_layoutObject->isBox())
return computeElementRect();
for (const AXObject* obj = this; obj; obj = obj->parentObject()) {
if (obj->isAXLayoutObject())
toAXLayoutObject(obj)->checkCachedElementRect();
}
for (const AXObject* obj = this; obj; obj = obj->parentObject()) {
if (obj->isAXLayoutObject())
toAXLayoutObject(obj)->updateCachedElementRect();
}
return m_cachedElementRect;
}
void AXLayoutObject::setLayoutObject(LayoutObject* layoutObject)
{
m_layoutObject = layoutObject;
setNode(layoutObject->node());
}
LayoutBoxModelObject* AXLayoutObject::layoutBoxModelObject() const
{
if (!m_layoutObject || !m_layoutObject->isBoxModelObject())
return 0;
return toLayoutBoxModelObject(m_layoutObject);
}
Document* AXLayoutObject::topDocument() const
{
if (!document())
return 0;
return &document()->topDocument();
}
bool AXLayoutObject::shouldNotifyActiveDescendant() const
{
// 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();
}
ScrollableArea* AXLayoutObject::getScrollableAreaIfScrollable() const
{
// FIXME(dmazzoni): the plan is to get rid of AXScrollView, but until
// this is done, a WebArea delegates its scrolling to its parent scroll view.
// http://crbug.com/484878
if (parentObject() && parentObject()->isAXScrollView())
return parentObject()->getScrollableAreaIfScrollable();
if (!m_layoutObject || !m_layoutObject->isBox())
return 0;
LayoutBox* box = toLayoutBox(m_layoutObject);
if (!box->canBeScrolledAndHasScrollableArea())
return 0;
return box->scrollableArea();
}
static bool isImageOrAltText(LayoutBoxModelObject* box, Node* node)
{
if (box && box->isImage())
return true;
if (isHTMLImageElement(node))
return true;
if (isHTMLInputElement(node) && toHTMLInputElement(node)->hasFallbackContent())
return true;
return false;
}
AccessibilityRole AXLayoutObject::determineAccessibilityRole()
{
if (!m_layoutObject)
return UnknownRole;
if ((m_ariaRole = determineAriaRoleAttribute()) != UnknownRole)
return m_ariaRole;
Node* node = m_layoutObject->node();
LayoutBoxModelObject* cssBox = layoutBoxModelObject();
if ((cssBox && cssBox->isListItem()) || isHTMLLIElement(node))
return ListItemRole;
if (m_layoutObject->isListMarker())
return ListMarkerRole;
if (m_layoutObject->isBR())
return LineBreakRole;
if (m_layoutObject->isText())
return StaticTextRole;
if (cssBox && isImageOrAltText(cssBox, node)) {
if (node && node->isLink())
return ImageMapRole;
if (isHTMLInputElement(node))
return ariaHasPopup() ? PopUpButtonRole : ButtonRole;
if (isSVGImage())
return SVGRootRole;
return ImageRole;
}
// Note: if JavaScript is disabled, the layoutObject won't be a LayoutHTMLCanvas.
if (isHTMLCanvasElement(node) && m_layoutObject->isCanvas())
return CanvasRole;
if (cssBox && cssBox->isLayoutView())
return WebAreaRole;
if (m_layoutObject->isSVGImage())
return ImageRole;
if (m_layoutObject->isSVGRoot())
return SVGRootRole;
// Table sections should be ignored.
if (m_layoutObject->isTableSection())
return IgnoredRole;
if (m_layoutObject->isHR())
return SplitterRole;
AccessibilityRole role = AXNodeObject::determineAccessibilityRoleUtil();
if (role != UnknownRole)
return role;
if (m_layoutObject->isLayoutBlockFlow())
return GroupRole;
// If the element does not have role, but it has ARIA attributes, accessibility should fallback to exposing it as a group.
if (supportsARIAAttributes())
return GroupRole;
return UnknownRole;
}
void AXLayoutObject::init()
{
AXNodeObject::init();
}
void AXLayoutObject::detach()
{
AXNodeObject::detach();
detachRemoteSVGRoot();
#if ENABLE(ASSERT)
if (m_layoutObject)
m_layoutObject->setHasAXObject(false);
#endif
m_layoutObject = 0;
}
//
// Check object role or purpose.
//
bool AXLayoutObject::isAttachment() const
{
LayoutBoxModelObject* layoutObject = layoutBoxModelObject();
if (!layoutObject)
return false;
// Widgets are the replaced elements that we represent to AX as attachments
bool isLayoutPart = layoutObject->isLayoutPart();
ASSERT(!isLayoutPart || (layoutObject->isReplaced() && !isImage()));
return isLayoutPart;
}
static bool isLinkable(const AXObject& object)
{
if (!object.layoutObject())
return false;
// See https://wiki.mozilla.org/Accessibility/AT-Windows-API for the elements
// Mozilla considers linkable.
return object.isLink() || object.isImage() || object.layoutObject()->isText();
}
// Requires layoutObject to be present because it relies on style
// user-modify. Don't move this logic to AXNodeObject.
bool AXLayoutObject::isEditable() const
{
if (m_layoutObject && m_layoutObject->isTextControl())
return true;
if (node() && node()->isContentEditable())
return true;
if (isWebArea()) {
Document& document = m_layoutObject->document();
HTMLElement* body = document.body();
if (body && body->isContentEditable())
return true;
return document.isContentEditable();
}
return AXNodeObject::isEditable();
}
bool AXLayoutObject::isRichlyEditable() const
{
if (node() && node()->isContentRichlyEditable())
return true;
if (isWebArea()) {
Document& document = m_layoutObject->document();
HTMLElement* body = document.body();
if (body && body->isContentRichlyEditable())
return true;
return document.isContentRichlyEditable();
}
return AXNodeObject::isRichlyEditable();
}
bool AXLayoutObject::isLinked() const
{
if (!isLinkable(*this))
return false;
Element* anchor = anchorElement();
if (!isHTMLAnchorElement(anchor))
return false;
return !toHTMLAnchorElement(*anchor).href().isEmpty();
}
bool AXLayoutObject::isLoaded() const
{
return !m_layoutObject->document().parser();
}
bool AXLayoutObject::isOffScreen() const
{
ASSERT(m_layoutObject);
IntRect contentRect = pixelSnappedIntRect(m_layoutObject->absoluteClippedOverflowRect());
FrameView* view = m_layoutObject->frame()->view();
IntRect viewRect = view->visibleContentRect();
viewRect.intersect(contentRect);
return viewRect.isEmpty();
}
bool AXLayoutObject::isReadOnly() const
{
ASSERT(m_layoutObject);
if (isWebArea()) {
Document& document = m_layoutObject->document();
HTMLElement* body = document.body();
if (body && body->hasEditableStyle())
return false;
return !document.hasEditableStyle();
}
return AXNodeObject::isReadOnly();
}
bool AXLayoutObject::isVisited() const
{
// FIXME: Is it a privacy violation to expose visited information to accessibility APIs?
return m_layoutObject->style()->isLink() && m_layoutObject->style()->insideLink() == InsideVisitedLink;
}
//
// Check object state.
//
bool AXLayoutObject::isFocused() const
{
if (!m_layoutObject)
return false;
Document& document = m_layoutObject->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_layoutObject->node()
|| (roleValue() == WebAreaRole && document.frame()->selection().isFocusedAndActive()))
return true;
return false;
}
bool AXLayoutObject::isSelected() const
{
if (!m_layoutObject)
return false;
Node* node = m_layoutObject->node();
if (!node)
return false;
const AtomicString& ariaSelected = getAttribute(aria_selectedAttr);
if (equalIgnoringCase(ariaSelected, "true"))
return true;
if (isTabItem() && isTabItemSelected())
return true;
return false;
}
//
// Whether objects are ignored, i.e. not included in the tree.
//
AXObjectInclusion AXLayoutObject::defaultObjectInclusion(IgnoredReasons* ignoredReasons) const
{
// The following cases can apply to any element that's a subclass of AXLayoutObject.
if (!m_layoutObject) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXNotRendered));
return IgnoreObject;
}
if (m_layoutObject->style()->visibility() != VISIBLE) {
// aria-hidden is meant to override visibility as the determinant in AX hierarchy inclusion.
if (equalIgnoringCase(getAttribute(aria_hiddenAttr), "false"))
return DefaultBehavior;
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXNotVisible));
return IgnoreObject;
}
return AXObject::defaultObjectInclusion(ignoredReasons);
}
bool AXLayoutObject::computeAccessibilityIsIgnored(IgnoredReasons* ignoredReasons) const
{
#if ENABLE(ASSERT)
ASSERT(m_initialized);
#endif
// 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 AXLayoutObjects take on.
AXObjectInclusion decision = defaultObjectInclusion(ignoredReasons);
if (decision == IncludeObject)
return false;
if (decision == IgnoreObject)
return true;
// If this element is within a parent that cannot have children, it should not be exposed
if (isDescendantOfLeafNode()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXAncestorIsLeafNode, leafNodeAncestor()));
return true;
}
if (roleValue() == IgnoredRole) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXUninteresting));
return true;
}
if (hasInheritedPresentationalRole()) {
if (ignoredReasons) {
const AXObject* inheritsFrom = inheritsPresentationalRoleFrom();
if (inheritsFrom == this)
ignoredReasons->append(IgnoredReason(AXPresentationalRole));
else
ignoredReasons->append(IgnoredReason(AXInheritsPresentation, inheritsFrom));
}
return true;
}
// An ARIA tree can only have tree items and static text as children.
if (AXObject* treeAncestor = treeAncestorDisallowingChild()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXAncestorDisallowsChild, treeAncestor));
return true;
}
// TODO: we should refactor this - but right now this is necessary to make
// sure scroll areas stay in the tree.
if (isAttachment())
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
AXObject* controlObject = correspondingControlForLabelElement();
if (controlObject && !controlObject->deprecatedExposesTitleUIElement() && controlObject->isCheckboxOrRadio()) {
if (ignoredReasons) {
HTMLLabelElement* label = labelElementContainer();
if (label && !label->isSameNode(node())) {
AXObject* labelAXObject = axObjectCache().getOrCreate(label);
ignoredReasons->append(IgnoredReason(AXLabelContainer, labelAXObject));
}
ignoredReasons->append(IgnoredReason(AXLabelFor, controlObject));
}
return true;
}
if (m_layoutObject->isBR())
return false;
if (m_layoutObject->isText()) {
// static text beneath MenuItems and MenuButtons are just reported along with the menu item, so it's ignored on an individual level
AXObject* parent = parentObjectUnignored();
if (parent && (parent->ariaRoleAttribute() == MenuItemRole || parent->ariaRoleAttribute() == MenuButtonRole)) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXStaticTextUsedAsNameFor, parent));
return true;
}
LayoutText* layoutText = toLayoutText(m_layoutObject);
if (!layoutText->firstTextBox()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXEmptyText));
return true;
}
// Don't ignore static text in editable text controls.
for (AXObject* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent->roleValue() == TextFieldRole)
return false;
}
// text elements that are just empty whitespace should not be returned
// FIXME(dmazzoni): we probably shouldn't ignore this if the style is 'pre', or similar...
if (layoutText->text().impl()->containsOnlyWhitespace()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXEmptyText));
return true;
}
return false;
}
if (isHeading())
return false;
if (isLandmarkRelated())
return false;
if (isLink())
return false;
// all controls are accessible
if (isControl())
return false;
if (ariaRoleAttribute() != UnknownRole)
return false;
// don't ignore labels, because they serve as TitleUIElements
Node* node = m_layoutObject->node();
if (isHTMLLabelElement(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;
// List items play an important role in defining the structure of lists. They should not be ignored.
if (roleValue() == ListItemRole)
return false;
if (roleValue() == BlockquoteRole)
return false;
if (roleValue() == DialogRole)
return false;
if (roleValue() == FigcaptionRole)
return false;
if (roleValue() == FigureRole)
return false;
if (roleValue() == DetailsRole)
return false;
if (roleValue() == MathRole)
return false;
if (roleValue() == MeterRole)
return false;
if (roleValue() == RubyRole)
return false;
if (roleValue() == TimeRole)
return false;
if (roleValue() == MarkRole)
return false;
// if this element has aria attributes on it, it should not be ignored.
if (supportsARIAAttributes())
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 GroupRole objects, and GroupRole
// 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 (isHTMLSpanElement(node)) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXUninteresting));
return true;
}
if (m_layoutObject->isLayoutBlockFlow() && m_layoutObject->childrenInline() && !canSetFocusAttribute()) {
if (toLayoutBlockFlow(m_layoutObject)->firstLineBox() || mouseButtonListener())
return false;
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXUninteresting));
return true;
}
// 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;
if (node && node->isElementNode()) {
Element* elt = toElement(node);
const AtomicString& alt = elt->getAttribute(altAttr);
// don't ignore an image that has an alt tag
if (!alt.string().containsOnlyWhitespace())
return false;
// informal standard is to ignore images with zero-length alt strings
if (!alt.isNull()) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXEmptyAlt));
return true;
}
}
if (isNativeImage() && m_layoutObject->isImage()) {
// check for one-dimensional image
LayoutImage* image = toLayoutImage(m_layoutObject);
if (image->size().height() <= 1 || image->size().width() <= 1) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXProbablyPresentational));
return true;
}
// check whether laid out image was stretched from one-dimensional file image
if (image->cachedImage()) {
LayoutSize imageSize = image->cachedImage()->imageSizeForLayoutObject(m_layoutObject, image->view()->zoomFactor());
if (imageSize.height() <= 1 || imageSize.width() <= 1) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXProbablyPresentational));
return true;
}
return false;
}
}
return false;
}
if (isCanvas()) {
if (canvasHasFallbackContent())
return false;
LayoutHTMLCanvas* canvas = toLayoutHTMLCanvas(m_layoutObject);
if (canvas->size().height() <= 1 || canvas->size().width() <= 1) {
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXProbablyPresentational));
return true;
}
// Otherwise fall through; use presence of help text, title, or description to decide.
}
if (isWebArea() || m_layoutObject->isListMarker())
return false;
// Using the help text, title or accessibility description (so we
// check if there's some kind of accessible name for the element)
// to decide an element's visibility is not as definitive as
// previous checks, so this should remain as one of the last.
//
// These checks are simplified in the interest of execution speed;
// for example, any element having an alt attribute will make it
// not ignored, rather than just images.
if (!getAttribute(aria_helpAttr).isEmpty() || !getAttribute(aria_describedbyAttr).isEmpty() || !getAttribute(altAttr).isEmpty() || !getAttribute(titleAttr).isEmpty())
return false;
// Don't ignore generic focusable elements like <div tabindex=0>
// unless they're completely empty, with no children.
if (isGenericFocusableElement() && node->hasChildren())
return false;
if (!ariaAccessibilityDescription().isEmpty())
return false;
if (isScrollableContainer())
return false;
// By default, objects should be ignored so that the AX hierarchy is not
// filled with unnecessary items.
if (ignoredReasons)
ignoredReasons->append(IgnoredReason(AXUninteresting));
return true;
}
//
// Properties of static elements.
//
const AtomicString& AXLayoutObject::accessKey() const
{
Node* node = m_layoutObject->node();
if (!node)
return nullAtom;
if (!node->isElementNode())
return nullAtom;
return toElement(node)->getAttribute(accesskeyAttr);
}
RGBA32 AXLayoutObject::backgroundColor() const
{
if (!m_layoutObject)
return AXNodeObject::backgroundColor();
const ComputedStyle* style = m_layoutObject->style();
if (!style || !style->hasBackground())
return AXNodeObject::backgroundColor();
Color color = style->visitedDependentColor(CSSPropertyBackgroundColor);
return color.rgb();
}
RGBA32 AXLayoutObject::color() const
{
if (!m_layoutObject || isColorWell())
return AXNodeObject::color();
const ComputedStyle* style = m_layoutObject->style();
if (!style)
return AXNodeObject::color();
Color color = style->visitedDependentColor(CSSPropertyColor);
return color.rgb();
}
// Font size is in pixels.
float AXLayoutObject::fontSize() const
{
if (!m_layoutObject)
return AXNodeObject::fontSize();
const ComputedStyle* style = m_layoutObject->style();
if (!style)
return AXNodeObject::fontSize();
return style->computedFontSize();
}
String AXLayoutObject::text() const
{
if (isPasswordFieldAndShouldHideValue()) {
if (!m_layoutObject)
return String();
const ComputedStyle* style = m_layoutObject->style();
if (!style)
return String();
unsigned unmaskedTextLength = AXNodeObject::text().length();
if (!unmaskedTextLength)
return String();
UChar maskCharacter = 0;
switch (style->textSecurity()) {
case TSNONE:
break; // Fall through to the non-password branch.
case TSDISC:
maskCharacter = bulletCharacter;
break;
case TSCIRCLE:
maskCharacter = whiteBulletCharacter;
break;
case TSSQUARE:
maskCharacter = blackSquareCharacter;
break;
}
if (maskCharacter) {
StringBuilder maskedText;
maskedText.reserveCapacity(unmaskedTextLength);
for (unsigned i = 0; i < unmaskedTextLength; ++i)
maskedText.append(maskCharacter);
return maskedText.toString();
}
}
return AXNodeObject::text();
}
AccessibilityTextDirection AXLayoutObject::textDirection() const
{
if (!m_layoutObject)
return AXNodeObject::textDirection();
const ComputedStyle* style = m_layoutObject->style();
if (!style)
return AXNodeObject::textDirection();
if (style->isHorizontalWritingMode()) {
switch (style->direction()) {
case LTR:
return AccessibilityTextDirectionLTR;
case RTL:
return AccessibilityTextDirectionRTL;
}
} else {
switch (style->direction()) {
case LTR:
return AccessibilityTextDirectionTTB;
case RTL:
return AccessibilityTextDirectionBTT;
}
}
return AXNodeObject::textDirection();
}
int AXLayoutObject::textLength() const
{
if (!isTextControl())
return -1;
return text().length();
}
TextStyle AXLayoutObject::textStyle() const
{
if (!m_layoutObject)
return AXNodeObject::textStyle();
const ComputedStyle* style = m_layoutObject->style();
if (!style)
return AXNodeObject::textStyle();
unsigned textStyle = TextStyleNone;
if (style->fontWeight() == FontWeightBold)
textStyle |= TextStyleBold;
if (style->fontDescription().style() == FontStyleItalic)
textStyle |= TextStyleItalic;
if (style->textDecoration() == TextDecorationUnderline)
textStyle |= TextStyleUnderline;
if (style->textDecoration() == TextDecorationLineThrough)
textStyle |= TextStyleLineThrough;
return static_cast<TextStyle>(textStyle);
}
KURL AXLayoutObject::url() const
{
if (isAnchor() && isHTMLAnchorElement(m_layoutObject->node())) {
if (HTMLAnchorElement* anchor = toHTMLAnchorElement(anchorElement()))
return anchor->href();
}
if (isWebArea())
return m_layoutObject->document().url();
if (isImage() && isHTMLImageElement(m_layoutObject->node()))
return toHTMLImageElement(*m_layoutObject->node()).src();
if (isInputImage())
return toHTMLInputElement(m_layoutObject->node())->src();
return KURL();
}
//
// Inline text boxes.
//
void AXLayoutObject::loadInlineTextBoxes()
{
if (!layoutObject() || !layoutObject()->isText())
return;
clearChildren();
addInlineTextBoxChildren(true);
}
AXObject* AXLayoutObject::nextOnLine() const
{
if (!m_layoutObject)
return 0;
InlineBox* inlineBox;
if (m_layoutObject->isLayoutInline())
inlineBox = toLayoutInline(m_layoutObject)->lastLineBox();
else if (m_layoutObject->isText())
inlineBox = toLayoutText(m_layoutObject)->lastTextBox();
else
return 0;
AXObject* result = 0;
for (InlineBox* next = inlineBox->nextOnLine(); next; next = next->nextOnLine()) {
LayoutObject* layoutObject = &next->layoutObject();
result = axObjectCache().getOrCreate(layoutObject);
if (result)
break;
}
// A static text node might span multiple lines. Try to return the first inline
// text box within that static text if possible.
if (result && result->roleValue() == StaticTextRole && result->children().size())
result = result->children()[0].get();
return result;
}
AXObject* AXLayoutObject::previousOnLine() const
{
if (!m_layoutObject)
return 0;
InlineBox* inlineBox;
if (m_layoutObject->isLayoutInline())
inlineBox = toLayoutInline(m_layoutObject)->firstLineBox();
else if (m_layoutObject->isText())
inlineBox = toLayoutText(m_layoutObject)->firstTextBox();
else
return 0;
AXObject* result = 0;
for (InlineBox* prev = inlineBox->prevOnLine(); prev; prev = prev->prevOnLine()) {
LayoutObject* layoutObject = &prev->layoutObject();
result = axObjectCache().getOrCreate(layoutObject);
if (result)
break;
}
// A static text node might span multiple lines. Try to return the last inline
// text box within that static text if possible.
if (result && result->roleValue() == StaticTextRole && result->children().size())
result = result->children()[result->children().size() - 1].get();
return result;
}
//
// Properties of interactive elements.
//
static String queryString(WebLocalizedString::Name name)
{
return Locale::defaultLocale().queryString(name);
}
String AXLayoutObject::actionVerb() const
{
switch (roleValue()) {
case ButtonRole:
case ToggleButtonRole:
return queryString(WebLocalizedString::AXButtonActionVerb);
case TextFieldRole:
return queryString(WebLocalizedString::AXTextFieldActionVerb);
case RadioButtonRole:
return queryString(WebLocalizedString::AXRadioButtonActionVerb);
case CheckBoxRole:
case SwitchRole:
return queryString(isChecked() ? WebLocalizedString::AXCheckedCheckBoxActionVerb : WebLocalizedString::AXUncheckedCheckBoxActionVerb);
case LinkRole:
return queryString(WebLocalizedString::AXLinkActionVerb);
default:
return emptyString();
}
}
String AXLayoutObject::stringValue() const
{
if (!m_layoutObject)
return String();
LayoutBoxModelObject* cssBox = layoutBoxModelObject();
if (ariaRoleAttribute() == StaticTextRole) {
String staticText = text();
if (!staticText.length())
staticText = deprecatedTextUnderElement(TextUnderElementAll);
return staticText;
}
if (m_layoutObject->isText())
return deprecatedTextUnderElement(TextUnderElementAll);
if (cssBox && cssBox->isMenuList()) {
// LayoutMenuList 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 = toHTMLSelectElement(m_layoutObject->node());
int selectedIndex = selectElement->selectedIndex();
const WillBeHeapVector<RawPtrWillBeMember<HTMLElement>>& listItems = selectElement->listItems();
if (selectedIndex >= 0 && static_cast<size_t>(selectedIndex) < listItems.size()) {
const AtomicString& overriddenDescription = listItems[selectedIndex]->fastGetAttribute(aria_labelAttr);
if (!overriddenDescription.isNull())
return overriddenDescription;
}
return toLayoutMenuList(m_layoutObject)->text();
}
if (m_layoutObject->isListMarker())
return toLayoutListMarker(m_layoutObject)->text();
if (isWebArea()) {
// FIXME: Why would a layoutObject exist when the Document isn't attached to a frame?
if (m_layoutObject->frame())
return String();
ASSERT_NOT_REACHED();
}
if (isTextControl())
return text();
if (m_layoutObject->isFileUploadControl())
return toLayoutFileUploadControl(m_layoutObject)->fileTextValue();
// Handle other HTML input elements that aren't text controls, like date and time
// controls, by returning the string value, with the exception of checkboxes
// and radio buttons (which would return "on").
if (node() && isHTMLInputElement(node())) {
HTMLInputElement* input = toHTMLInputElement(node());
if (input->type() != InputTypeNames::checkbox && input->type() != InputTypeNames::radio)
return input->value();
}
// 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();
}
//
// ARIA attributes.
//
AXObject* AXLayoutObject::activeDescendant() const
{
if (!m_layoutObject)
return 0;
if (m_layoutObject->node() && !m_layoutObject->node()->isElementNode())
return 0;
Element* element = toElement(m_layoutObject->node());
if (!element)
return 0;
const AtomicString& activeDescendantAttrStr = element->getAttribute(aria_activedescendantAttr);
if (activeDescendantAttrStr.isNull() || activeDescendantAttrStr.isEmpty())
return 0;
Element* target = element->treeScope().getElementById(activeDescendantAttrStr);
if (!target)
return 0;
AXObject* obj = axObjectCache().getOrCreate(target);
// An activedescendant is only useful if it has a layoutObject, because that's what's needed to post the notification.
if (obj && obj->isAXLayoutObject())
return obj;
return 0;
}
void AXLayoutObject::ariaFlowToElements(AccessibilityChildrenVector& flowTo) const
{
accessibilityChildrenFromAttribute(aria_flowtoAttr, flowTo);
}
void AXLayoutObject::ariaControlsElements(AccessibilityChildrenVector& controls) const
{
accessibilityChildrenFromAttribute(aria_controlsAttr, controls);
}
void AXLayoutObject::deprecatedAriaDescribedbyElements(AccessibilityChildrenVector& describedby) const
{
accessibilityChildrenFromAttribute(aria_describedbyAttr, describedby);
}
void AXLayoutObject::deprecatedAriaLabelledbyElements(AccessibilityChildrenVector& labelledby) const
{
accessibilityChildrenFromAttribute(aria_labelledbyAttr, labelledby);
}
void AXLayoutObject::ariaOwnsElements(AccessibilityChildrenVector& owns) const
{
accessibilityChildrenFromAttribute(aria_ownsAttr, owns);
}
bool AXLayoutObject::ariaHasPopup() const
{
return elementAttributeValue(aria_haspopupAttr);
}
bool AXLayoutObject::ariaRoleHasPresentationalChildren() const
{
switch (m_ariaRole) {
case ButtonRole:
case SliderRole:
case ImageRole:
case ProgressIndicatorRole:
case SpinButtonRole:
// case SeparatorRole:
return true;
default:
return false;
}
}
AXObject* AXLayoutObject::ancestorForWhichThisIsAPresentationalChild() const
{
// Walk the parent chain looking for a parent that has presentational children
AXObject* parent = parentObject();
while (parent) {
if (parent->ariaRoleHasPresentationalChildren())
break;
// The descendants of a AXMenuList that are AXLayoutObjects are all
// presentational. (The real descendants are a AXMenuListPopup and
// AXMenuListOptions, which are not AXLayoutObjects.)
if (parent->isMenuList())
break;
parent = parent->parentObjectIfExists();
}
return parent;
}
bool AXLayoutObject::shouldFocusActiveDescendant() const
{
switch (ariaRoleAttribute()) {
case ComboBoxRole:
case GridRole:
case GroupRole:
case ListBoxRole:
case MenuRole:
case MenuBarRole:
case OutlineRole:
case PopUpButtonRole:
case ProgressIndicatorRole:
case RadioGroupRole:
case RowRole:
case TabListRole:
case ToolbarRole:
case TreeRole:
case TreeGridRole:
return true;
default:
return false;
}
}
bool AXLayoutObject::supportsARIADragging() const
{
const AtomicString& grabbed = getAttribute(aria_grabbedAttr);
return equalIgnoringCase(grabbed, "true") || equalIgnoringCase(grabbed, "false");
}
bool AXLayoutObject::supportsARIADropping() const
{
const AtomicString& dropEffect = getAttribute(aria_dropeffectAttr);
return !dropEffect.isEmpty();
}
bool AXLayoutObject::supportsARIAFlowTo() const
{
return !getAttribute(aria_flowtoAttr).isEmpty();
}
bool AXLayoutObject::supportsARIAOwns() const
{
if (!m_layoutObject)
return false;
const AtomicString& ariaOwns = getAttribute(aria_ownsAttr);
return !ariaOwns.isEmpty();
}
//
// ARIA live-region features.
//
const AtomicString& AXLayoutObject::liveRegionStatus() const
{
DEFINE_STATIC_LOCAL(const AtomicString, liveRegionStatusAssertive, ("assertive", AtomicString::ConstructFromLiteral));
DEFINE_STATIC_LOCAL(const AtomicString, liveRegionStatusPolite, ("polite", AtomicString::ConstructFromLiteral));
DEFINE_STATIC_LOCAL(const AtomicString, liveRegionStatusOff, ("off", AtomicString::ConstructFromLiteral));
const AtomicString& liveRegionStatus = getAttribute(aria_liveAttr);
// These roles have implicit live region status.
if (liveRegionStatus.isEmpty()) {
switch (roleValue()) {
case AlertDialogRole:
case AlertRole:
return liveRegionStatusAssertive;
case LogRole:
case StatusRole:
return liveRegionStatusPolite;
case TimerRole:
case MarqueeRole:
return liveRegionStatusOff;
default:
break;
}
}
return liveRegionStatus;
}
const AtomicString& AXLayoutObject::liveRegionRelevant() const
{
DEFINE_STATIC_LOCAL(const AtomicString, defaultLiveRegionRelevant, ("additions text", AtomicString::ConstructFromLiteral));
const AtomicString& relevant = getAttribute(aria_relevantAttr);
// Default aria-relevant = "additions text".
if (relevant.isEmpty())
return defaultLiveRegionRelevant;
return relevant;
}
bool AXLayoutObject::liveRegionAtomic() const
{
// ARIA role status should have implicit aria-atomic value of true.
if (getAttribute(aria_atomicAttr).isEmpty() && roleValue() == StatusRole)
return true;
return elementAttributeValue(aria_atomicAttr);
}
bool AXLayoutObject::liveRegionBusy() const
{
return elementAttributeValue(aria_busyAttr);
}
//
// Accessibility Text.
//
String AXLayoutObject::deprecatedTextUnderElement(TextUnderElementMode mode) const
{
if (!m_layoutObject)
return String();
if (m_layoutObject->isBR())
return String("\n");
if (m_layoutObject->isFileUploadControl())
return toLayoutFileUploadControl(m_layoutObject)->buttonValue();
if (m_layoutObject->isText()) {
LayoutText* layoutText = toLayoutText(m_layoutObject);
String result = layoutText->plainText();
if (!result.isEmpty() || layoutText->isAllCollapsibleWhitespace())
return result;
return layoutText->text();
}
return AXNodeObject::deprecatedTextUnderElement(mode);
}
//
// Accessibility Text - (To be deprecated).
//
String AXLayoutObject::deprecatedHelpText() const
{
if (!m_layoutObject)
return String();
const AtomicString& ariaHelp = getAttribute(aria_helpAttr);
if (!ariaHelp.isEmpty())
return ariaHelp;
String describedBy = ariaDescribedByAttribute();
if (!describedBy.isEmpty())
return describedBy;
String description = deprecatedAccessibilityDescription();
for (LayoutObject* curr = m_layoutObject; curr; curr = curr->parent()) {
if (curr->node() && curr->node()->isHTMLElement()) {
const AtomicString& summary = toElement(curr->node())->getAttribute(summaryAttr);
if (!summary.isEmpty())
return summary;
// The title attribute should be used as help text unless it is already being used as descriptive text.
const AtomicString& title = toElement(curr->node())->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.
AXObject* axObj = axObjectCache().getOrCreate(curr);
if (axObj) {
AccessibilityRole role = axObj->roleValue();
if (role != GroupRole && role != UnknownRole)
break;
}
}
return String();
}
//
// Position and size.
//
void AXLayoutObject::checkCachedElementRect() const
{
if (m_cachedElementRectDirty)
return;
if (!m_layoutObject)
return;
if (!m_layoutObject->isBox())
return;
bool dirty = false;
LayoutBox* box = toLayoutBox(m_layoutObject);
if (box->frameRect() != m_cachedFrameRect)
dirty = true;
if (box->canBeScrolledAndHasScrollableArea()) {
ScrollableArea* scrollableArea = box->scrollableArea();
if (scrollableArea && scrollableArea->scrollPosition() != m_cachedScrollPosition)
dirty = true;
}
if (dirty)
markCachedElementRectDirty();
}
void AXLayoutObject::updateCachedElementRect() const
{
if (!m_cachedElementRectDirty)
return;
if (!m_layoutObject)
return;
if (!m_layoutObject->isBox())
return;
LayoutBox* box = toLayoutBox(m_layoutObject);
m_cachedFrameRect = box->frameRect();
if (box->canBeScrolledAndHasScrollableArea()) {
ScrollableArea* scrollableArea = box->scrollableArea();
if (scrollableArea)
m_cachedScrollPosition = scrollableArea->scrollPosition();
}
m_cachedElementRect = computeElementRect();
m_cachedElementRectDirty = false;
}
void AXLayoutObject::markCachedElementRectDirty() const
{
if (m_cachedElementRectDirty)
return;
// Marks children recursively, if this element changed.
m_cachedElementRectDirty = true;
for (AXObject* child = firstChild(); child; child = child->nextSibling())
child->markCachedElementRectDirty();
}
IntPoint AXLayoutObject::clickPoint()
{
// Headings are usually much wider than their textual content. If the mid point is used, often it can be wrong.
if (isHeading() && children().size() == 1)
return children()[0]->clickPoint();
// use the default position unless this is an editable web area, in which case we use the selection bounds.
if (!isWebArea() || isReadOnly())
return AXObject::clickPoint();
IntRect bounds = pixelSnappedIntRect(elementRect());
return IntPoint(bounds.x() + (bounds.width() / 2), bounds.y() - (bounds.height() / 2));
}
//
// Hit testing.
//
AXObject* AXLayoutObject::accessibilityHitTest(const IntPoint& point) const
{
if (!m_layoutObject || !m_layoutObject->hasLayer())
return 0;
DeprecatedPaintLayer* layer = toLayoutBox(m_layoutObject)->layer();
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active);
HitTestResult hitTestResult = HitTestResult(request, point);
layer->hitTest(hitTestResult);
if (!hitTestResult.innerNode())
return 0;
Node* node = hitTestResult.innerNode();
// Allow the hit test to return media control buttons.
if (node->isInShadowTree() && (!isHTMLInputElement(*node) || !node->isMediaControlElement()))
node = node->shadowHost();
if (isHTMLAreaElement(node))
return accessibilityImageMapHitTest(toHTMLAreaElement(node), point);
if (isHTMLOptionElement(node))
node = toHTMLOptionElement(*node).ownerSelectElement();
LayoutObject* obj = node->layoutObject();
if (!obj)
return 0;
AXObject* result = axObjectCache().getOrCreate(obj);
result->updateChildrenIfNecessary();
// Allow the element to perform any hit-testing it might need to do to reach non-layout children.
result = result->elementAccessibilityHitTest(point);
if (result && result->accessibilityIsIgnored()) {
// If this element is the label of a control, a hit test should return the control.
if (result->isAXLayoutObject()) {
AXObject* controlObject = toAXLayoutObject(result)->correspondingControlForLabelElement();
if (controlObject && !controlObject->deprecatedExposesTitleUIElement())
return controlObject;
}
result = result->parentObjectUnignored();
}
return result;
}
AXObject* AXLayoutObject::elementAccessibilityHitTest(const IntPoint& point) const
{
if (isSVGImage())
return remoteSVGElementHitTest(point);
return AXObject::elementAccessibilityHitTest(point);
}
//
// High-level accessibility tree access.
//
AXObject* AXLayoutObject::computeParent() const
{
if (!m_layoutObject)
return 0;
if (ariaRoleAttribute() == MenuBarRole)
return axObjectCache().getOrCreate(m_layoutObject->parent());
// menuButton and its corresponding menu are DOM siblings, but Accessibility needs them to be parent/child
if (ariaRoleAttribute() == MenuRole) {
AXObject* parent = menuButtonForMenu();
if (parent)
return parent;
}
LayoutObject* parentObj = layoutParentObject();
if (parentObj)
return axObjectCache().getOrCreate(parentObj);
// WebArea's parent should be the scroll view containing it.
if (isWebArea())
return axObjectCache().getOrCreate(m_layoutObject->frame()->view());
return 0;
}
AXObject* AXLayoutObject::computeParentIfExists() const
{
if (!m_layoutObject)
return 0;
if (ariaRoleAttribute() == MenuBarRole)
return axObjectCache().get(m_layoutObject->parent());
// menuButton and its corresponding menu are DOM siblings, but Accessibility needs them to be parent/child
if (ariaRoleAttribute() == MenuRole) {
AXObject* parent = menuButtonForMenu();
if (parent)
return parent;
}
LayoutObject* parentObj = layoutParentObject();
if (parentObj)
return axObjectCache().get(parentObj);
// WebArea's parent should be the scroll view containing it.
if (isWebArea())
return axObjectCache().get(m_layoutObject->frame()->view());
return 0;
}
//
// Low-level accessibility tree exploration, only for use within the accessibility module.
//
AXObject* AXLayoutObject::firstChild() const
{
if (!m_layoutObject)
return 0;
LayoutObject* firstChild = firstChildConsideringContinuation(m_layoutObject);
if (!firstChild)
return 0;
return axObjectCache().getOrCreate(firstChild);
}
AXObject* AXLayoutObject::nextSibling() const
{
if (!m_layoutObject)
return 0;
LayoutObject* nextSibling = 0;
LayoutInline* inlineContinuation = m_layoutObject->isLayoutBlock() ? toLayoutBlock(m_layoutObject)->inlineElementContinuation() : 0;
if (inlineContinuation) {
// Case 1: node is a block and has an inline continuation. Next sibling is the inline continuation's first child.
nextSibling = firstChildConsideringContinuation(inlineContinuation);
} else if (m_layoutObject->isAnonymousBlock() && lastChildHasContinuation(m_layoutObject)) {
// 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.
LayoutObject* lastParent = endOfContinuations(toLayoutBlock(m_layoutObject)->lastChild())->parent();
while (lastChildHasContinuation(lastParent))
lastParent = endOfContinuations(lastParent->slowLastChild())->parent();
nextSibling = lastParent->nextSibling();
} else if (LayoutObject* ns = m_layoutObject->nextSibling()) {
// Case 3: node has an actual next sibling
nextSibling = ns;
} else if (isInlineWithContinuation(m_layoutObject)) {
// Case 4: node is an inline with a continuation. Next sibling is the next sibling of the end
// of the continuation chain.
nextSibling = endOfContinuations(m_layoutObject)->nextSibling();
} else if (isInlineWithContinuation(m_layoutObject->parent())) {
// Case 5: node has no next sibling, and its parent is an inline with a continuation.
LayoutObject* continuation = toLayoutInline(m_layoutObject->parent())->continuation();
if (continuation->isLayoutBlock()) {
// Case 5a: continuation is a block - in this case the block itself is the next sibling.
nextSibling = continuation;
} else {
// Case 5b: continuation is an inline - in this case the inline's first child is the next sibling.
nextSibling = firstChildConsideringContinuation(continuation);
}
}
if (!nextSibling)
return 0;
return axObjectCache().getOrCreate(nextSibling);
}
void AXLayoutObject::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;
HeapVector<Member<AXObject>> ownedChildren;
computeAriaOwnsChildren(ownedChildren);
for (AXObject* obj = firstChild(); obj; obj = obj->nextSibling()) {
if (!axObjectCache().isAriaOwned(obj))
addChild(obj);
}
addHiddenChildren();
addAttachmentChildren();
addPopupChildren();
addImageMapChildren();
addTextFieldChildren();
addCanvasChildren();
addRemoteSVGChildren();
addInlineTextBoxChildren(false);
for (const auto& child : m_children) {
if (!child->cachedParentObject())
child->setParent(this);
}
for (const auto& ownedChild : ownedChildren)
addChild(ownedChild);
}
bool AXLayoutObject::canHaveChildren() const
{
if (!m_layoutObject)
return false;
return AXNodeObject::canHaveChildren();
}
void AXLayoutObject::updateChildrenIfNecessary()
{
if (needsToUpdateChildren())
clearChildren();
AXObject::updateChildrenIfNecessary();
}
void AXLayoutObject::clearChildren()
{
AXObject::clearChildren();
m_childrenDirty = false;
}
//
// Properties of the object's owning document or page.
//
double AXLayoutObject::estimatedLoadingProgress() const
{
if (!m_layoutObject)
return 0;
if (isLoaded())
return 1.0;
if (LocalFrame* frame = m_layoutObject->document().frame())
return frame->loader().progress().estimatedProgress();
return 0;
}
//
// DOM and layout tree access.
//
Node* AXLayoutObject::node() const
{
return m_layoutObject ? m_layoutObject->node() : 0;
}
Document* AXLayoutObject::document() const
{
if (!m_layoutObject)
return 0;
return &m_layoutObject->document();
}
FrameView* AXLayoutObject::documentFrameView() const
{
if (!m_layoutObject)
return 0;
// this is the LayoutObject's Document's LocalFrame's FrameView
return m_layoutObject->document().view();
}
Element* AXLayoutObject::anchorElement() const
{
if (!m_layoutObject)
return 0;
AXObjectCacheImpl& cache = axObjectCache();
LayoutObject* currLayoutObject;
// Search up the layout tree for a LayoutObject with a DOM node. Defer to an earlier continuation, though.
for (currLayoutObject = m_layoutObject; currLayoutObject && !currLayoutObject->node(); currLayoutObject = currLayoutObject->parent()) {
if (currLayoutObject->isAnonymousBlock()) {
LayoutObject* continuation = toLayoutBlock(currLayoutObject)->continuation();
if (continuation)
return cache.getOrCreate(continuation)->anchorElement();
}
}
// bail if none found
if (!currLayoutObject)
return 0;
// search up the DOM tree for an anchor element
// NOTE: this assumes that any non-image with an anchor is an HTMLAnchorElement
Node* node = currLayoutObject->node();
for ( ; node; node = node->parentNode()) {
if (isHTMLAnchorElement(*node) || (node->layoutObject() && cache.getOrCreate(node->layoutObject())->isAnchor()))
return toElement(node);
}
return 0;
}
Widget* AXLayoutObject::widgetForAttachmentView() const
{
if (!isAttachment())
return 0;
return toLayoutPart(m_layoutObject)->widget();
}
//
// Functions that retrieve the current selection.
//
AXObject::AXRange AXLayoutObject::selection() const
{
AXRange textSelection = textControlSelection();
if (textSelection.isValid())
return textSelection;
if (!layoutObject() || !layoutObject()->frame())
return AXRange();
VisibleSelection selection = layoutObject()->frame()->selection().selection();
RefPtrWillBeRawPtr<Range> selectionRange = firstRangeOf(selection);
if (!selectionRange)
return AXRange();
int anchorOffset = selectionRange->startOffset();
ASSERT(anchorOffset >= 0);
int focusOffset = selectionRange->endOffset();
ASSERT(focusOffset >= 0);
Node* anchorNode = selectionRange->startContainer();
ASSERT(anchorNode);
AXObject* anchorObject = nullptr;
// Find the closest node that has a corresponding AXObject.
// This is because some nodes may be aria hidden or might not even have
// a layout object if they are part of the shadow DOM.
while (anchorNode) {
anchorObject = getUnignoredObjectFromNode(*anchorNode);
if (anchorObject)
break;
if (anchorNode->nextSibling())
anchorNode = anchorNode->nextSibling();
else
anchorNode = anchorNode->parentNode();
}
if (anchorNode != selectionRange->startContainer())
anchorOffset = 0;
Node* focusNode = selectionRange->endContainer();
ASSERT(focusNode);
AXObject* focusObject = nullptr;
while (focusNode) {
focusObject = getUnignoredObjectFromNode(*focusNode);
if (focusObject)
break;
if (focusNode->previousSibling())
focusNode = focusNode->previousSibling();
else
focusNode = focusNode->parentNode();
}
if (focusNode != selectionRange->endContainer())
focusOffset = 0;
if (!anchorObject || !focusObject)
return AXRange();
return AXRange(
anchorObject, anchorOffset,
focusObject, focusOffset);
}
// Gets only the start and end offsets of the selection computed using the
// current object as the starting point. Returns a null selection if there is
// no selection in the subtree rooted at this object.
AXObject::AXRange AXLayoutObject::selectionUnderObject() const
{
AXRange textSelection = textControlSelection();
if (textSelection.isValid())
return textSelection;
if (!layoutObject() || !layoutObject()->frame())
return AXRange();
VisibleSelection selection = layoutObject()->frame()->selection().selection();
RefPtrWillBeRawPtr<Range> selectionRange = firstRangeOf(selection);
ContainerNode* parentNode = node()->parentNode();
int nodeIndex = node()->nodeIndex();
if (!selectionRange
// Selection is contained in node.
|| !(parentNode
&& selectionRange->comparePoint(parentNode, nodeIndex, IGNORE_EXCEPTION) < 0
&& selectionRange->comparePoint(parentNode, nodeIndex + 1, IGNORE_EXCEPTION) > 0)) {
return AXRange();
}
int start = indexForVisiblePosition(selection.visibleStart());
ASSERT(start >= 0);
int end = indexForVisiblePosition(selection.visibleEnd());
ASSERT(end >= 0);
return AXRange(start, end);
}
AXObject::AXRange AXLayoutObject::textControlSelection() const
{
if (!layoutObject())
return AXRange();
LayoutObject* layout = nullptr;
if (layoutObject()->isTextControl()) {
layout = layoutObject();
} else {
Element* focusedElement = document()->focusedElement();
if (focusedElement && focusedElement->layoutObject()
&& focusedElement->layoutObject()->isTextControl())
layout = focusedElement->layoutObject();
}
if (!layout)
return AXRange();
AXObject* axObject = axObjectCache().getOrCreate(layout);
if (!axObject || !axObject->isAXLayoutObject())
return AXRange();
HTMLTextFormControlElement* textControl = toLayoutTextControl(
layout)->textFormControlElement();
ASSERT(textControl);
int start = textControl->selectionStart();
int end = textControl->selectionEnd();
return AXRange(axObject, start, axObject, end);
}
int AXLayoutObject::indexForVisiblePosition(const VisiblePosition& position) const
{
if (layoutObject() && layoutObject()->isTextControl()) {
HTMLTextFormControlElement* textControl = toLayoutTextControl(
layoutObject())->textFormControlElement();
return textControl->indexForVisiblePosition(position);
}
if (!node())
return 0;
Position indexPosition = position.deepEquivalent();
if (indexPosition.isNull())
return 0;
RefPtrWillBeRawPtr<Range> range = Range::create(*document());
range->setStart(node(), 0, IGNORE_EXCEPTION);
range->setEnd(indexPosition, IGNORE_EXCEPTION);
return TextIterator::rangeLength(range->startPosition(), range->endPosition());
}
AXLayoutObject* AXLayoutObject::getUnignoredObjectFromNode(Node& node) const
{
if (isDetached())
return nullptr;
AXObject* axObject = axObjectCache().getOrCreate(&node);
if (!axObject)
return nullptr;
if (axObject->isAXLayoutObject() && !axObject->accessibilityIsIgnored())
return toAXLayoutObject(axObject);
return nullptr;
}
//
// Modify or take an action on an object.
//
void AXLayoutObject::setSelection(const AXRange& selection)
{
if (!layoutObject() || !selection.isValid())
return;
AXObject* anchorObject = selection.anchorObject ?
selection.anchorObject.get() : this;
AXObject* focusObject = selection.focusObject ?
selection.focusObject.get() : this;
if (!isValidSelectionBound(anchorObject)
|| !isValidSelectionBound(focusObject)) {
return;
}
if (anchorObject == focusObject
&& anchorObject->layoutObject()->isTextControl()) {
HTMLTextFormControlElement* textControl = toLayoutTextControl(
anchorObject->layoutObject())->textFormControlElement();
if (selection.anchorOffset <= selection.focusOffset) {
textControl->setSelectionRange(
selection.anchorOffset, selection.focusOffset,
SelectionHasForwardDirection, NotDispatchSelectEvent);
} else {
textControl->setSelectionRange(
selection.focusOffset, selection.anchorOffset,
SelectionHasBackwardDirection, NotDispatchSelectEvent);
}
return;
}
Node* anchorNode = nullptr;
while (anchorObject && !anchorNode) {
anchorNode = anchorObject->node();
anchorObject = anchorObject->parentObject();
}
Node* focusNode = nullptr;
while (focusObject && !focusNode) {
focusNode = focusObject->node();
focusObject = focusObject->parentObject();
}
if (!anchorNode || !focusNode)
return;
LocalFrame* frame = layoutObject()->frame();
if (!frame)
return;
frame->selection().setSelection(VisibleSelection(
Position(anchorNode, selection.anchorOffset),
Position(focusNode, selection.focusOffset)));
}
bool AXLayoutObject::isValidSelectionBound(const AXObject* boundObject) const
{
return layoutObject() && boundObject && !boundObject->isDetached()
&& boundObject->isAXLayoutObject() && boundObject->layoutObject()
&& boundObject->layoutObject()->frame() == layoutObject()->frame()
&& &boundObject->axObjectCache() == &axObjectCache();
}
void AXLayoutObject::setValue(const String& string)
{
if (!node() || !node()->isElementNode())
return;
if (!m_layoutObject || !m_layoutObject->isBoxModelObject())
return;
LayoutBoxModelObject* layoutObject = toLayoutBoxModelObject(m_layoutObject);
if (layoutObject->isTextField() && isHTMLInputElement(*node()))
toHTMLInputElement(*node()).setValue(string, DispatchInputAndChangeEvent);
else if (layoutObject->isTextArea() && isHTMLTextAreaElement(*node()))
toHTMLTextAreaElement(*node()).setValue(string, DispatchInputAndChangeEvent);
}
//
// Notifications that this object may have changed.
//
void AXLayoutObject::handleActiveDescendantChanged()
{
Element* element = toElement(layoutObject()->node());
if (!element)
return;
Document& doc = layoutObject()->document();
if (!doc.frame()->selection().isFocusedAndActive() || doc.focusedElement() != element)
return;
AXLayoutObject* activedescendant = toAXLayoutObject(activeDescendant());
if (activedescendant && shouldNotifyActiveDescendant())
toAXObjectCacheImpl(doc.axObjectCache())->postNotification(m_layoutObject, AXObjectCacheImpl::AXActiveDescendantChanged);
}
void AXLayoutObject::handleAriaExpandedChanged()
{
// Find if a parent of this object should handle aria-expanded changes.
AXObject* containerParent = this->parentObject();
while (containerParent) {
bool foundParent = false;
switch (containerParent->roleValue()) {
case TreeRole:
case TreeGridRole:
case GridRole:
case TableRole:
foundParent = true;
break;
default:
break;
}
if (foundParent)
break;
containerParent = containerParent->parentObject();
}
// Post that the row count changed.
if (containerParent)
axObjectCache().postNotification(containerParent, AXObjectCacheImpl::AXRowCountChanged);
// Post that the specific row either collapsed or expanded.
AccessibilityExpanded expanded = isExpanded();
if (!expanded)
return;
if (roleValue() == RowRole || roleValue() == TreeItemRole) {
AXObjectCacheImpl::AXNotification notification = AXObjectCacheImpl::AXRowExpanded;
if (expanded == ExpandedCollapsed)
notification = AXObjectCacheImpl::AXRowCollapsed;
axObjectCache().postNotification(this, notification);
}
}
void AXLayoutObject::textChanged()
{
if (!m_layoutObject)
return;
Settings* settings = document()->settings();
if (settings && settings->inlineTextBoxAccessibilityEnabled() && roleValue() == StaticTextRole)
childrenChanged();
// Do this last - AXNodeObject::textChanged posts live region announcements,
// and we should update the inline text boxes first.
AXNodeObject::textChanged();
}
//
// Text metrics. Most of these should be deprecated, needs major cleanup.
//
// NOTE: Consider providing this utility method as AX API
int AXLayoutObject::index(const VisiblePosition& position) const
{
if (position.isNull() || !isTextControl())
return -1;
if (layoutObjectContainsPosition(m_layoutObject, position.deepEquivalent()))
return indexForVisiblePosition(position);
return -1;
}
VisiblePosition AXLayoutObject::visiblePositionForIndex(int index) const
{
if (!m_layoutObject)
return VisiblePosition();
if (m_layoutObject->isTextControl())
return toLayoutTextControl(m_layoutObject)->textFormControlElement()->visiblePositionForIndex(index);
if (!allowsTextRanges() && !m_layoutObject->isText())
return VisiblePosition();
Node* node = m_layoutObject->node();
if (!node)
return VisiblePosition();
if (index <= 0)
return createVisiblePosition(firstPositionInOrBeforeNode(node));
Position start, end;
bool selected = Range::selectNodeContents(node, start, end);
if (!selected)
return VisiblePosition();
CharacterIterator it(start, end);
it.advance(index - 1);
return createVisiblePosition(Position(it.currentContainer(), it.endOffset()), TextAffinity::Upstream);
}
void AXLayoutObject::addInlineTextBoxChildren(bool force)
{
Settings* settings = document()->settings();
if (!force && (!settings || !settings->inlineTextBoxAccessibilityEnabled()))
return;
if (!layoutObject() || !layoutObject()->isText())
return;
if (layoutObject()->needsLayout()) {
// If a LayoutText needs layout, its inline text boxes are either
// nonexistent or invalid, so defer until the layout happens and
// the layoutObject calls AXObjectCacheImpl::inlineTextBoxesUpdated.
return;
}
LayoutText* layoutText = toLayoutText(layoutObject());
for (RefPtr<AbstractInlineTextBox> box = layoutText->firstAbstractInlineTextBox(); box.get(); box = box->nextInlineTextBox()) {
AXObject* axObject = axObjectCache().getOrCreate(box.get());
if (!axObject->accessibilityIsIgnored())
m_children.append(axObject);
}
}
void AXLayoutObject::lineBreaks(Vector<int>& lineBreaks) const
{
if (!isTextControl())
return;
VisiblePosition visiblePos = visiblePositionForIndex(0);
VisiblePosition prevVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0, HasEditableAXRole);
// nextLinePosition moves to the end of the current line when there are
// no more lines.
while (visiblePos.isNotNull() && !inSameLine(prevVisiblePos, visiblePos)) {
lineBreaks.append(indexForVisiblePosition(visiblePos));
prevVisiblePos = visiblePos;
visiblePos = nextLinePosition(visiblePos, 0, HasEditableAXRole);
}
}
//
// Private.
//
AXObject* AXLayoutObject::treeAncestorDisallowingChild() const
{
// Determine if this is in a tree. If so, we apply special behavior to make it work like an AXOutline.
AXObject* axObj = parentObject();
AXObject* treeAncestor = 0;
while (axObj) {
if (axObj->isTree()) {
treeAncestor = axObj;
break;
}
axObj = axObj->parentObject();
}
// If the object is in a tree, only tree items should be exposed (and the children of tree items).
if (treeAncestor) {
AccessibilityRole role = roleValue();
if (role != TreeItemRole && role != StaticTextRole)
return treeAncestor;
}
return 0;
}
void AXLayoutObject::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->isSelected() && child->ariaRoleAttribute() == ListBoxOptionRole) {
result.append(child);
if (!isMulti)
return;
}
}
}
bool AXLayoutObject::nodeIsTextControl(const Node* node) const
{
if (!node)
return false;
const AXObject* axObjectForNode = axObjectCache().getOrCreate(const_cast<Node*>(node));
if (!axObjectForNode)
return false;
return axObjectForNode->isTextControl();
}
bool AXLayoutObject::isTabItemSelected() const
{
if (!isTabItem() || !m_layoutObject)
return false;
Node* node = m_layoutObject->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.
AXObject* focusedElement = focusedUIElement();
if (!focusedElement)
return false;
WillBeHeapVector<RawPtrWillBeMember<Element>> elements;
elementsFromAttribute(elements, aria_controlsAttr);
for (const auto& element : elements) {
AXObject* tabPanel = axObjectCache().getOrCreate(element);
// A tab item should only control tab panels.
if (!tabPanel || tabPanel->roleValue() != TabPanelRole)
continue;
AXObject* 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;
}
AXObject* AXLayoutObject::accessibilityImageMapHitTest(HTMLAreaElement* area, const IntPoint& point) const
{
if (!area)
return 0;
AXObject* parent = axObjectCache().getOrCreate(area->imageElement());
if (!parent)
return 0;
for (const auto& child : parent->children()) {
if (child->elementRect().contains(point))
return child.get();
}
return 0;
}
LayoutObject* AXLayoutObject::layoutParentObject() const
{
if (!m_layoutObject)
return 0;
LayoutObject* startOfConts = m_layoutObject->isLayoutBlock() ? startOfContinuations(m_layoutObject) : 0;
if (startOfConts) {
// Case 1: node is a block and is an inline's continuation. Parent
// is the start of the continuation chain.
return startOfConts;
}
LayoutObject* parent = m_layoutObject->parent();
startOfConts = parent && parent->isLayoutInline() ? startOfContinuations(parent) : 0;
if (startOfConts) {
// Case 2: node's parent is an inline which is some node's continuation; parent is
// the earliest node in the continuation chain.
return startOfConts;
}
LayoutObject* firstChild = parent ? parent->slowFirstChild() : 0;
if (firstChild && firstChild->node()) {
// Case 3: The first sibling is the beginning of a continuation chain. Find the origin of that continuation.
// Get the node's layoutObject and follow that continuation chain until the first child is found.
for (LayoutObject* nodeLayoutFirstChild = firstChild->node()->layoutObject(); nodeLayoutFirstChild != firstChild; nodeLayoutFirstChild = firstChild->node()->layoutObject()) {
for (LayoutObject* contsTest = nodeLayoutFirstChild; contsTest; contsTest = nextContinuation(contsTest)) {
if (contsTest == firstChild) {
parent = nodeLayoutFirstChild->parent();
break;
}
}
LayoutObject* newFirstChild = parent->slowFirstChild();
if (firstChild == newFirstChild)
break;
firstChild = newFirstChild;
if (!firstChild->node())
break;
}
}
return parent;
}
bool AXLayoutObject::isSVGImage() const
{
return remoteSVGRootElement();
}
void AXLayoutObject::detachRemoteSVGRoot()
{
if (AXSVGRoot* root = remoteSVGRootElement())
root->setParent(0);
}
AXSVGRoot* AXLayoutObject::remoteSVGRootElement() const
{
// FIXME(dmazzoni): none of this code properly handled multiple references to the same
// remote SVG document. I'm disabling this support until it can be fixed properly.
return 0;
}
AXObject* AXLayoutObject::remoteSVGElementHitTest(const IntPoint& point) const
{
AXObject* remote = remoteSVGRootElement();
if (!remote)
return 0;
IntSize offset = point - roundedIntPoint(elementRect().location());
return remote->accessibilityHitTest(IntPoint(offset));
}
// 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 AXLayoutObject::offsetBoundingBoxForRemoteSVGElement(LayoutRect& rect) const
{
for (AXObject* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent->isAXSVGRoot()) {
rect.moveBy(parent->parentObject()->elementRect().location());
break;
}
}
}
// Hidden children are those that are not laid out or visible, but are specifically marked as aria-hidden=false,
// meaning that they should be exposed to the AX hierarchy.
void AXLayoutObject::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 : NodeTraversal::childrenOf(*node)) {
if (!child.layoutObject() && 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 : NodeTraversal::childrenOf(*node)) {
if (child.layoutObject()) {
// Find out where the last layout sibling is located within m_children.
if (AXObject* childObject = axObjectCache().get(child.layoutObject())) {
if (childObject->accessibilityIsIgnored()) {
const auto& children = childObject->children();
childObject = children.size() ? children.last().get() : 0;
}
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 AXLayoutObject::addTextFieldChildren()
{
Node* node = this->node();
if (!isHTMLInputElement(node))
return;
HTMLInputElement& input = toHTMLInputElement(*node);
Element* spinButtonElement = input.userAgentShadowRoot()->getElementById(ShadowElementNames::spinButton());
if (!spinButtonElement || !spinButtonElement->isSpinButtonElement())
return;
AXSpinButton* axSpinButton = toAXSpinButton(axObjectCache().getOrCreate(SpinButtonRole));
axSpinButton->setSpinButtonElement(toSpinButtonElement(spinButtonElement));
axSpinButton->setParent(this);
m_children.append(axSpinButton);
}
void AXLayoutObject::addImageMapChildren()
{
LayoutBoxModelObject* cssBox = layoutBoxModelObject();
if (!cssBox || !cssBox->isLayoutImage())
return;
HTMLMapElement* map = toLayoutImage(cssBox)->imageMap();
if (!map)
return;
for (HTMLAreaElement& area : Traversal<HTMLAreaElement>::descendantsOf(*map)) {
// add an <area> element for this child if it has a link
if (area.isLink()) {
AXImageMapLink* areaObject = toAXImageMapLink(axObjectCache().getOrCreate(ImageMapLinkRole));
areaObject->setHTMLAreaElement(&area);
areaObject->setHTMLMapElement(map);
areaObject->setParent(this);
ASSERT(areaObject->axObjectID() != 0);
if (!areaObject->accessibilityIsIgnored())
m_children.append(areaObject);
else
axObjectCache().remove(areaObject->axObjectID());
}
}
}
void AXLayoutObject::addCanvasChildren()
{
if (!isHTMLCanvasElement(node()))
return;
// If it's a canvas, it won't have laid out children, but it might have accessible fallback content.
// Clear m_haveChildren because AXNodeObject::addChildren will expect it to be false.
ASSERT(!m_children.size());
m_haveChildren = false;
AXNodeObject::addChildren();
}
void AXLayoutObject::addAttachmentChildren()
{
if (!isAttachment())
return;
// FrameView's need to be inserted into the AX hierarchy when encountered.
Widget* widget = widgetForAttachmentView();
if (!widget || !widget->isFrameView())
return;
AXObject* axWidget = axObjectCache().getOrCreate(widget);
if (!axWidget->accessibilityIsIgnored())
m_children.append(axWidget);
}
void AXLayoutObject::addPopupChildren()
{
if (!isHTMLInputElement(node()))
return;
if (AXObject* axPopup = toHTMLInputElement(node())->popupRootAXObject())
m_children.append(axPopup);
}
void AXLayoutObject::addRemoteSVGChildren()
{
AXSVGRoot* root = remoteSVGRootElement();
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 AXLayoutObject::ariaSelectedRows(AccessibilityChildrenVector& result)
{
// Get all the rows.
AccessibilityChildrenVector allRows;
if (isTree())
ariaTreeRows(allRows);
else if (isAXTable() && toAXTable(this)->supportsSelectedRows())
allRows = toAXTable(this)->rows();
// Determine which rows are selected.
bool isMulti = isMultiSelectable();
// Prefer active descendant over aria-selected.
AXObject* activeDesc = activeDescendant();
if (activeDesc && (activeDesc->isTreeItem() || activeDesc->isTableRow())) {
result.append(activeDesc);
if (!isMulti)
return;
}
for (const auto& row : allRows) {
if (row->isSelected()) {
result.append(row);
if (!isMulti)
break;
}
}
}
bool AXLayoutObject::elementAttributeValue(const QualifiedName& attributeName) const
{
if (!m_layoutObject)
return false;
return equalIgnoringCase(getAttribute(attributeName), "true");
}
LayoutRect AXLayoutObject::computeElementRect() const
{
LayoutObject* obj = m_layoutObject;
if (!obj)
return LayoutRect();
if (obj->node()) // If we are a continuation, we want to make sure to use the primary layoutObject.
obj = obj->node()->layoutObject();
// absoluteFocusRingBoundingBox 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.
LayoutRect result;
if (obj->isText()) {
Vector<FloatQuad> quads;
toLayoutText(obj)->absoluteQuads(quads, 0, LayoutText::ClipToEllipsis);
result = LayoutRect(boundingBoxForQuads(obj, quads));
} else if (isWebArea() || obj->isSVGRoot()) {
result = LayoutRect(obj->absoluteBoundingBoxRect());
} else {
result = LayoutRect(obj->absoluteElementBoundingBoxRect());
}
Document* document = this->document();
if (document && document->isSVGDocument())
offsetBoundingBoxForRemoteSVGElement(result);
if (document && document->frame() && document->frame()->pagePopupOwner()) {
IntPoint popupOrigin = document->view()->contentsToScreen(IntRect()).location();
IntPoint mainOrigin = axObjectCache().rootObject()->documentFrameView()->contentsToScreen(IntRect()).location();
result.moveBy(IntPoint(popupOrigin - mainOrigin));
}
// The size of the web area should be the content size, not the clipped size.
if (isWebArea() && obj->frame()->view())
result.setSize(LayoutSize(obj->frame()->view()->contentsSize()));
// Checkboxes and radio buttons include their label as part of their rect.
if (isCheckboxOrRadio()) {
HTMLLabelElement* label = labelForElement(toElement(m_layoutObject->node()));
if (label && label->layoutObject()) {
LayoutRect labelRect = axObjectCache().getOrCreate(label)->elementRect();
result.unite(labelRect);
}
}
return result;
}
} // namespace blink