| 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 | |