Accessible.cpp (33757B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "Accessible.h" 7 #include "ARIAMap.h" 8 #include "nsAccUtils.h" 9 #include "nsIURI.h" 10 #include "Pivot.h" 11 #include "Relation.h" 12 #include "States.h" 13 #include "mozilla/a11y/FocusManager.h" 14 #include "mozilla/a11y/HyperTextAccessibleBase.h" 15 #include "mozilla/BasicEvents.h" 16 #include "mozilla/Components.h" 17 #include "mozilla/ProfilerMarkers.h" 18 #include "nsIStringBundle.h" 19 20 #ifdef A11Y_LOG 21 # include "nsAccessibilityService.h" 22 #endif 23 24 using namespace mozilla; 25 using namespace mozilla::a11y; 26 27 Accessible::Accessible() 28 : mType(static_cast<uint32_t>(0)), 29 mGenericTypes(static_cast<uint32_t>(0)), 30 mRoleMapEntryIndex(aria::NO_ROLE_MAP_ENTRY_INDEX) {} 31 32 Accessible::Accessible(AccType aType, AccGenericType aGenericTypes, 33 uint8_t aRoleMapEntryIndex) 34 : mType(static_cast<uint32_t>(aType)), 35 mGenericTypes(static_cast<uint32_t>(aGenericTypes)), 36 mRoleMapEntryIndex(aRoleMapEntryIndex) {} 37 38 void Accessible::StaticAsserts() const { 39 static_assert(eLastAccType <= (1 << kTypeBits) - 1, 40 "Accessible::mType was oversized by eLastAccType!"); 41 static_assert( 42 eLastAccGenericType <= (1 << kGenericTypesBits) - 1, 43 "Accessible::mGenericType was oversized by eLastAccGenericType!"); 44 } 45 46 mozilla::a11y::role Accessible::Role() const { 47 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 48 mozilla::a11y::role r = 49 (!roleMapEntry || roleMapEntry->roleRule != kUseMapRole) 50 ? NativeRole() 51 : roleMapEntry->role; 52 r = ARIATransformRole(r); 53 return GetMinimumRole(r); 54 } 55 56 bool Accessible::IsBefore(const Accessible* aAcc) const { 57 // Build the chain of parents. 58 const Accessible* thisP = this; 59 const Accessible* otherP = aAcc; 60 AutoTArray<const Accessible*, 30> thisParents, otherParents; 61 do { 62 thisParents.AppendElement(thisP); 63 thisP = thisP->Parent(); 64 } while (thisP); 65 do { 66 otherParents.AppendElement(otherP); 67 otherP = otherP->Parent(); 68 } while (otherP); 69 70 // Find where the parent chain differs. 71 uint32_t thisPos = thisParents.Length(), otherPos = otherParents.Length(); 72 for (uint32_t len = std::min(thisPos, otherPos); len > 0; --len) { 73 const Accessible* thisChild = thisParents.ElementAt(--thisPos); 74 const Accessible* otherChild = otherParents.ElementAt(--otherPos); 75 if (thisChild != otherChild) { 76 return thisChild->IndexInParent() < otherChild->IndexInParent(); 77 } 78 } 79 80 // If the ancestries are the same length (both thisPos and otherPos are 0), 81 // we should have returned by now. 82 MOZ_ASSERT(thisPos != 0 || otherPos != 0); 83 // At this point, one of the ancestries is a superset of the other, so one of 84 // thisPos or otherPos should be 0. 85 MOZ_ASSERT(thisPos != otherPos); 86 // If the other Accessible is deeper than this one (otherPos > 0), this 87 // Accessible comes before the other. 88 return otherPos > 0; 89 } 90 91 const Accessible* Accessible::GetClosestCommonInclusiveAncestor( 92 const Accessible* aAcc) const { 93 if (aAcc == this) { 94 return this; 95 } 96 97 // Build the chain of parents. 98 const Accessible* thisAnc = this; 99 const Accessible* otherAnc = aAcc; 100 AutoTArray<const Accessible*, 30> thisAncs, otherAncs; 101 do { 102 thisAncs.AppendElement(thisAnc); 103 thisAnc = thisAnc->Parent(); 104 } while (thisAnc); 105 do { 106 otherAncs.AppendElement(otherAnc); 107 otherAnc = otherAnc->Parent(); 108 } while (otherAnc); 109 110 // Find where the parent chain differs. 111 size_t thisPos = thisAncs.Length(), otherPos = otherAncs.Length(); 112 const Accessible* common = nullptr; 113 for (size_t len = std::min(thisPos, otherPos); len > 0; --len) { 114 const Accessible* thisChild = thisAncs.ElementAt(--thisPos); 115 const Accessible* otherChild = otherAncs.ElementAt(--otherPos); 116 if (thisChild != otherChild) { 117 break; 118 } 119 common = thisChild; 120 } 121 return common; 122 } 123 124 Accessible* Accessible::FocusedChild() { 125 Accessible* doc = nsAccUtils::DocumentFor(this); 126 Accessible* child = doc->FocusedChild(); 127 if (child && (child == this || child->Parent() == this)) { 128 return child; 129 } 130 131 return nullptr; 132 } 133 134 const nsRoleMapEntry* Accessible::ARIARoleMap() const { 135 return aria::GetRoleMapFromIndex(mRoleMapEntryIndex); 136 } 137 138 bool Accessible::HasARIARole() const { 139 return mRoleMapEntryIndex != aria::NO_ROLE_MAP_ENTRY_INDEX; 140 } 141 142 bool Accessible::IsARIARole(nsAtom* aARIARole) const { 143 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 144 return roleMapEntry && roleMapEntry->Is(aARIARole); 145 } 146 147 bool Accessible::HasStrongARIARole() const { 148 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 149 return roleMapEntry && roleMapEntry->roleRule == kUseMapRole; 150 } 151 152 role Accessible::GetMinimumRole(role aRole) const { 153 if (aRole != roles::TEXT && aRole != roles::TEXT_CONTAINER && 154 aRole != roles::SECTION) { 155 // This isn't a generic role, so aRole is specific enough. 156 return aRole; 157 } 158 159 if (IsPopover()) { 160 return roles::GROUPING; 161 } 162 return aRole; 163 } 164 165 bool Accessible::HasGenericType(AccGenericType aType) const { 166 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 167 return (mGenericTypes & aType) || 168 (roleMapEntry && roleMapEntry->IsOfType(aType)); 169 } 170 171 nsIntRect Accessible::BoundsInCSSPixels() const { 172 return BoundsInAppUnits().ToNearestPixels(AppUnitsPerCSSPixel()); 173 } 174 175 LayoutDeviceIntSize Accessible::Size() const { return Bounds().Size(); } 176 177 LayoutDeviceIntPoint Accessible::Position(uint32_t aCoordType) { 178 LayoutDeviceIntPoint point = Bounds().TopLeft(); 179 nsAccUtils::ConvertScreenCoordsTo(&point.x.value, &point.y.value, aCoordType, 180 this); 181 return point; 182 } 183 184 bool Accessible::IsTextRole() { 185 if (!IsHyperText()) { 186 return false; 187 } 188 189 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 190 if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC || 191 roleMapEntry->role == roles::IMAGE_MAP || 192 roleMapEntry->role == roles::SLIDER || 193 roleMapEntry->role == roles::PROGRESSBAR || 194 roleMapEntry->role == roles::SEPARATOR || 195 roleMapEntry->role == roles::METER)) { 196 return false; 197 } 198 199 return true; 200 } 201 202 bool Accessible::IsEditableRoot() const { 203 if (IsTextField()) { 204 // A text field is always an editable root. 205 return true; 206 } 207 208 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 209 if (roleMapEntry && (roleMapEntry->role == roles::ENTRY || 210 roleMapEntry->role == roles::SEARCHBOX)) { 211 // An aria text field is always an editable root. 212 return true; 213 } 214 215 if (!IsEditable()) { 216 return false; 217 } 218 219 if (IsDoc()) { 220 return true; 221 } 222 223 Accessible* parent = Parent(); 224 if (parent && !parent->IsEditable()) { 225 return true; 226 } 227 228 return false; 229 } 230 231 uint32_t Accessible::StartOffset() { 232 MOZ_ASSERT(IsLink(), "StartOffset is called not on hyper link!"); 233 Accessible* parent = Parent(); 234 HyperTextAccessibleBase* hyperText = 235 parent ? parent->AsHyperTextBase() : nullptr; 236 return hyperText ? hyperText->GetChildOffset(this) : 0; 237 } 238 239 uint32_t Accessible::EndOffset() { 240 MOZ_ASSERT(IsLink(), "EndOffset is called on not hyper link!"); 241 Accessible* parent = Parent(); 242 HyperTextAccessibleBase* hyperText = 243 parent ? parent->AsHyperTextBase() : nullptr; 244 return hyperText ? (hyperText->GetChildOffset(this) + 1) : 0; 245 } 246 247 GroupPos Accessible::GroupPosition() { 248 GroupPos groupPos; 249 250 // Try aria-row/colcount/index. 251 if (IsTableRow()) { 252 Accessible* table = nsAccUtils::TableFor(this); 253 if (table) { 254 if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) { 255 if (*count >= 0) { 256 groupPos.setSize = *count; 257 } 258 } 259 } 260 if (auto index = GetIntARIAAttr(nsGkAtoms::aria_rowindex)) { 261 groupPos.posInSet = *index; 262 } 263 if (groupPos.setSize && groupPos.posInSet) { 264 return groupPos; 265 } 266 } 267 if (IsTableCell()) { 268 Accessible* table; 269 for (table = Parent(); table; table = table->Parent()) { 270 if (table->IsTable()) { 271 break; 272 } 273 } 274 if (table) { 275 if (auto count = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) { 276 if (*count >= 0) { 277 groupPos.setSize = *count; 278 } 279 } 280 } 281 if (auto index = GetIntARIAAttr(nsGkAtoms::aria_colindex)) { 282 groupPos.posInSet = *index; 283 } 284 if (groupPos.setSize && groupPos.posInSet) { 285 return groupPos; 286 } 287 } 288 289 // Get group position from ARIA attributes. 290 ARIAGroupPosition(&groupPos.level, &groupPos.setSize, &groupPos.posInSet); 291 292 // If ARIA is missed and the accessible is visible then calculate group 293 // position from hierarchy. 294 if (State() & states::INVISIBLE) return groupPos; 295 296 // Calculate group level if ARIA is missed. 297 if (groupPos.level == 0) { 298 groupPos.level = GetLevel(false); 299 } 300 301 // Calculate position in group and group size if ARIA is missed. 302 if (groupPos.posInSet == 0 || groupPos.setSize == 0) { 303 int32_t posInSet = 0, setSize = 0; 304 GetPositionAndSetSize(&posInSet, &setSize); 305 if (posInSet != 0 && setSize != 0) { 306 if (groupPos.posInSet == 0) groupPos.posInSet = posInSet; 307 308 if (groupPos.setSize == 0) groupPos.setSize = setSize; 309 } 310 } 311 312 return groupPos; 313 } 314 315 int32_t Accessible::GetLevel(bool aFast) const { 316 int32_t level = 0; 317 if (!Parent()) return level; 318 319 roles::Role role = Role(); 320 if (role == roles::OUTLINEITEM) { 321 // Always expose 'level' attribute for 'outlineitem' accessible. The number 322 // of nested 'grouping' accessibles containing 'outlineitem' accessible is 323 // its level. 324 level = 1; 325 326 if (!aFast) { 327 const Accessible* parent = this; 328 while ((parent = parent->Parent()) && !parent->IsDoc()) { 329 roles::Role parentRole = parent->Role(); 330 331 if (parentRole == roles::OUTLINE) break; 332 if (parentRole == roles::GROUPING) ++level; 333 } 334 } 335 } else if (role == roles::LISTITEM && !aFast) { 336 // Expose 'level' attribute on nested lists. We support two hierarchies: 337 // a) list -> listitem -> list -> listitem (nested list is a last child 338 // of listitem of the parent list); 339 // b) list -> listitem -> group -> listitem (nested listitems are contained 340 // by group that is a last child of the parent listitem). 341 342 // Calculate 'level' attribute based on number of parent listitems. 343 level = 0; 344 const Accessible* parent = this; 345 while ((parent = parent->Parent()) && !parent->IsDoc()) { 346 roles::Role parentRole = parent->Role(); 347 348 if (parentRole == roles::LISTITEM) { 349 ++level; 350 } else if (parentRole != roles::LIST && parentRole != roles::GROUPING) { 351 break; 352 } 353 } 354 355 if (level == 0) { 356 // If this listitem is on top of nested lists then expose 'level' 357 // attribute. 358 parent = Parent(); 359 uint32_t siblingCount = parent->ChildCount(); 360 for (uint32_t siblingIdx = 0; siblingIdx < siblingCount; siblingIdx++) { 361 Accessible* sibling = parent->ChildAt(siblingIdx); 362 363 Accessible* siblingChild = sibling->LastChild(); 364 if (siblingChild) { 365 roles::Role lastChildRole = siblingChild->Role(); 366 if (lastChildRole == roles::LIST || 367 lastChildRole == roles::GROUPING) { 368 return 1; 369 } 370 } 371 } 372 } else { 373 ++level; // level is 1-index based 374 } 375 } else if (role == roles::OPTION || role == roles::COMBOBOX_OPTION) { 376 if (const Accessible* parent = Parent()) { 377 if (parent->IsHTMLOptGroup()) { 378 return 2; 379 } 380 381 if (parent->IsListControl() && !parent->ARIARoleMap()) { 382 // This is for HTML selects only. 383 if (aFast) { 384 return 1; 385 } 386 387 for (uint32_t i = 0, count = parent->ChildCount(); i < count; ++i) { 388 if (parent->ChildAt(i)->IsHTMLOptGroup()) { 389 return 1; 390 } 391 } 392 } 393 } 394 } else if (role == roles::HEADING) { 395 nsAtom* tagName = TagName(); 396 if (tagName == nsGkAtoms::h1) { 397 return 1; 398 } 399 if (tagName == nsGkAtoms::h2) { 400 return 2; 401 } 402 if (tagName == nsGkAtoms::h3) { 403 return 3; 404 } 405 if (tagName == nsGkAtoms::h4) { 406 return 4; 407 } 408 if (tagName == nsGkAtoms::h5) { 409 return 5; 410 } 411 if (tagName == nsGkAtoms::h6) { 412 return 6; 413 } 414 415 const nsRoleMapEntry* ariaRole = this->ARIARoleMap(); 416 if (ariaRole && ariaRole->Is(nsGkAtoms::heading)) { 417 // An aria heading with no aria level has a default level of 2. 418 return 2; 419 } 420 } else if (role == roles::COMMENT) { 421 // For comments, count the ancestor elements with the same role to get the 422 // level. 423 level = 1; 424 425 if (!aFast) { 426 const Accessible* parent = this; 427 while ((parent = parent->Parent()) && !parent->IsDoc()) { 428 roles::Role parentRole = parent->Role(); 429 if (parentRole == roles::COMMENT) { 430 ++level; 431 } 432 } 433 } 434 } else if (role == roles::ROW) { 435 // It is a row inside flatten treegrid. Group level is always 1 until it 436 // is overriden by aria-level attribute. 437 const Accessible* parent = Parent(); 438 if (parent->Role() == roles::TREE_TABLE) { 439 return 1; 440 } 441 } 442 443 return level; 444 } 445 446 void Accessible::GetPositionAndSetSize(int32_t* aPosInSet, int32_t* aSetSize) { 447 auto groupInfo = GetOrCreateGroupInfo(); 448 if (groupInfo) { 449 *aPosInSet = groupInfo->PosInSet(); 450 *aSetSize = groupInfo->SetSize(); 451 } 452 } 453 454 bool Accessible::IsLinkValid() { 455 MOZ_ASSERT(IsLink(), "IsLinkValid is called on not hyper link!"); 456 457 // XXX In order to implement this we would need to follow every link 458 // Perhaps we can get information about invalid links from the cache 459 // In the mean time authors can use role="link" aria-invalid="true" 460 // to force it for links they internally know to be invalid 461 return (0 == (State() & mozilla::a11y::states::INVALID)); 462 } 463 464 uint32_t Accessible::AnchorCount() { 465 if (IsImageMap()) { 466 return ChildCount(); 467 } 468 469 MOZ_ASSERT(IsLink(), "AnchorCount is called on not hyper link!"); 470 return 1; 471 } 472 473 Accessible* Accessible::AnchorAt(uint32_t aAnchorIndex) const { 474 if (IsImageMap()) { 475 return ChildAt(aAnchorIndex); 476 } 477 478 MOZ_ASSERT(IsLink(), "GetAnchor is called on not hyper link!"); 479 return aAnchorIndex == 0 ? const_cast<Accessible*>(this) : nullptr; 480 } 481 482 already_AddRefed<nsIURI> Accessible::AnchorURIAt(uint32_t aAnchorIndex) const { 483 Accessible* anchor = nullptr; 484 485 if (IsTextLeaf() || IsImage()) { 486 for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc(); 487 parent = parent->Parent()) { 488 if (parent->IsLink()) { 489 anchor = parent->AnchorAt(aAnchorIndex); 490 } 491 } 492 } else { 493 anchor = AnchorAt(aAnchorIndex); 494 } 495 496 if (anchor) { 497 RefPtr<nsIURI> uri; 498 nsAutoString spec; 499 anchor->Value(spec); 500 nsresult rv = NS_NewURI(getter_AddRefs(uri), spec); 501 if (NS_SUCCEEDED(rv)) { 502 return uri.forget(); 503 } 504 } 505 506 return nullptr; 507 } 508 509 #ifdef A11Y_LOG 510 void Accessible::DebugDescription(nsCString& aDesc) const { 511 aDesc.Truncate(); 512 aDesc.AppendPrintf("%s", IsRemote() ? "Remote" : "Local"); 513 aDesc.AppendPrintf("[%p] ", this); 514 nsAutoString role; 515 GetAccService()->GetStringRole(Role(), role); 516 aDesc.Append(NS_ConvertUTF16toUTF8(role)); 517 518 if (nsAtom* tagAtom = TagName()) { 519 nsAutoCString tag; 520 tagAtom->ToUTF8String(tag); 521 aDesc.AppendPrintf(" %s", tag.get()); 522 523 nsAutoString id; 524 DOMNodeID(id); 525 if (!id.IsEmpty()) { 526 aDesc.Append("#"); 527 aDesc.Append(NS_ConvertUTF16toUTF8(id)); 528 } 529 } 530 nsAutoString id; 531 532 nsAutoString name; 533 Name(name); 534 if (!name.IsEmpty()) { 535 aDesc.Append(" '"); 536 aDesc.Append(NS_ConvertUTF16toUTF8(name)); 537 aDesc.Append("'"); 538 } 539 } 540 541 void Accessible::DebugPrint(const char* aPrefix, 542 const Accessible* aAccessible) { 543 nsAutoCString desc; 544 if (aAccessible) { 545 aAccessible->DebugDescription(desc); 546 } else { 547 desc.AssignLiteral("[null]"); 548 } 549 # if defined(ANDROID) || defined(MOZ_WIDGET_UIKIT) 550 printf_stderr("%s %s\n", aPrefix, desc.get()); 551 # else 552 printf("%s %s\n", aPrefix, desc.get()); 553 # endif 554 } 555 556 #endif 557 558 void Accessible::TranslateString(const nsString& aKey, nsAString& aStringOut, 559 const nsTArray<nsString>& aParams) { 560 nsCOMPtr<nsIStringBundleService> stringBundleService = 561 components::StringBundle::Service(); 562 if (!stringBundleService) return; 563 564 nsCOMPtr<nsIStringBundle> stringBundle; 565 stringBundleService->CreateBundle( 566 "chrome://global-platform/locale/accessible.properties", 567 getter_AddRefs(stringBundle)); 568 if (!stringBundle) return; 569 570 nsAutoString xsValue; 571 nsresult rv = NS_OK; 572 if (aParams.IsEmpty()) { 573 rv = stringBundle->GetStringFromName(NS_ConvertUTF16toUTF8(aKey).get(), 574 xsValue); 575 } else { 576 rv = stringBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(aKey).get(), 577 aParams, xsValue); 578 } 579 if (NS_SUCCEEDED(rv)) aStringOut.Assign(xsValue); 580 } 581 582 const Accessible* Accessible::ActionAncestor() const { 583 // We do want to consider a click handler on the document. However, we don't 584 // want to walk outside of this document, so we stop if we see an OuterDoc. 585 for (Accessible* parent = Parent(); parent && !parent->IsOuterDoc(); 586 parent = parent->Parent()) { 587 if (parent->HasPrimaryAction()) { 588 return parent; 589 } 590 } 591 592 return nullptr; 593 } 594 595 nsStaticAtom* Accessible::LandmarkRole() const { 596 // For certain cases below (e.g. ARIA region, HTML <header>), whether it is 597 // actually a landmark is conditional. Rather than duplicating that 598 // conditional logic here, we check the Gecko role. 599 if (const nsRoleMapEntry* roleMapEntry = ARIARoleMap()) { 600 // Explicit ARIA role should take precedence. 601 if (roleMapEntry->Is(nsGkAtoms::region)) { 602 if (Role() == roles::REGION) { 603 return nsGkAtoms::region; 604 } 605 } else if (roleMapEntry->Is(nsGkAtoms::form)) { 606 if (Role() == roles::FORM) { 607 return nsGkAtoms::form; 608 } 609 } else if (roleMapEntry->IsOfType(eLandmark)) { 610 return roleMapEntry->roleAtom; 611 } 612 } 613 614 nsAtom* tagName = TagName(); 615 if (!tagName) { 616 // Either no associated content, or no cache. 617 return nullptr; 618 } 619 620 if (tagName == nsGkAtoms::nav) { 621 return nsGkAtoms::navigation; 622 } 623 624 if (tagName == nsGkAtoms::aside) { 625 return nsGkAtoms::complementary; 626 } 627 628 if (tagName == nsGkAtoms::main) { 629 return nsGkAtoms::main; 630 } 631 632 if (tagName == nsGkAtoms::header) { 633 if (Role() == roles::LANDMARK) { 634 return nsGkAtoms::banner; 635 } 636 } 637 638 if (tagName == nsGkAtoms::footer) { 639 if (Role() == roles::LANDMARK) { 640 return nsGkAtoms::contentinfo; 641 } 642 } 643 644 if (tagName == nsGkAtoms::section) { 645 if (Role() == roles::REGION) { 646 return nsGkAtoms::region; 647 } 648 } 649 650 if (tagName == nsGkAtoms::form) { 651 if (Role() == roles::FORM_LANDMARK) { 652 return nsGkAtoms::form; 653 } 654 } 655 656 if (tagName == nsGkAtoms::search) { 657 return nsGkAtoms::search; 658 } 659 660 return nullptr; 661 } 662 663 nsStaticAtom* Accessible::ComputedARIARole() const { 664 const nsRoleMapEntry* roleMap = ARIARoleMap(); 665 if (roleMap && roleMap->IsOfType(eDPub)) { 666 return roleMap->roleAtom; 667 } 668 if (roleMap && roleMap->roleAtom != nsGkAtoms::_empty && 669 // region and form have their own Gecko roles and need to be handled 670 // specially. 671 roleMap->roleAtom != nsGkAtoms::region && 672 roleMap->roleAtom != nsGkAtoms::form && 673 (roleMap->roleRule == kUseNativeRole || roleMap->IsOfType(eLandmark) || 674 roleMap->roleAtom == nsGkAtoms::alertdialog || 675 roleMap->roleAtom == nsGkAtoms::feed)) { 676 // Explicit ARIA role (e.g. specified via the role attribute) which does not 677 // map to a unique Gecko role. 678 return roleMap->roleAtom; 679 } 680 role geckoRole = Role(); 681 if (geckoRole == roles::LANDMARK) { 682 // Landmark role from native markup; e.g. <main>, <nav>. 683 return LandmarkRole(); 684 } 685 // Role from native markup or layout. 686 #define ROLE(_geckoRole, stringRole, ariaRole, atkRole, macRole, macSubrole, \ 687 msaaRole, ia2Role, androidClass, iosIsElement, uiaControlType, \ 688 nameRule) \ 689 case roles::_geckoRole: \ 690 return ariaRole; 691 switch (geckoRole) { 692 #include "RoleMap.h" 693 } 694 #undef ROLE 695 MOZ_ASSERT_UNREACHABLE("Unknown role"); 696 return nullptr; 697 } 698 699 void Accessible::ApplyImplicitState(uint64_t& aState) const { 700 // nsAccessibilityService (and thus FocusManager) can be shut down before 701 // RemoteAccessibles. 702 if (const auto* focusMgr = FocusMgr()) { 703 if (focusMgr->IsFocused(this)) { 704 aState |= states::FOCUSED; 705 } 706 } 707 708 // If this is an option, tab or treeitem and if it's focused and not marked 709 // unselected explicitly (i.e. aria-selected="false") then expose it as 710 // selected to make ARIA widget authors life easier. 711 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 712 if (roleMapEntry && 713 (roleMapEntry->Is(nsGkAtoms::option) || 714 roleMapEntry->Is(nsGkAtoms::tab) || 715 roleMapEntry->Is(nsGkAtoms::treeitem)) && 716 !(aState & states::SELECTED) && ARIASelected().valueOr(true)) { 717 if (roleMapEntry->role == roles::PAGETAB && !(aState & states::FOCUSED)) { 718 // If focus is within the tab panel, this should mean the tab is selected. 719 // Note that we handle focus on the tab itself below. 720 Relation rel = RelationByType(RelationType::LABEL_FOR); 721 Accessible* relTarget = nullptr; 722 while ((relTarget = rel.Next())) { 723 if (relTarget->Role() == roles::PROPERTYPAGE && 724 FocusMgr()->IsFocusWithin(relTarget)) { 725 aState |= states::SELECTED; 726 } 727 } 728 } else if (aState & states::FOCUSED) { 729 Accessible* container = nsAccUtils::GetSelectableContainer(this, aState); 730 AUTO_PROFILER_MARKER_TEXT( 731 "Accessible::ApplyImplicitState::ImplicitSelection", A11Y, {}, ""_ns); 732 auto HasExplicitSelection = [](Accessible* aAcc) { 733 Pivot p = Pivot(aAcc); 734 PivotARIASelectedRule rule; 735 return p.First(rule) != nullptr; 736 }; 737 738 if (container && !(container->State() & states::MULTISELECTABLE) && 739 !HasExplicitSelection(container)) { 740 aState |= states::SELECTED; 741 } 742 } 743 } 744 745 if (Opacity() == 1.0f && !(aState & states::INVISIBLE)) { 746 aState |= states::OPAQUE1; 747 } 748 749 if (aState & states::EXPANDABLE && !(aState & states::EXPANDED)) { 750 aState |= states::COLLAPSED; 751 } 752 753 if (!(aState & states::UNAVAILABLE)) { 754 aState |= states::ENABLED | states::SENSITIVE; 755 } 756 757 if (aState & states::FOCUSABLE && !(aState & states::UNAVAILABLE)) { 758 // Propagate UNAVAILABLE state from ancestors down to any focusable 759 // descendant. 760 for (auto ancestor = Parent(); ancestor; ancestor = ancestor->Parent()) { 761 if (ancestor->IsDoc() || ancestor->IsOuterDoc()) { 762 break; 763 } 764 765 if (ancestor->State() & states::UNAVAILABLE) { 766 aState |= states::UNAVAILABLE; 767 break; 768 } 769 } 770 } 771 } 772 773 bool Accessible::NameIsEmpty() const { 774 nsAutoString name; 775 Name(name); 776 return name.IsEmpty(); 777 } 778 779 //////////////////////////////////////////////////////////////////////////////// 780 // KeyBinding class 781 782 // static 783 uint32_t KeyBinding::AccelModifier() { 784 switch (WidgetInputEvent::AccelModifier()) { 785 case MODIFIER_ALT: 786 return kAlt; 787 case MODIFIER_CONTROL: 788 return kControl; 789 case MODIFIER_META: 790 return kMeta; 791 default: 792 MOZ_CRASH("Handle the new result of WidgetInputEvent::AccelModifier()"); 793 return 0; 794 } 795 } 796 797 void KeyBinding::ToPlatformFormat(nsAString& aValue) const { 798 nsCOMPtr<nsIStringBundle> keyStringBundle; 799 nsCOMPtr<nsIStringBundleService> stringBundleService = 800 mozilla::components::StringBundle::Service(); 801 if (stringBundleService) { 802 stringBundleService->CreateBundle( 803 "chrome://global-platform/locale/platformKeys.properties", 804 getter_AddRefs(keyStringBundle)); 805 } 806 807 if (!keyStringBundle) return; 808 809 nsAutoString separator; 810 keyStringBundle->GetStringFromName("MODIFIER_SEPARATOR", separator); 811 812 nsAutoString modifierName; 813 if (mModifierMask & kControl) { 814 keyStringBundle->GetStringFromName("VK_CONTROL", modifierName); 815 816 aValue.Append(modifierName); 817 aValue.Append(separator); 818 } 819 820 if (mModifierMask & kAlt) { 821 keyStringBundle->GetStringFromName("VK_ALT", modifierName); 822 823 aValue.Append(modifierName); 824 aValue.Append(separator); 825 } 826 827 if (mModifierMask & kShift) { 828 keyStringBundle->GetStringFromName("VK_SHIFT", modifierName); 829 830 aValue.Append(modifierName); 831 aValue.Append(separator); 832 } 833 834 if (mModifierMask & kMeta) { 835 keyStringBundle->GetStringFromName("VK_META", modifierName); 836 837 aValue.Append(modifierName); 838 aValue.Append(separator); 839 } 840 841 aValue.Append(mKey); 842 } 843 844 void KeyBinding::ToAtkFormat(nsAString& aValue) const { 845 nsAutoString modifierName; 846 if (mModifierMask & kControl) aValue.AppendLiteral("<Control>"); 847 848 if (mModifierMask & kAlt) aValue.AppendLiteral("<Alt>"); 849 850 if (mModifierMask & kShift) aValue.AppendLiteral("<Shift>"); 851 852 if (mModifierMask & kMeta) aValue.AppendLiteral("<Meta>"); 853 854 aValue.Append(mKey); 855 } 856 857 role Accessible::FindNextValidARIARole( 858 std::initializer_list<nsStaticAtom*> aRolesToSkip) const { 859 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 860 if (roleMapEntry) { 861 if (!ARIAAttrValueIs(nsGkAtoms::role, roleMapEntry->roleAtom)) { 862 nsAutoString roles; 863 GetStringARIAAttr(nsGkAtoms::role, roles); 864 // Get the next valid token that isn't in the list of roles to skip. 865 uint8_t roleMapIndex = 866 aria::GetFirstValidRoleMapIndexExcluding(roles, aRolesToSkip); 867 // If we don't find a valid token, fall back to the minimum role. 868 if (roleMapIndex == aria::NO_ROLE_MAP_ENTRY_INDEX || 869 roleMapIndex == aria::LANDMARK_ROLE_MAP_ENTRY_INDEX) { 870 return NativeRole(); 871 } 872 const nsRoleMapEntry* fallbackRoleMapEntry = 873 aria::GetRoleMapFromIndex(roleMapIndex); 874 if (!fallbackRoleMapEntry) { 875 return NativeRole(); 876 } 877 // Return the next valid role, but validate that first, too. 878 return ARIATransformRole(fallbackRoleMapEntry->role); 879 } 880 } 881 // Fall back to the minimum role. 882 return NativeRole(); 883 } 884 885 role Accessible::ARIATransformRole(role aRole) const { 886 // Beginning with ARIA 1.1, user agents are expected to use the native host 887 // language role of the element when the form or region roles are used without 888 // a name. Says the spec, "the user agent MUST treat such elements as if no 889 // role had been provided." 890 // https://w3c.github.io/aria/#document-handling_author-errors_roles 891 // 892 // XXX: While the name computation algorithm can be non-trivial in the general 893 // case, it should not be especially bad here: If the author hasn't used the 894 // region role, this calculation won't occur. And the region role's name 895 // calculation rule excludes name from content. That said, this use case is 896 // another example of why we should consider caching the accessible name. See: 897 // https://bugzilla.mozilla.org/show_bug.cgi?id=1378235. 898 if (aRole == roles::REGION || aRole == roles::FORM) { 899 if (NameIsEmpty()) { 900 // If we have a "form" or "region" role, but no accessible name, we need 901 // to search for the next valid role. First, we search through the role 902 // attribute value string - there might be a valid fallback there. Skip 903 // all "form" or "region" attributes; we know they're not valid since 904 // there's no accessible name. If we find a valid role that's not "form" 905 // or "region", fall back to it (but run it through ARIATransformRole 906 // first). Otherwise, fall back to the element's native role. 907 return FindNextValidARIARole({nsGkAtoms::region, nsGkAtoms::form}); 908 } 909 return aRole; 910 } 911 912 // XXX: these unfortunate exceptions don't fit into the ARIA table. This is 913 // where the accessible role depends on both the role and ARIA state. 914 if (aRole == roles::PUSHBUTTON) { 915 if (HasARIAAttr(nsGkAtoms::aria_pressed)) { 916 // For simplicity, any existing pressed attribute except "" or "undefined" 917 // indicates a toggle. 918 return roles::TOGGLE_BUTTON; 919 } 920 921 if (ARIAAttrValueIs(nsGkAtoms::aria_haspopup, nsGkAtoms::_true)) { 922 // For button with aria-haspopup="true". 923 return roles::BUTTONMENU; 924 } 925 926 } else if (aRole == roles::LISTBOX) { 927 // A listbox inside of a combobox needs a special role because of ATK 928 // mapping to menu. 929 if (Parent() && Parent()->IsCombobox()) { 930 return roles::COMBOBOX_LIST; 931 } 932 933 } else if (aRole == roles::OPTION) { 934 const Accessible* listbox = FindAncestorIf([](const Accessible& aAcc) { 935 const role accRole = aAcc.Role(); 936 return (accRole == roles::LISTBOX || accRole == roles::COMBOBOX_LIST) 937 ? AncestorSearchOption::Found 938 : accRole == roles::GROUPING ? AncestorSearchOption::Continue 939 : AncestorSearchOption::NotFound; 940 }); 941 if (!listbox) { 942 // Orphaned option outside the context of a listbox. 943 return NativeRole(); 944 } 945 946 if (listbox->Role() == roles::COMBOBOX_LIST) { 947 return roles::COMBOBOX_OPTION; 948 } 949 } else if (aRole == roles::MENUITEM) { 950 // Menuitem has a submenu. 951 if (ARIAAttrValueIs(nsGkAtoms::aria_haspopup, nsGkAtoms::_true)) { 952 return roles::PARENT_MENUITEM; 953 } 954 955 // Orphaned menuitem outside the context of a menu/menubar. 956 const Accessible* menu = FindAncestorIf([](const Accessible& aAcc) { 957 const role accRole = aAcc.Role(); 958 return (accRole == roles::MENUBAR || accRole == roles::MENUPOPUP) 959 ? AncestorSearchOption::Found 960 : accRole == roles::GROUPING ? AncestorSearchOption::Continue 961 : AncestorSearchOption::NotFound; 962 }); 963 if (!menu) { 964 return NativeRole(); 965 } 966 } else if (aRole == roles::RADIO_MENU_ITEM || 967 aRole == roles::CHECK_MENU_ITEM) { 968 // Orphaned radio/checkbox menuitem outside the context of a menu/menubar. 969 const Accessible* menu = FindAncestorIf([](const Accessible& aAcc) { 970 const role accRole = aAcc.Role(); 971 return (accRole == roles::MENUBAR || accRole == roles::MENUPOPUP) 972 ? AncestorSearchOption::Found 973 : accRole == roles::GROUPING ? AncestorSearchOption::Continue 974 : AncestorSearchOption::NotFound; 975 }); 976 if (!menu) { 977 return NativeRole(); 978 } 979 } else if (aRole == roles::CELL) { 980 // A cell inside an ancestor table element that has a grid role needs a 981 // gridcell role 982 // (https://www.w3.org/TR/html-aam-1.0/#html-element-role-mappings). 983 const Accessible* table = nsAccUtils::TableFor(this); 984 if (table && table->IsARIARole(nsGkAtoms::grid)) { 985 return roles::GRID_CELL; 986 } 987 } else if (aRole == roles::ROW) { 988 // Orphaned rows outside the context of a table. 989 const Accessible* table = nsAccUtils::TableFor(this); 990 if (!table) { 991 return NativeRole(); 992 } 993 } else if (aRole == roles::ROWGROUP) { 994 // Orphaned rowgroups outside the context of a table. 995 const Accessible* table = FindAncestorIf([](const Accessible& aAcc) { 996 return aAcc.IsTable() ? AncestorSearchOption::Found 997 : AncestorSearchOption::NotFound; 998 }); 999 if (!table) { 1000 return NativeRole(); 1001 } 1002 } else if (aRole == roles::GRID_CELL || aRole == roles::ROWHEADER || 1003 aRole == roles::COLUMNHEADER) { 1004 // Orphaned gridcell/rowheader/columnheader outside the context of a row. 1005 const Accessible* row = FindAncestorIf([](const Accessible& aAcc) { 1006 return aAcc.IsTableRow() ? AncestorSearchOption::Found 1007 : AncestorSearchOption::NotFound; 1008 }); 1009 if (!row) { 1010 return NativeRole(); 1011 } 1012 } else if (aRole == roles::LISTITEM) { 1013 // doc-biblioentry and doc-endnote should not be treated as listitems. 1014 const nsRoleMapEntry* roleMapEntry = ARIARoleMap(); 1015 if (!roleMapEntry || (roleMapEntry->roleAtom != nsGkAtoms::docBiblioentry && 1016 roleMapEntry->roleAtom != nsGkAtoms::docEndnote)) { 1017 // Orphaned listitem outside the context of a list. 1018 const Accessible* list = FindAncestorIf([](const Accessible& aAcc) { 1019 return aAcc.IsList() ? AncestorSearchOption::Found 1020 : AncestorSearchOption::Continue; 1021 }); 1022 if (!list) { 1023 return NativeRole(); 1024 } 1025 } 1026 } else if (aRole == roles::PAGETAB) { 1027 // Orphaned tab outside the context of a tablist. 1028 const Accessible* tablist = FindAncestorIf([](const Accessible& aAcc) { 1029 return aAcc.Role() == roles::PAGETABLIST ? AncestorSearchOption::Found 1030 : AncestorSearchOption::NotFound; 1031 }); 1032 if (!tablist) { 1033 return NativeRole(); 1034 } 1035 } else if (aRole == roles::OUTLINEITEM) { 1036 // Orphaned treeitem outside the context of a tree. 1037 const Accessible* tree = FindAncestorIf([](const Accessible& aAcc) { 1038 return aAcc.Role() == roles::OUTLINE ? AncestorSearchOption::Found 1039 : AncestorSearchOption::Continue; 1040 }); 1041 if (!tree) { 1042 return NativeRole(); 1043 } 1044 } 1045 1046 return aRole; 1047 }