Bug Summary

File:Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore/accessibility/AccessibilityObject.cpp
Warning:line 1615, column 11
Local variable 'frameView' is unchecked and unsafe

Annotated Source Code

Press '?' to see keyboard shortcuts

clang -cc1 -cc1 -triple arm64-apple-macosx15.5.0 -O2 -Wundef-prefix=TARGET_OS_ -Werror=undef-prefix -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -analyze -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name UnifiedSource109.cpp -analyzer-checker=core -analyzer-checker=apiModeling -analyzer-checker=unix -analyzer-checker=osx -analyzer-checker=security.insecureAPI.decodeValueOfObjCType -analyzer-checker=deadcode -analyzer-checker=cplusplus -analyzer-checker=security.insecureAPI.UncheckedReturn -analyzer-checker=security.insecureAPI.getpw -analyzer-checker=security.insecureAPI.gets -analyzer-checker=security.insecureAPI.mktemp -analyzer-checker=security.insecureAPI.mkstemp -analyzer-checker=security.insecureAPI.vfork -analyzer-checker=nullability.NullPassedToNonnull -analyzer-checker=nullability.NullReturnedFromNonnull -analyzer-output plist -w -setup-static-analyzer -analyzer-config-compatibility-mode=true -mrelocation-model pic -pic-level 2 -mframe-pointer=non-leaf -ffp-contract=on -fno-rounding-math -target-sdk-version=15.5 -fcompatibility-qualified-id-block-type-checking -fvisibility-inlines-hidden-static-local-var -fno-modulemap-allow-subdirectory-search -fdefine-target-os-macros -enable-tlsdesc -target-cpu apple-m1 -target-feature +v8.4a -target-feature +aes -target-feature +altnzcv -target-feature +ccdp -target-feature +ccpp -target-feature +complxnum -target-feature +crc -target-feature +dotprod -target-feature +flagm -target-feature +fp-armv8 -target-feature +fp16fml -target-feature +fptoint -target-feature +fullfp16 -target-feature +jsconv -target-feature +lse -target-feature +neon -target-feature +pauth -target-feature +perfmon -target-feature +predres -target-feature +ras -target-feature +rcpc -target-feature +rdm -target-feature +sb -target-feature +sha2 -target-feature +sha3 -target-feature +specrestrict -target-feature +ssbs -target-abi darwinpcs -dwarf-ext-refs -fmodule-format=obj -debug-info-kind=standalone -dwarf-version=5 -debugger-tuning=lldb -fdebug-compilation-dir=/Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore -target-linker-version 1167.5 -fcoverage-compilation-dir=/Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore -resource-dir /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/llvm-project/build/lib/clang/21 -dependency-file /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/scan-build-output/StaticAnalyzer/WebCore/WebCore/normal/arm64/UnifiedSource109.d -skip-unused-modulemap-deps -MT dependencies -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk -iquote /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/WebCore-generated-files.hmap -iquote /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/WebCore-project-headers.hmap -isystem /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore/PAL/ThirdParty/dav1d/include -iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/System/Library/PrivateFrameworks -iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/System/Library/Frameworks -include-pch /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/PrecompiledHeaders/SharedPrecompiledHeaders/12902642326230122134/WebCorePrefix.h.gch -D _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE -D CLANG_WEBKIT_BRANCH=1 -D NDEBUG -D BUILDING_WEBKIT -D GL_SILENCE_DEPRECATION=1 -D GLES_SILENCE_DEPRECATION=1 -D __clang_analyzer__ -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/WebCore-own-target-headers.hmap -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/WebCore-all-target-headers.hmap -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/include -I PAL -I ForwardingHeaders -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/include/libxslt -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/include/libxml2 -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/DerivedSources/WebCore -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/usr/local/include -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/usr/local/include/WebKitAdditions -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/local/include/WebKitAdditions -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/usr/local/include -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitLibraries/SDKs/macosx15.0-additions.sdk/usr/local/include -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/local/include -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/usr/local/include/webrtc -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/local/include/webrtc -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/usr/local/include/webm -I /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/local/include/webm -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/PAL.build/PAL.build/DerivedSources -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/PAL.build/Release/PAL.build/DerivedSources -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/DerivedSources-normal/arm64 -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/DerivedSources/arm64 -I /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/WebCore.build/Release/WebCore.build/DerivedSources -F/Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release -I/usr/local/include -stdlib=libc++ -internal-isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/include/c++/v1 -internal-isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/local/include -internal-isystem /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/llvm-project/build/lib/clang/21/include -internal-externc-isystem /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/usr/include -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/System/Library/Frameworks -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/System/Library/SubFrameworks -internal-iframework /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.5.sdk/Library/Frameworks -Wno-trigraphs -Werror -Wno-missing-field-initializers -Wmissing-prototypes -Wunreachable-code -Wnon-virtual-dtor -Wno-overloaded-virtual -Wno-exit-time-destructors -Wno-missing-braces -Wparentheses -Wswitch -Wunused-function -Wno-unused-label -Wno-unused-parameter -Wunused-variable -Wunused-value -Wempty-body -Wuninitialized -Wno-unknown-pragmas -Wno-shadow -Wno-four-char-constants -Wno-conversion -Wconstant-conversion -Wint-conversion -Wbool-conversion -Wenum-conversion -Wno-float-conversion -Wnon-literal-null-conversion -Wobjc-literal-conversion -Wsign-compare -Wno-shorten-64-to-32 -Wnewline-eof -Wno-c++11-extensions -Wno-implicit-fallthrough -Wdeprecated-declarations -Winvalid-offsetof -Wno-sign-conversion -Winfinite-recursion -Wmove -Wcomma -Wblock-capture-autoreleasing -Wstrict-prototypes -Wrange-loop-analysis -Wno-semicolon-before-method-body -Wall -Wc99-designator -Wconditional-uninitialized -Wextra -Wdeprecated-enum-enum-conversion -Wdeprecated-enum-float-conversion -Wenum-float-conversion -Wfinal-dtor-non-final-class -Wformat=2 -Wmisleading-indentation -Wpointer-to-int-cast -Wreorder-init-list -Wundef -Wunused-but-set-variable -Wvla -Werror=unguarded-availability -Wno-elaborated-enum-base -Wcast-qual -Wchar-subscripts -Wextra-tokens -Winit-self -Wmissing-noreturn -Wpacked -Wpointer-arith -Wredundant-decls -Wwrite-strings -Wexit-time-destructors -Wglobal-constructors -Wtautological-compare -Wimplicit-fallthrough -Wvla -Wno-unknown-warning-option -Wliteral-conversion -Wthread-safety -Wno-profile-instr-out-of-date -Wno-profile-instr-unprofiled -Wno-backend-plugin -Wunsafe-buffer-usage -std=c++2b -fdeprecated-macro -ferror-limit 19 -fmacro-backtrace-limit=0 -fvisibility=hidden -fvisibility-inlines-hidden -stack-protector 1 -fblocks -fencode-extended-block-signature -fno-rtti -fregister-global-dtors-with-atexit -fgnuc-version=4.2.1 -fno-cxx-modules -fno-implicit-modules -fskip-odr-check-in-gmf -fpascal-strings -fmax-type-align=16 -fdiagnostics-show-note-include-stack -vectorize-loops -vectorize-slp -fsafe-buffer-usage-suggestions -analyzer-output=html -analyzer-config report-in-main-source-file=true -analyzer-config nullability:NoDiagnoseCallsToSystemHeaders=true -analyzer-checker optin.osx.cocoa.localizability.NonLocalizedStringChecker -analyzer-checker security.insecureAPI.UncheckedReturn -analyzer-checker security.insecureAPI.getpw -analyzer-checker security.insecureAPI.gets -analyzer-checker security.insecureAPI.mkstemp -analyzer-checker security.insecureAPI.mktemp -analyzer-disable-checker security.insecureAPI.rand -analyzer-disable-checker security.insecureAPI.strcpy -analyzer-checker security.insecureAPI.vfork -analyzer-disable-checker alpha,apiModeling,core,cplusplus,deadcode,debug,fuchsia,nullability,optin,osx,security,unix,webkit -analyzer-checker alpha.webkit.ForwardDeclChecker,alpha.webkit.MemoryUnsafeCastChecker,alpha.webkit.NoDeleteChecker,alpha.webkit.NoUncheckedPtrMemberChecker,alpha.webkit.NoUnretainedMemberChecker,alpha.webkit.RetainPtrCtorAdoptChecker,alpha.webkit.UncheckedCallArgsChecker,alpha.webkit.UncheckedLocalVarsChecker,alpha.webkit.UncountedCallArgsChecker,alpha.webkit.UncountedLocalVarsChecker,alpha.webkit.UnretainedCallArgsChecker,alpha.webkit.UnretainedLambdaCapturesChecker,alpha.webkit.UnretainedLocalVarsChecker,webkit.NoUncountedMemberChecker,webkit.RefCntblBaseVirtualDtor,webkit.UncountedLambdaCapturesChecker -analyzer-config max-nodes=10000000 -analyzer-config verbose-report-filename=true -fdwarf2-cfi-asm -o /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/scan-build-output/StaticAnalyzer/WebCore/WebCore/normal/arm64/UnifiedSource109.plist -x c++ /Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/WebKitBuild/Release/DerivedSources/WebCore/unified-sources/UnifiedSource109.cpp
1/*
2 * Copyright (C) 2008-2025 Apple Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
6 * are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 */
28
29#include "config.h"
30#include "AccessibilityObject.h"
31
32#include "AXAttributeCacheScope.h"
33#include "AXComputedObjectAttributeCache.h"
34#include "AXIsolatedTree.h"
35#include "AXLogger.h"
36#include "AXLoggerBase.h"
37#include "AXNotifications.h"
38#include "AXObjectCacheInlines.h"
39#include "AXObjectRareData.h"
40#include "AXRemoteFrame.h"
41#include "AXTextMarker.h"
42#include "AXUtilities.h"
43#include "AccessibilityMockObject.h"
44#include "AccessibilityObjectInlines.h"
45#include "AccessibilityRenderObject.h"
46#include "AccessibilityScrollView.h"
47#include "Chrome.h"
48#include "ChromeClient.h"
49#include "ContainerNodeInlines.h"
50#include "CustomElementDefaultARIA.h"
51#include "DOMTokenList.h"
52#include "DocumentPage.h"
53#include "EditingInlines.h"
54#include "Editor.h"
55#include "ElementInlines.h"
56#include "ElementIterator.h"
57#include "Event.h"
58#include "EventDispatcher.h"
59#include "EventHandler.h"
60#include "EventNames.h"
61#include "FloatRect.h"
62#include "FocusController.h"
63#include "FrameLoader.h"
64#include "FrameSelection.h"
65#include "GeometryUtilities.h"
66#include "HTMLAreaElement.h"
67#include "HTMLBodyElement.h"
68#include "HTMLDataListElement.h"
69#include "HTMLDetailsElement.h"
70#include "HTMLFormControlElement.h"
71#include "HTMLInputElement.h"
72#include "HTMLModelElement.h"
73#include "HTMLNames.h"
74#include "HTMLParserIdioms.h"
75#include "HTMLSlotElement.h"
76#include "HTMLTableSectionElement.h"
77#include "HTMLTextAreaElement.h"
78#include "HitTestResult.h"
79#include "LocalFrame.h"
80#include "LocalizedStrings.h"
81#include "Logging.h"
82#include "MathMLNames.h"
83#include "NodeList.h"
84#include "NodeName.h"
85#include "NodeTraversal.h"
86#include "Page.h"
87#include "PositionInlines.h"
88#include "ProgressTracker.h"
89#include "Range.h"
90#include "RenderElementInlines.h"
91#include "RenderImage.h"
92#include "RenderInline.h"
93#include "RenderLayer.h"
94#include "RenderLayerInlines.h"
95#include "RenderListItem.h"
96#include "RenderListMarker.h"
97#include "RenderObjectInlines.h"
98#include "RenderText.h"
99#include "RenderTextControl.h"
100#include "RenderTheme.h"
101#include "RenderView.h"
102#include "RenderWidget.h"
103#include "RenderedPosition.h"
104#include "SVGNames.h"
105#include "Settings.h"
106#include "TextCheckerClient.h"
107#include "TextCheckingHelper.h"
108#include "TextIterator.h"
109#include "UserGestureIndicator.h"
110#include "VisibleUnits.h"
111#include <numeric>
112#include <ranges>
113#include <wtf/NeverDestroyed.h>
114#include <wtf/StdLibExtras.h>
115#include <wtf/text/MakeString.h>
116#include <wtf/text/StringBuilder.h>
117#include <wtf/text/StringView.h>
118#include <wtf/text/WTFString.h>
119#include <wtf/unicode/CharacterNames.h>
120
121#if ENABLE(MODEL_ELEMENT_ACCESSIBILITY)(defined 1 && 1)
122#include "ModelPlayerAccessibilityChildren.h"
123#endif
124
125namespace WebCore {
126
127using namespace HTMLNames;
128
129AccessibilityObject::AccessibilityObject(AXID axID, AXObjectCache& cache)
130 : AXCoreObject(axID)
131 , m_axObjectCache(cache)
132{
133}
134
135AccessibilityObject::~AccessibilityObject()
136{
137 AX_ASSERT(isDetached())((void)0);
138}
139
140String AccessibilityObject::debugDescriptionInternal(bool verbose, std::optional<OptionSet<AXDebugStringOption>> debugOptions) const
141{
142 StringBuilder result;
143 result.append("{"_s);
144 result.append("role: "_s, roleToString(role()));
145 result.append(", ID "_s, objectID().loggingString());
146
147 if (debugOptions) {
148 if (verbose || *debugOptions & AXDebugStringOption::Ignored)
149 result.append(isIgnored() ? ", ignored"_s : emptyString());
150
151 if (verbose || *debugOptions & AXDebugStringOption::RelativeFrame) {
152 FloatRect frame = relativeFrame();
153 result.append(", relativeFrame ((x: "_s, frame.x(), ", y: "_s, frame.y(), "), (w: "_s, frame.width(), ", h: "_s, frame.height(), "))"_s);
154 }
155
156 if (verbose || *debugOptions & AXDebugStringOption::RemoteFrameOffset)
157 result.append(", remoteFrameOffset ("_s, remoteFrameOffset().x(), ", "_s, remoteFrameOffset().y(), ")"_s);
158
159 if (verbose || *debugOptions & AXDebugStringOption::IsRemoteFrame)
160 result.append(isRemoteFrame() ? ", remote frame"_s : emptyString());
161 }
162
163 if (auto* renderer = this->renderer())
164 result.append(", "_s, renderer->debugDescription());
165 else if (auto* node = this->node())
166 result.append(", "_s, node->debugDescription());
167
168 if (String extraDebugInfo = this->extraDebugInfo(); !extraDebugInfo.isEmpty()) {
169 result.append(" ("_s);
170 result.append(WTF::move(extraDebugInfo));
171 result.append(")"_s);
172 }
173
174 result.append("}"_s);
175 return result.toString();
176}
177
178void AccessibilityObject::postMenuClosedNotificationIfNecessary() const
179{
180 if (!isMenu())
181 return;
182 if (CheckedPtr cache = axObjectCache()) {
183 // Assistive technologies need to be informed when menus close.
184 // No element is passed in the notification because it's a destruction event.
185 cache->postNotification(nullptr, cache->document(), AXNotification::MenuClosed);
186 }
187}
188
189void AccessibilityObject::detachRemoteParts(AccessibilityDetachmentType detachmentType)
190{
191 if (detachmentType == AccessibilityDetachmentType::ElementDestroyed)
192 postMenuClosedNotificationIfNecessary();
193
194 // Clear any children and call detachFromParent on them so that
195 // no children are left with dangling pointers to their parent.
196 clearChildren();
197}
198
199OptionSet<AXAncestorFlag> AccessibilityObject::computeAncestorFlags() const
200{
201 OptionSet<AXAncestorFlag> computedFlags;
202
203 if (hasAncestorFlag(AXAncestorFlag::IsInDescriptionListDetail) || matchesAncestorFlag(AXAncestorFlag::IsInDescriptionListDetail))
204 computedFlags.set(AXAncestorFlag::IsInDescriptionListDetail, 1);
205
206 if (hasAncestorFlag(AXAncestorFlag::IsInDescriptionListTerm) || matchesAncestorFlag(AXAncestorFlag::IsInDescriptionListTerm))
207 computedFlags.set(AXAncestorFlag::IsInDescriptionListTerm, 1);
208
209 if (hasAncestorFlag(AXAncestorFlag::IsInCell) || matchesAncestorFlag(AXAncestorFlag::IsInCell))
210 computedFlags.set(AXAncestorFlag::IsInCell, 1);
211
212 if (hasAncestorFlag(AXAncestorFlag::IsInRow) || matchesAncestorFlag(AXAncestorFlag::IsInRow))
213 computedFlags.set(AXAncestorFlag::IsInRow, 1);
214
215 return computedFlags;
216}
217
218OptionSet<AXAncestorFlag> AccessibilityObject::computeAncestorFlagsWithTraversal() const
219{
220 // If this object's flags are initialized, this traversal is unnecessary. Use AccessibilityObject::ancestorFlags() instead.
221 AX_ASSERT(!ancestorFlagsAreInitialized())((void)0);
222
223 OptionSet<AXAncestorFlag> computedFlags;
224 computedFlags.set(AXAncestorFlag::FlagsInitialized, true);
225 Accessibility::enumerateAncestors<AccessibilityObject>(*this, false, [&] (const AccessibilityObject& ancestor) {
226 computedFlags.add(ancestor.computeAncestorFlags());
227 });
228 return computedFlags;
229}
230
231bool AccessibilityObject::matchesAncestorFlag(AXAncestorFlag flag) const
232{
233 auto role = this->role();
234 switch (flag) {
235 case AXAncestorFlag::IsInDescriptionListDetail:
236 return role == AccessibilityRole::DescriptionListDetail;
237 case AXAncestorFlag::IsInDescriptionListTerm:
238 return role == AccessibilityRole::DescriptionListTerm;
239 case AXAncestorFlag::IsInCell:
240 return role == AccessibilityRole::Cell;
241 case AXAncestorFlag::IsInRow:
242 return role == AccessibilityRole::Row;
243 default:
244 AX_ASSERT_NOT_REACHED()((void)0);
245 return false;
246 }
247}
248
249bool AccessibilityObject::hasAncestorMatchingFlag(AXAncestorFlag flag) const
250{
251 return Accessibility::findAncestor<AccessibilityObject>(*this, false, [flag] (const AccessibilityObject& object) {
252 if (object.ancestorFlagsAreInitialized())
253 return object.ancestorFlags().contains(flag);
254
255 return object.matchesAncestorFlag(flag);
256 }) != nullptr;
257}
258
259bool AccessibilityObject::isInDescriptionListDetail() const
260{
261 if (ancestorFlagsAreInitialized())
262 return m_ancestorFlags.contains(AXAncestorFlag::IsInDescriptionListDetail);
263
264 return hasAncestorMatchingFlag(AXAncestorFlag::IsInDescriptionListDetail);
265}
266
267bool AccessibilityObject::isInDescriptionListTerm() const
268{
269 if (ancestorFlagsAreInitialized())
270 return m_ancestorFlags.contains(AXAncestorFlag::IsInDescriptionListTerm);
271
272 return hasAncestorMatchingFlag(AXAncestorFlag::IsInDescriptionListTerm);
273}
274
275bool AccessibilityObject::isInCell() const
276{
277 if (ancestorFlagsAreInitialized())
278 return m_ancestorFlags.contains(AXAncestorFlag::IsInCell);
279
280 return hasAncestorMatchingFlag(AXAncestorFlag::IsInCell);
281}
282
283bool AccessibilityObject::isInRow() const
284{
285 if (ancestorFlagsAreInitialized())
286 return m_ancestorFlags.contains(AXAncestorFlag::IsInRow);
287
288 return hasAncestorMatchingFlag(AXAncestorFlag::IsInRow);
289}
290
291// ARIA marks elements as having their accessible name derive from either their contents, or their author-provided name.
292bool AccessibilityObject::accessibleNameDerivesFromContent() const
293{
294 // First check for objects specifically identified by ARIA.
295 switch (ariaRoleAttribute()) {
296 case AccessibilityRole::ApplicationAlert:
297 case AccessibilityRole::ApplicationAlertDialog:
298 case AccessibilityRole::ApplicationDialog:
299 case AccessibilityRole::ApplicationLog:
300 case AccessibilityRole::ApplicationMarquee:
301 case AccessibilityRole::ApplicationStatus:
302 case AccessibilityRole::ApplicationTimer:
303 case AccessibilityRole::ComboBox:
304 case AccessibilityRole::Definition:
305 case AccessibilityRole::Document:
306 case AccessibilityRole::DocumentArticle:
307 case AccessibilityRole::DocumentMath:
308 case AccessibilityRole::DocumentNote:
309 case AccessibilityRole::LandmarkRegion:
310 case AccessibilityRole::LandmarkDocRegion:
311 case AccessibilityRole::Form:
312 case AccessibilityRole::Grid:
313 case AccessibilityRole::Group:
314 case AccessibilityRole::Image:
315 case AccessibilityRole::List:
316 case AccessibilityRole::ListBox:
317 case AccessibilityRole::LandmarkBanner:
318 case AccessibilityRole::LandmarkComplementary:
319 case AccessibilityRole::LandmarkContentInfo:
320 case AccessibilityRole::LandmarkNavigation:
321 case AccessibilityRole::LandmarkMain:
322 case AccessibilityRole::LandmarkSearch:
323 case AccessibilityRole::Menu:
324 case AccessibilityRole::MenuBar:
325 case AccessibilityRole::ProgressIndicator:
326 case AccessibilityRole::Meter:
327 case AccessibilityRole::RadioGroup:
328 case AccessibilityRole::SectionFooter:
329 case AccessibilityRole::SectionHeader:
330 case AccessibilityRole::ScrollBar:
331 case AccessibilityRole::Slider:
332 case AccessibilityRole::SpinButton:
333 case AccessibilityRole::Splitter:
334 case AccessibilityRole::Table:
335 case AccessibilityRole::TabList:
336 case AccessibilityRole::TabPanel:
337 case AccessibilityRole::TextArea:
338 case AccessibilityRole::TextField:
339 case AccessibilityRole::Toolbar:
340 case AccessibilityRole::TreeGrid:
341 case AccessibilityRole::Tree:
342 case AccessibilityRole::WebApplication:
343 return false;
344 default:
345 break;
346 }
347
348 // Now check for generically derived elements now that we know the element does not match a specific ARIA role.
349 switch (role()) {
350 case AccessibilityRole::Slider:
351 case AccessibilityRole::ListBox:
352 return false;
353 default:
354 break;
355 }
356
357 return true;
358}
359
360String AccessibilityObject::computedLabel()
361{
362 // This method is being called by WebKit inspector, which may happen at any time, so we need to update our backing store now.
363 // Also hold onto this object in case updateBackingStore deletes this node.
364 Ref protectedThis { *this };
365 updateBackingStore();
366 Vector<AccessibilityText> text;
367 accessibilityText(text);
368 if (text.size())
369 return text[0].text;
370 return { };
371}
372
373// FIXME: We should unify all of the special cases for live regions, textUnderElement, title, description, etc. under one method, similar to computedLabel.
374String AccessibilityObject::announcementText() const
375{
376 Vector<AccessibilityText> computedText;
377 accessibilityText(computedText);
378
379#if PLATFORM(COCOA)(defined 1 && 1)
380 String descriptionText = descriptionAttributeValue(&computedText);
381 if (!descriptionText.isEmpty())
382 return descriptionText;
383#endif
384
385 if (is<HTMLInputElement>(node())) {
386 // Many inputs derive their accessible name from their title.
387 if (String titleText = title(&computedText); !titleText.isEmpty())
388 return titleText;
389 }
390
391 TextUnderElementMode mode;
392 mode.includeListMarkers = IncludeListMarkerText::Yes;
393 // We want all of the text beneath this object when speaking live regions.
394 mode.descendIntoContainers = DescendIntoContainers::Yes;
395 mode.childrenInclusion = TextUnderElementMode::Children::IncludeNameFromContentsChildren;
396 return textUnderElement(mode);
397}
398
399bool AccessibilityObject::isEditableWebArea() const
400{
401 if (!isWebArea())
402 return false;
403
404 RefPtr page = this->page();
405 if (page && page->isEditable())
406 return true;
407
408 RefPtr document = this->document();
409 return document && document->inDesignMode();
410}
411
412Vector<AXTextMarkerRange> AccessibilityObject::misspellingRanges() const
413{
414 AXTRACE("AccessibilityObject::misspellingRanges"_s)(void)0;
415
416 RefPtr node = this->node();
417 if (!node)
418 return { };
419
420 RefPtr frame = node->document().frame();
421 if (!frame)
422 return { };
423
424 auto* textChecker = frame->editor().textChecker();
425 if (!textChecker)
426 return { };
427
428 // In order to resolve to the correct ranges, Editor::rangeForTextCheckingResult(...)
429 // assumes that the selection is within the Node for which text we are calling checkTextOfParagraph.
430 // Therefore, remember the current selection, set it to the beginning of the Node and restore it aftwards.
431 auto originalSelection = frame->selection().selection();
432 if (auto range = simpleRange()) {
433 // Passing UserTriggered::No, which is the default value, guaranties that accessibility is not notified of text selection changes.
434 frame->selection().setSelectedRange(SimpleRange { range->start, range->start }, Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::No);
435 }
436
437 Vector<AXTextMarkerRange> ranges;
438 if (unifiedTextCheckerEnabled(frame.get())) {
439 Vector<TextCheckingResult> misspellings;
440 checkTextOfParagraph(*textChecker, stringValue(), TextCheckingType::Spelling, misspellings, frame->selection().selection());
441 for (auto& misspelling : misspellings) {
442 if (auto range = frame->editor().rangeForTextCheckingResult(misspelling))
443 ranges.append(range);
444 }
445 } else {
446 int location = -1;
447 int length = 0;
448 textChecker->checkSpellingOfString(stringValue(), &location, &length);
449 if (location > -1 && length > 0)
450 ranges = { { treeID(), objectID(), static_cast<unsigned>(location), static_cast<unsigned>(length) } };
451 }
452
453 frame->selection().setSelectedRange(originalSelection.range(), Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes, UserTriggered::No);
454 return ranges;
455}
456
457std::optional<SimpleRange> AccessibilityObject::misspellingRange(const SimpleRange& start, AccessibilitySearchDirection direction) const
458{
459 auto node = this->node();
460 if (!node)
461 return std::nullopt;
462
463 RefPtr frame = node->document().frame();
464 if (!frame)
465 return std::nullopt;
466
467 if (!unifiedTextCheckerEnabled(frame.get()))
468 return std::nullopt;
469
470 Ref editor = frame->editor();
471
472 TextCheckerClient* textChecker = editor->textChecker();
473 if (!textChecker)
474 return std::nullopt;
475
476 Vector<TextCheckingResult> misspellings;
477 checkTextOfParagraph(*textChecker, stringValue(), TextCheckingType::Spelling, misspellings, frame->selection().selection());
478
479 // Find the first misspelling past the start.
480 if (direction == AccessibilitySearchDirection::Next) {
481 for (auto& misspelling : misspellings) {
482 auto misspellingRange = editor->rangeForTextCheckingResult(misspelling);
483 if (misspellingRange && is_gt(treeOrder<ComposedTree>(misspellingRange->end, start.end)))
484 return *misspellingRange;
485 }
486 } else {
487 for (auto& misspelling : misspellings | std::views::reverse) {
488 auto misspellingRange = editor->rangeForTextCheckingResult(misspelling);
489 if (misspellingRange && is_lt(treeOrder<ComposedTree>(misspellingRange->start, start.start)))
490 return *misspellingRange;
491 }
492 }
493
494 return std::nullopt;
495}
496
497AXTextMarkerRange AccessibilityObject::textInputMarkedTextMarkerRange() const
498{
499 WeakPtr node = this->node();
500 if (!node)
501 return { };
502
503 RefPtr frame = node->document().frame();
504 if (!frame)
505 return { };
506
507 CheckedPtr cache = axObjectCache();
508 if (!cache)
509 return { };
510
511 Ref editor = frame->editor();
512 RefPtr object = cache->getOrCreate(editor->compositionNode());
513 if (!object)
514 return { };
515
516 if (RefPtr observableObject = object->observableObject())
517 object = observableObject;
518
519 if (object->objectID() != objectID())
520 return { };
521
522 return { editor->compositionRange() };
523}
524
525AccessibilityObject* AccessibilityObject::displayContentsParent() const
526{
527 RefPtr parentNode = node() ? node()->parentNode() : nullptr;
528 if (RefPtr parentElement = dynamicDowncast<Element>(parentNode); !parentElement || !parentElement->hasDisplayContents())
529 return nullptr;
530
531 CheckedPtr cache = axObjectCache();
532 return cache ? cache->getOrCreate(*parentNode) : nullptr;
533}
534
535AccessibilityObject* AccessibilityObject::nextSiblingUnignored(unsigned limit) const
536{
537 AX_ASSERT(limit)((void)0);
538
539 for (auto sibling = iterator(nextSibling()); limit && sibling; --limit, ++sibling) {
540 if (!sibling->isIgnored())
541 return sibling.ptr();
542 }
543 return nullptr;
544}
545
546AccessibilityObject* AccessibilityObject::previousSiblingUnignored(unsigned limit) const
547{
548 AX_ASSERT(limit)((void)0);
549
550 for (auto sibling = iterator(previousSibling()); limit && sibling; --limit, --sibling) {
551 if (!sibling->isIgnored())
552 return sibling.ptr();
553 }
554 return nullptr;
555}
556
557FloatRect AccessibilityObject::convertFrameToSpace(const FloatRect& frameRect, AccessibilityConversionSpace conversionSpace) const
558{
559 AX_ASSERT(isMainThread())((void)0);
560
561 // Find the appropriate scroll view to use to convert the contents to the window.
562 RefPtr parentAccessibilityScrollView = ancestorAccessibilityScrollView(false /* includeSelf */);
563 RefPtr parentScrollView = parentAccessibilityScrollView ? parentAccessibilityScrollView->scrollView() : nullptr;
564
565 auto snappedFrameRect = snappedIntRect(IntRect(frameRect));
566
567#if ENABLE(ACCESSIBILITY_LOCAL_FRAME)(defined 0 && 0)
568 if (conversionSpace == AccessibilityConversionSpace::Screen) {
569 // For screen space, use contentsToView() to adjust for scroll *within* this frame,
570 // then apply the frame's screen transform and position (which account for iframe offsets and viewport scale).
571 if (parentScrollView)
572 snappedFrameRect = parentScrollView->contentsToView(snappedFrameRect);
573
574 RefPtr rootScrollView = dynamicDowncast<AccessibilityScrollView>(ancestorAccessibilityScrollView(true /* includeSelf */));
575 if (!rootScrollView)
576 return snappedFrameRect;
577
578 auto geometry = rootScrollView->frameGeometry();
579
580 auto scaledRect = geometry.screenTransform.mapRect(FloatRect(snappedFrameRect));
581
582 // macOS uses bottom-left origin, non-macOS assumes top-left origin.
583 FloatPoint position = {
584 geometry.screenPosition.x() + scaledRect.x(),
585#if PLATFORM(MAC)(defined 1 && 1)
586 geometry.screenPosition.y() - scaledRect.maxY()
587#else
588 geometry.screenPosition.y() + scaledRect.y()
589#endif
590 };
591 return { position, scaledRect.size() };
592 }
593
594 // FIXME: ENABLE(ACCESSIBILITY_LOCAL_FRAME) doesn't support page-relative frame, but this is used for old tests. Remove this once all tests are updated.
595 if (parentScrollView)
596 snappedFrameRect = parentScrollView->contentsToRootView(snappedFrameRect);
597#else
598 // Legacy behavior: contentsToRootView walks up through all frames for local frames.
599 // For remote frames, the caller (e.g., relativeFrame()) adds remoteFrameOffset().
600 if (parentScrollView)
601 snappedFrameRect = parentScrollView->contentsToRootView(snappedFrameRect);
602
603 if (conversionSpace == AccessibilityConversionSpace::Screen) {
604 RefPtr page = this->page();
605 if (!page)
606 return snappedFrameRect;
607
608 // If we have an empty chrome client (like SVG) then we should use the page
609 // of the scroll view parent to help us get to the screen rect.
610 if (parentAccessibilityScrollView && page->chrome().client().isEmptyChromeClient())
611 page = parentAccessibilityScrollView->page();
612
613 snappedFrameRect = page->chrome().rootViewToAccessibilityScreen(snappedFrameRect);
614 }
615#endif // ENABLE(ACCESSIBILITY_LOCAL_FRAME)
616
617 return snappedFrameRect;
618}
619
620FloatRect AccessibilityObject::relativeFrame() const
621{
622 auto rect = elementRect();
623#if !ENABLE(ACCESSIBILITY_LOCAL_FRAME)(defined 0 && 0)
624 rect.moveBy(remoteFrameOffset());
625#endif
626 return convertFrameToSpace(rect, AccessibilityConversionSpace::Page);
627}
628
629AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const Node* node)
630{
631 return WebCore::firstAccessibleObjectFromNode(node, [] (const AccessibilityObject& accessible) {
632 return !accessible.isIgnored();
633 });
634}
635
636AccessibilityObject* firstAccessibleObjectFromNode(const Node* node, NOESCAPE[[clang::noescape]] const Function<bool(const AccessibilityObject&)>& isAccessible)
637{
638 RefPtr axNode = node;
639 if (!axNode)
640 return nullptr;
641
642 CheckedPtr cache = axNode->document().axObjectCache();
643 if (!cache)
644 return nullptr;
645
646 RefPtr accessibleObject = cache->getOrCreate(const_cast<Node&>(*axNode));
647 while (accessibleObject && !isAccessible(*accessibleObject)) {
648 axNode = NodeTraversal::next(*axNode);
649
650 while (axNode && !axNode->renderer())
651 axNode = NodeTraversal::nextSkippingChildren(*axNode);
652
653 if (!axNode)
654 return nullptr;
655
656 accessibleObject = cache->getOrCreate(const_cast<Node&>(*axNode));
657 }
658
659 return accessibleObject.unsafeGet();
660}
661
662// FIXME: Usages of this function should be replaced by a new flag in AccessibilityObject::m_ancestorFlags.
663bool AccessibilityObject::isDescendantOfRole(AccessibilityRole role) const
664{
665 return Accessibility::findAncestor<AccessibilityObject>(*this, false, [&role] (const AccessibilityObject& object) {
666 return object.role() == role;
667 }) != nullptr;
668}
669
670static bool isTableComponent(AXCoreObject& axObject)
671{
672 return axObject.isTable() || axObject.isTableColumn() || axObject.isExposedTableRow() || axObject.isTableCell();
673}
674
675void AccessibilityObject::insertChild(AccessibilityObject& child, unsigned index, DescendIfIgnored descendIfIgnored)
676{
677 auto owners = child.owners();
678 if (owners.size()) {
679 size_t indexOfThis = owners.findIf([this] (const Ref<AXCoreObject>& object) {
680 return object.ptr() == this;
681 });
682
683 if (indexOfThis == notFound) {
684 // The child is aria-owned, and not by us, so we shouldn't insert it.
685 return;
686 }
687 }
688
689 if (is<HTMLAreaElement>(child.node())) [[unlikely]] {
690 // Despite the DOM parent for <area> elements being <map>, we expose <area> elements as children
691 // of the <img> using the <map>. This provides a better experience for AT users, e.g. a screenreader
692 // would hear "image map" or "group" plus the image description, then the links, which provides the
693 // added context for what the links represent.
694 //
695 // Due to the difference in DOM vs. expected AX hierarchy, make sure area elements are only inserted
696 // by their associated image as children.
697 if (child.parentObject() != this)
698 return;
699 }
700
701 // If the parent is asking for this child's children, then either it's the first time (and clearing is a no-op),
702 // or its visibility has changed. In the latter case, this child may have a stale child cached.
703 // This can prevent aria-hidden changes from working correctly. Hence, whenever a parent is getting children, ensure data is not stale.
704 // Only clear the child's children when we know it's in the updating chain in order to avoid unnecessary work.
705 if (child.needsToUpdateChildren() || m_subtreeDirty) {
706 child.clearChildren();
707 // Pass m_subtreeDirty flag down to the child so that children cache gets reset properly.
708 if (m_subtreeDirty)
709 child.setNeedsToUpdateSubtree();
710 }
711
712#if USE(ATSPI)(defined USE_ATSPI && USE_ATSPI)
713 // FIXME: Consider removing this ATSPI-only branch with https://bugs.webkit.org/show_bug.cgi?id=282117.
714 RefPtr displayContentsParent = child.displayContentsParent();
715 // To avoid double-inserting a child of a `display: contents` element, only insert if `this` is the rightful parent.
716 if (displayContentsParent && displayContentsParent != this) {
717 // Make sure the display:contents parent object knows it has a child it needs to add.
718 displayContentsParent->setNeedsToUpdateChildren();
719
720 // 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.
721 bool allowInsert = isTableColumn() || role() == AccessibilityRole::TableHeaderContainer;
722
723 // AccessibilityTable::addChildren never actually calls `insertChild` for table section elements
724 // (e.g. tbody, thead), so don't block this `insertChild` for display:contents section elements,
725 // or else the child elements of the section element will never be inserted into the tree.
726 allowInsert = allowInsert || (isTable() && is<HTMLTableSectionElement>(displayContentsParent->element()));
727 if (!allowInsert)
728 return;
729 }
730#endif // USE(ATSPI)
731
732 auto insert = [this] (Ref<AXCoreObject>&& object, unsigned index) {
733 std::ignore = setChildIndexInParent(object.get(), index);
734 m_children.insert(index, WTF::move(object));
735 };
736
737 auto thisAncestorFlags = computeAncestorFlags();
738 child.initializeAncestorFlags(thisAncestorFlags);
739 setIsIgnoredFromParentDataForChild(child);
740 if (!includeIgnoredInCoreTree() && child.isIgnored()) {
741 if (descendIfIgnored == DescendIfIgnored::Yes) {
742 unsigned insertionIndex = index;
743 auto childAncestorFlags = child.computeAncestorFlags();
744 for (auto grandchildCoreObject : child.children()) {
745 Ref grandchild = downcast<AccessibilityObject>(grandchildCoreObject.get());
746
747 // Even though `child` is ignored, we still need to set ancestry flags based on it.
748 grandchild->initializeAncestorFlags(childAncestorFlags);
749 grandchild->addAncestorFlags(thisAncestorFlags);
750 // 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.
751 if (insertionIndex > m_children.size())
752 break;
753 insert(WTF::move(grandchild), insertionIndex);
754 ++insertionIndex;
755 }
756 }
757 } else {
758 // Table component child-parent relationships often don't line up properly, hence the need for methods
759 // like parentTable() and parentRow(). Exclude them from this ASSERT.
760 // FIXME: We hit this ASSERT on gmail.com. https://bugs.webkit.org/show_bug.cgi?id=293264
761 AX_BROKEN_ASSERT(isTableComponent(child) || isTableComponent(*this) || child.parentObject() == this)do { if (isAccessibilityLogChannelEnabled()) { do { if (__builtin_expect
(!!(!(isTableComponent(child) || isTableComponent(*this) || child
.parentObject() == this)), 0)) clang diagnostic push clang diagnostic
ignored "-Wunknown-warning-option" clang diagnostic ignored
"-Wunsafe-buffer-usage" clang diagnostic ignored "-Wunsafe-buffer-usage-in-libc-call"
clang diagnostic ignored "-Wunsafe-buffer-usage-in-format-attr-call"
[[clang::suppress]] __extension__({ os_log_t _log_tmp = (LogAccessibility
.osLogChannel); os_log_type_t _type_tmp = OS_LOG_TYPE_ERROR; if
(os_log_type_enabled(_log_tmp, _type_tmp)) { __extension__({
clang diagnostic push clang diagnostic ignored "-Wvla" clang
diagnostic error "-Wformat" _Static_assert(__builtin_constant_p
("BROKEN ASSERTION FAILED in %s(%d) : %s\n"), "format/label/description argument must be a string constant"
); __attribute__((section("__TEXT,__oslogstring,cstring_literals"
),internal_linkage)) static const char _os_fmt_str[] __asm("LOS_LOG3"
) = "BROKEN ASSERTION FAILED in %s(%d) : %s\n"; uint8_t _Alignas
(16) __attribute__((uninitialized)) _os_fmt_buf[__builtin_os_log_format_buffer_size
("BROKEN ASSERTION FAILED in %s(%d) : %s\n", "/Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore/accessibility/AccessibilityObject.cpp"
, 761, __PRETTY_FUNCTION__)]; _os_log_error_impl(&__dso_handle
, _log_tmp, _type_tmp, _os_fmt_str, (uint8_t *)__builtin_os_log_format
(_os_fmt_buf, "BROKEN ASSERTION FAILED in %s(%d) : %s\n", "/Volumes/Data/worker/macOS-Safer-CPP-Checks-EWS/build/Source/WebCore/accessibility/AccessibilityObject.cpp"
, 761, __PRETTY_FUNCTION__), (uint32_t)sizeof(_os_fmt_buf)) clang
diagnostic pop ; }); } }) clang diagnostic pop ; } while (0)
; } } while (0)
;
762 insert(Ref { child }, index);
763 }
764
765 // Reset the child's m_isIgnoredFromParentData since we are done adding that child and its children.
766 child.clearIsIgnoredFromParentData();
767}
768
769void AccessibilityObject::resetChildrenIndexInParent() const
770{
771 if (!shouldSetChildIndexInParent())
772 return;
773
774 unsigned index = 0;
775 for (const auto& child : m_children) {
776 bool didSet = setChildIndexInParent(child.get(), index);
777 // We check shouldSetChildIndexInParent above, so this should always be true.
778 ASSERT_UNUSED(didSet, didSet)((void)didSet);
779 ++index;
780 }
781}
782
783AXCoreObject::AccessibilityChildrenVector AccessibilityObject::findMatchingObjectsWithin(AccessibilitySearchCriteria&& criteria)
784{
785 if (CheckedPtr cache = axObjectCache())
786 cache->startCachingComputedObjectAttributesUntilTreeMutates();
787
788 return AXCoreObject::findMatchingObjectsWithin(WTF::move(criteria));
789}
790
791// Returns the range that is fewer positions away from the reference range.
792// NOTE: The after range is expected to ACTUALLY be after the reference range and the before
793// range is expected to ACTUALLY be before. These are not checked for performance reasons.
794static std::optional<SimpleRange> rangeClosestToRange(const SimpleRange& referenceRange, std::optional<SimpleRange>&& afterRange, std::optional<SimpleRange>&& beforeRange)
795{
796 if (!beforeRange)
797 return WTF::move(afterRange);
798 if (!afterRange)
799 return WTF::move(beforeRange);
800 auto distanceBefore = characterCount({ beforeRange->end, referenceRange.start });
801 auto distanceAfter = characterCount({ afterRange->start, referenceRange.end });
802 return WTF::move(distanceBefore <= distanceAfter ? beforeRange : afterRange);
803}
804
805std::optional<SimpleRange> AccessibilityObject::rangeOfStringClosestToRangeInDirection(const SimpleRange& referenceRange, AccessibilitySearchDirection searchDirection, const Vector<String>& searchStrings) const
806{
807 RefPtr frame = this->frame();
808 if (!frame)
809 return std::nullopt;
810
811 bool isBackwardSearch = searchDirection == AccessibilitySearchDirection::Previous;
812 FindOptions findOptions { FindOption::AtWordStarts, FindOption::AtWordEnds, FindOption::CaseInsensitive, FindOption::StartInSelection };
813 if (isBackwardSearch)
814 findOptions.add(FindOption::Backwards);
815
816 std::optional<SimpleRange> closestStringRange;
817 for (auto& searchString : searchStrings) {
818 if (std::optional foundStringRange = frame->editor().rangeOfString(searchString, referenceRange, findOptions)) {
819 bool foundStringIsCloser;
820 if (!closestStringRange)
821 foundStringIsCloser = true;
822 else {
823 foundStringIsCloser = isBackwardSearch
824 ? is_gt(treeOrder<ComposedTree>(foundStringRange->end, closestStringRange->end))
825 : is_lt(treeOrder<ComposedTree>(foundStringRange->start, closestStringRange->start));
826 }
827
828 if (foundStringIsCloser)
829 closestStringRange = *foundStringRange;
830 }
831 }
832 return closestStringRange;
833}
834
835VisibleSelection AccessibilityObject::selection() const
836{
837 RefPtr document = this->document();
838 RefPtr frame = document ? document->frame() : nullptr;
839 return frame ? frame->selection().selection() : VisibleSelection();
840}
841
842// Returns an collapsed range preceding the document contents if there is no selection.
843// FIXME: Why is that behavior more useful than returning null in that case?
844std::optional<SimpleRange> AccessibilityObject::selectionRange() const
845{
846 RefPtr frame = this->frame();
847 if (!frame)
848 return std::nullopt;
849
850 if (auto range = frame->selection().selection().firstRange())
851 return *range;
852
853 Ref document = *frame->document();
854 return { { { document.get(), 0 }, { document.get(), 0 } } };
855}
856
857std::optional<SimpleRange> AccessibilityObject::simpleRange() const
858{
859 RefPtr node = this->node();
860 if (!node)
861 return std::nullopt;
862
863 std::optional stitchGroup = stitchGroupIfRepresentative();
864 if (!stitchGroup)
865 return AXObjectCache::rangeForNodeContents(*node);
866
867 // |this| is a stitching of multiple objects, so we need to include all of their contents in the range.
868 CheckedPtr cache = axObjectCache();
869 if (RefPtr endNode = cache ? lastNonAriaHiddenNode(stitchGroup->members(), *cache) : nullptr) {
870 if (std::optional range = makeSimpleRange(positionBeforeNode(*node), positionAfterNode(*endNode)))
871 return range;
872 }
873 return AXObjectCache::rangeForNodeContents(*node);
874}
875
876Vector<BoundaryPoint> AccessibilityObject::previousLineStartBoundaryPoints(const VisiblePosition& startingPosition, const SimpleRange& targetRange, unsigned positionsToRetrieve) const
877{
878 Vector<BoundaryPoint> boundaryPoints;
879 boundaryPoints.reserveInitialCapacity(positionsToRetrieve);
880
881 std::optional<VisiblePosition> lastPosition = startingPosition;
882 for (unsigned i = 0; i < positionsToRetrieve; i++) {
883 lastPosition = previousLineStartPositionInternal(*lastPosition);
884 if (!lastPosition)
885 break;
886
887 auto boundaryPoint = makeBoundaryPoint(*lastPosition);
888 if (!boundaryPoint || !contains(targetRange, *boundaryPoint))
889 break;
890
891 boundaryPoints.append(WTF::move(*boundaryPoint));
892 }
893 boundaryPoints.shrinkToFit();
894 return boundaryPoints;
895}
896
897std::optional<BoundaryPoint> AccessibilityObject::lastBoundaryPointContainedInRect(const Vector<BoundaryPoint>& boundaryPoints, const BoundaryPoint& startBoundary, const FloatRect& rect, int leftIndex, int rightIndex, bool isFlippedWritingMode) const
898{
899 if (leftIndex > rightIndex || boundaryPoints.isEmpty())
900 return std::nullopt;
901
902 auto indexIsValid = [&] (int index) {
903 return index >= 0 && static_cast<size_t>(index) < boundaryPoints.size();
904 };
905 auto boundaryPointContainedInRect = [&] (const BoundaryPoint& boundary) {
906 return boundaryPointsContainedInRect(startBoundary, boundary, rect, isFlippedWritingMode);
907 };
908
909 int midIndex = std::midpoint(leftIndex, rightIndex);
910 if (boundaryPointContainedInRect(boundaryPoints.at(midIndex))) {
911 // We have a match if `midIndex` boundary point is contained in the rect, but the one at `midIndex - 1` isn't.
912 if (indexIsValid(midIndex - 1) && !boundaryPointContainedInRect(boundaryPoints.at(midIndex - 1)))
913 return boundaryPoints.at(midIndex);
914
915 return lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, rect, leftIndex, midIndex - 1, isFlippedWritingMode);
916 }
917 // 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.
918 if (indexIsValid(midIndex + 1) && boundaryPointContainedInRect(boundaryPoints.at(midIndex + 1)))
919 return boundaryPoints.at(midIndex + 1);
920
921 return lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, rect, midIndex + 1, rightIndex, isFlippedWritingMode);
922}
923
924static IntPoint NODELETE[[clang::annotate_type("webkit.nodelete")]] textStartPoint(const IntRect& rect, bool isFlippedWritingMode)
925{
926 if (!isFlippedWritingMode)
927 return rect.minXMinYCorner();
928 return rect.maxXMinYCorner();
929}
930
931static IntPoint NODELETE[[clang::annotate_type("webkit.nodelete")]] textEndPoint(const IntRect& rect, bool isFlippedWritingMode)
932{
933 if (!isFlippedWritingMode)
934 return rect.maxXMaxYCorner();
935 return rect.minXMaxYCorner();
936}
937
938bool AccessibilityObject::boundaryPointsContainedInRect(const BoundaryPoint& startBoundary, const BoundaryPoint& endBoundary, const FloatRect& rect, bool isFlippedWritingMode) const
939{
940 auto elementRect = boundsForRange({ startBoundary, endBoundary });
941 return rect.contains(textEndPoint(elementRect, isFlippedWritingMode));
942}
943
944std::optional<SimpleRange> AccessibilityObject::visibleCharacterRangeInternal(SimpleRange& range, const FloatRect& contentRect, const IntRect& startingElementRect) const
945{
946 if (!contentRect.intersects(startingElementRect))
947 return std::nullopt;
948
949 auto elementRect = startingElementRect;
950 auto startBoundary = range.start;
951 auto endBoundary = range.end;
952
953 const CheckedPtr style = this->style();
954 bool isFlipped = style && style->writingMode().isBlockFlipped();
955 // 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
956 // has a larger `x`-coordinate than the end.
957 bool laysOutIntoNegativeX = isFlipped && style->writingMode().isVertical();
958
959 // Origin isn't contained in visible rect, start moving forward by line.
960 while (!contentRect.contains(textStartPoint(elementRect, isFlipped))) {
961 auto currentPosition = VisiblePosition(makeContainerOffsetPosition(startBoundary));
962 auto nextLinePosition = nextLineEndPosition(currentPosition);
963 if (nextLinePosition == currentPosition) {
964 // We tried to move to the next line end, but got the same position back. Break to avoid
965 // looping infinitely. It would be better if we understood *why* nextLineEndPosition
966 // is returning the same position, but do this for now. If you hit this assert, please
967 // file a bug with steps to reproduce.
968 AX_ASSERT_NOT_REACHED()((void)0);
969 break;
970 }
971
972 auto testStartBoundary = makeBoundaryPoint(nextLinePosition);
973 if (!testStartBoundary || !contains(range, *testStartBoundary))
974 break;
975
976 // testStartBoundary is valid, so commit it and update the elementRect.
977 startBoundary = *testStartBoundary;
978 elementRect = boundsForRange(SimpleRange(startBoundary, range.end));
979 if (elementRect.isEmpty() || (elementRect.x() < 0 && !laysOutIntoNegativeX) || elementRect.y() < 0)
980 break;
981 }
982
983 bool didCorrectStartBoundary = false;
984 // Sometimes we shrink one line too far -- check the previous line start to see if it's in bounds.
985 auto previousLineStartPosition = previousLineStartPositionInternal(VisiblePosition(makeContainerOffsetPosition(startBoundary)));
986 if (previousLineStartPosition) {
987 if (auto previousLineStartBoundaryPoint = makeBoundaryPoint(*previousLineStartPosition)) {
988 auto lineStartRect = boundsForRange(SimpleRange(*previousLineStartBoundaryPoint, range.end));
989 if (previousLineStartBoundaryPoint->container.ptr() == startBoundary.container.ptr() && contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
990 elementRect = lineStartRect;
991 startBoundary = *previousLineStartBoundaryPoint;
992 didCorrectStartBoundary = true;
993 }
994 }
995 }
996
997 if (!didCorrectStartBoundary) {
998 // We iterated to a line-end position above. We must also check if the start of this line is in bounds.
999 auto startBoundaryLineStartPosition = startOfLine(VisiblePosition(makeContainerOffsetPosition(startBoundary)));
1000 auto lineStartBoundaryPoint = makeBoundaryPoint(startBoundaryLineStartPosition);
1001 if (lineStartBoundaryPoint && lineStartBoundaryPoint->container.ptr() == startBoundary.container.ptr()) {
1002 auto lineStartRect = boundsForRange(SimpleRange(*lineStartBoundaryPoint, range.end));
1003 Ref lineStartContainer = lineStartBoundaryPoint->container;
1004 if (contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
1005 elementRect = lineStartRect;
1006 startBoundary = *lineStartBoundaryPoint;
1007 } else if (lineStartBoundaryPoint->offset < lineStartContainer->length()) {
1008 // Sometimes we're one character off from being in-bounds. Check for this too.
1009 lineStartBoundaryPoint->offset = lineStartBoundaryPoint->offset + 1;
1010 lineStartRect = boundsForRange(SimpleRange(*lineStartBoundaryPoint, range.end));
1011 lineStartBoundaryPoint->offset = lineStartBoundaryPoint->offset - 1;
1012 if (contentRect.contains(textStartPoint(lineStartRect, isFlipped))) {
1013 elementRect = lineStartRect;
1014 startBoundary = *lineStartBoundaryPoint;
1015 }
1016 }
1017 }
1018 }
1019
1020 // Computing previous line start positions is cheap relative to computing boundsForRange, so compute the end boundary by
1021 // grabbing batches of lines and binary searching within them to minimize calls to boundsForRange.
1022 Vector<BoundaryPoint> boundaryPoints = { endBoundary };
1023 do {
1024 // If the first boundary point is contained in contentRect, then it's a match because we know everything in the last batch
1025 // of lines was not contained in contentRect.
1026 if (boundaryPointsContainedInRect(startBoundary, boundaryPoints.at(0), contentRect, isFlipped)) {
1027 endBoundary = boundaryPoints.at(0);
1028 break;
1029 }
1030
1031 auto lastBoundaryPoint = boundaryPoints.last();
1032 elementRect = boundsForRange({ startBoundary, lastBoundaryPoint });
1033 if (elementRect.isEmpty())
1034 break;
1035 // Otherwise if the last boundary point is contained in contentRect, then we know some boundary point in this batch is
1036 // our target end boundary point.
1037 if (contentRect.contains(textEndPoint(elementRect, isFlipped))) {
1038 endBoundary = lastBoundaryPointContainedInRect(boundaryPoints, startBoundary, contentRect, isFlipped).value_or(lastBoundaryPoint);
1039 break;
1040 }
1041 boundaryPoints = previousLineStartBoundaryPoints(VisiblePosition(makeContainerOffsetPosition(lastBoundaryPoint)), range, 64);
1042 } while (!boundaryPoints.isEmpty());
1043
1044 // Sometimes we shrink one line too far. Check the next line end to see if it's in bounds.
1045 auto nextLineEndPosition = this->nextLineEndPosition(VisiblePosition(makeContainerOffsetPosition(endBoundary)));
1046 auto nextLineEndBoundaryPoint = makeBoundaryPoint(nextLineEndPosition);
1047 if (nextLineEndBoundaryPoint && nextLineEndBoundaryPoint->container.ptr() == endBoundary.container.ptr()) {
1048 auto lineEndRect = boundsForRange(SimpleRange(startBoundary, *nextLineEndBoundaryPoint));
1049
1050 if (contentRect.contains(textEndPoint(lineEndRect, isFlipped)))
1051 endBoundary = *nextLineEndBoundaryPoint;
1052 }
1053
1054 return { { startBoundary, endBoundary } };
1055}
1056
1057std::optional<SimpleRange> AccessibilityObject::findTextRange(const Vector<String>& searchStrings, const SimpleRange& startRange, AccessibilitySearchTextDirection direction) const
1058{
1059 auto findRange = [this, &searchStrings, &direction] (const SimpleRange& referenceRange) -> std::optional<SimpleRange> {
1060 switch (direction) {
1061 case AccessibilitySearchTextDirection::Forward:
1062 return rangeOfStringClosestToRangeInDirection(referenceRange, AccessibilitySearchDirection::Next, searchStrings);
1063 case AccessibilitySearchTextDirection::Backward:
1064 return rangeOfStringClosestToRangeInDirection(referenceRange, AccessibilitySearchDirection::Previous, searchStrings);
1065 case AccessibilitySearchTextDirection::Closest: {
1066 std::optional foundAfter = rangeOfStringClosestToRangeInDirection(referenceRange, AccessibilitySearchDirection::Next, searchStrings);
1067 std::optional foundBefore = rangeOfStringClosestToRangeInDirection(referenceRange, AccessibilitySearchDirection::Previous, searchStrings);
1068 return rangeClosestToRange(referenceRange, WTF::move(foundAfter), WTF::move(foundBefore));
1069 }
1070 case AccessibilitySearchTextDirection::All: {
1071 // This function should never be called with the All variant, as we don't handle it properly at this time.
1072 ASSERT_NOT_REACHED()((void)0);
1073 break;
1074 }
1075 }
1076
1077 return std::nullopt;
1078 };
1079
1080 std::optional found = findRange(startRange);
1081 while (found && *found == startRange) {
1082 // We must make progress in some direction (forward or backward) — callers do not expect the returned
1083 // range to be the same as the range given as a parameter, and could infinitely loop if this happens.
1084 // Loop until we have moved to a new range.
1085
1086 // To achieve this, we slide the range to the next or previous node (depending on the direction).
1087 SimpleRange movedStartRange = startRange;
1088 if (direction == AccessibilitySearchTextDirection::Forward) {
1089 Ref currentNode = startRange.start.container.get();
1090 if (RefPtr nextNode = NodeTraversal::next(currentNode.get()))
1091 movedStartRange = { { *nextNode, 0 }, startRange.end };
1092 else
1093 break;
1094 } else if (direction == AccessibilitySearchTextDirection::Backward) {
1095 Ref currentNode = startRange.end.container.get();
1096 if (RefPtr previousNode = NodeTraversal::previous(currentNode.get())) {
1097 unsigned length = previousNode->isCharacterDataNode() ? previousNode->length() : previousNode->countChildNodes();
1098 movedStartRange = { startRange.start, { *previousNode, length } };
1099 } else
1100 break;
1101 } else {
1102 // For other directions, try advancing forward first, then backward if that fails.
1103 Ref startNode = startRange.start.container.get();
1104 if (RefPtr nextNode = NodeTraversal::next(startNode.get()))
1105 movedStartRange = { { *nextNode, 0 }, startRange.end };
1106 else {
1107 Ref endNode = startRange.end.container.get();
1108 if (RefPtr<Node> previousNode = NodeTraversal::previous(endNode.get())) {
1109 unsigned length = previousNode->isCharacterDataNode() ? previousNode->length() : previousNode->countChildNodes();
1110 movedStartRange = { startRange.start, { *previousNode, length } };
1111 } else
1112 break;
1113 }
1114 }
1115
1116 found = findRange(movedStartRange);
1117 }
1118
1119 if (found) {
1120 // If the search started within a text control, ensure that the result is inside that element.
1121 if (element() && element()->isTextField()) {
1122 if (!found->startContainer().isShadowIncludingDescendantOf(element())
1123 || !found->endContainer().isShadowIncludingDescendantOf(element()))
1124 return std::nullopt;
1125 }
1126 }
1127 return found;
1128}
1129
1130Vector<SimpleRange> AccessibilityObject::findTextRanges(const AccessibilitySearchTextCriteria& criteria) const
1131{
1132 std::optional<SimpleRange> range;
1133 if (criteria.start == AccessibilitySearchTextStartFrom::Selection)
1134 range = selectionRange();
1135 else
1136 range = simpleRange();
1137 if (!range)
1138 return { };
1139
1140 if (criteria.start == AccessibilitySearchTextStartFrom::Begin)
1141 range->end = range->start;
1142 else if (criteria.start == AccessibilitySearchTextStartFrom::End)
1143 range->start = range->end;
1144 else if (criteria.direction == AccessibilitySearchTextDirection::Backward)
1145 range->start = range->end;
1146 else
1147 range->end = range->start;
1148
1149 Vector<SimpleRange> result;
1150 switch (criteria.direction) {
1151 case AccessibilitySearchTextDirection::Forward:
1152 case AccessibilitySearchTextDirection::Backward:
1153 case AccessibilitySearchTextDirection::Closest:
1154 if (auto foundRange = findTextRange(criteria.searchStrings, *range, criteria.direction))
1155 result.append(*foundRange);
1156 break;
1157 case AccessibilitySearchTextDirection::All:
1158 auto appendFoundRanges = [&](AccessibilitySearchTextDirection direction) {
1159 for (std::optional foundRange = range; (foundRange = findTextRange(criteria.searchStrings, *foundRange, direction)); )
1160 result.append(*foundRange);
1161 };
1162 appendFoundRanges(AccessibilitySearchTextDirection::Forward);
1163 appendFoundRanges(AccessibilitySearchTextDirection::Backward);
1164 break;
1165 }
1166 return result;
1167}
1168
1169struct TextOperationRange {
1170 SimpleRange scope;
1171 CharacterRange characterRange;
1172};
1173
1174static std::optional<TextOperationRange> textOperationRangeFromRange(const SimpleRange& range)
1175{
1176 RefPtr<Element> rootEditableElement = range.startContainer().rootEditableElement();
1177 if (!rootEditableElement)
1178 return std::nullopt;
1179
1180 auto scopeStart = firstPositionInNode(*rootEditableElement);
1181 auto scopeEnd = lastPositionInNode(*rootEditableElement);
1182
1183 std::optional<SimpleRange> scope = makeSimpleRange(scopeStart, scopeEnd);
1184 if (!scope)
1185 return std::nullopt;
1186
1187 return TextOperationRange { *scope, characterRange(*scope, range, { }) };
1188}
1189
1190static SimpleRange rangeFromTextOperationRange(const TextOperationRange& textOperationRange)
1191{
1192 return resolveCharacterRange(textOperationRange.scope, textOperationRange.characterRange, { });
1193}
1194
1195Vector<String> AccessibilityObject::performTextOperation(const AccessibilityTextOperation& operation)
1196{
1197 Vector<TextOperationRange> textOperationRanges;
1198 textOperationRanges.reserveInitialCapacity(operation.textRanges.size());
1199
1200 Vector<String> result;
1201 result.reserveInitialCapacity(operation.textRanges.size());
1202
1203 if (operation.textRanges.isEmpty())
1204 return result;
1205
1206 RefPtr frame = this->frame();
1207 if (!frame)
1208 return result;
1209
1210 size_t replacementStringsCount = operation.replacementStrings.size();
1211 bool useFirstReplacementStringForAllReplacements = (replacementStringsCount == 1);
1212
1213 // Precompute character ranges with respect to their root editable element because
1214 // the SimpleRanges stored in AccessibilityTextOperation may be invalidated after
1215 // performing a replacement in the same editable element.
1216 for (const auto& range : operation.textRanges) {
1217 auto textOperationRange = textOperationRangeFromRange(range);
1218 if (!textOperationRange) {
1219 AX_ASSERT_NOT_REACHED()((void)0);
1220 return result;
1221 }
1222
1223 textOperationRanges.append(*textOperationRange);
1224 }
1225
1226 for (size_t i = 0; i < textOperationRanges.size(); ++i) {
1227 const auto& textOperationRange = textOperationRanges[i];
1228 auto textRange = rangeFromTextOperationRange(textOperationRange);
1229
1230 String replacementString;
1231 if (useFirstReplacementStringForAllReplacements)
1232 replacementString = operation.replacementStrings[0];
1233 else if (i < replacementStringsCount)
1234 replacementString = operation.replacementStrings[i];
1235
1236 if (!frame->selection().setSelectedRange(textRange, Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes))
1237 continue;
1238
1239 String text = plainText(textRange);
1240 bool replaceSelection = false;
1241 switch (operation.type) {
1242 case AccessibilityTextOperationType::Capitalize:
1243 replacementString = capitalize(text); // FIXME: Needs to take locale into account to work correctly.
1244 replaceSelection = true;
1245 break;
1246 case AccessibilityTextOperationType::Uppercase:
1247 replacementString = text.convertToUppercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
1248 replaceSelection = true;
1249 break;
1250 case AccessibilityTextOperationType::Lowercase:
1251 replacementString = text.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
1252 replaceSelection = true;
1253 break;
1254 case AccessibilityTextOperationType::Replace: {
1255 replaceSelection = true;
1256 // When applying find and replace activities, we want to match the capitalization of the replaced text,
1257 // (unless we're replacing with an abbreviation.)
1258 if (text.length() > 0
1259 && replacementString.length() > 2
1260 && replacementString != replacementString.convertToUppercaseWithoutLocale()) {
1261 if (text[0] == u_toupper(text[0]))
1262 replacementString = capitalize(replacementString); // FIXME: Needs to take locale into account to work correctly.
1263 else
1264 replacementString = replacementString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly.
1265 }
1266 break;
1267 }
1268 case AccessibilityTextOperationType::ReplacePreserveCase:
1269 replaceSelection = true;
1270 break;
1271 case AccessibilityTextOperationType::Select:
1272 break;
1273 }
1274
1275 // A bit obvious, but worth noting the API contract for this method is that we should
1276 // return the replacement string when replacing, but the selected string if not.
1277 if (replaceSelection) {
1278 // Insert text instead of replacing when the selection length is zero, because replacements
1279 // aren't performed correctly in certain edge cases like at the the boundary between nodes
1280 // separated by spaces <p> foo <i>bar</i>[insert here] baz </p>.
1281 if (textOperationRange.characterRange.length)
1282 frame->editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::Yes, operation.smartReplace == AccessibilityTextOperationSmartReplace::No ? Editor::SmartReplace::No : Editor::SmartReplace::Yes);
1283 else
1284 frame->editor().insertText(replacementString, /* triggeringEvent */ nullptr);
1285
1286 result.append(replacementString);
1287 } else
1288 result.append(text);
1289 }
1290
1291 return result;
1292}
1293
1294String AccessibilityObject::altTextFromAttributeOrStyle() const
1295{
1296 const auto& alt = getAttribute(altAttr);
1297 if (!alt.isNull()) {
1298 // Note that !isNull() is explicitly chosen over !isEmpty(), as alt="" is a meaningful value
1299 // and should be respected.
1300 return alt;
1301 }
1302
1303 CheckedPtr style = this->style();
1304 return style ? style->content().altText() : nullString();
1305}
1306
1307bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole)
1308{
1309 switch (ariaRole) {
1310 case AccessibilityRole::Checkbox:
1311 case AccessibilityRole::RadioButton:
1312 case AccessibilityRole::SearchField:
1313 case AccessibilityRole::Switch:
1314 case AccessibilityRole::TextField:
1315 return true;
1316 default:
1317 return false;
1318 }
1319}
1320
1321bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole)
1322{
1323 if (isARIAInput(ariaRole))
1324 return true;
1325
1326 switch (ariaRole) {
1327 case AccessibilityRole::Button:
1328 case AccessibilityRole::ComboBox:
1329 case AccessibilityRole::ListBox:
1330 case AccessibilityRole::PopUpButton:
1331 case AccessibilityRole::Slider:
1332 case AccessibilityRole::TextArea:
1333 case AccessibilityRole::ToggleButton:
1334 return true;
1335 default:
1336 return false;
1337 }
1338}
1339
1340bool AccessibilityObject::supportsCheckedState() const
1341{
1342 if (hasTreeItemRole())
1343 return hasAttribute(aria_checkedAttr);
1344
1345 auto role = this->role();
1346 return isCheckboxOrRadio()
1347 || role == AccessibilityRole::MenuItemCheckbox
1348 || role == AccessibilityRole::MenuItemRadio
1349 || role == AccessibilityRole::Switch
1350 || isToggleButton();
1351}
1352
1353bool AccessibilityObject::isRangeControl() const
1354{
1355 switch (role()) {
1356 case AccessibilityRole::Meter:
1357 case AccessibilityRole::ProgressIndicator:
1358 case AccessibilityRole::Slider:
1359 case AccessibilityRole::ScrollBar:
1360 case AccessibilityRole::SpinButton:
1361 return true;
1362 case AccessibilityRole::Splitter:
1363 return canSetFocusAttribute();
1364 default:
1365 return false;
1366 }
1367}
1368
1369static IntRect boundsForRects(const LayoutRect& rect1, const LayoutRect& rect2, const SimpleRange& dataRange)
1370{
1371 LayoutRect ourRect = rect1;
1372 ourRect.unite(rect2);
1373
1374 // If the rectangle spans lines and contains multiple text characters, use the range's bounding box intead.
1375 if (rect1.maxY() != rect2.maxY() && characterCount(dataRange) > 1) {
1376 if (auto boundingBox = unionRect(RenderObject::absoluteTextRects(dataRange)); !boundingBox.isEmpty())
1377 ourRect = boundingBox;
1378 }
1379
1380 return snappedIntRect(ourRect);
1381}
1382
1383IntRect AccessibilityRenderObject::boundsForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const
1384{
1385 if (visiblePositionRange.isNull())
1386 return IntRect();
1387
1388 // Create a mutable VisiblePositionRange.
1389 VisiblePositionRange range(visiblePositionRange);
1390 LayoutRect rect1 = range.start.absoluteCaretBounds();
1391 LayoutRect rect2 = range.end.absoluteCaretBounds();
1392
1393 // 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
1394 if (rect2.y() != rect1.y()) {
1395 VisiblePosition endOfFirstLine = endOfLine(range.start);
1396 if (range.start == endOfFirstLine) {
1397 range.start.setAffinity(Affinity::Downstream);
1398 rect1 = range.start.absoluteCaretBounds();
1399 }
1400 if (range.end == endOfFirstLine) {
1401 range.end.setAffinity(Affinity::Upstream);
1402 rect2 = range.end.absoluteCaretBounds();
1403 }
1404 }
1405
1406 return boundsForRects(rect1, rect2, *makeSimpleRange(range));
1407}
1408
1409IntRect AccessibilityObject::boundsForRange(const SimpleRange& range) const
1410{
1411 CheckedPtr cache = axObjectCache();
1412 if (!cache)
1413 return { };
1414
1415 auto start = cache->startOrEndCharacterOffsetForRange(range, true);
1416 auto end = cache->startOrEndCharacterOffsetForRange(range, false);
1417
1418 auto rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
1419 auto rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
1420
1421 // 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.
1422 if (rect2.y() != rect1.y()) {
1423 auto endOfFirstLine = cache->endCharacterOffsetOfLine(start);
1424 if (start.isEqual(endOfFirstLine)) {
1425 start = cache->nextCharacterOffset(start, false);
1426 rect1 = cache->absoluteCaretBoundsForCharacterOffset(start);
1427 }
1428 if (end.isEqual(endOfFirstLine)) {
1429 end = cache->previousCharacterOffset(end, false);
1430 rect2 = cache->absoluteCaretBoundsForCharacterOffset(end);
1431 }
1432 }
1433
1434 return boundsForRects(rect1, rect2, range);
1435}
1436
1437IntPoint AccessibilityObject::linkClickPoint()
1438{
1439 AX_ASSERT(isLink())((void)0);
1440 /* A link bounding rect can contain points that are not part of the link.
1441 For instance, a link that starts at the end of a line and finishes at the
1442 beginning of the next line will have a bounding rect that includes the
1443 entire two lines. In such a case, the middle point of the bounding rect
1444 may not belong to the link element and thus may not activate the link.
1445 Hence, return the middle point of the first character in the link if exists.
1446 */
1447 if (auto range = simpleRange()) {
1448 auto start = VisiblePosition { makeContainerOffsetPosition(range->start) };
1449 auto end = start.next();
1450 if (contains<ComposedTree>(*range, makeBoundaryPoint(end)))
1451 return { boundsForRange(*makeSimpleRange(start, end)).center() };
1452 }
1453 return clickPointFromElementRect();
1454}
1455
1456IntPoint AccessibilityObject::clickPoint()
1457{
1458 // Headings are usually much wider than their textual content. If the mid point is used, often it can be wrong.
1459 if (isHeading()) {
1460 const auto& children = unignoredChildren();
1461 if (children.size() == 1)
1462 return children.first()->clickPoint();
1463 }
1464
1465 if (isLink())
1466 return linkClickPoint();
1467
1468 // use the default position unless this is an editable web area, in which case we use the selection bounds.
1469 if (!isWebArea() || !canSetValueAttribute())
1470 return clickPointFromElementRect();
1471
1472 return boundsForVisiblePositionRange(selection()).center();
1473}
1474
1475IntPoint AccessibilityObject::clickPointFromElementRect() const
1476{
1477 return roundedIntPoint(elementRect().center());
1478}
1479
1480IntRect AccessibilityObject::boundingBoxForQuads(RenderObject* obj, const Vector<FloatQuad>& quads)
1481{
1482 AX_ASSERT(obj)((void)0);
1483 if (!obj)
1484 return IntRect();
1485
1486 FloatRect result;
1487 for (const auto& quad : quads) {
1488 FloatRect r = quad.enclosingBoundingBox();
1489 if (!r.isEmpty()) {
1490 if (obj->style().hasUsedAppearance())
1491 obj->theme().inflateRectForControlRenderer(downcast<RenderElement>(*obj), r);
1492 result.unite(r);
1493 }
1494 }
1495 return snappedIntRect(LayoutRect(result));
1496}
1497
1498bool AccessibilityObject::press()
1499{
1500 // The presence of the actionElement will confirm whether we should even attempt a press.
1501 RefPtr actionElement = this->actionElement();
1502 if (!actionElement)
1503 return false;
1504 if (RefPtr frame = actionElement->document().frame())
1505 frame->loader().resetMultipleFormSubmissionProtection();
1506
1507 // Hit test at this location to determine if there is a sub-node element that should act
1508 // as the target of the action.
1509 RefPtr<Element> hitTestElement;
1510 RefPtr document = this->document();
1511 if (document) {
1512 constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active, HitTestRequest::Type::AccessibilityHitTest };
1513 HitTestResult hitTestResult { clickPoint() };
1514 document->hitTest(hitType, hitTestResult);
1515 if (RefPtr innerNode = hitTestResult.innerNode()) {
1516 if (RefPtr shadowHost = innerNode->shadowHost())
1517 hitTestElement = WTF::move(shadowHost);
1518 else if (RefPtr element = dynamicDowncast<Element>(*innerNode))
1519 hitTestElement = WTF::move(element);
1520 else
1521 hitTestElement = innerNode->parentElement();
1522 }
1523 }
1524
1525 // Prefer the actionElement instead of this node, if the actionElement is inside this node.
1526 RefPtr pressElement = this->element();
1527 if (!pressElement || actionElement->isDescendantOf(*pressElement))
1528 pressElement = actionElement;
1529
1530 AX_ASSERT(pressElement)((void)0);
1531 // Prefer the hit test element, if it is inside the target element.
1532 if (hitTestElement && hitTestElement->isDescendantOf(*pressElement))
1533 pressElement = WTF::move(hitTestElement);
1534
1535 UserGestureIndicator gestureIndicator(IsProcessingUserGesture::Yes, document.get());
1536
1537 bool dispatchedEvent = false;
1538#if PLATFORM(IOS_FAMILY)(defined WTF_PLATFORM_IOS_FAMILY && WTF_PLATFORM_IOS_FAMILY
)
1539 if (hasTouchEventListener())
1540 dispatchedEvent = dispatchTouchEvent();
1541#endif
1542
1543 if (dispatchedEvent)
1544 return true;
1545
1546 if (RefPtr input = dynamicDowncast<HTMLInputElement>(*actionElement)) {
1547 if (RefPtr inputType = input->isDateField() || input->isDateTimeLocalField() ? input->inputType() : nullptr) {
1548 inputType->handleAccessibilityActivation();
1549 return true;
1550 }
1551 }
1552
1553 return pressElement->accessKeyAction(true) || pressElement->dispatchSimulatedClick(nullptr, SendMouseUpDownEvents);
1554}
1555
1556bool AccessibilityObject::dispatchTouchEvent()
1557{
1558#if ENABLE(IOS_TOUCH_EVENTS)(defined 0 && 0)
1559 if (RefPtr frame = localMainFrame())
1560 return frame->eventHandler().dispatchSimulatedTouchEvent(clickPoint());
1561#endif
1562 return false;
1563}
1564
1565RefPtr<LocalFrame> AccessibilityObject::localMainFrame() const
1566{
1567 if (RefPtr page = this->page())
1568 return page->localMainFrame();
1569 return nullptr;
1570}
1571
1572Document* AccessibilityObject::topDocument() const
1573{
1574 if (!document())
1575 return nullptr;
1576 return document()->mainFrameDocument();
1577}
1578
1579RenderView* AccessibilityObject::topRenderer() const
1580{
1581 if (RefPtr topDocument = this->topDocument())
1582 return topDocument->renderView();
1583 return nullptr;
1584}
1585
1586unsigned AccessibilityObject::ariaLevel() const
1587{
1588 return std::max(0, integralAttribute(aria_levelAttr));
1589}
1590
1591String AccessibilityObject::language() const
1592{
1593 const auto& lang = getAttribute(langAttr);
1594 if (!lang.isEmpty())
1595 return lang;
1596
1597 if (isScrollArea() && !parentObject()) {
1598 // If this is the root, use the content language specified in the meta tag.
1599 if (RefPtr document = this->document())
1600 return document->contentLanguage();
1601 }
1602
1603 // This object has no language of its own.
1604 return nullAtom();
1605}
1606
1607VisiblePosition AccessibilityObject::visiblePositionForPoint(const IntPoint& point) const
1608{
1609 // convert absolute point to view coordinates
1610 CheckedPtr renderView = topRenderer();
1611 if (!renderView)
1612 return VisiblePosition();
1613
1614#if PLATFORM(MAC)(defined 1 && 1)
1615 auto* frameView = &renderView->frameView();
Local variable 'frameView' is unchecked and unsafe
1616#endif
1617
1618 RefPtr<Node> innerNode;
1619
1620 // Locate the node containing the point
1621 // FIXME: Remove this loop and instead add HitTestRequest::Type::AllowVisibleChildFrameContentOnly to the hit test request type.
1622 LayoutPoint pointResult;
1623 while (1) {
1624 LayoutPoint pointToUse;
1625#if PLATFORM(MAC)(defined 1 && 1)
1626 pointToUse = frameView->screenToContents(point);
1627#else
1628 pointToUse = point;
1629#endif
1630 constexpr OptionSet<HitTestRequest::Type> hitType { HitTestRequest::Type::ReadOnly, HitTestRequest::Type::Active };
1631 HitTestResult result { pointToUse };
1632 renderView->document().hitTest(hitType, result);
1633 innerNode = result.innerNode();
1634 if (!innerNode)
1635 return VisiblePosition();
1636
1637 CheckedPtr renderer = innerNode->renderer();
1638 if (!renderer)
1639 return VisiblePosition();
1640
1641 pointResult = result.localPoint();
1642
1643 // done if hit something other than a widget
1644 RefPtr renderWidget = dynamicDowncast<RenderWidget>(*renderer);
1645 if (!renderWidget)
1646 break;
1647
1648 // descend into widget (FRAME, IFRAME, OBJECT...)
1649 RefPtr widget = renderWidget->widget();
1650 RefPtr frameView = dynamicDowncast<LocalFrameView>(widget);
1651 if (!frameView)
1652 break;
1653 RefPtr document = frameView->frame().document();
1654 if (!document)
1655 break;
1656
1657 renderView = document->renderView();
1658#if PLATFORM(MAC)(defined 1 && 1)
1659 // FIXME: Can this be removed? This seems like a redundant assignment.
1660 frameView = downcast<LocalFrameView>(widget);
1661#endif
1662 }
1663
1664 return innerNode->renderer()->visiblePositionForPoint(pointResult, HitTestSource::User);
1665}
1666
1667VisiblePositionRange AccessibilityObject::visiblePositionRangeForUnorderedPositions(const VisiblePosition& visiblePos1, const VisiblePosition& visiblePos2) const
1668{
1669 if (visiblePos1.isNull() || visiblePos2.isNull())
1670 return VisiblePositionRange();
1671
1672 // If there's no common tree scope between positions, return early.
1673 if (!commonTreeScope(visiblePos1.deepEquivalent().deprecatedNode(), visiblePos2.deepEquivalent().deprecatedNode()))
1674 return VisiblePositionRange();
1675
1676 VisiblePosition startPos;
1677 VisiblePosition endPos;
1678 bool alreadyInOrder;
1679
1680 // upstream is ordered before downstream for the same position
1681 if (visiblePos1 == visiblePos2 && visiblePos2.affinity() == Affinity::Upstream)
1682 alreadyInOrder = false;
1683
1684 // use selection order to see if the positions are in order
1685 else
1686 alreadyInOrder = VisibleSelection(visiblePos1, visiblePos2).isBaseFirst();
1687
1688 if (alreadyInOrder) {
1689 startPos = visiblePos1;
1690 endPos = visiblePos2;
1691 } else {
1692 startPos = visiblePos2;
1693 endPos = visiblePos1;
1694 }
1695
1696 return { startPos, endPos };
1697}
1698
1699static VisiblePosition updateAXLineStartForVisiblePosition(const VisiblePosition& visiblePosition)
1700{
1701 // A line in the accessibility sense should include floating objects, such as aligned image, as part of a line.
1702 // So let's update the position to include that.
1703 VisiblePosition tempPosition;
1704 VisiblePosition startPosition = visiblePosition;
1705 while (true) {
1706 tempPosition = startPosition.previous();
1707 if (tempPosition.isNull())
1708 break;
1709 Position p = tempPosition.deepEquivalent();
1710 CheckedPtr renderer = p.deprecatedNode()->renderer();
1711 if (!renderer || (renderer->isRenderBlock() && !p.deprecatedEditingOffset()))
1712 break;
1713 if (!RenderedPosition(tempPosition).isNull())
1714 break;
1715 startPosition = tempPosition;
1716 }
1717
1718 return startPosition;
1719}
1720
1721VisiblePositionRange AccessibilityObject::leftLineVisiblePositionRange(const VisiblePosition& visiblePos) const
1722{
1723 if (visiblePos.isNull())
1724 return VisiblePositionRange();
1725
1726 // make a caret selection for the position before marker position (to make sure
1727 // we move off of a line start)
1728 VisiblePosition prevVisiblePos = visiblePos.previous();
1729 if (prevVisiblePos.isNull())
1730 return VisiblePositionRange();
1731
1732 VisiblePosition startPosition = startOfLine(prevVisiblePos);
1733
1734 // keep searching for a valid line start position. Unless the VisiblePosition is at the very beginning, there should
1735 // always be a valid line range. However, startOfLine will return null for position next to a floating object,
1736 // since floating object doesn't really belong to any line.
1737 // This check will reposition the marker before the floating object, to ensure we get a line start.
1738 if (startPosition.isNull()) {
1739 while (startPosition.isNull() && prevVisiblePos.isNotNull()) {
1740 prevVisiblePos = prevVisiblePos.previous();
1741 startPosition = startOfLine(prevVisiblePos);
1742 }
1743 } else
1744 startPosition = updateAXLineStartForVisiblePosition(startPosition);
1745
1746 return { startPosition, endOfLine(prevVisiblePos) };
1747}
1748
1749VisiblePositionRange AccessibilityObject::rightLineVisiblePositionRange(const VisiblePosition& visiblePos) const
1750{
1751 if (visiblePos.isNull())
1752 return VisiblePositionRange();
1753
1754 // make sure we move off of a line end
1755 VisiblePosition nextVisiblePos = visiblePos.next();
1756 if (nextVisiblePos.isNull())
1757 return VisiblePositionRange();
1758
1759 VisiblePosition startPosition = startOfLine(nextVisiblePos);
1760
1761 // fetch for a valid line start position
1762 if (startPosition.isNull()) {
1763 startPosition = visiblePos;
1764 nextVisiblePos = nextVisiblePos.next();
1765 } else
1766 startPosition = updateAXLineStartForVisiblePosition(startPosition);
1767
1768 VisiblePosition endPosition = endOfLine(nextVisiblePos);
1769
1770 // as long as the position hasn't reached the end of the doc, keep searching for a valid line end position
1771 // Unless the VisiblePosition is at the very end, there should always be a valid line range. However, endOfLine will
1772 // return null for position by a floating object, since floating object doesn't really belong to any line.
1773 // This check will reposition the marker after the floating object, to ensure we get a line end.
1774 while (endPosition.isNull() && nextVisiblePos.isNotNull()) {
1775 nextVisiblePos = nextVisiblePos.next();
1776 endPosition = endOfLine(nextVisiblePos);
1777 }
1778
1779 return { startPosition, endPosition };
1780}
1781
1782static VisiblePosition startOfStyleRange(const VisiblePosition& visiblePos)
1783{
1784 CheckedPtr renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer();
1785 CheckedPtr startRenderer = renderer;
1786 CheckedPtr style = &renderer->style();
1787
1788 // traverse backward by renderer to look for style change
1789 for (CheckedPtr r = renderer->previousInPreOrder(); r; r = r->previousInPreOrder()) {
1790 // skip non-leaf nodes
1791 if (r->firstChildSlow())
1792 continue;
1793
1794 // stop at style change
1795 if (&r->style() != style.get())
1796 break;
1797
1798 // remember match
1799 startRenderer = r;
1800 }
1801
1802 return firstPositionInOrBeforeNode(startRenderer->node());
1803}
1804
1805static VisiblePosition endOfStyleRange(const VisiblePosition& visiblePos)
1806{
1807 CheckedPtr renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer();
1808 CheckedPtr endRenderer = renderer;
1809 const CheckedRef style = renderer->style();
1810
1811 // traverse forward by renderer to look for style change
1812 for (CheckedPtr r = renderer->nextInPreOrder(); r; r = r->nextInPreOrder()) {
1813 // skip non-leaf nodes
1814 if (r->firstChildSlow())
1815 continue;
1816
1817 // stop at style change
1818 if (&r->style() != style.ptr())
1819 break;
1820
1821 // remember match
1822 endRenderer = r;
1823 }
1824
1825 return lastPositionInOrAfterNode(endRenderer->node());
1826}
1827
1828VisiblePositionRange AccessibilityObject::styleRangeForPosition(const VisiblePosition& visiblePos) const
1829{
1830 if (visiblePos.isNull())
1831 return { };
1832
1833 return { startOfStyleRange(visiblePos), endOfStyleRange(visiblePos) };
1834}
1835
1836// NOTE: Consider providing this utility method as AX API
1837VisiblePositionRange AccessibilityObject::visiblePositionRangeForRange(const CharacterRange& range) const
1838{
1839 if (range.location + range.length > getLengthForTextRange())
1840 return { };
1841
1842 auto startPosition = visiblePositionForIndex(range.location);
1843 startPosition.setAffinity(Affinity::Downstream);
1844 return { startPosition, visiblePositionForIndex(range.location + range.length) };
1845}
1846
1847std::optional<SimpleRange> AccessibilityObject::rangeForCharacterRange(const CharacterRange& range) const
1848{
1849 unsigned textLength = getLengthForTextRange();
1850 if (range.location + range.length > textLength)
1851 return std::nullopt;
1852
1853 // Avoid setting selection to uneditable parent node in FrameSelection::setSelectedRange. See webkit.org/b/206093.
1854 if (!range.location && !range.length && !textLength)
1855 return std::nullopt;
1856
1857 if (CheckedPtr cache = axObjectCache()) {
1858 auto start = cache->characterOffsetForIndex(range.location, this);
1859 auto end = cache->characterOffsetForIndex(range.location + range.length, this);
1860 return cache->rangeForUnorderedCharacterOffsets(start, end);
1861 }
1862 return std::nullopt;
1863}
1864
1865VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosition& visiblePosition) const
1866{
1867 auto start = startOfLine(visiblePosition);
1868 if (start.isNull())
1869 return { };
1870
1871 // Move from the given visiblePosition forward until it hits the start of the next line or cross over a line break.
1872 auto end = visiblePosition;
1873 while (end.isNotNull() && inSameLine(end, visiblePosition)) {
1874 auto next = end.next();
1875 if (stringForVisiblePositionRange({ end, next }).contains("\n"_s)) {
1876 // Return the range including the line break.
1877 return { start, next };
1878 }
1879 end = next;
1880 }
1881
1882 if (end.isNotNull())
1883 return { start, end };
1884
1885 // Fallback to the VisibleUnits endOfLine.
1886 return { start, endOfLine(visiblePosition) };
1887}
1888
1889#if PLATFORM(MAC)(defined 1 && 1)
1890AXTextMarkerRange AccessibilityObject::selectedTextMarkerRange() const
1891{
1892 auto visibleRange = selectedVisiblePositionRange();
1893 if (visibleRange.isNull())
1894 return { };
1895 return { visibleRange };
1896}
1897#endif // PLATFORM(MAC)
1898
1899bool AccessibilityObject::replacedNodeNeedsCharacter(Node& replacedNode)
1900{
1901 // we should always be given a rendered node and a replaced node, but be safe
1902 // replaced nodes are either attachments (widgets) or images
1903 if (!isRendererReplacedElement(replacedNode.renderer()) || replacedNode.isTextNode())
1904 return false;
1905
1906 // create an AX object, but skip it if it is not supposed to be seen
1907 if (CheckedPtr cache = replacedNode.renderer()->document().axObjectCache()) {
1908 if (RefPtr axObject = cache->getOrCreate(replacedNode))
1909 return !axObject->isIgnored();
1910 }
1911
1912 return true;
1913}
1914
1915#if ENABLE(MODEL_ELEMENT_ACCESSIBILITY)(defined 1 && 1)
1916
1917ModelPlayerAccessibilityChildren AccessibilityObject::modelElementChildren()
1918{
1919 RefPtr model = dynamicDowncast<HTMLModelElement>(node());
1920 if (!model)
1921 return { };
1922
1923 return model->accessibilityChildren();
1924}
1925
1926#endif
1927
1928// Finds a RenderListItem parent given a node.
1929static RenderListItem* NODELETE[[clang::annotate_type("webkit.nodelete")]] renderListItemContainer(Node* node)
1930{
1931 for (; node; node = node->parentNode()) {
1932 if (auto* listItem = dynamicDowncast<RenderListItem>(node->renderer()))
1933 return listItem;
1934 }
1935 return nullptr;
1936}
1937
1938// Returns the text representing a list marker taking into account the position of the text in the line of text.
1939static StringView lineStartListMarkerText(const RenderListItem* listItem, const VisiblePosition& startVisiblePosition, std::optional<StringView> markerText = std::nullopt)
1940{
1941 if (!listItem)
1942 return { };
1943
1944 if (!markerText)
1945 markerText = listItem->markerTextWithSuffix();
1946 if (markerText->isEmpty())
1947 return { };
1948
1949 // 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.
1950 if (!isStartOfLine(startVisiblePosition) || !inSameLine(startVisiblePosition, firstPositionInNode(*listItem->element())))
1951 return { };
1952 return *markerText;
1953}
1954
1955StringView AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, Position&& startPosition)
1956{
1957 CheckedPtr listItem = renderListItemContainer(node);
1958 if (!listItem)
1959 return { };
1960 // Creating a VisiblePosition and determining its relationship to a line of text can be expensive.
1961 // Thus perform that determination only if we have some text to return.
1962 auto markerText = listItem->markerTextWithSuffix();
1963 if (markerText.isEmpty())
1964 return { };
1965 return lineStartListMarkerText(listItem.get(), startPosition, markerText);
1966}
1967
1968String AccessibilityObject::textContentPrefixFromListMarker() const
1969{
1970 // The code below creates a VisiblePosition, which is very expensive. Only do this if there's
1971 // any chance we're actually associated with a list marker.
1972 if (!renderListItemContainer(node()))
1973 return { };
1974
1975 // Get the attributed string for range (0, 1) and then delete the last character,
1976 // in order to extract the list marker that was added as a prefix to the text content.
1977 std::optional<SimpleRange> firstCharacterRange = rangeForCharacterRange({ 0, 1 });
1978 if (firstCharacterRange) {
1979 String firstCharacterText = AXTextMarkerRange { firstCharacterRange }.toString();
1980 if (firstCharacterText.length() > 1)
1981 return firstCharacterText.left(firstCharacterText.length() - 1);
1982 }
1983 return { };
1984}
1985
1986bool AccessibilityObject::shouldCacheStringValue() const
1987{
1988 if (!isStaticText()) {
1989 // Quick non-virtual exit path (this only checks m_role — renderer() is virtual).
1990 return true;
1991 }
1992
1993 CheckedPtr renderer = this->renderer();
1994 if (!renderer || !is<RenderText>(*renderer))
1995 return true;
1996 // Only consider RenderTexts for now.
1997
1998 if (renderer->isAnonymous()) {
1999 CheckedPtr parent = renderer ? renderer->parent() : nullptr;
2000 if (is<PseudoElement>(parent ? parent->element() : nullptr)) {
2001 // RenderTexts descending from pseudo-elements (e.g. ::before) can have alt text that
2002 // we don't currently handle via text runs, and thus we must cache the string value.
2003 return true;
2004 }
2005 }
2006
2007 if (CheckedPtr containingBlock = renderer->containingBlock()) {
2008 // Check for ::first-letter, which would require some special handling to serve off the main-thread
2009 // that we don't have right now.
2010 if (containingBlock->style().hasPseudoStyle(PseudoElementType::FirstLetter))
2011 return true;
2012 if (containingBlock->isAnonymous()) {
2013 containingBlock = containingBlock->containingBlock();
2014 return containingBlock && containingBlock->style().hasPseudoStyle(PseudoElementType::FirstLetter);
2015 }
2016 }
2017 // Getting to the end means we can avoid caching string value.
2018 return false;
2019}
2020
2021String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange)
2022{
2023 auto range = makeSimpleRange(visiblePositionRange);
2024 if (!range)
2025 return { };
2026
2027 StringBuilder builder;
2028 TextIterator it = textIteratorIgnoringFullSizeKana(*range);
2029 for (; !it.atEnd(); it.advance()) {
2030 // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX)
2031 if (it.text().length()) {
2032 // Add a textual representation for list marker text.
2033 builder.append(lineStartListMarkerText(renderListItemContainer(it.node()), visiblePositionRange.start));
2034 it.appendTextToStringBuilder(builder);
2035 } else {
2036 // locate the node and starting offset for this replaced range
2037 if (replacedNodeNeedsCharacter(*it.node()))
2038 builder.append(objectReplacementCharacter);
2039 }
2040 }
2041 return builder.toString();
2042}
2043
2044VisiblePosition AccessibilityObject::nextLineEndPosition(const VisiblePosition& startPosition) const
2045{
2046 if (startPosition.isNull())
2047 return { };
2048
2049 // Move to the next position to ensure we move off a line end.
2050 auto nextPosition = startPosition.next();
2051 if (nextPosition.isNull())
2052 return { };
2053
2054 auto lineEndPosition = endOfLine(nextPosition);
2055 // As long as the position hasn't reached the end of the document, keep searching for a valid line
2056 // end position. Skip past null positions, as there are cases like when the position is next to a
2057 // floating object that'll return null for end of line. Also, in certain scenarios, like when one
2058 // position is editable and the other isn't (e.g. in mixed-contenteditable-visible-character-range-hang.html),
2059 // we may end up back at the same position we started at. This is never valid, so keep moving forward
2060 // trying to find the next line end.
2061 while ((lineEndPosition.isNull() || lineEndPosition == startPosition) && nextPosition.isNotNull()) {
2062 nextPosition = nextPosition.next();
2063 lineEndPosition = endOfLine(nextPosition);
2064 }
2065 return lineEndPosition;
2066}
2067
2068std::optional<VisiblePosition> AccessibilityObject::previousLineStartPositionInternal(const VisiblePosition& visiblePosition) const
2069{
2070 if (visiblePosition.isNull())
2071 return std::nullopt;
2072
2073 // Make sure we move off of a line start.
2074 auto previousVisiblePosition = visiblePosition.previous();
2075 if (previousVisiblePosition.isNull())
2076 return std::nullopt;
2077
2078 auto startPosition = startOfLine(previousVisiblePosition);
2079 // As long as the position hasn't reached the beginning of the document, keep searching for a valid line start position.
2080 // This avoids returning a null position when we shouldn't, like when a position is next to a floating object.
2081 if (startPosition.isNull()) {
2082 while (startPosition.isNull() && previousVisiblePosition.isNotNull()) {
2083 previousVisiblePosition = previousVisiblePosition.previous();
2084 startPosition = startOfLine(previousVisiblePosition);
2085 }
2086 } else
2087 startPosition = updateAXLineStartForVisiblePosition(startPosition);
2088
2089 return startPosition;
2090}
2091
2092bool AccessibilityObject::isVisited() const
2093{
2094 if (!isLink()) {
2095 // Note that this isLink() check is necessary in addition to the RenderStyle::isLink() check below, as multiple
2096 // renderers can share the same style, e.g. RenderTexts within a link take their parent's (the link) style.
2097 return false;
2098 }
2099
2100 CheckedPtr style = this->style();
2101 if (!style || !style->isLink())
2102 return false;
2103 return style->insideLink() == InsideLink::InsideVisited;
2104}
2105
2106// If you call node->hasEditableStyle() since that will return true if an ancestor is editable.
2107// This only returns true if this is the element that actually has the contentEditable attribute set.
2108bool AccessibilityObject::hasContentEditableAttributeSet() const
2109{
2110 RefPtr element = this->element();
2111 return element && contentEditableAttributeIsEnabled(*element);
2112}
2113
2114bool AccessibilityObject::dependsOnTextUnderElement() const
2115{
2116 switch (role()) {
2117 case AccessibilityRole::PopUpButton:
2118 // Native popup buttons should not use their descendant's text as a title. That value is retrieved through stringValue().
2119 if (hasElementName(ElementName::HTML_select))
2120 break;
2121 [[fallthrough]];
2122 case AccessibilityRole::Summary:
2123 case AccessibilityRole::Button:
2124 case AccessibilityRole::ToggleButton:
2125 case AccessibilityRole::Checkbox:
2126 case AccessibilityRole::ListBoxOption:
2127#if !PLATFORM(COCOA)(defined 1 && 1)
2128 // macOS does not expect native <li> elements to expose label information, it only expects leaf node elements to do that.
2129 case AccessibilityRole::ListItem:
2130#endif
2131 case AccessibilityRole::MenuItem:
2132 case AccessibilityRole::MenuItemCheckbox:
2133 case AccessibilityRole::MenuItemRadio:
2134 case AccessibilityRole::RadioButton:
2135 case AccessibilityRole::Switch:
2136 case AccessibilityRole::Tab:
2137 return true;
2138 default:
2139 break;
2140 }
2141
2142 // If it's focusable but it's not content editable or a known control type, then it will appear to
2143 // the user as a single atomic object, so we should use its text as the default title.
2144 if (isHeading() || isLink())
2145 return true;
2146
2147 return isOutput();
2148}
2149
2150bool AccessibilityObject::supportsReadOnly() const
2151{
2152 auto role = this->role();
2153 return role == AccessibilityRole::Checkbox
2154 || role == AccessibilityRole::ComboBox
2155 || role == AccessibilityRole::Grid
2156 || role == AccessibilityRole::GridCell
2157 || role == AccessibilityRole::ListBox
2158 || role == AccessibilityRole::MenuItemCheckbox
2159 || role == AccessibilityRole::MenuItemRadio
2160 || role == AccessibilityRole::RadioGroup
2161 || role == AccessibilityRole::SearchField
2162 || role == AccessibilityRole::Slider
2163 || role == AccessibilityRole::SpinButton
2164 || role == AccessibilityRole::Switch
2165 || role == AccessibilityRole::TextField
2166 || role == AccessibilityRole::TreeGrid
2167 || isColumnHeader()
2168 || isRowHeader()
2169 || isSecureField();
2170}
2171
2172String AccessibilityObject::readOnlyValue() const
2173{
2174 bool hasReadOnlyAttribute = hasAttribute(aria_readonlyAttr);
2175 if (!hasReadOnlyAttribute && isTableCell()) {
2176 // ARIA 1.1 requires user agents to propagate the grid's aria-readonly value to all
2177 // gridcell elements if the property is not present on the gridcell element itself.
2178 if (RefPtr parent = parentTable())
2179 return parent->readOnlyValue();
2180 }
2181
2182 if (!hasReadOnlyAttribute)
2183 return ariaRoleAttribute() != AccessibilityRole::Unknown && supportsReadOnly() ? "false"_s : String();
2184
2185 return getAttribute(aria_readonlyAttr).string().convertToASCIILowercase();
2186}
2187
2188String AccessibilityObject::explicitAutoCompleteValue() const
2189{
2190 const AtomString& autoComplete = getAttribute(aria_autocompleteAttr);
2191 if (equalLettersIgnoringASCIICase(autoComplete, "inline"_s)
2192 || equalLettersIgnoringASCIICase(autoComplete, "list"_s)
2193 || equalLettersIgnoringASCIICase(autoComplete, "both"_s))
2194 return autoComplete;
2195
2196 return { };
2197}
2198
2199bool AccessibilityObject::contentEditableAttributeIsEnabled(Element& element)
2200{
2201 const AtomString& contentEditableValue = element.attributeWithoutSynchronization(contenteditableAttr);
2202 if (contentEditableValue.isNull())
2203 return false;
2204
2205 if (auto* htmlElement = dynamicDowncast<HTMLElement>(&element)) {
2206 if (htmlElement->isTextControlInnerTextElement())
2207 return false;
2208 }
2209
2210 // All of "true", "plaintext-only", (both case-insensitive) and the empty string count as true for accessibility.
2211 // This needs to be consistent with contentEditableType(const AtomString&) from HTMLElement.cpp.
2212 return contentEditableValue.isEmpty() || equalLettersIgnoringASCIICase(contentEditableValue, "true"_s) || equalLettersIgnoringASCIICase(contentEditableValue, "plaintext-only"_s);
2213}
2214
2215int AccessibilityObject::lineForPosition(const VisiblePosition& visiblePos) const
2216{
2217 if (visiblePos.isNull() || !node())
2218 return -1;
2219
2220 // If the position is not in the same editable region as this AX object, return -1.
2221 RefPtr containerNode = visiblePos.deepEquivalent().containerNode();
2222 if (!containerNode->isShadowIncludingInclusiveAncestorOf(node()) && !node()->isShadowIncludingInclusiveAncestorOf(containerNode.get()))
2223 return -1;
2224
2225 int lineCount = -1;
2226 VisiblePosition currentVisiblePos = visiblePos;
2227 VisiblePosition savedVisiblePos;
2228
2229 // move up until we get to the top
2230 // FIXME: This only takes us to the top of the rootEditableElement, not the top of the
2231 // top document.
2232 do {
2233 savedVisiblePos = currentVisiblePos;
2234 currentVisiblePos = previousLinePosition(currentVisiblePos, 0, HasEditableAXRole);
2235 ++lineCount;
2236 } while (currentVisiblePos.isNotNull() && !(inSameLine(currentVisiblePos, savedVisiblePos)));
2237
2238 return lineCount;
2239}
2240
2241// NOTE: Consider providing this utility method as AX API
2242CharacterRange AccessibilityObject::plainTextRangeForVisiblePositionRange(const VisiblePositionRange& positionRange) const
2243{
2244 int index1 = index(positionRange.start);
2245 int index2 = index(positionRange.end);
2246 if (index1 < 0 || index2 < 0 || index1 > index2)
2247 return { };
2248
2249 return CharacterRange(index1, index2 - index1);
2250}
2251
2252// The composed character range in the text associated with this accessibility object that
2253// is specified by the given screen coordinates. This parameterized attribute returns the
2254// complete range of characters (including surrogate pairs of multi-byte glyphs) at the given
2255// screen coordinates.
2256// NOTE: This varies from AppKit when the point is below the last line. AppKit returns an
2257// an error in that case. We return textControl->text().length(), 1. Does this matter?
2258CharacterRange AccessibilityObject::characterRangeForPoint(const IntPoint& point) const
2259{
2260 int i = index(visiblePositionForPoint(point));
2261 if (i < 0)
2262 return { };
2263 return { static_cast<uint64_t>(i), 1 };
2264}
2265
2266// Given a character index, the range of text associated with this accessibility object
2267// over which the style in effect at that character index applies.
2268CharacterRange AccessibilityObject::doAXStyleRangeForIndex(unsigned index) const
2269{
2270 VisiblePositionRange range = styleRangeForPosition(visiblePositionForIndex(index, false));
2271 return plainTextRangeForVisiblePositionRange(range);
2272}
2273
2274// Given an indexed character, the line number of the text associated with this accessibility
2275// object that contains the character.
2276unsigned AccessibilityObject::doAXLineForIndex(unsigned index)
2277{
2278 return lineForPosition(visiblePositionForIndex(index, false));
2279}
2280
2281void AccessibilityObject::updateBackingStore()
2282{
2283 if (!axObjectCache())
2284 return;
2285
2286 // Updating the layout may delete this object.
2287 RefPtr<AccessibilityObject> protectedThis(this);
2288 if (RefPtr document = this->document()) {
2289 if (!Accessibility::inRenderTreeOrStyleUpdate(*document))
2290 document->updateLayoutIgnorePendingStylesheets();
2291 }
2292
2293 if (CheckedPtr cache = axObjectCache())
2294 cache->performDeferredCacheUpdate(ForceLayout::Yes);
2295
2296 updateChildrenIfNecessary();
2297}
2298
2299const AccessibilityScrollView* AccessibilityObject::ancestorAccessibilityScrollView(bool includeSelf) const
2300{
2301 return downcast<AccessibilityScrollView>(Accessibility::findAncestor<AccessibilityObject>(*this, includeSelf, [] (const auto& object) {
2302 return is<AccessibilityScrollView>(object);
2303 }));
2304}
2305
2306#if PLATFORM(COCOA)(defined 1 && 1)
2307RetainPtr<RemoteAXObjectRef> AccessibilityObject::remoteParent() const
2308{
2309 RefPtr document = this->document();
2310 RefPtr frame = document ? document->frame() : nullptr;
2311 return frame ? frame->loader().client().accessibilityRemoteObject() : nullptr;
2312}
2313#endif
2314
2315IntPoint AccessibilityObject::remoteFrameOffset() const
2316{
2317#if PLATFORM(COCOA)(defined 1 && 1)
2318 RefPtr document = this->document();
2319 RefPtr frame = document ? document->frame() : nullptr;
2320 return frame ? frame->loader().client().accessibilityRemoteFrameOffset() : IntPoint();
2321#else
2322 return IntPoint();
2323#endif
2324}
2325
2326Document* AccessibilityObject::document() const
2327{
2328 RefPtr frameView = documentFrameView();
2329 if (!frameView)
2330 return nullptr;
2331
2332 return frameView->frame().document();
2333}
2334
2335Page* AccessibilityObject::page() const
2336{
2337 RefPtr document = this->document();
2338 if (!document)
2339 return nullptr;
2340 return document->page();
2341}
2342
2343LocalFrameView* AccessibilityObject::documentFrameView() const
2344{
2345 RefPtr<const AccessibilityObject> object = this;
2346 while (object) {
2347 // Ascend until we find an ancestor with a valid renderer or node, from which we can
2348 // actually get a frameview.
2349 if (auto* axRenderObject = dynamicDowncast<AccessibilityRenderObject>(*object)) {
2350 if (axRenderObject->renderer() || axRenderObject->node()) {
2351 object = axRenderObject;
2352 break;
2353 }
2354 } else if (auto* axNodeObject = dynamicDowncast<AccessibilityNodeObject>(*object); axNodeObject && axNodeObject->node()) {
2355 object = axNodeObject;
2356 break;
2357 }
2358 object = object->parentObject();
2359 }
2360 return object ? object->documentFrameView() : nullptr;
2361}
2362
2363void AccessibilityObject::updateChildrenIfNecessary()
2364{
2365 if (!childrenInitialized()) {
2366 // Enable the cache in case we end up adding a lot of children, we don't want to recompute axIsIgnored each time.
2367 AXAttributeCacheScope enableCache(axObjectCache());
2368 addChildren();
2369 }
2370}
2371
2372void AccessibilityObject::clearChildren()
2373{
2374 // Some objects have weak pointers to their parents and those associations need to be detached.
2375 for (const auto& child : m_children)
2376 child->detachFromParent();
2377
2378 m_children.clear();
2379 m_childrenInitialized = false;
2380}
2381
2382AccessibilityObject* AccessibilityObject::anchorElementForNode(Node& node)
2383{
2384 CheckedPtr renderer = node.renderer();
2385 if (!renderer)
2386 return nullptr;
2387
2388 WeakPtr cache = renderer->document().axObjectCache();
2389 RefPtr axObject = cache ? cache->getOrCreate(node) : nullptr;
2390 RefPtr anchor = axObject ? axObject->anchorElement() : nullptr;
2391 return anchor ? cache->getOrCreate(*anchor) : nullptr;
2392}
2393
2394AccessibilityObject* AccessibilityObject::headingElementForNode(Node* node)
2395{
2396 if (!node)
2397 return nullptr;
2398
2399 CheckedPtr renderObject = node->renderer();
2400 if (!renderObject)
2401 return nullptr;
2402
2403 RefPtr axObject = renderObject->document().axObjectCache()->getOrCreate(*node);
2404
2405 return Accessibility::findAncestor<AccessibilityObject>(*axObject, true, [] (const AccessibilityObject& object) {
2406 return object.role() == AccessibilityRole::Heading;
2407 });
2408}
2409
2410AXCoreObject::AccessibilityChildrenVector AccessibilityObject::disclosedRows()
2411{
2412 AccessibilityChildrenVector result;
2413
2414 for (const auto& obj : unignoredChildren()) {
2415 // Add tree items as the rows.
2416 if (obj->role() == AccessibilityRole::TreeItem)
2417 result.append(obj);
2418 // If it's not a tree item, then descend into the group to find more tree items.
2419 else
2420 result.appendVector(obj->ariaTreeRows());
2421 }
2422
2423 return result;
2424}
2425
2426String AccessibilityObject::localizedActionVerb() const
2427{
2428#if !PLATFORM(IOS_FAMILY)(defined WTF_PLATFORM_IOS_FAMILY && WTF_PLATFORM_IOS_FAMILY
)
2429 // FIXME: Need to add verbs for select elements.
2430 static NeverDestroyed<const String> buttonAction(AXButtonActionVerb());
2431 static NeverDestroyed<const String> textFieldAction(AXTextFieldActionVerb());
2432 static NeverDestroyed<const String> radioButtonAction(AXRadioButtonActionVerb());
2433 static NeverDestroyed<const String> checkedCheckboxAction(AXCheckedCheckboxActionVerb());
2434 static NeverDestroyed<const String> uncheckedCheckboxAction(AXUncheckedCheckboxActionVerb());
2435 static NeverDestroyed<const String> linkAction(AXLinkActionVerb());
2436 static NeverDestroyed<const String> menuListAction(AXMenuListActionVerb());
2437 static NeverDestroyed<const String> menuListPopupAction(AXMenuListPopupActionVerb());
2438 static NeverDestroyed<const String> listItemAction(AXListItemActionVerb());
2439
2440 switch (role()) {
2441 case AccessibilityRole::Button:
2442 case AccessibilityRole::ToggleButton:
2443 return buttonAction;
2444 case AccessibilityRole::TextField:
2445 case AccessibilityRole::TextArea:
2446 return textFieldAction;
2447 case AccessibilityRole::RadioButton:
2448 return radioButtonAction;
2449 case AccessibilityRole::Checkbox:
2450 case AccessibilityRole::Switch:
2451 return isChecked() ? checkedCheckboxAction : uncheckedCheckboxAction;
2452 case AccessibilityRole::Link:
2453 return linkAction;
2454 case AccessibilityRole::PopUpButton:
2455 return menuListAction;
2456 case AccessibilityRole::MenuListPopup:
2457 return menuListPopupAction;
2458 case AccessibilityRole::ListItem:
2459 return listItemAction;
2460 default:
2461 return nullAtom();
2462 }
2463#else
2464 return nullAtom();
2465#endif
2466}
2467
2468String AccessibilityObject::actionVerb() const
2469{
2470#if !PLATFORM(IOS_FAMILY)(defined WTF_PLATFORM_IOS_FAMILY && WTF_PLATFORM_IOS_FAMILY
)
2471 // FIXME: Need to add verbs for select elements.
2472 switch (role()) {
2473 case AccessibilityRole::Button:
2474 case AccessibilityRole::ToggleButton:
2475 return "press"_s;
2476 case AccessibilityRole::TextField:
2477 case AccessibilityRole::TextArea:
2478 return "activate"_s;
2479 case AccessibilityRole::RadioButton:
2480 return "select"_s;
2481 case AccessibilityRole::Checkbox:
2482 case AccessibilityRole::Switch:
2483 return isChecked() ? "uncheck"_s : "check"_s;
2484 case AccessibilityRole::Link:
2485 return "jump"_s;
2486 case AccessibilityRole::PopUpButton:
2487 case AccessibilityRole::MenuListPopup:
2488 case AccessibilityRole::ListItem:
2489 return "select"_s;
2490 default:
2491 break;
2492 }
2493#endif
2494 return { };
2495}
2496
2497String AccessibilityObject::explicitInvalidStatus() const
2498{
2499 static NeverDestroyed<String> grammarValue = "grammar"_s;
2500 static NeverDestroyed<String> falseValue = "false"_s;
2501 static NeverDestroyed<String> spellingValue = "spelling"_s;
2502 static NeverDestroyed<String> trueValue = "true"_s;
2503 static NeverDestroyed<String> undefinedValue = "undefined"_s;
2504
2505 // aria-invalid can return false (default), grammar, spelling, or true.
2506 auto ariaInvalid = getAttributeTrimmed(aria_invalidAttr);
2507
2508 if (ariaInvalid.isEmpty()) {
2509 auto* htmlElement = dynamicDowncast<HTMLElement>(this->node());
2510 if (RefPtr validatedFormListedElement = htmlElement ? htmlElement->asValidatedFormListedElement() : nullptr) {
2511 // "willValidate" is true if the element is able to be validated.
2512 if (validatedFormListedElement->willValidate() && !validatedFormListedElement->isValidFormControlElement())
2513 return trueValue;
2514 }
2515 return { };
2516 }
2517
2518 // If "false", "undefined" [sic, string value], empty, or missing, return "false".
2519 if (ariaInvalid == falseValue || ariaInvalid == undefinedValue)
2520 return falseValue;
2521 // Besides true/false/undefined, the only tokens defined by WAI-ARIA 1.0...
2522 // ...for @aria-invalid are "grammar" and "spelling".
2523 if (ariaInvalid == grammarValue)
2524 return grammarValue;
2525 if (ariaInvalid == spellingValue)
2526 return spellingValue;
2527 // Any other non empty string should be treated as "true".
2528 return trueValue;
2529}
2530
2531AccessibilityCurrentState AccessibilityObject::currentState() const
2532{
2533 // aria-current can return false (default), true, page, step, location, date or time.
2534 auto currentStateValue = getAttributeTrimmed(aria_currentAttr);
2535
2536 // If "false", empty, or missing, return false state.
2537 if (currentStateValue.isEmpty() || currentStateValue == "false"_s)
2538 return AccessibilityCurrentState::False;
2539
2540 if (currentStateValue == "page"_s)
2541 return AccessibilityCurrentState::Page;
2542 if (currentStateValue == "step"_s)
2543 return AccessibilityCurrentState::Step;
2544 if (currentStateValue == "location"_s)
2545 return AccessibilityCurrentState::Location;
2546 if (currentStateValue == "date"_s)
2547 return AccessibilityCurrentState::Date;
2548 if (currentStateValue == "time"_s)
2549 return AccessibilityCurrentState::Time;
2550
2551 // Any value not included in the list of allowed values should be treated as "true".
2552 return AccessibilityCurrentState::True;
2553}
2554
2555bool AccessibilityObject::isModalDescendant(Node& modalNode) const
2556{
2557 RefPtr node = this->node();
2558 // ARIA 1.1 aria-modal, indicates whether an element is modal when displayed.
2559 // For the decendants of the modal object, they should also be considered as aria-modal=true.
2560 // Determine descendancy by iterating the composed tree which inherently accounts for shadow roots and slots.
2561 for (RefPtr ancestor = node.get(); ancestor; ancestor = ancestor->parentInComposedTree()) {
2562 if (ancestor.get() == &modalNode)
2563 return true;
2564 }
2565 return false;
2566}
2567
2568bool AccessibilityObject::isModalNode() const
2569{
2570 if (CheckedPtr cache = axObjectCache())
2571 return node() && cache->modalNode() == node();
2572
2573 return false;
2574}
2575
2576
2577static CheckedPtr<RenderObject> nearestRendererFromNode(Node& node)
2578{
2579 CheckedPtr renderer = node.renderer();
2580 for (RefPtr ancestor = &node; ancestor && !renderer; ancestor = composedParentIgnoringDocumentFragments(*ancestor))
2581 renderer = ancestor->renderer();
2582
2583 return renderer;
2584}
2585
2586static int zIndexFromRenderer(RenderObject* renderer)
2587{
2588 for (CheckedPtr layer = renderer->enclosingLayer(); layer; layer = layer->parent()) {
2589 if (int zIndex = layer->zIndex())
2590 return zIndex;
2591 }
2592
2593 return 0;
2594}
2595
2596bool AccessibilityObject::ignoredFromModalPresence() const
2597{
2598 // We shouldn't ignore the top node.
2599 if (!node() || !node()->parentNode())
2600 return false;
2601
2602 CheckedPtr cache = axObjectCache();
2603 if (!cache)
2604 return false;
2605
2606 // modalNode is the current displayed modal dialog.
2607 RefPtr modalNode = cache->modalNode();
2608 if (!modalNode)
2609 return false;
2610
2611 // We only want to ignore the objects within the same frame as the modal dialog.
2612 if (modalNode->document().frame() != this->frame())
2613 return false;
2614
2615 // Some objects might be outside of a modal, but are linked to elements inside of it. Don't ignore those.
2616 for (RefPtr ancestor = this; ancestor; ancestor = ancestor->parentObject()) {
2617 for (auto& controller : ancestor->controllers()) {
2618 if (downcast<AccessibilityObject>(controller)->isModalDescendant(*modalNode))
2619 return false;
2620 }
2621
2622 for (auto& activeDescendant : ancestor->activeDescendantOfObjects()) {
2623 if (downcast<AccessibilityObject>(activeDescendant)->isModalDescendant(*modalNode))
2624 return false;
2625 }
2626 }
2627
2628 // If this element has a higher z-index than the active modal, also don't ignore it.
2629 if (CheckedPtr renderer = this->rendererOrNearestAncestor()) {
2630 if (CheckedPtr modalRenderer = nearestRendererFromNode(*modalNode)) {
2631 int thisZIndex = zIndexFromRenderer(renderer.get());
2632 int modalZIndex = zIndexFromRenderer(modalRenderer.get());
2633 if (thisZIndex > modalZIndex)
2634 return false;
2635 }
2636 }
2637
2638 return !isModalDescendant(*modalNode);
2639}
2640
2641bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const
2642{
2643 RefPtr element = this->element();
2644 if (!element)
2645 return false;
2646
2647 if (element->hasAttributeWithoutSynchronization(attribute))
2648 return true;
2649
2650 if (CheckedPtr defaultARIA = element->customElementDefaultARIAIfExists()) {
2651 // We do not want to use CustomElementDefaultARIA::hasAttribute here, as it returns true
2652 // even if the author has set the attribute to null (e.g. this.internals.ariaValueNow = null),
2653 // which should be treated the same as removing the attribute.
2654 return !defaultARIA->valueForAttribute(*element, attribute).isNull();
2655 }
2656
2657 return false;
2658}
2659
2660const AtomString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const
2661{
2662 RefPtr element = this->element();
2663 return element ? element->attributeWithDefaultARIA(attribute) : nullAtom();
2664}
2665
2666String AccessibilityObject::getAttributeTrimmed(const QualifiedName& attribute) const
2667{
2668 const auto& rawValue = getAttribute(attribute);
2669 if (rawValue.isEmpty())
2670 return { };
2671 auto value = rawValue.string();
2672 return value.trim(isASCIIWhitespace).simplifyWhiteSpace(isASCIIWhitespace);
2673}
2674
2675bool AccessibilityObject::replaceTextInRange(const String& replacementString, const CharacterRange& range)
2676{
2677 // If this is being called on the web area, redirect it to be on the body, which will have a renderer associated with it.
2678 if (RefPtr document = dynamicDowncast<Document>(node())) {
2679 if (RefPtr bodyObject = axObjectCache()->getOrCreate(document->body()))
2680 return bodyObject->replaceTextInRange(replacementString, range);
2681 return false;
2682 }
2683
2684 // FIXME: This checks node() is an Element, but below we assume that means renderer()->node() is an element.
2685 if (!renderer() || !is<Element>(node()))
2686 return false;
2687
2688 Ref element = downcast<Element>(*renderer()->node());
2689
2690 // We should use the editor's insertText to mimic typing into the field.
2691 // Also only do this when the field is in editing mode.
2692 Ref frame = renderer()->frame();
2693 if (element->shouldUseInputMethod()) {
2694 frame->selection().setSelectedRange(rangeForCharacterRange(range), Affinity::Downstream, FrameSelection::ShouldCloseTyping::Yes);
2695 frame->editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::No, Editor::SmartReplace::No);
2696 return true;
2697 }
2698
2699 if (RefPtr input = dynamicDowncast<HTMLInputElement>(element)) {
2700 input->setRangeText(replacementString, range.location, range.length, emptyString());
2701 return true;
2702 }
2703 if (RefPtr textarea = dynamicDowncast<HTMLTextAreaElement>(element)) {
2704 textarea->setRangeText(replacementString, range.location, range.length, emptyString());
2705 return true;
2706 }
2707
2708 return false;
2709}
2710
2711bool AccessibilityObject::insertText(const String& text)
2712{
2713 AXTRACE(makeString("AccessibilityObject::insertText text = "_s, text))(void)0;
2714
2715 if (!renderer())
2716 return false;
2717
2718 RefPtr element = dynamicDowncast<Element>(node());
2719 if (!element)
2720 return false;
2721
2722 // 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).
2723 if (!isSecureField() && !element->shouldUseInputMethod())
2724 return false;
2725
2726 // Use Editor::insertText to mimic typing into the field.
2727 Ref editor = renderer()->frame().editor();
2728 return editor->insertText(text, nullptr);
2729}
2730
2731using ARIARoleMap = HashMap<String, AccessibilityRole, ASCIICaseInsensitiveHash>;
2732using ARIAReverseRoleMap = HashMap<AccessibilityRole, String, DefaultHash<int>, WTF::UnsignedWithZeroKeyHashTraits<int>>;
2733
2734static ARIARoleMap* gAriaRoleMap = nullptr;
2735static ARIAReverseRoleMap* gAriaReverseRoleMap = nullptr;
2736
2737struct RoleEntry {
2738 String ariaRole;
2739 AccessibilityRole webcoreRole;
2740};
2741
2742static void initializeRoleMap()
2743{
2744 if (gAriaRoleMap)
2745 return;
2746 AX_ASSERT(!gAriaReverseRoleMap)((void)0);
2747
2748 const std::array roles {
2749 RoleEntry { "alert"_s, AccessibilityRole::ApplicationAlert },
2750 RoleEntry { "alertdialog"_s, AccessibilityRole::ApplicationAlertDialog },
2751 RoleEntry { "application"_s, AccessibilityRole::WebApplication },
2752 RoleEntry { "article"_s, AccessibilityRole::DocumentArticle },
2753 RoleEntry { "banner"_s, AccessibilityRole::LandmarkBanner },
2754 RoleEntry { "blockquote"_s, AccessibilityRole::Blockquote },
2755 RoleEntry { "button"_s, AccessibilityRole::Button },
2756 RoleEntry { "caption"_s, AccessibilityRole::Caption },
2757 RoleEntry { "code"_s, AccessibilityRole::Code },
2758 RoleEntry { "checkbox"_s, AccessibilityRole::Checkbox },
2759 RoleEntry { "complementary"_s, AccessibilityRole::LandmarkComplementary },
2760 RoleEntry { "contentinfo"_s, AccessibilityRole::LandmarkContentInfo },
2761 RoleEntry { "deletion"_s, AccessibilityRole::Deletion },
2762 RoleEntry { "dialog"_s, AccessibilityRole::ApplicationDialog },
2763 RoleEntry { "directory"_s, AccessibilityRole::Directory },
2764 // The 'doc-*' roles are defined the ARIA DPUB mobile: https://www.w3.org/TR/dpub-aam-1.0
2765 // Editor's draft is currently at https://w3c.github.io/dpub-aam
2766 RoleEntry { "doc-abstract"_s, AccessibilityRole::TextGroup },
2767 RoleEntry { "doc-acknowledgments"_s, AccessibilityRole::LandmarkDocRegion },
2768 RoleEntry { "doc-afterword"_s, AccessibilityRole::LandmarkDocRegion },
2769 RoleEntry { "doc-appendix"_s, AccessibilityRole::LandmarkDocRegion },
2770 RoleEntry { "doc-backlink"_s, AccessibilityRole::Link },
2771 RoleEntry { "doc-biblioentry"_s, AccessibilityRole::ListItem },
2772 RoleEntry { "doc-bibliography"_s, AccessibilityRole::LandmarkDocRegion },
2773 RoleEntry { "doc-biblioref"_s, AccessibilityRole::Link },
2774 RoleEntry { "doc-chapter"_s, AccessibilityRole::LandmarkDocRegion },
2775 RoleEntry { "doc-colophon"_s, AccessibilityRole::TextGroup },
2776 RoleEntry { "doc-conclusion"_s, AccessibilityRole::LandmarkDocRegion },
2777 RoleEntry { "doc-cover"_s, AccessibilityRole::Image },
2778 RoleEntry { "doc-credit"_s, AccessibilityRole::TextGroup },
2779 RoleEntry { "doc-credits"_s, AccessibilityRole::LandmarkDocRegion },
2780 RoleEntry { "doc-dedication"_s, AccessibilityRole::TextGroup },
2781 RoleEntry { "doc-endnote"_s, AccessibilityRole::ListItem },
2782 RoleEntry { "doc-endnotes"_s, AccessibilityRole::LandmarkDocRegion },
2783 RoleEntry { "doc-epigraph"_s, AccessibilityRole::TextGroup },
2784 RoleEntry { "doc-epilogue"_s, AccessibilityRole::LandmarkDocRegion },
2785 RoleEntry { "doc-errata"_s, AccessibilityRole::LandmarkDocRegion },
2786 RoleEntry { "doc-example"_s, AccessibilityRole::TextGroup },
2787 RoleEntry { "doc-footnote"_s, AccessibilityRole::Footnote },
2788 RoleEntry { "doc-foreword"_s, AccessibilityRole::LandmarkDocRegion },
2789 RoleEntry { "doc-glossary"_s, AccessibilityRole::LandmarkDocRegion },
2790 RoleEntry { "doc-glossref"_s, AccessibilityRole::Link },
2791 RoleEntry { "doc-index"_s, AccessibilityRole::LandmarkNavigation },
2792 RoleEntry { "doc-introduction"_s, AccessibilityRole::LandmarkDocRegion },
2793 RoleEntry { "doc-noteref"_s, AccessibilityRole::Link },
2794 RoleEntry { "doc-notice"_s, AccessibilityRole::DocumentNote },
2795 RoleEntry { "doc-pagebreak"_s, AccessibilityRole::Splitter },
2796 RoleEntry { "doc-pagelist"_s, AccessibilityRole::LandmarkNavigation },
2797 RoleEntry { "doc-part"_s, AccessibilityRole::LandmarkDocRegion },
2798 RoleEntry { "doc-preface"_s, AccessibilityRole::LandmarkDocRegion },
2799 RoleEntry { "doc-prologue"_s, AccessibilityRole::LandmarkDocRegion },
2800 RoleEntry { "doc-pullquote"_s, AccessibilityRole::TextGroup },
2801 RoleEntry { "doc-qna"_s, AccessibilityRole::TextGroup },
2802 RoleEntry { "doc-subtitle"_s, AccessibilityRole::Heading },
2803 RoleEntry { "doc-tip"_s, AccessibilityRole::DocumentNote },
2804 RoleEntry { "doc-toc"_s, AccessibilityRole::LandmarkNavigation },
2805 RoleEntry { "emphasis"_s, AccessibilityRole::Emphasis },
2806 RoleEntry { "figure"_s, AccessibilityRole::Figure },
2807 RoleEntry { "generic"_s, AccessibilityRole::Generic },
2808 // The mappings for 'graphics-*' roles are defined in this spec: https://w3c.github.io/graphics-aam/
2809 RoleEntry { "graphics-document"_s, AccessibilityRole::GraphicsDocument },
2810 RoleEntry { "graphics-object"_s, AccessibilityRole::GraphicsObject },
2811 RoleEntry { "graphics-symbol"_s, AccessibilityRole::GraphicsSymbol },
2812 RoleEntry { "grid"_s, AccessibilityRole::Grid },
2813 RoleEntry { "gridcell"_s, AccessibilityRole::GridCell },
2814 RoleEntry { "table"_s, AccessibilityRole::Table },
2815 RoleEntry { "cell"_s, AccessibilityRole::Cell },
2816 RoleEntry { "columnheader"_s, AccessibilityRole::ColumnHeader },
2817 RoleEntry { "combobox"_s, AccessibilityRole::ComboBox },
2818 RoleEntry { "definition"_s, AccessibilityRole::Definition },
2819 RoleEntry { "document"_s, AccessibilityRole::Document },
2820 RoleEntry { "feed"_s, AccessibilityRole::Feed },
2821 RoleEntry { "form"_s, AccessibilityRole::Form },
2822 RoleEntry { "rowheader"_s, AccessibilityRole::RowHeader },
2823 RoleEntry { "group"_s, AccessibilityRole::Group },
2824 RoleEntry { "heading"_s, AccessibilityRole::Heading },
2825 // The "image" role is synonymous with the "img" role. https://w3c.github.io/aria/#image
2826 RoleEntry { "image"_s, AccessibilityRole::Image },
2827 RoleEntry { "img"_s, AccessibilityRole::Image },
2828 RoleEntry { "insertion"_s, AccessibilityRole::Insertion },
2829 RoleEntry { "link"_s, AccessibilityRole::Link },
2830 RoleEntry { "list"_s, AccessibilityRole::List },
2831 RoleEntry { "listitem"_s, AccessibilityRole::ListItem },
2832 RoleEntry { "listbox"_s, AccessibilityRole::ListBox },
2833 RoleEntry { "log"_s, AccessibilityRole::ApplicationLog },
2834 RoleEntry { "main"_s, AccessibilityRole::LandmarkMain },
2835 RoleEntry { "marquee"_s, AccessibilityRole::ApplicationMarquee },
2836 RoleEntry { "math"_s, AccessibilityRole::DocumentMath },
2837 RoleEntry { "mark"_s, AccessibilityRole::Mark },
2838 RoleEntry { "menu"_s, AccessibilityRole::Menu },
2839 RoleEntry { "menubar"_s, AccessibilityRole::MenuBar },
2840 RoleEntry { "menuitem"_s, AccessibilityRole::MenuItem },
2841 RoleEntry { "menuitemcheckbox"_s, AccessibilityRole::MenuItemCheckbox },
2842 RoleEntry { "menuitemradio"_s, AccessibilityRole::MenuItemRadio },
2843 RoleEntry { "meter"_s, AccessibilityRole::Meter },
2844 RoleEntry { "none"_s, AccessibilityRole::Presentational },
2845 RoleEntry { "note"_s, AccessibilityRole::DocumentNote },
2846 RoleEntry { "navigation"_s, AccessibilityRole::LandmarkNavigation },
2847 RoleEntry { "option"_s, AccessibilityRole::ListBoxOption },
2848 RoleEntry { "paragraph"_s, AccessibilityRole::Paragraph },
2849 RoleEntry { "presentation"_s, AccessibilityRole::Presentational },
2850 RoleEntry { "progressbar"_s, AccessibilityRole::ProgressIndicator },
2851 RoleEntry { "radio"_s, AccessibilityRole::RadioButton },
2852 RoleEntry { "radiogroup"_s, AccessibilityRole::RadioGroup },
2853 RoleEntry { "region"_s, AccessibilityRole::LandmarkRegion },
2854 RoleEntry { "row"_s, AccessibilityRole::Row },
2855 RoleEntry { "rowgroup"_s, AccessibilityRole::RowGroup },
2856 RoleEntry { "scrollbar"_s, AccessibilityRole::ScrollBar },
2857 RoleEntry { "search"_s, AccessibilityRole::LandmarkSearch },
2858 RoleEntry { "searchbox"_s, AccessibilityRole::SearchField },
2859 RoleEntry { "sectionfooter"_s, AccessibilityRole::SectionFooter },
2860 RoleEntry { "sectionheader"_s, AccessibilityRole::SectionHeader },
2861 RoleEntry { "separator"_s, AccessibilityRole::Splitter },
2862 RoleEntry { "slider"_s, AccessibilityRole::Slider },
2863 RoleEntry { "spinbutton"_s, AccessibilityRole::SpinButton },
2864 RoleEntry { "status"_s, AccessibilityRole::ApplicationStatus },
2865 RoleEntry { "subscript"_s, AccessibilityRole::Subscript },
2866 RoleEntry { "suggestion"_s, AccessibilityRole::Suggestion },
2867 RoleEntry { "superscript"_s, AccessibilityRole::Superscript },
2868 RoleEntry { "strong"_s, AccessibilityRole::Strong },
2869 RoleEntry { "switch"_s, AccessibilityRole::Switch },
2870 RoleEntry { "tab"_s, AccessibilityRole::Tab },
2871 RoleEntry { "tablist"_s, AccessibilityRole::TabList },
2872 RoleEntry { "tabpanel"_s, AccessibilityRole::TabPanel },
2873 RoleEntry { "text"_s, AccessibilityRole::StaticText },
2874 RoleEntry { "textbox"_s, AccessibilityRole::TextField },
2875 RoleEntry { "term"_s, AccessibilityRole::Term },
2876 RoleEntry { "time"_s, AccessibilityRole::Time },
2877 RoleEntry { "timer"_s, AccessibilityRole::ApplicationTimer },
2878 RoleEntry { "toolbar"_s, AccessibilityRole::Toolbar },
2879 RoleEntry { "tooltip"_s, AccessibilityRole::UserInterfaceTooltip },
2880 RoleEntry { "tree"_s, AccessibilityRole::Tree },
2881 RoleEntry { "treegrid"_s, AccessibilityRole::TreeGrid },
2882 RoleEntry { "treeitem"_s, AccessibilityRole::TreeItem }
2883 };
2884
2885 gAriaRoleMap = new ARIARoleMap;
2886 gAriaReverseRoleMap = new ARIAReverseRoleMap;
2887 size_t roleLength = std::size(roles);
2888 for (size_t i = 0; i < roleLength; ++i) {
2889 gAriaRoleMap->set(roles[i].ariaRole, roles[i].webcoreRole);
2890 gAriaReverseRoleMap->set(std::to_underlying(roles[i].webcoreRole), roles[i].ariaRole);
2891 }
2892
2893 // Create specific synonyms for the computedRole which is used in WPT tests and the accessibility inspector.
2894 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::DateTime), "textbox"_s);
2895 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::TextArea), "textbox"_s);
2896
2897 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::DescriptionListDetail), "definition"_s);
2898 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::DescriptionListTerm), "term"_s);
2899 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::Details), "group"_s);
2900 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::Image), "image"_s);
2901 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::ListBoxOption), "option"_s);
2902 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::MenuListOption), "option"_s);
2903 gAriaReverseRoleMap->set(std::to_underlying(AccessibilityRole::Presentational), "none"_s);
2904}
2905
2906static ARIARoleMap& ariaRoleMap()
2907{
2908 initializeRoleMap();
2909 return *gAriaRoleMap;
2910}
2911
2912static ARIAReverseRoleMap& reverseAriaRoleMap()
2913{
2914 initializeRoleMap();
2915 return *gAriaReverseRoleMap;
2916}
2917
2918AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value)
2919{
2920 return ariaRoleToWebCoreRole(value, [] (const AccessibilityRole&) {
2921 return false;
2922 });
2923}
2924
2925AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value, NOESCAPE[[clang::noescape]] const Function<bool(const AccessibilityRole&)>& skipRole)
2926{
2927 if (value.isNull() || value.isEmpty())
2928 return AccessibilityRole::Unknown;
2929 auto simplifiedValue = value.simplifyWhiteSpace(isASCIIWhitespace);
2930 for (auto roleName : StringView(simplifiedValue).split(' ')) {
2931 AccessibilityRole role = ariaRoleMap().get<ASCIICaseInsensitiveStringViewHashTranslator>(roleName);
2932 if (skipRole(role))
2933 continue;
2934
2935 if (std::to_underlying(role))
2936 return role;
2937 }
2938 return AccessibilityRole::Unknown;
2939}
2940
2941String AccessibilityObject::computedRoleString() const
2942{
2943 // FIXME: Need a few special cases that aren't in the RoleMap: option, etc. http://webkit.org/b/128296
2944 auto role = this->role();
2945
2946 if (role == AccessibilityRole::Image && isIgnored())
2947 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::Presentational));
2948
2949 // We do compute a role string for block elements with author-provided roles.
2950 if (ariaRoleAttribute() == AccessibilityRole::TextGroup || role == AccessibilityRole::Footnote)
2951 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::Group));
2952
2953 // We do not compute a role string for generic block elements with user-agent assigned roles.
2954 if (role == AccessibilityRole::TextGroup)
2955 return emptyString();
2956
2957 if (role == AccessibilityRole::HorizontalRule)
2958 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::Splitter));
2959
2960 if (role == AccessibilityRole::PopUpButton || role == AccessibilityRole::ToggleButton)
2961 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::Button));
2962
2963 if (role == AccessibilityRole::LandmarkDocRegion)
2964 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::LandmarkRegion));
2965
2966 if (isColumnHeader())
2967 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::ColumnHeader));
2968
2969 if (isRowHeader())
2970 return reverseAriaRoleMap().get(std::to_underlying(AccessibilityRole::RowHeader));
2971
2972 return reverseAriaRoleMap().get(std::to_underlying(role));
2973}
2974
2975void AccessibilityObject::updateRole()
2976{
2977 auto previousRole = m_role;
2978 recomputeAriaRole();
2979 m_role = determineAccessibilityRole();
2980 if (previousRole != m_role) {
2981 if (CheckedPtr cache = axObjectCache())
2982 cache->handleRoleChanged(*this, previousRole);
2983 }
2984}
2985
2986#if !PLATFORM(MAC)(defined 1 && 1)
2987String AccessibilityObject::subrolePlatformString() const
2988{
2989 return String();
2990}
2991#endif
2992
2993String AccessibilityObject::embeddedImageDescription() const
2994{
2995 CheckedPtr renderImage = dynamicDowncast<RenderImage>(renderer());
2996 if (!renderImage)
2997 return { };
2998
2999 return renderImage->accessibilityDescription();
3000}
3001
3002bool AccessibilityObject::isLoaded() const
3003{
3004 RefPtr document = this->document();
3005 return document && !document->parser();
3006}
3007
3008CheckedPtr<RenderObject> AccessibilityObject::rendererOrNearestAncestor() const
3009{
3010 RefPtr node = this->node();
3011 return node ? nearestRendererFromNode(*node) : nullptr;
3012}
3013
3014const RenderStyle* AccessibilityObject::style() const
3015{
3016 if (auto* renderer = this->renderer()) {
3017 if (auto* renderText = dynamicDowncast<RenderText>(*renderer)) {
3018 // Trying to get the style from a RenderText that has no parent (e.g. because it hasn't been
3019 // set yet, or it was destroyed as part of an in-progress render-tree update) will cause a
3020 // crash because RenderTexts get their style from their parent.
3021 return renderText->parent() ? &renderText->style() : nullptr;
3022 }
3023 return &renderer->style();
3024 }
3025
3026 RefPtr element = this->element();
3027 return element ? safeStyleFrom(*element) : nullptr;
3028}
3029
3030bool AccessibilityObject::isValueAutofillAvailable() const
3031{
3032 if (!isNativeTextControl())
3033 return false;
3034
3035 RefPtr input = dynamicDowncast<HTMLInputElement>(node());
3036 return input && (input->autofillAvailable() || input->autofillButtonType() != AutoFillButtonType::None);
3037}
3038
3039AutoFillButtonType AccessibilityObject::valueAutofillButtonType() const
3040{
3041 if (!isValueAutofillAvailable())
3042 return AutoFillButtonType::None;
3043
3044 return downcast<HTMLInputElement>(*this->node()).autofillButtonType();
3045}
3046
3047bool AccessibilityObject::isSelected() const
3048{
3049 if (!renderer() && !node())
3050 return false;
3051
3052 if (equalLettersIgnoringASCIICase(getAttribute(aria_selectedAttr), "true"_s))
3053 return true;
3054
3055 if (isTabItem() && isTabItemSelected())
3056 return true;
3057
3058 // Menu items are considered selectable by assistive technologies
3059 if (isMenuItem()) {
3060 if (isFocused())
3061 return true;
3062 WeakPtr parent = parentObjectUnignored();
3063 return parent && parent->activeDescendant() == this;
3064 }
3065
3066 return false;
3067}
3068
3069bool AccessibilityObject::isTabItemSelected() const
3070{
3071 if (!isTabItem() || (!renderer() && !node()))
3072 return false;
3073
3074 WeakPtr node = this->node();
3075 if (!node || !node->isElementNode())
3076 return false;
3077
3078 // The ARIA spec says a tab item can also be selected if it is aria-labeled by a tabpanel
3079 // that has keyboard focus inside of it, or if a tabpanel in its aria-controls list has KB
3080 // focus inside of it.
3081 RefPtr focusedElement = focusedUIElement();
3082 if (!focusedElement)
3083 return false;
3084
3085 CheckedPtr cache = axObjectCache();
3086 if (!cache)
3087 return false;
3088
3089 auto elements = elementsFromAttribute(aria_controlsAttr);
3090 for (auto& element : elements) {
3091 RefPtr tabPanel = cache->getOrCreate(element.get());
3092
3093 // A tab item should only control tab panels.
3094 if (!tabPanel || tabPanel->role() != AccessibilityRole::TabPanel)
3095 continue;
3096
3097 RefPtr checkFocusElement = focusedElement;
3098 // Check if the focused element is a descendant of the element controlled by the tab item.
3099 while (checkFocusElement) {
3100 if (tabPanel == checkFocusElement)
3101 return true;
3102 checkFocusElement = checkFocusElement->parentObject();
3103 }
3104 }
3105 return false;
3106}
3107
3108unsigned AccessibilityObject::textLength() const
3109{
3110 AX_ASSERT(isTextControl())((void)0);
3111 return text().length();
3112}
3113
3114std::optional<String> AccessibilityObject::textContent() const
3115{
3116 if (!hasTextContent())
3117 return std::nullopt;
3118
3119 std::optional<SimpleRange> range;
3120 if (isTextControl())
3121 range = rangeForCharacterRange({ 0, text().length() });
3122 else
3123 range = simpleRange();
3124 if (range)
3125 return AXTextMarkerRange { range }.toString();
3126 return std::nullopt;
3127}
3128
3129const String AccessibilityObject::placeholderValue() const
3130{
3131 const AtomString& placeholder = getAttribute(placeholderAttr);
3132 if (!placeholder.isEmpty())
3133 return placeholder;
3134
3135 const AtomString& ariaPlaceholder = getAttribute(aria_placeholderAttr);
3136 if (!ariaPlaceholder.isEmpty())
3137 return ariaPlaceholder;
3138
3139 return nullAtom();
3140}
3141
3142bool AccessibilityObject::supportsARIAAttributes() const
3143{
3144 // This returns whether the element supports any global ARIA attributes.
3145 return supportsLiveRegion()
3146 || supportsDragging()
3147 || supportsDropping()
3148 || supportsARIAOwns()
3149 || hasAttribute(aria_atomicAttr)
3150 || hasAttribute(aria_busyAttr)
3151 || hasAttribute(aria_controlsAttr)
3152 || hasAttribute(aria_currentAttr)
3153 || hasAttribute(aria_describedbyAttr)
3154 || hasAttribute(aria_detailsAttr)
3155 || hasAttribute(aria_disabledAttr)
3156 || hasAttribute(aria_errormessageAttr)
3157 || hasAttribute(aria_flowtoAttr)
3158 || hasAttribute(aria_haspopupAttr)
3159 || hasAttribute(aria_invalidAttr)
3160 || hasAttribute(aria_labelAttr)
3161 || hasAttribute(aria_labelledbyAttr)
3162 || hasAttribute(aria_relevantAttr);
3163}
3164
3165RefPtr<AccessibilityObject> AccessibilityObject::elementAccessibilityHitTest(const IntPoint& point) const
3166{
3167 // Send the hit test back into the sub-frame if necessary.
3168 if (isAttachment()) {
3169 RefPtr widget = widgetForAttachmentView();
3170 // Normalize the point for the widget's bounds.
3171 if (widget && widget->isLocalFrameView()) {
3172 RefPtr widgetScrollView = dynamicDowncast<ScrollView>(widget);
3173 if (CheckedPtr cache = widgetScrollView ? axObjectCache() : nullptr) {
3174 IntPoint adjustedPoint = IntPoint(point - widget->frameRect().location()) + widgetScrollView->scrollPosition();
3175 return downcast<AccessibilityObject>(cache->getOrCreate(*widget)->accessibilityHitTest(adjustedPoint).get());
3176 }
3177 }
3178
3179 if (widget && widget->isRemoteFrameView()) {
3180 if (CheckedPtr cache = axObjectCache()) {
3181 if (RefPtr remoteHostWidget = cache->getOrCreate(*widget)) {
3182 remoteHostWidget->updateChildrenIfNecessary();
3183 RefPtr scrollView = dynamicDowncast<AccessibilityScrollView>(*remoteHostWidget);
3184 return scrollView ? scrollView->remoteFrame() : nullptr;
3185 }
3186 }
3187 }
3188 }
3189
3190 // Check if there are any mock elements that need to be handled.
3191 for (const auto& child : const_cast<AccessibilityObject*>(this)->unignoredChildren(/* updateChildrenIfNeeded */ false)) {
3192 if (RefPtr mockChild = dynamicDowncast<AccessibilityMockObject>(child.get()); mockChild && mockChild->elementRect().contains(point))
3193 return mockChild->elementAccessibilityHitTest(point);
3194 }
3195
3196 return const_cast<AccessibilityObject*>(this);
3197}
3198
3199AccessibilityObject* AccessibilityObject::focusedUIElement() const
3200{
3201 CheckedPtr cache = axObjectCache();
3202 return cache ? cache->focusedObjectForLocalFrame() : nullptr;
3203}
3204
3205AccessibilityObject* AccessibilityObject::focusedUIElementInAnyLocalFrame() const
3206{
3207 RefPtr page = this->page();
3208 if (!page)
3209 return nullptr;
3210
3211 RefPtr focusedOrMainFrame = page->focusController().focusedOrMainFrame();
3212 if (!focusedOrMainFrame)
3213 return nullptr;
3214
3215 RefPtr focusedDocument = focusedOrMainFrame->document();
3216 if (!focusedDocument)
3217 return nullptr;
3218
3219 CheckedPtr axObjectCache = focusedDocument->axObjectCache();
3220 if (!axObjectCache)
3221 return nullptr;
3222 return axObjectCache->focusedObjectForPage(page.get());
3223}
3224
3225void AccessibilityObject::setSelectedRows(AccessibilityChildrenVector&& selectedRows)
3226{
3227 // Setting selected only makes sense in trees and tables (and tree-tables).
3228 auto role = this->role();
3229 if (role != AccessibilityRole::Tree && role != AccessibilityRole::TreeGrid && role != AccessibilityRole::Table && role != AccessibilityRole::Grid)
3230 return;
3231
3232 bool isMulti = isMultiSelectable();
3233 for (const auto& selectedRow : selectedRows) {
3234 // 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.
3235 selectedRow->setSelected(true);
3236 if (isMulti)
3237 break;
3238 }
3239}
3240
3241void AccessibilityObject::setFocused(bool focus)
3242{
3243 if (focus) {
3244 // Ensure that the view is focused and active, otherwise, any attempt to set focus to an object inside it will fail.
3245 RefPtr frame = document() ? document()->frame() : nullptr;
3246 if (frame && frame->selection().isFocusedAndActive())
3247 return; // Nothing to do, already focused and active.
3248
3249 RefPtr page = document() ? document()->page() : nullptr;
3250 if (!page)
3251 return;
3252
3253 page->chrome().client().focus();
3254 // Reset the page pointer in case ChromeClient::focus() caused a side effect that invalidated our old one.
3255 page = document() ? document()->page() : nullptr;
3256 if (!page)
3257 return;
3258
3259#if PLATFORM(IOS_FAMILY)(defined WTF_PLATFORM_IOS_FAMILY && WTF_PLATFORM_IOS_FAMILY
)
3260 // Mark the page as focused so the focus ring can be drawn immediately. The page is also marked
3261 // as focused as part assistiveTechnologyMakeFirstResponder, but that requires some back-and-forth
3262 // IPC between the web and UI processes, during which we can miss the drawing of the focus ring for the
3263 // first focused element. Making the page focused is a requirement for making the page selection focused.
3264 // This is iOS only until there's a demonstrated need for this preemptive focus on other platforms.
3265 if (!page->focusController().isFocused())
3266 page->focusController().setFocused(true);
3267
3268 // Reset the page pointer in case FocusController::setFocused(true) caused a side effect that invalidated our old one.
3269 page = document() ? document()->page() : nullptr;
3270 if (!page)
3271 return;
3272#endif
3273
3274#if PLATFORM(COCOA)(defined 1 && 1)
3275 RefPtr frameView = documentFrameView();
3276 if (!frameView)
3277 return;
3278
3279 // Legacy WebKit1 case.
3280 if (frameView->platformWidget())
3281 page->chrome().client().makeFirstResponder((NSResponder *)frameView->platformWidget());
3282#endif
3283#if PLATFORM(MAC)(defined 1 && 1)
3284 else
3285 page->chrome().client().assistiveTechnologyMakeFirstResponder();
3286 // WebChromeClient::assistiveTechnologyMakeFirstResponder (the WebKit2 codepath) is intentionally
3287 // not called on iOS because stealing first-respondership causes issues such as:
3288 // 1. VoiceOver Speak Screen focus erroneously jumping to the top of the page when encountering an embedded WKWebView
3289 // 2. Third-party apps relying on WebKit to not steal first-respondership (https://bugs.webkit.org/show_bug.cgi?id=249976)
3290#endif
3291 }
3292}
3293
3294AccessibilitySortDirection AccessibilityObject::sortDirection() const
3295{
3296 // Only row and column headers are allowed to have aria-sort.
3297 // https://w3c.github.io/aria/#aria-sort
3298 if (!isColumnHeader() && !isRowHeader())
3299 return AccessibilitySortDirection::Invalid;
3300
3301 auto& sortAttribute = getAttribute(aria_sortAttr);
3302 if (sortAttribute.isNull())
3303 return AccessibilitySortDirection::None;
3304
3305 if (equalLettersIgnoringASCIICase(sortAttribute, "ascending"_s))
3306 return AccessibilitySortDirection::Ascending;
3307 if (equalLettersIgnoringASCIICase(sortAttribute, "descending"_s))
3308 return AccessibilitySortDirection::Descending;
3309 if (equalLettersIgnoringASCIICase(sortAttribute, "other"_s))
3310 return AccessibilitySortDirection::Other;
3311
3312 return AccessibilitySortDirection::None;
3313}
3314
3315bool AccessibilityObject::supportsHasPopup() const
3316{
3317 return hasAttribute(aria_haspopupAttr) || isComboBox();
3318}
3319
3320String AccessibilityObject::explicitPopupValue() const
3321{
3322 auto& hasPopup = getAttribute(aria_haspopupAttr);
3323 if (hasPopup.isEmpty()) {
3324 // In ARIA 1.1, the implicit value for datalists became "listbox."
3325 if (hasDatalist())
3326 return "listbox"_s;
3327 return { };
3328 }
3329
3330 for (auto& value : { "menu"_s, "listbox"_s, "tree"_s, "grid"_s, "dialog"_s }) {
3331 // 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.
3332 if (equalIgnoringASCIICase(hasPopup, value))
3333 return value;
3334 }
3335
3336 // aria-haspopup specification states that true must be treated as menu.
3337 if (equalLettersIgnoringASCIICase(hasPopup, "true"_s))
3338 return "menu"_s;
3339 return { };
3340}
3341
3342bool AccessibilityObject::supportsSetSize() const
3343{
3344 return hasAttribute(aria_setsizeAttr);
3345}
3346
3347bool AccessibilityObject::supportsPosInSet() const
3348{
3349 return hasAttribute(aria_posinsetAttr);
3350}
3351
3352int AccessibilityObject::setSize() const
3353{
3354 // https://github.com/w3c/aria/pull/2341
3355 // When aria-setsize isn't a positive integer (greater than or equal to 1), its value should be indeterminate, i.e., -1.
3356 int setSize = integralAttribute(aria_setsizeAttr);
3357 return setSize >= 1 ? setSize : -1;
3358}
3359
3360int AccessibilityObject::posInSet() const
3361{
3362 // https://github.com/w3c/aria/pull/2341
3363 // When aria-posinset isn't a positive integer (greater than or equal to 1), its value should be 1.
3364 return std::max(1, integralAttribute(aria_posinsetAttr));
3365}
3366
3367String AccessibilityObject::identifierAttribute() const
3368{
3369 return getAttribute(idAttr);
3370}
3371
3372Vector<String> AccessibilityObject::classList() const
3373{
3374 RefPtr element = this->element();
3375 if (!element)
3376 return { };
3377
3378 Ref domClassList = element->classList();
3379 Vector<String> classList;
3380 unsigned length = domClassList->length();
3381 classList.reserveInitialCapacity(length);
3382 for (unsigned k = 0; k < length; k++)
3383 classList.append(domClassList->item(k).string());
3384 return classList;
3385}
3386
3387String AccessibilityObject::extendedDescription() const
3388{
3389 auto describedBy = ariaDescribedByAttribute();
3390 if (!describedBy.isEmpty())
3391 return describedBy;
3392
3393 return getAttribute(HTMLNames::aria_descriptionAttr);
3394}
3395
3396bool AccessibilityObject::supportsPressed() const
3397{
3398 const AtomString& expanded = getAttribute(aria_pressedAttr);
3399 return equalLettersIgnoringASCIICase(expanded, "true"_s) || equalLettersIgnoringASCIICase(expanded, "false"_s);
3400}
3401
3402bool AccessibilityObject::supportsExpanded() const
3403{
3404 // commandfor attribute takes precedence over popovertarget attribute.
3405 if (RefPtr targetElement = commandForElement()) {
3406 // If the target element is a popover then check command is popover related.
3407 if (targetElement->popoverState() != PopoverState::None) {
3408 switch (commandType()) {
3409 // Expose an expanded state if the command is valid for a popover.
3410 case CommandType::ShowPopover:
3411 case CommandType::HidePopover:
3412 case CommandType::TogglePopover:
3413 return true;
3414 case CommandType::Invalid:
3415 case CommandType::Custom:
3416 case CommandType::ShowModal:
3417 case CommandType::Close:
3418 case CommandType::RequestClose:
3419 break;
3420 default:
3421 AX_ASSERT_NOT_REACHED()((void)0);
3422 break;
3423 }
3424 }
3425 } else if (popoverTargetElement())
3426 return true;
3427
3428 if (is<HTMLDetailsElement>(node()))
3429 return true;
3430
3431 auto hasValidAriaExpandedValue = [this] () -> bool {
3432 // Undefined values should not result in this attribute being exposed to ATs according to ARIA.
3433 const AtomString& expanded = getAttribute(aria_expandedAttr);
3434 return equalLettersIgnoringASCIICase(expanded, "true"_s) || equalLettersIgnoringASCIICase(expanded, "false"_s);
3435 };
3436
3437 if (isColumnHeader() || isRowHeader())
3438 return hasValidAriaExpandedValue();
3439
3440 switch (role()) {
3441 case AccessibilityRole::Details:
3442 return true;
3443 case AccessibilityRole::Button:
3444 case AccessibilityRole::Checkbox:
3445 case AccessibilityRole::ComboBox:
3446 case AccessibilityRole::GridCell:
3447 case AccessibilityRole::Link:
3448 case AccessibilityRole::MenuItem:
3449 case AccessibilityRole::MenuItemCheckbox:
3450 case AccessibilityRole::MenuItemRadio:
3451 case AccessibilityRole::Row:
3452 case AccessibilityRole::Switch:
3453 case AccessibilityRole::Tab:
3454 case AccessibilityRole::TreeItem:
3455 case AccessibilityRole::WebApplication:
3456 return hasValidAriaExpandedValue();
3457 default:
3458 return false;
3459 }
3460}
3461
3462double AccessibilityObject::loadingProgress() const
3463{
3464 if (isLoaded())
3465 return 1.0;
3466
3467 RefPtr page = this->page();
3468 if (!page)
3469 return 0.0;
3470
3471 return page->progress().estimatedProgress();
3472}
3473
3474bool AccessibilityObject::isExpanded() const
3475{
3476 if (RefPtr details = dynamicDowncast<HTMLDetailsElement>(node()))
3477 return details->hasAttribute(openAttr);
3478
3479 // Summary element should use its details parent's expanded status.
3480 if (isSummary()) {
3481 if (RefPtr parent = Accessibility::findAncestor<AccessibilityObject>(*this, false, [] (const AccessibilityObject& object) {
3482 return is<HTMLDetailsElement>(object.node());
3483 }))
3484 return parent->isExpanded();
3485 }
3486
3487 if (supportsExpanded()) {
3488 if (RefPtr commandForElement = this->commandForElement())
3489 return commandForElement->isPopoverShowing();
3490 if (RefPtr popoverTargetElement = this->popoverTargetElement())
3491 return popoverTargetElement->isPopoverShowing();
3492 return equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true"_s);
3493 }
3494
3495 return false;
3496}
3497
3498AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const
3499{
3500 // If this is a real checkbox or radio button, AccessibilityNodeObject will handle.
3501 // If it's an ARIA checkbox, radio, or switch the aria-checked attribute should be used.
3502 // If it's a toggle button, the aria-pressed attribute is consulted.
3503
3504 if (isToggleButton()) {
3505 const AtomString& ariaPressed = getAttribute(aria_pressedAttr);
3506 if (equalLettersIgnoringASCIICase(ariaPressed, "true"_s))
3507 return AccessibilityButtonState::On;
3508 if (equalLettersIgnoringASCIICase(ariaPressed, "mixed"_s))
3509 return AccessibilityButtonState::Mixed;
3510 return AccessibilityButtonState::Off;
3511 }
3512
3513 const AtomString& result = getAttribute(aria_checkedAttr);
3514 if (equalLettersIgnoringASCIICase(result, "true"_s))
3515 return AccessibilityButtonState::On;
3516 if (equalLettersIgnoringASCIICase(result, "mixed"_s)) {
3517 // ARIA says that radio, menuitemradio, and switch elements must NOT expose button state mixed.
3518 AccessibilityRole ariaRole = ariaRoleAttribute();
3519 if (ariaRole == AccessibilityRole::RadioButton || ariaRole == AccessibilityRole::MenuItemRadio || ariaRole == AccessibilityRole::Switch)
3520 return AccessibilityButtonState::Off;
3521 return AccessibilityButtonState::Mixed;
3522 }
3523
3524 return AccessibilityButtonState::Off;
3525}
3526
3527HashMap<String, AXEditingStyleValueVariant> AccessibilityObject::resolvedEditingStyles() const
3528{
3529 RefPtr document = this->document();
3530 if (!document)
3531 return { };
3532
3533 auto selectionStyle = EditingStyle::styleAtSelectionStart(document->selection().selection());
3534 if (!selectionStyle)
3535 return { };
3536
3537 HashMap<String, AXEditingStyleValueVariant> styles;
3538 styles.add("bold"_s, selectionStyle->hasStyle(CSSPropertyFontWeight, "bold"_s));
3539 styles.add("italic"_s, selectionStyle->hasStyle(CSSPropertyFontStyle, "italic"_s));
3540 styles.add("underline"_s, selectionStyle->hasStyle(CSSPropertyWebkitTextDecorationsInEffect, "underline"_s));
3541 styles.add("fontsize"_s, selectionStyle->legacyFontSize(*document));
3542 return styles;
3543}
3544
3545// This is a 1-dimensional scroll offset helper function that's applied
3546// separately in the horizontal and vertical directions, because the
3547// logic is the same. The goal is to compute the best scroll offset
3548// in order to make an object visible within a viewport.
3549//
3550// If the object is already fully visible, returns the same scroll
3551// offset.
3552//
3553// In case the whole object cannot fit, you can specify a
3554// subfocus - a smaller region within the object that should
3555// be prioritized. If the whole object can fit, the subfocus is
3556// ignored.
3557//
3558// If possible, the object and subfocus are centered within the
3559// viewport.
3560//
3561// Example 1: the object is already visible, so nothing happens.
3562// +----------Viewport---------+
3563// +---Object---+
3564// +--SubFocus--+
3565//
3566// Example 2: the object is not fully visible, so it's centered
3567// within the viewport.
3568// Before:
3569// +----------Viewport---------+
3570// +---Object---+
3571// +--SubFocus--+
3572//
3573// After:
3574// +----------Viewport---------+
3575// +---Object---+
3576// +--SubFocus--+
3577//
3578// Example 3: the object is larger than the viewport, so the
3579// viewport moves to show as much of the object as possible,
3580// while also trying to center the subfocus.
3581// Before:
3582// +----------Viewport---------+
3583// +---------------Object--------------+
3584// +-SubFocus-+
3585//
3586// After:
3587// +----------Viewport---------+
3588// +---------------Object--------------+
3589// +-SubFocus-+
3590//
3591// When constraints cannot be fully satisfied, the min
3592// (left/top) position takes precedence over the max (right/bottom).
3593//
3594// Note that the return value represents the ideal new scroll offset.
3595// This may be out of range - the calling function should clip this
3596// to the available range.
3597static int computeBestScrollOffset(int currentScrollOffset, int subfocusMin, int subfocusMax, int objectMin, int objectMax, int viewportMin, int viewportMax)
3598{
3599 int viewportSize = viewportMax - viewportMin;
3600
3601 // If the object size is larger than the viewport size, consider
3602 // only a portion that's as large as the viewport, centering on
3603 // the subfocus as much as possible.
3604 if (objectMax - objectMin > viewportSize) {
3605 // Since it's impossible to fit the whole object in the
3606 // viewport, exit now if the subfocus is already within the viewport.
3607 if (subfocusMin - currentScrollOffset >= viewportMin && subfocusMax - currentScrollOffset <= viewportMax)
3608 return currentScrollOffset;
3609
3610 // Subfocus must be within focus.
3611 subfocusMin = std::max(subfocusMin, objectMin);
3612 subfocusMax = std::min(subfocusMax, objectMax);
3613
3614 // Subfocus must be no larger than the viewport size; favor top/left.
3615 if (subfocusMax - subfocusMin > viewportSize)
3616 subfocusMax = subfocusMin + viewportSize;
3617
3618 // Compute the size of an object centered on the subfocus, the size of the viewport.
3619 int centeredObjectMin = (subfocusMin + subfocusMax - viewportSize) / 2;
3620 int centeredObjectMax = centeredObjectMin + viewportSize;
3621
3622 objectMin = std::max(objectMin, centeredObjectMin);
3623 objectMax = std::min(objectMax, centeredObjectMax);
3624 }
3625
3626 // Exit now if the focus is already within the viewport.
3627 if (objectMin - currentScrollOffset >= viewportMin
3628 && objectMax - currentScrollOffset <= viewportMax)
3629 return currentScrollOffset;
3630
3631 // Center the object in the viewport.
3632 return (objectMin + objectMax - viewportMin - viewportMax) / 2;
3633}
3634
3635bool AccessibilityObject::isOnScreen() const
3636{
3637 // To figure out if the element is onscreen, we start by building of a stack starting with the
3638 // element, and then include every scrollable parent in the hierarchy.
3639 Vector<Ref<const AccessibilityObject>> objects;
3640
3641 objects.append(*this);
3642 for (RefPtr ancestor = parentObject(); ancestor; ancestor = ancestor->parentObject()) {
3643 if (ancestor->getScrollableAreaIfScrollable())
3644 objects.append(*ancestor);
3645 }
3646
3647 // Now, go back through that chain and make sure each inner object is within the
3648 // visible bounds of the outer object.
3649 size_t levels = objects.size() - 1;
3650 for (size_t i = levels; i >= 1; i--) {
3651 Ref outer = objects[i];
3652 Ref inner = objects[i - 1];
3653 // FIXME: unclear if we need LegacyIOSDocumentVisibleRect.
3654 const IntRect outerRect = i < levels ? snappedIntRect(outer->boundingBoxRect()) : outer->getScrollableAreaIfScrollable()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
3655
3656 IntRect innerRect = snappedIntRect(inner->boundingBoxRect());
3657 if (RefPtr scrollView = !outer->isRoot() ? outer->scrollView() : nullptr)
3658 innerRect = scrollView->contentsToRootView(innerRect);
3659
3660 if (!outerRect.intersects(innerRect))
3661 return false;
3662 }
3663 return true;
3664}
3665
3666void AccessibilityObject::scrollToMakeVisible(const ScrollRectToVisibleOptions& options) const
3667{
3668 if (isScrollArea() && parentObject())
3669 parentObject()->scrollToMakeVisible();
3670
3671 if (auto* renderer = this->renderer())
3672 LocalFrameView::scrollRectToVisible(boundingBoxRect(), *renderer, false, options);
3673}
3674
3675void AccessibilityObject::scrollToMakeVisibleWithSubFocus(IntRect&& subfocus) const
3676{
3677 // Search up the parent chain until we find the first one that's scrollable.
3678 AccessibilityObject* scrollParent = parentObject();
3679 CheckedPtr<ScrollableArea> scrollableArea;
3680 while (scrollParent) {
3681 scrollableArea = scrollParent->getScrollableAreaIfScrollable();
3682 if (scrollableArea)
3683 break;
3684 scrollParent = scrollParent->parentObject();
3685 }
3686 if (!scrollableArea)
3687 return;
3688
3689 LayoutRect objectRect = boundingBoxRect();
3690 IntPoint scrollPosition = scrollableArea->scrollPosition();
3691 // FIXME: unclear if we need LegacyIOSDocumentVisibleRect.
3692 IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
3693
3694 if (!scrollParent->isScrollArea()) {
3695 objectRect.moveBy(scrollPosition);
3696 objectRect.moveBy(-snappedIntRect(scrollParent->elementRect()).location());
3697 }
3698
3699 int desiredX = computeBestScrollOffset(
3700 scrollPosition.x(),
3701 objectRect.x() + subfocus.x(), objectRect.x() + subfocus.maxX(),
3702 objectRect.x(), objectRect.maxX(),
3703 0, scrollVisibleRect.width());
3704 int desiredY = computeBestScrollOffset(
3705 scrollPosition.y(),
3706 objectRect.y() + subfocus.y(), objectRect.y() + subfocus.maxY(),
3707 objectRect.y(), objectRect.maxY(),
3708 0, scrollVisibleRect.height());
3709
3710 scrollParent->scrollTo(IntPoint(desiredX, desiredY));
3711
3712 // Convert the subfocus into the coordinates of the scroll parent.
3713 IntRect newElementRect = snappedIntRect(elementRect());
3714 IntRect scrollParentRect = snappedIntRect(scrollParent->elementRect());
3715 subfocus.move(newElementRect.x(), newElementRect.y());
3716 subfocus.move(-scrollParentRect.x(), -scrollParentRect.y());
3717
3718 // Recursively make sure the scroll parent itself is visible.
3719 if (scrollParent->parentObject())
3720 scrollParent->scrollToMakeVisibleWithSubFocus(WTF::move(subfocus));
3721}
3722
3723FloatRect AccessibilityObject::unobscuredContentRect() const
3724{
3725 RefPtr document = this->document();
3726 if (!document || !document->view())
3727 return { };
3728 return FloatRect(snappedIntRect(document->view()->unobscuredContentRect()));
3729}
3730
3731void AccessibilityObject::scrollToGlobalPoint(IntPoint&& point) const
3732{
3733 // Search up the parent chain and create a vector of all scrollable parent objects
3734 // and ending with this object itself.
3735 Vector<const AccessibilityObject*> objects;
3736
3737 objects.append(this);
3738 for (AccessibilityObject* parentObject = this->parentObject(); parentObject; parentObject = parentObject->parentObject()) {
3739 if (parentObject->getScrollableAreaIfScrollable())
3740 objects.append(parentObject);
3741 }
3742
3743 objects.reverse();
3744
3745 // Start with the outermost scrollable (the main window) and try to scroll the
3746 // next innermost object to the given point.
3747 int offsetX = 0, offsetY = 0;
3748 size_t levels = objects.size() - 1;
3749 for (size_t i = 0; i < levels; i++) {
3750 RefPtr outer = objects[i];
3751 RefPtr inner = objects[i + 1];
3752
3753 CheckedPtr scrollableArea = outer->getScrollableAreaIfScrollable();
3754
3755 LayoutRect innerRect = inner->isScrollArea() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect();
3756 LayoutRect objectRect = innerRect;
3757 IntPoint scrollPosition = scrollableArea->scrollPosition();
3758
3759 // Convert the object rect into local coordinates.
3760 objectRect.move(offsetX, offsetY);
3761 if (!outer->isScrollArea())
3762 objectRect.move(scrollPosition.x(), scrollPosition.y());
3763
3764 int desiredX = computeBestScrollOffset(
3765 0,
3766 objectRect.x(), objectRect.maxX(),
3767 objectRect.x(), objectRect.maxX(),
3768 point.x(), point.x());
3769 int desiredY = computeBestScrollOffset(
3770 0,
3771 objectRect.y(), objectRect.maxY(),
3772 objectRect.y(), objectRect.maxY(),
3773 point.y(), point.y());
3774 outer->scrollTo(IntPoint(desiredX, desiredY));
3775
3776 if (outer->isScrollArea() && !inner->isScrollArea()) {
3777 // If outer object we just scrolled is a scroll view (main window or iframe) but the
3778 // inner object is not, keep track of the coordinate transformation to apply to
3779 // future nested calculations.
3780 scrollPosition = scrollableArea->scrollPosition();
3781 offsetX -= (scrollPosition.x() + point.x());
3782 offsetY -= (scrollPosition.y() + point.y());
3783 point.move(
3784 scrollPosition.x() - innerRect.x(),
3785 scrollPosition.y() - innerRect.y());
3786 } else if (inner->isScrollArea()) {
3787 // Otherwise, if the inner object is a scroll view, reset the coordinate transformation.
3788 offsetX = 0;
3789 offsetY = 0;
3790 }
3791 }
3792}
3793
3794void AccessibilityObject::scrollAreaAndAncestor(std::pair<ScrollableArea*, AccessibilityObject*>& scrollers) const
3795{
3796 // Search up the parent chain until we find the first one that's scrollable.
3797 scrollers.first = nullptr;
3798 for (scrollers.second = parentObject(); scrollers.second; scrollers.second = scrollers.second->parentObject()) {
3799 if ((scrollers.first = scrollers.second->getScrollableAreaIfScrollable()))
3800 break;
3801 }
3802}
3803
3804ScrollableArea* AccessibilityObject::scrollableAreaAncestor() const
3805{
3806 std::pair<ScrollableArea*, AccessibilityObject*> scrollers;
3807 scrollAreaAndAncestor(scrollers);
3808 return scrollers.first;
3809}
3810
3811IntPoint AccessibilityObject::scrollPosition() const
3812{
3813 if (CheckedPtr scroller = scrollableAreaAncestor())
3814 return scroller->scrollPosition();
3815
3816 return IntPoint();
3817}
3818
3819IntRect AccessibilityObject::scrollVisibleContentRect() const
3820{
3821 if (CheckedPtr scroller = scrollableAreaAncestor())
3822 return scroller->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
3823
3824 return IntRect();
3825}
3826
3827IntSize AccessibilityObject::scrollContentsSize() const
3828{
3829 if (CheckedPtr scroller = scrollableAreaAncestor())
3830 return scroller->contentsSize();
3831
3832 return IntSize();
3833}
3834
3835bool AccessibilityObject::scrollByPage(ScrollByPageDirection direction) const
3836{
3837 std::pair<ScrollableArea*, AccessibilityObject*> scrollers;
3838 scrollAreaAndAncestor(scrollers);
3839 CheckedPtr scrollableArea = scrollers.first;
3840 RefPtr scrollParent = scrollers.second;
3841
3842 if (!scrollableArea)
3843 return false;
3844
3845 IntPoint scrollPosition = scrollableArea->scrollPosition();
3846 IntPoint newScrollPosition = scrollPosition;
3847 IntSize scrollSize = scrollableArea->contentsSize();
3848 IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
3849 switch (direction) {
3850 case ScrollByPageDirection::Right: {
3851 int scrollAmount = scrollVisibleRect.size().width();
3852 int newX = scrollPosition.x() - scrollAmount;
3853 newScrollPosition.setX(std::max(newX, 0));
3854 break;
3855 }
3856 case ScrollByPageDirection::Left: {
3857 int scrollAmount = scrollVisibleRect.size().width();
3858 int newX = scrollAmount + scrollPosition.x();
3859 int maxX = scrollSize.width() - scrollAmount;
3860 newScrollPosition.setX(std::min(newX, maxX));
3861 break;
3862 }
3863 case ScrollByPageDirection::Up: {
3864 int scrollAmount = scrollVisibleRect.size().height();
3865 int newY = scrollPosition.y() - scrollAmount;
3866 newScrollPosition.setY(std::max(newY, 0));
3867 break;
3868 }
3869 case ScrollByPageDirection::Down: {
3870 int scrollAmount = scrollVisibleRect.size().height();
3871 int newY = scrollAmount + scrollPosition.y();
3872 int maxY = scrollSize.height() - scrollAmount;
3873 newScrollPosition.setY(std::min(newY, maxY));
3874 break;
3875 }
3876 }
3877
3878 if (newScrollPosition != scrollPosition) {
3879 scrollParent->scrollTo(newScrollPosition);
3880 protect(document())->updateLayoutIgnorePendingStylesheets();
3881 return true;
3882 }
3883
3884 return false;
3885}
3886
3887bool AccessibilityObject::includeIgnoredInCoreTree() const
3888{
3889#if ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE)(defined 1 && 1)
3890 RefPtr document = this->document();
3891 return document ? document->settings().includeIgnoredInCoreAXTree() : false;
3892#else
3893 return false;
3894#endif // ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE)
3895}
3896
3897bool AccessibilityObject::pressedIsPresent() const
3898{
3899 return !getAttribute(aria_pressedAttr).isEmpty();
3900}
3901
3902TextIteratorBehaviors AccessibilityObject::textIteratorBehaviorForTextRange() const
3903{
3904 TextIteratorBehaviors behaviors { TextIteratorBehavior::IgnoresStyleVisibility, TextIteratorBehavior::IgnoresFullSizeKana };
3905
3906#if USE(ATSPI)(defined USE_ATSPI && USE_ATSPI)
3907 // We need to emit replaced elements for ATSPI, and present
3908 // them with the 'object replacement character' (0xFFFC).
3909 behaviors.add(TextIteratorBehavior::EmitsObjectReplacementCharacters);
3910#endif
3911
3912 return behaviors;
3913}
3914
3915AccessibilityRole AccessibilityObject::buttonRoleType() const
3916{
3917 // If aria-pressed is present, then it should be exposed as a toggle button.
3918 // https://www.w3.org/TR/wai-aria#aria-pressed
3919 if (pressedIsPresent())
3920 return AccessibilityRole::ToggleButton;
3921 if (selfOrAncestorLinkHasPopup())
3922 return AccessibilityRole::PopUpButton;
3923 // We don't contemplate AccessibilityRole::RadioButton, as it depends on the input type.
3924
3925 return AccessibilityRole::Button;
3926}
3927
3928std::optional<InputType::Type> AccessibilityObject::inputType() const
3929{
3930 RefPtr input = dynamicDowncast<HTMLInputElement>(node());
3931 RefPtr inputType = input ? input->inputType() : nullptr;
3932 return inputType ? std::optional(inputType->type()) : std::nullopt;
3933}
3934
3935bool AccessibilityObject::isARIAHidden() const
3936{
3937 if (isFocused())
3938 return false;
3939
3940 RefPtr node = this->node();
3941 RefPtr element = dynamicDowncast<Element>(node);
3942 AtomString tag = element ? element->localName() : nullAtom();
3943 // https://github.com/w3c/aria/pull/1880
3944 // To prevent authors from hiding all content from assistive technology users, do not respect
3945 // aria-hidden on html, body, or document-root svg elements.
3946 if (tag == bodyTag || tag == htmlTag || (tag == SVGNames::svgTag && !element->parentNode()))
3947 return false;
3948
3949 if (RefPtr assignedSlot = node ? node->assignedSlot() : nullptr) {
3950 if (equalLettersIgnoringASCIICase(assignedSlot->attributeWithDefaultARIA(aria_hiddenAttr), "true"_s))
3951 return true;
3952 }
3953 return element && equalLettersIgnoringASCIICase(element->attributeWithDefaultARIA(aria_hiddenAttr), "true"_s);
3954}
3955
3956bool AccessibilityObject::isShowingValidationMessage() const
3957{
3958 if (RefPtr element = this->element()) {
3959 if (RefPtr listedElement = element->asValidatedFormListedElement())
3960 return listedElement->isShowingValidationMessage();
3961 }
3962 return false;
3963}
3964
3965String AccessibilityObject::validationMessage() const
3966{
3967 if (RefPtr element = this->element()) {
3968 if (RefPtr listedElement = element->asValidatedFormListedElement())
3969 return listedElement->validationMessage();
3970 }
3971 return String();
3972}
3973
3974AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const
3975{
3976 bool isHiddenUntilFound = false;
3977 if (CheckedPtr style = this->style()) {
3978 if (style->effectiveInert())
3979 return AccessibilityObjectInclusion::IgnoreObject;
3980 if (isVisibilityHidden(*style)) {
3981 isHiddenUntilFound = isHiddenUntilFoundContainer();
3982 if (!isHiddenUntilFound)
3983 return AccessibilityObjectInclusion::IgnoreObject;
3984 // We handle the `isHiddenUntilFound == true` case below.
3985 }
3986 }
3987
3988 if (CheckedPtr style = this->style()) {
3989 if (style->display() == Style::DisplayType::None && !isImageMapLink())
3990 return AccessibilityObjectInclusion::IgnoreObject;
3991 }
3992
3993 bool useParentData = !m_isIgnoredFromParentData.isNull();
3994 if (useParentData && (m_isIgnoredFromParentData.isAXHidden || m_isIgnoredFromParentData.isPresentationalChildOfAriaRole))
3995 return AccessibilityObjectInclusion::IgnoreObject;
3996
3997 if (isARIAHidden() || isWithinHiddenWebArea())
3998 return AccessibilityObjectInclusion::IgnoreObject;
3999
4000 bool ignoreARIAHidden = isFocused();
4001 if (Accessibility::findAncestor<AccessibilityObject>(*this, false, [&] (const auto& object) {
4002 const auto* style = object.style();
4003 if (style && style->display() == Style::DisplayType::None) {
4004 // We don't want to use AccessibilityObject::isRenderHidden(), as that also checks and returns true
4005 // for visibility:hidden, which would be wrong if |this| has a visibility:visible ancestor before
4006 // this visibility:hidden ancestor (visibility:visible cancels out visibility:hidden).
4007 //
4008 // We check the isVisibilityHidden at the top of this method, so that covers us as far as visibility goes.
4009 return true;
4010 }
4011
4012 return (!ignoreARIAHidden && object.isARIAHidden()) || object.ariaRoleHasPresentationalChildren() || !object.canHaveChildren();
4013 }))
4014 return AccessibilityObjectInclusion::IgnoreObject;
4015
4016 // Include <dialog> elements and elements with role="dialog".
4017 if (role() == AccessibilityRole::ApplicationDialog)
4018 return AccessibilityObjectInclusion::IncludeObject;
4019
4020 if (isHiddenUntilFound) {
4021 // We don't want to ignore hidden-until-found containers because we
4022 // want AXSearchManager::findMatchingObjects to walk over them in order
4023 // to search within them for revealable text matching the search query string.
4024 return AccessibilityObjectInclusion::IncludeObject;
4025 }
4026 return accessibilityPlatformIncludesObject();
4027}
4028
4029bool AccessibilityObject::isWithinHiddenWebArea() const
4030{
4031 RefPtr webArea = this->containingWebArea();
4032 if (!webArea)
4033 return false;
4034
4035#if ENABLE_ACCESSIBILITY_LOCAL_FRAME0
4036 if (RefPtr parentScrollView = dynamicDowncast<AccessibilityScrollView>(webArea->parentObject())) {
4037 if (parentScrollView->isHostingFrameInert() || parentScrollView->isHostingFrameRenderHidden()) {
4038 // The frame that hosts this web area is inert or render-hidden, so this entire frame should be hidden as well.
4039 return true;
4040 }
4041 }
4042#endif
4043
4044 // Fallback for same-process visibility/inert check.
4045 CheckedPtr renderView = webArea ? dynamicDowncast<RenderView>(webArea->renderer()) : nullptr;
4046 CheckedPtr frameRenderer = renderView ? renderView->frameView().frame().ownerRenderer() : nullptr;
4047 while (frameRenderer) {
4048 const CheckedRef style = frameRenderer->style();
4049 if (isVisibilityHidden(style) || style->effectiveInert())
4050 return true;
4051
4052 renderView = frameRenderer->document().renderView();
4053 frameRenderer = renderView ? renderView->frameView().frame().ownerRenderer() : nullptr;
4054 }
4055 return false;
4056}
4057
4058bool AccessibilityObject::isIgnored() const
4059{
4060 AXComputedObjectAttributeCache* attributeCache = nullptr;
4061 CheckedPtr axObjectCache = this->axObjectCache();
4062 if (axObjectCache)
4063 attributeCache = axObjectCache->computedObjectAttributeCache();
4064
4065 if (attributeCache) {
4066 AccessibilityObjectInclusion ignored = attributeCache->getIgnored(objectID());
4067 switch (ignored) {
4068 case AccessibilityObjectInclusion::IgnoreObject:
4069 return true;
4070 case AccessibilityObjectInclusion::IncludeObject:
4071 return false;
4072 case AccessibilityObjectInclusion::DefaultBehavior:
4073 break;
4074 }
4075 }
4076
4077 bool ignored = isIgnoredWithoutCache(axObjectCache);
4078
4079 // Refetch the attribute cache in case it was enabled as part of computing isIgnored.
4080 if (axObjectCache && (attributeCache = axObjectCache->computedObjectAttributeCache()))
4081 attributeCache->setIgnored(objectID(), ignored ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject);
4082
4083 return ignored;
4084}
4085
4086std::optional<bool> AccessibilityObject::cachedIsIgnored() const
4087{
4088 switch (m_lastKnownIsIgnoredValue) {
4089 case AccessibilityObjectInclusion::IgnoreObject:
4090 return true;
4091 case AccessibilityObjectInclusion::IncludeObject:
4092 return false;
4093 case AccessibilityObjectInclusion::DefaultBehavior:
4094 return std::nullopt;
4095 }
4096 return std::nullopt;
4097}
4098
4099bool AccessibilityObject::isIgnoredWithoutCache(AXObjectCache* cache) const
4100{
4101 // If we are in the midst of retrieving the current modal node, we only need to consider whether the object
4102 // is inherently ignored via computeIsIgnored. Also, calling ignoredFromModalPresence
4103 // in this state would cause infinite recursion.
4104 bool ignored = cache && cache->isRetrievingCurrentModalNode() ? false : ignoredFromModalPresence();
4105 if (!ignored)
4106 ignored = computeIsIgnored();
4107
4108 auto previousLastKnownIsIgnoredValue = m_lastKnownIsIgnoredValue;
4109 const_cast<AccessibilityObject*>(this)->setLastKnownIsIgnoredValue(ignored);
4110
4111 if (cache) {
4112 bool becameUnignored = previousLastKnownIsIgnoredValue == AccessibilityObjectInclusion::IgnoreObject && !ignored;
4113 bool becameIgnored = !becameUnignored && previousLastKnownIsIgnoredValue == AccessibilityObjectInclusion::IncludeObject && ignored;
4114
4115 if (becameIgnored) {
4116 // If a menu became ignored, e.g. because it became display:none, ATs need to be informed.
4117 postMenuClosedNotificationIfNecessary();
4118 cache->objectBecameIgnored(*this);
4119 } else if (becameUnignored)
4120 cache->objectBecameUnignored(*this);
4121
4122 if (becameUnignored || becameIgnored) {
4123 // FIXME: We should not have to submit a children-changed when ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE), but that causes a few failing
4124 // tests. We should fix that or remove this comment before enabling ENABLE(INCLUDE_IGNORED_IN_CORE_AX_TREE) by default for any port.
4125 cache->childrenChanged(parentObject());
4126 markPlatformWrapperIgnoredStateDirty();
4127 }
4128 }
4129 return ignored;
4130}
4131
4132void AccessibilityObject::recomputeIsIgnoredForDescendants(bool includeSelf)
4133{
4134 Accessibility::enumerateDescendantsIncludingIgnored<AXCoreObject>(*this, includeSelf, [] (auto& descendant) {
4135 downcast<AccessibilityObject>(descendant).recomputeIsIgnored();
4136 });
4137}
4138
4139Vector<Ref<Element>> AccessibilityObject::elementsFromAttribute(const QualifiedName& attribute) const
4140{
4141 RefPtr element = dynamicDowncast<Element>(node());
4142 if (!element)
4143 return { };
4144
4145 if (auto elementsFromAttribute = element->elementsArrayForAttributeInternal(attribute))
4146 return elementsFromAttribute.value();
4147
4148 if (CheckedPtr defaultARIA = element->customElementDefaultARIAIfExists()) {
4149 if (std::optional elements = defaultARIA->elementsForAttribute(*element, attribute))
4150 return *elements;
4151 }
4152
4153 return { };
4154}
4155
4156#if PLATFORM(COCOA)(defined 1 && 1)
4157bool AccessibilityObject::preventKeyboardDOMEventDispatch() const
4158{
4159 RefPtr frame = this->frame();
4160 return frame && frame->settings().preventKeyboardDOMEventDispatch();
4161}
4162
4163void AccessibilityObject::setPreventKeyboardDOMEventDispatch(bool on)
4164{
4165 RefPtr frame = this->frame();
4166 if (!frame)
4167 return;
4168 frame->settings().setPreventKeyboardDOMEventDispatch(on);
4169}
4170#endif
4171
4172AccessibilityObject* AccessibilityObject::radioGroupAncestor() const
4173{
4174 return Accessibility::findAncestor<AccessibilityObject>(*this, false, [] (const AccessibilityObject& object) {
4175 return object.isRadioGroup();
4176 });
4177}
4178
4179bool AccessibilityObject::isStyleFormatGroup() const
4180{
4181 if (isCode())
4182 return true;
4183
4184 auto elementName = this->elementName();
4185 return elementName == ElementName::HTML_kbd || elementName == ElementName::HTML_code
4186 || elementName == ElementName::HTML_pre || elementName == ElementName::HTML_samp
4187 || elementName == ElementName::HTML_var || elementName == ElementName::HTML_cite
4188 || elementName == ElementName::HTML_ins || elementName == ElementName::HTML_del
4189 || elementName == ElementName::HTML_sup || elementName == ElementName::HTML_sub;
4190}
4191
4192bool AccessibilityObject::isKeyboardFocusable() const
4193{
4194 RefPtr element = this->element();
4195 return element && element->isFocusable();
4196}
4197
4198bool AccessibilityObject::isContainedBySecureField() const
4199{
4200 Node* node = this->node();
4201 if (!node)
4202 return false;
4203
4204 if (ariaRoleAttribute() != AccessibilityRole::Unknown)
4205 return false;
4206
4207 RefPtr input = dynamicDowncast<HTMLInputElement>(node->shadowHost());
4208 return input && input->isSecureField();
4209}
4210
4211AXCoreObject::AccessibilityChildrenVector AccessibilityObject::relatedObjects(AXRelation relation) const
4212{
4213 CheckedPtr cache = axObjectCache();
4214 if (!cache)
4215 return { };
4216
4217 auto relatedObjectIDs = cache->relatedObjectIDsFor(*this, relation);
4218 if (!relatedObjectIDs)
4219 return { };
4220 return cache->objectsForIDs(*relatedObjectIDs);
4221}
4222
4223bool AccessibilityObject::shouldFocusActiveDescendant() const
4224{
4225 switch (ariaRoleAttribute()) {
4226 case AccessibilityRole::Group:
4227 case AccessibilityRole::ListBox:
4228 case AccessibilityRole::Menu:
4229 case AccessibilityRole::MenuBar:
4230 case AccessibilityRole::RadioGroup:
4231 case AccessibilityRole::Row:
4232 case AccessibilityRole::PopUpButton:
4233 case AccessibilityRole::Meter:
4234 case AccessibilityRole::ProgressIndicator:
4235 case AccessibilityRole::Toolbar:
4236 case AccessibilityRole::Tree:
4237 case AccessibilityRole::Grid:
4238 /* FIXME: replace these with actual roles when they are added to AccessibilityRole
4239 composite
4240 alert
4241 alertdialog
4242 status
4243 timer
4244 */
4245 return true;
4246 default:
4247 return false;
4248 }
4249}
4250
4251bool AccessibilityObject::ariaRoleHasPresentationalChildren() const
4252{
4253 switch (ariaRoleAttribute()) {
4254 case AccessibilityRole::Button:
4255 case AccessibilityRole::Slider:
4256 case AccessibilityRole::Image:
4257 case AccessibilityRole::ProgressIndicator:
4258 case AccessibilityRole::SpinButton:
4259 return true;
4260 default:
4261 return false;
4262 }
4263}
4264
4265void AccessibilityObject::setIsIgnoredFromParentDataForChild(AccessibilityObject& child)
4266{
4267 AccessibilityIsIgnoredFromParentData result = AccessibilityIsIgnoredFromParentData(this);
4268 if (!m_isIgnoredFromParentData.isNull()) {
4269 result.isAXHidden = (m_isIgnoredFromParentData.isAXHidden || child.isARIAHidden()) && !child.isFocused();
4270 result.isPresentationalChildOfAriaRole = m_isIgnoredFromParentData.isPresentationalChildOfAriaRole || ariaRoleHasPresentationalChildren();
4271 result.isDescendantOfBarrenParent = m_isIgnoredFromParentData.isDescendantOfBarrenParent || !canHaveChildren();
4272 } else {
4273 if (child.isARIAHidden())
4274 result.isAXHidden = true;
4275
4276 bool ignoreARIAHidden = child.isFocused();
4277 for (auto* object = child.parentObject(); object; object = object->parentObject()) {
4278 if (!result.isAXHidden && !ignoreARIAHidden && object->isARIAHidden())
4279 result.isAXHidden = true;
4280
4281 if (!result.isPresentationalChildOfAriaRole && object->ariaRoleHasPresentationalChildren())
4282 result.isPresentationalChildOfAriaRole = true;
4283
4284 if (!result.isDescendantOfBarrenParent && !object->canHaveChildren())
4285 result.isDescendantOfBarrenParent = true;
4286 }
4287 }
4288
4289 child.setIsIgnoredFromParentData(result);
4290}
4291
4292String AccessibilityObject::innerHTML() const
4293{
4294 RefPtr element = this->element();
4295 return element ? element->innerHTML() : String();
4296}
4297
4298String AccessibilityObject::outerHTML() const
4299{
4300 RefPtr element = this->element();
4301 return element ? element->outerHTML() : String();
4302}
4303
4304bool AccessibilityObject::ignoredByRowAncestor() const
4305{
4306 bool wasExposedRow = false;
4307 RefPtr ancestor = Accessibility::findAncestor<AccessibilityObject>(*this, false, [&wasExposedRow] (const AccessibilityObject& ancestor) {
4308 // If an object has a table cell ancestor (before a table row), that is a cell's contents, so don't ignore it.
4309 // 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.
4310 if (ancestor.isTableCell() || ancestor.isTable())
4311 return true;
4312
4313 if (ancestor.isExposedTableRow())
4314 wasExposedRow = true;
4315
4316 return wasExposedRow;
4317 });
4318
4319 return ancestor && wasExposedRow;
4320}
4321
4322AccessibilityObject* AccessibilityObject::containingWebArea() const
4323{
4324 CheckedPtr frameView = documentFrameView();
4325 CheckedPtr cache = axObjectCache();
4326 RefPtr root = cache ? dynamicDowncast<AccessibilityScrollView>(cache->getOrCreate(frameView.get())) : nullptr;
4327 return root ? root->webAreaObject() : nullptr;
4328}
4329
4330} // namespace WebCore