summaryrefslogtreecommitdiff
path: root/Source/WebCore/accessibility/AccessibilityObject.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Source/WebCore/accessibility/AccessibilityObject.cpp')
-rw-r--r--Source/WebCore/accessibility/AccessibilityObject.cpp1530
1 files changed, 1278 insertions, 252 deletions
diff --git a/Source/WebCore/accessibility/AccessibilityObject.cpp b/Source/WebCore/accessibility/AccessibilityObject.cpp
index 2294167e3..0f6319e37 100644
--- a/Source/WebCore/accessibility/AccessibilityObject.cpp
+++ b/Source/WebCore/accessibility/AccessibilityObject.cpp
@@ -10,7 +10,7 @@
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
- * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * 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.
*
@@ -31,26 +31,34 @@
#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 "NotImplemented.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"
@@ -63,6 +71,7 @@
#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>
@@ -77,8 +86,8 @@ AccessibilityObject::AccessibilityObject()
, m_haveChildren(false)
, m_role(UnknownRole)
, m_lastKnownIsIgnoredValue(DefaultBehavior)
-#if PLATFORM(GTK) || (PLATFORM(EFL) && HAVE(ACCESSIBILITY))
- , m_wrapper(0)
+#if PLATFORM(GTK)
+ , m_wrapper(nullptr)
#endif
{
}
@@ -99,7 +108,7 @@ void AccessibilityObject::detach(AccessibilityDetachmentType detachmentType, AXO
clearChildren();
#if HAVE(ACCESSIBILITY)
- setWrapper(0);
+ setWrapper(nullptr);
#endif
}
@@ -204,6 +213,9 @@ bool AccessibilityObject::isAccessibilityObjectSearchMatchAtIndex(AccessibilityO
case MisspelledWordSearchKey:
return axObject->hasMisspelling();
+ case OutlineSearchKey:
+ return axObject->isTree();
+
case PlainTextSearchKey:
return axObject->hasPlainText();
@@ -223,11 +235,11 @@ bool AccessibilityObject::isAccessibilityObjectSearchMatchAtIndex(AccessibilityO
case TableSameLevelSearchKey:
return criteria->startObject
- && axObject->isAccessibilityTable()
- && axObject->tableLevel() == criteria->startObject->tableLevel();
+ && is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility()
+ && downcast<AccessibilityTable>(*axObject).tableLevel() == criteria->startObject->tableLevel();
case TableSearchKey:
- return axObject->isAccessibilityTable();
+ return is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility();
case TextFieldSearchKey:
return axObject->isTextControl();
@@ -280,17 +292,95 @@ bool AccessibilityObject::accessibilityObjectContainsText(String* text) const
|| 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 node() && node()->hasTagName(blockquoteTag);
+ return roleValue() == BlockquoteRole;
}
bool AccessibilityObject::isTextControl() const
{
switch (roleValue()) {
+ case ComboBoxRole:
+ case SearchFieldRole:
case TextAreaRole:
case TextFieldRole:
- case ComboBoxRole:
return true;
default:
return false;
@@ -299,19 +389,24 @@ bool AccessibilityObject::isTextControl() const
bool AccessibilityObject::isARIATextControl() const
{
- return ariaRoleAttribute() == TextAreaRole || ariaRoleAttribute() == TextFieldRole;
+ return ariaRoleAttribute() == TextAreaRole || ariaRoleAttribute() == TextFieldRole || ariaRoleAttribute() == SearchFieldRole;
+}
+
+bool AccessibilityObject::isNonNativeTextControl() const
+{
+ return (isARIATextControl() || hasContentEditableAttributeSet()) && !isNativeTextControl();
}
bool AccessibilityObject::isLandmark() const
{
AccessibilityRole role = roleValue();
- return role == LandmarkApplicationRole
- || role == LandmarkBannerRole
+ return role == LandmarkBannerRole
|| role == LandmarkComplementaryRole
|| role == LandmarkContentInfoRole
|| role == LandmarkMainRole
|| role == LandmarkNavigationRole
+ || role == LandmarkRegionRole
|| role == LandmarkSearchRole;
}
@@ -330,13 +425,11 @@ bool AccessibilityObject::hasMisspelling() const
if (!textChecker)
return false;
- const UChar* chars = stringValue().deprecatedCharacters();
- int charsLength = stringValue().length();
bool isMisspelled = false;
if (unifiedTextCheckerEnabled(frame)) {
Vector<TextCheckingResult> results;
- checkTextOfParagraph(textChecker, chars, charsLength, TextCheckingTypeSpelling, results);
+ checkTextOfParagraph(*textChecker, stringValue(), TextCheckingTypeSpelling, results, frame->selection().selection());
if (!results.isEmpty())
isMisspelled = true;
return isMisspelled;
@@ -344,16 +437,16 @@ bool AccessibilityObject::hasMisspelling() const
int misspellingLength = 0;
int misspellingLocation = -1;
- textChecker->checkSpellingOfString(chars, charsLength, &misspellingLocation, &misspellingLength);
+ textChecker->checkSpellingOfString(stringValue(), &misspellingLocation, &misspellingLength);
if (misspellingLength || misspellingLocation != -1)
isMisspelled = true;
return isMisspelled;
}
-int AccessibilityObject::blockquoteLevel() const
+unsigned AccessibilityObject::blockquoteLevel() const
{
- int level = 0;
+ unsigned level = 0;
for (Node* elementNode = node(); elementNode; elementNode = elementNode->parentNode()) {
if (elementNode->hasTagName(blockquoteTag))
++level;
@@ -364,29 +457,53 @@ int AccessibilityObject::blockquoteLevel() const
AccessibilityObject* AccessibilityObject::parentObjectUnignored() const
{
- AccessibilityObject* parent;
- for (parent = parentObject(); parent && parent->accessibilityIsIgnored(); parent = parent->parentObject()) {
+ return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) {
+ return !object.accessibilityIsIgnored();
+ }));
+}
+
+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 parent;
+ 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 0;
+ 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);
+ node = NodeTraversal::next(*node);
while (node && !node->renderer())
- node = NodeTraversal::nextSkippingChildren(node);
+ node = NodeTraversal::nextSkippingChildren(*node);
if (!node)
- return 0;
+ return nullptr;
accessibleObject = cache->getOrCreate(node->renderer());
}
@@ -394,19 +511,26 @@ AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const No
return accessibleObject;
}
+bool AccessibilityObject::isDescendantOfRole(AccessibilityRole role) const
+{
+ return AccessibilityObject::matchedParent(*this, false, [&role] (const AccessibilityObject& object) {
+ return object.roleValue() == role;
+ }) != nullptr;
+}
+
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 (!widget || !widget->isFrameView())
+ if (!is<FrameView>(widget))
return;
- Document* doc = toFrameView(widget)->frame().document();
- if (!doc || !doc->hasLivingRenderTree())
+ Document* document = downcast<FrameView>(*widget).frame().document();
+ if (!document || !document->hasLivingRenderTree())
return;
- object = object->axObjectCache()->getOrCreate(doc);
+ object = object->axObjectCache()->getOrCreate(document);
}
if (object)
@@ -417,7 +541,7 @@ static void appendChildrenToArray(AccessibilityObject* object, bool isForward, A
{
// 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 = object->isAccessibilityTable() ? toAccessibilityTable(object)->rows() : object->children();
+ const auto& searchChildren = is<AccessibilityTable>(*object) && downcast<AccessibilityTable>(*object).isExposableThroughAccessibility() ? downcast<AccessibilityTable>(*object).rows() : object->children();
size_t childrenSize = searchChildren.size();
@@ -463,7 +587,8 @@ void AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria* crite
if (!criteria)
return;
- axObjectCache()->startCachingComputedObjectAttributesUntilTreeMutates();
+ 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.
@@ -475,21 +600,23 @@ void AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria* crite
bool isForward = criteria->searchDirection == SearchDirectionNext;
- // In the first iteration of the loop, it will examine the children of the start object for matches.
- // However, when going backwards, those children should not be considered, so the loop is skipped ahead.
- AccessibilityObject* previousObject = 0;
- if (!isForward) {
+ // 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()) {
+ for (AccessibilityObject* stopSearchElement = parentObjectUnignored(); startObject && 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;
- appendChildrenToArray(startObject, isForward, previousObject, 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()) {
@@ -499,20 +626,194 @@ void AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria* crite
if (objectMatchesSearchCriteriaWithResultLimit(searchObject, criteria, results))
break;
- appendChildrenToArray(searchObject, isForward, 0, searchStack);
+ 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 && objectMatchesSearchCriteriaWithResultLimit(startObject, criteria, results))
+ 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 afterRange;
+ if (!afterRange && beforeRange)
+ return 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).releaseReturnValue();
+ 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.
@@ -532,7 +833,7 @@ bool AccessibilityObject::hasAttributesRequiredForInclusion() const
bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole)
{
- return ariaRole == RadioButtonRole || ariaRole == CheckBoxRole || ariaRole == TextFieldRole;
+ return ariaRole == RadioButtonRole || ariaRole == CheckBoxRole || ariaRole == TextFieldRole || ariaRole == SwitchRole || ariaRole == SearchFieldRole;
}
bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole)
@@ -576,34 +877,89 @@ IntRect AccessibilityObject::boundingBoxForQuads(RenderObject* obj, const Vector
if (!obj)
return IntRect();
- size_t count = quads.size();
- if (!count)
- return IntRect();
-
- IntRect result;
- for (size_t i = 0; i < count; ++i) {
- IntRect r = quads[i].enclosingBoundingBox();
+ FloatRect result;
+ for (const auto& quad : quads) {
+ FloatRect r = quad.enclosingBoundingBox();
if (!r.isEmpty()) {
if (obj->style().hasAppearance())
- obj->theme().adjustRepaintRect(obj, r);
+ obj->theme().adjustRepaintRect(*obj, r);
result.unite(r);
}
}
- return result;
+ return snappedIntRect(LayoutRect(result));
}
-bool AccessibilityObject::press() const
+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();
- UserGestureIndicator gestureIndicator(DefinitelyProcessingUserGesture);
- actionElem->accessKeyAction(true);
+ // 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;
+
+ ASSERT(pressElement);
+ // 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
{
@@ -794,7 +1150,7 @@ static VisiblePosition startOfStyleRange(const VisiblePosition& visiblePos)
{
RenderObject* renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer();
RenderObject* startRenderer = renderer;
- RenderStyle* style = &renderer->style();
+ auto* style = &renderer->style();
// traverse backward by renderer to look for style change
for (RenderObject* r = renderer->previousInPreOrder(); r; r = r->previousInPreOrder()) {
@@ -857,7 +1213,28 @@ VisiblePositionRange AccessibilityObject::visiblePositionRangeForRange(const Pla
return VisiblePositionRange(startPosition, endPosition);
}
-static bool replacedNodeNeedsCharacter(Node* replacedNode)
+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
@@ -877,30 +1254,73 @@ static RenderListItem* renderListItemContainerForNode(Node* node)
{
for (; node; node = node->parentNode()) {
RenderBoxModelObject* renderer = node->renderBoxModelObject();
- if (renderer && renderer->isListItem())
- return toRenderListItem(renderer);
+ if (is<RenderListItem>(renderer))
+ return downcast<RenderListItem>(renderer);
}
- return 0;
+ 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) const
+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 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();
+ 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();
-String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
+ 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();
@@ -909,21 +1329,16 @@ String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionR
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.length()) {
- // Add a textual representation for list marker text
- String listMarkerText = listMarkerTextForNodeAndPosition(it.node(), visiblePositionRange.start);
- if (!listMarkerText.isEmpty())
- builder.append(listMarkerText);
-
+ 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
- int exception = 0;
- Node* node = it.range()->startContainer(exception);
- ASSERT(node == it.range()->endContainer(exception));
- int offset = it.range()->startOffset(exception);
-
- if (replacedNodeNeedsCharacter(node->childNode(offset)))
+ Node& node = it.range()->startContainer();
+ ASSERT(&node == &it.range()->endContainer());
+ int offset = it.range()->startOffset();
+ if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
builder.append(objectReplacementCharacter);
}
}
@@ -941,17 +1356,16 @@ int AccessibilityObject::lengthForVisiblePositionRange(const VisiblePositionRang
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.length())
- length += it.length();
+ if (it.text().length())
+ length += it.text().length();
else {
// locate the node and starting offset for this replaced range
- int exception = 0;
- Node* node = it.range()->startContainer(exception);
- ASSERT(node == it.range()->endContainer(exception));
- int offset = it.range()->startOffset(exception);
+ Node& node = it.range()->startContainer();
+ ASSERT(&node == &it.range()->endContainer());
+ int offset = it.range()->startOffset();
- if (replacedNodeNeedsCharacter(node->childNode(offset)))
- length++;
+ if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset)))
+ ++length;
}
}
@@ -1141,15 +1555,66 @@ VisiblePosition AccessibilityObject::previousParagraphStartPosition(const Visibl
AccessibilityObject* AccessibilityObject::accessibilityObjectForPosition(const VisiblePosition& visiblePos) const
{
if (visiblePos.isNull())
- return 0;
+ return nullptr;
RenderObject* obj = visiblePos.deepEquivalent().deprecatedNode()->renderer();
if (!obj)
- return 0;
+ 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
{
@@ -1224,18 +1689,32 @@ unsigned AccessibilityObject::doAXLineForIndex(unsigned index)
void AccessibilityObject::updateBackingStore()
{
// Updating the layout may delete this object.
+ RefPtr<AccessibilityObject> protectedThis(this);
+
if (Document* document = this->document()) {
- if (!document->view()->isInLayout())
+ if (!document->view()->isInRenderTreeLayout())
document->updateLayoutIgnorePendingStylesheets();
}
+
+ updateChildrenIfNecessary();
}
#endif
-
+
+ScrollView* AccessibilityObject::scrollViewAncestor() const
+{
+ if (const AccessibilityObject* scrollParent = AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) {
+ return is<AccessibilityScrollView>(object);
+ }))
+ return downcast<AccessibilityScrollView>(*scrollParent).scrollView();
+
+ return nullptr;
+}
+
Document* AccessibilityObject::document() const
{
FrameView* frameView = documentFrameView();
if (!frameView)
- return 0;
+ return nullptr;
return frameView->frame().document();
}
@@ -1244,7 +1723,7 @@ Page* AccessibilityObject::page() const
{
Document* document = this->document();
if (!document)
- return 0;
+ return nullptr;
return document->page();
}
@@ -1255,7 +1734,7 @@ FrameView* AccessibilityObject::documentFrameView() const
object = object->parentObject();
if (!object)
- return 0;
+ return nullptr;
return object->documentFrameView();
}
@@ -1272,8 +1751,11 @@ const AccessibilityObject::AccessibilityChildrenVector& AccessibilityObject::chi
void AccessibilityObject::updateChildrenIfNecessary()
{
- if (!hasChildren())
- addChildren();
+ 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()
@@ -1290,16 +1772,16 @@ AccessibilityObject* AccessibilityObject::anchorElementForNode(Node* node)
{
RenderObject* obj = node->renderer();
if (!obj)
- return 0;
+ return nullptr;
RefPtr<AccessibilityObject> axObj = obj->document().axObjectCache()->getOrCreate(obj);
Element* anchor = axObj->anchorElement();
if (!anchor)
- return 0;
+ return nullptr;
RenderObject* anchorRenderer = anchor->renderer();
if (!anchorRenderer)
- return 0;
+ return nullptr;
return anchorRenderer->document().axObjectCache()->getOrCreate(anchorRenderer);
}
@@ -1307,16 +1789,27 @@ AccessibilityObject* AccessibilityObject::anchorElementForNode(Node* node)
AccessibilityObject* AccessibilityObject::headingElementForNode(Node* node)
{
if (!node)
- return 0;
+ return nullptr;
RenderObject* renderObject = node->renderer();
if (!renderObject)
- return 0;
+ return nullptr;
AccessibilityObject* axObject = renderObject->document().axObjectCache()->getOrCreate(renderObject);
- for (; axObject && axObject->roleValue() != HeadingRole; axObject = axObject->parentObject()) { }
- return axObject;
+ return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*axObject, true, [] (const AccessibilityObject& object) {
+ return object.roleValue() == HeadingRole;
+ }));
+}
+
+const AccessibilityObject* AccessibilityObject::matchedParent(const AccessibilityObject& object, bool includeSelf, const std::function<bool(const AccessibilityObject&)>& matches)
+{
+ const AccessibilityObject* parent = includeSelf ? &object : object.parentObject();
+ for (; parent; parent = parent->parentObject()) {
+ if (matches(*parent))
+ return parent;
+ }
+ return nullptr;
}
void AccessibilityObject::ariaTreeRows(AccessibilityChildrenVector& result)
@@ -1354,21 +1847,38 @@ void AccessibilityObject::ariaTreeItemDisclosedRows(AccessibilityChildrenVector&
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.
- DEFINE_STATIC_LOCAL(const String, buttonAction, (AXButtonActionVerb()));
- DEFINE_STATIC_LOCAL(const String, textFieldAction, (AXTextFieldActionVerb()));
- DEFINE_STATIC_LOCAL(const String, radioButtonAction, (AXRadioButtonActionVerb()));
- DEFINE_STATIC_LOCAL(const String, checkedCheckBoxAction, (AXCheckedCheckBoxActionVerb()));
- DEFINE_STATIC_LOCAL(const String, uncheckedCheckBoxAction, (AXUncheckedCheckBoxActionVerb()));
- DEFINE_STATIC_LOCAL(const String, linkAction, (AXLinkActionVerb()));
- DEFINE_STATIC_LOCAL(const String, menuListAction, (AXMenuListActionVerb()));
- DEFINE_STATIC_LOCAL(const String, menuListPopupAction, (AXMenuListPopupActionVerb()));
- DEFINE_STATIC_LOCAL(const String, listItemAction, (AXListItemActionVerb()));
+ 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:
@@ -1380,6 +1890,7 @@ const String& AccessibilityObject::actionVerb() const
case RadioButtonRole:
return radioButtonAction;
case CheckBoxRole:
+ case SwitchRole:
return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction;
case LinkRole:
case WebCoreLinkRole:
@@ -1401,52 +1912,127 @@ const String& AccessibilityObject::actionVerb() const
bool AccessibilityObject::ariaIsMultiline() const
{
- return equalIgnoringCase(getAttribute(aria_multilineAttr), "true");
+ return equalLettersIgnoringASCIICase(getAttribute(aria_multilineAttr), "true");
}
-const AtomicString& AccessibilityObject::invalidStatus() const
+String AccessibilityObject::invalidStatus() const
{
- DEFINE_STATIC_LOCAL(const AtomicString, invalidStatusFalse, ("false", AtomicString::ConstructFromLiteral));
- DEFINE_STATIC_LOCAL(const AtomicString, invalidStatusTrue, ("true", AtomicString::ConstructFromLiteral));
-
- // aria-invalid can return false (default), grammer, spelling, or true.
- const AtomicString& ariaInvalid = getAttribute(aria_invalidAttr);
+ 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', empty or not present, it should return false.
- if (ariaInvalid.isEmpty() || equalIgnoringCase(ariaInvalid, invalidStatusFalse))
- return invalidStatusFalse;
+ if (ariaInvalid.isEmpty()) {
+ // We should expose invalid status for input types.
+ Node* node = this->node();
+ if (node && is<HTMLInputElement>(*node)) {
+ HTMLInputElement& input = downcast<HTMLInputElement>(*node);
+ if (input.hasBadInput() || input.typeMismatch())
+ return trueValue;
+ }
+ return falseValue;
+ }
- // Only 'true', 'grammar' and 'spelling' are values recognised by the WAI-ARIA
- // specification. Any other non empty string should be treated as 'true'.
- if (equalIgnoringCase(ariaInvalid, "spelling") || equalIgnoringCase(ariaInvalid, "grammar"))
- return ariaInvalid;
- return invalidStatusTrue;
+ // If "false", "undefined" [sic, string value], empty, or missing, return "false".
+ if (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;
}
-bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
+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
{
- Node* elementNode = node();
- if (!elementNode)
+ // 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;
- if (!elementNode->isElementNode())
+ // We only want to ignore the objects within the same frame as the modal dialog.
+ if (ariaModalNode->document().frame() != this->frame())
return false;
- Element* element = toElement(elementNode);
- return element->fastHasAttribute(attribute);
+ return !isAriaModalDescendant(ariaModalNode);
+}
+
+bool AccessibilityObject::hasTagName(const QualifiedName& tagName) const
+{
+ Node* node = this->node();
+ return is<Element>(node) && downcast<Element>(*node).hasTagName(tagName);
}
-const AtomicString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const
+bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
{
- Node* elementNode = node();
- if (!elementNode)
- return nullAtom;
+ Node* node = this->node();
+ if (!is<Element>(node))
+ return false;
- if (!elementNode->isElementNode())
- return nullAtom;
+ return downcast<Element>(*node).hasAttributeWithoutSynchronization(attribute);
+}
- Element* element = toElement(elementNode);
- return element->fastGetAttribute(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;
@@ -1458,20 +2044,17 @@ AccessibilityOrientation AccessibilityObject::orientation() const
if (bounds.size().height() > bounds.size().width())
return AccessibilityOrientationVertical;
- // A tie goes to horizontal.
- return AccessibilityOrientationHorizontal;
+ 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;
+
+ return AccessibilityObject::matchedParent(*this, false, [axObject] (const AccessibilityObject& object) {
+ return &object == axObject;
+ }) != nullptr;
}
bool AccessibilityObject::isAncestorOfObject(const AccessibilityObject* axObject) const
@@ -1488,22 +2071,30 @@ AccessibilityObject* AccessibilityObject::firstAnonymousBlockChild() const
if (child->renderer() && child->renderer()->isAnonymousBlock())
return child;
}
- return 0;
+ return nullptr;
}
-typedef HashMap<String, AccessibilityRole, CaseFoldingHash> ARIARoleMap;
+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 ARIARoleMap* createARIARoleMap()
+static void initializeRoleMap()
{
+ if (gAriaRoleMap)
+ return;
+ ASSERT(!gAriaReverseRoleMap);
+
const RoleEntry roles[] = {
{ "alert", ApplicationAlertRole },
{ "alertdialog", ApplicationAlertDialogRole },
- { "application", LandmarkApplicationRole },
+ { "application", WebApplicationRole },
{ "article", DocumentArticleRole },
{ "banner", LandmarkBannerRole },
{ "button", ButtonRole },
@@ -1512,12 +2103,15 @@ static ARIARoleMap* createARIARoleMap()
{ "contentinfo", LandmarkContentInfoRole },
{ "dialog", ApplicationDialogRole },
{ "directory", DirectoryRole },
- { "grid", TableRole },
- { "gridcell", CellRole },
+ { "grid", GridRole },
+ { "gridcell", GridCellRole },
+ { "table", TableRole },
+ { "cell", CellRole },
{ "columnheader", ColumnHeaderRole },
{ "combobox", ComboBoxRole },
{ "definition", DefinitionRole },
{ "document", DocumentRole },
+ { "form", FormRole },
{ "rowheader", RowHeaderRole },
{ "group", GroupRole },
{ "heading", HeadingRole },
@@ -1536,6 +2130,7 @@ static ARIARoleMap* createARIARoleMap()
{ "menuitem", MenuItemRole },
{ "menuitemcheckbox", MenuItemCheckboxRole },
{ "menuitemradio", MenuItemRadioRole },
+ { "none", PresentationalRole },
{ "note", DocumentNoteRole },
{ "navigation", LandmarkNavigationRole },
{ "option", ListBoxOptionRole },
@@ -1543,14 +2138,17 @@ static ARIARoleMap* createARIARoleMap()
{ "progressbar", ProgressIndicatorRole },
{ "radio", RadioButtonRole },
{ "radiogroup", RadioGroupRole },
- { "region", DocumentRegionRole },
+ { "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 },
@@ -1563,26 +2161,37 @@ static ARIARoleMap* createARIARoleMap()
{ "treegrid", TreeGridRole },
{ "treeitem", TreeItemRole }
};
- ARIARoleMap* roleMap = new ARIARoleMap;
- for (size_t i = 0; i < WTF_ARRAY_LENGTH(roles); ++i)
- roleMap->set(roles[i].ariaRole, roles[i].webcoreRole);
- return roleMap;
+ 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());
- static const ARIARoleMap* roleMap = createARIARoleMap();
-
Vector<String> roleVector;
value.split(' ', roleVector);
AccessibilityRole role = UnknownRole;
- unsigned size = roleVector.size();
- for (unsigned i = 0; i < size; ++i) {
- String roleName = roleVector[i];
- role = roleMap->get(roleName);
+ for (const auto& roleName : roleVector) {
+ role = ariaRoleMap().get(roleName);
if (role)
return role;
}
@@ -1590,6 +2199,18 @@ AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value
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;
+ if (role == PopUpButtonRole || role == ToggleButtonRole)
+ role = ButtonRole;
+
+ return reverseAriaRoleMap().get(role);
+}
+
bool AccessibilityObject::hasHighlighting() const
{
for (Node* node = this->node(); node; node = node->parentNode()) {
@@ -1600,12 +2221,89 @@ bool AccessibilityObject::hasHighlighting() const
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 (node && node->isElementNode())
- return toElement(node);
- return 0;
+ 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
@@ -1614,6 +2312,10 @@ const AtomicString& AccessibilityObject::placeholderValue() const
if (!placeholder.isEmpty())
return placeholder;
+ const AtomicString& ariaPlaceholder = getAttribute(aria_placeholderAttr);
+ if (!ariaPlaceholder.isEmpty())
+ return ariaPlaceholder;
+
return nullAtom;
}
@@ -1622,12 +2324,9 @@ bool AccessibilityObject::isInsideARIALiveRegion() const
if (supportsARIALiveRegion())
return true;
- for (AccessibilityObject* axParent = parentObject(); axParent; axParent = axParent->parentObject()) {
- if (axParent->supportsARIALiveRegion())
- return true;
- }
-
- return false;
+ return AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) {
+ return object.supportsARIALiveRegion();
+ }) != nullptr;
}
bool AccessibilityObject::supportsARIAAttributes() const
@@ -1636,13 +2335,14 @@ bool AccessibilityObject::supportsARIAAttributes() const
return supportsARIALiveRegion()
|| supportsARIADragging()
|| supportsARIADropping()
- || supportsARIAFlowTo()
|| 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)
@@ -1650,10 +2350,14 @@ bool AccessibilityObject::supportsARIAAttributes() const
|| hasAttribute(aria_relevantAttr);
}
+bool AccessibilityObject::liveRegionStatusIsEnabled(const AtomicString& liveRegionStatus)
+{
+ return equalLettersIgnoringASCIICase(liveRegionStatus, "polite") || equalLettersIgnoringASCIICase(liveRegionStatus, "assertive");
+}
+
bool AccessibilityObject::supportsARIALiveRegion() const
{
- const AtomicString& liveRegion = ariaLiveRegionStatus();
- return equalIgnoringCase(liveRegion, "polite") || equalIgnoringCase(liveRegion, "assertive");
+ return liveRegionStatusIsEnabled(ariaLiveRegionStatus());
}
AccessibilityObject* AccessibilityObject::elementAccessibilityHitTest(const IntPoint& point) const
@@ -1662,8 +2366,10 @@ AccessibilityObject* AccessibilityObject::elementAccessibilityHitTest(const IntP
if (isAttachment()) {
Widget* widget = widgetForAttachmentView();
// Normalize the point for the widget's bounds.
- if (widget && widget->isFrameView())
- return axObjectCache()->getOrCreate(widget)->accessibilityHitTest(IntPoint(point - widget->frameRect().location()));
+ 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.
@@ -1672,7 +2378,7 @@ AccessibilityObject* AccessibilityObject::elementAccessibilityHitTest(const IntP
return child->elementAccessibilityHitTest(point);
}
- return const_cast<AccessibilityObject*>(this);
+ return const_cast<AccessibilityObject*>(this);
}
AXObjectCache* AccessibilityObject::axObjectCache() const
@@ -1680,18 +2386,18 @@ AXObjectCache* AccessibilityObject::axObjectCache() const
Document* doc = document();
if (doc)
return doc->axObjectCache();
- return 0;
+ return nullptr;
}
AccessibilityObject* AccessibilityObject::focusedUIElement() const
{
Document* doc = document();
if (!doc)
- return 0;
+ return nullptr;
Page* page = doc->page();
if (!page)
- return 0;
+ return nullptr;
return AXObjectCache::focusedUIElementForPage(page);
}
@@ -1699,11 +2405,11 @@ AccessibilityObject* AccessibilityObject::focusedUIElement() const
AccessibilitySortDirection AccessibilityObject::sortDirection() const
{
const AtomicString& sortAttribute = getAttribute(aria_sortAttr);
- if (equalIgnoringCase(sortAttribute, "ascending"))
+ if (equalLettersIgnoringASCIICase(sortAttribute, "ascending"))
return SortDirectionAscending;
- if (equalIgnoringCase(sortAttribute, "descending"))
+ if (equalLettersIgnoringASCIICase(sortAttribute, "descending"))
return SortDirectionDescending;
- if (equalIgnoringCase(sortAttribute, "other"))
+ if (equalLettersIgnoringASCIICase(sortAttribute, "other"))
return SortDirectionOther;
return SortDirectionNone;
@@ -1714,7 +2420,8 @@ bool AccessibilityObject::supportsRangeValue() const
return isProgressIndicator()
|| isSlider()
|| isScrollbar()
- || isSpinButton();
+ || isSpinButton()
+ || isAttachmentElement();
}
bool AccessibilityObject::supportsARIASetSize() const
@@ -1745,50 +2452,100 @@ String AccessibilityObject::identifierAttribute() const
void AccessibilityObject::classList(Vector<String>& classList) const
{
Node* node = this->node();
- if (!node || !node->isElementNode())
+ if (!is<Element>(node))
return;
- Element* element = toElement(node);
- DOMTokenList* list = element->classList();
- if (!list)
- return;
- unsigned length = list->length();
+ 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());
+ 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::supportsARIAExpanded() const
+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);
- return equalIgnoringCase(expanded, "true") || equalIgnoringCase(expanded, "false");
+ 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 (equalIgnoringCase(getAttribute(aria_expandedAttr), "true"))
+ if (equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"))
return true;
+ if (is<HTMLDetailsElement>(node()))
+ return downcast<HTMLDetailsElement>(node())->isOpen();
+
+ // Summary element should use its details parent's expanded status.
+ if (isSummary()) {
+ if (const AccessibilityObject* parent = AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) {
+ return object.roleValue() == DetailsRole;
+ }))
+ return parent->isExpanded();
+ }
+
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 or radio, the aria-checked attribute should be used.
-
+ // 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 (equalIgnoringCase(result, "true"))
+ if (equalLettersIgnoringASCIICase(result, "true"))
return ButtonStateOn;
- if (equalIgnoringCase(result, "mixed")) {
- // ARIA says that radio and menuitemradio elements must NOT expose button state mixed.
+ 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)
+ if (ariaRole == RadioButtonRole || ariaRole == MenuItemRadioRole || ariaRole == SwitchRole)
return ButtonStateOff;
return ButtonStateMixed;
}
+ if (isIndeterminate())
+ return ButtonStateMixed;
+
return ButtonStateOff;
}
@@ -1797,71 +2554,89 @@ AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const
// 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.
//
-// Example: the viewport is scrolled to the right just enough
-// that the object is in view.
+// 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---------+
+// +----------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) {
+static int computeBestScrollOffset(int currentScrollOffset, int subfocusMin, int subfocusMax, int objectMin, int objectMax, int viewportMin, int viewportMax)
+{
int viewportSize = viewportMax - viewportMin;
-
- // If the focus size is larger than the viewport size, shrink it in the
- // direction of subfocus.
+
+ // 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) {
- // Subfocus must be within focus:
+ // 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;
- if (subfocusMin + viewportSize > objectMax)
- objectMin = objectMax - viewportSize;
- else {
- objectMin = subfocusMin;
- objectMax = subfocusMin + 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;
-
- // Scroll left if we're too far to the right.
- if (objectMax - currentScrollOffset > viewportMax)
- return objectMax - viewportMax;
-
- // Scroll right if we're too far to the left.
- if (objectMin - currentScrollOffset < viewportMin)
- return objectMin - viewportMin;
-
- ASSERT_NOT_REACHED();
-
- // This shouldn't happen.
- return currentScrollOffset;
+
+ // Center the object in the viewport.
+ return (objectMin + objectMax - viewportMin - viewportMax) / 2;
}
bool AccessibilityObject::isOnscreen() const
@@ -1886,8 +2661,8 @@ bool AccessibilityObject::isOnscreen() const
const AccessibilityObject* outer = objects[i];
const AccessibilityObject* inner = objects[i - 1];
// FIXME: unclear if we need LegacyIOSDocumentVisibleRect.
- const IntRect outerRect = i < levels ? pixelSnappedIntRect(outer->boundingBoxRect()) : outer->getScrollableAreaIfScrollable()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
- const IntRect innerRect = pixelSnappedIntRect(inner->isAccessibilityScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect());
+ 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;
@@ -1900,7 +2675,7 @@ bool AccessibilityObject::isOnscreen() const
void AccessibilityObject::scrollToMakeVisible() const
{
- IntRect objectRect = pixelSnappedIntRect(boundingBoxRect());
+ IntRect objectRect = snappedIntRect(boundingBoxRect());
objectRect.setLocation(IntPoint());
scrollToMakeVisibleWithSubFocus(objectRect);
}
@@ -1910,7 +2685,7 @@ void AccessibilityObject::scrollToMakeVisibleWithSubFocus(const IntRect& subfocu
// Search up the parent chain until we find the first one that's scrollable.
AccessibilityObject* scrollParent = parentObject();
ScrollableArea* scrollableArea;
- for (scrollableArea = 0;
+ for (scrollableArea = nullptr;
scrollParent && !(scrollableArea = scrollParent->getScrollableAreaIfScrollable());
scrollParent = scrollParent->parentObject()) { }
if (!scrollableArea)
@@ -1921,6 +2696,11 @@ void AccessibilityObject::scrollToMakeVisibleWithSubFocus(const IntRect& subfocu
// 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(),
@@ -1934,9 +2714,16 @@ void AccessibilityObject::scrollToMakeVisibleWithSubFocus(const IntRect& subfocu
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->scrollToMakeVisible();
+ scrollParent->scrollToMakeVisibleWithSubFocus(newSubfocus);
}
void AccessibilityObject::scrollToGlobalPoint(const IntPoint& globalPoint) const
@@ -2001,6 +2788,102 @@ void AccessibilityObject::scrollToGlobalPoint(const IntPoint& globalPoint) const
}
}
}
+
+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()
{
@@ -2019,7 +2902,8 @@ void AccessibilityObject::notifyIfIgnoredValueChanged()
{
bool isIgnored = accessibilityIsIgnored();
if (lastKnownIsIgnoredValue() != isIgnored) {
- axObjectCache()->childrenChanged(parentObject());
+ if (AXObjectCache* cache = axObjectCache())
+ cache->childrenChanged(parentObject());
setLastKnownIsIgnoredValue(isIgnored);
}
}
@@ -2033,7 +2917,7 @@ TextIteratorBehavior AccessibilityObject::textIteratorBehaviorForTextRange() con
{
TextIteratorBehavior behavior = TextIteratorIgnoresStyleVisibility;
-#if PLATFORM(GTK) || PLATFORM(EFL)
+#if PLATFORM(GTK)
// We need to emit replaced elements for GTK, and present
// them with the 'object replacement character' (0xFFFC).
behavior = static_cast<TextIteratorBehavior>(behavior | TextIteratorEmitsObjectReplacementCharacters);
@@ -2072,11 +2956,9 @@ bool AccessibilityObject::accessibilityIsIgnoredByDefault() const
// http://www.w3.org/TR/wai-aria/terms#def_hidden
bool AccessibilityObject::isARIAHidden() const
{
- for (const AccessibilityObject* object = this; object; object = object->parentObject()) {
- if (equalIgnoringCase(object->getAttribute(aria_hiddenAttr), "true"))
- return true;
- }
- return false;
+ return AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) {
+ return equalLettersIgnoringASCIICase(object.getAttribute(aria_hiddenAttr), "true");
+ }) != nullptr;
}
// DOM component of hidden definition.
@@ -2096,6 +2978,9 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const
if (isARIAHidden())
return IgnoreObject;
+ if (ignoredFromARIAModalPresence())
+ return IgnoreObject;
+
if (isPresentationalChildOfAriaRole())
return IgnoreObject;
@@ -2104,7 +2989,11 @@ AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const
bool AccessibilityObject::accessibilityIsIgnored() const
{
- AXComputedObjectAttributeCache* attributeCache = axObjectCache()->computedObjectAttributeCache();
+ AXComputedObjectAttributeCache* attributeCache = nullptr;
+ AXObjectCache* cache = axObjectCache();
+ if (cache)
+ attributeCache = cache->computedObjectAttributeCache();
+
if (attributeCache) {
AccessibilityObjectInclusion ignored = attributeCache->getIgnored(axObjectID());
switch (ignored) {
@@ -2119,7 +3008,8 @@ bool AccessibilityObject::accessibilityIsIgnored() const
bool result = computeAccessibilityIsIgnored();
- if (attributeCache)
+ // 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;
@@ -2141,10 +3031,146 @@ void AccessibilityObject::elementsFromAttribute(Vector<Element*>& elements, cons
Vector<String> idVector;
idList.split(' ', idVector);
- for (auto idName : 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()
+{
+ return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) {
+ return object.canSetFocusAttribute();
+ }));
+}
+
+AccessibilityObject* AccessibilityObject::editableAncestor()
+{
+ return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) {
+ return object.isTextControl();
+ }));
+}
+
+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::isFigure() const
+{
+ Node* node = this->node();
+ return node && node->hasTagName(figureTag);
+}
+
+bool AccessibilityObject::isOutput() const
+{
+ Node* node = this->node();
+ return node && node->hasTagName(outputTag);
+}
+
+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