blob: cc5ed69f7a514d6f7dc36acc63b3ab6d32d46f24 [file] [log] [blame]
/*
* Copyright (C) 2008-2025 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 "AXLogger.h"
#include "AXObjectCache.h"
#include "AXRemoteFrame.h"
#include "AXSearchManager.h"
#include "AXTextMarker.h"
#include "AccessibilityMockObject.h"
#include "AccessibilityRenderObject.h"
#include "AccessibilityScrollView.h"
#include "AccessibilityTable.h"
#include "Chrome.h"
#include "ChromeClient.h"
#include "ContainerNodeInlines.h"
#include "CustomElementDefaultARIA.h"
#include "DOMTokenList.h"
#include "DocumentInlines.h"
#include "EditingInlines.h"
#include "Editor.h"
#include "ElementInlines.h"
#include "ElementIterator.h"
#include "Event.h"
#include "EventDispatcher.h"
#include "EventHandler.h"
#include "EventNames.h"
#include "FloatRect.h"
#include "FocusController.h"
#include "FrameLoader.h"
#include "FrameSelection.h"
#include "GeometryUtilities.h"
#include "HTMLAreaElement.h"
#include "HTMLBodyElement.h"
#include "HTMLDataListElement.h"
#include "HTMLDetailsElement.h"
#include "HTMLFormControlElement.h"
#include "HTMLInputElement.h"
#include "HTMLMediaElement.h"
#include "HTMLModelElement.h"
#include "HTMLNames.h"
#include "HTMLParserIdioms.h"
#include "HTMLSlotElement.h"
#include "HTMLTableSectionElement.h"
#include "HTMLTextAreaElement.h"
#include "HitTestResult.h"
#include "LocalFrame.h"
#include "LocalizedStrings.h"
#include "MathMLNames.h"
#include "NodeList.h"
#include "NodeName.h"
#include "NodeTraversal.h"
#include "Page.h"
#include "PositionInlines.h"
#include "ProgressTracker.h"
#include "Range.h"
#include "RenderImage.h"
#include "RenderInline.h"
#include "RenderLayer.h"
#include "RenderListItem.h"
#include "RenderListMarker.h"
#include "RenderMenuList.h"
#include "RenderObjectInlines.h"
#include "RenderText.h"
#include "RenderTextControl.h"
#include "RenderTheme.h"
#include "RenderTreeBuilder.h"
#include "RenderView.h"
#include "RenderWidget.h"
#include "RenderedPosition.h"
#include "SVGNames.h"
#include "Settings.h"
#include "TextCheckerClient.h"
#include "TextCheckingHelper.h"
#include "TextIterator.h"
#include "UserGestureIndicator.h"
#include "VisibleUnits.h"
#include <numeric>
#include <wtf/NeverDestroyed.h>
#include <wtf/StdLibExtras.h>
#include <wtf/text/MakeString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringView.h>
#include <wtf/text/WTFString.h>
#include <wtf/unicode/CharacterNames.h>
namespace WebCore {
using namespace HTMLNames;
AccessibilityObject::AccessibilityObject(AXID axID)
: AXCoreObject(axID)
{
}
AccessibilityObject::~AccessibilityObject()
{
ASSERT(isDetached());
}
void AccessibilityObject::init()
{
m_role = determineAccessibilityRole();
}
std::optional<AXID> AccessibilityObject::treeID() const
{
auto* cache = axObjectCache();
return cache ? std::optional { cache->treeID() } : std::nullopt;
}
String AccessibilityObject::dbgInternal(bool verbose, OptionSet<AXDebugStringOption> debugOptions) const
{
StringBuilder result;
result.append("{"_s);
result.append("role: "_s, accessibilityRoleToString(role()));
result.append(", ID "_s, objectID().loggingString());
if (verbose || debugOptions & AXDebugStringOption::Ignored)
result.append(isIgnored() ? ", ignored"_s : emptyString());
if (verbose || debugOptions & AXDebugStringOption::RelativeFrame) {
FloatRect frame = relativeFrame();
result.append(", relativeFrame ((x: "_s, frame.x(), ", y: "_s, frame.y(), "), (w: "_s, frame.width(), ", h: "_s, frame.height(), "))"_s);
}
if (verbose || debugOptions & AXDebugStringOption::RemoteFrameOffset)
result.append(", remoteFrameOffset ("_s, remoteFrameOffset().x(), ", "_s, remoteFrameOffset().y(), ")"_s);
if (verbose || debugOptions & AXDebugStringOption::IsRemoteFrame)
result.append(isRemoteFrame() ? ", remote frame"_s : emptyString());
if (auto* renderer = this->renderer())
result.append(", "_s, renderer->debugDescription());
else if (auto* node = this->node())
result.append(", "_s, node->debugDescription());
result.append("}"_s);
return result.toString();
}
void AccessibilityObject::detachRemoteParts(AccessibilityDetachmentType detachmentType)
{
// Menu close events need to notify the platform. No element is used in the notification because it's a destruction event.
if (detachmentType == AccessibilityDetachmentType::ElementDestroyed && role() == AccessibilityRole::Menu) {
if (auto* cache = axObjectCache())
cache->postNotification(nullptr, cache->document(), AXNotification::MenuClosed);
}
// Clear any children and call detachFromParent on them so that
// no children are left with dangling pointers to their parent.
clearChildren();
}
bool AccessibilityObject::isDetached() const
{
return !wrapper();
}
OptionSet<AXAncestorFlag> AccessibilityObject::computeAncestorFlags() const
{
OptionSet<AXAncestorFlag> computedFlags;
if (hasAncestorFlag(AXAncestorFlag::IsInDescriptionListDetail) || matchesAncestorFlag(AXAncestorFlag::IsInDescriptionListDetail))
computedFlags.set(AXAncestorFlag::IsInDescriptionListDetail, 1);
if (hasAncestorFlag(AXAncestorFlag::IsInDescriptionListTerm) || matchesAncestorFlag(AXAncestorFlag::IsInDescriptionListTerm))
computedFlags.set(AXAncestorFlag::IsInDescriptionListTerm, 1);
if (hasAncestorFlag(AXAncestorFlag::IsInCell) || matchesAncestorFlag(AXAncestorFlag::IsInCell))
computedFlags.set(AXAncestorFlag::IsInCell, 1);
if (hasAncestorFlag(AXAncestorFlag::IsInRow) || matchesAncestorFlag(AXAncestorFlag::IsInRow))
computedFlags.set(AXAncestorFlag::IsInRow, 1);
return computedFlags;
}
OptionSet<AXAncestorFlag> AccessibilityObject::computeAncestorFlagsWithTraversal() const
{
// If this object's flags are initialized, this traversal is unnecessary. Use AccessibilityObject::ancestorFlags() instead.
ASSERT(!ancestorFlagsAreInitialized());
OptionSet<AXAncestorFlag> computedFlags;
computedFlags.set(AXAncestorFlag::FlagsInitialized, true);
Accessibility::enumerateAncestors<AccessibilityObject>(*this, false, [&] (const AccessibilityObject& ancestor) {
computedFlags.add(ancestor.computeAncestorFlags());
});
return computedFlags;
}
void AccessibilityObject::initializeAncestorFlags(const OptionSet<AXAncestorFlag>& flags)
{
m_ancestorFlags.set(AXAncestorFlag::FlagsInitialized, true);
m_ancestorFlags.add(flags);
}
bool AccessibilityObject::matchesAncestorFlag(AXAncestorFlag flag) const
{
auto role = this->role();
switch (flag) {
case AXAncestorFlag::IsInDescriptionListDetail:
return role == AccessibilityRole::DescriptionListDetail;
case AXAncestorFlag::IsInDescriptionListTerm:
return role == AccessibilityRole::DescriptionListTerm;
case AXAncestorFlag::IsInCell:
return role == AccessibilityRole::Cell;
case AXAncestorFlag::IsInRow:
return role == AccessibilityRole::Row;
default:
ASSERT_NOT_REACHED();
return false;
}
}
bool AccessibilityObject::hasAncestorMatchingFlag(AXAncestorFlag flag) const
{
return Accessibility::findAncestor<AccessibilityObject>(*this, false, [flag] (const AccessibilityObject& object) {
if (object.ancestorFlagsAreInitialized())
return object.ancestorFlags().contains(flag);
return object.matchesAncestorFlag(flag);
}) != nullptr;
}
bool AccessibilityObject::isInDescriptionListDetail() const
{
if (ancestorFlagsAreInitialized())
return m_ancestorFlags.contains(AXAncestorFlag::IsInDescriptionListDetail);
return hasAncestorMatchingFlag(AXAncestorFlag::IsInDescriptionListDetail);
}
bool AccessibilityObject::isInDescriptionListTerm() const
{
if (ancestorFlagsAreInitialized())
return m_ancestorFlags.contains(AXAncestorFlag::IsInDescriptionListTerm);
return hasAncestorMatchingFlag(AXAncestorFlag::IsInDescriptionListTerm);
}
bool AccessibilityObject::isInCell() const
{
if (ancestorFlagsAreInitialized())
return m_ancestorFlags.contains(AXAncestorFlag::IsInCell);
return hasAncestorMatchingFlag(AXAncestorFlag::IsInCell);
}
bool AccessibilityObject::isInRow() const
{
if (ancestorFlagsAreInitialized())
return m_ancestorFlags.contains(AXAncestorFlag::IsInRow);
return hasAncestorMatchingFlag(AXAncestorFlag::IsInRow);
}
// 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 AccessibilityRole::ApplicationAlert:
case AccessibilityRole::ApplicationAlertDialog:
case AccessibilityRole::ApplicationDialog:
case AccessibilityRole::ApplicationLog:
case AccessibilityRole::ApplicationMarquee:
case AccessibilityRole::ApplicationStatus:
case AccessibilityRole::ApplicationTimer:
case AccessibilityRole::ComboBox:
case AccessibilityRole::Definition:
case AccessibilityRole::Document:
case AccessibilityRole::DocumentArticle:
case AccessibilityRole::DocumentMath:
case AccessibilityRole::DocumentNote:
case AccessibilityRole::LandmarkRegion:
case AccessibilityRole::LandmarkDocRegion:
case AccessibilityRole::Form:
case AccessibilityRole::Grid:
case AccessibilityRole::Group:
case AccessibilityRole::Image:
case AccessibilityRole::List:
case AccessibilityRole::ListBox:
case AccessibilityRole::LandmarkBanner:
case AccessibilityRole::LandmarkComplementary:
case AccessibilityRole::LandmarkContentInfo:
case AccessibilityRole::LandmarkNavigation:
case AccessibilityRole::LandmarkMain:
case AccessibilityRole::LandmarkSearch:
case AccessibilityRole::Menu:
case AccessibilityRole::MenuBar:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::Meter:
case AccessibilityRole::RadioGroup:
case AccessibilityRole::ScrollBar:
case AccessibilityRole::Slider:
case AccessibilityRole::SpinButton:
case AccessibilityRole::Splitter:
case AccessibilityRole::Table:
case AccessibilityRole::TabList:
case AccessibilityRole::TabPanel:
case AccessibilityRole::TextArea:
case AccessibilityRole::TextField:
case AccessibilityRole::Toolbar:
case AccessibilityRole::TreeGrid:
case AccessibilityRole::Tree:
case AccessibilityRole::WebApplication:
return false;
default:
break;
}
// Now check for generically derived elements now that we know the element does not match a specific ARIA role.
switch (role()) {
case AccessibilityRole::Slider:
case AccessibilityRole::ListBox:
return false;
default:
break;
}
return true;
}
// https://github.com/w3c/aria/pull/1860
// If accname cannot be derived from content or author, accname can be derived on permitted roles
// from the first descendant element node with a heading role.
bool AccessibilityObject::accessibleNameDerivesFromHeading() const
{
switch (role()) {
case AccessibilityRole::ApplicationAlertDialog:
case AccessibilityRole::ApplicationDialog:
case AccessibilityRole::DocumentArticle:
return true;
default:
return false;
}
}
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.
Ref protectedThis { *this };
updateBackingStore();
Vector<AccessibilityText> text;
accessibilityText(text);
if (text.size())
return text[0].text;
return { };
}
bool AccessibilityObject::isARIATextControl() const
{
return ariaRoleAttribute() == AccessibilityRole::TextArea || ariaRoleAttribute() == AccessibilityRole::TextField || ariaRoleAttribute() == AccessibilityRole::SearchField;
}
bool AccessibilityObject::isEditableWebArea() const
{
if (!isWebArea())
return false;
auto* page = this->page();
if (page && page->isEditable())
return true;
auto* document = this->document();
return document && document->inDesignMode();
}
bool AccessibilityObject::isNonNativeTextControl() const
{
return (isARIATextControl() || hasContentEditableAttributeSet()) && !isNativeTextControl();
}
Vector<AXTextMarkerRange> AccessibilityObject::misspellingRanges() const
{
AXTRACE("AccessibilityObject::misspellingRanges"_s);
RefPtr node = this->node();
if (!node)
return { };
RefPtr frame = node->document().frame();
if (!frame)
return { };
auto* textChecker = frame->editor().textChecker();
if (!textChecker)
return { };
// In order to resolve to the correct ranges, Editor::rangeForTextCheckingResult(...)
// assumes that the selection is within the Node for which text we are calling checkTextOfParagraph.
// Therefore, remember the current selection, set it to the beginning of the Node and restore it aftwards.
auto originalSelection = frame->selection().selection();
if (auto range = simpleRange()) {
// Passing UserTriggered::No, which is the default value, guaranties that accessibility is not notified of text selection changes.
frame->selection().setSelectedRange(SimpleRange { range->start, range->start }, Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::No);
}
Vector<AXTextMarkerRange> ranges;
if (unifiedTextCheckerEnabled(frame.get())) {
Vector<TextCheckingResult> misspellings;
checkTextOfParagraph(*textChecker, stringValue(), TextCheckingType::Spelling, misspellings, frame->selection().selection());
for (auto& misspelling : misspellings) {
if (auto range = frame->editor().rangeForTextCheckingResult(misspelling))
ranges.append(range);
}
} else {
int location = -1;
int length = 0;
textChecker->checkSpellingOfString(stringValue(), &location, &length);
if (location > -1 && length > 0)
ranges = { { treeID(), objectID(), static_cast<unsigned>(location), static_cast<unsigned>(length) } };
}
frame->selection().setSelectedRange(originalSelection.range(), Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::No);
return ranges;
}
std::optional<SimpleRange> AccessibilityObject::misspellingRange(const SimpleRange& start, AccessibilitySearchDirection direction) const
{
auto node = this->node();
if (!node)
return std::nullopt;
auto* frame = node->document().frame();
if (!frame)
return std::nullopt;
if (!unifiedTextCheckerEnabled(frame))
return std::nullopt;
Editor& editor = frame->editor();
TextCheckerClient* textChecker = editor.textChecker();
if (!textChecker)
return std::nullopt;
Vector<TextCheckingResult> misspellings;
checkTextOfParagraph(*textChecker, stringValue(), TextCheckingType::Spelling, misspellings, frame->selection().selection());
// Find the first misspelling past the start.
if (direction == AccessibilitySearchDirection::Next) {
for (auto& misspelling : misspellings) {
auto misspellingRange = editor.rangeForTextCheckingResult(misspelling);
if (misspellingRange && is_gt(treeOrder<ComposedTree>(misspellingRange->end, start.end)))
return *misspellingRange;
}
} else {
for (auto& misspelling : makeReversedRange(misspellings)) {
auto misspellingRange = editor.rangeForTextCheckingResult(misspelling);
if (misspellingRange && is_lt(treeOrder<ComposedTree>(misspellingRange->start, start.start)))
return *misspellingRange;
}
}
return std::nullopt;
}
AXTextMarkerRange AccessibilityObject::textInputMarkedTextMarkerRange() const
{
WeakPtr node = this->node();
if (!node)
return { };
auto* frame = node->document().frame();
if (!frame)
return { };
auto* cache = axObjectCache();
if (!cache)
return { };
auto& editor = frame->editor();
auto* object = cache->getOrCreate(editor.compositionNode());
if (!object)
return { };
if (auto* observableObject = object->observableObject())
object = observableObject;
if (object->objectID() != objectID())
return { };
return { editor.compositionRange() };
}
AccessibilityObject* AccessibilityObject::displayContentsParent() const
{
auto* parentNode = node() ? node()->parentNode() : nullptr;
if (RefPtr parentElement = dynamicDowncast<Element>(parentNode); !parentElement || !parentElement->hasDisplayContents())
return nullptr;
auto* cache = axObjectCache();
return cache ? cache->getOrCreate(*parentNode) : nullptr;
}
AccessibilityObject* AccessibilityObject::nextSiblingUnignored(unsigned limit) const
{
ASSERT(limit);
for (auto sibling = iterator(nextSibling()); limit && sibling; --limit, ++sibling) {
if (!sibling->isIgnored())
return sibling.ptr();
}
return nullptr;
}
AccessibilityObject* AccessibilityObject::previousSiblingUnignored(unsigned limit) const
{
ASSERT(limit);
for (auto sibling = iterator(previousSibling()); limit && sibling; --limit, --sibling) {
if (!sibling->isIgnored())
return sibling.ptr();
}
return nullptr;
}
FloatRect AccessibilityObject::convertFrameToSpace(const FloatRect& frameRect, AccessibilityConversionSpace conversionSpace) const
{
ASSERT(isMainThread());
// Find the appropriate scroll view to use to convert the contents to the window.
const auto parentAccessibilityScrollView = ancestorAccessibilityScrollView(false /* includeSelf */);
auto* parentScrollView = parentAccessibilityScrollView ? parentAccessibilityScrollView->scrollView() : nullptr;
auto snappedFrameRect = snappedIntRect(IntRect(frameRect));
if (parentScrollView)
snappedFrameRect = parentScrollView->contentsToRootView(snappedFrameRect);
if (conversionSpace == AccessibilityConversionSpace::Screen) {
auto page = this->page();
if (!page)
return snappedFrameRect;
// If we have an empty chrome client (like SVG) then we should use the page
// of the scroll view parent to help us get to the screen rect.
if (parentAccessibilityScrollView && page->chrome().client().isEmptyChromeClient())
page = parentAccessibilityScrollView->page();
snappedFrameRect = page->chrome().rootViewToAccessibilityScreen(snappedFrameRect);
}
return snappedFrameRect;
}
FloatRect AccessibilityObject::relativeFrame() const
{
auto rect = elementRect();
rect.moveBy(remoteFrameOffset());
return convertFrameToSpace(rect, AccessibilityConversionSpace::Page);
}
AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const Node* node)
{
return WebCore::firstAccessibleObjectFromNode(node, [] (const AccessibilityObject& accessible) {
return !accessible.isIgnored();
});
}
AccessibilityObject* firstAccessibleObjectFromNode(const Node* node, NOESCAPE const Function<bool(const AccessibilityObject&)>& isAccessible)
{
if (!node)
return nullptr;
AXObjectCache* cache = node->document().axObjectCache();
if (!cache)
return nullptr;
AccessibilityObject* accessibleObject = cache->getOrCreate(node->renderer());
while (accessibleObject && !isAccessible(*accessibleObject)) {
node = NodeTraversal::next(*node);
while (node && !node->renderer())
node = NodeTraversal::nextSkippingChildren(*node);
if (!node)
return nullptr;
accessibleObject = cache->getOrCreate(node->renderer());
}
return accessibleObject;
}
// FIXME: Usages of this function should be replaced by a new flag in AccessibilityObject::m_ancestorFlags.
bool AccessibilityObject::isDescendantOfRole(AccessibilityRole role) const
{
return Accessibility::findAncestor<AccessibilityObject>(*this, false, [&role] (const AccessibilityObject& object) {
return object.role() == role;
}) != nullptr;
}
#if ASSERT_ENABLED
static bool isTableComponent(AXCoreObject& axObject)
{
return axObject.isTable() || axObject.isTableColumn() || axObject.isTableRow() || axObject.isTableCell();
}
#endif
void AccessibilityObject::insertChild(AccessibilityObject& child, unsigned index, DescendIfIgnored descendIfIgnored)
{
auto owners = child.owners();
if (owners.size()) {
size_t indexOfThis = owners.findIf([this] (const Ref<AXCoreObject>& object) {
return object.ptr() == this;
});
if (indexOfThis == notFound) {
// The child is aria-owned, and not by us, so we shouldn't insert it.
return;
}
}
if (is<HTMLAreaElement>(child.node())) [[unlikely]] {
// Despite the DOM parent for <area> elements being <map>, we expose <area> elements as children
// of the <img> using the <map>. This provides a better experience for AT users, e.g. a screenreader
// would hear "image map" or "group" plus the image description, then the links, which provides the
// added context for what the links represent.
//
// Due to the difference in DOM vs. expected AX hierarchy, make sure area elements are only inserted
// by their associated image as children.
if (child.parentObject() != this)
return;
}
// If the parent is asking for this child's children, then either it's the first time (and clearing is a no-op),
// or its visibility has changed. In the latter case, this child may have a stale child cached.
// This can prevent aria-hidden changes from working correctly. Hence, whenever a parent is getting children, ensure data is not stale.
// Only clear the child's children when we know it's in the updating chain in order to avoid unnecessary work.
if (child.needsToUpdateChildren() || m_subtreeDirty) {
child.clearChildren();
// Pass m_subtreeDirty flag down to the child so that children cache gets reset properly.
if (m_subtreeDirty)
child.setNeedsToUpdateSubtree();
}
#if USE(ATSPI)
// FIXME: Consider removing this ATSPI-only branch with https://bugs.webkit.org/show_bug.cgi?id=282117.
RefPtr displayContentsParent = child.displayContentsParent();
// To avoid double-inserting a child of a `display: contents` element, only insert if `this` is the rightful parent.
if (displayContentsParent && displayContentsParent != this) {
// Make sure the display:contents parent object knows it has a child it needs to add.
displayContentsParent->setNeedsToUpdateChildren();
// Don't exit early for certain table components, as they rely on inserting children for which they are not the rightful parent to behave correctly.
bool allowInsert = isTableColumn() || role() == AccessibilityRole::TableHeaderContainer;
// AccessibilityTable::addChildren never actually calls `insertChild` for table section elements
// (e.g. tbody, thead), so don't block this `insertChild` for display:contents section elements,
// or else the child elements of the section element will never be inserted into the tree.
allowInsert = allowInsert || (isAccessibilityTableInstance() && is<HTMLTableSectionElement>(displayContentsParent->element()));
if (!allowInsert)
return;
}
#endif // USE(ATSPI)
auto insert = [this] (Ref<AXCoreObject>&& object, unsigned index) {
std::ignore = setChildIndexInParent(object.get(), index);
m_children.insert(index, WTFMove(object));
};
auto thisAncestorFlags = computeAncestorFlags();
child.initializeAncestorFlags(thisAncestorFlags);
setIsIgnoredFromParentDataForChild(child);
if (!includeIgnoredInCoreTree() && child.isIgnored()) {
if (descendIfIgnored == DescendIfIgnored::Yes) {
unsigned insertionIndex = index;
auto childAncestorFlags = child.computeAncestorFlags();
for (auto grandchildCoreObject : child.children()) {
Ref grandchild = downcast<AccessibilityObject>(grandchildCoreObject.get());
// Even though `child` is ignored, we still need to set ancestry flags based on it.
grandchild->initializeAncestorFlags(childAncestorFlags);
grandchild->addAncestorFlags(thisAncestorFlags);
// Calls to `child.isIgnored()` or `child.children()` can cause layout, which in turn can cause this object to clear its m_children. This can cause `insertionIndex` to no longer be valid. Detect this and break early if necessary.
if (insertionIndex > m_children.size())
break;
insert(WTFMove(grandchild), insertionIndex);
++insertionIndex;
}
}
} else {
// Table component child-parent relationships often don't line up properly, hence the need for methods
// like parentTable() and parentRow(). Exclude them from this ASSERT.
// FIXME: We hit this ASSERT on gmail.com. https://bugs.webkit.org/show_bug.cgi?id=293264
ASSERT(isTableComponent(child) || isTableComponent(*this) || child.parentObject() == this);
insert(Ref { child }, index);
}
// Reset the child's m_isIgnoredFromParentData since we are done adding that child and its children.
child.clearIsIgnoredFromParentData();
}
void AccessibilityObject::resetChildrenIndexInParent() const
{
if (!shouldSetChildIndexInParent())
return;
unsigned index = 0;
for (const auto& child : m_children) {
bool didSet = setChildIndexInParent(child.get(), index);
// We check shouldSetChildIndexInParent above, so this should always be true.
ASSERT_UNUSED(didSet, didSet);
++index;
}
}
AXCoreObject::AccessibilityChildrenVector AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria&& criteria)
{
if (auto* cache = axObjectCache())
cache->startCachingComputedObjectAttributesUntilTreeMutates();
criteria.anchorObject = this;
return AXSearchManager().findMatchingObjects(WTFMove(criteria));
}
// 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 std::optional<SimpleRange> rangeClosestToRange(const SimpleRange& referenceRange, std::optional<SimpleRange>&& afterRange, std::optional<SimpleRange>&& beforeRange)
{
if (!beforeRange)
return WTFMove(afterRange);
if (!afterRange)
return WTFMove(beforeRange);
auto distanceBefore = characterCount({ beforeRange->end, referenceRange.start });
auto distanceAfter = characterCount({ afterRange->start, referenceRange.end });
return WTFMove(distanceBefore <= distanceAfter ? beforeRange : afterRange);
}
std::optional<SimpleRange> AccessibilityObject::rangeOfStringClosestToRangeInDirection(const SimpleRange& referenceRange, AccessibilitySearchDirection searchDirection, const Vector<String>& searchStrings) const
{
auto* frame = this->frame();
if (!frame)
return std::nullopt;
bool isBackwardSearch = searchDirection == AccessibilitySearchDirection::Previous;
FindOptions findOptions { FindOption::AtWordStarts, FindOption::AtWordEnds, FindOption::CaseInsensitive, FindOption::StartInSelection };
if (isBackwardSearch)
findOptions.add(FindOption::Backwards);
std::optional<SimpleRange> closestStringRange;
for (auto& searchString : searchStrings) {
if (auto foundStringRange = frame->editor().rangeOfString(searchString, referenceRange, findOptions)) {
bool foundStringIsCloser;
if (!closestStringRange)
foundStringIsCloser = true;
else {
foundStringIsCloser = isBackwardSearch
? is_gt(treeOrder<ComposedTree>(foundStringRange->end, closestStringRange->end))
: is_lt(treeOrder<ComposedTree>(foundStringRange->start, closestStringRange->start));
}
if (foundStringIsCloser)
closestStringRange = *foundStringRange;
}
}
return closestStringRange;
}
VisibleSelection AccessibilityObject::selection() const
{
auto* document = this->document();
auto* frame = document ? document->frame() : nullptr;
return frame ? frame->selection().selection() : VisibleSelection();
}
// Returns an collapsed range preceding the document contents if there is no selection.
// FIXME: Why is that behavior more useful than returning null in that case?
std::optional<SimpleRange> AccessibilityObject::selectionRange() const
{
auto frame = this->frame();
if (!frame)
return std::nullopt;
if (auto range = frame->selection().selection().firstRange())
return *range;
auto& document = *frame->document();
return { { { document, 0 }, { document, 0 } } };
}
std::optional<SimpleRange> AccessibilityObject::simpleRange() const
{
auto* node = this->node();
if (!node)
return std::nullopt;
return AXObjectCache::rangeForNodeContents(*node);
}
AXTextMarkerRange AccessibilityObject::textMarkerRange() const
{
return simpleRange();
}
Vector<BoundaryPoint> AccessibilityObject::previousLineStartBoundaryPoints(const VisiblePosition& startingPosition, const SimpleRange& targetRange, unsigned positionsToRetrieve) const
{
Vector<BoundaryPoint> boundaryPoints;
boundaryPoints.reserveInitialCapacity(positionsToRetrieve);
std::optional<VisiblePosition> lastPosition = startingPosition;
for (unsigned i = 0; i < positionsToRetrieve; i++) {
lastPosition = previousLineStartPositionInternal(*lastPosition);
if (!lastPosition)
break;
auto boundaryPoint = makeBoundaryPoint(*lastPosition);
if (!boundaryPoint || !contains(targetRange, *boundaryPoint))
break;
boundaryPoints.append(WTFMove(*boundaryPoint));
}
boundaryPoints.shrinkToFit();
return boundaryPoints;
}
std::optional<BoundaryPoint> AccessibilityObject::lastBoundaryPointContainedInRect(const Vector<BoundaryPoint>& boundaryPoints, const BoundaryPoint& startBoundary, const FloatRect& rect, int leftIndex, int rightIndex, bool isFlippedWritingMode) const
{
if (leftIndex > rightIndex || boundaryPoints.isEmpty())
return std::nullopt;
auto indexIsValid = [&] (int index) {
return index >= 0 && static_cast<size_t>(index) < boundaryPoints.size();
};
auto boundaryPointContainedInRect = [&] (const BoundaryPoint& boundary) {
return boundaryPointsContainedInRect(startBoundary, boundary, rect, isFlippedWritingMode);
};
int midIndex = std::midpoint(leftIndex, rightIndex);
if (boundaryPointContainedInRect(boundaryPoints.at(midIndex))) {
// We have a match if `midIndex` boundary point is contained in the rect, but the one at `midIndex - 1` isn't.
if (indexIsValid(midIndex - 1) && !boundaryPointContainedInRect(boundaryPoints.at(midIndex - 1)))
return boundaryPoints.at(midIndex);
return lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, rect, leftIndex, midIndex - 1, isFlippedWritingMode);
}
// And vice versa, we have a match if the `midIndex` boundary point is not contained in the rect, but the one at `midIndex + 1` is.
if (indexIsValid(midIndex + 1) && boundaryPointContainedInRect(boundaryPoints.at(midIndex + 1)))
return boundaryPoints.at(midIndex + 1);
return lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, rect, midIndex + 1, rightIndex, isFlippedWritingMode);
}
static IntPoint textStartPoint(const IntRect& rect, bool isFlippedWritingMode)
{
if (!isFlippedWritingMode)
return rect.minXMinYCorner();
return rect.maxXMinYCorner();
}
static IntPoint textEndPoint(const IntRect& rect, bool isFlippedWritingMode)
{
if (!isFlippedWritingMode)
return rect.maxXMaxYCorner();
return rect.minXMaxYCorner();
}
bool AccessibilityObject::boundaryPointsContainedInRect(const BoundaryPoint& startBoundary, const BoundaryPoint& endBoundary, const FloatRect& rect, bool isFlippedWritingMode) const
{
auto elementRect = boundsForRange({ startBoundary, endBoundary });
return rect.contains(textEndPoint(elementRect, isFlippedWritingMode));
}
std::optional<SimpleRange> AccessibilityObject::visibleCharacterRange() const
{
auto range = simpleRange();
if (!range)
return std::nullopt;
auto contentRect = unobscuredContentRect();
auto elementRect = snappedIntRect(this->elementRect());
return visibleCharacterRangeInternal(*range, contentRect, elementRect);
}
std::optional<SimpleRange> AccessibilityObject::visibleCharacterRangeInternal(SimpleRange& range, const FloatRect& contentRect, const IntRect& startingElementRect) const
{
if (!contentRect.intersects(startingElementRect))
return std::nullopt;
auto elementRect = startingElementRect;
auto startBoundary = range.start;
auto endBoundary = range.end;
const auto* style = this->style();
bool isFlipped = style && style->writingMode().isBlockFlipped();
// In vertical-rl writing-modes (e.g. some Japanese text), text lays out vertically from right-to-left, meaning the the start of the text
// has a larger `x`-coordinate than the end.
bool laysOutIntoNegativeX = isFlipped && style->writingMode().isVertical();
// Origin isn't contained in visible rect, start moving forward by line.
while (!contentRect.contains(textStartPoint(elementRect, isFlipped))) {
auto currentPosition = VisiblePosition(makeContainerOffsetPosition(startBoundary));
auto nextLinePosition = nextLineEndPosition(currentPosition);
if (nextLinePosition == currentPosition) {
// We tried to move to the next line end, but got the same position back. Break to avoid
// looping infinitely. It would be better if we understood *why* nextLineEndPosition
// is returning the same position, but do this for now. If you hit this assert, please
// file a bug with steps to reproduce.
ASSERT_NOT_REACHED();
break;
}
auto testStartBoundary = makeBoundaryPoint(nextLinePosition);
if (!testStartBoundary || !contains(range, *testStartBoundary))
break;
// testStartBoundary is valid, so commit it and update the elementRect.
startBoundary = *testStartBoundary;
elementRect = boundsForRange(SimpleRange(startBoundary, range.end));
if (elementRect.isEmpty() || (elementRect.x() < 0 && !laysOutIntoNegativeX) || elementRect.y() < 0)
break;
}
bool didCorrectStartBoundary = false;
// Sometimes we shrink one line too far -- check the previous line start to see if it's in bounds.
auto previousLineStartPosition = previousLineStartPositionInternal(VisiblePosition(makeContainerOffsetPosition(startBoundary)));
if (previousLineStartPosition) {
if (auto previousLineStartBoundaryPoint = makeBoundaryPoint(*previousLineStartPosition)) {
auto lineStartRect = boundsForRange(SimpleRange(*previousLineStartBoundaryPoint, range.end));
if (previousLineStartBoundaryPoint->container.ptr() == startBoundary.container.ptr() && contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
elementRect = lineStartRect;
startBoundary = *previousLineStartBoundaryPoint;
didCorrectStartBoundary = true;
}
}
}
if (!didCorrectStartBoundary) {
// We iterated to a line-end position above. We must also check if the start of this line is in bounds.
auto startBoundaryLineStartPosition = startOfLine(VisiblePosition(makeContainerOffsetPosition(startBoundary)));
auto lineStartBoundaryPoint = makeBoundaryPoint(startBoundaryLineStartPosition);
if (lineStartBoundaryPoint && lineStartBoundaryPoint->container.ptr() == startBoundary.container.ptr()) {
auto lineStartRect = boundsForRange(SimpleRange(*lineStartBoundaryPoint, range.end));
if (contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
elementRect = lineStartRect;
startBoundary = *lineStartBoundaryPoint;
} else if (lineStartBoundaryPoint->offset < lineStartBoundaryPoint->container->length()) {
// Sometimes we're one character off from being in-bounds. Check for this too.
lineStartBoundaryPoint->offset = lineStartBoundaryPoint->offset + 1;
lineStartRect = boundsForRange(SimpleRange(*lineStartBoundaryPoint, range.end));
lineStartBoundaryPoint->offset = lineStartBoundaryPoint->offset - 1;
if (contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
elementRect = lineStartRect;
startBoundary = *lineStartBoundaryPoint;
}
}
}
}
// Computing previous line start positions is cheap relative to computing boundsForRange, so compute the end boundary by
// grabbing batches of lines and binary searching within them to minimize calls to boundsForRange.
Vector<BoundaryPoint> boundaryPoints = { endBoundary };
do {
// If the first boundary point is contained in contentRect, then it's a match because we know everything in the last batch
// of lines was not contained in contentRect.
if (boundaryPointsContainedInRect(startBoundary, boundaryPoints.at(0), contentRect, isFlipped)) {
endBoundary = boundaryPoints.at(0);
break;
}
auto lastBoundaryPoint = boundaryPoints.last();
elementRect = boundsForRange({ startBoundary, lastBoundaryPoint });
if (elementRect.isEmpty())
break;
// Otherwise if the last boundary point is contained in contentRect, then we know some boundary point in this batch is
// our target end boundary point.
if (contentRect.contains(textEndPoint(elementRect, isFlipped))) {
endBoundary = lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, contentRect, isFlipped).value_or(lastBoundaryPoint);
break;
}
boundaryPoints = previousLineStartBoundaryPoints(VisiblePosition(makeContainerOffsetPosition(lastBoundaryPoint)), range, 64);
} while (!boundaryPoints.isEmpty());
// Sometimes we shrink one line too far. Check the next line end to see if it's in bounds.
auto nextLineEndPosition = this->nextLineEndPosition(VisiblePosition(makeContainerOffsetPosition(endBoundary)));
auto nextLineEndBoundaryPoint = makeBoundaryPoint(nextLineEndPosition);
if (nextLineEndBoundaryPoint && nextLineEndBoundaryPoint->container.ptr() == endBoundary.container.ptr()) {
auto lineEndRect = boundsForRange(SimpleRange(startBoundary, *nextLineEndBoundaryPoint));
if (contentRect.contains(textEndPoint(lineEndRect, isFlipped)))
endBoundary = *nextLineEndBoundaryPoint;
}
return { { startBoundary, endBoundary } };
}
std::optional<SimpleRange> AccessibilityObject::findTextRange(const Vector<String>& searchStrings, const SimpleRange& start, AccessibilitySearchTextDirection direction) const
{
std::optional<SimpleRange> found;
if (direction == AccessibilitySearchTextDirection::Forward)
found = rangeOfStringClosestToRangeInDirection(start, AccessibilitySearchDirection::Next, searchStrings);
else if (direction == AccessibilitySearchTextDirection::Backward)
found = rangeOfStringClosestToRangeInDirection(start, AccessibilitySearchDirection::Previous, searchStrings);
else if (direction == AccessibilitySearchTextDirection::Closest) {
auto foundAfter = rangeOfStringClosestToRangeInDirection(start, AccessibilitySearchDirection::Next, searchStrings);
auto foundBefore = rangeOfStringClosestToRangeInDirection(start, AccessibilitySearchDirection::Previous, searchStrings);
found = rangeClosestToRange(start, WTFMove(foundAfter), WTFMove(foundBefore));
}
if (found) {
// If the search started within a text control, ensure that the result is inside that element.
if (element() && element()->isTextField()) {
if (!found->startContainer().isShadowIncludingDescendantOf(element())
|| !found->endContainer().isShadowIncludingDescendantOf(element()))
return std::nullopt;
}
}
return found;
}
Vector<SimpleRange> AccessibilityObject::findTextRanges(const AccessibilitySearchTextCriteria& criteria) const
{
std::optional<SimpleRange> range;
if (criteria.start == AccessibilitySearchTextStartFrom::Selection)
range = selectionRange();
else
range = simpleRange();
if (!range)
return { };
if (criteria.start == AccessibilitySearchTextStartFrom::Begin)
range->end = range->start;
else if (criteria.start == AccessibilitySearchTextStartFrom::End)
range->start = range->end;
else if (criteria.direction == AccessibilitySearchTextDirection::Backward)
range->start = range->end;
else
range->end = range->start;
Vector<SimpleRange> result;
switch (criteria.direction) {
case AccessibilitySearchTextDirection::Forward:
case AccessibilitySearchTextDirection::Backward:
case AccessibilitySearchTextDirection::Closest:
if (auto foundRange = findTextRange(criteria.searchStrings, *range, criteria.direction))
result.append(*foundRange);
break;
case AccessibilitySearchTextDirection::All:
auto appendFoundRanges = [&](AccessibilitySearchTextDirection direction) {
for (auto foundRange = range; (foundRange = findTextRange(criteria.searchStrings, *foundRange, direction)); )
result.append(*foundRange);
};
appendFoundRanges(AccessibilitySearchTextDirection::Forward);
appendFoundRanges(AccessibilitySearchTextDirection::Backward);
break;
}
return result;
}
struct TextOperationRange {
SimpleRange scope;
CharacterRange characterRange;
};
static std::optional<TextOperationRange> textOperationRangeFromRange(const SimpleRange& range)
{
RefPtr<Element> rootEditableElement = range.startContainer().rootEditableElement();
if (!rootEditableElement)
return std::nullopt;
auto scopeStart = firstPositionInNode(rootEditableElement.get());
auto scopeEnd = lastPositionInNode(rootEditableElement.get());
std::optional<SimpleRange> scope = makeSimpleRange(scopeStart, scopeEnd);
if (!scope)
return std::nullopt;
return TextOperationRange { *scope, characterRange(*scope, range, { }) };
}
static SimpleRange rangeFromTextOperationRange(const TextOperationRange& textOperationRange)
{
return resolveCharacterRange(textOperationRange.scope, textOperationRange.characterRange, { });
}
Vector<String> AccessibilityObject::performTextOperation(const AccessibilityTextOperation& operation)
{
Vector<TextOperationRange> textOperationRanges;
textOperationRanges.reserveInitialCapacity(operation.textRanges.size());
Vector<String> result;
result.reserveInitialCapacity(operation.textRanges.size());
if (operation.textRanges.isEmpty())
return result;
auto* frame = this->frame();
if (!frame)
return result;
size_t replacementStringsCount = operation.replacementStrings.size();
bool useFirstReplacementStringForAllReplacements = (replacementStringsCount == 1);
// Precompute character ranges with respect to their root editable element because
// the SimpleRanges stored in AccessibilityTextOperation may be invalidated after
// performing a replacement in the same editable element.
for (const auto& range : operation.textRanges) {
auto textOperationRange = textOperationRangeFromRange(range);
if (!textOperationRange) {
ASSERT_NOT_REACHED();
return result;
}
textOperationRanges.append(*textOperationRange);
}
for (size_t i = 0; i < textOperationRanges.size(); ++i) {
const auto& textOperationRange = textOperationRanges[i];
auto textRange = rangeFromTextOperationRange(textOperationRange);
String replacementString;
if (useFirstReplacementStringForAllReplacements)
replacementString = operation.replacementStrings[0];
else if (i < replacementStringsCount)
replacementString = operation.replacementStrings[i];
if (!frame->selection().setSelectedRange(textRange, Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes))
continue;
String text = plainText(textRange);
bool replaceSelection = false;
switch (operation.type) {
case AccessibilityTextOperationType::Capitalize:
replacementString = capitalize(text); // FIXME: Needs to take locale into account to work correctly.
replaceSelection = true;
break;
case AccessibilityTextOperationType::Uppercase:
replacementString = text.convertToUppercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
replaceSelection = true;
break;
case AccessibilityTextOperationType::Lowercase:
replacementString = text.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
replaceSelection = true;
break;
case AccessibilityTextOperationType::Replace: {
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 (text.length() > 0
&& replacementString.length() > 2
&& replacementString != replacementString.convertToUppercaseWithoutLocale()) {
if (text[0] == u_toupper(text[0]))
replacementString = capitalize(replacementString); // FIXME: Needs to take locale into account to work correctly.
else
replacementString = replacementString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
}
break;
}
case AccessibilityTextOperationType::ReplacePreserveCase:
replaceSelection = true;
break;
case AccessibilityTextOperationType::Select:
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) {
// Insert text instead of replacing when the selection length is zero, because replacements
// aren't performed correctly in certain edge cases like at the the boundary between nodes
// separated by spaces <p> foo <i>bar</i>[insert here] baz </p>.
if (textOperationRange.characterRange.length)
frame->editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::Yes, operation.smartReplace == AccessibilityTextOperationSmartReplace::No ? Editor::SmartReplace::No : Editor::SmartReplace::Yes);
else
frame->editor().insertText(replacementString, /* triggeringEvent */ nullptr);
result.append(replacementString);
} else
result.append(text);
}
return result;
}
bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole)
{
switch (ariaRole) {
case AccessibilityRole::Checkbox:
case AccessibilityRole::RadioButton:
case AccessibilityRole::SearchField:
case AccessibilityRole::Switch:
case AccessibilityRole::TextField:
return true;
default:
return false;
}
}
bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole)
{
if (isARIAInput(ariaRole))
return true;
switch (ariaRole) {
case AccessibilityRole::Button:
case AccessibilityRole::ComboBox:
case AccessibilityRole::ListBox:
case AccessibilityRole::PopUpButton:
case AccessibilityRole::Slider:
case AccessibilityRole::TextArea:
case AccessibilityRole::ToggleButton:
return true;
default:
return false;
}
}
bool AccessibilityObject::isRangeControl() const
{
switch (role()) {
case AccessibilityRole::Meter:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::Slider:
case AccessibilityRole::ScrollBar:
case AccessibilityRole::SpinButton:
return true;
case AccessibilityRole::Splitter:
return canSetFocusAttribute();
default:
return false;
}
}
static IntRect boundsForRects(const LayoutRect& rect1, const LayoutRect& rect2, const SimpleRange& dataRange)
{
LayoutRect ourRect = rect1;
ourRect.unite(rect2);
// If the rectangle spans lines and contains multiple text characters, use the range's bounding box intead.
if (rect1.maxY() != rect2.maxY() && characterCount(dataRange) > 1) {
if (auto boundingBox = unionRect(RenderObject::absoluteTextRects(dataRange)); !boundingBox.isEmpty())
ourRect = boundingBox;
}
return snappedIntRect(ourRect);
}
IntRect AccessibilityRenderObject::boundsForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
{
if (visiblePositionRange.isNull())
return IntRect();
// Create a mutable VisiblePositionRange.
VisiblePositionRange range(visiblePositionRange);
LayoutRect rect1 = range.start.absoluteCaretBounds();
LayoutRect rect2 = range.end.absoluteCaretBounds();
// Readjust for position at the edge of a line. This is to exclude line rect that doesn't need to be accounted in the range bounds
if (rect2.y() != rect1.y()) {
VisiblePosition endOfFirstLine = endOfLine(range.start);
if (range.start == endOfFirstLine) {
range.start.setAffinity(Affinity::Downstream);
rect1 = range.start.absoluteCaretBounds();
}
if (range.end == endOfFirstLine) {
range.end.setAffinity(Affinity::Upstream);
rect2 = range.end.absoluteCaretBounds();
}
}
return boundsForRects(rect1, rect2, *makeSimpleRange(range));
}
IntRect AccessibilityObject::boundsForRange(const SimpleRange& range) const
{
auto cache = axObjectCache();
if (!cache)
return { };
auto start = cache->startOrEndCharacterOffsetForRange(range, true);
auto end = cache->startOrEndCharacterOffsetForRange(range, false);
auto rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
auto rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
// Readjust for position at the edge of a line. This is to exclude line rect that doesn't need to be accounted in the range bounds.
if (rect2.y() != rect1.y()) {
auto endOfFirstLine = cache->endCharacterOffsetOfLine(start);
if (start.isEqual(endOfFirstLine)) {
start = cache->nextCharacterOffset(start, false);
rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
}
if (end.isEqual(endOfFirstLine)) {
end = cache->previousCharacterOffset(end, false);
rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
}
}
return boundsForRects(rect1, rect2, range);
}
IntPoint AccessibilityObject::linkClickPoint()
{
ASSERT(isLink());
/* A link bounding rect can contain points that are not part of the link.
For instance, a link that starts at the end of a line and finishes at the
beginning of the next line will have a bounding rect that includes the
entire two lines. In such a case, the middle point of the bounding rect
may not belong to the link element and thus may not activate the link.
Hence, return the middle point of the first character in the link if exists.
*/
if (auto range = simpleRange()) {
auto start = VisiblePosition { makeContainerOffsetPosition(range->start) };
auto end = start.next();
if (contains<ComposedTree>(*range, makeBoundaryPoint(end)))
return { boundsForRange(*makeSimpleRange(start, end)).center() };
}
return clickPointFromElementRect();
}
IntPoint AccessibilityObject::clickPoint()
{
// Headings are usually much wider than their textual content. If the mid point is used, often it can be wrong.
if (isHeading()) {
const auto& children = unignoredChildren();
if (children.size() == 1)
return children.first()->clickPoint();
}
if (isLink())
return linkClickPoint();
// use the default position unless this is an editable web area, in which case we use the selection bounds.
if (!isWebArea() || !canSetValueAttribute())
return clickPointFromElementRect();
return boundsForVisiblePositionRange(selection()).center();
}
IntPoint AccessibilityObject::clickPointFromElementRect() const
{
return roundedIntPoint(elementRect().center());
}
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().hasUsedAppearance())
obj->theme().inflateRectForControlRenderer(*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.
RefPtr actionElem = actionElement();
if (!actionElem)
return false;
if (auto* frame = actionElem->document().frame())
frame->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.
RefPtr<Element> hitTestElement;
RefPtr document = this->document();
if (document) {
constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active, HitTestRequest::Type::AccessibilityHitTest };
HitTestResult hitTestResult { clickPoint() };
document->hitTest(hitType, hitTestResult);
if (RefPtr innerNode = hitTestResult.innerNode()) {
if (RefPtr shadowHost = innerNode->shadowHost())
hitTestElement = WTFMove(shadowHost);
else if (RefPtr element = dynamicDowncast<Element>(*innerNode))
hitTestElement = WTFMove(element);
else
hitTestElement = innerNode->parentElement();
}
}
// Prefer the actionElement instead of this node, if the actionElement is inside this node.
RefPtr pressElement = this->element();
if (!pressElement || actionElem->isDescendantOf(*pressElement))
pressElement = WTFMove(actionElem);
ASSERT(pressElement);
// Prefer the hit test element, if it is inside the target element.
if (hitTestElement && hitTestElement->isDescendantOf(*pressElement))
pressElement = WTFMove(hitTestElement);
UserGestureIndicator gestureIndicator(IsProcessingUserGesture::Yes, document.get());
bool dispatchedEvent = false;
#if PLATFORM(IOS_FAMILY)
if (hasTouchEventListener())
dispatchedEvent = dispatchTouchEvent();
#endif
return dispatchedEvent || pressElement->accessKeyAction(true) || pressElement->dispatchSimulatedClick(nullptr, SendMouseUpDownEvents);
}
bool AccessibilityObject::dispatchTouchEvent()
{
#if ENABLE(IOS_TOUCH_EVENTS)
if (RefPtr frame = localMainFrame())
return frame->eventHandler().dispatchSimulatedTouchEvent(clickPoint());
#endif
return false;
}
LocalFrame* AccessibilityObject::frame() const
{
Node* node = this->node();
return node ? node->document().frame() : nullptr;
}
RefPtr<LocalFrame> AccessibilityObject::localMainFrame() const
{
if (RefPtr page = this->page())
return page->localMainFrame();
return nullptr;
}
Document* AccessibilityObject::topDocument() const
{
if (!document())
return nullptr;
return document()->mainFrameDocument();
}
RenderView* AccessibilityObject::topRenderer() const
{
if (auto* topDocument = this->topDocument())
return topDocument->renderView();
return nullptr;
}
unsigned AccessibilityObject::ariaLevel() const
{
int level = getIntegralAttribute(aria_levelAttr);
return level > 0 ? level : 0;
}
String AccessibilityObject::language() const
{
const auto& lang = getAttribute(langAttr);
if (!lang.isEmpty())
return lang;
if (isScrollView() && !parentObject()) {
// If this is the root, use the content language specified in the meta tag.
if (auto* document = this->document())
return document->contentLanguage();
}
// This object has no language of its own.
return nullAtom();
}
VisiblePosition AccessibilityObject::visiblePositionForPoint(const IntPoint& point) const
{
// convert absolute point to view coordinates
RenderView* renderView = topRenderer();
if (!renderView)
return VisiblePosition();
#if PLATFORM(MAC)
auto* frameView = &renderView->frameView();
#endif
Node* innerNode = nullptr;
// Locate the node containing the point
// FIXME: Remove this loop and instead add HitTestRequest::Type::AllowVisibleChildFrameContentOnly to the hit test request type.
LayoutPoint pointResult;
while (1) {
LayoutPoint pointToUse;
#if PLATFORM(MAC)
pointToUse = frameView->screenToContents(point);
#else
pointToUse = point;
#endif
constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active };
HitTestResult result { pointToUse };
renderView->document().hitTest(hitType, result);
innerNode = result.innerNode();
if (!innerNode)
return VisiblePosition();
RenderObject* renderer = innerNode->renderer();
if (!renderer)
return VisiblePosition();
pointResult = result.localPoint();
// done if hit something other than a widget
auto* renderWidget = dynamicDowncast<RenderWidget>(*renderer);
if (!renderWidget)
break;
// descend into widget (FRAME, IFRAME, OBJECT...)
auto* widget = renderWidget->widget();
auto* frameView = dynamicDowncast<LocalFrameView>(widget);
if (!frameView)
break;
auto* document = frameView->frame().document();
if (!document)
break;
renderView = document->renderView();
#if PLATFORM(MAC)
// FIXME: Can this be removed? This seems like a redundant assignment.
frameView = downcast<LocalFrameView>(widget);
#endif
}
return innerNode->renderer()->positionForPoint(pointResult, HitTestSource::User, nullptr);
}
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() == 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 { startPos, endPos };
}
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);
return { startPosition, endOfLine(prevVisiblePos) };
}
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 { 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 { };
return { startOfStyleRange(visiblePos), endOfStyleRange(visiblePos) };
}
// NOTE: Consider providing this utility method as AX API
VisiblePositionRange AccessibilityObject::visiblePositionRangeForRange(const CharacterRange& range) const
{
if (range.location + range.length > getLengthForTextRange())
return { };
auto startPosition = visiblePositionForIndex(range.location);
startPosition.setAffinity(Affinity::Downstream);
return { startPosition, visiblePositionForIndex(range.location + range.length) };
}
std::optional<SimpleRange> AccessibilityObject::rangeForCharacterRange(const CharacterRange& range) const
{
unsigned textLength = getLengthForTextRange();
if (range.location + range.length > textLength)
return std::nullopt;
// Avoid setting selection to uneditable parent node in FrameSelection::setSelectedRange. See webkit.org/b/206093.
if (!range.location && !range.length && !textLength)
return std::nullopt;
if (auto* cache = axObjectCache()) {
auto start = cache->characterOffsetForIndex(range.location, this);
auto end = cache->characterOffsetForIndex(range.location + range.length, this);
return cache->rangeForUnorderedCharacterOffsets(start, end);
}
return std::nullopt;
}
VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosition& visiblePosition) const
{
VisiblePosition startPosition = startOfLine(visiblePosition);
VisiblePosition endPosition = endOfLine(visiblePosition);
if (endPosition.isNull() || endPosition < startPosition) {
// When endOfLine fails to return a plausible result, try nextLineEndPosition, which is more robust, but ensure it doesn't return a result from a subsequent line.
VisiblePosition nextLineEnd = nextLineEndPosition(startPosition);
while (!nextLineEnd.isNull() && nextLineEnd > startPosition && !inSameLine(nextLineEnd, startPosition))
nextLineEnd = nextLineEnd.previous();
if (!nextLineEnd.isNull())
endPosition = nextLineEnd;
}
return { startPosition, endPosition };
}
#if PLATFORM(MAC)
AXTextMarkerRange AccessibilityObject::selectedTextMarkerRange() const
{
auto visibleRange = selectedVisiblePositionRange();
if (visibleRange.isNull())
return { };
return { visibleRange };
}
#endif // PLATFORM(MAC)
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 (!isRendererReplacedElement(replacedNode.renderer()) || replacedNode.isTextNode())
return false;
// create an AX object, but skip it if it is not supposed to be seen
if (CheckedPtr cache = replacedNode.renderer()->document().axObjectCache()) {
if (RefPtr axObject = cache->getOrCreate(replacedNode))
return !axObject->isIgnored();
}
return true;
}
#if PLATFORM(COCOA) && ENABLE(MODEL_ELEMENT)
Vector<RetainPtr<id>> AccessibilityObject::modelElementChildren()
{
RefPtr model = dynamicDowncast<HTMLModelElement>(node());
if (!model)
return { };
return model->accessibilityChildren();
}
#endif
// Finds a RenderListItem parent given a node.
static RenderListItem* renderListItemContainer(Node* node)
{
for (; node; node = node->parentNode()) {
if (auto* listItem = dynamicDowncast<RenderListItem>(node->renderer()))
return listItem;
}
return nullptr;
}
// Returns the text representing a list marker taking into account the position of the text in the line of text.
static StringView lineStartListMarkerText(const RenderListItem* listItem, const VisiblePosition& startVisiblePosition, std::optional<StringView> markerText = std::nullopt)
{
if (!listItem)
return { };
if (!markerText)
markerText = listItem->markerTextWithSuffix();
if (markerText->isEmpty())
return { };
// Only include the list marker if the range includes the line start (where the marker would be), and is in the same line as the marker.
if (!isStartOfLine(startVisiblePosition) || !inSameLine(startVisiblePosition, firstPositionInNode(listItem->element())))
return { };
return *markerText;
}
StringView AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, Position&& startPosition)
{
auto* listItem = renderListItemContainer(node);
if (!listItem)
return { };
// Creating a VisiblePosition and determining its relationship to a line of text can be expensive.
// Thus perform that determination only if we have some text to return.
auto markerText = listItem->markerTextWithSuffix();
if (markerText.isEmpty())
return { };
return lineStartListMarkerText(listItem, startPosition, markerText);
}
String AccessibilityObject::textContentPrefixFromListMarker() const
{
// The code below creates a VisiblePosition, which is very expensive. Only do this if there's
// any chance we're actually associated with a list marker.
if (!renderListItemContainer(node()))
return { };
// Get the attributed string for range (0, 1) and then delete the last character,
// in order to extract the list marker that was added as a prefix to the text content.
std::optional<SimpleRange> firstCharacterRange = rangeForCharacterRange({ 0, 1 });
if (firstCharacterRange) {
String firstCharacterText = AXTextMarkerRange { firstCharacterRange }.toString();
if (firstCharacterText.length() > 1)
return firstCharacterText.left(firstCharacterText.length() - 1);
}
return { };
}
String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange)
{
auto range = makeSimpleRange(visiblePositionRange);
if (!range)
return { };
StringBuilder builder;
TextIterator it = textIteratorIgnoringFullSizeKana(*range);
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.
builder.append(lineStartListMarkerText(renderListItemContainer(it.node()), visiblePositionRange.start));
it.appendTextToStringBuilder(builder);
} else {
// locate the node and starting offset for this replaced range
if (replacedNodeNeedsCharacter(*it.node()))
builder.append(objectReplacementCharacter);
}
}
return builder.toString();
}
VisiblePosition AccessibilityObject::nextLineEndPosition(const VisiblePosition& startPosition) const
{
if (startPosition.isNull())
return { };
// Move to the next position to ensure we move off a line end.
auto nextPosition = startPosition.next();
if (nextPosition.isNull())
return { };
auto lineEndPosition = endOfLine(nextPosition);
// As long as the position hasn't reached the end of the document, keep searching for a valid line
// end position. Skip past null positions, as there are cases like when the position is next to a
// floating object that'll return null for end of line. Also, in certain scenarios, like when one
// position is editable and the other isn't (e.g. in mixed-contenteditable-visible-character-range-hang.html),
// we may end up back at the same position we started at. This is never valid, so keep moving forward
// trying to find the next line end.
while ((lineEndPosition.isNull() || lineEndPosition == startPosition) && nextPosition.isNotNull()) {
nextPosition = nextPosition.next();
lineEndPosition = endOfLine(nextPosition);
}
return lineEndPosition;
}
std::optional<VisiblePosition> AccessibilityObject::previousLineStartPositionInternal(const VisiblePosition& visiblePosition) const
{
if (visiblePosition.isNull())
return std::nullopt;
// Make sure we move off of a line start.
auto previousVisiblePosition = visiblePosition.previous();
if (previousVisiblePosition.isNull())
return std::nullopt;
auto startPosition = startOfLine(previousVisiblePosition);
// As long as the position hasn't reached the beginning of the document, keep searching for a valid line start position.
// This avoids returning a null position when we shouldn't, like when a position is next to a floating object.
if (startPosition.isNull()) {
while (startPosition.isNull() && previousVisiblePosition.isNotNull()) {
previousVisiblePosition = previousVisiblePosition.previous();
startPosition = startOfLine(previousVisiblePosition);
}
} else
startPosition = updateAXLineStartForVisiblePosition(startPosition);
return startPosition;
}
bool AccessibilityObject::hasRowGroupTag() const
{
auto elementName = this->elementName();
return elementName == ElementName::HTML_thead || elementName == ElementName::HTML_tbody || elementName == ElementName::HTML_tfoot;
}
bool AccessibilityObject::isVisited() const
{
if (!isLink()) {
// Note that this isLink() check is necessary in addition to the RenderStyle::isLink() check below, as multiple
// renderers can share the same style, e.g. RenderTexts within a link take their parent's (the link) style.
return false;
}
auto* style = this->style();
if (!style || !style->isLink())
return false;
return style->insideLink() == InsideLink::InsideVisited;
}
// 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
{
auto* element = this->element();
return element && contentEditableAttributeIsEnabled(*element);
}
bool AccessibilityObject::dependsOnTextUnderElement() const
{
switch (role()) {
case AccessibilityRole::PopUpButton:
// Native popup buttons should not use their descendant's text as a title. That value is retrieved through stringValue().
if (hasElementName(ElementName::HTML_select))
break;
[[fallthrough]];
case AccessibilityRole::Summary:
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
case AccessibilityRole::Checkbox:
case AccessibilityRole::ListBoxOption:
#if !PLATFORM(COCOA)
// macOS does not expect native <li> elements to expose label information, it only expects leaf node elements to do that.
case AccessibilityRole::ListItem:
#endif
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::RadioButton:
case AccessibilityRole::Switch:
case AccessibilityRole::Tab:
return true;
default:
break;
}
// If it's focusable but it's not content editable or a known control type, then it will appear to
// the user as a single atomic object, so we should use its text as the default title.
if (isHeading() || isLink())
return true;
return isOutput();
}
bool AccessibilityObject::supportsReadOnly() const
{
auto role = this->role();
return role == AccessibilityRole::Checkbox
|| role == AccessibilityRole::ComboBox
|| role == AccessibilityRole::Grid
|| role == AccessibilityRole::GridCell
|| role == AccessibilityRole::ListBox
|| role == AccessibilityRole::MenuItemCheckbox
|| role == AccessibilityRole::MenuItemRadio
|| role == AccessibilityRole::RadioGroup
|| role == AccessibilityRole::SearchField
|| role == AccessibilityRole::Slider
|| role == AccessibilityRole::SpinButton
|| role == AccessibilityRole::Switch
|| role == AccessibilityRole::TextField
|| role == AccessibilityRole::TreeGrid
|| isColumnHeader()
|| isRowHeader()
|| isSecureField();
}
String AccessibilityObject::readOnlyValue() const
{
if (!hasAttribute(aria_readonlyAttr))
return ariaRoleAttribute() != AccessibilityRole::Unknown && supportsReadOnly() ? "false"_s : String();
return getAttribute(aria_readonlyAttr).string().convertToASCIILowercase();
}
bool AccessibilityObject::supportsCheckedState() const
{
auto role = this->role();
return isCheckboxOrRadio()
|| role == AccessibilityRole::MenuItemCheckbox
|| role == AccessibilityRole::MenuItemRadio
|| role == AccessibilityRole::Switch
|| isToggleButton();
}
bool AccessibilityObject::supportsAutoComplete() const
{
return (isComboBox() || isARIATextControl()) && hasAttribute(aria_autocompleteAttr);
}
String AccessibilityObject::explicitAutoCompleteValue() const
{
const AtomString& autoComplete = getAttribute(aria_autocompleteAttr);
if (equalLettersIgnoringASCIICase(autoComplete, "inline"_s)
|| equalLettersIgnoringASCIICase(autoComplete, "list"_s)
|| equalLettersIgnoringASCIICase(autoComplete, "both"_s))
return autoComplete;
return { };
}
bool AccessibilityObject::contentEditableAttributeIsEnabled(Element& element)
{
const AtomString& 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"_s);
}
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->isShadowIncludingInclusiveAncestorOf(node()) && !node()->isShadowIncludingInclusiveAncestorOf(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;
currentVisiblePos = previousLinePosition(currentVisiblePos, 0, HasEditableAXRole);
++lineCount;
} while (currentVisiblePos.isNotNull() && !(inSameLine(currentVisiblePos, savedVisiblePos)));
return lineCount;
}
// NOTE: Consider providing this utility method as AX API
CharacterRange AccessibilityObject::plainTextRangeForVisiblePositionRange(const VisiblePositionRange& positionRange) const
{
int index1 = index(positionRange.start);
int index2 = index(positionRange.end);
if (index1 < 0 || index2 < 0 || index1 > index2)
return { };
return CharacterRange(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?
CharacterRange AccessibilityObject::characterRangeForPoint(const IntPoint& point) const
{
int i = index(visiblePositionForPoint(point));
if (i < 0)
return { };
return { static_cast<uint64_t>(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.
CharacterRange 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));
}
void AccessibilityObject::updateBackingStore()
{
if (!axObjectCache())
return;
// Updating the layout may delete this object.
RefPtr<AccessibilityObject> protectedThis(this);
if (RefPtr document = this->document()) {
if (!Accessibility::inRenderTreeOrStyleUpdate(*document))
document->updateLayoutIgnorePendingStylesheets();
}
if (auto* cache = axObjectCache())
cache->performDeferredCacheUpdate(ForceLayout::Yes);
updateChildrenIfNecessary();
}
const AccessibilityScrollView* AccessibilityObject::ancestorAccessibilityScrollView(bool includeSelf) const
{
return downcast<AccessibilityScrollView>(Accessibility::findAncestor<AccessibilityObject>(*this, includeSelf, [] (const auto& object) {
return is<AccessibilityScrollView>(object);
}));
}
#if PLATFORM(COCOA)
RetainPtr<RemoteAXObjectRef> AccessibilityObject::remoteParent() const
{
auto* document = this->document();
auto* frame = document ? document->frame() : nullptr;
return frame ? frame->loader().client().accessibilityRemoteObject() : nullptr;
}
#endif
IntPoint AccessibilityObject::remoteFrameOffset() const
{
#if PLATFORM(COCOA)
auto* document = this->document();
auto* frame = document ? document->frame() : nullptr;
return frame ? frame->loader().client().accessibilityRemoteFrameOffset() : IntPoint();
#else
return IntPoint();
#endif
}
Document* AccessibilityObject::document() const
{
auto* frameView = documentFrameView();
if (!frameView)
return nullptr;
return frameView->frame().document();
}
RefPtr<Document> AccessibilityObject::protectedDocument() const
{
return document();
}
Page* AccessibilityObject::page() const
{
Document* document = this->document();
if (!document)
return nullptr;
return document->page();
}
LocalFrameView* AccessibilityObject::documentFrameView() const
{
RefPtr<const AccessibilityObject> object = this;
while (object) {
// Ascend until we find an ancestor with a valid renderer or node, from which we can
// actually get a frameview.
if (auto* axRenderObject = dynamicDowncast<AccessibilityRenderObject>(*object)) {
if (axRenderObject->renderer() || axRenderObject->node()) {
object = axRenderObject;
break;
}
} else if (auto* axNodeObject = dynamicDowncast<AccessibilityNodeObject>(*object); axNodeObject && axNodeObject->node()) {
object = axNodeObject;
break;
}
object = object->parentObject();
}
return object ? object->documentFrameView() : nullptr;
}
const AccessibilityObject::AccessibilityChildrenVector& AccessibilityObject::children(bool updateChildrenIfNeeded)
{
if (updateChildrenIfNeeded)
updateChildrenIfNecessary();
return m_children;
}
void AccessibilityObject::updateChildrenIfNecessary()
{
if (!childrenInitialized()) {
// 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_childrenInitialized = false;
}
AccessibilityObject* AccessibilityObject::anchorElementForNode(Node& node)
{
CheckedPtr renderer = node.renderer();
if (!renderer)
return nullptr;
WeakPtr cache = renderer->document().axObjectCache();
RefPtr axObject = cache ? cache->getOrCreate(renderer.get()) : nullptr;
auto* anchor = axObject ? axObject->anchorElement() : nullptr;
return anchor ? cache->getOrCreate(anchor->renderer()) : nullptr;
}
AccessibilityObject* AccessibilityObject::headingElementForNode(Node* node)
{
if (!node)
return nullptr;
RenderObject* renderObject = node->renderer();
if (!renderObject)
return nullptr;
AccessibilityObject* axObject = renderObject->document().axObjectCache()->getOrCreate(*renderObject);
return Accessibility::findAncestor<AccessibilityObject>(*axObject, true, [] (const AccessibilityObject& object) {
return object.role() == AccessibilityRole::Heading;
});
}
AXCoreObject::AccessibilityChildrenVector AccessibilityObject::disclosedRows()
{
AccessibilityChildrenVector result;
for (const auto& obj : unignoredChildren()) {
// Add tree items as the rows.
if (obj->role() == AccessibilityRole::TreeItem)
result.append(obj);
// If it's not a tree item, then descend into the group to find more tree items.
else
result.appendVector(obj->ariaTreeRows());
}
return result;
}
String AccessibilityObject::localizedActionVerb() const
{
#if !PLATFORM(IOS_FAMILY)
// FIXME: Need to add verbs for select elements.
static NeverDestroyed<const String> buttonAction(AXButtonActionVerb());
static NeverDestroyed<const String> textFieldAction(AXTextFieldActionVerb());
static NeverDestroyed<const String> radioButtonAction(AXRadioButtonActionVerb());
static NeverDestroyed<const String> checkedCheckboxAction(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 (role()) {
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
return buttonAction;
case AccessibilityRole::TextField:
case AccessibilityRole::TextArea:
return textFieldAction;
case AccessibilityRole::RadioButton:
return radioButtonAction;
case AccessibilityRole::Checkbox:
case AccessibilityRole::Switch:
return isChecked() ? checkedCheckboxAction : uncheckedCheckboxAction;
case AccessibilityRole::Link:
return linkAction;
case AccessibilityRole::PopUpButton:
return menuListAction;
case AccessibilityRole::MenuListPopup:
return menuListPopupAction;
case AccessibilityRole::ListItem:
return listItemAction;
default:
return nullAtom();
}
#else
return nullAtom();
#endif
}
String AccessibilityObject::actionVerb() const
{
#if !PLATFORM(IOS_FAMILY)
// FIXME: Need to add verbs for select elements.
switch (role()) {
case AccessibilityRole::Button:
case AccessibilityRole::ToggleButton:
return "press"_s;
case AccessibilityRole::TextField:
case AccessibilityRole::TextArea:
return "activate"_s;
case AccessibilityRole::RadioButton:
return "select"_s;
case AccessibilityRole::Checkbox:
case AccessibilityRole::Switch:
return isChecked() ? "uncheck"_s : "check"_s;
case AccessibilityRole::Link:
return "jump"_s;
case AccessibilityRole::PopUpButton:
case AccessibilityRole::MenuListPopup:
case AccessibilityRole::ListItem:
return "select"_s;
default:
break;
}
#endif
return { };
}
bool AccessibilityObject::ariaIsMultiline() const
{
return equalLettersIgnoringASCIICase(getAttribute(aria_multilineAttr), "true"_s);
}
String AccessibilityObject::explicitInvalidStatus() const
{
static NeverDestroyed<String> grammarValue = "grammar"_s;
static NeverDestroyed<String> falseValue = "false"_s;
static NeverDestroyed<String> spellingValue = "spelling"_s;
static NeverDestroyed<String> trueValue = "true"_s;
static NeverDestroyed<String> undefinedValue = "undefined"_s;
// aria-invalid can return false (default), grammar, spelling, or true.
auto ariaInvalid = getAttributeTrimmed(aria_invalidAttr);
if (ariaInvalid.isEmpty()) {
auto* htmlElement = dynamicDowncast<HTMLElement>(this->node());
if (auto* validatedFormListedElement = htmlElement ? htmlElement->asValidatedFormListedElement() : nullptr) {
// "willValidate" is true if the element is able to be validated.
if (validatedFormListedElement->willValidate() && !validatedFormListedElement->isValidFormControlElement())
return trueValue;
}
return { };
}
// 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::supportsCurrent() const
{
return hasAttribute(aria_currentAttr);
}
AccessibilityCurrentState AccessibilityObject::currentState() const
{
// aria-current can return false (default), true, page, step, location, date or time.
auto currentStateValue = getAttributeTrimmed(aria_currentAttr);
// If "false", empty, or missing, return false state.
if (currentStateValue.isEmpty() || currentStateValue == "false"_s)
return AccessibilityCurrentState::False;
if (currentStateValue == "page"_s)
return AccessibilityCurrentState::Page;
if (currentStateValue == "step"_s)
return AccessibilityCurrentState::Step;
if (currentStateValue == "location"_s)
return AccessibilityCurrentState::Location;
if (currentStateValue == "date"_s)
return AccessibilityCurrentState::Date;
if (currentStateValue == "time"_s)
return AccessibilityCurrentState::Time;
// Any value not included in the list of allowed values should be treated as "true".
return AccessibilityCurrentState::True;
}
bool AccessibilityObject::isModalDescendant(Node& modalNode) const
{
RefPtr node = this->node();
// 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.
// Determine descendancy by iterating the composed tree which inherently accounts for shadow roots and slots.
for (auto* ancestor = node.get(); ancestor; ancestor = ancestor->parentInComposedTree()) {
if (ancestor == &modalNode)
return true;
}
return false;
}
bool AccessibilityObject::isModalNode() const
{
if (AXObjectCache* cache = axObjectCache())
return node() && cache->modalNode() == node();
return false;
}
bool AccessibilityObject::ignoredFromModalPresence() const
{
// We shouldn't ignore the top node.
if (!node() || !node()->parentNode())
return false;
AXObjectCache* cache = axObjectCache();
if (!cache)
return false;
// modalNode is the current displayed modal dialog.
Node* modalNode = cache->modalNode();
if (!modalNode)
return false;
// We only want to ignore the objects within the same frame as the modal dialog.
if (modalNode->document().frame() != this->frame())
return false;
return !isModalDescendant(*modalNode);
}
bool AccessibilityObject::hasElementName(ElementName name) const
{
return elementName() == name;
}
bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
{
RefPtr element = this->element();
if (!element)
return false;
if (element->hasAttributeWithoutSynchronization(attribute))
return true;
if (auto* defaultARIA = element->customElementDefaultARIAIfExists()) {
// We do not want to use CustomElementDefaultARIA::hasAttribute here, as it returns true
// even if the author has set the attribute to null (e.g. this.internals.ariaValueNow = null),
// which should be treated the same as removing the attribute.
return !defaultARIA->valueForAttribute(*element, attribute).isNull();
}
return false;
}
const AtomString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const
{
RefPtr element = this->element();
return element ? element->attributeWithDefaultARIA(attribute) : nullAtom();
}
String AccessibilityObject::getAttributeTrimmed(const QualifiedName& attribute) const
{
const auto& rawValue = getAttribute(attribute);
if (rawValue.isEmpty())
return { };
auto value = rawValue.string();
return value.trim(isASCIIWhitespace).simplifyWhiteSpace(isASCIIWhitespace);
}
String AccessibilityObject::nameAttribute() const
{
return getAttribute(nameAttr);
}
int AccessibilityObject::getIntegralAttribute(const QualifiedName& attributeName) const
{
return parseHTMLInteger(getAttribute(attributeName)).value_or(0);
}
bool AccessibilityObject::replaceTextInRange(const String& replacementString, const CharacterRange& range)
{
// If this is being called on the web area, redirect it to be on the body, which will have a renderer associated with it.
if (RefPtr document = dynamicDowncast<Document>(node())) {
if (auto bodyObject = axObjectCache()->getOrCreate(document->body()))
return bodyObject->replaceTextInRange(replacementString, range);
return false;
}
// FIXME: This checks node() is an Element, but below we assume that means renderer()->node() is an element.
if (!renderer() || !is<Element>(node()))
return false;
auto& element = downcast<Element>(*renderer()->node());
// We should use the editor's insertText to mimic typing into the field.
// Also only do this when the field is in editing mode.
auto& frame = renderer()->frame();
if (element.shouldUseInputMethod()) {
frame.selection().setSelectedRange(rangeForCharacterRange(range), Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes);
frame.editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::No, Editor::SmartReplace::No);
return true;
}
if (RefPtr input = dynamicDowncast<HTMLInputElement>(element)) {
input->setRangeText(replacementString, range.location, range.length, emptyString());
return true;
}
if (RefPtr textarea = dynamicDowncast<HTMLTextAreaElement>(element)) {
textarea->setRangeText(replacementString, range.location, range.length, emptyString());
return true;
}
return false;
}
bool AccessibilityObject::insertText(const String& text)
{
AXTRACE(makeString("AccessibilityObject::insertText text = "_s, text));
if (!renderer())
return false;
RefPtr element = dynamicDowncast<Element>(node());
if (!element)
return false;
// Only try to insert text if the field is in editing mode (excluding secure fields, which we do still want to try to insert into).
if (!isSecureField() && !element->shouldUseInputMethod())
return false;
// Use Editor::insertText to mimic typing into the field.
auto& editor = renderer()->frame().editor();
return editor.insertText(text, nullptr);
}
using ARIARoleMap = HashMap<String, AccessibilityRole, ASCIICaseInsensitiveHash>;
using ARIAReverseRoleMap = HashMap<AccessibilityRole, String, DefaultHash<int>, WTF::UnsignedWithZeroKeyHashTraits<int>>;
static ARIARoleMap* gAriaRoleMap = nullptr;
static ARIAReverseRoleMap* gAriaReverseRoleMap = nullptr;
struct RoleEntry {
String ariaRole;
AccessibilityRole webcoreRole;
};
static void initializeRoleMap()
{
if (gAriaRoleMap)
return;
ASSERT(!gAriaReverseRoleMap);
const std::array roles {
RoleEntry { "alert"_s, AccessibilityRole::ApplicationAlert },
RoleEntry { "alertdialog"_s, AccessibilityRole::ApplicationAlertDialog },
RoleEntry { "application"_s, AccessibilityRole::WebApplication },
RoleEntry { "article"_s, AccessibilityRole::DocumentArticle },
RoleEntry { "banner"_s, AccessibilityRole::LandmarkBanner },
RoleEntry { "blockquote"_s, AccessibilityRole::Blockquote },
RoleEntry { "button"_s, AccessibilityRole::Button },
RoleEntry { "caption"_s, AccessibilityRole::Caption },
RoleEntry { "code"_s, AccessibilityRole::Code },
RoleEntry { "checkbox"_s, AccessibilityRole::Checkbox },
RoleEntry { "complementary"_s, AccessibilityRole::LandmarkComplementary },
RoleEntry { "contentinfo"_s, AccessibilityRole::LandmarkContentInfo },
RoleEntry { "deletion"_s, AccessibilityRole::Deletion },
RoleEntry { "dialog"_s, AccessibilityRole::ApplicationDialog },
RoleEntry { "directory"_s, AccessibilityRole::Directory },
// The 'doc-*' roles are defined the ARIA DPUB mobile: https://www.w3.org/TR/dpub-aam-1.0
// Editor's draft is currently at https://w3c.github.io/dpub-aam
RoleEntry { "doc-abstract"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-acknowledgments"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-afterword"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-appendix"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-backlink"_s, AccessibilityRole::Link },
RoleEntry { "doc-biblioentry"_s, AccessibilityRole::ListItem },
RoleEntry { "doc-bibliography"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-biblioref"_s, AccessibilityRole::Link },
RoleEntry { "doc-chapter"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-colophon"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-conclusion"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-cover"_s, AccessibilityRole::Image },
RoleEntry { "doc-credit"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-credits"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-dedication"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-endnote"_s, AccessibilityRole::ListItem },
RoleEntry { "doc-endnotes"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-epigraph"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-epilogue"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-errata"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-example"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-footnote"_s, AccessibilityRole::Footnote },
RoleEntry { "doc-foreword"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-glossary"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-glossref"_s, AccessibilityRole::Link },
RoleEntry { "doc-index"_s, AccessibilityRole::LandmarkNavigation },
RoleEntry { "doc-introduction"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-noteref"_s, AccessibilityRole::Link },
RoleEntry { "doc-notice"_s, AccessibilityRole::DocumentNote },
RoleEntry { "doc-pagebreak"_s, AccessibilityRole::Splitter },
RoleEntry { "doc-pagelist"_s, AccessibilityRole::LandmarkNavigation },
RoleEntry { "doc-part"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-preface"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-prologue"_s, AccessibilityRole::LandmarkDocRegion },
RoleEntry { "doc-pullquote"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-qna"_s, AccessibilityRole::TextGroup },
RoleEntry { "doc-subtitle"_s, AccessibilityRole::Heading },
RoleEntry { "doc-tip"_s, AccessibilityRole::DocumentNote },
RoleEntry { "doc-toc"_s, AccessibilityRole::LandmarkNavigation },
RoleEntry { "emphasis"_s, AccessibilityRole::Emphasis },
RoleEntry { "figure"_s, AccessibilityRole::Figure },
RoleEntry { "generic"_s, AccessibilityRole::Generic },
// The mappings for 'graphics-*' roles are defined in this spec: https://w3c.github.io/graphics-aam/
RoleEntry { "graphics-document"_s, AccessibilityRole::GraphicsDocument },
RoleEntry { "graphics-object"_s, AccessibilityRole::GraphicsObject },
RoleEntry { "graphics-symbol"_s, AccessibilityRole::GraphicsSymbol },
RoleEntry { "grid"_s, AccessibilityRole::Grid },
RoleEntry { "gridcell"_s, AccessibilityRole::GridCell },
RoleEntry { "table"_s, AccessibilityRole::Table },
RoleEntry { "cell"_s, AccessibilityRole::Cell },
RoleEntry { "columnheader"_s, AccessibilityRole::ColumnHeader },
RoleEntry { "combobox"_s, AccessibilityRole::ComboBox },
RoleEntry { "definition"_s, AccessibilityRole::Definition },
RoleEntry { "document"_s, AccessibilityRole::Document },
RoleEntry { "feed"_s, AccessibilityRole::Feed },
RoleEntry { "form"_s, AccessibilityRole::Form },
RoleEntry { "rowheader"_s, AccessibilityRole::RowHeader },
RoleEntry { "group"_s, AccessibilityRole::Group },
RoleEntry { "heading"_s, AccessibilityRole::Heading },
// The "image" role is synonymous with the "img" role. https://w3c.github.io/aria/#image
RoleEntry { "image"_s, AccessibilityRole::Image },
RoleEntry { "img"_s, AccessibilityRole::Image },
RoleEntry { "insertion"_s, AccessibilityRole::Insertion },
RoleEntry { "link"_s, AccessibilityRole::Link },
RoleEntry { "list"_s, AccessibilityRole::List },
RoleEntry { "listitem"_s, AccessibilityRole::ListItem },
RoleEntry { "listbox"_s, AccessibilityRole::ListBox },
RoleEntry { "log"_s, AccessibilityRole::ApplicationLog },
RoleEntry { "main"_s, AccessibilityRole::LandmarkMain },
RoleEntry { "marquee"_s, AccessibilityRole::ApplicationMarquee },
RoleEntry { "math"_s, AccessibilityRole::DocumentMath },
RoleEntry { "mark"_s, AccessibilityRole::Mark },
RoleEntry { "menu"_s, AccessibilityRole::Menu },
RoleEntry { "menubar"_s, AccessibilityRole::MenuBar },
RoleEntry { "menuitem"_s, AccessibilityRole::MenuItem },
RoleEntry { "menuitemcheckbox"_s, AccessibilityRole::MenuItemCheckbox },
RoleEntry { "menuitemradio"_s, AccessibilityRole::MenuItemRadio },
RoleEntry { "meter"_s, AccessibilityRole::Meter },
RoleEntry { "none"_s, AccessibilityRole::Presentational },
RoleEntry { "note"_s, AccessibilityRole::DocumentNote },
RoleEntry { "navigation"_s, AccessibilityRole::LandmarkNavigation },
RoleEntry { "option"_s, AccessibilityRole::ListBoxOption },
RoleEntry { "paragraph"_s, AccessibilityRole::Paragraph },
RoleEntry { "presentation"_s, AccessibilityRole::Presentational },
RoleEntry { "progressbar"_s, AccessibilityRole::ProgressIndicator },
RoleEntry { "radio"_s, AccessibilityRole::RadioButton },
RoleEntry { "radiogroup"_s, AccessibilityRole::RadioGroup },
RoleEntry { "region"_s, AccessibilityRole::LandmarkRegion },
RoleEntry { "row"_s, AccessibilityRole::Row },
RoleEntry { "rowgroup"_s, AccessibilityRole::RowGroup },
RoleEntry { "scrollbar"_s, AccessibilityRole::ScrollBar },
RoleEntry { "search"_s, AccessibilityRole::LandmarkSearch },
RoleEntry { "searchbox"_s, AccessibilityRole::SearchField },
RoleEntry { "separator"_s, AccessibilityRole::Splitter },
RoleEntry { "slider"_s, AccessibilityRole::Slider },
RoleEntry { "spinbutton"_s, AccessibilityRole::SpinButton },
RoleEntry { "status"_s, AccessibilityRole::ApplicationStatus },
RoleEntry { "subscript"_s, AccessibilityRole::Subscript },
RoleEntry { "suggestion"_s, AccessibilityRole::Suggestion },
RoleEntry { "superscript"_s, AccessibilityRole::Superscript },
RoleEntry { "strong"_s, AccessibilityRole::Strong },
RoleEntry { "switch"_s, AccessibilityRole::Switch },
RoleEntry { "tab"_s, AccessibilityRole::Tab },
RoleEntry { "tablist"_s, AccessibilityRole::TabList },
RoleEntry { "tabpanel"_s, AccessibilityRole::TabPanel },
RoleEntry { "text"_s, AccessibilityRole::StaticText },
RoleEntry { "textbox"_s, AccessibilityRole::TextField },
RoleEntry { "term"_s, AccessibilityRole::Term },
RoleEntry { "time"_s, AccessibilityRole::Time },
RoleEntry { "timer"_s, AccessibilityRole::ApplicationTimer },
RoleEntry { "toolbar"_s, AccessibilityRole::Toolbar },
RoleEntry { "tooltip"_s, AccessibilityRole::UserInterfaceTooltip },
RoleEntry { "tree"_s, AccessibilityRole::Tree },
RoleEntry { "treegrid"_s, AccessibilityRole::TreeGrid },
RoleEntry { "treeitem"_s, AccessibilityRole::TreeItem }
};
gAriaRoleMap = new ARIARoleMap;
gAriaReverseRoleMap = new ARIAReverseRoleMap;
size_t roleLength = std::size(roles);
for (size_t i = 0; i < roleLength; ++i) {
gAriaRoleMap->set(roles[i].ariaRole, roles[i].webcoreRole);
gAriaReverseRoleMap->set(enumToUnderlyingType(roles[i].webcoreRole), roles[i].ariaRole);
}
// Create specific synonyms for the computedRole which is used in WPT tests and the accessibility inspector.
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::DateTime), "textbox"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::TextArea), "textbox"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::DescriptionListDetail), "definition"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::DescriptionListTerm), "term"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::Details), "group"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::Image), "image"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::ListBoxOption), "option"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::MenuListOption), "option"_s);
gAriaReverseRoleMap->set(enumToUnderlyingType(AccessibilityRole::Presentational), "none"_s);
}
static ARIARoleMap& ariaRoleMap()
{
initializeRoleMap();
return *gAriaRoleMap;
}
static ARIAReverseRoleMap& reverseAriaRoleMap()
{
initializeRoleMap();
return *gAriaReverseRoleMap;
}
AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value)
{
return ariaRoleToWebCoreRole(value, [] (const AccessibilityRole&) {
return false;
});
}
AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value, NOESCAPE const Function<bool(const AccessibilityRole&)>& skipRole)
{
if (value.isNull() || value.isEmpty())
return AccessibilityRole::Unknown;
auto simplifiedValue = value.simplifyWhiteSpace(isASCIIWhitespace);
for (auto roleName : StringView(simplifiedValue).split(' ')) {
AccessibilityRole role = ariaRoleMap().get<ASCIICaseInsensitiveStringViewHashTranslator>(roleName);
if (skipRole(role))
continue;
if (enumToUnderlyingType(role))
return role;
}
return AccessibilityRole::Unknown;
}
String AccessibilityObject::computedRoleString() const
{
// FIXME: Need a few special cases that aren't in the RoleMap: option, etc. http://webkit.org/b/128296
auto role = this->role();
if (role == AccessibilityRole::Image && isIgnored())
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Presentational));
// We do compute a role string for block elements with author-provided roles.
if (ariaRoleAttribute() == AccessibilityRole::TextGroup
|| role == AccessibilityRole::Footnote
|| role == AccessibilityRole::GraphicsObject)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Group));
// We do not compute a role string for generic block elements with user-agent assigned roles.
if (role == AccessibilityRole::TextGroup)
return emptyString();
if (role == AccessibilityRole::GraphicsDocument)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Document));
if (role == AccessibilityRole::GraphicsSymbol)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Image));
if (role == AccessibilityRole::HorizontalRule)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Splitter));
if (role == AccessibilityRole::PopUpButton || role == AccessibilityRole::ToggleButton)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::Button));
if (role == AccessibilityRole::LandmarkDocRegion)
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::LandmarkRegion));
if (isColumnHeader())
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::ColumnHeader));
if (isRowHeader())
return reverseAriaRoleMap().get(enumToUnderlyingType(AccessibilityRole::RowHeader));
return reverseAriaRoleMap().get(enumToUnderlyingType(role));
}
void AccessibilityObject::updateRole()
{
auto previousRole = m_role;
m_role = determineAccessibilityRole();
if (previousRole != m_role) {
if (auto* cache = axObjectCache())
cache->handleRoleChanged(*this, previousRole);
}
}
SRGBA<uint8_t> AccessibilityObject::colorValue() const
{
return Color::black;
}
#if !PLATFORM(MAC)
String AccessibilityObject::subrolePlatformString() const
{
return String();
}
#endif
String AccessibilityObject::embeddedImageDescription() const
{
CheckedPtr renderImage = dynamicDowncast<RenderImage>(renderer());
if (!renderImage)
return { };
return renderImage->accessibilityDescription();
}
bool AccessibilityObject::supportsDatetimeAttribute() const
{
auto elementName = this->elementName();
return elementName == ElementName::HTML_ins || elementName == ElementName::HTML_del || elementName == ElementName::HTML_time;
}
String AccessibilityObject::datetimeAttributeValue() const
{
return getAttribute(datetimeAttr);
}
String AccessibilityObject::linkRelValue() const
{
return getAttribute(relAttr);
}
bool AccessibilityObject::isLoaded() const
{
auto* document = this->document();
return document && !document->parser();
}
bool AccessibilityObject::isInlineText() const
{
return is<RenderInline>(renderer());
}
bool AccessibilityObject::supportsKeyShortcuts() const
{
return hasAttribute(aria_keyshortcutsAttr);
}
String AccessibilityObject::keyShortcuts() const
{
return getAttribute(aria_keyshortcutsAttr);
}
Element* AccessibilityObject::element() const
{
return dynamicDowncast<Element>(node());
}
const RenderStyle* AccessibilityObject::style() const
{
if (auto* renderer = this->renderer()) {
if (auto* renderText = dynamicDowncast<RenderText>(*renderer)) {
// Trying to get the style from a RenderText that has no parent (e.g. because it hasn't been
// set yet, or it was destroyed as part of an in-progress render-tree update) will cause a
// crash because RenderTexts get their style from their parent.
return renderText->parent() ? &renderText->style() : nullptr;
}
return &renderer->style();
}
RefPtr element = this->element();
if (!element)
return nullptr;
// We cannot resolve style (as computedStyle() does) if we are downstream of an existing render tree
// update. Otherwise, a RELEASE_ASSERT preventing re-entrancy will be hit inside RenderTreeBuilder.
return RenderTreeBuilder::current() ? element->existingComputedStyle() : element->computedStyle();
}
bool AccessibilityObject::isValueAutofillAvailable() const
{
if (!isNativeTextControl())
return false;
RefPtr input = dynamicDowncast<HTMLInputElement>(node());
return input && (input->autofillAvailable() || input->autofillButtonType() != AutoFillButtonType::None);
}
AutoFillButtonType AccessibilityObject::valueAutofillButtonType() const
{
if (!isValueAutofillAvailable())
return AutoFillButtonType::None;
return downcast<HTMLInputElement>(*this->node()).autofillButtonType();
}
bool AccessibilityObject::isSelected() const
{
if (!renderer() && !node())
return false;
if (equalLettersIgnoringASCIICase(getAttribute(aria_selectedAttr), "true"_s))
return true;
if (isTabItem() && isTabItemSelected())
return true;
// Menu items are considered selectable by assistive technologies
if (isMenuItem()) {
if (isFocused())
return true;
WeakPtr parent = parentObjectUnignored();
return parent && parent->activeDescendant() == this;
}
return false;
}
bool AccessibilityObject::isTabItemSelected() const
{
if (!isTabItem() || (!renderer() && !node()))
return false;
WeakPtr node = this->node();
if (!node || !node->isElementNode())
return false;
// The ARIA spec says a tab item can also be selected if it is aria-labeled by a tabpanel
// that has keyboard focus inside of it, or if a tabpanel in its aria-controls list has KB
// focus inside of it.
auto* focusedElement = focusedUIElement();
if (!focusedElement)
return false;
auto* cache = axObjectCache();
if (!cache)
return false;
auto elements = elementsFromAttribute(aria_controlsAttr);
for (auto& element : elements) {
auto* tabPanel = cache->getOrCreate(element.ptr());
// A tab item should only control tab panels.
if (!tabPanel || tabPanel->role() != AccessibilityRole::TabPanel)
continue;
auto* checkFocusElement = focusedElement;
// Check if the focused element is a descendant of the element controlled by the tab item.
while (checkFocusElement) {
if (tabPanel == checkFocusElement)
return true;
checkFocusElement = checkFocusElement->parentObject();
}
}
return false;
}
unsigned AccessibilityObject::textLength() const
{
ASSERT(isTextControl());
return text().length();
}
std::optional<String> AccessibilityObject::textContent() const
{
if (!hasTextContent())
return std::nullopt;
std::optional<SimpleRange> range;
if (isTextControl())
range = rangeForCharacterRange({ 0, text().length() });
else
range = simpleRange();
if (range)
return AXTextMarkerRange { range }.toString();
return std::nullopt;
}
const String AccessibilityObject::placeholderValue() const
{
const AtomString& placeholder = getAttribute(placeholderAttr);
if (!placeholder.isEmpty())
return placeholder;
const AtomString& ariaPlaceholder = getAttribute(aria_placeholderAttr);
if (!ariaPlaceholder.isEmpty())
return ariaPlaceholder;
return nullAtom();
}
bool AccessibilityObject::supportsARIAAttributes() const
{
// This returns whether the element supports any global ARIA attributes.
return supportsLiveRegion()
|| supportsDragging()
|| supportsDropping()
|| supportsARIAOwns()
|| hasAttribute(aria_atomicAttr)
|| hasAttribute(aria_busyAttr)
|| hasAttribute(aria_controlsAttr)
|| hasAttribute(aria_currentAttr)
|| hasAttribute(aria_describedbyAttr)
|| hasAttribute(aria_detailsAttr)
|| hasAttribute(aria_disabledAttr)
|| hasAttribute(aria_errormessageAttr)
|| hasAttribute(aria_flowtoAttr)
|| hasAttribute(aria_haspopupAttr)
|| hasAttribute(aria_invalidAttr)
|| hasAttribute(aria_labelAttr)
|| hasAttribute(aria_labelledbyAttr)
|| hasAttribute(aria_relevantAttr);
}
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->isLocalFrameView()) {
if (CheckedPtr cache = axObjectCache())
return cache->getOrCreate(*widget)->accessibilityHitTest(IntPoint(point - widget->frameRect().location()));
}
if (widget && widget->isRemoteFrameView()) {
if (CheckedPtr cache = axObjectCache()) {
if (RefPtr remoteHostWidget = cache->getOrCreate(*widget)) {
remoteHostWidget->updateChildrenIfNecessary();
RefPtr scrollView = dynamicDowncast<AccessibilityScrollView>(*remoteHostWidget);
return scrollView ? scrollView->remoteFrame().get() : nullptr;
}
}
}
}
// Check if there are any mock elements that need to be handled.
for (const auto& child : const_cast<AccessibilityObject*>(this)->unignoredChildren(/* updateChildrenIfNeeded */ false)) {
if (auto* mockChild = dynamicDowncast<AccessibilityMockObject>(child.get()); mockChild && mockChild->elementRect().contains(point))
return mockChild->elementAccessibilityHitTest(point);
}
return const_cast<AccessibilityObject*>(this);
}
AXObjectCache* AccessibilityObject::axObjectCache() const
{
auto* document = this->document();
return document ? document->axObjectCache() : nullptr;
}
CommandType AccessibilityObject::commandType() const
{
return CommandType::Invalid;
}
AccessibilityObject* AccessibilityObject::focusedUIElement() const
{
auto* page = this->page();
auto* axObjectCache = this->axObjectCache();
return page && axObjectCache ? axObjectCache->focusedObjectForPage(page) : nullptr;
}
void AccessibilityObject::setSelectedRows(AccessibilityChildrenVector&& selectedRows)
{
// Setting selected only makes sense in trees and tables (and tree-tables).
auto role = this->role();
if (role != AccessibilityRole::Tree && role != AccessibilityRole::TreeGrid && role != AccessibilityRole::Table && role != AccessibilityRole::Grid)
return;
bool isMulti = isMultiSelectable();
for (const auto& selectedRow : selectedRows) {
// FIXME: At the time of writing, setSelected is only implemented for AccessibilityListBoxOption and AccessibilityMenuListOption which are unlikely to be "rows", so this function probably isn't doing anything useful.
selectedRow->setSelected(true);
if (isMulti)
break;
}
}
void AccessibilityObject::setFocused(bool focus)
{
if (focus) {
// Ensure that the view is focused and active, otherwise, any attempt to set focus to an object inside it will fail.
auto* frame = document() ? document()->frame() : nullptr;
if (frame && frame->selection().isFocusedAndActive())
return; // Nothing to do, already focused and active.
auto* page = document() ? document()->page() : nullptr;
if (!page)
return;
page->chrome().client().focus();
// Reset the page pointer in case ChromeClient::focus() caused a side effect that invalidated our old one.
page = document() ? document()->page() : nullptr;
if (!page)
return;
#if PLATFORM(IOS_FAMILY)
// Mark the page as focused so the focus ring can be drawn immediately. The page is also marked
// as focused as part assistiveTechnologyMakeFirstResponder, but that requires some back-and-forth
// IPC between the web and UI processes, during which we can miss the drawing of the focus ring for the
// first focused element. Making the page focused is a requirement for making the page selection focused.
// This is iOS only until there's a demonstrated need for this preemptive focus on other platforms.
if (!page->focusController().isFocused())
page->checkedFocusController()->setFocused(true);
// Reset the page pointer in case FocusController::setFocused(true) caused a side effect that invalidated our old one.
page = document() ? document()->page() : nullptr;
if (!page)
return;
#endif
#if PLATFORM(COCOA)
auto* frameView = documentFrameView();
if (!frameView)
return;
// Legacy WebKit1 case.
if (frameView->platformWidget())
page->chrome().client().makeFirstResponder((NSResponder *)frameView->platformWidget());
#endif
#if PLATFORM(MAC)
else
page->chrome().client().assistiveTechnologyMakeFirstResponder();
// WebChromeClient::assistiveTechnologyMakeFirstResponder (the WebKit2 codepath) is intentionally
// not called on iOS because stealing first-respondership causes issues such as:
// 1. VoiceOver Speak Screen focus erroneously jumping to the top of the page when encountering an embedded WKWebView
// 2. Third-party apps relying on WebKit to not steal first-respondership (https://bugs.webkit.org/show_bug.cgi?id=249976)
#endif
}
}
AccessibilitySortDirection AccessibilityObject::sortDirection() const
{
// Only row and column headers are allowed to have aria-sort.
// https://w3c.github.io/aria/#aria-sort
if (!isColumnHeader() && !isRowHeader())
return AccessibilitySortDirection::Invalid;
auto& sortAttribute = getAttribute(aria_sortAttr);
if (sortAttribute.isNull())
return AccessibilitySortDirection::None;
if (equalLettersIgnoringASCIICase(sortAttribute, "ascending"_s))
return AccessibilitySortDirection::Ascending;
if (equalLettersIgnoringASCIICase(sortAttribute, "descending"_s))
return AccessibilitySortDirection::Descending;
if (equalLettersIgnoringASCIICase(sortAttribute, "other"_s))
return AccessibilitySortDirection::Other;
return AccessibilitySortDirection::None;
}
bool AccessibilityObject::supportsHasPopup() const
{
return hasAttribute(aria_haspopupAttr) || isComboBox();
}
String AccessibilityObject::explicitPopupValue() const
{
auto& hasPopup = getAttribute(aria_haspopupAttr);
if (hasPopup.isEmpty()) {
// In ARIA 1.1, the implicit value for datalists became "listbox."
if (hasDatalist())
return "listbox"_s;
return { };
}
for (auto& value : { "menu"_s, "listbox"_s, "tree"_s, "grid"_s, "dialog"_s }) {
// FIXME: Should fix ambiguity so we don't have to write "characters", but also don't create/destroy a String when passing an ASCIILiteral to equalIgnoringASCIICase.
if (equalIgnoringASCIICase(hasPopup, value))
return value;
}
// aria-haspopup specification states that true must be treated as menu.
if (equalLettersIgnoringASCIICase(hasPopup, "true"_s))
return "menu"_s;
return { };
}
bool AccessibilityObject::hasDatalist() const
{
RefPtr input = dynamicDowncast<HTMLInputElement>(element());
return input && input->hasDataList();
}
bool AccessibilityObject::supportsSetSize() const
{
return hasAttribute(aria_setsizeAttr);
}
bool AccessibilityObject::supportsPosInSet() const
{
return hasAttribute(aria_posinsetAttr);
}
int AccessibilityObject::setSize() const
{
// https://github.com/w3c/aria/pull/2341
// When aria-setsize isn't a positive integer (greater than or equal to 1), its value should be indeterminate, i.e., -1.
int setSize = getIntegralAttribute(aria_setsizeAttr);
return (setSize >= 1) ? setSize : -1;
}
int AccessibilityObject::posInSet() const
{
// https://github.com/w3c/aria/pull/2341
// When aria-posinset isn't a positive integer (greater than or equal to 1), its value should be 1.
int posInSet = getIntegralAttribute(aria_posinsetAttr);
return (posInSet >= 1) ? posInSet : 1;
}
String AccessibilityObject::identifierAttribute() const
{
return getAttribute(idAttr);
}
Vector<String> AccessibilityObject::classList() const
{
auto* element = this->element();
if (!element)
return { };
auto& domClassList = element->classList();
Vector<String> classList;
unsigned length = domClassList.length();
classList.reserveInitialCapacity(length);
for (unsigned k = 0; k < length; k++)
classList.append(domClassList.item(k).string());
return classList;
}
String AccessibilityObject::extendedDescription() const
{
auto describedBy = ariaDescribedByAttribute();
if (!describedBy.isEmpty())
return describedBy;
return getAttribute(HTMLNames::aria_descriptionAttr);
}
bool AccessibilityObject::supportsPressed() const
{
const AtomString& expanded = getAttribute(aria_pressedAttr);
return equalLettersIgnoringASCIICase(expanded, "true"_s) || equalLettersIgnoringASCIICase(expanded, "false"_s);
}
bool AccessibilityObject::supportsExpanded() const
{
// commandfor attribute takes precedence over popovertarget attribute.
if (RefPtr targetElement = commandForElement()) {
// If the target element is a popover then check command is popover related.
if (targetElement->popoverState() != PopoverState::None) {
switch (commandType()) {
// Expose an expanded state if the command is valid for a popover.
case CommandType::ShowPopover:
case CommandType::HidePopover:
case CommandType::TogglePopover:
return true;
case CommandType::Invalid:
case CommandType::Custom:
case CommandType::ShowModal:
case CommandType::Close:
case CommandType::RequestClose:
break;
default:
ASSERT_NOT_REACHED();
break;
}
}
} else if (popoverTargetElement())
return true;
if (is<HTMLDetailsElement>(node()))
return true;
auto hasValidAriaExpandedValue = [this] () -> bool {
// Undefined values should not result in this attribute being exposed to ATs according to ARIA.
const AtomString& expanded = getAttribute(aria_expandedAttr);
return equalLettersIgnoringASCIICase(expanded, "true"_s) || equalLettersIgnoringASCIICase(expanded, "false"_s);
};
if (isColumnHeader() || isRowHeader())
return hasValidAriaExpandedValue();
switch (role()) {
case AccessibilityRole::Details:
return true;
case AccessibilityRole::Button:
case AccessibilityRole::Checkbox:
case AccessibilityRole::ComboBox:
case AccessibilityRole::GridCell:
case AccessibilityRole::Link:
case AccessibilityRole::MenuItem:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::Row:
case AccessibilityRole::Switch:
case AccessibilityRole::Tab:
case AccessibilityRole::TreeItem:
case AccessibilityRole::WebApplication:
return hasValidAriaExpandedValue();
default:
return false;
}
}
double AccessibilityObject::loadingProgress() const
{
if (isLoaded())
return 1.0;
auto* page = this->page();
if (!page)
return 0.0;
return page->progress().estimatedProgress();
}
bool AccessibilityObject::isExpanded() const
{
if (RefPtr details = dynamicDowncast<HTMLDetailsElement>(node()))
return details->hasAttribute(openAttr);
// Summary element should use its details parent's expanded status.
if (isSummary()) {
if (const AccessibilityObject* parent = Accessibility::findAncestor<AccessibilityObject>(*this, false, [] (const AccessibilityObject& object) {
return is<HTMLDetailsElement>(object.node());
}))
return parent->isExpanded();
}
if (supportsExpanded()) {
if (RefPtr commandForElement = this->commandForElement())
return commandForElement->isPopoverShowing();
if (RefPtr popoverTargetElement = this->popoverTargetElement())
return popoverTargetElement->isPopoverShowing();
return equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"_s);
}
return false;
}
bool AccessibilityObject::supportsChecked() const
{
switch (role()) {
case AccessibilityRole::Checkbox:
case AccessibilityRole::MenuItemCheckbox:
case AccessibilityRole::MenuItemRadio:
case AccessibilityRole::RadioButton:
case AccessibilityRole::Switch:
return true;
default:
return false;
}
}
bool AccessibilityObject::supportsRowCountChange() const
{
switch (role()) {
case AccessibilityRole::Tree:
case AccessibilityRole::TreeGrid:
case AccessibilityRole::Grid:
case AccessibilityRole::Table:
return true;
default:
return false;
}
}
AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const
{
// If this is a real checkbox or radio button, AccessibilityNodeObject 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 AtomString& ariaPressed = getAttribute(aria_pressedAttr);
if (equalLettersIgnoringASCIICase(ariaPressed, "true"_s))
return AccessibilityButtonState::On;
if (equalLettersIgnoringASCIICase(ariaPressed, "mixed"_s))
return AccessibilityButtonState::Mixed;
return AccessibilityButtonState::Off;
}
const AtomString& result = getAttribute(aria_checkedAttr);
if (equalLettersIgnoringASCIICase(result, "true"_s))
return AccessibilityButtonState::On;
if (equalLettersIgnoringASCIICase(result, "mixed"_s)) {
// ARIA says that radio, menuitemradio, and switch elements must NOT expose button state mixed.
AccessibilityRole ariaRole = ariaRoleAttribute();
if (ariaRole == AccessibilityRole::RadioButton || ariaRole == AccessibilityRole::MenuItemRadio || ariaRole == AccessibilityRole::Switch)
return AccessibilityButtonState::Off;
return AccessibilityButtonState::Mixed;
}
return AccessibilityButtonState::Off;
}
HashMap<String, AXEditingStyleValueVariant> AccessibilityObject::resolvedEditingStyles() const
{
auto document = this->document();
if (!document)
return { };
auto selectionStyle = EditingStyle::styleAtSelectionStart(document->selection().selection());
if (!selectionStyle)
return { };
HashMap<String, AXEditingStyleValueVariant> styles;
styles.add("bold"_s, selectionStyle->hasStyle(CSSPropertyFontWeight, "bold"_s));
styles.add("italic"_s, selectionStyle->hasStyle(CSSPropertyFontStyle, "italic"_s));
styles.add("underline"_s, selectionStyle->hasStyle(CSSPropertyWebkitTextDecorationsInEffect, "underline"_s));
styles.add("fontsize"_s, selectionStyle->legacyFontSize(*document));
return styles;
}
// 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->isScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect());
if (!outerRect.intersects(innerRect)) {
isOnscreen = false;
break;
}
}
return isOnscreen;
}
void AccessibilityObject::scrollToMakeVisible() const
{
scrollToMakeVisible({ SelectionRevealMode::Reveal, ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded, ShouldAllowCrossOriginScrolling::Yes });
}
void AccessibilityObject::scrollToMakeVisible(const ScrollRectToVisibleOptions& options) const
{
if (isScrollView() && parentObject())
parentObject()->scrollToMakeVisible();
if (auto* renderer = this->renderer())
LocalFrameView::scrollRectToVisible(boundingBoxRect(), *renderer, false, options);
}
void AccessibilityObject::scrollToMakeVisibleWithSubFocus(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 newElementRect = snappedIntRect(elementRect());
IntRect scrollParentRect = snappedIntRect(scrollParent->elementRect());
subfocus.move(newElementRect.x(), newElementRect.y());
subfocus.move(-scrollParentRect.x(), -scrollParentRect.y());
// Recursively make sure the scroll parent itself is visible.
if (scrollParent->parentObject())
scrollParent->scrollToMakeVisibleWithSubFocus(WTFMove(subfocus));
}
FloatRect AccessibilityObject::unobscuredContentRect() const
{
auto document = this->document();
if (!document || !document->view())
return { };
return FloatRect(snappedIntRect(document->view()->unobscuredContentRect()));
}
void AccessibilityObject::scrollToGlobalPoint(IntPoint&& point) 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;
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->isScrollView() ? 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->isScrollView())
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->isScrollView() && !inner->isScrollView()) {
// 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->isScrollView()) {
// 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 ScrollByPageDirection::Right: {
int scrollAmount = scrollVisibleRect.size().width();
int newX = scrollPosition.x() - scrollAmount;
newScrollPosition.setX(std::max(newX, 0));
break;
}
case ScrollByPageDirection::Left: {
int scrollAmount = scrollVisibleRect.size().width();
int newX = scrollAmount + scrollPosition.x();
int maxX = scrollSize.width() - scrollAmount;
newScrollPosition.setX(std::min(newX, maxX));
break;
}
case ScrollByPageDirection::Up: {
int scrollAmount = scrollVisibleRect.size().height();
int newY = scrollPosition.y() - scrollAmount;
newScrollPosition.setY(std::max(newY, 0));
break;
}
case ScrollByPageDirection::Down: {
int scrollAmount = scrollVisibleRect.size().height();
int newY = scrollAmount + scrollPosition.y();
int maxY = scrollSize.height() - scrollAmount;
newScrollPosition.setY(std::min(newY, maxY));
break;
}
}
if (newScrollPosition != scrollPosition) {
scrollParent->scrollTo(newScrollPosition);
protectedDocument()->updateLayoutIgnorePendingStylesheets();
return true;
}
return false;
}
void AccessibilityObject::setLastKnownIsIgnoredValue(bool isIgnored)
{
m_lastKnownIsIgnoredValue = isIgnored ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject;
}
bool AccessibilityObject::ignoredFromPresentationalRole() const
{
return role() == AccessibilityRole::Presentational || inheritsPresentationalRole();
}
bool AccessibilityObject::includeIgnoredInCoreTree() const
{
#if ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE)
RefPtr document = this->document();
return document ? document->settings().includeIgnoredInCoreAXTree() : false;
#else
return false;
#endif // ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE)
}
bool AccessibilityObject::pressedIsPresent() const
{
return !getAttribute(aria_pressedAttr).isEmpty();
}
TextIteratorBehaviors AccessibilityObject::textIteratorBehaviorForTextRange() const
{
TextIteratorBehaviors behaviors { TextIteratorBehavior::IgnoresStyleVisibility, TextIteratorBehavior::IgnoresFullSizeKana };
#if USE(ATSPI)
// We need to emit replaced elements for ATSPI, and present
// them with the 'object replacement character' (0xFFFC).
behaviors.add(TextIteratorBehavior::EmitsObjectReplacementCharacters);
#endif
return behaviors;
}
TextIterator AccessibilityObject::textIteratorIgnoringFullSizeKana(const SimpleRange& range)
{
return TextIterator(range, { TextIteratorBehavior::IgnoresFullSizeKana });
}
AccessibilityRole AccessibilityObject::buttonRoleType() const
{
// If aria-pressed is present, then it should be exposed as a toggle button.
// https://www.w3.org/TR/wai-aria#aria-pressed
if (pressedIsPresent())
return AccessibilityRole::ToggleButton;
if (selfOrAncestorLinkHasPopup())
return AccessibilityRole::PopUpButton;
// We don't contemplate AccessibilityRole::RadioButton, as it depends on the input type.
return AccessibilityRole::Button;
}
std::optional<InputType::Type> AccessibilityObject::inputType() const
{
RefPtr input = dynamicDowncast<HTMLInputElement>(node());
RefPtr inputType = input ? input->inputType() : nullptr;
return inputType ? std::optional(inputType->type()) : std::nullopt;
}
bool AccessibilityObject::isIgnoredByDefault() const
{
return defaultObjectInclusion() == AccessibilityObjectInclusion::IgnoreObject;
}
bool AccessibilityObject::isARIAHidden() const
{
if (isFocused())
return false;
auto* node = this->node();
auto* element = dynamicDowncast<Element>(node);
AtomString tag = element ? element->localName() : nullAtom();
// https://github.com/w3c/aria/pull/1880
// To prevent authors from hiding all content from assistive technology users, do not respect
// aria-hidden on html, body, or document-root svg elements.
if (tag == bodyTag || tag == htmlTag || (tag == SVGNames::svgTag && !element->parentNode()))
return false;
if (auto* assignedSlot = node ? node->assignedSlot() : nullptr) {
if (equalLettersIgnoringASCIICase(assignedSlot->attributeWithDefaultARIA(aria_hiddenAttr), "true"_s))
return true;
}
return element && equalLettersIgnoringASCIICase(element->attributeWithDefaultARIA(aria_hiddenAttr), "true"_s);
}
// ARIA component of hidden definition.
// https://www.w3.org/TR/wai-aria/#dfn-hidden
bool AccessibilityObject::isAXHidden() const
{
if (isFocused())
return false;
return Accessibility::findAncestor<AccessibilityObject>(*this, true, [] (const auto& object) {
return object.isARIAHidden();
}) != nullptr;
}
bool AccessibilityObject::isRenderHidden() const
{
return WebCore::isRenderHidden(style());
}
bool AccessibilityObject::isShowingValidationMessage() const
{
if (RefPtr element = this->element()) {
if (auto* listedElement = element->asValidatedFormListedElement())
return listedElement->isShowingValidationMessage();
}
return false;
}
String AccessibilityObject::validationMessage() const
{
if (RefPtr element = this->element()) {
if (auto* listedElement = element->asValidatedFormListedElement())
return listedElement->validationMessage();
}
return String();
}
AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const
{
if (const auto* style = this->style()) {
if (style->effectiveInert())
return AccessibilityObjectInclusion::IgnoreObject;
if (isVisibilityHidden(*style))
return AccessibilityObjectInclusion::IgnoreObject;
}
bool useParentData = !m_isIgnoredFromParentData.isNull();
if (useParentData && (m_isIgnoredFromParentData.isAXHidden || m_isIgnoredFromParentData.isPresentationalChildOfAriaRole))
return AccessibilityObjectInclusion::IgnoreObject;
if (isARIAHidden() || isWithinHiddenWebArea())
return AccessibilityObjectInclusion::IgnoreObject;
bool ignoreARIAHidden = isFocused();
if (Accessibility::findAncestor<AccessibilityObject>(*this, false, [&] (const auto& object) {
const auto* style = object.style();
if (style && style->display() == DisplayType::None) {
// We don't want to use AccessibilityObject::isRenderHidden(), as that also checks and returns true
// for visibility:hidden, which would be wrong if |this| has a visibility:visible ancestor before
// this visibility:hidden ancestor (visibility:visible cancels out visibility:hidden).
//
// We check the isVisibilityHidden at the top of this method, so that covers us as far as visibility goes.
return true;
}
return (!ignoreARIAHidden && object.isARIAHidden()) || object.ariaRoleHasPresentationalChildren() || !object.canHaveChildren();
}))
return AccessibilityObjectInclusion::IgnoreObject;
// Include <dialog> elements and elements with role="dialog".
if (role() == AccessibilityRole::ApplicationDialog)
return AccessibilityObjectInclusion::IncludeObject;
return accessibilityPlatformIncludesObject();
}
bool AccessibilityObject::isWithinHiddenWebArea() const
{
RefPtr webArea = this->containingWebArea();
CheckedPtr renderView = webArea ? dynamicDowncast<RenderView>(webArea->renderer()) : nullptr;
CheckedPtr frameRenderer = renderView ? renderView->frameView().frame().ownerRenderer() : nullptr;
while (frameRenderer) {
const auto& style = frameRenderer->style();
if (isVisibilityHidden(style) || style.effectiveInert())
return true;
renderView = frameRenderer->document().renderView();
frameRenderer = renderView ? renderView->frameView().frame().ownerRenderer() : nullptr;
}
return false;
}
bool AccessibilityObject::isIgnored() const
{
AXComputedObjectAttributeCache* attributeCache = nullptr;
auto* axObjectCache = this->axObjectCache();
if (axObjectCache)
attributeCache = axObjectCache->computedObjectAttributeCache();
if (attributeCache) {
AccessibilityObjectInclusion ignored = attributeCache->getIgnored(objectID());
switch (ignored) {
case AccessibilityObjectInclusion::IgnoreObject:
return true;
case AccessibilityObjectInclusion::IncludeObject:
return false;
case AccessibilityObjectInclusion::DefaultBehavior:
break;
}
}
bool ignored = isIgnoredWithoutCache(axObjectCache);
// Refetch the attribute cache in case it was enabled as part of computing isIgnored.
if (axObjectCache && (attributeCache = axObjectCache->computedObjectAttributeCache()))
attributeCache->setIgnored(objectID(), ignored ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject);
return ignored;
}
bool AccessibilityObject::isIgnoredWithoutCache(AXObjectCache* cache) const
{
// If we are in the midst of retrieving the current modal node, we only need to consider whether the object
// is inherently ignored via computeIsIgnored. Also, calling ignoredFromModalPresence
// in this state would cause infinite recursion.
bool ignored = cache && cache->isRetrievingCurrentModalNode() ? false : ignoredFromModalPresence();
if (!ignored)
ignored = computeIsIgnored();
auto previousLastKnownIsIgnoredValue = m_lastKnownIsIgnoredValue;
const_cast<AccessibilityObject*>(this)->setLastKnownIsIgnoredValue(ignored);
if (cache) {
bool becameUnignored = previousLastKnownIsIgnoredValue == AccessibilityObjectInclusion::IgnoreObject && !ignored;
bool becameIgnored = !becameUnignored && previousLastKnownIsIgnoredValue == AccessibilityObjectInclusion::IncludeObject && ignored;
#if ENABLE(ACCESSIBILITY_ISOLATED_TREE)
if (becameIgnored)
cache->objectBecameIgnored(*this);
else if (becameUnignored)
cache->objectBecameUnignored(*this);
#endif // ENABLE(ACCESSIBILITY_ISOLATED_TREE)
if (becameUnignored || becameIgnored) {
// FIXME: We should not have to submit a children-changed when ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE), but that causes a few failing
// tests. We should fix that or remove this comment before enabling ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) by default for any port.
cache->childrenChanged(parentObject());
}
}
return ignored;
}
Vector<Ref<Element>> AccessibilityObject::elementsFromAttribute(const QualifiedName& attribute) const
{
RefPtr element = dynamicDowncast<Element>(node());
if (!element)
return { };
if (auto elementsFromAttribute = element->elementsArrayForAttributeInternal(attribute))
return elementsFromAttribute.value();
if (auto* defaultARIA = element->customElementDefaultARIAIfExists())
return defaultARIA->elementsForAttribute(*element, attribute);
return { };
}
#if PLATFORM(COCOA)
bool AccessibilityObject::preventKeyboardDOMEventDispatch() const
{
auto* frame = this->frame();
return frame && frame->settings().preventKeyboardDOMEventDispatch();
}
void AccessibilityObject::setPreventKeyboardDOMEventDispatch(bool on)
{
auto* frame = this->frame();
if (!frame)
return;
frame->settings().setPreventKeyboardDOMEventDispatch(on);
}
#endif
AccessibilityObject* AccessibilityObject::radioGroupAncestor() const
{
return Accessibility::findAncestor<AccessibilityObject>(*this, false, [] (const AccessibilityObject& object) {
return object.isRadioGroup();
});
}
ElementName AccessibilityObject::elementName() const
{
auto* element = this->element();
return element ? element->elementName() : ElementName::Unknown;
}
bool AccessibilityObject::isStyleFormatGroup() const
{
if (isCode())
return true;
auto elementName = this->elementName();
return elementName == ElementName::HTML_kbd || elementName == ElementName::HTML_code
|| elementName == ElementName::HTML_pre || elementName == ElementName::HTML_samp
|| elementName == ElementName::HTML_var || elementName == ElementName::HTML_cite
|| elementName == ElementName::HTML_ins || elementName == ElementName::HTML_del
|| elementName == ElementName::HTML_sup || elementName == ElementName::HTML_sub;
}
bool AccessibilityObject::isFigureElement() const
{
return elementName() == ElementName::HTML_figure;
}
bool AccessibilityObject::isKeyboardFocusable() const
{
if (auto element = this->element())
return element->isFocusable();
return false;
}
bool AccessibilityObject::isOutput() const
{
return elementName() == ElementName::HTML_output;
}
bool AccessibilityObject::isContainedBySecureField() const
{
Node* node = this->node();
if (!node)
return false;
if (ariaRoleAttribute() != AccessibilityRole::Unknown)
return false;
RefPtr input = dynamicDowncast<HTMLInputElement>(node->shadowHost());
return input && input->isSecureField();
}
AXCoreObject::AccessibilityChildrenVector AccessibilityObject::relatedObjects(AXRelation relation) const
{
auto* cache = axObjectCache();
if (!cache)
return { };
auto relatedObjectIDs = cache->relatedObjectIDsFor(*this, relation);
if (!relatedObjectIDs)
return { };
return cache->objectsForIDs(*relatedObjectIDs);
}
bool AccessibilityObject::shouldFocusActiveDescendant() const
{
switch (ariaRoleAttribute()) {
case AccessibilityRole::Group:
case AccessibilityRole::ListBox:
case AccessibilityRole::Menu:
case AccessibilityRole::MenuBar:
case AccessibilityRole::RadioGroup:
case AccessibilityRole::Row:
case AccessibilityRole::PopUpButton:
case AccessibilityRole::Meter:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::Toolbar:
case AccessibilityRole::Tree:
case AccessibilityRole::Grid:
/* FIXME: replace these with actual roles when they are added to AccessibilityRole
composite
alert
alertdialog
status
timer
*/
return true;
default:
return false;
}
}
bool AccessibilityObject::ariaRoleHasPresentationalChildren() const
{
switch (ariaRoleAttribute()) {
case AccessibilityRole::Button:
case AccessibilityRole::Slider:
case AccessibilityRole::Image:
case AccessibilityRole::ProgressIndicator:
case AccessibilityRole::SpinButton:
return true;
default:
return false;
}
}
void AccessibilityObject::setIsIgnoredFromParentDataForChild(AccessibilityObject& child)
{
AccessibilityIsIgnoredFromParentData result = AccessibilityIsIgnoredFromParentData(this);
if (!m_isIgnoredFromParentData.isNull()) {
result.isAXHidden = (m_isIgnoredFromParentData.isAXHidden || child.isARIAHidden()) && !child.isFocused();
result.isPresentationalChildOfAriaRole = m_isIgnoredFromParentData.isPresentationalChildOfAriaRole || ariaRoleHasPresentationalChildren();
result.isDescendantOfBarrenParent = m_isIgnoredFromParentData.isDescendantOfBarrenParent || !canHaveChildren();
} else {
if (child.isARIAHidden())
result.isAXHidden = true;
bool ignoreARIAHidden = child.isFocused();
for (auto* object = child.parentObject(); object; object = object->parentObject()) {
if (!result.isAXHidden && !ignoreARIAHidden && object->isARIAHidden())
result.isAXHidden = true;
if (!result.isPresentationalChildOfAriaRole && object->ariaRoleHasPresentationalChildren())
result.isPresentationalChildOfAriaRole = true;
if (!result.isDescendantOfBarrenParent && !object->canHaveChildren())
result.isDescendantOfBarrenParent = true;
}
}
child.setIsIgnoredFromParentData(result);
}
String AccessibilityObject::innerHTML() const
{
auto* element = this->element();
return element ? element->innerHTML() : String();
}
String AccessibilityObject::outerHTML() const
{
auto* element = this->element();
return element ? element->outerHTML() : String();
}
bool AccessibilityObject::ignoredByRowAncestor() const
{
auto* ancestor = Accessibility::findAncestor<AccessibilityObject>(*this, false, [] (const AccessibilityObject& ancestor) {
// If an object has a table cell ancestor (before a table row), that is a cell's contents, so don't ignore it.
// Similarly, if an object has a table ancestor (before a row), that could be a row, row group or other container, so don't ignore it.
return ancestor.isTableCell() || ancestor.isTableRow() || ancestor.isTable();
});
return ancestor && ancestor->isTableRow();
}
AccessibilityObject* AccessibilityObject::containingWebArea() const
{
CheckedPtr frameView = documentFrameView();
CheckedPtr cache = axObjectCache();
RefPtr root = cache ? dynamicDowncast<AccessibilityScrollView>(cache->getOrCreate(frameView.get())) : nullptr;
return root ? root->webAreaObject() : nullptr;
}
} // namespace WebCore