blob: 8049baa2d2669df3bb41d2d34d6cd84eb4f7f2ca [file] [log] [blame]
/*
* Copyright (C) 2008, 2009, 2011 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 "AccessibilityObject.h"
#include "AXObjectCache.h"
#include "AccessibilityRenderObject.h"
#include "AccessibilityScrollView.h"
#include "AccessibilityTable.h"
#include "DOMTokenList.h"
#include "Editor.h"
#include "ElementIterator.h"
#include "EventHandler.h"
#include "FloatRect.h"
#include "FocusController.h"
#include "Frame.h"
#include "FrameLoader.h"
#include "FrameSelection.h"
#include "HTMLDetailsElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLParserIdioms.h"
#include "HitTestResult.h"
#include "LocalizedStrings.h"
#include "MainFrame.h"
#include "MathMLNames.h"
#include "NodeList.h"
#include "NodeTraversal.h"
#include "Page.h"
#include "RenderImage.h"
#include "RenderLayer.h"
#include "RenderListItem.h"
#include "RenderListMarker.h"
#include "RenderMenuList.h"
#include "RenderText.h"
#include "RenderTextControl.h"
#include "RenderTheme.h"
#include "RenderView.h"
#include "RenderWidget.h"
#include "RenderedPosition.h"
#include "Settings.h"
#include "TextCheckerClient.h"
#include "TextCheckingHelper.h"
#include "TextIterator.h"
#include "UserGestureIndicator.h"
#include "VisibleUnits.h"
#include "htmlediting.h"
#include <wtf/NeverDestroyed.h>
#include <wtf/StdLibExtras.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/WTFString.h>
#include <wtf/unicode/CharacterNames.h>
namespace WebCore {
using namespace HTMLNames;
AccessibilityObject::AccessibilityObject()
: m_id(0)
, m_haveChildren(false)
, m_role(UnknownRole)
, m_lastKnownIsIgnoredValue(DefaultBehavior)
#if PLATFORM(GTK) || (PLATFORM(EFL) && HAVE(ACCESSIBILITY))
, m_wrapper(nullptr)
#endif
{
}
AccessibilityObject::~AccessibilityObject()
{
ASSERT(isDetached());
}
void AccessibilityObject::detach(AccessibilityDetachmentType detachmentType, AXObjectCache* cache)
{
// Menu close events need to notify the platform. No element is used in the notification because it's a destruction event.
if (detachmentType == ElementDestroyed && roleValue() == MenuRole && cache)
cache->postNotification(nullptr, &cache->document(), AXObjectCache::AXMenuClosed);
// Clear any children and call detachFromParent on them so that
// no children are left with dangling pointers to their parent.
clearChildren();
#if HAVE(ACCESSIBILITY)
setWrapper(nullptr);
#endif
}
bool AccessibilityObject::isDetached() const
{
#if HAVE(ACCESSIBILITY)
return !wrapper();
#else
return true;
#endif
}
bool AccessibilityObject::isAccessibilityObjectSearchMatchAtIndex(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria, size_t index)
{
switch (criteria->searchKeys[index]) {
// The AnyTypeSearchKey matches any non-null AccessibilityObject.
case AnyTypeSearchKey:
return true;
case BlockquoteSameLevelSearchKey:
return criteria->startObject
&& axObject->isBlockquote()
&& axObject->blockquoteLevel() == criteria->startObject->blockquoteLevel();
case BlockquoteSearchKey:
return axObject->isBlockquote();
case BoldFontSearchKey:
return axObject->hasBoldFont();
case ButtonSearchKey:
return axObject->isButton();
case CheckBoxSearchKey:
return axObject->isCheckbox();
case ControlSearchKey:
return axObject->isControl();
case DifferentTypeSearchKey:
return criteria->startObject
&& axObject->roleValue() != criteria->startObject->roleValue();
case FontChangeSearchKey:
return criteria->startObject
&& !axObject->hasSameFont(criteria->startObject->renderer());
case FontColorChangeSearchKey:
return criteria->startObject
&& !axObject->hasSameFontColor(criteria->startObject->renderer());
case FrameSearchKey:
return axObject->isWebArea();
case GraphicSearchKey:
return axObject->isImage();
case HeadingLevel1SearchKey:
return axObject->headingLevel() == 1;
case HeadingLevel2SearchKey:
return axObject->headingLevel() == 2;
case HeadingLevel3SearchKey:
return axObject->headingLevel() == 3;
case HeadingLevel4SearchKey:
return axObject->headingLevel() == 4;
case HeadingLevel5SearchKey:
return axObject->headingLevel() == 5;
case HeadingLevel6SearchKey:
return axObject->headingLevel() == 6;
case HeadingSameLevelSearchKey:
return criteria->startObject
&& axObject->isHeading()
&& axObject->headingLevel() == criteria->startObject->headingLevel();
case HeadingSearchKey:
return axObject->isHeading();
case HighlightedSearchKey:
return axObject->hasHighlighting();
case ItalicFontSearchKey:
return axObject->hasItalicFont();
case LandmarkSearchKey:
return axObject->isLandmark();
case LinkSearchKey:
return axObject->isLink();
case ListSearchKey:
return axObject->isList();
case LiveRegionSearchKey:
return axObject->supportsARIALiveRegion();
case MisspelledWordSearchKey:
return axObject->hasMisspelling();
case OutlineSearchKey:
return axObject->isTree();
case PlainTextSearchKey:
return axObject->hasPlainText();
case RadioGroupSearchKey:
return axObject->isRadioGroup();
case SameTypeSearchKey:
return criteria->startObject
&& axObject->roleValue() == criteria->startObject->roleValue();
case StaticTextSearchKey:
return axObject->isStaticText();
case StyleChangeSearchKey:
return criteria->startObject
&& !axObject->hasSameStyle(criteria->startObject->renderer());
case TableSameLevelSearchKey:
return criteria->startObject
&& is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility()
&& downcast<AccessibilityTable>(*axObject).tableLevel() == criteria->startObject->tableLevel();
case TableSearchKey:
return is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility();
case TextFieldSearchKey:
return axObject->isTextControl();
case UnderlineSearchKey:
return axObject->hasUnderline();
case UnvisitedLinkSearchKey:
return axObject->isUnvisited();
case VisitedLinkSearchKey:
return axObject->isVisited();
default:
return false;
}
}
bool AccessibilityObject::isAccessibilityObjectSearchMatch(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria)
{
if (!axObject || !criteria)
return false;
size_t length = criteria->searchKeys.size();
for (size_t i = 0; i < length; ++i) {
if (isAccessibilityObjectSearchMatchAtIndex(axObject, criteria, i)) {
if (criteria->visibleOnly && !axObject->isOnscreen())
return false;
return true;
}
}
return false;
}
bool AccessibilityObject::isAccessibilityTextSearchMatch(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria)
{
if (!axObject || !criteria)
return false;
return axObject->accessibilityObjectContainsText(&criteria->searchText);
}
bool AccessibilityObject::accessibilityObjectContainsText(String* text) const
{
// If text is null or empty we return true.
return !text
|| text->isEmpty()
|| title().contains(*text, false)
|| accessibilityDescription().contains(*text, false)
|| stringValue().contains(*text, false);
}
// ARIA marks elements as having their accessible name derive from either their contents, or their author provide name.
bool AccessibilityObject::accessibleNameDerivesFromContent() const
{
// First check for objects specifically identified by ARIA.
switch (ariaRoleAttribute()) {
case ApplicationAlertRole:
case ApplicationAlertDialogRole:
case ApplicationDialogRole:
case ApplicationLogRole:
case ApplicationMarqueeRole:
case ApplicationStatusRole:
case ApplicationTimerRole:
case ComboBoxRole:
case DefinitionRole:
case DocumentRole:
case DocumentArticleRole:
case DocumentMathRole:
case DocumentNoteRole:
case LandmarkRegionRole:
case FormRole:
case GridRole:
case GroupRole:
case ImageRole:
case ListRole:
case ListBoxRole:
case LandmarkBannerRole:
case LandmarkComplementaryRole:
case LandmarkContentInfoRole:
case LandmarkNavigationRole:
case LandmarkMainRole:
case LandmarkSearchRole:
case MenuRole:
case MenuBarRole:
case ProgressIndicatorRole:
case RadioGroupRole:
case ScrollBarRole:
case SliderRole:
case SpinButtonRole:
case SplitterRole:
case TableRole:
case TabListRole:
case TabPanelRole:
case TextAreaRole:
case TextFieldRole:
case ToolbarRole:
case TreeGridRole:
case TreeRole:
case WebApplicationRole:
return false;
default:
break;
}
// Now check for generically derived elements now that we know the element does not match a specific ARIA role.
switch (roleValue()) {
case SliderRole:
return false;
default:
break;
}
return true;
}
String AccessibilityObject::computedLabel()
{
// This method is being called by WebKit inspector, which may happen at any time, so we need to update our backing store now.
// Also hold onto this object in case updateBackingStore deletes this node.
RefPtr<AccessibilityObject> protectedThis(this);
updateBackingStore();
Vector<AccessibilityText> text;
accessibilityText(text);
if (text.size())
return text[0].text;
return String();
}
bool AccessibilityObject::isBlockquote() const
{
return roleValue() == BlockquoteRole;
}
bool AccessibilityObject::isTextControl() const
{
switch (roleValue()) {
case ComboBoxRole:
case SearchFieldRole:
case TextAreaRole:
case TextFieldRole:
return true;
default:
return false;
}
}
bool AccessibilityObject::isARIATextControl() const
{
return ariaRoleAttribute() == TextAreaRole || ariaRoleAttribute() == TextFieldRole || ariaRoleAttribute() == SearchFieldRole;
}
bool AccessibilityObject::isNonNativeTextControl() const
{
return (isARIATextControl() || hasContentEditableAttributeSet()) && !isNativeTextControl();
}
bool AccessibilityObject::isLandmark() const
{
AccessibilityRole role = roleValue();
return role == LandmarkBannerRole
|| role == LandmarkComplementaryRole
|| role == LandmarkContentInfoRole
|| role == LandmarkMainRole
|| role == LandmarkNavigationRole
|| role == LandmarkRegionRole
|| role == LandmarkSearchRole;
}
bool AccessibilityObject::hasMisspelling() const
{
if (!node())
return false;
Frame* frame = node()->document().frame();
if (!frame)
return false;
Editor& editor = frame->editor();
TextCheckerClient* textChecker = editor.textChecker();
if (!textChecker)
return false;
bool isMisspelled = false;
if (unifiedTextCheckerEnabled(frame)) {
Vector<TextCheckingResult> results;
checkTextOfParagraph(*textChecker, stringValue(), TextCheckingTypeSpelling, results, frame->selection().selection());
if (!results.isEmpty())
isMisspelled = true;
return isMisspelled;
}
int misspellingLength = 0;
int misspellingLocation = -1;
textChecker->checkSpellingOfString(stringValue(), &misspellingLocation, &misspellingLength);
if (misspellingLength || misspellingLocation != -1)
isMisspelled = true;
return isMisspelled;
}
int AccessibilityObject::blockquoteLevel() const
{
int level = 0;
for (Node* elementNode = node(); elementNode; elementNode = elementNode->parentNode()) {
if (elementNode->hasTagName(blockquoteTag))
++level;
}
return level;
}
AccessibilityObject* AccessibilityObject::parentObjectUnignored() const
{
AccessibilityObject* parent;
for (parent = parentObject(); parent && parent->accessibilityIsIgnored(); parent = parent->parentObject()) {
}
return parent;
}
AccessibilityObject* AccessibilityObject::previousSiblingUnignored(int limit) const
{
AccessibilityObject* previous;
ASSERT(limit >= 0);
for (previous = previousSibling(); previous && previous->accessibilityIsIgnored(); previous = previous->previousSibling()) {
limit--;
if (limit <= 0)
break;
}
return previous;
}
AccessibilityObject* AccessibilityObject::nextSiblingUnignored(int limit) const
{
AccessibilityObject* next;
ASSERT(limit >= 0);
for (next = nextSibling(); next && next->accessibilityIsIgnored(); next = next->nextSibling()) {
limit--;
if (limit <= 0)
break;
}
return next;
}
AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const Node* node)
{
if (!node)
return nullptr;
AXObjectCache* cache = node->document().axObjectCache();
if (!cache)
return nullptr;
AccessibilityObject* accessibleObject = cache->getOrCreate(node->renderer());
while (accessibleObject && accessibleObject->accessibilityIsIgnored()) {
node = NodeTraversal::next(*node);
while (node && !node->renderer())
node = NodeTraversal::nextSkippingChildren(*node);
if (!node)
return nullptr;
accessibleObject = cache->getOrCreate(node->renderer());
}
return accessibleObject;
}
static void appendAccessibilityObject(AccessibilityObject* object, AccessibilityObject::AccessibilityChildrenVector& results)
{
// Find the next descendant of this attachment object so search can continue through frames.
if (object->isAttachment()) {
Widget* widget = object->widgetForAttachmentView();
if (!is<FrameView>(widget))
return;
Document* document = downcast<FrameView>(*widget).frame().document();
if (!document || !document->hasLivingRenderTree())
return;
object = object->axObjectCache()->getOrCreate(document);
}
if (object)
results.append(object);
}
static void appendChildrenToArray(AccessibilityObject* object, bool isForward, AccessibilityObject* startObject, AccessibilityObject::AccessibilityChildrenVector& results)
{
// A table's children includes elements whose own children are also the table's children (due to the way the Mac exposes tables).
// The rows from the table should be queried, since those are direct descendants of the table, and they contain content.
const auto& searchChildren = is<AccessibilityTable>(*object) && downcast<AccessibilityTable>(*object).isExposableThroughAccessibility() ? downcast<AccessibilityTable>(*object).rows() : object->children();
size_t childrenSize = searchChildren.size();
size_t startIndex = isForward ? childrenSize : 0;
size_t endIndex = isForward ? 0 : childrenSize;
size_t searchPosition = startObject ? searchChildren.find(startObject) : WTF::notFound;
if (searchPosition != WTF::notFound) {
if (isForward)
endIndex = searchPosition + 1;
else
endIndex = searchPosition;
}
// This is broken into two statements so that it's easier read.
if (isForward) {
for (size_t i = startIndex; i > endIndex; i--)
appendAccessibilityObject(searchChildren.at(i - 1).get(), results);
} else {
for (size_t i = startIndex; i < endIndex; i++)
appendAccessibilityObject(searchChildren.at(i).get(), results);
}
}
// Returns true if the number of results is now >= the number of results desired.
bool AccessibilityObject::objectMatchesSearchCriteriaWithResultLimit(AccessibilityObject* object, AccessibilitySearchCriteria* criteria, AccessibilityChildrenVector& results)
{
if (isAccessibilityObjectSearchMatch(object, criteria) && isAccessibilityTextSearchMatch(object, criteria)) {
results.append(object);
// Enough results were found to stop searching.
if (results.size() >= criteria->resultsLimit)
return true;
}
return false;
}
void AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria* criteria, AccessibilityChildrenVector& results)
{
ASSERT(criteria);
if (!criteria)
return;
if (AXObjectCache* cache = axObjectCache())
cache->startCachingComputedObjectAttributesUntilTreeMutates();
// This search mechanism only searches the elements before/after the starting object.
// It does this by stepping up the parent chain and at each level doing a DFS.
// If there's no start object, it means we want to search everything.
AccessibilityObject* startObject = criteria->startObject;
if (!startObject)
startObject = this;
bool isForward = criteria->searchDirection == SearchDirectionNext;
// The first iteration of the outer loop will examine the children of the start object for matches. However, when
// iterating backwards, the start object children should not be considered, so the loop is skipped ahead. We make an
// exception when no start object was specified because we want to search everything regardless of search direction.
AccessibilityObject* previousObject = nullptr;
if (!isForward && startObject != this) {
previousObject = startObject;
startObject = startObject->parentObjectUnignored();
}
// The outer loop steps up the parent chain each time (unignored is important here because otherwise elements would be searched twice)
for (AccessibilityObject* stopSearchElement = parentObjectUnignored(); startObject != stopSearchElement; startObject = startObject->parentObjectUnignored()) {
// Only append the children after/before the previous element, so that the search does not check elements that are
// already behind/ahead of start element.
AccessibilityChildrenVector searchStack;
if (!criteria->immediateDescendantsOnly || startObject == this)
appendChildrenToArray(startObject, isForward, previousObject, searchStack);
// This now does a DFS at the current level of the parent.
while (!searchStack.isEmpty()) {
AccessibilityObject* searchObject = searchStack.last().get();
searchStack.removeLast();
if (objectMatchesSearchCriteriaWithResultLimit(searchObject, criteria, results))
break;
if (!criteria->immediateDescendantsOnly)
appendChildrenToArray(searchObject, isForward, 0, searchStack);
}
if (results.size() >= criteria->resultsLimit)
break;
// When moving backwards, the parent object needs to be checked, because technically it's "before" the starting element.
if (!isForward && startObject != this && objectMatchesSearchCriteriaWithResultLimit(startObject, criteria, results))
break;
previousObject = startObject;
}
}
// Returns the range that is fewer positions away from the reference range.
// NOTE: The after range is expected to ACTUALLY be after the reference range and the before
// range is expected to ACTUALLY be before. These are not checked for performance reasons.
static RefPtr<Range> rangeClosestToRange(Range* referenceRange, RefPtr<Range>&& afterRange, RefPtr<Range>&& beforeRange)
{
if (!referenceRange)
return nullptr;
// The treeScope for shadow nodes may not be the same scope as another element in a document.
// Comparisons may fail in that case, which are expected behavior and should not assert.
if (afterRange && (referenceRange->endPosition().isNull() || ((afterRange->startPosition().anchorNode()->compareDocumentPosition(*referenceRange->endPosition().anchorNode()) & Node::DOCUMENT_POSITION_DISCONNECTED) == Node::DOCUMENT_POSITION_DISCONNECTED)))
return nullptr;
ASSERT(!afterRange || afterRange->startPosition() >= referenceRange->endPosition());
if (beforeRange && (referenceRange->startPosition().isNull() || ((beforeRange->endPosition().anchorNode()->compareDocumentPosition(*referenceRange->startPosition().anchorNode()) & Node::DOCUMENT_POSITION_DISCONNECTED) == Node::DOCUMENT_POSITION_DISCONNECTED)))
return nullptr;
ASSERT(!beforeRange || beforeRange->endPosition() <= referenceRange->startPosition());
if (!afterRange && !beforeRange)
return nullptr;
if (afterRange && !beforeRange)
return std::move(afterRange);
if (!afterRange && beforeRange)
return std::move(beforeRange);
unsigned positionsToAfterRange = Position::positionCountBetweenPositions(afterRange->startPosition(), referenceRange->endPosition());
unsigned positionsToBeforeRange = Position::positionCountBetweenPositions(beforeRange->endPosition(), referenceRange->startPosition());
return positionsToAfterRange < positionsToBeforeRange ? afterRange : beforeRange;
}
RefPtr<Range> AccessibilityObject::rangeOfStringClosestToRangeInDirection(Range* referenceRange, AccessibilitySearchDirection searchDirection, Vector<String>& searchStrings) const
{
Frame* frame = this->frame();
if (!frame)
return nullptr;
if (!referenceRange)
return nullptr;
bool isBackwardSearch = searchDirection == SearchDirectionPrevious;
FindOptions findOptions = AtWordStarts | AtWordEnds | CaseInsensitive | StartInSelection;
if (isBackwardSearch)
findOptions |= Backwards;
RefPtr<Range> closestStringRange = nullptr;
for (const auto& searchString : searchStrings) {
if (RefPtr<Range> searchStringRange = frame->editor().rangeOfString(searchString, referenceRange, findOptions)) {
if (!closestStringRange)
closestStringRange = searchStringRange;
else {
// If searching backward, use the trailing range edges to correctly determine which
// range is closest. Similarly, if searching forward, use the leading range edges.
Position closestStringPosition = isBackwardSearch ? closestStringRange->endPosition() : closestStringRange->startPosition();
Position searchStringPosition = isBackwardSearch ? searchStringRange->endPosition() : searchStringRange->startPosition();
int closestPositionOffset = closestStringPosition.computeOffsetInContainerNode();
int searchPositionOffset = searchStringPosition.computeOffsetInContainerNode();
Node* closestContainerNode = closestStringPosition.containerNode();
Node* searchContainerNode = searchStringPosition.containerNode();
short result = Range::compareBoundaryPoints(closestContainerNode, closestPositionOffset, searchContainerNode, searchPositionOffset, ASSERT_NO_EXCEPTION);
if ((!isBackwardSearch && result > 0) || (isBackwardSearch && result < 0))
closestStringRange = searchStringRange;
}
}
}
return closestStringRange;
}
// Returns the range of the entire document if there is no selection.
RefPtr<Range> AccessibilityObject::selectionRange() const
{
Frame* frame = this->frame();
if (!frame)
return nullptr;
const VisibleSelection& selection = frame->selection().selection();
if (!selection.isNone())
return selection.firstRange();
return Range::create(*frame->document());
}
RefPtr<Range> AccessibilityObject::elementRange() const
{
return AXObjectCache::rangeForNodeContents(node());
}
String AccessibilityObject::selectText(AccessibilitySelectTextCriteria* criteria)
{
ASSERT(criteria);
if (!criteria)
return String();
Frame* frame = this->frame();
if (!frame)
return String();
AccessibilitySelectTextActivity& activity = criteria->activity;
AccessibilitySelectTextAmbiguityResolution& ambiguityResolution = criteria->ambiguityResolution;
String& replacementString = criteria->replacementString;
Vector<String>& searchStrings = criteria->searchStrings;
RefPtr<Range> selectedStringRange = selectionRange();
// When starting our search again, make this a zero length range so that search forwards will find this selected range if its appropriate.
selectedStringRange->setEnd(selectedStringRange->startContainer(), selectedStringRange->startOffset());
RefPtr<Range> closestAfterStringRange = nullptr;
RefPtr<Range> closestBeforeStringRange = nullptr;
// Search forward if necessary.
if (ambiguityResolution == ClosestAfterSelectionAmbiguityResolution || ambiguityResolution == ClosestToSelectionAmbiguityResolution)
closestAfterStringRange = rangeOfStringClosestToRangeInDirection(selectedStringRange.get(), SearchDirectionNext, searchStrings);
// Search backward if necessary.
if (ambiguityResolution == ClosestBeforeSelectionAmbiguityResolution || ambiguityResolution == ClosestToSelectionAmbiguityResolution)
closestBeforeStringRange = rangeOfStringClosestToRangeInDirection(selectedStringRange.get(), SearchDirectionPrevious, searchStrings);
// Determine which candidate is closest to the selection and perform the activity.
if (RefPtr<Range> closestStringRange = rangeClosestToRange(selectedStringRange.get(), WTFMove(closestAfterStringRange), WTFMove(closestBeforeStringRange))) {
// If the search started within a text control, ensure that the result is inside that element.
if (element() && element()->isTextFormControl()) {
if (!closestStringRange->startContainer().isDescendantOrShadowDescendantOf(element()) || !closestStringRange->endContainer().isDescendantOrShadowDescendantOf(element()))
return String();
}
String closestString = closestStringRange->text();
bool replaceSelection = false;
if (frame->selection().setSelectedRange(closestStringRange.get(), DOWNSTREAM, true)) {
switch (activity) {
case FindAndCapitalize:
replacementString = closestString;
makeCapitalized(&replacementString, 0);
replaceSelection = true;
break;
case FindAndUppercase:
replacementString = closestString.convertToUppercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
replaceSelection = true;
break;
case FindAndLowercase:
replacementString = closestString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
replaceSelection = true;
break;
case FindAndReplaceActivity: {
replaceSelection = true;
// When applying find and replace activities, we want to match the capitalization of the replaced text,
// (unless we're replacing with an abbreviation.)
if (closestString.length() > 0 && replacementString.length() > 2 && replacementString != replacementString.convertToUppercaseWithoutLocale()) {
if (closestString[0] == u_toupper(closestString[0]))
makeCapitalized(&replacementString, 0);
else
replacementString = replacementString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
}
break;
}
case FindAndSelectActivity:
break;
}
// A bit obvious, but worth noting the API contract for this method is that we should
// return the replacement string when replacing, but the selected string if not.
if (replaceSelection) {
frame->editor().replaceSelectionWithText(replacementString, true, true);
return replacementString;
}
return closestString;
}
}
return String();
}
bool AccessibilityObject::hasAttributesRequiredForInclusion() const
{
// These checks are simplified in the interest of execution speed.
if (!getAttribute(aria_helpAttr).isEmpty()
|| !getAttribute(aria_describedbyAttr).isEmpty()
|| !getAttribute(altAttr).isEmpty()
|| !getAttribute(titleAttr).isEmpty())
return true;
#if ENABLE(MATHML)
if (!getAttribute(MathMLNames::alttextAttr).isEmpty())
return true;
#endif
return false;
}
bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole)
{
return ariaRole == RadioButtonRole || ariaRole == CheckBoxRole || ariaRole == TextFieldRole || ariaRole == SwitchRole || ariaRole == SearchFieldRole;
}
bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole)
{
return isARIAInput(ariaRole) || ariaRole == TextAreaRole || ariaRole == ButtonRole
|| ariaRole == ComboBoxRole || ariaRole == SliderRole;
}
bool AccessibilityObject::isRangeControl() const
{
switch (roleValue()) {
case ProgressIndicatorRole:
case SliderRole:
case ScrollBarRole:
case SpinButtonRole:
return true;
default:
return false;
}
}
bool AccessibilityObject::isMeter() const
{
#if ENABLE(METER_ELEMENT)
RenderObject* renderer = this->renderer();
return renderer && renderer->isMeter();
#else
return false;
#endif
}
IntPoint AccessibilityObject::clickPoint()
{
LayoutRect rect = elementRect();
return roundedIntPoint(LayoutPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2));
}
IntRect AccessibilityObject::boundingBoxForQuads(RenderObject* obj, const Vector<FloatQuad>& quads)
{
ASSERT(obj);
if (!obj)
return IntRect();
FloatRect result;
for (const auto& quad : quads) {
FloatRect r = quad.enclosingBoundingBox();
if (!r.isEmpty()) {
if (obj->style().hasAppearance())
obj->theme().adjustRepaintRect(*obj, r);
result.unite(r);
}
}
return snappedIntRect(LayoutRect(result));
}
bool AccessibilityObject::press()
{
// The presence of the actionElement will confirm whether we should even attempt a press.
Element* actionElem = actionElement();
if (!actionElem)
return false;
if (Frame* f = actionElem->document().frame())
f->loader().resetMultipleFormSubmissionProtection();
// Hit test at this location to determine if there is a sub-node element that should act
// as the target of the action.
Element* hitTestElement = nullptr;
Document* document = this->document();
if (document) {
HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AccessibilityHitTest);
HitTestResult hitTestResult(clickPoint());
document->renderView()->hitTest(request, hitTestResult);
if (hitTestResult.innerNode()) {
Node* innerNode = hitTestResult.innerNode()->deprecatedShadowAncestorNode();
if (is<Element>(*innerNode))
hitTestElement = downcast<Element>(innerNode);
else if (innerNode)
hitTestElement = innerNode->parentElement();
}
}
// Prefer the actionElement instead of this node, if the actionElement is inside this node.
Element* pressElement = this->element();
if (!pressElement || actionElem->isDescendantOf(pressElement))
pressElement = actionElem;
// Prefer the hit test element, if it is inside the target element.
if (hitTestElement && hitTestElement->isDescendantOf(pressElement))
pressElement = hitTestElement;
UserGestureIndicator gestureIndicator(ProcessingUserGesture, document);
bool dispatchedTouchEvent = false;
#if PLATFORM(IOS)
if (hasTouchEventListener())
dispatchedTouchEvent = dispatchTouchEvent();
#endif
if (!dispatchedTouchEvent)
pressElement->accessKeyAction(true);
return true;
}
bool AccessibilityObject::dispatchTouchEvent()
{
bool handled = false;
#if ENABLE(IOS_TOUCH_EVENTS)
MainFrame* frame = mainFrame();
if (!frame)
return false;
handled = frame->eventHandler().dispatchSimulatedTouchEvent(clickPoint());
#endif
return handled;
}
Frame* AccessibilityObject::frame() const
{
Node* node = this->node();
if (!node)
return nullptr;
return node->document().frame();
}
MainFrame* AccessibilityObject::mainFrame() const
{
Document* document = topDocument();
if (!document)
return nullptr;
Frame* frame = document->frame();
if (!frame)
return nullptr;
return &frame->mainFrame();
}
Document* AccessibilityObject::topDocument() const
{
if (!document())
return nullptr;
return &document()->topDocument();
}
String AccessibilityObject::language() const
{
const AtomicString& lang = getAttribute(langAttr);
if (!lang.isEmpty())
return lang;
AccessibilityObject* parent = parentObject();
// as a last resort, fall back to the content language specified in the meta tag
if (!parent) {
Document* doc = document();
if (doc)
return doc->contentLanguage();
return nullAtom;
}
return parent->language();
}
VisiblePositionRange AccessibilityObject::visiblePositionRangeForUnorderedPositions(const VisiblePosition& visiblePos1, const VisiblePosition& visiblePos2) const
{
if (visiblePos1.isNull() || visiblePos2.isNull())
return VisiblePositionRange();
// If there's no common tree scope between positions, return early.
if (!commonTreeScope(visiblePos1.deepEquivalent().deprecatedNode(), visiblePos2.deepEquivalent().deprecatedNode()))
return VisiblePositionRange();
VisiblePosition startPos;
VisiblePosition endPos;
bool alreadyInOrder;
// upstream is ordered before downstream for the same position
if (visiblePos1 == visiblePos2 && visiblePos2.affinity() == UPSTREAM)
alreadyInOrder = false;
// use selection order to see if the positions are in order
else
alreadyInOrder = VisibleSelection(visiblePos1, visiblePos2).isBaseFirst();
if (alreadyInOrder) {
startPos = visiblePos1;
endPos = visiblePos2;
} else {
startPos = visiblePos2;
endPos = visiblePos1;
}
return VisiblePositionRange(startPos, endPos);
}
VisiblePositionRange AccessibilityObject::positionOfLeftWord(const VisiblePosition& visiblePos) const
{
VisiblePosition startPosition = startOfWord(visiblePos, LeftWordIfOnBoundary);
VisiblePosition endPosition = endOfWord(startPosition);
return VisiblePositionRange(startPosition, endPosition);
}
VisiblePositionRange AccessibilityObject::positionOfRightWord(const VisiblePosition& visiblePos) const
{
VisiblePosition startPosition = startOfWord(visiblePos, RightWordIfOnBoundary);
VisiblePosition endPosition = endOfWord(startPosition);
return VisiblePositionRange(startPosition, endPosition);
}
static VisiblePosition updateAXLineStartForVisiblePosition(const VisiblePosition& visiblePosition)
{
// A line in the accessibility sense should include floating objects, such as aligned image, as part of a line.
// So let's update the position to include that.
VisiblePosition tempPosition;
VisiblePosition startPosition = visiblePosition;
while (true) {
tempPosition = startPosition.previous();
if (tempPosition.isNull())
break;
Position p = tempPosition.deepEquivalent();
RenderObject* renderer = p.deprecatedNode()->renderer();
if (!renderer || (renderer->isRenderBlock() && !p.deprecatedEditingOffset()))
break;
if (!RenderedPosition(tempPosition).isNull())
break;
startPosition = tempPosition;
}
return startPosition;
}
VisiblePositionRange AccessibilityObject::leftLineVisiblePositionRange(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePositionRange();
// make a caret selection for the position before marker position (to make sure
// we move off of a line start)
VisiblePosition prevVisiblePos = visiblePos.previous();
if (prevVisiblePos.isNull())
return VisiblePositionRange();
VisiblePosition startPosition = startOfLine(prevVisiblePos);
// keep searching for a valid line start position. Unless the VisiblePosition is at the very beginning, there should
// always be a valid line range. However, startOfLine will return null for position next to a floating object,
// since floating object doesn't really belong to any line.
// This check will reposition the marker before the floating object, to ensure we get a line start.
if (startPosition.isNull()) {
while (startPosition.isNull() && prevVisiblePos.isNotNull()) {
prevVisiblePos = prevVisiblePos.previous();
startPosition = startOfLine(prevVisiblePos);
}
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
VisiblePosition endPosition = endOfLine(prevVisiblePos);
return VisiblePositionRange(startPosition, endPosition);
}
VisiblePositionRange AccessibilityObject::rightLineVisiblePositionRange(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePositionRange();
// make sure we move off of a line end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return VisiblePositionRange();
VisiblePosition startPosition = startOfLine(nextVisiblePos);
// fetch for a valid line start position
if (startPosition.isNull()) {
startPosition = visiblePos;
nextVisiblePos = nextVisiblePos.next();
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
VisiblePosition endPosition = endOfLine(nextVisiblePos);
// as long as the position hasn't reached the end of the doc, keep searching for a valid line end position
// Unless the VisiblePosition is at the very end, there should always be a valid line range. However, endOfLine will
// return null for position by a floating object, since floating object doesn't really belong to any line.
// This check will reposition the marker after the floating object, to ensure we get a line end.
while (endPosition.isNull() && nextVisiblePos.isNotNull()) {
nextVisiblePos = nextVisiblePos.next();
endPosition = endOfLine(nextVisiblePos);
}
return VisiblePositionRange(startPosition, endPosition);
}
VisiblePositionRange AccessibilityObject::sentenceForPosition(const VisiblePosition& visiblePos) const
{
// FIXME: FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
VisiblePosition startPosition = startOfSentence(visiblePos);
VisiblePosition endPosition = endOfSentence(startPosition);
return VisiblePositionRange(startPosition, endPosition);
}
VisiblePositionRange AccessibilityObject::paragraphForPosition(const VisiblePosition& visiblePos) const
{
VisiblePosition startPosition = startOfParagraph(visiblePos);
VisiblePosition endPosition = endOfParagraph(startPosition);
return VisiblePositionRange(startPosition, endPosition);
}
static VisiblePosition startOfStyleRange(const VisiblePosition& visiblePos)
{
RenderObject* renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer();
RenderObject* startRenderer = renderer;
auto* style = &renderer->style();
// traverse backward by renderer to look for style change
for (RenderObject* r = renderer->previousInPreOrder(); r; r = r->previousInPreOrder()) {
// skip non-leaf nodes
if (r->firstChildSlow())
continue;
// stop at style change
if (&r->style() != style)
break;
// remember match
startRenderer = r;
}
return firstPositionInOrBeforeNode(startRenderer->node());
}
static VisiblePosition endOfStyleRange(const VisiblePosition& visiblePos)
{
RenderObject* renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer();
RenderObject* endRenderer = renderer;
const RenderStyle& style = renderer->style();
// traverse forward by renderer to look for style change
for (RenderObject* r = renderer->nextInPreOrder(); r; r = r->nextInPreOrder()) {
// skip non-leaf nodes
if (r->firstChildSlow())
continue;
// stop at style change
if (&r->style() != &style)
break;
// remember match
endRenderer = r;
}
return lastPositionInOrAfterNode(endRenderer->node());
}
VisiblePositionRange AccessibilityObject::styleRangeForPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePositionRange();
return VisiblePositionRange(startOfStyleRange(visiblePos), endOfStyleRange(visiblePos));
}
// NOTE: Consider providing this utility method as AX API
VisiblePositionRange AccessibilityObject::visiblePositionRangeForRange(const PlainTextRange& range) const
{
unsigned textLength = getLengthForTextRange();
if (range.start + range.length > textLength)
return VisiblePositionRange();
VisiblePosition startPosition = visiblePositionForIndex(range.start);
startPosition.setAffinity(DOWNSTREAM);
VisiblePosition endPosition = visiblePositionForIndex(range.start + range.length);
return VisiblePositionRange(startPosition, endPosition);
}
RefPtr<Range> AccessibilityObject::rangeForPlainTextRange(const PlainTextRange& range) const
{
unsigned textLength = getLengthForTextRange();
if (range.start + range.length > textLength)
return nullptr;
if (AXObjectCache* cache = axObjectCache()) {
CharacterOffset start = cache->characterOffsetForIndex(range.start, this);
CharacterOffset end = cache->characterOffsetForIndex(range.start + range.length, this);
return cache->rangeForUnorderedCharacterOffsets(start, end);
}
return nullptr;
}
VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosition& visiblePosition) const
{
VisiblePosition startPosition = startOfLine(visiblePosition);
VisiblePosition endPosition = endOfLine(visiblePosition);
return VisiblePositionRange(startPosition, endPosition);
}
bool AccessibilityObject::replacedNodeNeedsCharacter(Node* replacedNode)
{
// we should always be given a rendered node and a replaced node, but be safe
// replaced nodes are either attachments (widgets) or images
if (!replacedNode || !isRendererReplacedElement(replacedNode->renderer()) || replacedNode->isTextNode())
return false;
// create an AX object, but skip it if it is not supposed to be seen
AccessibilityObject* object = replacedNode->renderer()->document().axObjectCache()->getOrCreate(replacedNode);
if (object->accessibilityIsIgnored())
return false;
return true;
}
// Finds a RenderListItem parent give a node.
static RenderListItem* renderListItemContainerForNode(Node* node)
{
for (; node; node = node->parentNode()) {
RenderBoxModelObject* renderer = node->renderBoxModelObject();
if (is<RenderListItem>(renderer))
return downcast<RenderListItem>(renderer);
}
return nullptr;
}
static String listMarkerTextForNode(Node* node)
{
RenderListItem* listItem = renderListItemContainerForNode(node);
if (!listItem)
return String();
// If this is in a list item, we need to manually add the text for the list marker
// because a RenderListMarker does not have a Node equivalent and thus does not appear
// when iterating text.
return listItem->markerTextWithSuffix();
}
// Returns the text associated with a list marker if this node is contained within a list item.
String AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, const VisiblePosition& visiblePositionStart)
{
// If the range does not contain the start of the line, the list marker text should not be included.
if (!isStartOfLine(visiblePositionStart))
return String();
// We should speak the list marker only for the first line.
RenderListItem* listItem = renderListItemContainerForNode(node);
if (!listItem)
return String();
if (!inSameLine(visiblePositionStart, firstPositionInNode(&listItem->element())))
return String();
return listMarkerTextForNode(node);
}
String AccessibilityObject::stringForRange(RefPtr<Range> range) const
{
if (!range)
return String();
TextIterator it(range.get());
if (it.atEnd())
return String();
StringBuilder builder;
for (; !it.atEnd(); it.advance()) {
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.text().length()) {
// Add a textual representation for list marker text.
// Don't add list marker text for new line character.
if (it.text().length() != 1 || !isSpaceOrNewline(it.text()[0]))
builder.append(listMarkerTextForNodeAndPosition(it.node(), VisiblePosition(range->startPosition())));
it.appendTextToStringBuilder(builder);
} else {
// locate the node and starting offset for this replaced range
Node& node = it.range()->startContainer();
ASSERT(&node == &it.range()->endContainer());
int offset = it.range()->startOffset();
if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
builder.append(objectReplacementCharacter);
}
}
return builder.toString();
}
String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange)
{
if (visiblePositionRange.isNull())
return String();
StringBuilder builder;
RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end);
for (TextIterator it(range.get()); !it.atEnd(); it.advance()) {
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.text().length()) {
// Add a textual representation for list marker text.
builder.append(listMarkerTextForNodeAndPosition(it.node(), visiblePositionRange.start));
it.appendTextToStringBuilder(builder);
} else {
// locate the node and starting offset for this replaced range
Node& node = it.range()->startContainer();
ASSERT(&node == &it.range()->endContainer());
int offset = it.range()->startOffset();
if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
builder.append(objectReplacementCharacter);
}
}
return builder.toString();
}
int AccessibilityObject::lengthForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
{
// FIXME: Multi-byte support
if (visiblePositionRange.isNull())
return -1;
int length = 0;
RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end);
for (TextIterator it(range.get()); !it.atEnd(); it.advance()) {
// non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
if (it.text().length())
length += it.text().length();
else {
// locate the node and starting offset for this replaced range
Node& node = it.range()->startContainer();
ASSERT(&node == &it.range()->endContainer());
int offset = it.range()->startOffset();
if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
++length;
}
}
return length;
}
VisiblePosition AccessibilityObject::visiblePositionForBounds(const IntRect& rect, AccessibilityVisiblePositionForBounds visiblePositionForBounds) const
{
if (rect.isEmpty())
return VisiblePosition();
MainFrame* mainFrame = this->mainFrame();
if (!mainFrame)
return VisiblePosition();
// FIXME: Add support for right-to-left languages.
IntPoint corner = (visiblePositionForBounds == FirstVisiblePositionForBounds) ? rect.minXMinYCorner() : rect.maxXMaxYCorner();
VisiblePosition position = mainFrame->visiblePositionForPoint(corner);
if (rect.contains(position.absoluteCaretBounds().center()))
return position;
// If the initial position is located outside the bounds adjust it incrementally as needed.
VisiblePosition nextPosition = position.next();
VisiblePosition previousPosition = position.previous();
while (nextPosition.isNotNull() || previousPosition.isNotNull()) {
if (rect.contains(nextPosition.absoluteCaretBounds().center()))
return nextPosition;
if (rect.contains(previousPosition.absoluteCaretBounds().center()))
return previousPosition;
nextPosition = nextPosition.next();
previousPosition = previousPosition.previous();
}
return VisiblePosition();
}
VisiblePosition AccessibilityObject::nextWordEnd(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a word end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return VisiblePosition();
return endOfWord(nextVisiblePos, LeftWordIfOnBoundary);
}
VisiblePosition AccessibilityObject::previousWordStart(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a word start
VisiblePosition prevVisiblePos = visiblePos.previous();
if (prevVisiblePos.isNull())
return VisiblePosition();
return startOfWord(prevVisiblePos, RightWordIfOnBoundary);
}
VisiblePosition AccessibilityObject::nextLineEndPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// to make sure we move off of a line end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return VisiblePosition();
VisiblePosition endPosition = endOfLine(nextVisiblePos);
// as long as the position hasn't reached the end of the doc, keep searching for a valid line end position
// There are cases like when the position is next to a floating object that'll return null for end of line. This code will avoid returning null.
while (endPosition.isNull() && nextVisiblePos.isNotNull()) {
nextVisiblePos = nextVisiblePos.next();
endPosition = endOfLine(nextVisiblePos);
}
return endPosition;
}
VisiblePosition AccessibilityObject::previousLineStartPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a line start
VisiblePosition prevVisiblePos = visiblePos.previous();
if (prevVisiblePos.isNull())
return VisiblePosition();
VisiblePosition startPosition = startOfLine(prevVisiblePos);
// as long as the position hasn't reached the beginning of the doc, keep searching for a valid line start position
// There are cases like when the position is next to a floating object that'll return null for start of line. This code will avoid returning null.
if (startPosition.isNull()) {
while (startPosition.isNull() && prevVisiblePos.isNotNull()) {
prevVisiblePos = prevVisiblePos.previous();
startPosition = startOfLine(prevVisiblePos);
}
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
return startPosition;
}
VisiblePosition AccessibilityObject::nextSentenceEndPosition(const VisiblePosition& visiblePos) const
{
// FIXME: FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a sentence end
VisiblePosition nextVisiblePos = visiblePos.next();
if (nextVisiblePos.isNull())
return VisiblePosition();
// an empty line is considered a sentence. If it's skipped, then the sentence parser will not
// see this empty line. Instead, return the end position of the empty line.
VisiblePosition endPosition;
String lineString = plainText(makeRange(startOfLine(nextVisiblePos), endOfLine(nextVisiblePos)).get());
if (lineString.isEmpty())
endPosition = nextVisiblePos;
else
endPosition = endOfSentence(nextVisiblePos);
return endPosition;
}
VisiblePosition AccessibilityObject::previousSentenceStartPosition(const VisiblePosition& visiblePos) const
{
// FIXME: FO 2 IMPLEMENT (currently returns incorrect answer)
// Related? <rdar://problem/3927736> Text selection broken in 8A336
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a sentence start
VisiblePosition previousVisiblePos = visiblePos.previous();
if (previousVisiblePos.isNull())
return VisiblePosition();
// treat empty line as a separate sentence.
VisiblePosition startPosition;
String lineString = plainText(makeRange(startOfLine(previousVisiblePos), endOfLine(previousVisiblePos)).get());
if (lineString.isEmpty())
startPosition = previousVisiblePos;
else
startPosition = startOfSentence(previousVisiblePos);
return startPosition;
}
VisiblePosition AccessibilityObject::nextParagraphEndPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a paragraph end
VisiblePosition nextPos = visiblePos.next();
if (nextPos.isNull())
return VisiblePosition();
return endOfParagraph(nextPos);
}
VisiblePosition AccessibilityObject::previousParagraphStartPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return VisiblePosition();
// make sure we move off of a paragraph start
VisiblePosition previousPos = visiblePos.previous();
if (previousPos.isNull())
return VisiblePosition();
return startOfParagraph(previousPos);
}
AccessibilityObject* AccessibilityObject::accessibilityObjectForPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
return nullptr;
RenderObject* obj = visiblePos.deepEquivalent().deprecatedNode()->renderer();
if (!obj)
return nullptr;
return obj->document().axObjectCache()->getOrCreate(obj);
}
// If you call node->hasEditableStyle() since that will return true if an ancestor is editable.
// This only returns true if this is the element that actually has the contentEditable attribute set.
bool AccessibilityObject::hasContentEditableAttributeSet() const
{
return contentEditableAttributeIsEnabled(element());
}
bool AccessibilityObject::supportsARIAReadOnly() const
{
AccessibilityRole role = roleValue();
return role == CheckBoxRole
|| role == ColumnHeaderRole
|| role == ComboBoxRole
|| role == GridRole
|| role == GridCellRole
|| role == ListBoxRole
|| role == MenuItemCheckboxRole
|| role == MenuItemRadioRole
|| role == RadioGroupRole
|| role == RowHeaderRole
|| role == SearchFieldRole
|| role == SliderRole
|| role == SpinButtonRole
|| role == SwitchRole
|| role == TextFieldRole
|| role == TreeGridRole
|| isPasswordField();
}
String AccessibilityObject::ariaReadOnlyValue() const
{
if (!hasAttribute(aria_readonlyAttr))
return ariaRoleAttribute() != UnknownRole && supportsARIAReadOnly() ? "false" : String();
return getAttribute(aria_readonlyAttr).string().convertToASCIILowercase();
}
bool AccessibilityObject::contentEditableAttributeIsEnabled(Element* element)
{
if (!element)
return false;
const AtomicString& contentEditableValue = element->attributeWithoutSynchronization(contenteditableAttr);
if (contentEditableValue.isNull())
return false;
// Both "true" (case-insensitive) and the empty string count as true.
return contentEditableValue.isEmpty() || equalLettersIgnoringASCIICase(contentEditableValue, "true");
}
#if HAVE(ACCESSIBILITY)
int AccessibilityObject::lineForPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull() || !node())
return -1;
// If the position is not in the same editable region as this AX object, return -1.
Node* containerNode = visiblePos.deepEquivalent().containerNode();
if (!containerNode->containsIncludingShadowDOM(node()) && !node()->containsIncludingShadowDOM(containerNode))
return -1;
int lineCount = -1;
VisiblePosition currentVisiblePos = visiblePos;
VisiblePosition savedVisiblePos;
// move up until we get to the top
// FIXME: This only takes us to the top of the rootEditableElement, not the top of the
// top document.
do {
savedVisiblePos = currentVisiblePos;
VisiblePosition prevVisiblePos = previousLinePosition(currentVisiblePos, 0, HasEditableAXRole);
currentVisiblePos = prevVisiblePos;
++lineCount;
} while (currentVisiblePos.isNotNull() && !(inSameLine(currentVisiblePos, savedVisiblePos)));
return lineCount;
}
#endif
// NOTE: Consider providing this utility method as AX API
PlainTextRange AccessibilityObject::plainTextRangeForVisiblePositionRange(const VisiblePositionRange& positionRange) const
{
int index1 = index(positionRange.start);
int index2 = index(positionRange.end);
if (index1 < 0 || index2 < 0 || 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 screen coordinates. This parameterized attribute returns the
// complete range of characters (including surrogate pairs of multi-byte glyphs) at the given
// screen coordinates.
// NOTE: This varies from AppKit when the point is below the last line. AppKit returns an
// an error in that case. We return textControl->text().length(), 1. Does this matter?
PlainTextRange AccessibilityObject::doAXRangeForPosition(const IntPoint& point) const
{
int i = index(visiblePositionForPoint(point));
if (i < 0)
return PlainTextRange();
return PlainTextRange(i, 1);
}
// Given a character index, the range of text associated with this accessibility object
// over which the style in effect at that character index applies.
PlainTextRange AccessibilityObject::doAXStyleRangeForIndex(unsigned index) const
{
VisiblePositionRange range = styleRangeForPosition(visiblePositionForIndex(index, false));
return plainTextRangeForVisiblePositionRange(range);
}
// Given an indexed character, the line number of the text associated with this accessibility
// object that contains the character.
unsigned AccessibilityObject::doAXLineForIndex(unsigned index)
{
return lineForPosition(visiblePositionForIndex(index, false));
}
#if HAVE(ACCESSIBILITY)
void AccessibilityObject::updateBackingStore()
{
// Updating the layout may delete this object.
RefPtr<AccessibilityObject> protectedThis(this);
if (Document* document = this->document()) {
if (!document->view()->isInRenderTreeLayout())
document->updateLayoutIgnorePendingStylesheets();
}
updateChildrenIfNecessary();
}
#endif
ScrollView* AccessibilityObject::scrollViewAncestor() const
{
for (const AccessibilityObject* scrollParent = this; scrollParent; scrollParent = scrollParent->parentObject()) {
if (is<AccessibilityScrollView>(*scrollParent))
return downcast<AccessibilityScrollView>(*scrollParent).scrollView();
}
return nullptr;
}
Document* AccessibilityObject::document() const
{
FrameView* frameView = documentFrameView();
if (!frameView)
return nullptr;
return frameView->frame().document();
}
Page* AccessibilityObject::page() const
{
Document* document = this->document();
if (!document)
return nullptr;
return document->page();
}
FrameView* AccessibilityObject::documentFrameView() const
{
const AccessibilityObject* object = this;
while (object && !object->isAccessibilityRenderObject())
object = object->parentObject();
if (!object)
return nullptr;
return object->documentFrameView();
}
#if HAVE(ACCESSIBILITY)
const AccessibilityObject::AccessibilityChildrenVector& AccessibilityObject::children(bool updateChildrenIfNeeded)
{
if (updateChildrenIfNeeded)
updateChildrenIfNecessary();
return m_children;
}
#endif
void AccessibilityObject::updateChildrenIfNecessary()
{
if (!hasChildren()) {
// Enable the cache in case we end up adding a lot of children, we don't want to recompute axIsIgnored each time.
AXAttributeCacheEnabler enableCache(axObjectCache());
addChildren();
}
}
void AccessibilityObject::clearChildren()
{
// Some objects have weak pointers to their parents and those associations need to be detached.
for (const auto& child : m_children)
child->detachFromParent();
m_children.clear();
m_haveChildren = false;
}
AccessibilityObject* AccessibilityObject::anchorElementForNode(Node* node)
{
RenderObject* obj = node->renderer();
if (!obj)
return nullptr;
RefPtr<AccessibilityObject> axObj = obj->document().axObjectCache()->getOrCreate(obj);
Element* anchor = axObj->anchorElement();
if (!anchor)
return nullptr;
RenderObject* anchorRenderer = anchor->renderer();
if (!anchorRenderer)
return nullptr;
return anchorRenderer->document().axObjectCache()->getOrCreate(anchorRenderer);
}
AccessibilityObject* AccessibilityObject::headingElementForNode(Node* node)
{
if (!node)
return nullptr;
RenderObject* renderObject = node->renderer();
if (!renderObject)
return nullptr;
AccessibilityObject* axObject = renderObject->document().axObjectCache()->getOrCreate(renderObject);
for (; axObject && axObject->roleValue() != HeadingRole; axObject = axObject->parentObject()) { }
return axObject;
}
void AccessibilityObject::ariaTreeRows(AccessibilityChildrenVector& result)
{
for (const auto& child : children()) {
// Add tree items as the rows.
if (child->roleValue() == TreeItemRole)
result.append(child);
// Now see if this item also has rows hiding inside of it.
child->ariaTreeRows(result);
}
}
void AccessibilityObject::ariaTreeItemContent(AccessibilityChildrenVector& result)
{
// The ARIA tree item content are the item that are not other tree items or their containing groups.
for (const auto& child : children()) {
AccessibilityRole role = child->roleValue();
if (role == TreeItemRole || role == GroupRole)
continue;
result.append(child);
}
}
void AccessibilityObject::ariaTreeItemDisclosedRows(AccessibilityChildrenVector& result)
{
for (const auto& obj : children()) {
// Add tree items as the rows.
if (obj->roleValue() == TreeItemRole)
result.append(obj);
// If it's not a tree item, then descend into the group to find more tree items.
else
obj->ariaTreeRows(result);
}
}
const String AccessibilityObject::defaultLiveRegionStatusForRole(AccessibilityRole role)
{
switch (role) {
case ApplicationAlertDialogRole:
case ApplicationAlertRole:
return ASCIILiteral("assertive");
case ApplicationLogRole:
case ApplicationStatusRole:
return ASCIILiteral("polite");
case ApplicationTimerRole:
case ApplicationMarqueeRole:
return ASCIILiteral("off");
default:
return nullAtom;
}
}
#if HAVE(ACCESSIBILITY)
const String& AccessibilityObject::actionVerb() const
{
#if !PLATFORM(IOS)
// 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(AXCheckedCheckBoxActionVerb());
static NeverDestroyed<const String> uncheckedCheckBoxAction(AXUncheckedCheckBoxActionVerb());
static NeverDestroyed<const String> linkAction(AXLinkActionVerb());
static NeverDestroyed<const String> menuListAction(AXMenuListActionVerb());
static NeverDestroyed<const String> menuListPopupAction(AXMenuListPopupActionVerb());
static NeverDestroyed<const String> listItemAction(AXListItemActionVerb());
switch (roleValue()) {
case ButtonRole:
case ToggleButtonRole:
return buttonAction;
case TextFieldRole:
case TextAreaRole:
return textFieldAction;
case RadioButtonRole:
return radioButtonAction;
case CheckBoxRole:
case SwitchRole:
return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction;
case LinkRole:
case WebCoreLinkRole:
return linkAction;
case PopUpButtonRole:
return menuListAction;
case MenuListPopupRole:
return menuListPopupAction;
case ListItemRole:
return listItemAction;
default:
return nullAtom;
}
#else
return nullAtom;
#endif
}
#endif
bool AccessibilityObject::ariaIsMultiline() const
{
return equalLettersIgnoringASCIICase(getAttribute(aria_multilineAttr), "true");
}
String AccessibilityObject::invalidStatus() const
{
String grammarValue = ASCIILiteral("grammar");
String falseValue = ASCIILiteral("false");
String spellingValue = ASCIILiteral("spelling");
String trueValue = ASCIILiteral("true");
String undefinedValue = ASCIILiteral("undefined");
// aria-invalid can return false (default), grammar, spelling, or true.
String ariaInvalid = stripLeadingAndTrailingHTMLSpaces(getAttribute(aria_invalidAttr));
// If "false", "undefined" [sic, string value], empty, or missing, return "false".
if (ariaInvalid.isEmpty() || ariaInvalid == falseValue || ariaInvalid == undefinedValue)
return falseValue;
// Besides true/false/undefined, the only tokens defined by WAI-ARIA 1.0...
// ...for @aria-invalid are "grammar" and "spelling".
if (ariaInvalid == grammarValue)
return grammarValue;
if (ariaInvalid == spellingValue)
return spellingValue;
// Any other non empty string should be treated as "true".
return trueValue;
}
AccessibilityARIACurrentState AccessibilityObject::ariaCurrentState() const
{
// aria-current can return false (default), true, page, step, location, date or time.
String currentStateValue = stripLeadingAndTrailingHTMLSpaces(getAttribute(aria_currentAttr));
// If "false", empty, or missing, return false state.
if (currentStateValue.isEmpty() || currentStateValue == "false")
return ARIACurrentFalse;
if (currentStateValue == "page")
return ARIACurrentPage;
if (currentStateValue == "step")
return ARIACurrentStep;
if (currentStateValue == "location")
return ARIACurrentLocation;
if (currentStateValue == "date")
return ARIACurrentDate;
if (currentStateValue == "time")
return ARIACurrentTime;
// Any value not included in the list of allowed values should be treated as "true".
return ARIACurrentTrue;
}
bool AccessibilityObject::isAriaModalDescendant(Node* ariaModalNode) const
{
if (!ariaModalNode || !this->element())
return false;
if (this->element() == ariaModalNode)
return true;
// ARIA 1.1 aria-modal, indicates whether an element is modal when displayed.
// For the decendants of the modal object, they should also be considered as aria-modal=true.
for (auto& ancestor : elementAncestors(this->element())) {
if (&ancestor == ariaModalNode)
return true;
}
return false;
}
bool AccessibilityObject::ignoredFromARIAModalPresence() const
{
// We shouldn't ignore the top node.
if (!node() || !node()->parentNode())
return false;
AXObjectCache* cache = axObjectCache();
if (!cache)
return false;
// ariaModalNode is the current displayed modal dialog.
Node* ariaModalNode = cache->ariaModalNode();
if (!ariaModalNode)
return false;
// We only want to ignore the objects within the same frame as the modal dialog.
if (ariaModalNode->document().frame() != this->frame())
return false;
return !isAriaModalDescendant(ariaModalNode);
}
bool AccessibilityObject::hasTagName(const QualifiedName& tagName) const
{
Node* node = this->node();
return is<Element>(node) && downcast<Element>(*node).hasTagName(tagName);
}
bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
{
Node* node = this->node();
if (!is<Element>(node))
return false;
return downcast<Element>(*node).hasAttributeWithoutSynchronization(attribute);
}
const AtomicString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const
{
if (Element* element = this->element())
return element->attributeWithoutSynchronization(attribute);
return nullAtom;
}
// Lacking concrete evidence of orientation, horizontal means width > height. vertical is height > width;
AccessibilityOrientation AccessibilityObject::orientation() const
{
LayoutRect bounds = elementRect();
if (bounds.size().width() > bounds.size().height())
return AccessibilityOrientationHorizontal;
if (bounds.size().height() > bounds.size().width())
return AccessibilityOrientationVertical;
return AccessibilityOrientationUndefined;
}
bool AccessibilityObject::isDescendantOfObject(const AccessibilityObject* axObject) const
{
if (!axObject || !axObject->hasChildren())
return false;
for (const AccessibilityObject* parent = parentObject(); parent; parent = parent->parentObject()) {
if (parent == axObject)
return true;
}
return false;
}
bool AccessibilityObject::isAncestorOfObject(const AccessibilityObject* axObject) const
{
if (!axObject)
return false;
return this == axObject || axObject->isDescendantOfObject(this);
}
AccessibilityObject* AccessibilityObject::firstAnonymousBlockChild() const
{
for (AccessibilityObject* child = firstChild(); child; child = child->nextSibling()) {
if (child->renderer() && child->renderer()->isAnonymousBlock())
return child;
}
return nullptr;
}
typedef HashMap<String, AccessibilityRole, ASCIICaseInsensitiveHash> ARIARoleMap;
typedef HashMap<AccessibilityRole, String, DefaultHash<int>::Hash, WTF::UnsignedWithZeroKeyHashTraits<int>> ARIAReverseRoleMap;
static ARIARoleMap* gAriaRoleMap = nullptr;
static ARIAReverseRoleMap* gAriaReverseRoleMap = nullptr;
struct RoleEntry {
String ariaRole;
AccessibilityRole webcoreRole;
};
static void initializeRoleMap()
{
if (gAriaRoleMap)
return;
ASSERT(!gAriaReverseRoleMap);
const RoleEntry roles[] = {
{ "alert", ApplicationAlertRole },
{ "alertdialog", ApplicationAlertDialogRole },
{ "application", WebApplicationRole },
{ "article", DocumentArticleRole },
{ "banner", LandmarkBannerRole },
{ "button", ButtonRole },
{ "checkbox", CheckBoxRole },
{ "complementary", LandmarkComplementaryRole },
{ "contentinfo", LandmarkContentInfoRole },
{ "dialog", ApplicationDialogRole },
{ "directory", DirectoryRole },
{ "grid", GridRole },
{ "gridcell", GridCellRole },
{ "table", TableRole },
{ "cell", CellRole },
{ "columnheader", ColumnHeaderRole },
{ "combobox", ComboBoxRole },
{ "definition", DefinitionRole },
{ "document", DocumentRole },
{ "form", FormRole },
{ "rowheader", RowHeaderRole },
{ "group", GroupRole },
{ "heading", HeadingRole },
{ "img", ImageRole },
{ "link", WebCoreLinkRole },
{ "list", ListRole },
{ "listitem", ListItemRole },
{ "listbox", ListBoxRole },
{ "log", ApplicationLogRole },
// "option" isn't here because it may map to different roles depending on the parent element's role
{ "main", LandmarkMainRole },
{ "marquee", ApplicationMarqueeRole },
{ "math", DocumentMathRole },
{ "menu", MenuRole },
{ "menubar", MenuBarRole },
{ "menuitem", MenuItemRole },
{ "menuitemcheckbox", MenuItemCheckboxRole },
{ "menuitemradio", MenuItemRadioRole },
{ "none", PresentationalRole },
{ "note", DocumentNoteRole },
{ "navigation", LandmarkNavigationRole },
{ "option", ListBoxOptionRole },
{ "presentation", PresentationalRole },
{ "progressbar", ProgressIndicatorRole },
{ "radio", RadioButtonRole },
{ "radiogroup", RadioGroupRole },
{ "region", LandmarkRegionRole },
{ "row", RowRole },
{ "rowgroup", RowGroupRole },
{ "scrollbar", ScrollBarRole },
{ "search", LandmarkSearchRole },
{ "searchbox", SearchFieldRole },
{ "separator", SplitterRole },
{ "slider", SliderRole },
{ "spinbutton", SpinButtonRole },
{ "status", ApplicationStatusRole },
{ "switch", SwitchRole },
{ "tab", TabRole },
{ "tablist", TabListRole },
{ "tabpanel", TabPanelRole },
{ "text", StaticTextRole },
{ "textbox", TextAreaRole },
{ "timer", ApplicationTimerRole },
{ "toolbar", ToolbarRole },
{ "tooltip", UserInterfaceTooltipRole },
{ "tree", TreeRole },
{ "treegrid", TreeGridRole },
{ "treeitem", TreeItemRole }
};
gAriaRoleMap = new ARIARoleMap;
gAriaReverseRoleMap = new ARIAReverseRoleMap;
size_t roleLength = WTF_ARRAY_LENGTH(roles);
for (size_t i = 0; i < roleLength; ++i) {
gAriaRoleMap->set(roles[i].ariaRole, roles[i].webcoreRole);
gAriaReverseRoleMap->set(roles[i].webcoreRole, roles[i].ariaRole);
}
}
static ARIARoleMap& ariaRoleMap()
{
initializeRoleMap();
return *gAriaRoleMap;
}
static ARIAReverseRoleMap& reverseAriaRoleMap()
{
initializeRoleMap();
return *gAriaReverseRoleMap;
}
AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value)
{
ASSERT(!value.isEmpty());
Vector<String> roleVector;
value.split(' ', roleVector);
AccessibilityRole role = UnknownRole;
for (const auto& roleName : roleVector) {
role = ariaRoleMap().get(roleName);
if (role)
return role;
}
return role;
}
String AccessibilityObject::computedRoleString() const
{
// FIXME: Need a few special cases that aren't in the RoleMap: option, etc. http://webkit.org/b/128296
AccessibilityRole role = roleValue();
if (role == HorizontalRuleRole)
role = SplitterRole;
return reverseAriaRoleMap().get(role);
}
bool AccessibilityObject::hasHighlighting() const
{
for (Node* node = this->node(); node; node = node->parentNode()) {
if (node->hasTagName(markTag))
return true;
}
return false;
}
String AccessibilityObject::roleDescription() const
{
return getAttribute(aria_roledescriptionAttr);
}
static bool nodeHasPresentationRole(Node* node)
{
return nodeHasRole(node, "presentation") || nodeHasRole(node, "none");
}
bool AccessibilityObject::supportsPressAction() const
{
if (isButton())
return true;
if (roleValue() == DetailsRole)
return true;
Element* actionElement = this->actionElement();
if (!actionElement)
return false;
// [Bug: 136247] Heuristic: element handlers that have more than one accessible descendant should not be exposed as supporting press.
if (actionElement != element()) {
if (AccessibilityObject* axObj = axObjectCache()->getOrCreate(actionElement)) {
AccessibilityChildrenVector results;
// Search within for immediate descendants that are static text. If we find more than one
// then this is an event delegator actionElement and we should expose the press action.
Vector<AccessibilitySearchKey> keys({ StaticTextSearchKey, ControlSearchKey, GraphicSearchKey, HeadingSearchKey, LinkSearchKey });
AccessibilitySearchCriteria criteria(axObj, SearchDirectionNext, emptyString(), 2, false, false);
criteria.searchKeys = keys;
axObj->findMatchingObjects(&criteria, results);
if (results.size() > 1)
return false;
}
}
// [Bug: 133613] Heuristic: If the action element is presentational, we shouldn't expose press as a supported action.
return !nodeHasPresentationRole(actionElement);
}
bool AccessibilityObject::supportsDatetimeAttribute() const
{
return hasTagName(insTag) || hasTagName(delTag) || hasTagName(timeTag);
}
Element* AccessibilityObject::element() const
{
Node* node = this->node();
if (is<Element>(node))
return downcast<Element>(node);
return nullptr;
}
bool AccessibilityObject::isValueAutofillAvailable() const
{
if (!isNativeTextControl())
return false;
Node* node = this->node();
if (!is<HTMLInputElement>(node))
return false;
return downcast<HTMLInputElement>(*node).autoFillButtonType() != AutoFillButtonType::None;
}
AutoFillButtonType AccessibilityObject::valueAutofillButtonType() const
{
if (!isValueAutofillAvailable())
return AutoFillButtonType::None;
return downcast<HTMLInputElement>(*this->node()).autoFillButtonType();
}
bool AccessibilityObject::isValueAutofilled() const
{
if (!isNativeTextControl())
return false;
Node* node = this->node();
if (!is<HTMLInputElement>(node))
return false;
return downcast<HTMLInputElement>(*node).isAutoFilled();
}
const AtomicString& AccessibilityObject::placeholderValue() const
{
const AtomicString& ariaPlaceholder = getAttribute(aria_placeholderAttr);
if (!ariaPlaceholder.isEmpty())
return ariaPlaceholder;
const AtomicString& placeholder = getAttribute(placeholderAttr);
if (!placeholder.isEmpty())
return placeholder;
return nullAtom;
}
bool AccessibilityObject::isInsideARIALiveRegion() const
{
if (supportsARIALiveRegion())
return true;
for (AccessibilityObject* axParent = parentObject(); axParent; axParent = axParent->parentObject()) {
if (axParent->supportsARIALiveRegion())
return true;
}
return false;
}
bool AccessibilityObject::supportsARIAAttributes() const
{
// This returns whether the element supports any global ARIA attributes.
return supportsARIALiveRegion()
|| supportsARIADragging()
|| supportsARIADropping()
|| supportsARIAOwns()
|| hasAttribute(aria_atomicAttr)
|| hasAttribute(aria_busyAttr)
|| hasAttribute(aria_controlsAttr)
|| hasAttribute(aria_currentAttr)
|| hasAttribute(aria_describedbyAttr)
|| hasAttribute(aria_disabledAttr)
|| hasAttribute(aria_flowtoAttr)
|| hasAttribute(aria_haspopupAttr)
|| hasAttribute(aria_invalidAttr)
|| hasAttribute(aria_labelAttr)
|| hasAttribute(aria_labelledbyAttr)
|| hasAttribute(aria_relevantAttr);
}
bool AccessibilityObject::liveRegionStatusIsEnabled(const AtomicString& liveRegionStatus)
{
return equalLettersIgnoringASCIICase(liveRegionStatus, "polite") || equalLettersIgnoringASCIICase(liveRegionStatus, "assertive");
}
bool AccessibilityObject::supportsARIALiveRegion() const
{
return liveRegionStatusIsEnabled(ariaLiveRegionStatus());
}
AccessibilityObject* AccessibilityObject::elementAccessibilityHitTest(const IntPoint& point) const
{
// Send the hit test back into the sub-frame if necessary.
if (isAttachment()) {
Widget* widget = widgetForAttachmentView();
// Normalize the point for the widget's bounds.
if (widget && widget->isFrameView()) {
if (AXObjectCache* cache = axObjectCache())
return cache->getOrCreate(widget)->accessibilityHitTest(IntPoint(point - widget->frameRect().location()));
}
}
// Check if there are any mock elements that need to be handled.
for (const auto& child : m_children) {
if (child->isMockObject() && child->elementRect().contains(point))
return child->elementAccessibilityHitTest(point);
}
return const_cast<AccessibilityObject*>(this);
}
AXObjectCache* AccessibilityObject::axObjectCache() const
{
Document* doc = document();
if (doc)
return doc->axObjectCache();
return nullptr;
}
AccessibilityObject* AccessibilityObject::focusedUIElement() const
{
Document* doc = document();
if (!doc)
return nullptr;
Page* page = doc->page();
if (!page)
return nullptr;
return AXObjectCache::focusedUIElementForPage(page);
}
AccessibilitySortDirection AccessibilityObject::sortDirection() const
{
const AtomicString& sortAttribute = getAttribute(aria_sortAttr);
if (equalLettersIgnoringASCIICase(sortAttribute, "ascending"))
return SortDirectionAscending;
if (equalLettersIgnoringASCIICase(sortAttribute, "descending"))
return SortDirectionDescending;
if (equalLettersIgnoringASCIICase(sortAttribute, "other"))
return SortDirectionOther;
return SortDirectionNone;
}
bool AccessibilityObject::supportsRangeValue() const
{
return isProgressIndicator()
|| isSlider()
|| isScrollbar()
|| isSpinButton()
|| isAttachmentElement();
}
bool AccessibilityObject::supportsARIASetSize() const
{
return hasAttribute(aria_setsizeAttr);
}
bool AccessibilityObject::supportsARIAPosInSet() const
{
return hasAttribute(aria_posinsetAttr);
}
int AccessibilityObject::ariaSetSize() const
{
return getAttribute(aria_setsizeAttr).toInt();
}
int AccessibilityObject::ariaPosInSet() const
{
return getAttribute(aria_posinsetAttr).toInt();
}
String AccessibilityObject::identifierAttribute() const
{
return getAttribute(idAttr);
}
void AccessibilityObject::classList(Vector<String>& classList) const
{
Node* node = this->node();
if (!is<Element>(node))
return;
Element* element = downcast<Element>(node);
DOMTokenList& list = element->classList();
unsigned length = list.length();
for (unsigned k = 0; k < length; k++)
classList.append(list.item(k).string());
}
bool AccessibilityObject::supportsARIAPressed() const
{
const AtomicString& expanded = getAttribute(aria_pressedAttr);
return equalLettersIgnoringASCIICase(expanded, "true") || equalLettersIgnoringASCIICase(expanded, "false");
}
bool AccessibilityObject::supportsExpanded() const
{
// Undefined values should not result in this attribute being exposed to ATs according to ARIA.
const AtomicString& expanded = getAttribute(aria_expandedAttr);
if (equalLettersIgnoringASCIICase(expanded, "true") || equalLettersIgnoringASCIICase(expanded, "false"))
return true;
switch (roleValue()) {
case ComboBoxRole:
case DisclosureTriangleRole:
case DetailsRole:
return true;
default:
return false;
}
}
bool AccessibilityObject::isExpanded() const
{
if (equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"))
return true;
if (is<HTMLDetailsElement>(node()))
return downcast<HTMLDetailsElement>(node())->isOpen();
return false;
}
bool AccessibilityObject::supportsChecked() const
{
switch (roleValue()) {
case CheckBoxRole:
case MenuItemCheckboxRole:
case MenuItemRadioRole:
case RadioButtonRole:
case SwitchRole:
return true;
default:
return false;
}
}
AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const
{
// If this is a real checkbox or radio button, AccessibilityRenderObject will handle.
// If it's an ARIA checkbox, radio, or switch the aria-checked attribute should be used.
// If it's a toggle button, the aria-pressed attribute is consulted.
if (isToggleButton()) {
const AtomicString& ariaPressed = getAttribute(aria_pressedAttr);
if (equalLettersIgnoringASCIICase(ariaPressed, "true"))
return ButtonStateOn;
if (equalLettersIgnoringASCIICase(ariaPressed, "mixed"))
return ButtonStateMixed;
return ButtonStateOff;
}
const AtomicString& result = getAttribute(aria_checkedAttr);
if (equalLettersIgnoringASCIICase(result, "true"))
return ButtonStateOn;
if (equalLettersIgnoringASCIICase(result, "mixed")) {
// ARIA says that radio, menuitemradio, and switch elements must NOT expose button state mixed.
AccessibilityRole ariaRole = ariaRoleAttribute();
if (ariaRole == RadioButtonRole || ariaRole == MenuItemRadioRole || ariaRole == SwitchRole)
return ButtonStateOff;
return ButtonStateMixed;
}
if (isIndeterminate())
return ButtonStateMixed;
return ButtonStateOff;
}
// This is a 1-dimensional scroll offset helper function that's applied
// separately in the horizontal and vertical directions, because the
// logic is the same. The goal is to compute the best scroll offset
// in order to make an object visible within a viewport.
//
// If the object is already fully visible, returns the same scroll
// offset.
//
// In case the whole object cannot fit, you can specify a
// subfocus - a smaller region within the object that should
// be prioritized. If the whole object can fit, the subfocus is
// ignored.
//
// If possible, the object and subfocus are centered within the
// viewport.
//
// Example 1: the object is already visible, so nothing happens.
// +----------Viewport---------+
// +---Object---+
// +--SubFocus--+
//
// Example 2: the object is not fully visible, so it's centered
// within the viewport.
// Before:
// +----------Viewport---------+
// +---Object---+
// +--SubFocus--+
//
// After:
// +----------Viewport---------+
// +---Object---+
// +--SubFocus--+
//
// Example 3: the object is larger than the viewport, so the
// viewport moves to show as much of the object as possible,
// while also trying to center the subfocus.
// Before:
// +----------Viewport---------+
// +---------------Object--------------+
// +-SubFocus-+
//
// After:
// +----------Viewport---------+
// +---------------Object--------------+
// +-SubFocus-+
//
// When constraints cannot be fully satisfied, the min
// (left/top) position takes precedence over the max (right/bottom).
//
// Note that the return value represents the ideal new scroll offset.
// This may be out of range - the calling function should clip this
// to the available range.
static int computeBestScrollOffset(int currentScrollOffset, int subfocusMin, int subfocusMax, int objectMin, int objectMax, int viewportMin, int viewportMax)
{
int viewportSize = viewportMax - viewportMin;
// If the object size is larger than the viewport size, consider
// only a portion that's as large as the viewport, centering on
// the subfocus as much as possible.
if (objectMax - objectMin > viewportSize) {
// Since it's impossible to fit the whole object in the
// viewport, exit now if the subfocus is already within the viewport.
if (subfocusMin - currentScrollOffset >= viewportMin && subfocusMax - currentScrollOffset <= viewportMax)
return currentScrollOffset;
// Subfocus must be within focus.
subfocusMin = std::max(subfocusMin, objectMin);
subfocusMax = std::min(subfocusMax, objectMax);
// Subfocus must be no larger than the viewport size; favor top/left.
if (subfocusMax - subfocusMin > viewportSize)
subfocusMax = subfocusMin + viewportSize;
// Compute the size of an object centered on the subfocus, the size of the viewport.
int centeredObjectMin = (subfocusMin + subfocusMax - viewportSize) / 2;
int centeredObjectMax = centeredObjectMin + viewportSize;
objectMin = std::max(objectMin, centeredObjectMin);
objectMax = std::min(objectMax, centeredObjectMax);
}
// Exit now if the focus is already within the viewport.
if (objectMin - currentScrollOffset >= viewportMin
&& objectMax - currentScrollOffset <= viewportMax)
return currentScrollOffset;
// Center the object in the viewport.
return (objectMin + objectMax - viewportMin - viewportMax) / 2;
}
bool AccessibilityObject::isOnscreen() const
{
bool isOnscreen = true;
// To figure out if the element is onscreen, we start by building of a stack starting with the
// element, and then include every scrollable parent in the hierarchy.
Vector<const AccessibilityObject*> objects;
objects.append(this);
for (AccessibilityObject* parentObject = this->parentObject(); parentObject; parentObject = parentObject->parentObject()) {
if (parentObject->getScrollableAreaIfScrollable())
objects.append(parentObject);
}
// Now, go back through that chain and make sure each inner object is within the
// visible bounds of the outer object.
size_t levels = objects.size() - 1;
for (size_t i = levels; i >= 1; i--) {
const AccessibilityObject* outer = objects[i];
const AccessibilityObject* inner = objects[i - 1];
// FIXME: unclear if we need LegacyIOSDocumentVisibleRect.
const IntRect outerRect = i < levels ? snappedIntRect(outer->boundingBoxRect()) : outer->getScrollableAreaIfScrollable()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
const IntRect innerRect = snappedIntRect(inner->isAccessibilityScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect());
if (!outerRect.intersects(innerRect)) {
isOnscreen = false;
break;
}
}
return isOnscreen;
}
void AccessibilityObject::scrollToMakeVisible() const
{
IntRect objectRect = snappedIntRect(boundingBoxRect());
objectRect.setLocation(IntPoint());
scrollToMakeVisibleWithSubFocus(objectRect);
}
void AccessibilityObject::scrollToMakeVisibleWithSubFocus(const IntRect& subfocus) const
{
// Search up the parent chain until we find the first one that's scrollable.
AccessibilityObject* scrollParent = parentObject();
ScrollableArea* scrollableArea;
for (scrollableArea = nullptr;
scrollParent && !(scrollableArea = scrollParent->getScrollableAreaIfScrollable());
scrollParent = scrollParent->parentObject()) { }
if (!scrollableArea)
return;
LayoutRect objectRect = boundingBoxRect();
IntPoint scrollPosition = scrollableArea->scrollPosition();
// FIXME: unclear if we need LegacyIOSDocumentVisibleRect.
IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
if (!scrollParent->isScrollView()) {
objectRect.moveBy(scrollPosition);
objectRect.moveBy(-snappedIntRect(scrollParent->elementRect()).location());
}
int desiredX = computeBestScrollOffset(
scrollPosition.x(),
objectRect.x() + subfocus.x(), objectRect.x() + subfocus.maxX(),
objectRect.x(), objectRect.maxX(),
0, scrollVisibleRect.width());
int desiredY = computeBestScrollOffset(
scrollPosition.y(),
objectRect.y() + subfocus.y(), objectRect.y() + subfocus.maxY(),
objectRect.y(), objectRect.maxY(),
0, scrollVisibleRect.height());
scrollParent->scrollTo(IntPoint(desiredX, desiredY));
// Convert the subfocus into the coordinates of the scroll parent.
IntRect newSubfocus = subfocus;
IntRect newElementRect = snappedIntRect(elementRect());
IntRect scrollParentRect = snappedIntRect(scrollParent->elementRect());
newSubfocus.move(newElementRect.x(), newElementRect.y());
newSubfocus.move(-scrollParentRect.x(), -scrollParentRect.y());
// Recursively make sure the scroll parent itself is visible.
if (scrollParent->parentObject())
scrollParent->scrollToMakeVisibleWithSubFocus(newSubfocus);
}
void AccessibilityObject::scrollToGlobalPoint(const IntPoint& globalPoint) const
{
// Search up the parent chain and create a vector of all scrollable parent objects
// and ending with this object itself.
Vector<const AccessibilityObject*> objects;
objects.append(this);
for (AccessibilityObject* parentObject = this->parentObject(); parentObject; parentObject = parentObject->parentObject()) {
if (parentObject->getScrollableAreaIfScrollable())
objects.append(parentObject);
}
objects.reverse();
// Start with the outermost scrollable (the main window) and try to scroll the
// next innermost object to the given point.
int offsetX = 0, offsetY = 0;
IntPoint point = globalPoint;
size_t levels = objects.size() - 1;
for (size_t i = 0; i < levels; i++) {
const AccessibilityObject* outer = objects[i];
const AccessibilityObject* inner = objects[i + 1];
ScrollableArea* scrollableArea = outer->getScrollableAreaIfScrollable();
LayoutRect innerRect = inner->isAccessibilityScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect();
LayoutRect objectRect = innerRect;
IntPoint scrollPosition = scrollableArea->scrollPosition();
// Convert the object rect into local coordinates.
objectRect.move(offsetX, offsetY);
if (!outer->isAccessibilityScrollView())
objectRect.move(scrollPosition.x(), scrollPosition.y());
int desiredX = computeBestScrollOffset(
0,
objectRect.x(), objectRect.maxX(),
objectRect.x(), objectRect.maxX(),
point.x(), point.x());
int desiredY = computeBestScrollOffset(
0,
objectRect.y(), objectRect.maxY(),
objectRect.y(), objectRect.maxY(),
point.y(), point.y());
outer->scrollTo(IntPoint(desiredX, desiredY));
if (outer->isAccessibilityScrollView() && !inner->isAccessibilityScrollView()) {
// If outer object we just scrolled is a scroll view (main window or iframe) but the
// inner object is not, keep track of the coordinate transformation to apply to
// future nested calculations.
scrollPosition = scrollableArea->scrollPosition();
offsetX -= (scrollPosition.x() + point.x());
offsetY -= (scrollPosition.y() + point.y());
point.move(scrollPosition.x() - innerRect.x(),
scrollPosition.y() - innerRect.y());
} else if (inner->isAccessibilityScrollView()) {
// Otherwise, if the inner object is a scroll view, reset the coordinate transformation.
offsetX = 0;
offsetY = 0;
}
}
}
void AccessibilityObject::scrollAreaAndAncestor(std::pair<ScrollableArea*, AccessibilityObject*>& scrollers) const
{
// Search up the parent chain until we find the first one that's scrollable.
scrollers.first = nullptr;
for (scrollers.second = parentObject(); scrollers.second; scrollers.second = scrollers.second->parentObject()) {
if ((scrollers.first = scrollers.second->getScrollableAreaIfScrollable()))
break;
}
}
ScrollableArea* AccessibilityObject::scrollableAreaAncestor() const
{
std::pair<ScrollableArea*, AccessibilityObject*> scrollers;
scrollAreaAndAncestor(scrollers);
return scrollers.first;
}
IntPoint AccessibilityObject::scrollPosition() const
{
if (auto scroller = scrollableAreaAncestor())
return scroller->scrollPosition();
return IntPoint();
}
IntRect AccessibilityObject::scrollVisibleContentRect() const
{
if (auto scroller = scrollableAreaAncestor())
return scroller->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
return IntRect();
}
IntSize AccessibilityObject::scrollContentsSize() const
{
if (auto scroller = scrollableAreaAncestor())
return scroller->contentsSize();
return IntSize();
}
bool AccessibilityObject::scrollByPage(ScrollByPageDirection direction) const
{
std::pair<ScrollableArea*, AccessibilityObject*> scrollers;
scrollAreaAndAncestor(scrollers);
ScrollableArea* scrollableArea = scrollers.first;
AccessibilityObject* scrollParent = scrollers.second;
if (!scrollableArea)
return false;
IntPoint scrollPosition = scrollableArea->scrollPosition();
IntPoint newScrollPosition = scrollPosition;
IntSize scrollSize = scrollableArea->contentsSize();
IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
switch (direction) {
case Right: {
int scrollAmount = scrollVisibleRect.size().width();
int newX = scrollPosition.x() - scrollAmount;
newScrollPosition.setX(std::max(newX, 0));
break;
}
case Left: {
int scrollAmount = scrollVisibleRect.size().width();
int newX = scrollAmount + scrollPosition.x();
int maxX = scrollSize.width() - scrollAmount;
newScrollPosition.setX(std::min(newX, maxX));
break;
}
case Up: {
int scrollAmount = scrollVisibleRect.size().height();
int newY = scrollPosition.y() - scrollAmount;
newScrollPosition.setY(std::max(newY, 0));
break;
}
case Down: {
int scrollAmount = scrollVisibleRect.size().height();
int newY = scrollAmount + scrollPosition.y();
int maxY = scrollSize.height() - scrollAmount;
newScrollPosition.setY(std::min(newY, maxY));
break;
}
default:
break;
}
if (newScrollPosition != scrollPosition) {
scrollParent->scrollTo(newScrollPosition);
document()->updateLayoutIgnorePendingStylesheets();
return true;
}
return false;
}
bool AccessibilityObject::lastKnownIsIgnoredValue()
{
if (m_lastKnownIsIgnoredValue == DefaultBehavior)
m_lastKnownIsIgnoredValue = accessibilityIsIgnored() ? IgnoreObject : IncludeObject;
return m_lastKnownIsIgnoredValue == IgnoreObject;
}
void AccessibilityObject::setLastKnownIsIgnoredValue(bool isIgnored)
{
m_lastKnownIsIgnoredValue = isIgnored ? IgnoreObject : IncludeObject;
}
void AccessibilityObject::notifyIfIgnoredValueChanged()
{
bool isIgnored = accessibilityIsIgnored();
if (lastKnownIsIgnoredValue() != isIgnored) {
if (AXObjectCache* cache = axObjectCache())
cache->childrenChanged(parentObject());
setLastKnownIsIgnoredValue(isIgnored);
}
}
bool AccessibilityObject::ariaPressedIsPresent() const
{
return !getAttribute(aria_pressedAttr).isEmpty();
}
TextIteratorBehavior AccessibilityObject::textIteratorBehaviorForTextRange() const
{
TextIteratorBehavior behavior = TextIteratorIgnoresStyleVisibility;
#if PLATFORM(GTK) || PLATFORM(EFL)
// We need to emit replaced elements for GTK, and present
// them with the 'object replacement character' (0xFFFC).
behavior = static_cast<TextIteratorBehavior>(behavior | TextIteratorEmitsObjectReplacementCharacters);
#endif
return behavior;
}
AccessibilityRole AccessibilityObject::buttonRoleType() const
{
// If aria-pressed is present, then it should be exposed as a toggle button.
// http://www.w3.org/TR/wai-aria/states_and_properties#aria-pressed
if (ariaPressedIsPresent())
return ToggleButtonRole;
if (ariaHasPopup())
return PopUpButtonRole;
// We don't contemplate RadioButtonRole, as it depends on the input
// type.
return ButtonRole;
}
bool AccessibilityObject::isButton() const
{
AccessibilityRole role = roleValue();
return role == ButtonRole || role == PopUpButtonRole || role == ToggleButtonRole;
}
bool AccessibilityObject::accessibilityIsIgnoredByDefault() const
{
return defaultObjectInclusion() == IgnoreObject;
}
// ARIA component of hidden definition.
// http://www.w3.org/TR/wai-aria/terms#def_hidden
bool AccessibilityObject::isARIAHidden() const
{
for (const AccessibilityObject* object = this; object; object = object->parentObject()) {
if (equalLettersIgnoringASCIICase(object->getAttribute(aria_hiddenAttr), "true"))
return true;
}
return false;
}
// DOM component of hidden definition.
// http://www.w3.org/TR/wai-aria/terms#def_hidden
bool AccessibilityObject::isDOMHidden() const
{
RenderObject* renderer = this->renderer();
if (!renderer)
return true;
const RenderStyle& style = renderer->style();
return style.display() == NONE || style.visibility() != VISIBLE;
}
AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const
{
if (isARIAHidden())
return IgnoreObject;
if (ignoredFromARIAModalPresence())
return IgnoreObject;
if (isPresentationalChildOfAriaRole())
return IgnoreObject;
return accessibilityPlatformIncludesObject();
}
bool AccessibilityObject::accessibilityIsIgnored() const
{
AXComputedObjectAttributeCache* attributeCache = nullptr;
AXObjectCache* cache = axObjectCache();
if (cache)
attributeCache = cache->computedObjectAttributeCache();
if (attributeCache) {
AccessibilityObjectInclusion ignored = attributeCache->getIgnored(axObjectID());
switch (ignored) {
case IgnoreObject:
return true;
case IncludeObject:
return false;
case DefaultBehavior:
break;
}
}
bool result = computeAccessibilityIsIgnored();
// In case computing axIsIgnored disables attribute caching, we should refetch the object to see if it exists.
if (cache && (attributeCache = cache->computedObjectAttributeCache()))
attributeCache->setIgnored(axObjectID(), result ? IgnoreObject : IncludeObject);
return result;
}
void AccessibilityObject::elementsFromAttribute(Vector<Element*>& elements, const QualifiedName& attribute) const
{
Node* node = this->node();
if (!node || !node->isElementNode())
return;
TreeScope& treeScope = node->treeScope();
String idList = getAttribute(attribute).string();
if (idList.isEmpty())
return;
idList.replace('\n', ' ');
Vector<String> idVector;
idList.split(' ', idVector);
for (const auto& idName : idVector) {
if (Element* idElement = treeScope.getElementById(idName))
elements.append(idElement);
}
}
#if PLATFORM(COCOA)
bool AccessibilityObject::preventKeyboardDOMEventDispatch() const
{
Frame* frame = this->frame();
return frame && frame->settings().preventKeyboardDOMEventDispatch();
}
void AccessibilityObject::setPreventKeyboardDOMEventDispatch(bool on)
{
Frame* frame = this->frame();
if (!frame)
return;
frame->settings().setPreventKeyboardDOMEventDispatch(on);
}
#endif
AccessibilityObject* AccessibilityObject::focusableAncestor()
{
AccessibilityObject* potentialFocusableAncestor = this;
while (potentialFocusableAncestor) {
if (potentialFocusableAncestor->canSetFocusAttribute())
return potentialFocusableAncestor;
potentialFocusableAncestor = potentialFocusableAncestor->parentObject();
}
return nullptr;
}
AccessibilityObject* AccessibilityObject::editableAncestor()
{
AccessibilityObject* potentialEditableAncestor = this;
while (potentialEditableAncestor) {
if (potentialEditableAncestor->isTextControl())
return potentialEditableAncestor;
potentialEditableAncestor = potentialEditableAncestor->parentObject();
}
return nullptr;
}
AccessibilityObject* AccessibilityObject::highestEditableAncestor()
{
AccessibilityObject* editableAncestor = this->editableAncestor();
AccessibilityObject* previousEditableAncestor = nullptr;
while (editableAncestor) {
if (editableAncestor == previousEditableAncestor) {
if (AccessibilityObject* parent = editableAncestor->parentObject()) {
editableAncestor = parent->editableAncestor();
continue;
}
break;
}
previousEditableAncestor = editableAncestor;
editableAncestor = editableAncestor->editableAncestor();
}
return previousEditableAncestor;
}
bool AccessibilityObject::isStyleFormatGroup() const
{
Node* node = this->node();
if (!node)
return false;
return node->hasTagName(kbdTag) || node->hasTagName(codeTag)
|| node->hasTagName(preTag) || node->hasTagName(sampTag)
|| node->hasTagName(varTag) || node->hasTagName(citeTag)
|| node->hasTagName(insTag) || node->hasTagName(delTag)
|| node->hasTagName(supTag) || node->hasTagName(subTag);
}
bool AccessibilityObject::isSubscriptStyleGroup() const
{
Node* node = this->node();
return node && node->hasTagName(subTag);
}
bool AccessibilityObject::isSuperscriptStyleGroup() const
{
Node* node = this->node();
return node && node->hasTagName(supTag);
}
bool AccessibilityObject::isContainedByPasswordField() const
{
Node* node = this->node();
if (!node)
return false;
if (ariaRoleAttribute() != UnknownRole)
return false;
Element* element = node->shadowHost();
return is<HTMLInputElement>(element) && downcast<HTMLInputElement>(*element).isPasswordField();
}
void AccessibilityObject::ariaElementsFromAttribute(AccessibilityChildrenVector& children, const QualifiedName& attributeName) const
{
Vector<Element*> elements;
elementsFromAttribute(elements, attributeName);
AXObjectCache* cache = axObjectCache();
for (const auto& element : elements) {
if (AccessibilityObject* axObject = cache->getOrCreate(element))
children.append(axObject);
}
}
void AccessibilityObject::ariaControlsElements(AccessibilityChildrenVector& ariaControls) const
{
ariaElementsFromAttribute(ariaControls, aria_controlsAttr);
}
void AccessibilityObject::ariaDescribedByElements(AccessibilityChildrenVector& ariaDescribedBy) const
{
ariaElementsFromAttribute(ariaDescribedBy, aria_describedbyAttr);
}
void AccessibilityObject::ariaFlowToElements(AccessibilityChildrenVector& flowTo) const
{
ariaElementsFromAttribute(flowTo, aria_flowtoAttr);
}
void AccessibilityObject::ariaLabelledByElements(AccessibilityChildrenVector& ariaLabelledBy) const
{
ariaElementsFromAttribute(ariaLabelledBy, aria_labelledbyAttr);
if (!ariaLabelledBy.size())
ariaElementsFromAttribute(ariaLabelledBy, aria_labeledbyAttr);
}
void AccessibilityObject::ariaOwnsElements(AccessibilityChildrenVector& axObjects) const
{
ariaElementsFromAttribute(axObjects, aria_ownsAttr);
}
} // namespace WebCore