1 | /* |
2 | * Copyright (C) 2008-2009, 2011, 2017 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 "AXObjectCache.h" |
33 | #include "AccessibilityRenderObject.h" |
34 | #include "AccessibilityScrollView.h" |
35 | #include "AccessibilityTable.h" |
36 | #include "Chrome.h" |
37 | #include "ChromeClient.h" |
38 | #include "DOMTokenList.h" |
39 | #include "Editing.h" |
40 | #include "Editor.h" |
41 | #include "ElementIterator.h" |
42 | #include "Event.h" |
43 | #include "EventDispatcher.h" |
44 | #include "EventHandler.h" |
45 | #include "EventNames.h" |
46 | #include "FloatRect.h" |
47 | #include "FocusController.h" |
48 | #include "Frame.h" |
49 | #include "FrameLoader.h" |
50 | #include "FrameSelection.h" |
51 | #include "HTMLDetailsElement.h" |
52 | #include "HTMLFormControlElement.h" |
53 | #include "HTMLInputElement.h" |
54 | #include "HTMLMediaElement.h" |
55 | #include "HTMLNames.h" |
56 | #include "HTMLParserIdioms.h" |
57 | #include "HTMLTextAreaElement.h" |
58 | #include "HitTestResult.h" |
59 | #include "LocalizedStrings.h" |
60 | #include "MathMLNames.h" |
61 | #include "NodeList.h" |
62 | #include "NodeTraversal.h" |
63 | #include "Page.h" |
64 | #include "RenderImage.h" |
65 | #include "RenderLayer.h" |
66 | #include "RenderListItem.h" |
67 | #include "RenderListMarker.h" |
68 | #include "RenderMenuList.h" |
69 | #include "RenderText.h" |
70 | #include "RenderTextControl.h" |
71 | #include "RenderTheme.h" |
72 | #include "RenderView.h" |
73 | #include "RenderWidget.h" |
74 | #include "RenderedPosition.h" |
75 | #include "RuntimeEnabledFeatures.h" |
76 | #include "Settings.h" |
77 | #include "TextCheckerClient.h" |
78 | #include "TextCheckingHelper.h" |
79 | #include "TextIterator.h" |
80 | #include "UserGestureIndicator.h" |
81 | #include "VisibleUnits.h" |
82 | #include <wtf/NeverDestroyed.h> |
83 | #include <wtf/StdLibExtras.h> |
84 | #include <wtf/text/StringBuilder.h> |
85 | #include <wtf/text/StringView.h> |
86 | #include <wtf/text/WTFString.h> |
87 | #include <wtf/unicode/CharacterNames.h> |
88 | |
89 | namespace WebCore { |
90 | |
91 | using namespace HTMLNames; |
92 | |
93 | AccessibilityObject::~AccessibilityObject() |
94 | { |
95 | ASSERT(isDetached()); |
96 | } |
97 | |
98 | void AccessibilityObject::detach(AccessibilityDetachmentType detachmentType, AXObjectCache* cache) |
99 | { |
100 | // Menu close events need to notify the platform. No element is used in the notification because it's a destruction event. |
101 | if (detachmentType == AccessibilityDetachmentType::ElementDestroyed && roleValue() == AccessibilityRole::Menu && cache) |
102 | cache->postNotification(nullptr, &cache->document(), AXObjectCache::AXMenuClosed); |
103 | |
104 | // Clear any children and call detachFromParent on them so that |
105 | // no children are left with dangling pointers to their parent. |
106 | clearChildren(); |
107 | |
108 | #if HAVE(ACCESSIBILITY) |
109 | setWrapper(nullptr); |
110 | #endif |
111 | } |
112 | |
113 | bool AccessibilityObject::isDetached() const |
114 | { |
115 | #if HAVE(ACCESSIBILITY) |
116 | return !wrapper(); |
117 | #else |
118 | return true; |
119 | #endif |
120 | } |
121 | |
122 | bool AccessibilityObject::isAccessibilityObjectSearchMatchAtIndex(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria, size_t index) |
123 | { |
124 | switch (criteria->searchKeys[index]) { |
125 | // The AccessibilitySearchKey::AnyType matches any non-null AccessibilityObject. |
126 | case AccessibilitySearchKey::AnyType: |
127 | return true; |
128 | |
129 | case AccessibilitySearchKey::Article: |
130 | return axObject->roleValue() == AccessibilityRole::DocumentArticle; |
131 | |
132 | case AccessibilitySearchKey::BlockquoteSameLevel: |
133 | return criteria->startObject |
134 | && axObject->isBlockquote() |
135 | && axObject->blockquoteLevel() == criteria->startObject->blockquoteLevel(); |
136 | |
137 | case AccessibilitySearchKey::Blockquote: |
138 | return axObject->isBlockquote(); |
139 | |
140 | case AccessibilitySearchKey::BoldFont: |
141 | return axObject->hasBoldFont(); |
142 | |
143 | case AccessibilitySearchKey::Button: |
144 | return axObject->isButton(); |
145 | |
146 | case AccessibilitySearchKey::CheckBox: |
147 | return axObject->isCheckbox(); |
148 | |
149 | case AccessibilitySearchKey::Control: |
150 | return axObject->isControl(); |
151 | |
152 | case AccessibilitySearchKey::DifferentType: |
153 | return criteria->startObject |
154 | && axObject->roleValue() != criteria->startObject->roleValue(); |
155 | |
156 | case AccessibilitySearchKey::FontChange: |
157 | return criteria->startObject |
158 | && !axObject->hasSameFont(criteria->startObject->renderer()); |
159 | |
160 | case AccessibilitySearchKey::FontColorChange: |
161 | return criteria->startObject |
162 | && !axObject->hasSameFontColor(criteria->startObject->renderer()); |
163 | |
164 | case AccessibilitySearchKey::Frame: |
165 | return axObject->isWebArea(); |
166 | |
167 | case AccessibilitySearchKey::Graphic: |
168 | return axObject->isImage(); |
169 | |
170 | case AccessibilitySearchKey::HeadingLevel1: |
171 | return axObject->headingLevel() == 1; |
172 | |
173 | case AccessibilitySearchKey::HeadingLevel2: |
174 | return axObject->headingLevel() == 2; |
175 | |
176 | case AccessibilitySearchKey::HeadingLevel3: |
177 | return axObject->headingLevel() == 3; |
178 | |
179 | case AccessibilitySearchKey::HeadingLevel4: |
180 | return axObject->headingLevel() == 4; |
181 | |
182 | case AccessibilitySearchKey::HeadingLevel5: |
183 | return axObject->headingLevel() == 5; |
184 | |
185 | case AccessibilitySearchKey::HeadingLevel6: |
186 | return axObject->headingLevel() == 6; |
187 | |
188 | case AccessibilitySearchKey::HeadingSameLevel: |
189 | return criteria->startObject |
190 | && axObject->isHeading() |
191 | && axObject->headingLevel() == criteria->startObject->headingLevel(); |
192 | |
193 | case AccessibilitySearchKey::Heading: |
194 | return axObject->isHeading(); |
195 | |
196 | case AccessibilitySearchKey::Highlighted: |
197 | return axObject->hasHighlighting(); |
198 | |
199 | case AccessibilitySearchKey::KeyboardFocusable: |
200 | return axObject->isKeyboardFocusable(); |
201 | |
202 | case AccessibilitySearchKey::ItalicFont: |
203 | return axObject->hasItalicFont(); |
204 | |
205 | case AccessibilitySearchKey::Landmark: |
206 | return axObject->isLandmark(); |
207 | |
208 | case AccessibilitySearchKey::Link: { |
209 | bool isLink = axObject->isLink(); |
210 | #if PLATFORM(IOS_FAMILY) |
211 | if (!isLink) |
212 | isLink = axObject->isDescendantOfRole(AccessibilityRole::WebCoreLink); |
213 | #endif |
214 | return isLink; |
215 | } |
216 | |
217 | case AccessibilitySearchKey::List: |
218 | return axObject->isList(); |
219 | |
220 | case AccessibilitySearchKey::LiveRegion: |
221 | return axObject->supportsLiveRegion(); |
222 | |
223 | case AccessibilitySearchKey::MisspelledWord: |
224 | return axObject->hasMisspelling(); |
225 | |
226 | case AccessibilitySearchKey::Outline: |
227 | return axObject->isTree(); |
228 | |
229 | case AccessibilitySearchKey::PlainText: |
230 | return axObject->hasPlainText(); |
231 | |
232 | case AccessibilitySearchKey::RadioGroup: |
233 | return axObject->isRadioGroup(); |
234 | |
235 | case AccessibilitySearchKey::SameType: |
236 | return criteria->startObject |
237 | && axObject->roleValue() == criteria->startObject->roleValue(); |
238 | |
239 | case AccessibilitySearchKey::StaticText: |
240 | return axObject->isStaticText(); |
241 | |
242 | case AccessibilitySearchKey::StyleChange: |
243 | return criteria->startObject |
244 | && !axObject->hasSameStyle(criteria->startObject->renderer()); |
245 | |
246 | case AccessibilitySearchKey::TableSameLevel: |
247 | return criteria->startObject |
248 | && is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility() |
249 | && downcast<AccessibilityTable>(*axObject).tableLevel() == criteria->startObject->tableLevel(); |
250 | |
251 | case AccessibilitySearchKey::Table: |
252 | return is<AccessibilityTable>(*axObject) && downcast<AccessibilityTable>(*axObject).isExposableThroughAccessibility(); |
253 | |
254 | case AccessibilitySearchKey::TextField: |
255 | return axObject->isTextControl(); |
256 | |
257 | case AccessibilitySearchKey::Underline: |
258 | return axObject->hasUnderline(); |
259 | |
260 | case AccessibilitySearchKey::UnvisitedLink: |
261 | return axObject->isUnvisited(); |
262 | |
263 | case AccessibilitySearchKey::VisitedLink: |
264 | return axObject->isVisited(); |
265 | |
266 | default: |
267 | return false; |
268 | } |
269 | } |
270 | |
271 | bool AccessibilityObject::isAccessibilityObjectSearchMatch(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria) |
272 | { |
273 | if (!axObject || !criteria) |
274 | return false; |
275 | |
276 | size_t length = criteria->searchKeys.size(); |
277 | for (size_t i = 0; i < length; ++i) { |
278 | if (isAccessibilityObjectSearchMatchAtIndex(axObject, criteria, i)) { |
279 | if (criteria->visibleOnly && !axObject->isOnscreen()) |
280 | return false; |
281 | return true; |
282 | } |
283 | } |
284 | return false; |
285 | } |
286 | |
287 | bool AccessibilityObject::isAccessibilityTextSearchMatch(AccessibilityObject* axObject, AccessibilitySearchCriteria* criteria) |
288 | { |
289 | if (!axObject || !criteria) |
290 | return false; |
291 | |
292 | return axObject->accessibilityObjectContainsText(&criteria->searchText); |
293 | } |
294 | |
295 | bool AccessibilityObject::accessibilityObjectContainsText(String* text) const |
296 | { |
297 | // If text is null or empty we return true. |
298 | return !text |
299 | || text->isEmpty() |
300 | || findPlainText(title(), *text, CaseInsensitive) |
301 | || findPlainText(accessibilityDescription(), *text, CaseInsensitive) |
302 | || findPlainText(stringValue(), *text, CaseInsensitive); |
303 | } |
304 | |
305 | // ARIA marks elements as having their accessible name derive from either their contents, or their author provide name. |
306 | bool AccessibilityObject::accessibleNameDerivesFromContent() const |
307 | { |
308 | // First check for objects specifically identified by ARIA. |
309 | switch (ariaRoleAttribute()) { |
310 | case AccessibilityRole::ApplicationAlert: |
311 | case AccessibilityRole::ApplicationAlertDialog: |
312 | case AccessibilityRole::ApplicationDialog: |
313 | case AccessibilityRole::ApplicationGroup: |
314 | case AccessibilityRole::ApplicationLog: |
315 | case AccessibilityRole::ApplicationMarquee: |
316 | case AccessibilityRole::ApplicationStatus: |
317 | case AccessibilityRole::ApplicationTimer: |
318 | case AccessibilityRole::ComboBox: |
319 | case AccessibilityRole::Definition: |
320 | case AccessibilityRole::Document: |
321 | case AccessibilityRole::DocumentArticle: |
322 | case AccessibilityRole::DocumentMath: |
323 | case AccessibilityRole::DocumentNote: |
324 | case AccessibilityRole::LandmarkRegion: |
325 | case AccessibilityRole::LandmarkDocRegion: |
326 | case AccessibilityRole::Form: |
327 | case AccessibilityRole::Grid: |
328 | case AccessibilityRole::Group: |
329 | case AccessibilityRole::Image: |
330 | case AccessibilityRole::List: |
331 | case AccessibilityRole::ListBox: |
332 | case AccessibilityRole::LandmarkBanner: |
333 | case AccessibilityRole::LandmarkComplementary: |
334 | case AccessibilityRole::LandmarkContentInfo: |
335 | case AccessibilityRole::LandmarkNavigation: |
336 | case AccessibilityRole::LandmarkMain: |
337 | case AccessibilityRole::LandmarkSearch: |
338 | case AccessibilityRole::Menu: |
339 | case AccessibilityRole::MenuBar: |
340 | case AccessibilityRole::ProgressIndicator: |
341 | case AccessibilityRole::Meter: |
342 | case AccessibilityRole::RadioGroup: |
343 | case AccessibilityRole::ScrollBar: |
344 | case AccessibilityRole::Slider: |
345 | case AccessibilityRole::SpinButton: |
346 | case AccessibilityRole::Splitter: |
347 | case AccessibilityRole::Table: |
348 | case AccessibilityRole::TabList: |
349 | case AccessibilityRole::TabPanel: |
350 | case AccessibilityRole::TextArea: |
351 | case AccessibilityRole::TextField: |
352 | case AccessibilityRole::Toolbar: |
353 | case AccessibilityRole::TreeGrid: |
354 | case AccessibilityRole::Tree: |
355 | case AccessibilityRole::WebApplication: |
356 | return false; |
357 | default: |
358 | break; |
359 | } |
360 | |
361 | // Now check for generically derived elements now that we know the element does not match a specific ARIA role. |
362 | switch (roleValue()) { |
363 | case AccessibilityRole::Slider: |
364 | case AccessibilityRole::ListBox: |
365 | return false; |
366 | default: |
367 | break; |
368 | } |
369 | |
370 | return true; |
371 | } |
372 | |
373 | String AccessibilityObject::computedLabel() |
374 | { |
375 | // This method is being called by WebKit inspector, which may happen at any time, so we need to update our backing store now. |
376 | // Also hold onto this object in case updateBackingStore deletes this node. |
377 | RefPtr<AccessibilityObject> protectedThis(this); |
378 | updateBackingStore(); |
379 | Vector<AccessibilityText> text; |
380 | accessibilityText(text); |
381 | if (text.size()) |
382 | return text[0].text; |
383 | return String(); |
384 | } |
385 | |
386 | bool AccessibilityObject::isBlockquote() const |
387 | { |
388 | return roleValue() == AccessibilityRole::Blockquote; |
389 | } |
390 | |
391 | bool AccessibilityObject::isTextControl() const |
392 | { |
393 | switch (roleValue()) { |
394 | case AccessibilityRole::ComboBox: |
395 | case AccessibilityRole::SearchField: |
396 | case AccessibilityRole::TextArea: |
397 | case AccessibilityRole::TextField: |
398 | return true; |
399 | default: |
400 | return false; |
401 | } |
402 | } |
403 | |
404 | bool AccessibilityObject::isARIATextControl() const |
405 | { |
406 | return ariaRoleAttribute() == AccessibilityRole::TextArea || ariaRoleAttribute() == AccessibilityRole::TextField || ariaRoleAttribute() == AccessibilityRole::SearchField; |
407 | } |
408 | |
409 | bool AccessibilityObject::isNonNativeTextControl() const |
410 | { |
411 | return (isARIATextControl() || hasContentEditableAttributeSet()) && !isNativeTextControl(); |
412 | } |
413 | |
414 | bool AccessibilityObject::isLandmark() const |
415 | { |
416 | switch (roleValue()) { |
417 | case AccessibilityRole::LandmarkBanner: |
418 | case AccessibilityRole::LandmarkComplementary: |
419 | case AccessibilityRole::LandmarkContentInfo: |
420 | case AccessibilityRole::LandmarkDocRegion: |
421 | case AccessibilityRole::LandmarkMain: |
422 | case AccessibilityRole::LandmarkNavigation: |
423 | case AccessibilityRole::LandmarkRegion: |
424 | case AccessibilityRole::LandmarkSearch: |
425 | return true; |
426 | default: |
427 | return false; |
428 | } |
429 | } |
430 | |
431 | bool AccessibilityObject::hasMisspelling() const |
432 | { |
433 | if (!node()) |
434 | return false; |
435 | |
436 | Frame* frame = node()->document().frame(); |
437 | if (!frame) |
438 | return false; |
439 | |
440 | Editor& editor = frame->editor(); |
441 | |
442 | TextCheckerClient* textChecker = editor.textChecker(); |
443 | if (!textChecker) |
444 | return false; |
445 | |
446 | bool isMisspelled = false; |
447 | |
448 | if (unifiedTextCheckerEnabled(frame)) { |
449 | Vector<TextCheckingResult> results; |
450 | checkTextOfParagraph(*textChecker, stringValue(), TextCheckingType::Spelling, results, frame->selection().selection()); |
451 | if (!results.isEmpty()) |
452 | isMisspelled = true; |
453 | return isMisspelled; |
454 | } |
455 | |
456 | int misspellingLength = 0; |
457 | int misspellingLocation = -1; |
458 | textChecker->checkSpellingOfString(stringValue(), &misspellingLocation, &misspellingLength); |
459 | if (misspellingLength || misspellingLocation != -1) |
460 | isMisspelled = true; |
461 | |
462 | return isMisspelled; |
463 | } |
464 | |
465 | unsigned AccessibilityObject::blockquoteLevel() const |
466 | { |
467 | unsigned level = 0; |
468 | for (Node* elementNode = node(); elementNode; elementNode = elementNode->parentNode()) { |
469 | if (elementNode->hasTagName(blockquoteTag)) |
470 | ++level; |
471 | } |
472 | |
473 | return level; |
474 | } |
475 | |
476 | AccessibilityObject* AccessibilityObject::parentObjectUnignored() const |
477 | { |
478 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) { |
479 | return !object.accessibilityIsIgnored(); |
480 | })); |
481 | } |
482 | |
483 | AccessibilityObject* AccessibilityObject::previousSiblingUnignored(int limit) const |
484 | { |
485 | AccessibilityObject* previous; |
486 | ASSERT(limit >= 0); |
487 | for (previous = previousSibling(); previous && previous->accessibilityIsIgnored(); previous = previous->previousSibling()) { |
488 | limit--; |
489 | if (limit <= 0) |
490 | break; |
491 | } |
492 | return previous; |
493 | } |
494 | |
495 | FloatRect AccessibilityObject::convertFrameToSpace(const FloatRect& frameRect, AccessibilityConversionSpace conversionSpace) const |
496 | { |
497 | ASSERT(isMainThread()); |
498 | |
499 | // Find the appropriate scroll view to use to convert the contents to the window. |
500 | const auto parentAccessibilityScrollView = ancestorAccessibilityScrollView(false /* includeSelf */); |
501 | auto* parentScrollView = parentAccessibilityScrollView ? parentAccessibilityScrollView->scrollView() : nullptr; |
502 | |
503 | auto snappedFrameRect = snappedIntRect(IntRect(frameRect)); |
504 | if (parentScrollView) |
505 | snappedFrameRect = parentScrollView->contentsToRootView(snappedFrameRect); |
506 | |
507 | if (conversionSpace == AccessibilityConversionSpace::Screen) { |
508 | auto page = this->page(); |
509 | if (!page) |
510 | return snappedFrameRect; |
511 | |
512 | // If we have an empty chrome client (like SVG) then we should use the page |
513 | // of the scroll view parent to help us get to the screen rect. |
514 | if (parentAccessibilityScrollView && page->chrome().client().isEmptyChromeClient()) |
515 | page = parentAccessibilityScrollView->page(); |
516 | |
517 | snappedFrameRect = page->chrome().rootViewToAccessibilityScreen(snappedFrameRect); |
518 | } |
519 | |
520 | return snappedFrameRect; |
521 | } |
522 | |
523 | FloatRect AccessibilityObject::relativeFrame() const |
524 | { |
525 | return convertFrameToSpace(elementRect(), AccessibilityConversionSpace::Page); |
526 | } |
527 | |
528 | AccessibilityObject* AccessibilityObject::nextSiblingUnignored(int limit) const |
529 | { |
530 | AccessibilityObject* next; |
531 | ASSERT(limit >= 0); |
532 | for (next = nextSibling(); next && next->accessibilityIsIgnored(); next = next->nextSibling()) { |
533 | limit--; |
534 | if (limit <= 0) |
535 | break; |
536 | } |
537 | return next; |
538 | } |
539 | |
540 | AccessibilityObject* AccessibilityObject::firstAccessibleObjectFromNode(const Node* node) |
541 | { |
542 | if (!node) |
543 | return nullptr; |
544 | |
545 | AXObjectCache* cache = node->document().axObjectCache(); |
546 | if (!cache) |
547 | return nullptr; |
548 | |
549 | AccessibilityObject* accessibleObject = cache->getOrCreate(node->renderer()); |
550 | while (accessibleObject && accessibleObject->accessibilityIsIgnored()) { |
551 | node = NodeTraversal::next(*node); |
552 | |
553 | while (node && !node->renderer()) |
554 | node = NodeTraversal::nextSkippingChildren(*node); |
555 | |
556 | if (!node) |
557 | return nullptr; |
558 | |
559 | accessibleObject = cache->getOrCreate(node->renderer()); |
560 | } |
561 | |
562 | return accessibleObject; |
563 | } |
564 | |
565 | bool AccessibilityObject::isDescendantOfRole(AccessibilityRole role) const |
566 | { |
567 | return AccessibilityObject::matchedParent(*this, false, [&role] (const AccessibilityObject& object) { |
568 | return object.roleValue() == role; |
569 | }) != nullptr; |
570 | } |
571 | |
572 | static void appendAccessibilityObject(AccessibilityObject* object, AccessibilityObject::AccessibilityChildrenVector& results) |
573 | { |
574 | // Find the next descendant of this attachment object so search can continue through frames. |
575 | if (object->isAttachment()) { |
576 | Widget* widget = object->widgetForAttachmentView(); |
577 | if (!is<FrameView>(widget)) |
578 | return; |
579 | |
580 | Document* document = downcast<FrameView>(*widget).frame().document(); |
581 | if (!document || !document->hasLivingRenderTree()) |
582 | return; |
583 | |
584 | object = object->axObjectCache()->getOrCreate(document); |
585 | } |
586 | |
587 | if (object) |
588 | results.append(object); |
589 | } |
590 | |
591 | void AccessibilityObject::insertChild(AccessibilityObject* child, unsigned index) |
592 | { |
593 | if (!child) |
594 | return; |
595 | |
596 | // If the parent is asking for this child's children, then either it's the first time (and clearing is a no-op), |
597 | // or its visibility has changed. In the latter case, this child may have a stale child cached. |
598 | // This can prevent aria-hidden changes from working correctly. Hence, whenever a parent is getting children, ensure data is not stale. |
599 | // Only clear the child's children when we know it's in the updating chain in order to avoid unnecessary work. |
600 | if (child->needsToUpdateChildren() || m_subtreeDirty) { |
601 | child->clearChildren(); |
602 | // Pass m_subtreeDirty flag down to the child so that children cache gets reset properly. |
603 | if (m_subtreeDirty) |
604 | child->setNeedsToUpdateSubtree(); |
605 | } else { |
606 | // For some reason the grand children might be detached so that we need to regenerate the |
607 | // children list of this child. |
608 | for (const auto& grandChild : child->children(false)) { |
609 | if (grandChild->isDetachedFromParent()) { |
610 | child->clearChildren(); |
611 | break; |
612 | } |
613 | } |
614 | } |
615 | |
616 | setIsIgnoredFromParentDataForChild(child); |
617 | if (child->accessibilityIsIgnored()) { |
618 | const auto& children = child->children(); |
619 | size_t length = children.size(); |
620 | for (size_t i = 0; i < length; ++i) |
621 | m_children.insert(index + i, children[i]); |
622 | } else { |
623 | ASSERT(child->parentObject() == this); |
624 | m_children.insert(index, child); |
625 | } |
626 | |
627 | // Reset the child's m_isIgnoredFromParentData since we are done adding that child and its children. |
628 | child->clearIsIgnoredFromParentData(); |
629 | } |
630 | |
631 | void AccessibilityObject::addChild(AccessibilityObject* child) |
632 | { |
633 | insertChild(child, m_children.size()); |
634 | } |
635 | |
636 | static void appendChildrenToArray(AccessibilityObject* object, bool isForward, AccessibilityObject* startObject, AccessibilityObject::AccessibilityChildrenVector& results) |
637 | { |
638 | // A table's children includes elements whose own children are also the table's children (due to the way the Mac exposes tables). |
639 | // The rows from the table should be queried, since those are direct descendants of the table, and they contain content. |
640 | const auto& searchChildren = is<AccessibilityTable>(*object) && downcast<AccessibilityTable>(*object).isExposableThroughAccessibility() ? downcast<AccessibilityTable>(*object).rows() : object->children(); |
641 | |
642 | size_t childrenSize = searchChildren.size(); |
643 | |
644 | size_t startIndex = isForward ? childrenSize : 0; |
645 | size_t endIndex = isForward ? 0 : childrenSize; |
646 | |
647 | // If the startObject is ignored, we should use an accessible sibling as a start element instead. |
648 | if (startObject && startObject->accessibilityIsIgnored() && startObject->isDescendantOfObject(object)) { |
649 | AccessibilityObject* parentObject = startObject->parentObject(); |
650 | // Go up the parent chain to find the highest ancestor that's also being ignored. |
651 | while (parentObject && parentObject->accessibilityIsIgnored()) { |
652 | if (parentObject == object) |
653 | break; |
654 | startObject = parentObject; |
655 | parentObject = parentObject->parentObject(); |
656 | } |
657 | // Get the un-ignored sibling based on the search direction, and update the searchPosition. |
658 | while (startObject && startObject->accessibilityIsIgnored()) |
659 | startObject = isForward ? startObject->previousSibling() : startObject->nextSibling(); |
660 | } |
661 | |
662 | size_t searchPosition = startObject ? searchChildren.find(startObject) : WTF::notFound; |
663 | |
664 | if (searchPosition != WTF::notFound) { |
665 | if (isForward) |
666 | endIndex = searchPosition + 1; |
667 | else |
668 | endIndex = searchPosition; |
669 | } |
670 | |
671 | // This is broken into two statements so that it's easier read. |
672 | if (isForward) { |
673 | for (size_t i = startIndex; i > endIndex; i--) |
674 | appendAccessibilityObject(searchChildren.at(i - 1).get(), results); |
675 | } else { |
676 | for (size_t i = startIndex; i < endIndex; i++) |
677 | appendAccessibilityObject(searchChildren.at(i).get(), results); |
678 | } |
679 | } |
680 | |
681 | // Returns true if the number of results is now >= the number of results desired. |
682 | bool AccessibilityObject::objectMatchesSearchCriteriaWithResultLimit(AccessibilityObject* object, AccessibilitySearchCriteria* criteria, AccessibilityChildrenVector& results) |
683 | { |
684 | if (isAccessibilityObjectSearchMatch(object, criteria) && isAccessibilityTextSearchMatch(object, criteria)) { |
685 | results.append(object); |
686 | |
687 | // Enough results were found to stop searching. |
688 | if (results.size() >= criteria->resultsLimit) |
689 | return true; |
690 | } |
691 | |
692 | return false; |
693 | } |
694 | |
695 | void AccessibilityObject::findMatchingObjects(AccessibilitySearchCriteria* criteria, AccessibilityChildrenVector& results) |
696 | { |
697 | ASSERT(criteria); |
698 | |
699 | if (!criteria) |
700 | return; |
701 | |
702 | if (AXObjectCache* cache = axObjectCache()) |
703 | cache->startCachingComputedObjectAttributesUntilTreeMutates(); |
704 | |
705 | // This search mechanism only searches the elements before/after the starting object. |
706 | // It does this by stepping up the parent chain and at each level doing a DFS. |
707 | |
708 | // If there's no start object, it means we want to search everything. |
709 | AccessibilityObject* startObject = criteria->startObject; |
710 | if (!startObject) |
711 | startObject = this; |
712 | |
713 | bool isForward = criteria->searchDirection == AccessibilitySearchDirection::Next; |
714 | |
715 | // The first iteration of the outer loop will examine the children of the start object for matches. However, when |
716 | // iterating backwards, the start object children should not be considered, so the loop is skipped ahead. We make an |
717 | // exception when no start object was specified because we want to search everything regardless of search direction. |
718 | AccessibilityObject* previousObject = nullptr; |
719 | if (!isForward && startObject != this) { |
720 | previousObject = startObject; |
721 | startObject = startObject->parentObjectUnignored(); |
722 | } |
723 | |
724 | // The outer loop steps up the parent chain each time (unignored is important here because otherwise elements would be searched twice) |
725 | for (AccessibilityObject* stopSearchElement = parentObjectUnignored(); startObject && startObject != stopSearchElement; startObject = startObject->parentObjectUnignored()) { |
726 | |
727 | // Only append the children after/before the previous element, so that the search does not check elements that are |
728 | // already behind/ahead of start element. |
729 | AccessibilityChildrenVector searchStack; |
730 | if (!criteria->immediateDescendantsOnly || startObject == this) |
731 | appendChildrenToArray(startObject, isForward, previousObject, searchStack); |
732 | |
733 | // This now does a DFS at the current level of the parent. |
734 | while (!searchStack.isEmpty()) { |
735 | AccessibilityObject* searchObject = searchStack.last().get(); |
736 | searchStack.removeLast(); |
737 | |
738 | if (objectMatchesSearchCriteriaWithResultLimit(searchObject, criteria, results)) |
739 | break; |
740 | |
741 | if (!criteria->immediateDescendantsOnly) |
742 | appendChildrenToArray(searchObject, isForward, 0, searchStack); |
743 | } |
744 | |
745 | if (results.size() >= criteria->resultsLimit) |
746 | break; |
747 | |
748 | // When moving backwards, the parent object needs to be checked, because technically it's "before" the starting element. |
749 | if (!isForward && startObject != this && objectMatchesSearchCriteriaWithResultLimit(startObject, criteria, results)) |
750 | break; |
751 | |
752 | previousObject = startObject; |
753 | } |
754 | } |
755 | |
756 | // Returns the range that is fewer positions away from the reference range. |
757 | // NOTE: The after range is expected to ACTUALLY be after the reference range and the before |
758 | // range is expected to ACTUALLY be before. These are not checked for performance reasons. |
759 | static RefPtr<Range> rangeClosestToRange(RefPtr<Range> const& referenceRange, RefPtr<Range>&& afterRange, RefPtr<Range>&& beforeRange) |
760 | { |
761 | if (!referenceRange) |
762 | return nullptr; |
763 | |
764 | // The treeScope for shadow nodes may not be the same scope as another element in a document. |
765 | // Comparisons may fail in that case, which are expected behavior and should not assert. |
766 | if (afterRange && (referenceRange->endPosition().isNull() || ((afterRange->startPosition().anchorNode()->compareDocumentPosition(*referenceRange->endPosition().anchorNode()) & Node::DOCUMENT_POSITION_DISCONNECTED) == Node::DOCUMENT_POSITION_DISCONNECTED))) |
767 | return nullptr; |
768 | ASSERT(!afterRange || afterRange->compareBoundaryPoints(Range::START_TO_START, *referenceRange).releaseReturnValue() >= 0); |
769 | |
770 | if (beforeRange && (referenceRange->startPosition().isNull() || ((beforeRange->endPosition().anchorNode()->compareDocumentPosition(*referenceRange->startPosition().anchorNode()) & Node::DOCUMENT_POSITION_DISCONNECTED) == Node::DOCUMENT_POSITION_DISCONNECTED))) |
771 | return nullptr; |
772 | ASSERT(!beforeRange || beforeRange->compareBoundaryPoints(Range::START_TO_START, *referenceRange).releaseReturnValue() <= 0); |
773 | |
774 | if (!afterRange && !beforeRange) |
775 | return nullptr; |
776 | if (afterRange && !beforeRange) |
777 | return WTFMove(afterRange); |
778 | if (!afterRange && beforeRange) |
779 | return WTFMove(beforeRange); |
780 | |
781 | unsigned positionsToAfterRange = Position::positionCountBetweenPositions(afterRange->startPosition(), referenceRange->endPosition()); |
782 | unsigned positionsToBeforeRange = Position::positionCountBetweenPositions(beforeRange->endPosition(), referenceRange->startPosition()); |
783 | |
784 | return positionsToAfterRange < positionsToBeforeRange ? afterRange : beforeRange; |
785 | } |
786 | |
787 | RefPtr<Range> AccessibilityObject::rangeOfStringClosestToRangeInDirection(Range* referenceRange, AccessibilitySearchDirection searchDirection, Vector<String> const& searchStrings) const |
788 | { |
789 | Frame* frame = this->frame(); |
790 | if (!frame) |
791 | return nullptr; |
792 | |
793 | if (!referenceRange) |
794 | return nullptr; |
795 | |
796 | bool isBackwardSearch = searchDirection == AccessibilitySearchDirection::Previous; |
797 | FindOptions findOptions { AtWordStarts, AtWordEnds, CaseInsensitive, StartInSelection }; |
798 | if (isBackwardSearch) |
799 | findOptions.add(FindOptionFlag::Backwards); |
800 | |
801 | RefPtr<Range> closestStringRange = nullptr; |
802 | for (const auto& searchString : searchStrings) { |
803 | if (RefPtr<Range> searchStringRange = frame->editor().rangeOfString(searchString, referenceRange, findOptions)) { |
804 | if (!closestStringRange) |
805 | closestStringRange = searchStringRange; |
806 | else { |
807 | // If searching backward, use the trailing range edges to correctly determine which |
808 | // range is closest. Similarly, if searching forward, use the leading range edges. |
809 | Position closestStringPosition = isBackwardSearch ? closestStringRange->endPosition() : closestStringRange->startPosition(); |
810 | Position searchStringPosition = isBackwardSearch ? searchStringRange->endPosition() : searchStringRange->startPosition(); |
811 | |
812 | int closestPositionOffset = closestStringPosition.computeOffsetInContainerNode(); |
813 | int searchPositionOffset = searchStringPosition.computeOffsetInContainerNode(); |
814 | Node* closestContainerNode = closestStringPosition.containerNode(); |
815 | Node* searchContainerNode = searchStringPosition.containerNode(); |
816 | |
817 | short result = Range::compareBoundaryPoints(closestContainerNode, closestPositionOffset, searchContainerNode, searchPositionOffset).releaseReturnValue(); |
818 | if ((!isBackwardSearch && result > 0) || (isBackwardSearch && result < 0)) |
819 | closestStringRange = searchStringRange; |
820 | } |
821 | } |
822 | } |
823 | return closestStringRange; |
824 | } |
825 | |
826 | // Returns the range of the entire document if there is no selection. |
827 | RefPtr<Range> AccessibilityObject::selectionRange() const |
828 | { |
829 | Frame* frame = this->frame(); |
830 | if (!frame) |
831 | return nullptr; |
832 | |
833 | const VisibleSelection& selection = frame->selection().selection(); |
834 | if (!selection.isNone()) |
835 | return selection.firstRange(); |
836 | |
837 | return Range::create(*frame->document()); |
838 | } |
839 | |
840 | RefPtr<Range> AccessibilityObject::elementRange() const |
841 | { |
842 | return AXObjectCache::rangeForNodeContents(node()); |
843 | } |
844 | |
845 | RefPtr<Range> AccessibilityObject::(Vector<String> const& searchStrings, RefPtr<Range> const& start, AccessibilitySearchTextDirection direction) const |
846 | { |
847 | RefPtr<Range> found; |
848 | if (direction == AccessibilitySearchTextDirection::Forward) |
849 | found = rangeOfStringClosestToRangeInDirection(start.get(), AccessibilitySearchDirection::Next, searchStrings); |
850 | else if (direction == AccessibilitySearchTextDirection::Backward) |
851 | found = rangeOfStringClosestToRangeInDirection(start.get(), AccessibilitySearchDirection::Previous, searchStrings); |
852 | else if (direction == AccessibilitySearchTextDirection::Closest) { |
853 | auto foundAfter = rangeOfStringClosestToRangeInDirection(start.get(), AccessibilitySearchDirection::Next, searchStrings); |
854 | auto foundBefore = rangeOfStringClosestToRangeInDirection(start.get(), AccessibilitySearchDirection::Previous, searchStrings); |
855 | found = rangeClosestToRange(start.get(), WTFMove(foundAfter), WTFMove(foundBefore)); |
856 | } |
857 | |
858 | if (found) { |
859 | // If the search started within a text control, ensure that the result is inside that element. |
860 | if (element() && element()->isTextField()) { |
861 | if (!found->startContainer().isDescendantOrShadowDescendantOf(element()) |
862 | || !found->endContainer().isDescendantOrShadowDescendantOf(element())) |
863 | return nullptr; |
864 | } |
865 | } |
866 | return found; |
867 | } |
868 | |
869 | Vector<RefPtr<Range>> AccessibilityObject::(AccessibilitySearchTextCriteria const& criteria) const |
870 | { |
871 | Vector<RefPtr<Range>> result; |
872 | |
873 | // Determine start range. |
874 | RefPtr<Range> startRange; |
875 | if (criteria.start == AccessibilitySearchTextStartFrom::Selection) |
876 | startRange = selectionRange(); |
877 | else |
878 | startRange = elementRange(); |
879 | |
880 | if (startRange) { |
881 | // Collapse the range to the start unless searching from the end of the doc or searching backwards. |
882 | if (criteria.start == AccessibilitySearchTextStartFrom::Begin) |
883 | startRange->collapse(true); |
884 | else if (criteria.start == AccessibilitySearchTextStartFrom::End) |
885 | startRange->collapse(false); |
886 | else |
887 | startRange->collapse(criteria.direction != AccessibilitySearchTextDirection::Backward); |
888 | } else |
889 | return result; |
890 | |
891 | RefPtr<Range> found; |
892 | switch (criteria.direction) { |
893 | case AccessibilitySearchTextDirection::Forward: |
894 | case AccessibilitySearchTextDirection::Backward: |
895 | case AccessibilitySearchTextDirection::Closest: |
896 | found = findTextRange(criteria.searchStrings, startRange, criteria.direction); |
897 | if (found) |
898 | result.append(found); |
899 | break; |
900 | case AccessibilitySearchTextDirection::All: { |
901 | auto findAll = [&](AccessibilitySearchTextDirection dir) { |
902 | found = findTextRange(criteria.searchStrings, startRange, dir); |
903 | while (found) { |
904 | result.append(found); |
905 | found = findTextRange(criteria.searchStrings, found, dir); |
906 | } |
907 | }; |
908 | findAll(AccessibilitySearchTextDirection::Forward); |
909 | findAll(AccessibilitySearchTextDirection::Backward); |
910 | break; |
911 | } |
912 | } |
913 | |
914 | return result; |
915 | } |
916 | |
917 | Vector<String> AccessibilityObject::performTextOperation(AccessibilityTextOperation const& operation) |
918 | { |
919 | Vector<String> result; |
920 | |
921 | if (operation.textRanges.isEmpty()) |
922 | return result; |
923 | |
924 | Frame* frame = this->frame(); |
925 | if (!frame) |
926 | return result; |
927 | |
928 | for (auto : operation.textRanges) { |
929 | if (!frame->selection().setSelectedRange(textRange.get(), DOWNSTREAM, FrameSelection::ShouldCloseTyping::Yes)) |
930 | continue; |
931 | |
932 | String text = textRange->text(); |
933 | String replacementString = operation.replacementText; |
934 | bool replaceSelection = false; |
935 | switch (operation.type) { |
936 | case AccessibilityTextOperationType::Capitalize: |
937 | replacementString = capitalize(text, ' '); // FIXME: Needs to take locale into account to work correctly. |
938 | replaceSelection = true; |
939 | break; |
940 | case AccessibilityTextOperationType::Uppercase: |
941 | replacementString = text.convertToUppercaseWithoutLocale(); // FIXME: Needs locale to work correctly. |
942 | replaceSelection = true; |
943 | break; |
944 | case AccessibilityTextOperationType::Lowercase: |
945 | replacementString = text.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly. |
946 | replaceSelection = true; |
947 | break; |
948 | case AccessibilityTextOperationType::Replace: { |
949 | replaceSelection = true; |
950 | // When applying find and replace activities, we want to match the capitalization of the replaced text, |
951 | // (unless we're replacing with an abbreviation.) |
952 | if (text.length() > 0 |
953 | && replacementString.length() > 2 |
954 | && replacementString != replacementString.convertToUppercaseWithoutLocale()) { |
955 | if (text[0] == u_toupper(text[0])) |
956 | replacementString = capitalize(replacementString, ' '); // FIXME: Needs to take locale into account to work correctly. |
957 | else |
958 | replacementString = replacementString.convertToLowercaseWithoutLocale(); // FIXME: Needs locale to work correctly. |
959 | } |
960 | break; |
961 | } |
962 | case AccessibilityTextOperationType::Select: |
963 | break; |
964 | } |
965 | |
966 | // A bit obvious, but worth noting the API contract for this method is that we should |
967 | // return the replacement string when replacing, but the selected string if not. |
968 | if (replaceSelection) { |
969 | frame->editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::Yes, Editor::SmartReplace::Yes); |
970 | result.append(replacementString); |
971 | } else |
972 | result.append(text); |
973 | } |
974 | |
975 | return result; |
976 | } |
977 | |
978 | bool AccessibilityObject::hasAttributesRequiredForInclusion() const |
979 | { |
980 | // These checks are simplified in the interest of execution speed. |
981 | if (!getAttribute(aria_helpAttr).isEmpty() |
982 | || !getAttribute(aria_describedbyAttr).isEmpty() |
983 | || !getAttribute(altAttr).isEmpty() |
984 | || !getAttribute(titleAttr).isEmpty()) |
985 | return true; |
986 | |
987 | #if ENABLE(MATHML) |
988 | if (!getAttribute(MathMLNames::alttextAttr).isEmpty()) |
989 | return true; |
990 | #endif |
991 | |
992 | return false; |
993 | } |
994 | |
995 | bool AccessibilityObject::isARIAInput(AccessibilityRole ariaRole) |
996 | { |
997 | return ariaRole == AccessibilityRole::RadioButton || ariaRole == AccessibilityRole::CheckBox || ariaRole == AccessibilityRole::TextField || ariaRole == AccessibilityRole::Switch || ariaRole == AccessibilityRole::SearchField; |
998 | } |
999 | |
1000 | bool AccessibilityObject::isARIAControl(AccessibilityRole ariaRole) |
1001 | { |
1002 | return isARIAInput(ariaRole) || ariaRole == AccessibilityRole::TextArea || ariaRole == AccessibilityRole::Button || ariaRole == AccessibilityRole::ComboBox || ariaRole == AccessibilityRole::Slider || ariaRole == AccessibilityRole::ListBox; |
1003 | } |
1004 | |
1005 | bool AccessibilityObject::isRangeControl() const |
1006 | { |
1007 | switch (roleValue()) { |
1008 | case AccessibilityRole::Meter: |
1009 | case AccessibilityRole::ProgressIndicator: |
1010 | case AccessibilityRole::Slider: |
1011 | case AccessibilityRole::ScrollBar: |
1012 | case AccessibilityRole::SpinButton: |
1013 | return true; |
1014 | case AccessibilityRole::Splitter: |
1015 | return canSetFocusAttribute(); |
1016 | default: |
1017 | return false; |
1018 | } |
1019 | } |
1020 | |
1021 | bool AccessibilityObject::isMeter() const |
1022 | { |
1023 | if (ariaRoleAttribute() == AccessibilityRole::Meter) |
1024 | return true; |
1025 | |
1026 | #if ENABLE(METER_ELEMENT) |
1027 | RenderObject* renderer = this->renderer(); |
1028 | return renderer && renderer->isMeter(); |
1029 | #else |
1030 | return false; |
1031 | #endif |
1032 | } |
1033 | |
1034 | IntPoint AccessibilityObject::clickPoint() |
1035 | { |
1036 | LayoutRect rect = elementRect(); |
1037 | return roundedIntPoint(LayoutPoint(rect.x() + rect.width() / 2, rect.y() + rect.height() / 2)); |
1038 | } |
1039 | |
1040 | IntRect AccessibilityObject::boundingBoxForQuads(RenderObject* obj, const Vector<FloatQuad>& quads) |
1041 | { |
1042 | ASSERT(obj); |
1043 | if (!obj) |
1044 | return IntRect(); |
1045 | |
1046 | FloatRect result; |
1047 | for (const auto& quad : quads) { |
1048 | FloatRect r = quad.enclosingBoundingBox(); |
1049 | if (!r.isEmpty()) { |
1050 | if (obj->style().hasAppearance()) |
1051 | obj->theme().adjustRepaintRect(*obj, r); |
1052 | result.unite(r); |
1053 | } |
1054 | } |
1055 | return snappedIntRect(LayoutRect(result)); |
1056 | } |
1057 | |
1058 | bool AccessibilityObject::press() |
1059 | { |
1060 | // The presence of the actionElement will confirm whether we should even attempt a press. |
1061 | Element* actionElem = actionElement(); |
1062 | if (!actionElem) |
1063 | return false; |
1064 | if (Frame* f = actionElem->document().frame()) |
1065 | f->loader().resetMultipleFormSubmissionProtection(); |
1066 | |
1067 | // Hit test at this location to determine if there is a sub-node element that should act |
1068 | // as the target of the action. |
1069 | Element* hitTestElement = nullptr; |
1070 | Document* document = this->document(); |
1071 | if (document) { |
1072 | HitTestRequest request(HitTestRequest::ReadOnly | HitTestRequest::Active | HitTestRequest::AccessibilityHitTest); |
1073 | HitTestResult hitTestResult(clickPoint()); |
1074 | document->renderView()->hitTest(request, hitTestResult); |
1075 | if (auto* innerNode = hitTestResult.innerNode()) { |
1076 | if (auto* shadowHost = innerNode->shadowHost()) |
1077 | hitTestElement = shadowHost; |
1078 | else if (is<Element>(*innerNode)) |
1079 | hitTestElement = &downcast<Element>(*innerNode); |
1080 | else |
1081 | hitTestElement = innerNode->parentElement(); |
1082 | } |
1083 | } |
1084 | |
1085 | // Prefer the actionElement instead of this node, if the actionElement is inside this node. |
1086 | Element* pressElement = this->element(); |
1087 | if (!pressElement || actionElem->isDescendantOf(*pressElement)) |
1088 | pressElement = actionElem; |
1089 | |
1090 | ASSERT(pressElement); |
1091 | // Prefer the hit test element, if it is inside the target element. |
1092 | if (hitTestElement && hitTestElement->isDescendantOf(*pressElement)) |
1093 | pressElement = hitTestElement; |
1094 | |
1095 | UserGestureIndicator gestureIndicator(ProcessingUserGesture, document); |
1096 | |
1097 | bool dispatchedTouchEvent = false; |
1098 | #if PLATFORM(IOS_FAMILY) |
1099 | if (hasTouchEventListener()) |
1100 | dispatchedTouchEvent = dispatchTouchEvent(); |
1101 | #endif |
1102 | if (!dispatchedTouchEvent) |
1103 | pressElement->accessKeyAction(true); |
1104 | |
1105 | return true; |
1106 | } |
1107 | |
1108 | bool AccessibilityObject::dispatchTouchEvent() |
1109 | { |
1110 | #if ENABLE(IOS_TOUCH_EVENTS) |
1111 | if (auto* frame = mainFrame()) |
1112 | return frame->eventHandler().dispatchSimulatedTouchEvent(clickPoint()); |
1113 | #endif |
1114 | return false; |
1115 | } |
1116 | |
1117 | Frame* AccessibilityObject::frame() const |
1118 | { |
1119 | Node* node = this->node(); |
1120 | return node ? node->document().frame() : nullptr; |
1121 | } |
1122 | |
1123 | Frame* AccessibilityObject::mainFrame() const |
1124 | { |
1125 | Document* document = topDocument(); |
1126 | if (!document) |
1127 | return nullptr; |
1128 | |
1129 | Frame* frame = document->frame(); |
1130 | if (!frame) |
1131 | return nullptr; |
1132 | |
1133 | return &frame->mainFrame(); |
1134 | } |
1135 | |
1136 | Document* AccessibilityObject::topDocument() const |
1137 | { |
1138 | if (!document()) |
1139 | return nullptr; |
1140 | return &document()->topDocument(); |
1141 | } |
1142 | |
1143 | String AccessibilityObject::language() const |
1144 | { |
1145 | const AtomicString& lang = getAttribute(langAttr); |
1146 | if (!lang.isEmpty()) |
1147 | return lang; |
1148 | |
1149 | AccessibilityObject* parent = parentObject(); |
1150 | |
1151 | // as a last resort, fall back to the content language specified in the meta tag |
1152 | if (!parent) { |
1153 | Document* doc = document(); |
1154 | if (doc) |
1155 | return doc->contentLanguage(); |
1156 | return nullAtom(); |
1157 | } |
1158 | |
1159 | return parent->language(); |
1160 | } |
1161 | |
1162 | VisiblePositionRange AccessibilityObject::visiblePositionRangeForUnorderedPositions(const VisiblePosition& visiblePos1, const VisiblePosition& visiblePos2) const |
1163 | { |
1164 | if (visiblePos1.isNull() || visiblePos2.isNull()) |
1165 | return VisiblePositionRange(); |
1166 | |
1167 | // If there's no common tree scope between positions, return early. |
1168 | if (!commonTreeScope(visiblePos1.deepEquivalent().deprecatedNode(), visiblePos2.deepEquivalent().deprecatedNode())) |
1169 | return VisiblePositionRange(); |
1170 | |
1171 | VisiblePosition startPos; |
1172 | VisiblePosition endPos; |
1173 | bool alreadyInOrder; |
1174 | |
1175 | // upstream is ordered before downstream for the same position |
1176 | if (visiblePos1 == visiblePos2 && visiblePos2.affinity() == UPSTREAM) |
1177 | alreadyInOrder = false; |
1178 | |
1179 | // use selection order to see if the positions are in order |
1180 | else |
1181 | alreadyInOrder = VisibleSelection(visiblePos1, visiblePos2).isBaseFirst(); |
1182 | |
1183 | if (alreadyInOrder) { |
1184 | startPos = visiblePos1; |
1185 | endPos = visiblePos2; |
1186 | } else { |
1187 | startPos = visiblePos2; |
1188 | endPos = visiblePos1; |
1189 | } |
1190 | |
1191 | return VisiblePositionRange(startPos, endPos); |
1192 | } |
1193 | |
1194 | VisiblePositionRange AccessibilityObject::positionOfLeftWord(const VisiblePosition& visiblePos) const |
1195 | { |
1196 | VisiblePosition startPosition = startOfWord(visiblePos, LeftWordIfOnBoundary); |
1197 | VisiblePosition endPosition = endOfWord(startPosition); |
1198 | return VisiblePositionRange(startPosition, endPosition); |
1199 | } |
1200 | |
1201 | VisiblePositionRange AccessibilityObject::positionOfRightWord(const VisiblePosition& visiblePos) const |
1202 | { |
1203 | VisiblePosition startPosition = startOfWord(visiblePos, RightWordIfOnBoundary); |
1204 | VisiblePosition endPosition = endOfWord(startPosition); |
1205 | return VisiblePositionRange(startPosition, endPosition); |
1206 | } |
1207 | |
1208 | static VisiblePosition updateAXLineStartForVisiblePosition(const VisiblePosition& visiblePosition) |
1209 | { |
1210 | // A line in the accessibility sense should include floating objects, such as aligned image, as part of a line. |
1211 | // So let's update the position to include that. |
1212 | VisiblePosition tempPosition; |
1213 | VisiblePosition startPosition = visiblePosition; |
1214 | while (true) { |
1215 | tempPosition = startPosition.previous(); |
1216 | if (tempPosition.isNull()) |
1217 | break; |
1218 | Position p = tempPosition.deepEquivalent(); |
1219 | RenderObject* renderer = p.deprecatedNode()->renderer(); |
1220 | if (!renderer || (renderer->isRenderBlock() && !p.deprecatedEditingOffset())) |
1221 | break; |
1222 | if (!RenderedPosition(tempPosition).isNull()) |
1223 | break; |
1224 | startPosition = tempPosition; |
1225 | } |
1226 | |
1227 | return startPosition; |
1228 | } |
1229 | |
1230 | VisiblePositionRange AccessibilityObject::leftLineVisiblePositionRange(const VisiblePosition& visiblePos) const |
1231 | { |
1232 | if (visiblePos.isNull()) |
1233 | return VisiblePositionRange(); |
1234 | |
1235 | // make a caret selection for the position before marker position (to make sure |
1236 | // we move off of a line start) |
1237 | VisiblePosition prevVisiblePos = visiblePos.previous(); |
1238 | if (prevVisiblePos.isNull()) |
1239 | return VisiblePositionRange(); |
1240 | |
1241 | VisiblePosition startPosition = startOfLine(prevVisiblePos); |
1242 | |
1243 | // keep searching for a valid line start position. Unless the VisiblePosition is at the very beginning, there should |
1244 | // always be a valid line range. However, startOfLine will return null for position next to a floating object, |
1245 | // since floating object doesn't really belong to any line. |
1246 | // This check will reposition the marker before the floating object, to ensure we get a line start. |
1247 | if (startPosition.isNull()) { |
1248 | while (startPosition.isNull() && prevVisiblePos.isNotNull()) { |
1249 | prevVisiblePos = prevVisiblePos.previous(); |
1250 | startPosition = startOfLine(prevVisiblePos); |
1251 | } |
1252 | } else |
1253 | startPosition = updateAXLineStartForVisiblePosition(startPosition); |
1254 | |
1255 | VisiblePosition endPosition = endOfLine(prevVisiblePos); |
1256 | return VisiblePositionRange(startPosition, endPosition); |
1257 | } |
1258 | |
1259 | VisiblePositionRange AccessibilityObject::rightLineVisiblePositionRange(const VisiblePosition& visiblePos) const |
1260 | { |
1261 | if (visiblePos.isNull()) |
1262 | return VisiblePositionRange(); |
1263 | |
1264 | // make sure we move off of a line end |
1265 | VisiblePosition nextVisiblePos = visiblePos.next(); |
1266 | if (nextVisiblePos.isNull()) |
1267 | return VisiblePositionRange(); |
1268 | |
1269 | VisiblePosition startPosition = startOfLine(nextVisiblePos); |
1270 | |
1271 | // fetch for a valid line start position |
1272 | if (startPosition.isNull()) { |
1273 | startPosition = visiblePos; |
1274 | nextVisiblePos = nextVisiblePos.next(); |
1275 | } else |
1276 | startPosition = updateAXLineStartForVisiblePosition(startPosition); |
1277 | |
1278 | VisiblePosition endPosition = endOfLine(nextVisiblePos); |
1279 | |
1280 | // as long as the position hasn't reached the end of the doc, keep searching for a valid line end position |
1281 | // Unless the VisiblePosition is at the very end, there should always be a valid line range. However, endOfLine will |
1282 | // return null for position by a floating object, since floating object doesn't really belong to any line. |
1283 | // This check will reposition the marker after the floating object, to ensure we get a line end. |
1284 | while (endPosition.isNull() && nextVisiblePos.isNotNull()) { |
1285 | nextVisiblePos = nextVisiblePos.next(); |
1286 | endPosition = endOfLine(nextVisiblePos); |
1287 | } |
1288 | |
1289 | return VisiblePositionRange(startPosition, endPosition); |
1290 | } |
1291 | |
1292 | VisiblePositionRange AccessibilityObject::sentenceForPosition(const VisiblePosition& visiblePos) const |
1293 | { |
1294 | // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) |
1295 | // Related? <rdar://problem/3927736> Text selection broken in 8A336 |
1296 | VisiblePosition startPosition = startOfSentence(visiblePos); |
1297 | VisiblePosition endPosition = endOfSentence(startPosition); |
1298 | return VisiblePositionRange(startPosition, endPosition); |
1299 | } |
1300 | |
1301 | VisiblePositionRange AccessibilityObject::paragraphForPosition(const VisiblePosition& visiblePos) const |
1302 | { |
1303 | VisiblePosition startPosition = startOfParagraph(visiblePos); |
1304 | VisiblePosition endPosition = endOfParagraph(startPosition); |
1305 | return VisiblePositionRange(startPosition, endPosition); |
1306 | } |
1307 | |
1308 | static VisiblePosition startOfStyleRange(const VisiblePosition& visiblePos) |
1309 | { |
1310 | RenderObject* renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer(); |
1311 | RenderObject* startRenderer = renderer; |
1312 | auto* style = &renderer->style(); |
1313 | |
1314 | // traverse backward by renderer to look for style change |
1315 | for (RenderObject* r = renderer->previousInPreOrder(); r; r = r->previousInPreOrder()) { |
1316 | // skip non-leaf nodes |
1317 | if (r->firstChildSlow()) |
1318 | continue; |
1319 | |
1320 | // stop at style change |
1321 | if (&r->style() != style) |
1322 | break; |
1323 | |
1324 | // remember match |
1325 | startRenderer = r; |
1326 | } |
1327 | |
1328 | return firstPositionInOrBeforeNode(startRenderer->node()); |
1329 | } |
1330 | |
1331 | static VisiblePosition endOfStyleRange(const VisiblePosition& visiblePos) |
1332 | { |
1333 | RenderObject* renderer = visiblePos.deepEquivalent().deprecatedNode()->renderer(); |
1334 | RenderObject* endRenderer = renderer; |
1335 | const RenderStyle& style = renderer->style(); |
1336 | |
1337 | // traverse forward by renderer to look for style change |
1338 | for (RenderObject* r = renderer->nextInPreOrder(); r; r = r->nextInPreOrder()) { |
1339 | // skip non-leaf nodes |
1340 | if (r->firstChildSlow()) |
1341 | continue; |
1342 | |
1343 | // stop at style change |
1344 | if (&r->style() != &style) |
1345 | break; |
1346 | |
1347 | // remember match |
1348 | endRenderer = r; |
1349 | } |
1350 | |
1351 | return lastPositionInOrAfterNode(endRenderer->node()); |
1352 | } |
1353 | |
1354 | VisiblePositionRange AccessibilityObject::styleRangeForPosition(const VisiblePosition& visiblePos) const |
1355 | { |
1356 | if (visiblePos.isNull()) |
1357 | return VisiblePositionRange(); |
1358 | |
1359 | return VisiblePositionRange(startOfStyleRange(visiblePos), endOfStyleRange(visiblePos)); |
1360 | } |
1361 | |
1362 | // NOTE: Consider providing this utility method as AX API |
1363 | VisiblePositionRange AccessibilityObject::(const PlainTextRange& range) const |
1364 | { |
1365 | unsigned textLength = getLengthForTextRange(); |
1366 | if (range.start + range.length > textLength) |
1367 | return VisiblePositionRange(); |
1368 | |
1369 | VisiblePosition startPosition = visiblePositionForIndex(range.start); |
1370 | startPosition.setAffinity(DOWNSTREAM); |
1371 | VisiblePosition endPosition = visiblePositionForIndex(range.start + range.length); |
1372 | return VisiblePositionRange(startPosition, endPosition); |
1373 | } |
1374 | |
1375 | RefPtr<Range> AccessibilityObject::(const PlainTextRange& range) const |
1376 | { |
1377 | unsigned textLength = getLengthForTextRange(); |
1378 | if (range.start + range.length > textLength) |
1379 | return nullptr; |
1380 | |
1381 | if (AXObjectCache* cache = axObjectCache()) { |
1382 | CharacterOffset start = cache->characterOffsetForIndex(range.start, this); |
1383 | CharacterOffset end = cache->characterOffsetForIndex(range.start + range.length, this); |
1384 | return cache->rangeForUnorderedCharacterOffsets(start, end); |
1385 | } |
1386 | return nullptr; |
1387 | } |
1388 | |
1389 | VisiblePositionRange AccessibilityObject::lineRangeForPosition(const VisiblePosition& visiblePosition) const |
1390 | { |
1391 | VisiblePosition startPosition = startOfLine(visiblePosition); |
1392 | VisiblePosition endPosition = endOfLine(visiblePosition); |
1393 | return VisiblePositionRange(startPosition, endPosition); |
1394 | } |
1395 | |
1396 | bool AccessibilityObject::replacedNodeNeedsCharacter(Node* replacedNode) |
1397 | { |
1398 | // we should always be given a rendered node and a replaced node, but be safe |
1399 | // replaced nodes are either attachments (widgets) or images |
1400 | if (!replacedNode || !isRendererReplacedElement(replacedNode->renderer()) || replacedNode->isTextNode()) |
1401 | return false; |
1402 | |
1403 | // create an AX object, but skip it if it is not supposed to be seen |
1404 | AccessibilityObject* object = replacedNode->renderer()->document().axObjectCache()->getOrCreate(replacedNode); |
1405 | if (object->accessibilityIsIgnored()) |
1406 | return false; |
1407 | |
1408 | return true; |
1409 | } |
1410 | |
1411 | // Finds a RenderListItem parent give a node. |
1412 | static RenderListItem* renderListItemContainerForNode(Node* node) |
1413 | { |
1414 | for (; node; node = node->parentNode()) { |
1415 | RenderBoxModelObject* renderer = node->renderBoxModelObject(); |
1416 | if (is<RenderListItem>(renderer)) |
1417 | return downcast<RenderListItem>(renderer); |
1418 | } |
1419 | return nullptr; |
1420 | } |
1421 | |
1422 | static String listMarkerTextForNode(Node* node) |
1423 | { |
1424 | RenderListItem* listItem = renderListItemContainerForNode(node); |
1425 | if (!listItem) |
1426 | return String(); |
1427 | |
1428 | // If this is in a list item, we need to manually add the text for the list marker |
1429 | // because a RenderListMarker does not have a Node equivalent and thus does not appear |
1430 | // when iterating text. |
1431 | return listItem->markerTextWithSuffix(); |
1432 | } |
1433 | |
1434 | // Returns the text associated with a list marker if this node is contained within a list item. |
1435 | String AccessibilityObject::listMarkerTextForNodeAndPosition(Node* node, const VisiblePosition& visiblePositionStart) |
1436 | { |
1437 | // If the range does not contain the start of the line, the list marker text should not be included. |
1438 | if (!isStartOfLine(visiblePositionStart)) |
1439 | return String(); |
1440 | |
1441 | // We should speak the list marker only for the first line. |
1442 | RenderListItem* listItem = renderListItemContainerForNode(node); |
1443 | if (!listItem) |
1444 | return String(); |
1445 | if (!inSameLine(visiblePositionStart, firstPositionInNode(&listItem->element()))) |
1446 | return String(); |
1447 | |
1448 | return listMarkerTextForNode(node); |
1449 | } |
1450 | |
1451 | String AccessibilityObject::stringForRange(RefPtr<Range> range) const |
1452 | { |
1453 | if (!range) |
1454 | return String(); |
1455 | |
1456 | TextIterator it(range.get()); |
1457 | if (it.atEnd()) |
1458 | return String(); |
1459 | |
1460 | StringBuilder builder; |
1461 | for (; !it.atEnd(); it.advance()) { |
1462 | // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX) |
1463 | if (it.text().length()) { |
1464 | // Add a textual representation for list marker text. |
1465 | // Don't add list marker text for new line character. |
1466 | if (it.text().length() != 1 || !isSpaceOrNewline(it.text()[0])) |
1467 | builder.append(listMarkerTextForNodeAndPosition(it.node(), VisiblePosition(range->startPosition()))); |
1468 | it.appendTextToStringBuilder(builder); |
1469 | } else { |
1470 | // locate the node and starting offset for this replaced range |
1471 | Node& node = it.range()->startContainer(); |
1472 | ASSERT(&node == &it.range()->endContainer()); |
1473 | int offset = it.range()->startOffset(); |
1474 | if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset))) |
1475 | builder.append(objectReplacementCharacter); |
1476 | } |
1477 | } |
1478 | |
1479 | return builder.toString(); |
1480 | } |
1481 | |
1482 | String AccessibilityObject::stringForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) |
1483 | { |
1484 | if (visiblePositionRange.isNull()) |
1485 | return String(); |
1486 | |
1487 | StringBuilder builder; |
1488 | RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end); |
1489 | for (TextIterator it(range.get()); !it.atEnd(); it.advance()) { |
1490 | // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX) |
1491 | if (it.text().length()) { |
1492 | // Add a textual representation for list marker text. |
1493 | builder.append(listMarkerTextForNodeAndPosition(it.node(), visiblePositionRange.start)); |
1494 | it.appendTextToStringBuilder(builder); |
1495 | } else { |
1496 | // locate the node and starting offset for this replaced range |
1497 | Node& node = it.range()->startContainer(); |
1498 | ASSERT(&node == &it.range()->endContainer()); |
1499 | int offset = it.range()->startOffset(); |
1500 | if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset))) |
1501 | builder.append(objectReplacementCharacter); |
1502 | } |
1503 | } |
1504 | |
1505 | return builder.toString(); |
1506 | } |
1507 | |
1508 | int AccessibilityObject::lengthForVisiblePositionRange(const VisiblePositionRange& visiblePositionRange) const |
1509 | { |
1510 | // FIXME: Multi-byte support |
1511 | if (visiblePositionRange.isNull()) |
1512 | return -1; |
1513 | |
1514 | int length = 0; |
1515 | RefPtr<Range> range = makeRange(visiblePositionRange.start, visiblePositionRange.end); |
1516 | for (TextIterator it(range.get()); !it.atEnd(); it.advance()) { |
1517 | // non-zero length means textual node, zero length means replaced node (AKA "attachments" in AX) |
1518 | if (it.text().length()) |
1519 | length += it.text().length(); |
1520 | else { |
1521 | // locate the node and starting offset for this replaced range |
1522 | Node& node = it.range()->startContainer(); |
1523 | ASSERT(&node == &it.range()->endContainer()); |
1524 | int offset = it.range()->startOffset(); |
1525 | |
1526 | if (replacedNodeNeedsCharacter(node.traverseToChildAt(offset))) |
1527 | ++length; |
1528 | } |
1529 | } |
1530 | |
1531 | return length; |
1532 | } |
1533 | |
1534 | VisiblePosition AccessibilityObject::visiblePositionForBounds(const IntRect& rect, AccessibilityVisiblePositionForBounds visiblePositionForBounds) const |
1535 | { |
1536 | if (rect.isEmpty()) |
1537 | return VisiblePosition(); |
1538 | |
1539 | auto* mainFrame = this->mainFrame(); |
1540 | if (!mainFrame) |
1541 | return VisiblePosition(); |
1542 | |
1543 | // FIXME: Add support for right-to-left languages. |
1544 | IntPoint corner = (visiblePositionForBounds == AccessibilityVisiblePositionForBounds::First) ? rect.minXMinYCorner() : rect.maxXMaxYCorner(); |
1545 | VisiblePosition position = mainFrame->visiblePositionForPoint(corner); |
1546 | |
1547 | if (rect.contains(position.absoluteCaretBounds().center())) |
1548 | return position; |
1549 | |
1550 | // If the initial position is located outside the bounds adjust it incrementally as needed. |
1551 | VisiblePosition nextPosition = position.next(); |
1552 | VisiblePosition previousPosition = position.previous(); |
1553 | while (nextPosition.isNotNull() || previousPosition.isNotNull()) { |
1554 | if (rect.contains(nextPosition.absoluteCaretBounds().center())) |
1555 | return nextPosition; |
1556 | if (rect.contains(previousPosition.absoluteCaretBounds().center())) |
1557 | return previousPosition; |
1558 | |
1559 | nextPosition = nextPosition.next(); |
1560 | previousPosition = previousPosition.previous(); |
1561 | } |
1562 | |
1563 | return VisiblePosition(); |
1564 | } |
1565 | |
1566 | VisiblePosition AccessibilityObject::nextWordEnd(const VisiblePosition& visiblePos) const |
1567 | { |
1568 | if (visiblePos.isNull()) |
1569 | return VisiblePosition(); |
1570 | |
1571 | // make sure we move off of a word end |
1572 | VisiblePosition nextVisiblePos = visiblePos.next(); |
1573 | if (nextVisiblePos.isNull()) |
1574 | return VisiblePosition(); |
1575 | |
1576 | return endOfWord(nextVisiblePos, LeftWordIfOnBoundary); |
1577 | } |
1578 | |
1579 | VisiblePosition AccessibilityObject::previousWordStart(const VisiblePosition& visiblePos) const |
1580 | { |
1581 | if (visiblePos.isNull()) |
1582 | return VisiblePosition(); |
1583 | |
1584 | // make sure we move off of a word start |
1585 | VisiblePosition prevVisiblePos = visiblePos.previous(); |
1586 | if (prevVisiblePos.isNull()) |
1587 | return VisiblePosition(); |
1588 | |
1589 | return startOfWord(prevVisiblePos, RightWordIfOnBoundary); |
1590 | } |
1591 | |
1592 | VisiblePosition AccessibilityObject::nextLineEndPosition(const VisiblePosition& visiblePos) const |
1593 | { |
1594 | if (visiblePos.isNull()) |
1595 | return VisiblePosition(); |
1596 | |
1597 | // to make sure we move off of a line end |
1598 | VisiblePosition nextVisiblePos = visiblePos.next(); |
1599 | if (nextVisiblePos.isNull()) |
1600 | return VisiblePosition(); |
1601 | |
1602 | VisiblePosition endPosition = endOfLine(nextVisiblePos); |
1603 | |
1604 | // as long as the position hasn't reached the end of the doc, keep searching for a valid line end position |
1605 | // There are cases like when the position is next to a floating object that'll return null for end of line. This code will avoid returning null. |
1606 | while (endPosition.isNull() && nextVisiblePos.isNotNull()) { |
1607 | nextVisiblePos = nextVisiblePos.next(); |
1608 | endPosition = endOfLine(nextVisiblePos); |
1609 | } |
1610 | |
1611 | return endPosition; |
1612 | } |
1613 | |
1614 | VisiblePosition AccessibilityObject::previousLineStartPosition(const VisiblePosition& visiblePos) const |
1615 | { |
1616 | if (visiblePos.isNull()) |
1617 | return VisiblePosition(); |
1618 | |
1619 | // make sure we move off of a line start |
1620 | VisiblePosition prevVisiblePos = visiblePos.previous(); |
1621 | if (prevVisiblePos.isNull()) |
1622 | return VisiblePosition(); |
1623 | |
1624 | VisiblePosition startPosition = startOfLine(prevVisiblePos); |
1625 | |
1626 | // as long as the position hasn't reached the beginning of the doc, keep searching for a valid line start position |
1627 | // There are cases like when the position is next to a floating object that'll return null for start of line. This code will avoid returning null. |
1628 | if (startPosition.isNull()) { |
1629 | while (startPosition.isNull() && prevVisiblePos.isNotNull()) { |
1630 | prevVisiblePos = prevVisiblePos.previous(); |
1631 | startPosition = startOfLine(prevVisiblePos); |
1632 | } |
1633 | } else |
1634 | startPosition = updateAXLineStartForVisiblePosition(startPosition); |
1635 | |
1636 | return startPosition; |
1637 | } |
1638 | |
1639 | VisiblePosition AccessibilityObject::nextSentenceEndPosition(const VisiblePosition& visiblePos) const |
1640 | { |
1641 | // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) |
1642 | // Related? <rdar://problem/3927736> Text selection broken in 8A336 |
1643 | if (visiblePos.isNull()) |
1644 | return VisiblePosition(); |
1645 | |
1646 | // make sure we move off of a sentence end |
1647 | VisiblePosition nextVisiblePos = visiblePos.next(); |
1648 | if (nextVisiblePos.isNull()) |
1649 | return VisiblePosition(); |
1650 | |
1651 | // an empty line is considered a sentence. If it's skipped, then the sentence parser will not |
1652 | // see this empty line. Instead, return the end position of the empty line. |
1653 | VisiblePosition endPosition; |
1654 | |
1655 | String lineString = plainText(makeRange(startOfLine(nextVisiblePos), endOfLine(nextVisiblePos)).get()); |
1656 | if (lineString.isEmpty()) |
1657 | endPosition = nextVisiblePos; |
1658 | else |
1659 | endPosition = endOfSentence(nextVisiblePos); |
1660 | |
1661 | return endPosition; |
1662 | } |
1663 | |
1664 | VisiblePosition AccessibilityObject::previousSentenceStartPosition(const VisiblePosition& visiblePos) const |
1665 | { |
1666 | // FIXME: FO 2 IMPLEMENT (currently returns incorrect answer) |
1667 | // Related? <rdar://problem/3927736> Text selection broken in 8A336 |
1668 | if (visiblePos.isNull()) |
1669 | return VisiblePosition(); |
1670 | |
1671 | // make sure we move off of a sentence start |
1672 | VisiblePosition previousVisiblePos = visiblePos.previous(); |
1673 | if (previousVisiblePos.isNull()) |
1674 | return VisiblePosition(); |
1675 | |
1676 | // treat empty line as a separate sentence. |
1677 | VisiblePosition startPosition; |
1678 | |
1679 | String lineString = plainText(makeRange(startOfLine(previousVisiblePos), endOfLine(previousVisiblePos)).get()); |
1680 | if (lineString.isEmpty()) |
1681 | startPosition = previousVisiblePos; |
1682 | else |
1683 | startPosition = startOfSentence(previousVisiblePos); |
1684 | |
1685 | return startPosition; |
1686 | } |
1687 | |
1688 | VisiblePosition AccessibilityObject::nextParagraphEndPosition(const VisiblePosition& visiblePos) const |
1689 | { |
1690 | if (visiblePos.isNull()) |
1691 | return VisiblePosition(); |
1692 | |
1693 | // make sure we move off of a paragraph end |
1694 | VisiblePosition nextPos = visiblePos.next(); |
1695 | if (nextPos.isNull()) |
1696 | return VisiblePosition(); |
1697 | |
1698 | return endOfParagraph(nextPos); |
1699 | } |
1700 | |
1701 | VisiblePosition AccessibilityObject::previousParagraphStartPosition(const VisiblePosition& visiblePos) const |
1702 | { |
1703 | if (visiblePos.isNull()) |
1704 | return VisiblePosition(); |
1705 | |
1706 | // make sure we move off of a paragraph start |
1707 | VisiblePosition previousPos = visiblePos.previous(); |
1708 | if (previousPos.isNull()) |
1709 | return VisiblePosition(); |
1710 | |
1711 | return startOfParagraph(previousPos); |
1712 | } |
1713 | |
1714 | AccessibilityObject* AccessibilityObject::accessibilityObjectForPosition(const VisiblePosition& visiblePos) const |
1715 | { |
1716 | if (visiblePos.isNull()) |
1717 | return nullptr; |
1718 | |
1719 | RenderObject* obj = visiblePos.deepEquivalent().deprecatedNode()->renderer(); |
1720 | if (!obj) |
1721 | return nullptr; |
1722 | |
1723 | return obj->document().axObjectCache()->getOrCreate(obj); |
1724 | } |
1725 | |
1726 | // If you call node->hasEditableStyle() since that will return true if an ancestor is editable. |
1727 | // This only returns true if this is the element that actually has the contentEditable attribute set. |
1728 | bool AccessibilityObject::hasContentEditableAttributeSet() const |
1729 | { |
1730 | return contentEditableAttributeIsEnabled(element()); |
1731 | } |
1732 | |
1733 | bool AccessibilityObject::supportsReadOnly() const |
1734 | { |
1735 | AccessibilityRole role = roleValue(); |
1736 | |
1737 | return role == AccessibilityRole::CheckBox |
1738 | || role == AccessibilityRole::ColumnHeader |
1739 | || role == AccessibilityRole::ComboBox |
1740 | || role == AccessibilityRole::Grid |
1741 | || role == AccessibilityRole::GridCell |
1742 | || role == AccessibilityRole::ListBox |
1743 | || role == AccessibilityRole::MenuItemCheckbox |
1744 | || role == AccessibilityRole::MenuItemRadio |
1745 | || role == AccessibilityRole::RadioGroup |
1746 | || role == AccessibilityRole::RowHeader |
1747 | || role == AccessibilityRole::SearchField |
1748 | || role == AccessibilityRole::Slider |
1749 | || role == AccessibilityRole::SpinButton |
1750 | || role == AccessibilityRole::Switch |
1751 | || role == AccessibilityRole::TextField |
1752 | || role == AccessibilityRole::TreeGrid |
1753 | || isPasswordField(); |
1754 | } |
1755 | |
1756 | String AccessibilityObject::readOnlyValue() const |
1757 | { |
1758 | if (!hasAttribute(aria_readonlyAttr)) |
1759 | return ariaRoleAttribute() != AccessibilityRole::Unknown && supportsReadOnly() ? "false" : String(); |
1760 | |
1761 | return getAttribute(aria_readonlyAttr).string().convertToASCIILowercase(); |
1762 | } |
1763 | |
1764 | bool AccessibilityObject::supportsAutoComplete() const |
1765 | { |
1766 | return (isComboBox() || isARIATextControl()) && hasAttribute(aria_autocompleteAttr); |
1767 | } |
1768 | |
1769 | String AccessibilityObject::autoCompleteValue() const |
1770 | { |
1771 | const AtomicString& autoComplete = getAttribute(aria_autocompleteAttr); |
1772 | if (equalLettersIgnoringASCIICase(autoComplete, "inline" ) |
1773 | || equalLettersIgnoringASCIICase(autoComplete, "list" ) |
1774 | || equalLettersIgnoringASCIICase(autoComplete, "both" )) |
1775 | return autoComplete; |
1776 | |
1777 | return "none" ; |
1778 | } |
1779 | |
1780 | bool AccessibilityObject::contentEditableAttributeIsEnabled(Element* element) |
1781 | { |
1782 | if (!element) |
1783 | return false; |
1784 | |
1785 | const AtomicString& contentEditableValue = element->attributeWithoutSynchronization(contenteditableAttr); |
1786 | if (contentEditableValue.isNull()) |
1787 | return false; |
1788 | |
1789 | // Both "true" (case-insensitive) and the empty string count as true. |
1790 | return contentEditableValue.isEmpty() || equalLettersIgnoringASCIICase(contentEditableValue, "true" ); |
1791 | } |
1792 | |
1793 | #if HAVE(ACCESSIBILITY) |
1794 | int AccessibilityObject::lineForPosition(const VisiblePosition& visiblePos) const |
1795 | { |
1796 | if (visiblePos.isNull() || !node()) |
1797 | return -1; |
1798 | |
1799 | // If the position is not in the same editable region as this AX object, return -1. |
1800 | Node* containerNode = visiblePos.deepEquivalent().containerNode(); |
1801 | if (!containerNode->containsIncludingShadowDOM(node()) && !node()->containsIncludingShadowDOM(containerNode)) |
1802 | return -1; |
1803 | |
1804 | int lineCount = -1; |
1805 | VisiblePosition currentVisiblePos = visiblePos; |
1806 | VisiblePosition savedVisiblePos; |
1807 | |
1808 | // move up until we get to the top |
1809 | // FIXME: This only takes us to the top of the rootEditableElement, not the top of the |
1810 | // top document. |
1811 | do { |
1812 | savedVisiblePos = currentVisiblePos; |
1813 | VisiblePosition prevVisiblePos = previousLinePosition(currentVisiblePos, 0, HasEditableAXRole); |
1814 | currentVisiblePos = prevVisiblePos; |
1815 | ++lineCount; |
1816 | } while (currentVisiblePos.isNotNull() && !(inSameLine(currentVisiblePos, savedVisiblePos))); |
1817 | |
1818 | return lineCount; |
1819 | } |
1820 | #endif |
1821 | |
1822 | // NOTE: Consider providing this utility method as AX API |
1823 | PlainTextRange AccessibilityObject::(const VisiblePositionRange& positionRange) const |
1824 | { |
1825 | int index1 = index(positionRange.start); |
1826 | int index2 = index(positionRange.end); |
1827 | if (index1 < 0 || index2 < 0 || index1 > index2) |
1828 | return PlainTextRange(); |
1829 | |
1830 | return PlainTextRange(index1, index2 - index1); |
1831 | } |
1832 | |
1833 | // The composed character range in the text associated with this accessibility object that |
1834 | // is specified by the given screen coordinates. This parameterized attribute returns the |
1835 | // complete range of characters (including surrogate pairs of multi-byte glyphs) at the given |
1836 | // screen coordinates. |
1837 | // NOTE: This varies from AppKit when the point is below the last line. AppKit returns an |
1838 | // an error in that case. We return textControl->text().length(), 1. Does this matter? |
1839 | PlainTextRange AccessibilityObject::doAXRangeForPosition(const IntPoint& point) const |
1840 | { |
1841 | int i = index(visiblePositionForPoint(point)); |
1842 | if (i < 0) |
1843 | return PlainTextRange(); |
1844 | |
1845 | return PlainTextRange(i, 1); |
1846 | } |
1847 | |
1848 | // Given a character index, the range of text associated with this accessibility object |
1849 | // over which the style in effect at that character index applies. |
1850 | PlainTextRange AccessibilityObject::doAXStyleRangeForIndex(unsigned index) const |
1851 | { |
1852 | VisiblePositionRange range = styleRangeForPosition(visiblePositionForIndex(index, false)); |
1853 | return plainTextRangeForVisiblePositionRange(range); |
1854 | } |
1855 | |
1856 | // Given an indexed character, the line number of the text associated with this accessibility |
1857 | // object that contains the character. |
1858 | unsigned AccessibilityObject::doAXLineForIndex(unsigned index) |
1859 | { |
1860 | return lineForPosition(visiblePositionForIndex(index, false)); |
1861 | } |
1862 | |
1863 | #if HAVE(ACCESSIBILITY) |
1864 | void AccessibilityObject::updateBackingStore() |
1865 | { |
1866 | if (!axObjectCache()) |
1867 | return; |
1868 | |
1869 | // Updating the layout may delete this object. |
1870 | RefPtr<AccessibilityObject> protectedThis(this); |
1871 | if (auto* document = this->document()) { |
1872 | if (!document->view()->layoutContext().isInRenderTreeLayout() && !document->inRenderTreeUpdate() && !document->inStyleRecalc()) |
1873 | document->updateLayoutIgnorePendingStylesheets(); |
1874 | } |
1875 | |
1876 | if (auto cache = axObjectCache()) |
1877 | cache->performDeferredCacheUpdate(); |
1878 | |
1879 | updateChildrenIfNecessary(); |
1880 | } |
1881 | #endif |
1882 | |
1883 | const AccessibilityScrollView* AccessibilityObject::ancestorAccessibilityScrollView(bool includeSelf) const |
1884 | { |
1885 | return downcast<AccessibilityScrollView>(AccessibilityObject::matchedParent(*this, includeSelf, [] (const auto& object) { |
1886 | return is<AccessibilityScrollView>(object); |
1887 | })); |
1888 | } |
1889 | |
1890 | ScrollView* AccessibilityObject::scrollViewAncestor() const |
1891 | { |
1892 | if (auto parentScrollView = ancestorAccessibilityScrollView(true/* includeSelf */)) |
1893 | return parentScrollView->scrollView(); |
1894 | |
1895 | return nullptr; |
1896 | } |
1897 | |
1898 | Document* AccessibilityObject::document() const |
1899 | { |
1900 | FrameView* frameView = documentFrameView(); |
1901 | if (!frameView) |
1902 | return nullptr; |
1903 | |
1904 | return frameView->frame().document(); |
1905 | } |
1906 | |
1907 | Page* AccessibilityObject::page() const |
1908 | { |
1909 | Document* document = this->document(); |
1910 | if (!document) |
1911 | return nullptr; |
1912 | return document->page(); |
1913 | } |
1914 | |
1915 | FrameView* AccessibilityObject::documentFrameView() const |
1916 | { |
1917 | const AccessibilityObject* object = this; |
1918 | while (object && !object->isAccessibilityRenderObject()) |
1919 | object = object->parentObject(); |
1920 | |
1921 | if (!object) |
1922 | return nullptr; |
1923 | |
1924 | return object->documentFrameView(); |
1925 | } |
1926 | |
1927 | #if HAVE(ACCESSIBILITY) |
1928 | const AccessibilityObject::AccessibilityChildrenVector& AccessibilityObject::children(bool updateChildrenIfNeeded) |
1929 | { |
1930 | if (updateChildrenIfNeeded) |
1931 | updateChildrenIfNecessary(); |
1932 | |
1933 | return m_children; |
1934 | } |
1935 | #endif |
1936 | |
1937 | void AccessibilityObject::updateChildrenIfNecessary() |
1938 | { |
1939 | if (!hasChildren()) { |
1940 | // Enable the cache in case we end up adding a lot of children, we don't want to recompute axIsIgnored each time. |
1941 | AXAttributeCacheEnabler enableCache(axObjectCache()); |
1942 | addChildren(); |
1943 | } |
1944 | } |
1945 | |
1946 | void AccessibilityObject::clearChildren() |
1947 | { |
1948 | // Some objects have weak pointers to their parents and those associations need to be detached. |
1949 | for (const auto& child : m_children) |
1950 | child->detachFromParent(); |
1951 | |
1952 | m_children.clear(); |
1953 | m_haveChildren = false; |
1954 | } |
1955 | |
1956 | AccessibilityObject* AccessibilityObject::anchorElementForNode(Node* node) |
1957 | { |
1958 | RenderObject* obj = node->renderer(); |
1959 | if (!obj) |
1960 | return nullptr; |
1961 | |
1962 | RefPtr<AccessibilityObject> axObj = obj->document().axObjectCache()->getOrCreate(obj); |
1963 | Element* anchor = axObj->anchorElement(); |
1964 | if (!anchor) |
1965 | return nullptr; |
1966 | |
1967 | RenderObject* anchorRenderer = anchor->renderer(); |
1968 | if (!anchorRenderer) |
1969 | return nullptr; |
1970 | |
1971 | return anchorRenderer->document().axObjectCache()->getOrCreate(anchorRenderer); |
1972 | } |
1973 | |
1974 | AccessibilityObject* AccessibilityObject::headingElementForNode(Node* node) |
1975 | { |
1976 | if (!node) |
1977 | return nullptr; |
1978 | |
1979 | RenderObject* renderObject = node->renderer(); |
1980 | if (!renderObject) |
1981 | return nullptr; |
1982 | |
1983 | AccessibilityObject* axObject = renderObject->document().axObjectCache()->getOrCreate(renderObject); |
1984 | |
1985 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*axObject, true, [] (const AccessibilityObject& object) { |
1986 | return object.roleValue() == AccessibilityRole::Heading; |
1987 | })); |
1988 | } |
1989 | |
1990 | const AccessibilityObject* AccessibilityObject::matchedParent(const AccessibilityObject& object, bool includeSelf, const WTF::Function<bool(const AccessibilityObject&)>& matches) |
1991 | { |
1992 | const AccessibilityObject* parent = includeSelf ? &object : object.parentObject(); |
1993 | for (; parent; parent = parent->parentObject()) { |
1994 | if (matches(*parent)) |
1995 | return parent; |
1996 | } |
1997 | return nullptr; |
1998 | } |
1999 | |
2000 | void AccessibilityObject::ariaTreeRows(AccessibilityChildrenVector& result) |
2001 | { |
2002 | for (const auto& child : children()) { |
2003 | // Add tree items as the rows. |
2004 | if (child->roleValue() == AccessibilityRole::TreeItem) |
2005 | result.append(child); |
2006 | |
2007 | // Now see if this item also has rows hiding inside of it. |
2008 | child->ariaTreeRows(result); |
2009 | } |
2010 | } |
2011 | |
2012 | void AccessibilityObject::ariaTreeItemContent(AccessibilityChildrenVector& result) |
2013 | { |
2014 | // The ARIA tree item content are the item that are not other tree items or their containing groups. |
2015 | for (const auto& child : children()) { |
2016 | if (!child->isGroup() && child->roleValue() != AccessibilityRole::TreeItem) |
2017 | result.append(child); |
2018 | } |
2019 | } |
2020 | |
2021 | void AccessibilityObject::ariaTreeItemDisclosedRows(AccessibilityChildrenVector& result) |
2022 | { |
2023 | for (const auto& obj : children()) { |
2024 | // Add tree items as the rows. |
2025 | if (obj->roleValue() == AccessibilityRole::TreeItem) |
2026 | result.append(obj); |
2027 | // If it's not a tree item, then descend into the group to find more tree items. |
2028 | else |
2029 | obj->ariaTreeRows(result); |
2030 | } |
2031 | } |
2032 | |
2033 | const String AccessibilityObject::defaultLiveRegionStatusForRole(AccessibilityRole role) |
2034 | { |
2035 | switch (role) { |
2036 | case AccessibilityRole::ApplicationAlertDialog: |
2037 | case AccessibilityRole::ApplicationAlert: |
2038 | return "assertive"_s ; |
2039 | case AccessibilityRole::ApplicationLog: |
2040 | case AccessibilityRole::ApplicationStatus: |
2041 | return "polite"_s ; |
2042 | case AccessibilityRole::ApplicationTimer: |
2043 | case AccessibilityRole::ApplicationMarquee: |
2044 | return "off"_s ; |
2045 | default: |
2046 | return nullAtom(); |
2047 | } |
2048 | } |
2049 | |
2050 | #if HAVE(ACCESSIBILITY) |
2051 | const String& AccessibilityObject::actionVerb() const |
2052 | { |
2053 | #if !PLATFORM(IOS_FAMILY) |
2054 | // FIXME: Need to add verbs for select elements. |
2055 | static NeverDestroyed<const String> buttonAction(AXButtonActionVerb()); |
2056 | static NeverDestroyed<const String> textFieldAction(AXTextFieldActionVerb()); |
2057 | static NeverDestroyed<const String> radioButtonAction(AXRadioButtonActionVerb()); |
2058 | static NeverDestroyed<const String> checkedCheckBoxAction(AXCheckedCheckBoxActionVerb()); |
2059 | static NeverDestroyed<const String> uncheckedCheckBoxAction(AXUncheckedCheckBoxActionVerb()); |
2060 | static NeverDestroyed<const String> linkAction(AXLinkActionVerb()); |
2061 | static NeverDestroyed<const String> (AXMenuListActionVerb()); |
2062 | static NeverDestroyed<const String> (AXMenuListPopupActionVerb()); |
2063 | static NeverDestroyed<const String> listItemAction(AXListItemActionVerb()); |
2064 | |
2065 | switch (roleValue()) { |
2066 | case AccessibilityRole::Button: |
2067 | case AccessibilityRole::ToggleButton: |
2068 | return buttonAction; |
2069 | case AccessibilityRole::TextField: |
2070 | case AccessibilityRole::TextArea: |
2071 | return textFieldAction; |
2072 | case AccessibilityRole::RadioButton: |
2073 | return radioButtonAction; |
2074 | case AccessibilityRole::CheckBox: |
2075 | case AccessibilityRole::Switch: |
2076 | return isChecked() ? checkedCheckBoxAction : uncheckedCheckBoxAction; |
2077 | case AccessibilityRole::Link: |
2078 | case AccessibilityRole::WebCoreLink: |
2079 | return linkAction; |
2080 | case AccessibilityRole::PopUpButton: |
2081 | return menuListAction; |
2082 | case AccessibilityRole::MenuListPopup: |
2083 | return menuListPopupAction; |
2084 | case AccessibilityRole::ListItem: |
2085 | return listItemAction; |
2086 | default: |
2087 | return nullAtom(); |
2088 | } |
2089 | #else |
2090 | return nullAtom(); |
2091 | #endif |
2092 | } |
2093 | #endif |
2094 | |
2095 | bool AccessibilityObject::ariaIsMultiline() const |
2096 | { |
2097 | return equalLettersIgnoringASCIICase(getAttribute(aria_multilineAttr), "true" ); |
2098 | } |
2099 | |
2100 | String AccessibilityObject::invalidStatus() const |
2101 | { |
2102 | String grammarValue = "grammar"_s ; |
2103 | String falseValue = "false"_s ; |
2104 | String spellingValue = "spelling"_s ; |
2105 | String trueValue = "true"_s ; |
2106 | String undefinedValue = "undefined"_s ; |
2107 | |
2108 | // aria-invalid can return false (default), grammar, spelling, or true. |
2109 | String ariaInvalid = stripLeadingAndTrailingHTMLSpaces(getAttribute(aria_invalidAttr)); |
2110 | |
2111 | if (ariaInvalid.isEmpty()) { |
2112 | // We should expose invalid status for input types. |
2113 | Node* node = this->node(); |
2114 | if (node && is<HTMLInputElement>(*node)) { |
2115 | HTMLInputElement& input = downcast<HTMLInputElement>(*node); |
2116 | if (input.hasBadInput() || input.typeMismatch()) |
2117 | return trueValue; |
2118 | } |
2119 | return falseValue; |
2120 | } |
2121 | |
2122 | // If "false", "undefined" [sic, string value], empty, or missing, return "false". |
2123 | if (ariaInvalid == falseValue || ariaInvalid == undefinedValue) |
2124 | return falseValue; |
2125 | // Besides true/false/undefined, the only tokens defined by WAI-ARIA 1.0... |
2126 | // ...for @aria-invalid are "grammar" and "spelling". |
2127 | if (ariaInvalid == grammarValue) |
2128 | return grammarValue; |
2129 | if (ariaInvalid == spellingValue) |
2130 | return spellingValue; |
2131 | // Any other non empty string should be treated as "true". |
2132 | return trueValue; |
2133 | } |
2134 | |
2135 | bool AccessibilityObject::supportsCurrent() const |
2136 | { |
2137 | return hasAttribute(aria_currentAttr); |
2138 | } |
2139 | |
2140 | AccessibilityCurrentState AccessibilityObject::currentState() const |
2141 | { |
2142 | // aria-current can return false (default), true, page, step, location, date or time. |
2143 | String currentStateValue = stripLeadingAndTrailingHTMLSpaces(getAttribute(aria_currentAttr)); |
2144 | |
2145 | // If "false", empty, or missing, return false state. |
2146 | if (currentStateValue.isEmpty() || currentStateValue == "false" ) |
2147 | return AccessibilityCurrentState::False; |
2148 | |
2149 | if (currentStateValue == "page" ) |
2150 | return AccessibilityCurrentState::Page; |
2151 | if (currentStateValue == "step" ) |
2152 | return AccessibilityCurrentState::Step; |
2153 | if (currentStateValue == "location" ) |
2154 | return AccessibilityCurrentState::Location; |
2155 | if (currentStateValue == "date" ) |
2156 | return AccessibilityCurrentState::Date; |
2157 | if (currentStateValue == "time" ) |
2158 | return AccessibilityCurrentState::Time; |
2159 | |
2160 | // Any value not included in the list of allowed values should be treated as "true". |
2161 | return AccessibilityCurrentState::True; |
2162 | } |
2163 | |
2164 | String AccessibilityObject::currentValue() const |
2165 | { |
2166 | switch (currentState()) { |
2167 | case AccessibilityCurrentState::False: |
2168 | return "false" ; |
2169 | case AccessibilityCurrentState::Page: |
2170 | return "page" ; |
2171 | case AccessibilityCurrentState::Step: |
2172 | return "step" ; |
2173 | case AccessibilityCurrentState::Location: |
2174 | return "location" ; |
2175 | case AccessibilityCurrentState::Time: |
2176 | return "time" ; |
2177 | case AccessibilityCurrentState::Date: |
2178 | return "date" ; |
2179 | default: |
2180 | case AccessibilityCurrentState::True: |
2181 | return "true" ; |
2182 | } |
2183 | } |
2184 | |
2185 | bool AccessibilityObject::isModalDescendant(Node* modalNode) const |
2186 | { |
2187 | Node* node = this->node(); |
2188 | if (!modalNode || !node) |
2189 | return false; |
2190 | |
2191 | if (node == modalNode) |
2192 | return true; |
2193 | |
2194 | // ARIA 1.1 aria-modal, indicates whether an element is modal when displayed. |
2195 | // For the decendants of the modal object, they should also be considered as aria-modal=true. |
2196 | return node->isDescendantOf(*modalNode); |
2197 | } |
2198 | |
2199 | bool AccessibilityObject::isModalNode() const |
2200 | { |
2201 | if (AXObjectCache* cache = axObjectCache()) |
2202 | return node() && cache->modalNode() == node(); |
2203 | |
2204 | return false; |
2205 | } |
2206 | |
2207 | bool AccessibilityObject::ignoredFromModalPresence() const |
2208 | { |
2209 | // We shouldn't ignore the top node. |
2210 | if (!node() || !node()->parentNode()) |
2211 | return false; |
2212 | |
2213 | AXObjectCache* cache = axObjectCache(); |
2214 | if (!cache) |
2215 | return false; |
2216 | |
2217 | // modalNode is the current displayed modal dialog. |
2218 | Node* modalNode = cache->modalNode(); |
2219 | if (!modalNode) |
2220 | return false; |
2221 | |
2222 | // We only want to ignore the objects within the same frame as the modal dialog. |
2223 | if (modalNode->document().frame() != this->frame()) |
2224 | return false; |
2225 | |
2226 | return !isModalDescendant(modalNode); |
2227 | } |
2228 | |
2229 | bool AccessibilityObject::hasTagName(const QualifiedName& tagName) const |
2230 | { |
2231 | Node* node = this->node(); |
2232 | return is<Element>(node) && downcast<Element>(*node).hasTagName(tagName); |
2233 | } |
2234 | |
2235 | bool AccessibilityObject::hasAttribute(const QualifiedName& attribute) const |
2236 | { |
2237 | Node* node = this->node(); |
2238 | if (!is<Element>(node)) |
2239 | return false; |
2240 | |
2241 | return downcast<Element>(*node).hasAttributeWithoutSynchronization(attribute); |
2242 | } |
2243 | |
2244 | const AtomicString& AccessibilityObject::getAttribute(const QualifiedName& attribute) const |
2245 | { |
2246 | if (auto* element = this->element()) |
2247 | return element->attributeWithoutSynchronization(attribute); |
2248 | return nullAtom(); |
2249 | } |
2250 | |
2251 | bool AccessibilityObject::(const String& replacementString, const PlainTextRange& range) |
2252 | { |
2253 | if (!renderer() || !is<Element>(node())) |
2254 | return false; |
2255 | |
2256 | auto& element = downcast<Element>(*renderer()->node()); |
2257 | |
2258 | // We should use the editor's insertText to mimic typing into the field. |
2259 | // Also only do this when the field is in editing mode. |
2260 | auto& frame = renderer()->frame(); |
2261 | if (element.shouldUseInputMethod()) { |
2262 | frame.selection().setSelectedRange(rangeForPlainTextRange(range).get(), DOWNSTREAM, FrameSelection::ShouldCloseTyping::Yes); |
2263 | frame.editor().replaceSelectionWithText(replacementString, Editor::SelectReplacement::No, Editor::SmartReplace::No); |
2264 | return true; |
2265 | } |
2266 | |
2267 | if (is<HTMLInputElement>(element)) { |
2268 | downcast<HTMLInputElement>(element).setRangeText(replacementString, range.start, range.length, "" ); |
2269 | return true; |
2270 | } |
2271 | if (is<HTMLTextAreaElement>(element)) { |
2272 | downcast<HTMLTextAreaElement>(element).setRangeText(replacementString, range.start, range.length, "" ); |
2273 | return true; |
2274 | } |
2275 | |
2276 | return false; |
2277 | } |
2278 | |
2279 | // Lacking concrete evidence of orientation, horizontal means width > height. vertical is height > width; |
2280 | AccessibilityOrientation AccessibilityObject::orientation() const |
2281 | { |
2282 | LayoutRect bounds = elementRect(); |
2283 | if (bounds.size().width() > bounds.size().height()) |
2284 | return AccessibilityOrientation::Horizontal; |
2285 | if (bounds.size().height() > bounds.size().width()) |
2286 | return AccessibilityOrientation::Vertical; |
2287 | |
2288 | return AccessibilityOrientation::Undefined; |
2289 | } |
2290 | |
2291 | bool AccessibilityObject::isDescendantOfObject(const AccessibilityObject* axObject) const |
2292 | { |
2293 | if (!axObject || !axObject->hasChildren()) |
2294 | return false; |
2295 | |
2296 | return AccessibilityObject::matchedParent(*this, false, [axObject] (const AccessibilityObject& object) { |
2297 | return &object == axObject; |
2298 | }) != nullptr; |
2299 | } |
2300 | |
2301 | bool AccessibilityObject::isAncestorOfObject(const AccessibilityObject* axObject) const |
2302 | { |
2303 | if (!axObject) |
2304 | return false; |
2305 | |
2306 | return this == axObject || axObject->isDescendantOfObject(this); |
2307 | } |
2308 | |
2309 | AccessibilityObject* AccessibilityObject::firstAnonymousBlockChild() const |
2310 | { |
2311 | for (AccessibilityObject* child = firstChild(); child; child = child->nextSibling()) { |
2312 | if (child->renderer() && child->renderer()->isAnonymousBlock()) |
2313 | return child; |
2314 | } |
2315 | return nullptr; |
2316 | } |
2317 | |
2318 | using ARIARoleMap = HashMap<String, AccessibilityRole, ASCIICaseInsensitiveHash>; |
2319 | using ARIAReverseRoleMap = HashMap<AccessibilityRole, String, DefaultHash<int>::Hash, WTF::UnsignedWithZeroKeyHashTraits<int>>; |
2320 | |
2321 | static ARIARoleMap* gAriaRoleMap = nullptr; |
2322 | static ARIAReverseRoleMap* gAriaReverseRoleMap = nullptr; |
2323 | |
2324 | struct RoleEntry { |
2325 | String ariaRole; |
2326 | AccessibilityRole webcoreRole; |
2327 | }; |
2328 | |
2329 | static void initializeRoleMap() |
2330 | { |
2331 | if (gAriaRoleMap) |
2332 | return; |
2333 | ASSERT(!gAriaReverseRoleMap); |
2334 | |
2335 | const RoleEntry roles[] = { |
2336 | { "alert" , AccessibilityRole::ApplicationAlert }, |
2337 | { "alertdialog" , AccessibilityRole::ApplicationAlertDialog }, |
2338 | { "application" , AccessibilityRole::WebApplication }, |
2339 | { "article" , AccessibilityRole::DocumentArticle }, |
2340 | { "banner" , AccessibilityRole::LandmarkBanner }, |
2341 | { "blockquote" , AccessibilityRole::Blockquote }, |
2342 | { "button" , AccessibilityRole::Button }, |
2343 | { "caption" , AccessibilityRole::Caption }, |
2344 | { "checkbox" , AccessibilityRole::CheckBox }, |
2345 | { "complementary" , AccessibilityRole::LandmarkComplementary }, |
2346 | { "contentinfo" , AccessibilityRole::LandmarkContentInfo }, |
2347 | { "dialog" , AccessibilityRole::ApplicationDialog }, |
2348 | { "directory" , AccessibilityRole::Directory }, |
2349 | // The 'doc-*' roles are defined the ARIA DPUB mobile: https://www.w3.org/TR/dpub-aam-1.0/ |
2350 | // Editor's draft is currently at https://rawgit.com/w3c/aria/master/dpub-aam/dpub-aam.html |
2351 | { "doc-abstract" , AccessibilityRole::ApplicationTextGroup }, |
2352 | { "doc-acknowledgments" , AccessibilityRole::LandmarkDocRegion }, |
2353 | { "doc-afterword" , AccessibilityRole::LandmarkDocRegion }, |
2354 | { "doc-appendix" , AccessibilityRole::LandmarkDocRegion }, |
2355 | { "doc-backlink" , AccessibilityRole::WebCoreLink }, |
2356 | { "doc-biblioentry" , AccessibilityRole::ListItem }, |
2357 | { "doc-bibliography" , AccessibilityRole::LandmarkDocRegion }, |
2358 | { "doc-biblioref" , AccessibilityRole::WebCoreLink }, |
2359 | { "doc-chapter" , AccessibilityRole::LandmarkDocRegion }, |
2360 | { "doc-colophon" , AccessibilityRole::ApplicationTextGroup }, |
2361 | { "doc-conclusion" , AccessibilityRole::LandmarkDocRegion }, |
2362 | { "doc-cover" , AccessibilityRole::Image }, |
2363 | { "doc-credit" , AccessibilityRole::ApplicationTextGroup }, |
2364 | { "doc-credits" , AccessibilityRole::LandmarkDocRegion }, |
2365 | { "doc-dedication" , AccessibilityRole::ApplicationTextGroup }, |
2366 | { "doc-endnote" , AccessibilityRole::ListItem }, |
2367 | { "doc-endnotes" , AccessibilityRole::LandmarkDocRegion }, |
2368 | { "doc-epigraph" , AccessibilityRole::ApplicationTextGroup }, |
2369 | { "doc-epilogue" , AccessibilityRole::LandmarkDocRegion }, |
2370 | { "doc-errata" , AccessibilityRole::LandmarkDocRegion }, |
2371 | { "doc-example" , AccessibilityRole::ApplicationTextGroup }, |
2372 | { "doc-footnote" , AccessibilityRole::Footnote }, |
2373 | { "doc-foreword" , AccessibilityRole::LandmarkDocRegion }, |
2374 | { "doc-glossary" , AccessibilityRole::LandmarkDocRegion }, |
2375 | { "doc-glossref" , AccessibilityRole::WebCoreLink }, |
2376 | { "doc-index" , AccessibilityRole::LandmarkNavigation }, |
2377 | { "doc-introduction" , AccessibilityRole::LandmarkDocRegion }, |
2378 | { "doc-noteref" , AccessibilityRole::WebCoreLink }, |
2379 | { "doc-notice" , AccessibilityRole::DocumentNote }, |
2380 | { "doc-pagebreak" , AccessibilityRole::Splitter }, |
2381 | { "doc-pagelist" , AccessibilityRole::LandmarkNavigation }, |
2382 | { "doc-part" , AccessibilityRole::LandmarkDocRegion }, |
2383 | { "doc-preface" , AccessibilityRole::LandmarkDocRegion }, |
2384 | { "doc-prologue" , AccessibilityRole::LandmarkDocRegion }, |
2385 | { "doc-pullquote" , AccessibilityRole::ApplicationTextGroup }, |
2386 | { "doc-qna" , AccessibilityRole::ApplicationTextGroup }, |
2387 | { "doc-subtitle" , AccessibilityRole::Heading }, |
2388 | { "doc-tip" , AccessibilityRole::DocumentNote }, |
2389 | { "doc-toc" , AccessibilityRole::LandmarkNavigation }, |
2390 | { "figure" , AccessibilityRole::Figure }, |
2391 | // The mappings for 'graphics-*' roles are defined in this spec: https://w3c.github.io/graphics-aam/ |
2392 | { "graphics-document" , AccessibilityRole::GraphicsDocument }, |
2393 | { "graphics-object" , AccessibilityRole::GraphicsObject }, |
2394 | { "graphics-symbol" , AccessibilityRole::GraphicsSymbol }, |
2395 | { "grid" , AccessibilityRole::Grid }, |
2396 | { "gridcell" , AccessibilityRole::GridCell }, |
2397 | { "table" , AccessibilityRole::Table }, |
2398 | { "cell" , AccessibilityRole::Cell }, |
2399 | { "columnheader" , AccessibilityRole::ColumnHeader }, |
2400 | { "combobox" , AccessibilityRole::ComboBox }, |
2401 | { "definition" , AccessibilityRole::Definition }, |
2402 | { "document" , AccessibilityRole::Document }, |
2403 | { "feed" , AccessibilityRole::Feed }, |
2404 | { "form" , AccessibilityRole::Form }, |
2405 | { "rowheader" , AccessibilityRole::RowHeader }, |
2406 | { "group" , AccessibilityRole::ApplicationGroup }, |
2407 | { "heading" , AccessibilityRole::Heading }, |
2408 | { "img" , AccessibilityRole::Image }, |
2409 | { "link" , AccessibilityRole::WebCoreLink }, |
2410 | { "list" , AccessibilityRole::List }, |
2411 | { "listitem" , AccessibilityRole::ListItem }, |
2412 | { "listbox" , AccessibilityRole::ListBox }, |
2413 | { "log" , AccessibilityRole::ApplicationLog }, |
2414 | { "main" , AccessibilityRole::LandmarkMain }, |
2415 | { "marquee" , AccessibilityRole::ApplicationMarquee }, |
2416 | { "math" , AccessibilityRole::DocumentMath }, |
2417 | { "menu" , AccessibilityRole::Menu }, |
2418 | { "menubar" , AccessibilityRole::MenuBar }, |
2419 | { "menuitem" , AccessibilityRole::MenuItem }, |
2420 | { "menuitemcheckbox" , AccessibilityRole::MenuItemCheckbox }, |
2421 | { "menuitemradio" , AccessibilityRole::MenuItemRadio }, |
2422 | { "meter" , AccessibilityRole::Meter }, |
2423 | { "none" , AccessibilityRole::Presentational }, |
2424 | { "note" , AccessibilityRole::DocumentNote }, |
2425 | { "navigation" , AccessibilityRole::LandmarkNavigation }, |
2426 | { "option" , AccessibilityRole::ListBoxOption }, |
2427 | { "paragraph" , AccessibilityRole::Paragraph }, |
2428 | { "presentation" , AccessibilityRole::Presentational }, |
2429 | { "progressbar" , AccessibilityRole::ProgressIndicator }, |
2430 | { "radio" , AccessibilityRole::RadioButton }, |
2431 | { "radiogroup" , AccessibilityRole::RadioGroup }, |
2432 | { "region" , AccessibilityRole::LandmarkRegion }, |
2433 | { "row" , AccessibilityRole::Row }, |
2434 | { "rowgroup" , AccessibilityRole::RowGroup }, |
2435 | { "scrollbar" , AccessibilityRole::ScrollBar }, |
2436 | { "search" , AccessibilityRole::LandmarkSearch }, |
2437 | { "searchbox" , AccessibilityRole::SearchField }, |
2438 | { "separator" , AccessibilityRole::Splitter }, |
2439 | { "slider" , AccessibilityRole::Slider }, |
2440 | { "spinbutton" , AccessibilityRole::SpinButton }, |
2441 | { "status" , AccessibilityRole::ApplicationStatus }, |
2442 | { "switch" , AccessibilityRole::Switch }, |
2443 | { "tab" , AccessibilityRole::Tab }, |
2444 | { "tablist" , AccessibilityRole::TabList }, |
2445 | { "tabpanel" , AccessibilityRole::TabPanel }, |
2446 | { "text" , AccessibilityRole::StaticText }, |
2447 | { "textbox" , AccessibilityRole::TextArea }, |
2448 | { "term" , AccessibilityRole::Term }, |
2449 | { "timer" , AccessibilityRole::ApplicationTimer }, |
2450 | { "toolbar" , AccessibilityRole::Toolbar }, |
2451 | { "tooltip" , AccessibilityRole::UserInterfaceTooltip }, |
2452 | { "tree" , AccessibilityRole::Tree }, |
2453 | { "treegrid" , AccessibilityRole::TreeGrid }, |
2454 | { "treeitem" , AccessibilityRole::TreeItem } |
2455 | }; |
2456 | |
2457 | gAriaRoleMap = new ARIARoleMap; |
2458 | gAriaReverseRoleMap = new ARIAReverseRoleMap; |
2459 | size_t roleLength = WTF_ARRAY_LENGTH(roles); |
2460 | for (size_t i = 0; i < roleLength; ++i) { |
2461 | gAriaRoleMap->set(roles[i].ariaRole, roles[i].webcoreRole); |
2462 | gAriaReverseRoleMap->set(static_cast<int>(roles[i].webcoreRole), roles[i].ariaRole); |
2463 | } |
2464 | } |
2465 | |
2466 | static ARIARoleMap& ariaRoleMap() |
2467 | { |
2468 | initializeRoleMap(); |
2469 | return *gAriaRoleMap; |
2470 | } |
2471 | |
2472 | static ARIAReverseRoleMap& reverseAriaRoleMap() |
2473 | { |
2474 | initializeRoleMap(); |
2475 | return *gAriaReverseRoleMap; |
2476 | } |
2477 | |
2478 | AccessibilityRole AccessibilityObject::ariaRoleToWebCoreRole(const String& value) |
2479 | { |
2480 | if (value.isNull() || value.isEmpty()) |
2481 | return AccessibilityRole::Unknown; |
2482 | |
2483 | for (auto roleName : StringView(value).split(' ')) { |
2484 | AccessibilityRole role = ariaRoleMap().get<ASCIICaseInsensitiveStringViewHashTranslator>(roleName); |
2485 | if (static_cast<int>(role)) |
2486 | return role; |
2487 | } |
2488 | return AccessibilityRole::Unknown; |
2489 | } |
2490 | |
2491 | String AccessibilityObject::computedRoleString() const |
2492 | { |
2493 | // FIXME: Need a few special cases that aren't in the RoleMap: option, etc. http://webkit.org/b/128296 |
2494 | AccessibilityRole role = roleValue(); |
2495 | |
2496 | // We do not compute a role string for generic block elements with user-agent assigned roles. |
2497 | if (role == AccessibilityRole::Group || role == AccessibilityRole::TextGroup) |
2498 | return "" ; |
2499 | |
2500 | // We do compute a role string for block elements with author-provided roles. |
2501 | if (role == AccessibilityRole::ApplicationTextGroup |
2502 | || role == AccessibilityRole::Footnote |
2503 | || role == AccessibilityRole::GraphicsObject) |
2504 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::ApplicationGroup)); |
2505 | |
2506 | if (role == AccessibilityRole::GraphicsDocument) |
2507 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::Document)); |
2508 | |
2509 | if (role == AccessibilityRole::GraphicsSymbol) |
2510 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::Image)); |
2511 | |
2512 | if (role == AccessibilityRole::HorizontalRule) |
2513 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::Splitter)); |
2514 | |
2515 | if (role == AccessibilityRole::PopUpButton || role == AccessibilityRole::ToggleButton) |
2516 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::Button)); |
2517 | |
2518 | if (role == AccessibilityRole::LandmarkDocRegion) |
2519 | return reverseAriaRoleMap().get(static_cast<int>(AccessibilityRole::LandmarkRegion)); |
2520 | |
2521 | return reverseAriaRoleMap().get(static_cast<int>(role)); |
2522 | } |
2523 | |
2524 | bool AccessibilityObject::hasHighlighting() const |
2525 | { |
2526 | for (Node* node = this->node(); node; node = node->parentNode()) { |
2527 | if (node->hasTagName(markTag)) |
2528 | return true; |
2529 | } |
2530 | |
2531 | return false; |
2532 | } |
2533 | |
2534 | String AccessibilityObject::roleDescription() const |
2535 | { |
2536 | return stripLeadingAndTrailingHTMLSpaces(getAttribute(aria_roledescriptionAttr)); |
2537 | } |
2538 | |
2539 | bool nodeHasPresentationRole(Node* node) |
2540 | { |
2541 | return nodeHasRole(node, "presentation" ) || nodeHasRole(node, "none" ); |
2542 | } |
2543 | |
2544 | bool AccessibilityObject::supportsPressAction() const |
2545 | { |
2546 | if (isButton()) |
2547 | return true; |
2548 | if (roleValue() == AccessibilityRole::Details) |
2549 | return true; |
2550 | |
2551 | Element* actionElement = this->actionElement(); |
2552 | if (!actionElement) |
2553 | return false; |
2554 | |
2555 | // [Bug: 136247] Heuristic: element handlers that have more than one accessible descendant should not be exposed as supporting press. |
2556 | if (actionElement != element()) { |
2557 | if (AccessibilityObject* axObj = axObjectCache()->getOrCreate(actionElement)) { |
2558 | AccessibilityChildrenVector results; |
2559 | // Search within for immediate descendants that are static text. If we find more than one |
2560 | // then this is an event delegator actionElement and we should expose the press action. |
2561 | Vector<AccessibilitySearchKey> keys({ AccessibilitySearchKey::StaticText, AccessibilitySearchKey::Control, AccessibilitySearchKey::Graphic, AccessibilitySearchKey::Heading, AccessibilitySearchKey::Link }); |
2562 | AccessibilitySearchCriteria criteria(axObj, AccessibilitySearchDirection::Next, emptyString(), 2, false, false); |
2563 | criteria.searchKeys = keys; |
2564 | axObj->findMatchingObjects(&criteria, results); |
2565 | if (results.size() > 1) |
2566 | return false; |
2567 | } |
2568 | } |
2569 | |
2570 | // [Bug: 133613] Heuristic: If the action element is presentational, we shouldn't expose press as a supported action. |
2571 | return !nodeHasPresentationRole(actionElement); |
2572 | } |
2573 | |
2574 | bool AccessibilityObject::supportsDatetimeAttribute() const |
2575 | { |
2576 | return hasTagName(insTag) || hasTagName(delTag) || hasTagName(timeTag); |
2577 | } |
2578 | |
2579 | const AtomicString& AccessibilityObject::datetimeAttributeValue() const |
2580 | { |
2581 | return getAttribute(datetimeAttr); |
2582 | } |
2583 | |
2584 | const AtomicString& AccessibilityObject::linkRelValue() const |
2585 | { |
2586 | return getAttribute(relAttr); |
2587 | } |
2588 | |
2589 | const String AccessibilityObject::keyShortcutsValue() const |
2590 | { |
2591 | return getAttribute(aria_keyshortcutsAttr); |
2592 | } |
2593 | |
2594 | Element* AccessibilityObject::element() const |
2595 | { |
2596 | Node* node = this->node(); |
2597 | if (is<Element>(node)) |
2598 | return downcast<Element>(node); |
2599 | return nullptr; |
2600 | } |
2601 | |
2602 | bool AccessibilityObject::isValueAutofillAvailable() const |
2603 | { |
2604 | if (!isNativeTextControl()) |
2605 | return false; |
2606 | |
2607 | Node* node = this->node(); |
2608 | if (!is<HTMLInputElement>(node)) |
2609 | return false; |
2610 | |
2611 | return downcast<HTMLInputElement>(*node).isAutoFillAvailable() || downcast<HTMLInputElement>(*node).autoFillButtonType() != AutoFillButtonType::None; |
2612 | } |
2613 | |
2614 | AutoFillButtonType AccessibilityObject::valueAutofillButtonType() const |
2615 | { |
2616 | if (!isValueAutofillAvailable()) |
2617 | return AutoFillButtonType::None; |
2618 | |
2619 | return downcast<HTMLInputElement>(*this->node()).autoFillButtonType(); |
2620 | } |
2621 | |
2622 | bool AccessibilityObject::isValueAutofilled() const |
2623 | { |
2624 | if (!isNativeTextControl()) |
2625 | return false; |
2626 | |
2627 | Node* node = this->node(); |
2628 | if (!is<HTMLInputElement>(node)) |
2629 | return false; |
2630 | |
2631 | return downcast<HTMLInputElement>(*node).isAutoFilled(); |
2632 | } |
2633 | |
2634 | const String AccessibilityObject::placeholderValue() const |
2635 | { |
2636 | const AtomicString& placeholder = getAttribute(placeholderAttr); |
2637 | if (!placeholder.isEmpty()) |
2638 | return placeholder; |
2639 | |
2640 | const AtomicString& ariaPlaceholder = getAttribute(aria_placeholderAttr); |
2641 | if (!ariaPlaceholder.isEmpty()) |
2642 | return ariaPlaceholder; |
2643 | |
2644 | return nullAtom(); |
2645 | } |
2646 | |
2647 | bool AccessibilityObject::isInsideLiveRegion(bool excludeIfOff) const |
2648 | { |
2649 | return liveRegionAncestor(excludeIfOff); |
2650 | } |
2651 | |
2652 | AccessibilityObject* AccessibilityObject::liveRegionAncestor(bool excludeIfOff) const |
2653 | { |
2654 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, true, [excludeIfOff] (const AccessibilityObject& object) { |
2655 | return object.supportsLiveRegion(excludeIfOff); |
2656 | })); |
2657 | } |
2658 | |
2659 | bool AccessibilityObject::supportsARIAAttributes() const |
2660 | { |
2661 | // This returns whether the element supports any global ARIA attributes. |
2662 | return supportsLiveRegion() |
2663 | || supportsARIADragging() |
2664 | || supportsARIADropping() |
2665 | || supportsARIAOwns() |
2666 | || hasAttribute(aria_atomicAttr) |
2667 | || hasAttribute(aria_busyAttr) |
2668 | || hasAttribute(aria_controlsAttr) |
2669 | || hasAttribute(aria_currentAttr) |
2670 | || hasAttribute(aria_describedbyAttr) |
2671 | || hasAttribute(aria_detailsAttr) |
2672 | || hasAttribute(aria_disabledAttr) |
2673 | || hasAttribute(aria_errormessageAttr) |
2674 | || hasAttribute(aria_flowtoAttr) |
2675 | || hasAttribute(aria_haspopupAttr) |
2676 | || hasAttribute(aria_invalidAttr) |
2677 | || hasAttribute(aria_labelAttr) |
2678 | || hasAttribute(aria_labelledbyAttr) |
2679 | || hasAttribute(aria_relevantAttr); |
2680 | } |
2681 | |
2682 | bool AccessibilityObject::liveRegionStatusIsEnabled(const AtomicString& liveRegionStatus) |
2683 | { |
2684 | return equalLettersIgnoringASCIICase(liveRegionStatus, "polite" ) || equalLettersIgnoringASCIICase(liveRegionStatus, "assertive" ); |
2685 | } |
2686 | |
2687 | bool AccessibilityObject::supportsLiveRegion(bool excludeIfOff) const |
2688 | { |
2689 | const AtomicString& liveRegionStatusValue = liveRegionStatus(); |
2690 | return excludeIfOff ? liveRegionStatusIsEnabled(liveRegionStatusValue) : !liveRegionStatusValue.isEmpty(); |
2691 | } |
2692 | |
2693 | AccessibilityObjectInterface* AccessibilityObject::elementAccessibilityHitTest(const IntPoint& point) const |
2694 | { |
2695 | // Send the hit test back into the sub-frame if necessary. |
2696 | if (isAttachment()) { |
2697 | Widget* widget = widgetForAttachmentView(); |
2698 | // Normalize the point for the widget's bounds. |
2699 | if (widget && widget->isFrameView()) { |
2700 | if (AXObjectCache* cache = axObjectCache()) |
2701 | return cache->getOrCreate(widget)->accessibilityHitTest(IntPoint(point - widget->frameRect().location())); |
2702 | } |
2703 | } |
2704 | |
2705 | // Check if there are any mock elements that need to be handled. |
2706 | for (const auto& child : m_children) { |
2707 | if (child->isMockObject() && child->elementRect().contains(point)) |
2708 | return child->elementAccessibilityHitTest(point); |
2709 | } |
2710 | |
2711 | return const_cast<AccessibilityObject*>(this); |
2712 | } |
2713 | |
2714 | AXObjectCache* AccessibilityObject::axObjectCache() const |
2715 | { |
2716 | auto* document = this->document(); |
2717 | return document ? document->axObjectCache() : nullptr; |
2718 | } |
2719 | |
2720 | AccessibilityObjectInterface* AccessibilityObject::focusedUIElement() const |
2721 | { |
2722 | auto* page = this->page(); |
2723 | return page ? AXObjectCache::focusedUIElementForPage(page) : nullptr; |
2724 | } |
2725 | |
2726 | AccessibilitySortDirection AccessibilityObject::sortDirection() const |
2727 | { |
2728 | AccessibilityRole role = roleValue(); |
2729 | if (role != AccessibilityRole::RowHeader && role != AccessibilityRole::ColumnHeader) |
2730 | return AccessibilitySortDirection::Invalid; |
2731 | |
2732 | const AtomicString& sortAttribute = getAttribute(aria_sortAttr); |
2733 | if (equalLettersIgnoringASCIICase(sortAttribute, "ascending" )) |
2734 | return AccessibilitySortDirection::Ascending; |
2735 | if (equalLettersIgnoringASCIICase(sortAttribute, "descending" )) |
2736 | return AccessibilitySortDirection::Descending; |
2737 | if (equalLettersIgnoringASCIICase(sortAttribute, "other" )) |
2738 | return AccessibilitySortDirection::Other; |
2739 | |
2740 | return AccessibilitySortDirection::None; |
2741 | } |
2742 | |
2743 | bool AccessibilityObject::supportsRangeValue() const |
2744 | { |
2745 | return isProgressIndicator() |
2746 | || isSlider() |
2747 | || isScrollbar() |
2748 | || isSpinButton() |
2749 | || (isSplitter() && canSetFocusAttribute()) |
2750 | || isAttachmentElement(); |
2751 | } |
2752 | |
2753 | bool AccessibilityObject::() const |
2754 | { |
2755 | return hasAttribute(aria_haspopupAttr) || isComboBox(); |
2756 | } |
2757 | |
2758 | String AccessibilityObject::() const |
2759 | { |
2760 | const AtomicString& = getAttribute(aria_haspopupAttr); |
2761 | if (equalLettersIgnoringASCIICase(hasPopup, "true" ) |
2762 | || equalLettersIgnoringASCIICase(hasPopup, "dialog" ) |
2763 | || equalLettersIgnoringASCIICase(hasPopup, "grid" ) |
2764 | || equalLettersIgnoringASCIICase(hasPopup, "listbox" ) |
2765 | || equalLettersIgnoringASCIICase(hasPopup, "menu" ) |
2766 | || equalLettersIgnoringASCIICase(hasPopup, "tree" )) |
2767 | return hasPopup; |
2768 | |
2769 | // In ARIA 1.1, the implicit value for combobox became "listbox." |
2770 | if (isComboBox() && hasPopup.isEmpty()) |
2771 | return "listbox" ; |
2772 | |
2773 | // The spec states that "User agents must treat any value of aria-haspopup that is not |
2774 | // included in the list of allowed values, including an empty string, as if the value |
2775 | // false had been provided." |
2776 | return "false" ; |
2777 | } |
2778 | |
2779 | bool AccessibilityObject::supportsSetSize() const |
2780 | { |
2781 | return hasAttribute(aria_setsizeAttr); |
2782 | } |
2783 | |
2784 | bool AccessibilityObject::supportsPosInSet() const |
2785 | { |
2786 | return hasAttribute(aria_posinsetAttr); |
2787 | } |
2788 | |
2789 | int AccessibilityObject::setSize() const |
2790 | { |
2791 | return getAttribute(aria_setsizeAttr).toInt(); |
2792 | } |
2793 | |
2794 | int AccessibilityObject::posInSet() const |
2795 | { |
2796 | return getAttribute(aria_posinsetAttr).toInt(); |
2797 | } |
2798 | |
2799 | const AtomicString& AccessibilityObject::identifierAttribute() const |
2800 | { |
2801 | return getAttribute(idAttr); |
2802 | } |
2803 | |
2804 | void AccessibilityObject::classList(Vector<String>& classList) const |
2805 | { |
2806 | Node* node = this->node(); |
2807 | if (!is<Element>(node)) |
2808 | return; |
2809 | |
2810 | Element* element = downcast<Element>(node); |
2811 | DOMTokenList& list = element->classList(); |
2812 | unsigned length = list.length(); |
2813 | for (unsigned k = 0; k < length; k++) |
2814 | classList.append(list.item(k).string()); |
2815 | } |
2816 | |
2817 | bool AccessibilityObject::supportsPressed() const |
2818 | { |
2819 | const AtomicString& expanded = getAttribute(aria_pressedAttr); |
2820 | return equalLettersIgnoringASCIICase(expanded, "true" ) || equalLettersIgnoringASCIICase(expanded, "false" ); |
2821 | } |
2822 | |
2823 | bool AccessibilityObject::supportsExpanded() const |
2824 | { |
2825 | // Undefined values should not result in this attribute being exposed to ATs according to ARIA. |
2826 | const AtomicString& expanded = getAttribute(aria_expandedAttr); |
2827 | if (equalLettersIgnoringASCIICase(expanded, "true" ) || equalLettersIgnoringASCIICase(expanded, "false" )) |
2828 | return true; |
2829 | switch (roleValue()) { |
2830 | case AccessibilityRole::ComboBox: |
2831 | case AccessibilityRole::DisclosureTriangle: |
2832 | case AccessibilityRole::Details: |
2833 | return true; |
2834 | default: |
2835 | return false; |
2836 | } |
2837 | } |
2838 | |
2839 | bool AccessibilityObject::isExpanded() const |
2840 | { |
2841 | if (equalLettersIgnoringASCIICase(getAttribute(aria_expandedAttr), "true" )) |
2842 | return true; |
2843 | |
2844 | if (is<HTMLDetailsElement>(node())) |
2845 | return downcast<HTMLDetailsElement>(node())->isOpen(); |
2846 | |
2847 | // Summary element should use its details parent's expanded status. |
2848 | if (isSummary()) { |
2849 | if (const AccessibilityObject* parent = AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) { |
2850 | return is<HTMLDetailsElement>(object.node()); |
2851 | })) |
2852 | return parent->isExpanded(); |
2853 | } |
2854 | |
2855 | return false; |
2856 | } |
2857 | |
2858 | bool AccessibilityObject::supportsChecked() const |
2859 | { |
2860 | switch (roleValue()) { |
2861 | case AccessibilityRole::CheckBox: |
2862 | case AccessibilityRole::MenuItemCheckbox: |
2863 | case AccessibilityRole::MenuItemRadio: |
2864 | case AccessibilityRole::RadioButton: |
2865 | case AccessibilityRole::Switch: |
2866 | return true; |
2867 | default: |
2868 | return false; |
2869 | } |
2870 | } |
2871 | |
2872 | AccessibilityButtonState AccessibilityObject::checkboxOrRadioValue() const |
2873 | { |
2874 | // If this is a real checkbox or radio button, AccessibilityRenderObject will handle. |
2875 | // If it's an ARIA checkbox, radio, or switch the aria-checked attribute should be used. |
2876 | // If it's a toggle button, the aria-pressed attribute is consulted. |
2877 | |
2878 | if (isToggleButton()) { |
2879 | const AtomicString& ariaPressed = getAttribute(aria_pressedAttr); |
2880 | if (equalLettersIgnoringASCIICase(ariaPressed, "true" )) |
2881 | return AccessibilityButtonState::On; |
2882 | if (equalLettersIgnoringASCIICase(ariaPressed, "mixed" )) |
2883 | return AccessibilityButtonState::Mixed; |
2884 | return AccessibilityButtonState::Off; |
2885 | } |
2886 | |
2887 | const AtomicString& result = getAttribute(aria_checkedAttr); |
2888 | if (equalLettersIgnoringASCIICase(result, "true" )) |
2889 | return AccessibilityButtonState::On; |
2890 | if (equalLettersIgnoringASCIICase(result, "mixed" )) { |
2891 | // ARIA says that radio, menuitemradio, and switch elements must NOT expose button state mixed. |
2892 | AccessibilityRole ariaRole = ariaRoleAttribute(); |
2893 | if (ariaRole == AccessibilityRole::RadioButton || ariaRole == AccessibilityRole::MenuItemRadio || ariaRole == AccessibilityRole::Switch) |
2894 | return AccessibilityButtonState::Off; |
2895 | return AccessibilityButtonState::Mixed; |
2896 | } |
2897 | |
2898 | if (isIndeterminate()) |
2899 | return AccessibilityButtonState::Mixed; |
2900 | |
2901 | return AccessibilityButtonState::Off; |
2902 | } |
2903 | |
2904 | // This is a 1-dimensional scroll offset helper function that's applied |
2905 | // separately in the horizontal and vertical directions, because the |
2906 | // logic is the same. The goal is to compute the best scroll offset |
2907 | // in order to make an object visible within a viewport. |
2908 | // |
2909 | // If the object is already fully visible, returns the same scroll |
2910 | // offset. |
2911 | // |
2912 | // In case the whole object cannot fit, you can specify a |
2913 | // subfocus - a smaller region within the object that should |
2914 | // be prioritized. If the whole object can fit, the subfocus is |
2915 | // ignored. |
2916 | // |
2917 | // If possible, the object and subfocus are centered within the |
2918 | // viewport. |
2919 | // |
2920 | // Example 1: the object is already visible, so nothing happens. |
2921 | // +----------Viewport---------+ |
2922 | // +---Object---+ |
2923 | // +--SubFocus--+ |
2924 | // |
2925 | // Example 2: the object is not fully visible, so it's centered |
2926 | // within the viewport. |
2927 | // Before: |
2928 | // +----------Viewport---------+ |
2929 | // +---Object---+ |
2930 | // +--SubFocus--+ |
2931 | // |
2932 | // After: |
2933 | // +----------Viewport---------+ |
2934 | // +---Object---+ |
2935 | // +--SubFocus--+ |
2936 | // |
2937 | // Example 3: the object is larger than the viewport, so the |
2938 | // viewport moves to show as much of the object as possible, |
2939 | // while also trying to center the subfocus. |
2940 | // Before: |
2941 | // +----------Viewport---------+ |
2942 | // +---------------Object--------------+ |
2943 | // +-SubFocus-+ |
2944 | // |
2945 | // After: |
2946 | // +----------Viewport---------+ |
2947 | // +---------------Object--------------+ |
2948 | // +-SubFocus-+ |
2949 | // |
2950 | // When constraints cannot be fully satisfied, the min |
2951 | // (left/top) position takes precedence over the max (right/bottom). |
2952 | // |
2953 | // Note that the return value represents the ideal new scroll offset. |
2954 | // This may be out of range - the calling function should clip this |
2955 | // to the available range. |
2956 | static int computeBestScrollOffset(int currentScrollOffset, int subfocusMin, int subfocusMax, int objectMin, int objectMax, int viewportMin, int viewportMax) |
2957 | { |
2958 | int viewportSize = viewportMax - viewportMin; |
2959 | |
2960 | // If the object size is larger than the viewport size, consider |
2961 | // only a portion that's as large as the viewport, centering on |
2962 | // the subfocus as much as possible. |
2963 | if (objectMax - objectMin > viewportSize) { |
2964 | // Since it's impossible to fit the whole object in the |
2965 | // viewport, exit now if the subfocus is already within the viewport. |
2966 | if (subfocusMin - currentScrollOffset >= viewportMin && subfocusMax - currentScrollOffset <= viewportMax) |
2967 | return currentScrollOffset; |
2968 | |
2969 | // Subfocus must be within focus. |
2970 | subfocusMin = std::max(subfocusMin, objectMin); |
2971 | subfocusMax = std::min(subfocusMax, objectMax); |
2972 | |
2973 | // Subfocus must be no larger than the viewport size; favor top/left. |
2974 | if (subfocusMax - subfocusMin > viewportSize) |
2975 | subfocusMax = subfocusMin + viewportSize; |
2976 | |
2977 | // Compute the size of an object centered on the subfocus, the size of the viewport. |
2978 | int centeredObjectMin = (subfocusMin + subfocusMax - viewportSize) / 2; |
2979 | int centeredObjectMax = centeredObjectMin + viewportSize; |
2980 | |
2981 | objectMin = std::max(objectMin, centeredObjectMin); |
2982 | objectMax = std::min(objectMax, centeredObjectMax); |
2983 | } |
2984 | |
2985 | // Exit now if the focus is already within the viewport. |
2986 | if (objectMin - currentScrollOffset >= viewportMin |
2987 | && objectMax - currentScrollOffset <= viewportMax) |
2988 | return currentScrollOffset; |
2989 | |
2990 | // Center the object in the viewport. |
2991 | return (objectMin + objectMax - viewportMin - viewportMax) / 2; |
2992 | } |
2993 | |
2994 | bool AccessibilityObject::isOnscreen() const |
2995 | { |
2996 | bool isOnscreen = true; |
2997 | |
2998 | // To figure out if the element is onscreen, we start by building of a stack starting with the |
2999 | // element, and then include every scrollable parent in the hierarchy. |
3000 | Vector<const AccessibilityObject*> objects; |
3001 | |
3002 | objects.append(this); |
3003 | for (AccessibilityObject* parentObject = this->parentObject(); parentObject; parentObject = parentObject->parentObject()) { |
3004 | if (parentObject->getScrollableAreaIfScrollable()) |
3005 | objects.append(parentObject); |
3006 | } |
3007 | |
3008 | // Now, go back through that chain and make sure each inner object is within the |
3009 | // visible bounds of the outer object. |
3010 | size_t levels = objects.size() - 1; |
3011 | |
3012 | for (size_t i = levels; i >= 1; i--) { |
3013 | const AccessibilityObject* outer = objects[i]; |
3014 | const AccessibilityObject* inner = objects[i - 1]; |
3015 | // FIXME: unclear if we need LegacyIOSDocumentVisibleRect. |
3016 | const IntRect outerRect = i < levels ? snappedIntRect(outer->boundingBoxRect()) : outer->getScrollableAreaIfScrollable()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); |
3017 | const IntRect innerRect = snappedIntRect(inner->isAccessibilityScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect()); |
3018 | |
3019 | if (!outerRect.intersects(innerRect)) { |
3020 | isOnscreen = false; |
3021 | break; |
3022 | } |
3023 | } |
3024 | |
3025 | return isOnscreen; |
3026 | } |
3027 | |
3028 | void AccessibilityObject::scrollToMakeVisible() const |
3029 | { |
3030 | scrollToMakeVisible({ SelectionRevealMode::Reveal, ScrollAlignment::alignCenterIfNeeded, ScrollAlignment::alignCenterIfNeeded, ShouldAllowCrossOriginScrolling::Yes }); |
3031 | } |
3032 | |
3033 | void AccessibilityObject::scrollToMakeVisible(const ScrollRectToVisibleOptions& options) const |
3034 | { |
3035 | if (isScrollView() && parentObject()) |
3036 | parentObject()->scrollToMakeVisible(); |
3037 | |
3038 | if (auto* renderer = this->renderer()) |
3039 | renderer->scrollRectToVisible(boundingBoxRect(), false, options); |
3040 | } |
3041 | |
3042 | void AccessibilityObject::scrollToMakeVisibleWithSubFocus(const IntRect& subfocus) const |
3043 | { |
3044 | // Search up the parent chain until we find the first one that's scrollable. |
3045 | AccessibilityObject* scrollParent = parentObject(); |
3046 | ScrollableArea* scrollableArea; |
3047 | for (scrollableArea = nullptr; |
3048 | scrollParent && !(scrollableArea = scrollParent->getScrollableAreaIfScrollable()); |
3049 | scrollParent = scrollParent->parentObject()) { } |
3050 | if (!scrollableArea) |
3051 | return; |
3052 | |
3053 | LayoutRect objectRect = boundingBoxRect(); |
3054 | IntPoint scrollPosition = scrollableArea->scrollPosition(); |
3055 | // FIXME: unclear if we need LegacyIOSDocumentVisibleRect. |
3056 | IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); |
3057 | |
3058 | if (!scrollParent->isScrollView()) { |
3059 | objectRect.moveBy(scrollPosition); |
3060 | objectRect.moveBy(-snappedIntRect(scrollParent->elementRect()).location()); |
3061 | } |
3062 | |
3063 | int desiredX = computeBestScrollOffset( |
3064 | scrollPosition.x(), |
3065 | objectRect.x() + subfocus.x(), objectRect.x() + subfocus.maxX(), |
3066 | objectRect.x(), objectRect.maxX(), |
3067 | 0, scrollVisibleRect.width()); |
3068 | int desiredY = computeBestScrollOffset( |
3069 | scrollPosition.y(), |
3070 | objectRect.y() + subfocus.y(), objectRect.y() + subfocus.maxY(), |
3071 | objectRect.y(), objectRect.maxY(), |
3072 | 0, scrollVisibleRect.height()); |
3073 | |
3074 | scrollParent->scrollTo(IntPoint(desiredX, desiredY)); |
3075 | |
3076 | // Convert the subfocus into the coordinates of the scroll parent. |
3077 | IntRect newSubfocus = subfocus; |
3078 | IntRect newElementRect = snappedIntRect(elementRect()); |
3079 | IntRect scrollParentRect = snappedIntRect(scrollParent->elementRect()); |
3080 | newSubfocus.move(newElementRect.x(), newElementRect.y()); |
3081 | newSubfocus.move(-scrollParentRect.x(), -scrollParentRect.y()); |
3082 | |
3083 | // Recursively make sure the scroll parent itself is visible. |
3084 | if (scrollParent->parentObject()) |
3085 | scrollParent->scrollToMakeVisibleWithSubFocus(newSubfocus); |
3086 | } |
3087 | |
3088 | void AccessibilityObject::scrollToGlobalPoint(const IntPoint& globalPoint) const |
3089 | { |
3090 | // Search up the parent chain and create a vector of all scrollable parent objects |
3091 | // and ending with this object itself. |
3092 | Vector<const AccessibilityObject*> objects; |
3093 | |
3094 | objects.append(this); |
3095 | for (AccessibilityObject* parentObject = this->parentObject(); parentObject; parentObject = parentObject->parentObject()) { |
3096 | if (parentObject->getScrollableAreaIfScrollable()) |
3097 | objects.append(parentObject); |
3098 | } |
3099 | |
3100 | objects.reverse(); |
3101 | |
3102 | // Start with the outermost scrollable (the main window) and try to scroll the |
3103 | // next innermost object to the given point. |
3104 | int offsetX = 0, offsetY = 0; |
3105 | IntPoint point = globalPoint; |
3106 | size_t levels = objects.size() - 1; |
3107 | for (size_t i = 0; i < levels; i++) { |
3108 | const AccessibilityObject* outer = objects[i]; |
3109 | const AccessibilityObject* inner = objects[i + 1]; |
3110 | |
3111 | ScrollableArea* scrollableArea = outer->getScrollableAreaIfScrollable(); |
3112 | |
3113 | LayoutRect innerRect = inner->isAccessibilityScrollView() ? inner->parentObject()->boundingBoxRect() : inner->boundingBoxRect(); |
3114 | LayoutRect objectRect = innerRect; |
3115 | IntPoint scrollPosition = scrollableArea->scrollPosition(); |
3116 | |
3117 | // Convert the object rect into local coordinates. |
3118 | objectRect.move(offsetX, offsetY); |
3119 | if (!outer->isAccessibilityScrollView()) |
3120 | objectRect.move(scrollPosition.x(), scrollPosition.y()); |
3121 | |
3122 | int desiredX = computeBestScrollOffset( |
3123 | 0, |
3124 | objectRect.x(), objectRect.maxX(), |
3125 | objectRect.x(), objectRect.maxX(), |
3126 | point.x(), point.x()); |
3127 | int desiredY = computeBestScrollOffset( |
3128 | 0, |
3129 | objectRect.y(), objectRect.maxY(), |
3130 | objectRect.y(), objectRect.maxY(), |
3131 | point.y(), point.y()); |
3132 | outer->scrollTo(IntPoint(desiredX, desiredY)); |
3133 | |
3134 | if (outer->isAccessibilityScrollView() && !inner->isAccessibilityScrollView()) { |
3135 | // If outer object we just scrolled is a scroll view (main window or iframe) but the |
3136 | // inner object is not, keep track of the coordinate transformation to apply to |
3137 | // future nested calculations. |
3138 | scrollPosition = scrollableArea->scrollPosition(); |
3139 | offsetX -= (scrollPosition.x() + point.x()); |
3140 | offsetY -= (scrollPosition.y() + point.y()); |
3141 | point.move(scrollPosition.x() - innerRect.x(), |
3142 | scrollPosition.y() - innerRect.y()); |
3143 | } else if (inner->isAccessibilityScrollView()) { |
3144 | // Otherwise, if the inner object is a scroll view, reset the coordinate transformation. |
3145 | offsetX = 0; |
3146 | offsetY = 0; |
3147 | } |
3148 | } |
3149 | } |
3150 | |
3151 | void AccessibilityObject::scrollAreaAndAncestor(std::pair<ScrollableArea*, AccessibilityObject*>& scrollers) const |
3152 | { |
3153 | // Search up the parent chain until we find the first one that's scrollable. |
3154 | scrollers.first = nullptr; |
3155 | for (scrollers.second = parentObject(); scrollers.second; scrollers.second = scrollers.second->parentObject()) { |
3156 | if ((scrollers.first = scrollers.second->getScrollableAreaIfScrollable())) |
3157 | break; |
3158 | } |
3159 | } |
3160 | |
3161 | ScrollableArea* AccessibilityObject::scrollableAreaAncestor() const |
3162 | { |
3163 | std::pair<ScrollableArea*, AccessibilityObject*> scrollers; |
3164 | scrollAreaAndAncestor(scrollers); |
3165 | return scrollers.first; |
3166 | } |
3167 | |
3168 | IntPoint AccessibilityObject::scrollPosition() const |
3169 | { |
3170 | if (auto scroller = scrollableAreaAncestor()) |
3171 | return scroller->scrollPosition(); |
3172 | |
3173 | return IntPoint(); |
3174 | } |
3175 | |
3176 | IntRect AccessibilityObject::scrollVisibleContentRect() const |
3177 | { |
3178 | if (auto scroller = scrollableAreaAncestor()) |
3179 | return scroller->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); |
3180 | |
3181 | return IntRect(); |
3182 | } |
3183 | |
3184 | IntSize AccessibilityObject::scrollContentsSize() const |
3185 | { |
3186 | if (auto scroller = scrollableAreaAncestor()) |
3187 | return scroller->contentsSize(); |
3188 | |
3189 | return IntSize(); |
3190 | } |
3191 | |
3192 | bool AccessibilityObject::scrollByPage(ScrollByPageDirection direction) const |
3193 | { |
3194 | std::pair<ScrollableArea*, AccessibilityObject*> scrollers; |
3195 | scrollAreaAndAncestor(scrollers); |
3196 | ScrollableArea* scrollableArea = scrollers.first; |
3197 | AccessibilityObject* scrollParent = scrollers.second; |
3198 | |
3199 | if (!scrollableArea) |
3200 | return false; |
3201 | |
3202 | IntPoint scrollPosition = scrollableArea->scrollPosition(); |
3203 | IntPoint newScrollPosition = scrollPosition; |
3204 | IntSize scrollSize = scrollableArea->contentsSize(); |
3205 | IntRect scrollVisibleRect = scrollableArea->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect); |
3206 | switch (direction) { |
3207 | case ScrollByPageDirection::Right: { |
3208 | int scrollAmount = scrollVisibleRect.size().width(); |
3209 | int newX = scrollPosition.x() - scrollAmount; |
3210 | newScrollPosition.setX(std::max(newX, 0)); |
3211 | break; |
3212 | } |
3213 | case ScrollByPageDirection::Left: { |
3214 | int scrollAmount = scrollVisibleRect.size().width(); |
3215 | int newX = scrollAmount + scrollPosition.x(); |
3216 | int maxX = scrollSize.width() - scrollAmount; |
3217 | newScrollPosition.setX(std::min(newX, maxX)); |
3218 | break; |
3219 | } |
3220 | case ScrollByPageDirection::Up: { |
3221 | int scrollAmount = scrollVisibleRect.size().height(); |
3222 | int newY = scrollPosition.y() - scrollAmount; |
3223 | newScrollPosition.setY(std::max(newY, 0)); |
3224 | break; |
3225 | } |
3226 | case ScrollByPageDirection::Down: { |
3227 | int scrollAmount = scrollVisibleRect.size().height(); |
3228 | int newY = scrollAmount + scrollPosition.y(); |
3229 | int maxY = scrollSize.height() - scrollAmount; |
3230 | newScrollPosition.setY(std::min(newY, maxY)); |
3231 | break; |
3232 | } |
3233 | } |
3234 | |
3235 | if (newScrollPosition != scrollPosition) { |
3236 | scrollParent->scrollTo(newScrollPosition); |
3237 | document()->updateLayoutIgnorePendingStylesheets(); |
3238 | return true; |
3239 | } |
3240 | |
3241 | return false; |
3242 | } |
3243 | |
3244 | |
3245 | bool AccessibilityObject::lastKnownIsIgnoredValue() |
3246 | { |
3247 | if (m_lastKnownIsIgnoredValue == AccessibilityObjectInclusion::DefaultBehavior) |
3248 | m_lastKnownIsIgnoredValue = accessibilityIsIgnored() ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject; |
3249 | |
3250 | return m_lastKnownIsIgnoredValue == AccessibilityObjectInclusion::IgnoreObject; |
3251 | } |
3252 | |
3253 | void AccessibilityObject::setLastKnownIsIgnoredValue(bool isIgnored) |
3254 | { |
3255 | m_lastKnownIsIgnoredValue = isIgnored ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject; |
3256 | } |
3257 | |
3258 | void AccessibilityObject::notifyIfIgnoredValueChanged() |
3259 | { |
3260 | bool isIgnored = accessibilityIsIgnored(); |
3261 | if (lastKnownIsIgnoredValue() != isIgnored) { |
3262 | if (AXObjectCache* cache = axObjectCache()) |
3263 | cache->childrenChanged(parentObject()); |
3264 | setLastKnownIsIgnoredValue(isIgnored); |
3265 | } |
3266 | } |
3267 | |
3268 | bool AccessibilityObject::pressedIsPresent() const |
3269 | { |
3270 | return !getAttribute(aria_pressedAttr).isEmpty(); |
3271 | } |
3272 | |
3273 | TextIteratorBehavior AccessibilityObject::() const |
3274 | { |
3275 | TextIteratorBehavior behavior = TextIteratorIgnoresStyleVisibility; |
3276 | |
3277 | #if PLATFORM(GTK) |
3278 | // We need to emit replaced elements for GTK, and present |
3279 | // them with the 'object replacement character' (0xFFFC). |
3280 | behavior = static_cast<TextIteratorBehavior>(behavior | TextIteratorEmitsObjectReplacementCharacters); |
3281 | #endif |
3282 | |
3283 | return behavior; |
3284 | } |
3285 | |
3286 | AccessibilityRole AccessibilityObject::buttonRoleType() const |
3287 | { |
3288 | // If aria-pressed is present, then it should be exposed as a toggle button. |
3289 | // http://www.w3.org/TR/wai-aria/states_and_properties#aria-pressed |
3290 | if (pressedIsPresent()) |
3291 | return AccessibilityRole::ToggleButton; |
3292 | if (hasPopup()) |
3293 | return AccessibilityRole::PopUpButton; |
3294 | // We don't contemplate AccessibilityRole::RadioButton, as it depends on the input |
3295 | // type. |
3296 | |
3297 | return AccessibilityRole::Button; |
3298 | } |
3299 | |
3300 | bool AccessibilityObject::isButton() const |
3301 | { |
3302 | AccessibilityRole role = roleValue(); |
3303 | |
3304 | return role == AccessibilityRole::Button || role == AccessibilityRole::PopUpButton || role == AccessibilityRole::ToggleButton; |
3305 | } |
3306 | |
3307 | bool AccessibilityObject::accessibilityIsIgnoredByDefault() const |
3308 | { |
3309 | return defaultObjectInclusion() == AccessibilityObjectInclusion::IgnoreObject; |
3310 | } |
3311 | |
3312 | // ARIA component of hidden definition. |
3313 | // http://www.w3.org/TR/wai-aria/terms#def_hidden |
3314 | bool AccessibilityObject::isAXHidden() const |
3315 | { |
3316 | return AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) { |
3317 | return equalLettersIgnoringASCIICase(object.getAttribute(aria_hiddenAttr), "true" ); |
3318 | }) != nullptr; |
3319 | } |
3320 | |
3321 | // DOM component of hidden definition. |
3322 | // http://www.w3.org/TR/wai-aria/terms#def_hidden |
3323 | bool AccessibilityObject::isDOMHidden() const |
3324 | { |
3325 | RenderObject* renderer = this->renderer(); |
3326 | if (!renderer) |
3327 | return true; |
3328 | |
3329 | const RenderStyle& style = renderer->style(); |
3330 | return style.display() == DisplayType::None || style.visibility() != Visibility::Visible; |
3331 | } |
3332 | |
3333 | bool AccessibilityObject::isShowingValidationMessage() const |
3334 | { |
3335 | if (is<HTMLFormControlElement>(node())) |
3336 | return downcast<HTMLFormControlElement>(*node()).isShowingValidationMessage(); |
3337 | return false; |
3338 | } |
3339 | |
3340 | String AccessibilityObject::validationMessage() const |
3341 | { |
3342 | if (is<HTMLFormControlElement>(node())) |
3343 | return downcast<HTMLFormControlElement>(*node()).validationMessage(); |
3344 | return String(); |
3345 | } |
3346 | |
3347 | AccessibilityObjectInclusion AccessibilityObject::defaultObjectInclusion() const |
3348 | { |
3349 | bool useParentData = !m_isIgnoredFromParentData.isNull(); |
3350 | |
3351 | if (useParentData ? m_isIgnoredFromParentData.isAXHidden : isAXHidden()) |
3352 | return AccessibilityObjectInclusion::IgnoreObject; |
3353 | |
3354 | if (ignoredFromModalPresence()) |
3355 | return AccessibilityObjectInclusion::IgnoreObject; |
3356 | |
3357 | if (useParentData ? m_isIgnoredFromParentData.isPresentationalChildOfAriaRole : isPresentationalChildOfAriaRole()) |
3358 | return AccessibilityObjectInclusion::IgnoreObject; |
3359 | |
3360 | return accessibilityPlatformIncludesObject(); |
3361 | } |
3362 | |
3363 | bool AccessibilityObject::accessibilityIsIgnored() const |
3364 | { |
3365 | AXComputedObjectAttributeCache* attributeCache = nullptr; |
3366 | AXObjectCache* cache = axObjectCache(); |
3367 | if (cache) |
3368 | attributeCache = cache->computedObjectAttributeCache(); |
3369 | |
3370 | if (attributeCache) { |
3371 | AccessibilityObjectInclusion ignored = attributeCache->getIgnored(axObjectID()); |
3372 | switch (ignored) { |
3373 | case AccessibilityObjectInclusion::IgnoreObject: |
3374 | return true; |
3375 | case AccessibilityObjectInclusion::IncludeObject: |
3376 | return false; |
3377 | case AccessibilityObjectInclusion::DefaultBehavior: |
3378 | break; |
3379 | } |
3380 | } |
3381 | |
3382 | bool result = computeAccessibilityIsIgnored(); |
3383 | |
3384 | // In case computing axIsIgnored disables attribute caching, we should refetch the object to see if it exists. |
3385 | if (cache && (attributeCache = cache->computedObjectAttributeCache())) |
3386 | attributeCache->setIgnored(axObjectID(), result ? AccessibilityObjectInclusion::IgnoreObject : AccessibilityObjectInclusion::IncludeObject); |
3387 | |
3388 | return result; |
3389 | } |
3390 | |
3391 | void AccessibilityObject::elementsFromAttribute(Vector<Element*>& elements, const QualifiedName& attribute) const |
3392 | { |
3393 | Node* node = this->node(); |
3394 | if (!node || !node->isElementNode()) |
3395 | return; |
3396 | |
3397 | TreeScope& treeScope = node->treeScope(); |
3398 | |
3399 | const AtomicString& idList = getAttribute(attribute); |
3400 | if (idList.isEmpty()) |
3401 | return; |
3402 | |
3403 | auto spaceSplitString = SpaceSplitString(idList, false); |
3404 | size_t length = spaceSplitString.size(); |
3405 | for (size_t i = 0; i < length; ++i) { |
3406 | if (auto* idElement = treeScope.getElementById(spaceSplitString[i])) |
3407 | elements.append(idElement); |
3408 | } |
3409 | } |
3410 | |
3411 | #if PLATFORM(COCOA) |
3412 | bool AccessibilityObject::preventKeyboardDOMEventDispatch() const |
3413 | { |
3414 | Frame* frame = this->frame(); |
3415 | return frame && frame->settings().preventKeyboardDOMEventDispatch(); |
3416 | } |
3417 | |
3418 | void AccessibilityObject::setPreventKeyboardDOMEventDispatch(bool on) |
3419 | { |
3420 | Frame* frame = this->frame(); |
3421 | if (!frame) |
3422 | return; |
3423 | frame->settings().setPreventKeyboardDOMEventDispatch(on); |
3424 | } |
3425 | #endif |
3426 | |
3427 | AccessibilityObject* AccessibilityObject::focusableAncestor() |
3428 | { |
3429 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) { |
3430 | return object.canSetFocusAttribute(); |
3431 | })); |
3432 | } |
3433 | |
3434 | AccessibilityObject* AccessibilityObject::editableAncestor() |
3435 | { |
3436 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, true, [] (const AccessibilityObject& object) { |
3437 | return object.isTextControl(); |
3438 | })); |
3439 | } |
3440 | |
3441 | AccessibilityObject* AccessibilityObject::highestEditableAncestor() |
3442 | { |
3443 | AccessibilityObject* editableAncestor = this->editableAncestor(); |
3444 | AccessibilityObject* previousEditableAncestor = nullptr; |
3445 | while (editableAncestor) { |
3446 | if (editableAncestor == previousEditableAncestor) { |
3447 | if (AccessibilityObject* parent = editableAncestor->parentObject()) { |
3448 | editableAncestor = parent->editableAncestor(); |
3449 | continue; |
3450 | } |
3451 | break; |
3452 | } |
3453 | previousEditableAncestor = editableAncestor; |
3454 | editableAncestor = editableAncestor->editableAncestor(); |
3455 | } |
3456 | return previousEditableAncestor; |
3457 | } |
3458 | |
3459 | AccessibilityObject* AccessibilityObject::radioGroupAncestor() const |
3460 | { |
3461 | return const_cast<AccessibilityObject*>(AccessibilityObject::matchedParent(*this, false, [] (const AccessibilityObject& object) { |
3462 | return object.isRadioGroup(); |
3463 | })); |
3464 | } |
3465 | |
3466 | bool AccessibilityObject::isStyleFormatGroup() const |
3467 | { |
3468 | Node* node = this->node(); |
3469 | if (!node) |
3470 | return false; |
3471 | |
3472 | return node->hasTagName(kbdTag) || node->hasTagName(codeTag) |
3473 | || node->hasTagName(preTag) || node->hasTagName(sampTag) |
3474 | || node->hasTagName(varTag) || node->hasTagName(citeTag) |
3475 | || node->hasTagName(insTag) || node->hasTagName(delTag) |
3476 | || node->hasTagName(supTag) || node->hasTagName(subTag); |
3477 | } |
3478 | |
3479 | bool AccessibilityObject::isSubscriptStyleGroup() const |
3480 | { |
3481 | Node* node = this->node(); |
3482 | return node && node->hasTagName(subTag); |
3483 | } |
3484 | |
3485 | bool AccessibilityObject::isSuperscriptStyleGroup() const |
3486 | { |
3487 | Node* node = this->node(); |
3488 | return node && node->hasTagName(supTag); |
3489 | } |
3490 | |
3491 | bool AccessibilityObject::isFigureElement() const |
3492 | { |
3493 | Node* node = this->node(); |
3494 | return node && node->hasTagName(figureTag); |
3495 | } |
3496 | |
3497 | bool AccessibilityObject::isKeyboardFocusable() const |
3498 | { |
3499 | if (auto element = this->element()) |
3500 | return element->isFocusable(); |
3501 | return false; |
3502 | } |
3503 | |
3504 | bool AccessibilityObject::isOutput() const |
3505 | { |
3506 | Node* node = this->node(); |
3507 | return node && node->hasTagName(outputTag); |
3508 | } |
3509 | |
3510 | bool AccessibilityObject::isContainedByPasswordField() const |
3511 | { |
3512 | Node* node = this->node(); |
3513 | if (!node) |
3514 | return false; |
3515 | |
3516 | if (ariaRoleAttribute() != AccessibilityRole::Unknown) |
3517 | return false; |
3518 | |
3519 | Element* element = node->shadowHost(); |
3520 | return is<HTMLInputElement>(element) && downcast<HTMLInputElement>(*element).isPasswordField(); |
3521 | } |
3522 | |
3523 | AccessibilityObject* AccessibilityObject::selectedListItem() |
3524 | { |
3525 | for (const auto& child : children()) { |
3526 | if (child->isListItem() && (child->isSelected() || child->isActiveDescendantOfFocusedContainer())) |
3527 | return child.get(); |
3528 | } |
3529 | |
3530 | return nullptr; |
3531 | } |
3532 | |
3533 | void AccessibilityObject::ariaElementsFromAttribute(AccessibilityChildrenVector& children, const QualifiedName& attributeName) const |
3534 | { |
3535 | Vector<Element*> elements; |
3536 | elementsFromAttribute(elements, attributeName); |
3537 | AXObjectCache* cache = axObjectCache(); |
3538 | for (const auto& element : elements) { |
3539 | if (AccessibilityObject* axObject = cache->getOrCreate(element)) |
3540 | children.append(axObject); |
3541 | } |
3542 | } |
3543 | |
3544 | void AccessibilityObject::ariaElementsReferencedByAttribute(AccessibilityChildrenVector& elements, const QualifiedName& attribute) const |
3545 | { |
3546 | auto id = identifierAttribute(); |
3547 | if (id.isEmpty()) |
3548 | return; |
3549 | |
3550 | AXObjectCache* cache = axObjectCache(); |
3551 | if (!cache) |
3552 | return; |
3553 | |
3554 | for (auto& element : descendantsOfType<Element>(node()->treeScope().rootNode())) { |
3555 | const AtomicString& idList = element.attributeWithoutSynchronization(attribute); |
3556 | if (!SpaceSplitString(idList, false).contains(id)) |
3557 | continue; |
3558 | |
3559 | if (AccessibilityObject* axObject = cache->getOrCreate(&element)) |
3560 | elements.append(axObject); |
3561 | } |
3562 | } |
3563 | |
3564 | bool AccessibilityObject::isActiveDescendantOfFocusedContainer() const |
3565 | { |
3566 | AccessibilityChildrenVector containers; |
3567 | ariaActiveDescendantReferencingElements(containers); |
3568 | for (auto& container : containers) { |
3569 | if (container->isFocused()) |
3570 | return true; |
3571 | } |
3572 | |
3573 | return false; |
3574 | } |
3575 | |
3576 | void AccessibilityObject::ariaActiveDescendantReferencingElements(AccessibilityChildrenVector& containers) const |
3577 | { |
3578 | ariaElementsReferencedByAttribute(containers, aria_activedescendantAttr); |
3579 | } |
3580 | |
3581 | void AccessibilityObject::ariaControlsElements(AccessibilityChildrenVector& ariaControls) const |
3582 | { |
3583 | ariaElementsFromAttribute(ariaControls, aria_controlsAttr); |
3584 | } |
3585 | |
3586 | void AccessibilityObject::ariaControlsReferencingElements(AccessibilityChildrenVector& controllers) const |
3587 | { |
3588 | ariaElementsReferencedByAttribute(controllers, aria_controlsAttr); |
3589 | } |
3590 | |
3591 | void AccessibilityObject::ariaDescribedByElements(AccessibilityChildrenVector& ariaDescribedBy) const |
3592 | { |
3593 | ariaElementsFromAttribute(ariaDescribedBy, aria_describedbyAttr); |
3594 | } |
3595 | |
3596 | void AccessibilityObject::ariaDescribedByReferencingElements(AccessibilityChildrenVector& describers) const |
3597 | { |
3598 | ariaElementsReferencedByAttribute(describers, aria_describedbyAttr); |
3599 | } |
3600 | |
3601 | void AccessibilityObject::ariaDetailsElements(AccessibilityChildrenVector& ariaDetails) const |
3602 | { |
3603 | ariaElementsFromAttribute(ariaDetails, aria_detailsAttr); |
3604 | } |
3605 | |
3606 | void AccessibilityObject::ariaDetailsReferencingElements(AccessibilityChildrenVector& detailsFor) const |
3607 | { |
3608 | ariaElementsReferencedByAttribute(detailsFor, aria_detailsAttr); |
3609 | } |
3610 | |
3611 | void AccessibilityObject::ariaErrorMessageElements(AccessibilityChildrenVector& ariaErrorMessage) const |
3612 | { |
3613 | ariaElementsFromAttribute(ariaErrorMessage, aria_errormessageAttr); |
3614 | } |
3615 | |
3616 | void AccessibilityObject::ariaErrorMessageReferencingElements(AccessibilityChildrenVector& errorMessageFor) const |
3617 | { |
3618 | ariaElementsReferencedByAttribute(errorMessageFor, aria_errormessageAttr); |
3619 | } |
3620 | |
3621 | void AccessibilityObject::ariaFlowToElements(AccessibilityChildrenVector& flowTo) const |
3622 | { |
3623 | ariaElementsFromAttribute(flowTo, aria_flowtoAttr); |
3624 | } |
3625 | |
3626 | void AccessibilityObject::ariaFlowToReferencingElements(AccessibilityChildrenVector& flowFrom) const |
3627 | { |
3628 | ariaElementsReferencedByAttribute(flowFrom, aria_flowtoAttr); |
3629 | } |
3630 | |
3631 | void AccessibilityObject::ariaLabelledByElements(AccessibilityChildrenVector& ariaLabelledBy) const |
3632 | { |
3633 | ariaElementsFromAttribute(ariaLabelledBy, aria_labelledbyAttr); |
3634 | if (!ariaLabelledBy.size()) |
3635 | ariaElementsFromAttribute(ariaLabelledBy, aria_labeledbyAttr); |
3636 | } |
3637 | |
3638 | void AccessibilityObject::ariaLabelledByReferencingElements(AccessibilityChildrenVector& labels) const |
3639 | { |
3640 | ariaElementsReferencedByAttribute(labels, aria_labelledbyAttr); |
3641 | if (!labels.size()) |
3642 | ariaElementsReferencedByAttribute(labels, aria_labeledbyAttr); |
3643 | } |
3644 | |
3645 | void AccessibilityObject::ariaOwnsElements(AccessibilityChildrenVector& axObjects) const |
3646 | { |
3647 | ariaElementsFromAttribute(axObjects, aria_ownsAttr); |
3648 | } |
3649 | |
3650 | void AccessibilityObject::ariaOwnsReferencingElements(AccessibilityChildrenVector& owners) const |
3651 | { |
3652 | ariaElementsReferencedByAttribute(owners, aria_ownsAttr); |
3653 | } |
3654 | |
3655 | void AccessibilityObject::setIsIgnoredFromParentDataForChild(AccessibilityObject* child) |
3656 | { |
3657 | if (!child) |
3658 | return; |
3659 | |
3660 | if (child->parentObject() != this) { |
3661 | child->clearIsIgnoredFromParentData(); |
3662 | return; |
3663 | } |
3664 | |
3665 | AccessibilityIsIgnoredFromParentData result = AccessibilityIsIgnoredFromParentData(this); |
3666 | if (!m_isIgnoredFromParentData.isNull()) { |
3667 | result.isAXHidden = m_isIgnoredFromParentData.isAXHidden || equalLettersIgnoringASCIICase(child->getAttribute(aria_hiddenAttr), "true" ); |
3668 | result.isPresentationalChildOfAriaRole = m_isIgnoredFromParentData.isPresentationalChildOfAriaRole || ariaRoleHasPresentationalChildren(); |
3669 | result.isDescendantOfBarrenParent = m_isIgnoredFromParentData.isDescendantOfBarrenParent || !canHaveChildren(); |
3670 | } else { |
3671 | result.isAXHidden = child->isAXHidden(); |
3672 | result.isPresentationalChildOfAriaRole = child->isPresentationalChildOfAriaRole(); |
3673 | result.isDescendantOfBarrenParent = child->isDescendantOfBarrenParent(); |
3674 | } |
3675 | |
3676 | child->setIsIgnoredFromParentData(result); |
3677 | } |
3678 | |
3679 | } // namespace WebCore |
3680 | |