DocAccessible.cpp (116686B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "LocalAccessible-inl.h" 8 #include "AccIterator.h" 9 #include "AccAttributes.h" 10 #include "ARIAMap.h" 11 #include "CachedTableAccessible.h" 12 #include "DocAccessible-inl.h" 13 #include "EventTree.h" 14 #include "HTMLImageMapAccessible.h" 15 #include "Relation.h" 16 #include "mozilla/ProfilerMarkers.h" 17 #include "nsAccUtils.h" 18 #include "nsEventShell.h" 19 #include "nsIIOService.h" 20 #include "nsLayoutUtils.h" 21 #include "nsTextEquivUtils.h" 22 #include "mozilla/a11y/Role.h" 23 #include "TreeWalker.h" 24 #include "xpcAccessibleDocument.h" 25 26 #include "AnchorPositioningUtils.h" 27 #include "nsIDocShell.h" 28 #include "mozilla/dom/Document.h" 29 #include "nsPIDOMWindow.h" 30 #include "nsIContentInlines.h" 31 #include "nsIEditingSession.h" 32 #include "nsIFrame.h" 33 #include "nsIInterfaceRequestorUtils.h" 34 #include "nsImageFrame.h" 35 #include "nsIMutationObserver.h" 36 #include "nsIURI.h" 37 #include "nsIWebNavigation.h" 38 #include "nsFocusManager.h" 39 #include "mozilla/AppShutdown.h" 40 #include "mozilla/Assertions.h" 41 #include "mozilla/Components.h" // for mozilla::components 42 #include "mozilla/EditorBase.h" 43 #include "mozilla/HTMLEditor.h" 44 #include "mozilla/PerfStats.h" 45 #include "mozilla/PresShell.h" 46 #include "mozilla/ScrollContainerFrame.h" 47 #include "nsAccessibilityService.h" 48 #include "mozilla/a11y/DocAccessibleChild.h" 49 #include "mozilla/dom/AncestorIterator.h" 50 #include "mozilla/dom/BrowserChild.h" 51 #include "mozilla/dom/DocumentType.h" 52 #include "mozilla/dom/Element.h" 53 #include "mozilla/dom/ElementInlines.h" 54 #include "mozilla/dom/HTMLSelectElement.h" 55 #include "mozilla/dom/UserActivation.h" 56 57 using namespace mozilla; 58 using namespace mozilla::a11y; 59 60 //////////////////////////////////////////////////////////////////////////////// 61 // Static member initialization 62 63 static nsStaticAtom* const kRelationAttrs[] = { 64 nsGkAtoms::aria_labelledby, nsGkAtoms::aria_describedby, 65 nsGkAtoms::aria_details, nsGkAtoms::aria_owns, 66 nsGkAtoms::aria_controls, nsGkAtoms::aria_flowto, 67 nsGkAtoms::aria_errormessage, nsGkAtoms::_for, 68 nsGkAtoms::control, nsGkAtoms::popovertarget, 69 nsGkAtoms::commandfor, nsGkAtoms::aria_activedescendant, 70 nsGkAtoms::aria_actions}; 71 72 static const uint32_t kRelationAttrsLen = std::size(kRelationAttrs); 73 74 static nsStaticAtom* const kSingleElementRelationIdlAttrs[] = { 75 nsGkAtoms::popovertarget, nsGkAtoms::commandfor}; 76 77 //////////////////////////////////////////////////////////////////////////////// 78 // Constructor/desctructor 79 80 DocAccessible::DocAccessible(dom::Document* aDocument, 81 PresShell* aPresShell) 82 : // XXX don't pass a document to the LocalAccessible constructor so that 83 // we don't set mDoc until our vtable is fully setup. If we set mDoc 84 // before setting up the vtable we will call LocalAccessible::AddRef() 85 // but not the overrides of it for subclasses. It is important to call 86 // those overrides to avoid confusing leak checking machinary. 87 HyperTextAccessible(nullptr, nullptr), 88 // XXX aaronl should we use an algorithm for the initial cache size? 89 mAccessibleCache(kDefaultCacheLength), 90 mNodeToAccessibleMap(kDefaultCacheLength), 91 mDocumentNode(aDocument), 92 mLoadState(eTreeConstructionPending), 93 mDocFlags(0), 94 mViewportCacheDirty(false), 95 mLoadEventType(0), 96 mPrevStateBits(0), 97 mPresShell(aPresShell), 98 mIPCDoc(nullptr) { 99 mGenericTypes |= eDocument; 100 mStateFlags |= eNotNodeMapEntry; 101 mDoc = this; 102 103 MOZ_ASSERT(mPresShell, "should have been given a pres shell"); 104 mPresShell->SetDocAccessible(this); 105 } 106 107 DocAccessible::~DocAccessible() { 108 NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); 109 } 110 111 //////////////////////////////////////////////////////////////////////////////// 112 // nsISupports 113 114 NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) 115 116 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, 117 LocalAccessible) 118 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) 119 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) 120 for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) { 121 for (const auto& providers : hashEntry->Values()) { 122 for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) { 123 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( 124 cb, "content of dependent ids hash entry of document accessible"); 125 126 const auto& provider = (*providers)[provIdx]; 127 cb.NoteXPCOMChild(provider->mContent); 128 } 129 } 130 } 131 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) 132 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) 133 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) 134 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates) 135 for (const auto& ar : tmp->mARIAOwnsHash.Values()) { 136 for (uint32_t i = 0; i < ar->Length(); i++) { 137 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item"); 138 cb.NoteXPCOMChild(ar->ElementAt(i)); 139 } 140 } 141 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 142 143 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible) 144 NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) 145 NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) 146 tmp->mDependentIDsHashes.Clear(); 147 tmp->mNodeToAccessibleMap.Clear(); 148 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) 149 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) 150 NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList) 151 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates) 152 NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE 153 tmp->mARIAOwnsHash.Clear(); 154 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 155 156 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible) 157 NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) 158 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) 159 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) 160 NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible) 161 162 NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible) 163 NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible) 164 165 //////////////////////////////////////////////////////////////////////////////// 166 // nsIAccessible 167 168 ENameValueFlag DocAccessible::DirectName(nsString& aName) const { 169 aName.Truncate(); 170 171 if (mParent) { 172 mParent->Name(aName); // Allow owning iframe to override the name 173 } 174 if (aName.IsEmpty()) { 175 // Allow name via aria-labelledby or title attribute 176 LocalAccessible::DirectName(aName); 177 } 178 if (aName.IsEmpty()) { 179 Title(aName); // Try title element 180 } 181 if (aName.IsEmpty()) { // Last resort: use URL 182 URL(aName); 183 } 184 185 if (aName.IsEmpty()) { 186 aName.SetIsVoid(true); 187 } 188 189 return eNameOK; 190 } 191 192 // LocalAccessible public method 193 role DocAccessible::NativeRole() const { 194 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); 195 if (docShell) { 196 nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot; 197 docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); 198 int32_t itemType = docShell->ItemType(); 199 if (sameTypeRoot == docShell) { 200 // Root of content or chrome tree 201 if (itemType == nsIDocShellTreeItem::typeChrome) { 202 return roles::CHROME_WINDOW; 203 } 204 205 if (itemType == nsIDocShellTreeItem::typeContent) { 206 return roles::DOCUMENT; 207 } 208 } else if (itemType == nsIDocShellTreeItem::typeContent) { 209 return roles::DOCUMENT; 210 } 211 } 212 213 return roles::PANE; // Fall back; 214 } 215 216 EDescriptionValueFlag DocAccessible::Description(nsString& aDescription) const { 217 if (mParent) mParent->Description(aDescription); 218 219 if (HasOwnContent() && aDescription.IsEmpty()) { 220 nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, 221 aDescription); 222 } 223 return eDescriptionFromARIA; 224 } 225 226 // LocalAccessible public method 227 uint64_t DocAccessible::NativeState() const { 228 // Document is always focusable. 229 uint64_t state = 230 states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl 231 if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED; 232 233 // Expose stale state until the document is ready (DOM is loaded and tree is 234 // constructed). 235 if (!HasLoadState(eReady)) state |= states::STALE; 236 237 // Expose state busy until the document and all its subdocuments is completely 238 // loaded. 239 if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY; 240 241 nsIFrame* frame = GetFrame(); 242 if (!frame || !frame->IsVisibleConsideringAncestors( 243 nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { 244 state |= states::INVISIBLE | states::OFFSCREEN; 245 } 246 247 RefPtr<EditorBase> editorBase = GetEditor(); 248 state |= editorBase ? states::EDITABLE : states::READONLY; 249 250 // GetFrame() returns the root frame, which is normally what we want. However, 251 // user-select: none might be set on the body, in which case this won't be 252 // exposed on the root frame. Therefore, we explicitly use the body frame 253 // here (if any). 254 nsIFrame* bodyFrame = mContent ? mContent->GetPrimaryFrame() : nullptr; 255 if ((state & states::EDITABLE) || (bodyFrame && bodyFrame->IsSelectable())) { 256 // If the accessible is editable the layout selectable state only disables 257 // mouse selection, but keyboard (shift+arrow) selection is still possible. 258 state |= states::SELECTABLE_TEXT; 259 } 260 261 return state; 262 } 263 264 uint64_t DocAccessible::NativeInteractiveState() const { 265 // Document is always focusable. 266 return states::FOCUSABLE; 267 } 268 269 bool DocAccessible::NativelyUnavailable() const { return false; } 270 271 // LocalAccessible public method 272 void DocAccessible::ApplyARIAState(uint64_t* aState) const { 273 // Grab states from content element. 274 if (mContent) LocalAccessible::ApplyARIAState(aState); 275 276 // Allow iframe/frame etc. to have final state override via ARIA. 277 if (mParent) mParent->ApplyARIAState(aState); 278 } 279 280 Accessible* DocAccessible::FocusedChild() { 281 // Return an accessible for the current global focus, which does not have to 282 // be contained within the current document. 283 return FocusMgr()->FocusedAccessible(); 284 } 285 286 void DocAccessible::TakeFocus() const { 287 // Focus the document. 288 nsFocusManager* fm = nsFocusManager::GetFocusManager(); 289 RefPtr<dom::Element> newFocus; 290 dom::AutoHandlingUserInputStatePusher inputStatePusher(true); 291 fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, 292 nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus)); 293 } 294 295 // HyperTextAccessible method 296 already_AddRefed<EditorBase> DocAccessible::GetEditor() const { 297 // Check if document is editable (designMode="on" case). Otherwise check if 298 // the html:body (for HTML document case) or document element is editable. 299 if (!mDocumentNode->IsInDesignMode() && 300 (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) { 301 return nullptr; 302 } 303 304 nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell(); 305 if (!docShell) { 306 return nullptr; 307 } 308 309 nsCOMPtr<nsIEditingSession> editingSession; 310 docShell->GetEditingSession(getter_AddRefs(editingSession)); 311 if (!editingSession) return nullptr; // No editing session interface 312 313 RefPtr<HTMLEditor> htmlEditor = 314 editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow()); 315 if (!htmlEditor) { 316 return nullptr; 317 } 318 319 bool isEditable = false; 320 htmlEditor->GetIsDocumentEditable(&isEditable); 321 if (isEditable) { 322 return htmlEditor.forget(); 323 } 324 325 return nullptr; 326 } 327 328 // DocAccessible public method 329 330 void DocAccessible::URL(nsAString& aURL) const { 331 aURL.Truncate(); 332 nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer(); 333 nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container)); 334 if (MOZ_UNLIKELY(!webNav)) { 335 return; 336 } 337 338 nsCOMPtr<nsIURI> uri; 339 webNav->GetCurrentURI(getter_AddRefs(uri)); 340 if (MOZ_UNLIKELY(!uri)) { 341 return; 342 } 343 // Let's avoid treating too long URI in the main process for avoiding 344 // memory fragmentation as far as possible. 345 if (uri->SchemeIs("data") || uri->SchemeIs("blob")) { 346 return; 347 } 348 349 nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service(); 350 if (NS_WARN_IF(!io)) { 351 return; 352 } 353 nsCOMPtr<nsIURI> exposableURI; 354 if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) || 355 MOZ_UNLIKELY(!exposableURI)) { 356 return; 357 } 358 nsAutoCString theURL; 359 if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) { 360 CopyUTF8toUTF16(theURL, aURL); 361 } 362 } 363 364 void DocAccessible::Title(nsString& aTitle) const { 365 mDocumentNode->GetTitle(aTitle); 366 } 367 368 void DocAccessible::MimeType(nsAString& aType) const { 369 mDocumentNode->GetContentType(aType); 370 } 371 372 void DocAccessible::DocType(nsAString& aType) const { 373 dom::DocumentType* docType = mDocumentNode->GetDoctype(); 374 if (docType) docType->GetPublicId(aType); 375 } 376 377 // Certain cache domain updates might require updating other cache domains. 378 // This function takes the given cache domains and returns those cache domains 379 // plus any other required associated cache domains. Made for use with 380 // QueueCacheUpdate. 381 static uint64_t GetCacheDomainsQueueUpdateSuperset(uint64_t aCacheDomains) { 382 // Text domain updates imply updates to the TextOffsetAttributes and 383 // TextBounds domains. 384 if (aCacheDomains & CacheDomain::Text) { 385 aCacheDomains |= CacheDomain::TextOffsetAttributes; 386 aCacheDomains |= CacheDomain::TextBounds; 387 } 388 // Bounds domain updates imply updates to the TextBounds domain. 389 if (aCacheDomains & CacheDomain::Bounds) { 390 aCacheDomains |= CacheDomain::TextBounds; 391 } 392 return aCacheDomains; 393 } 394 395 void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain, 396 bool aBypassActiveDomains) { 397 if (!mIPCDoc) { 398 return; 399 } 400 // These strong references aren't necessary because WithEntryHandle is 401 // guaranteed to run synchronously. However, static analysis complains without 402 // them. 403 RefPtr<DocAccessible> self = this; 404 RefPtr<LocalAccessible> acc = aAcc; 405 size_t arrayIndex = 406 mQueuedCacheUpdatesHash.WithEntryHandle(aAcc, [self, acc](auto&& entry) { 407 if (entry.HasEntry()) { 408 // This LocalAccessible has already been queued. Return its index in 409 // the queue array so we can update its queued domains. 410 return entry.Data(); 411 } 412 // Add this LocalAccessible to the queue array. 413 size_t index = self->mQueuedCacheUpdatesArray.Length(); 414 self->mQueuedCacheUpdatesArray.EmplaceBack(std::make_pair(acc, 0)); 415 // Also add it to the hash map so we can avoid processing the same 416 // LocalAccessible twice. 417 return entry.Insert(index); 418 }); 419 420 // We may need to bypass the active domain restriction when populating domains 421 // for the first time. In that case, queue cache updates regardless of domain. 422 if (aBypassActiveDomains) { 423 auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex]; 424 MOZ_ASSERT(arrayAcc == aAcc); 425 domain |= aNewDomain; 426 Controller()->ScheduleProcessing(); 427 return; 428 } 429 430 // Potentially queue updates for required related domains. 431 const uint64_t newDomains = GetCacheDomainsQueueUpdateSuperset(aNewDomain); 432 433 // Only queue cache updates for domains that are active. 434 const uint64_t domainsToUpdate = 435 nsAccessibilityService::GetActiveCacheDomains() & newDomains; 436 437 // Avoid queueing cache updates if we have no domains to update. 438 if (domainsToUpdate == CacheDomain::None) { 439 return; 440 } 441 442 auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex]; 443 MOZ_ASSERT(arrayAcc == aAcc); 444 domain |= domainsToUpdate; 445 Controller()->ScheduleProcessing(); 446 } 447 448 void DocAccessible::QueueCacheUpdateForDependentRelations( 449 LocalAccessible* aAcc, const nsAttrValue* aOldId) { 450 if (!mIPCDoc || !aAcc || !aAcc->IsInDocument() || aAcc->IsDefunct()) { 451 return; 452 } 453 dom::Element* el = aAcc->Elm(); 454 if (!el) { 455 return; 456 } 457 458 // We call this function when we've noticed an ID change, or when an acc 459 // is getting bound to its document. We need to ensure any existing accs 460 // that depend on this acc's ID or Element have their relation cache entries 461 // updated. 462 RelatedAccIterator iter(this, el, nullptr); 463 while (LocalAccessible* relatedAcc = iter.Next()) { 464 if (relatedAcc->IsDefunct() || !relatedAcc->IsInDocument() || 465 mInsertedAccessibles.Contains(relatedAcc)) { 466 continue; 467 } 468 QueueCacheUpdate(relatedAcc, CacheDomain::Relations); 469 } 470 471 if (aOldId) { 472 // If we have an old ID, we need to update any accessibles that depended on 473 // that ID as well. 474 nsAutoString id; 475 aOldId->ToString(id); 476 if (!id.IsEmpty()) { 477 auto* providers = GetRelProviders(el, id); 478 if (providers) { 479 for (auto& provider : *providers) { 480 if (LocalAccessible* oldRelatedAcc = 481 GetAccessible(provider->mContent)) { 482 QueueCacheUpdate(oldRelatedAcc, CacheDomain::Relations); 483 } 484 } 485 } 486 } 487 } 488 489 if (const nsIFrame* anchorFrame = nsCoreUtils::GetAnchorForPositionedFrame( 490 mPresShell, aAcc->GetFrame())) { 491 // If this accessible is anchored, retrieve the anchor and update its 492 // relations. 493 if (LocalAccessible* anchorAcc = GetAccessible(anchorFrame->GetContent())) { 494 if (!mInsertedAccessibles.Contains(anchorAcc)) { 495 QueueCacheUpdate(anchorAcc, CacheDomain::Relations); 496 } 497 } 498 } 499 500 if (nsIFrame* positionedFrame = nsCoreUtils::GetPositionedFrameForAnchor( 501 mPresShell, aAcc->GetFrame())) { 502 // If this accessible is an anchor, retrieve the positioned frame and 503 // refresh the cache on all its anchors. 504 if (LocalAccessible* targetAcc = 505 GetAccessible(positionedFrame->GetContent())) { 506 RefreshAnchorRelationCacheForTarget(targetAcc); 507 } 508 } 509 } 510 511 //////////////////////////////////////////////////////////////////////////////// 512 // LocalAccessible 513 514 void DocAccessible::Init() { 515 #ifdef A11Y_LOG 516 if (logging::IsEnabled(logging::eDocCreate)) { 517 logging::DocCreate("document initialize", mDocumentNode, this); 518 } 519 #endif 520 521 // Initialize notification controller. 522 mNotificationController = new NotificationController(this, mPresShell); 523 524 // Mark the DocAccessible as loaded if its DOM document is already loaded at 525 // this point. This can happen for one of three reasons: 526 // 1. A11y was started late. 527 // 2. DOM loading for a document (probably an in-process iframe) completed 528 // before its Accessible container was created. 529 // 3. The PresShell for the document was created after DOM loading completed. 530 // In that case, we tried to create the DocAccessible when DOM loading 531 // completed, but we can't create a DocAccessible without a PresShell, so 532 // this failed. The DocAccessible was subsequently created due to a layout 533 // notification. 534 if (mDocumentNode->GetReadyStateEnum() == 535 dom::Document::READYSTATE_COMPLETE && 536 !mDocumentNode->IsUncommittedInitialDocument()) { 537 mLoadState |= eDOMLoaded; 538 // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a 539 // doc load complete event. If it happened due to reason 3, we need to fire 540 // doc load complete because clients (especially tests) might be waiting 541 // for the document to load using this event. We can't distinguish why this 542 // happened at this point, so just fire it regardless. It won't do any 543 // harm even if it isn't necessary. We set mLoadEventType here and it will 544 // be fired in ProcessLoad as usual. 545 mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; 546 } else if (mDocumentNode->IsUncommittedInitialDocument()) { 547 // The initial about:blank always has its readyState as "complete" 548 // even if it didn't fire a load event yet. We cannot know whether 549 // it will load, so mark it loaded to avoid waiting for it. 550 mLoadState |= eDOMLoaded; 551 } 552 553 AddEventListeners(); 554 } 555 556 void DocAccessible::Shutdown() { 557 if (!mPresShell) { // already shutdown 558 return; 559 } 560 561 #ifdef A11Y_LOG 562 if (logging::IsEnabled(logging::eDocDestroy)) { 563 logging::DocDestroy("document shutdown", mDocumentNode, this); 564 } 565 #endif 566 567 // Mark the document as shutdown before AT is notified about the document 568 // removal from its container (valid for root documents on ATK and due to 569 // some reason for MSAA, refer to bug 757392 for details). 570 MOZ_DIAGNOSTIC_ASSERT(!IsDefunct(), 571 "Already marked defunct. Reentrant shutdown!"); 572 mStateFlags |= eIsDefunct; 573 574 if (mNotificationController) { 575 mNotificationController->Shutdown(); 576 mNotificationController = nullptr; 577 } 578 579 RemoveEventListeners(); 580 581 if (mParent) { 582 DocAccessible* parentDocument = mParent->Document(); 583 if (parentDocument) parentDocument->RemoveChildDocument(this); 584 585 mParent->RemoveChild(this); 586 MOZ_ASSERT(!mParent, "Parent has to be null!"); 587 } 588 589 mPresShell->SetDocAccessible(nullptr); 590 mPresShell = nullptr; // Avoid reentrancy 591 592 // Walk the array backwards because child documents remove themselves from the 593 // array as they are shutdown. 594 int32_t childDocCount = mChildDocuments.Length(); 595 for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { 596 mChildDocuments[idx]->Shutdown(); 597 } 598 599 mChildDocuments.Clear(); 600 // mQueuedCacheUpdates* can contain a reference to this document (ex. if the 601 // doc is scrollable and we're sending a scroll position update). Clear the 602 // map here to avoid creating ref cycles. 603 mQueuedCacheUpdatesArray.Clear(); 604 mQueuedCacheUpdatesHash.Clear(); 605 606 // XXX thinking about ordering? 607 if (mIPCDoc) { 608 MOZ_ASSERT(IPCAccessibilityActive()); 609 mIPCDoc->Shutdown(); 610 MOZ_ASSERT(!mIPCDoc); 611 } 612 613 mDependentIDsHashes.Clear(); 614 mDependentElementsMap.Clear(); 615 mNodeToAccessibleMap.Clear(); 616 617 mAnchorJumpElm = nullptr; 618 mInvalidationList.Clear(); 619 mPendingUpdates.Clear(); 620 621 for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) { 622 LocalAccessible* accessible = iter.Data(); 623 MOZ_ASSERT(accessible); 624 if (accessible) { 625 // This might have been focused with FocusManager::ActiveItemChanged. In 626 // that case, we must notify FocusManager so that it clears the active 627 // item. Otherwise, it will hold on to a defunct Accessible. Normally, 628 // this happens in UnbindFromDocument, but we don't call that when the 629 // whole document shuts down. 630 if (FocusMgr()->WasLastFocused(accessible)) { 631 FocusMgr()->ActiveItemChanged(nullptr); 632 #ifdef A11Y_LOG 633 if (logging::IsEnabled(logging::eFocus)) { 634 logging::ActiveItemChangeCausedBy("doc shutdown", accessible); 635 } 636 #endif 637 } 638 if (!accessible->IsDefunct()) { 639 // Unlink parent to avoid its cleaning overhead in shutdown. 640 accessible->mParent = nullptr; 641 accessible->Shutdown(); 642 } 643 } 644 iter.Remove(); 645 } 646 647 HyperTextAccessible::Shutdown(); 648 649 MOZ_ASSERT(GetAccService()); 650 GetAccService()->NotifyOfDocumentShutdown(this, mDocumentNode); 651 mDocumentNode = nullptr; 652 } 653 654 nsIFrame* DocAccessible::GetFrame() const { 655 nsIFrame* root = nullptr; 656 if (mPresShell) { 657 root = mPresShell->GetRootFrame(); 658 } 659 660 return root; 661 } 662 663 nsINode* DocAccessible::GetNode() const { return mDocumentNode; } 664 665 // DocAccessible protected member 666 nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const { 667 *aRelativeFrame = GetFrame(); 668 669 dom::Document* document = mDocumentNode; 670 dom::Document* parentDoc = nullptr; 671 672 nsRect bounds; 673 while (document) { 674 PresShell* presShell = document->GetPresShell(); 675 if (!presShell) { 676 return nsRect(); 677 } 678 679 nsRect scrollPort; 680 ScrollContainerFrame* sf = presShell->GetRootScrollContainerFrame(); 681 if (sf) { 682 scrollPort = sf->GetScrollPortRect(); 683 } else { 684 nsIFrame* rootFrame = presShell->GetRootFrame(); 685 if (!rootFrame) return nsRect(); 686 687 scrollPort = rootFrame->GetRect(); 688 } 689 690 if (parentDoc) { // After first time thru loop 691 // XXXroc bogus code! scrollPort is relative to the viewport of 692 // this document, but we're intersecting rectangles derived from 693 // multiple documents and assuming they're all in the same coordinate 694 // system. See bug 514117. 695 bounds.IntersectRect(scrollPort, bounds); 696 } else { // First time through loop 697 bounds = scrollPort; 698 } 699 700 document = parentDoc = document->GetInProcessParentDocument(); 701 } 702 703 return bounds; 704 } 705 706 // DocAccessible protected member 707 nsresult DocAccessible::AddEventListeners() { 708 SelectionMgr()->AddDocSelectionListener(mPresShell); 709 710 // Add document observer. 711 mDocumentNode->AddObserver(this); 712 return NS_OK; 713 } 714 715 // DocAccessible protected member 716 nsresult DocAccessible::RemoveEventListeners() { 717 // Remove listeners associated with content documents 718 NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); 719 720 if (mDocumentNode) { 721 mDocumentNode->RemoveObserver(this); 722 } 723 724 if (mScrollWatchTimer) { 725 mScrollWatchTimer->Cancel(); 726 mScrollWatchTimer = nullptr; 727 NS_RELEASE_THIS(); // Kung fu death grip 728 } 729 730 SelectionMgr()->RemoveDocSelectionListener(mPresShell); 731 return NS_OK; 732 } 733 734 void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) { 735 DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure); 736 737 if (docAcc) { 738 // Dispatch a scroll-end for all entries in table. They have not 739 // been scrolled in at least `kScrollEventInterval`. 740 for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done(); 741 iter.Next()) { 742 docAcc->DispatchScrollingEvent(iter.Key(), 743 nsIAccessibleEvent::EVENT_SCROLLING_END); 744 iter.Remove(); 745 } 746 747 if (docAcc->mScrollWatchTimer) { 748 docAcc->mScrollWatchTimer = nullptr; 749 NS_RELEASE(docAcc); // Release kung fu death grip 750 } 751 } 752 } 753 754 void DocAccessible::HandleScroll(nsINode* aTarget) { 755 nsINode* target = aTarget; 756 LocalAccessible* targetAcc = GetAccessible(target); 757 if (!targetAcc && target->IsInNativeAnonymousSubtree()) { 758 // The scroll event for textareas comes from a native anonymous div. We need 759 // the closest non-anonymous ancestor to get the right Accessible. 760 target = target->GetClosestNativeAnonymousSubtreeRootParentOrHost(); 761 targetAcc = GetAccessible(target); 762 } 763 // Regardless of our scroll timer, we need to send a cache update 764 // to ensure the next Bounds() query accurately reflects our position 765 // after scrolling. 766 if (targetAcc) { 767 QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition); 768 } 769 770 const uint32_t kScrollEventInterval = 100; 771 // If we haven't dispatched a scrolling event for a target in at least 772 // kScrollEventInterval milliseconds, dispatch one now. 773 mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) { 774 const TimeStamp now = TimeStamp::Now(); 775 776 if (!lastDispatch || 777 (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) { 778 // We can't fire events on a document whose tree isn't constructed yet. 779 if (HasLoadState(eTreeConstructed)) { 780 DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING); 781 } 782 lastDispatch.InsertOrUpdate(now); 783 } 784 }); 785 786 // If timer callback is still pending, push it 100ms into the future. 787 // When scrolling ends and we don't fire HandleScroll anymore, the 788 // timer callback will fire and dispatch an EVENT_SCROLLING_END. 789 if (!mScrollWatchTimer) { 790 // Can only fail on OOM and in that case we'd crash. 791 mScrollWatchTimer = NS_NewTimer(); 792 NS_ADDREF_THIS(); // Kung fu death grip 793 } 794 mScrollWatchTimer->InitWithNamedFuncCallback( 795 ScrollTimerCallback, this, kScrollEventInterval, nsITimer::TYPE_ONE_SHOT, 796 "a11y::DocAccessible::ScrollPositionDidChange"_ns); 797 } 798 799 std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData( 800 const LocalAccessible* aAcc, bool aShouldScaleByResolution) { 801 nsPoint scrollPoint; 802 nsRect scrollRange; 803 804 if (nsIFrame* frame = aAcc->GetFrame()) { 805 ScrollContainerFrame* sf = aAcc == this 806 ? mPresShell->GetRootScrollContainerFrame() 807 : frame->GetScrollTargetFrame(); 808 809 // If there is no scrollable frame, it's likely a scroll in a popup, like 810 // <select>. Return a scroll offset and range of 0. The scroll info 811 // is currently only used on Android, and popups are rendered natively 812 // there. 813 if (sf) { 814 scrollPoint = sf->GetScrollPosition(); 815 scrollRange = sf->GetScrollRange(); 816 817 if (aShouldScaleByResolution) { 818 scrollPoint = scrollPoint * mPresShell->GetResolution(); 819 scrollRange.ScaleRoundOut(mPresShell->GetResolution()); 820 } 821 } 822 } 823 824 return {scrollPoint, scrollRange}; 825 } 826 827 //////////////////////////////////////////////////////////////////////////////// 828 // nsIDocumentObserver 829 830 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible) 831 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible) 832 833 // When a reflected element IDL attribute changes, we might get the following 834 // synchronous calls: 835 // 1. AttributeWillChange for the element. 836 // 2. AttributeWillChange for the content attribute. 837 // 3. AttributeChanged for the content attribute. 838 // 4. AttributeChanged for the element. 839 // Since the content attribute value is "" for any element, we won't always get 840 // 2 or 3. Even if we do, they might occur after the element has already 841 // changed, which means we can't detect any relevant state changes there; e.g. 842 // mPrevStateBits. Thus, we need 1 and 4, and we must ignore 2 and 3. To 843 // facilitate this, sIsAttrElementChanging will be set to true for 2 and 3. 844 static bool sIsAttrElementChanging = false; 845 846 void DocAccessible::AttributeWillChange(dom::Element* aElement, 847 int32_t aNameSpaceID, 848 nsAtom* aAttribute, 849 AttrModType aModType) { 850 if (sIsAttrElementChanging) { 851 // See the comment above the definition of sIsAttrElementChanging. 852 return; 853 } 854 LocalAccessible* accessible = GetAccessible(aElement); 855 if (!accessible) { 856 if (aElement != mContent) return; 857 858 accessible = this; 859 } 860 861 // Update dependent IDs cache. Take care of elements that are accessible 862 // because dependent IDs cache doesn't contain IDs from non accessible 863 // elements. We do this for attribute additions as well because there might 864 // be an ElementInternals default value. 865 RemoveDependentIDsFor(accessible, aAttribute); 866 RemoveDependentElementsFor(accessible, aAttribute); 867 868 if (aAttribute == nsGkAtoms::id) { 869 if (accessible->IsActiveDescendant()) { 870 RefPtr<AccEvent> event = 871 new AccStateChangeEvent(accessible, states::ACTIVE, false); 872 FireDelayedEvent(event); 873 } 874 875 RelocateARIAOwnedIfNeeded(aElement); 876 } 877 878 if (aAttribute == nsGkAtoms::aria_activedescendant) { 879 if (LocalAccessible* activeDescendant = accessible->CurrentItem()) { 880 RefPtr<AccEvent> event = 881 new AccStateChangeEvent(activeDescendant, states::ACTIVE, false); 882 FireDelayedEvent(event); 883 } 884 } 885 886 if ((aModType == AttrModType::Modification || 887 aModType == AttrModType::Removal)) { 888 // If this is a modification or removal of aria-actions, and the 889 // accessible's name is calculated by the subtree, there may be a change to 890 // the name of the accessible. 891 // If this is a modification or removal of an id, an aria-actions relation 892 // might be severed, and thus change the name of any ancestors. 893 // XXX: We don't track the actual changes, so the name change event might 894 // be fired for not actual name change, but better to fire an event than to 895 // not. 896 MaybeHandleChangeToAriaActions(accessible, aAttribute); 897 } 898 899 // If attribute affects accessible's state, store the old state so we can 900 // later compare it against the state of the accessible after the attribute 901 // change. 902 if (accessible->AttributeChangesState(aAttribute)) { 903 mPrevStateBits = accessible->State(); 904 } else { 905 mPrevStateBits = 0; 906 } 907 } 908 909 void DocAccessible::AttributeChanged(dom::Element* aElement, 910 int32_t aNameSpaceID, nsAtom* aAttribute, 911 AttrModType aModType, 912 const nsAttrValue* aOldValue) { 913 if (sIsAttrElementChanging) { 914 // See the comment above the definition of sIsAttrElementChanging. 915 return; 916 } 917 NS_ASSERTION(!IsDefunct(), 918 "Attribute changed called on defunct document accessible!"); 919 920 // Proceed even if the element is not accessible because element may become 921 // accessible if it gets certain attribute. 922 if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return; 923 924 // Update the accessible tree on aria-hidden change. Make sure to not create 925 // a tree under aria-hidden='true'. 926 if (aAttribute == nsGkAtoms::aria_hidden) { 927 if (aria::IsValidARIAHidden(aElement)) { 928 ContentRemoved(aElement); 929 } else { 930 ContentInserted(aElement, aElement->GetNextSibling()); 931 } 932 return; 933 } 934 935 if (aAttribute == nsGkAtoms::slot && 936 !aElement->GetFlattenedTreeParentNode() && aElement != mContent) { 937 // Element is inside a shadow host but is no longer slotted. 938 mDoc->ContentRemoved(aElement); 939 return; 940 } 941 942 LocalAccessible* accessible = GetAccessible(aElement); 943 if (!accessible) { 944 if (mContent == aElement) { 945 // The attribute change occurred on the root content of this 946 // DocAccessible, so handle it as an attribute change on this. 947 accessible = this; 948 } else { 949 if (aModType == AttrModType::Addition && 950 aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) { 951 // The element doesn't have an Accessible, but a global ARIA attribute 952 // was just added, which means we should probably create an Accessible. 953 ContentInserted(aElement, aElement->GetNextSibling()); 954 return; 955 } 956 // The element doesn't have an Accessible, so ignore the attribute 957 // change. 958 return; 959 } 960 } 961 962 MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(), 963 "DOM attribute change on an accessible detached from the tree"); 964 965 if (aAttribute == nsGkAtoms::id) { 966 dom::Element* elm = accessible->Elm(); 967 RelocateARIAOwnedIfNeeded(elm); 968 ARIAActiveDescendantIDMaybeMoved(accessible); 969 QueueCacheUpdate(accessible, CacheDomain::DOMNodeIDAndClass); 970 QueueCacheUpdateForDependentRelations(accessible, aOldValue); 971 } 972 973 // The activedescendant universal property redirects accessible focus events 974 // to the element with the id that activedescendant points to. Make sure 975 // the tree up to date before processing. In other words, when a node has just 976 // been inserted, the tree won't be up to date yet, so we must always schedule 977 // an async notification so that a newly inserted node will be present in 978 // the tree. 979 if (aAttribute == nsGkAtoms::aria_activedescendant) { 980 mNotificationController 981 ->ScheduleNotification<DocAccessible, LocalAccessible>( 982 this, &DocAccessible::ARIAActiveDescendantChanged, accessible); 983 } 984 985 // Defer to accessible any needed actions like changing states or emiting 986 // events. 987 accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue, 988 mPrevStateBits); 989 990 // Update dependent IDs cache. We handle elements with accessibles. 991 // If the accessible or element with the ID doesn't exist yet the cache will 992 // be updated when they are added. 993 if (IsAdditionOrModification(aModType)) { 994 AddDependentIDsFor(accessible, aAttribute); 995 AddDependentElementsFor(accessible, aAttribute); 996 997 // If this is a modification or addition of aria-actions, and the 998 // accessible's name is calculated by the subtree, there may be a change to 999 // the name of the accessible. 1000 // If this is a modification or addition of an id, an aria-actions relation 1001 // might be restored, and thus change the name of any ancestors. 1002 // XXX: We don't track the actual changes, so the name change event might 1003 // be fired for not actual name change, but better to fire an event than to 1004 // not. 1005 // In the case of a modification we may have already queued a name 1006 // change event in the `AttributeWillChange` stage, but we rely on 1007 // EventQueue to quash any duplicates. 1008 MaybeHandleChangeToAriaActions(accessible, aAttribute); 1009 } 1010 } 1011 1012 void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement, 1013 nsAtom* aAttribute, 1014 AttrModType aModType) { 1015 NS_ASSERTION(!IsDefunct(), 1016 "Attribute changed called on defunct document accessible!"); 1017 1018 if (aElement->HasAttr(aAttribute)) { 1019 return; 1020 } 1021 1022 AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType); 1023 } 1024 1025 void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement, 1026 nsAtom* aAttribute, 1027 AttrModType aModType) { 1028 NS_ASSERTION(!IsDefunct(), 1029 "Attribute changed called on defunct document accessible!"); 1030 1031 if (aElement->HasAttr(aAttribute)) { 1032 return; 1033 } 1034 1035 AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr); 1036 } 1037 1038 void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) { 1039 if (dom::Element* elm = aAccessible->Elm()) { 1040 nsAutoString id; 1041 if (dom::Element* activeDescendantElm = 1042 nsCoreUtils::GetAriaActiveDescendantElement(elm)) { 1043 LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm); 1044 if (activeDescendant) { 1045 RefPtr<AccEvent> event = 1046 new AccStateChangeEvent(activeDescendant, states::ACTIVE, true); 1047 FireDelayedEvent(event); 1048 if (aAccessible->IsActiveWidget()) { 1049 FocusMgr()->ActiveItemChanged(activeDescendant, false); 1050 #ifdef A11Y_LOG 1051 if (logging::IsEnabled(logging::eFocus)) { 1052 logging::ActiveItemChangeCausedBy("ARIA activedescedant changed", 1053 activeDescendant); 1054 } 1055 #endif 1056 } 1057 return; 1058 } 1059 } 1060 1061 // aria-activedescendant was cleared or changed to a non-existent node. 1062 // Move focus back to the element itself if it has DOM focus. 1063 if (aAccessible->IsActiveWidget()) { 1064 FocusMgr()->ActiveItemChanged(aAccessible, false); 1065 #ifdef A11Y_LOG 1066 if (logging::IsEnabled(logging::eFocus)) { 1067 logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared", 1068 aAccessible); 1069 } 1070 #endif 1071 } 1072 } 1073 } 1074 1075 void DocAccessible::ContentAppended(nsIContent* aFirstNewContent, 1076 const ContentAppendInfo&) { 1077 MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent); 1078 } 1079 1080 void DocAccessible::ElementStateChanged(dom::Document* aDocument, 1081 dom::Element* aElement, 1082 dom::ElementState aStateMask) { 1083 LocalAccessible* accessible = 1084 aElement == mContent ? this : GetAccessible(aElement); 1085 1086 if (!accessible) { 1087 return; 1088 } 1089 1090 if (aStateMask.HasState(dom::ElementState::READWRITE) && 1091 !accessible->IsTextField()) { 1092 const bool isEditable = 1093 aElement->State().HasState(dom::ElementState::READWRITE); 1094 RefPtr<AccEvent> event = 1095 new AccStateChangeEvent(accessible, states::EDITABLE, isEditable); 1096 FireDelayedEvent(event); 1097 if (accessible == this || aElement->IsHTMLElement(nsGkAtoms::article)) { 1098 // We want <article> to behave like a document in terms of readonly state. 1099 event = 1100 new AccStateChangeEvent(accessible, states::READONLY, !isEditable); 1101 FireDelayedEvent(event); 1102 } 1103 1104 if (aElement->HasAttr(nsGkAtoms::aria_owns)) { 1105 // If this has aria-owns, update children that are relocated into here. 1106 // If we are becoming editable, put them back into their original 1107 // containers, if we are becoming readonly, acquire them. 1108 mNotificationController->ScheduleRelocation(accessible); 1109 } 1110 1111 // If this is a node inside of a newly editable subtree, it needs to be 1112 // un-aria-owned. And inversely, if the node becomes uneditable, allow the 1113 // node to be aria-owned. 1114 RelocateARIAOwnedIfNeeded(aElement); 1115 } 1116 1117 if (aStateMask.HasState(dom::ElementState::CHECKED)) { 1118 LocalAccessible* widget = accessible->ContainerWidget(); 1119 if (widget && widget->IsSelect()) { 1120 // Changing selection here changes what we cache for 1121 // the viewport. 1122 SetViewportCacheDirty(true); 1123 AccSelChangeEvent::SelChangeType selChangeType = 1124 aElement->State().HasState(dom::ElementState::CHECKED) 1125 ? AccSelChangeEvent::eSelectionAdd 1126 : AccSelChangeEvent::eSelectionRemove; 1127 RefPtr<AccEvent> event = 1128 new AccSelChangeEvent(widget, accessible, selChangeType); 1129 FireDelayedEvent(event); 1130 return; 1131 } 1132 1133 RefPtr<AccEvent> event = new AccStateChangeEvent( 1134 accessible, states::CHECKED, 1135 aElement->State().HasState(dom::ElementState::CHECKED)); 1136 FireDelayedEvent(event); 1137 } 1138 1139 if (aStateMask.HasState(dom::ElementState::INVALID)) { 1140 RefPtr<AccEvent> event = 1141 new AccStateChangeEvent(accessible, states::INVALID); 1142 FireDelayedEvent(event); 1143 } 1144 1145 if (aStateMask.HasState(dom::ElementState::REQUIRED)) { 1146 RefPtr<AccEvent> event = 1147 new AccStateChangeEvent(accessible, states::REQUIRED); 1148 FireDelayedEvent(event); 1149 } 1150 1151 if (aStateMask.HasState(dom::ElementState::VISITED)) { 1152 RefPtr<AccEvent> event = 1153 new AccStateChangeEvent(accessible, states::TRAVERSED, true); 1154 FireDelayedEvent(event); 1155 } 1156 1157 // We only expose dom::ElementState::DEFAULT on buttons, but we can get 1158 // notifications for other controls like checkboxes. 1159 if (aStateMask.HasState(dom::ElementState::DEFAULT) && 1160 accessible->IsButton()) { 1161 RefPtr<AccEvent> event = 1162 new AccStateChangeEvent(accessible, states::DEFAULT); 1163 FireDelayedEvent(event); 1164 } 1165 1166 if (aStateMask.HasState(dom::ElementState::INDETERMINATE)) { 1167 RefPtr<AccEvent> event = new AccStateChangeEvent(accessible, states::MIXED); 1168 FireDelayedEvent(event); 1169 } 1170 1171 if (aStateMask.HasState(dom::ElementState::DISABLED) && 1172 !nsAccUtils::ARIAAttrValueIs(aElement, nsGkAtoms::aria_disabled, 1173 nsGkAtoms::_true, eCaseMatters)) { 1174 // The DOM disabled state has changed and there is no aria-disabled="true" 1175 // taking precedence. 1176 RefPtr<AccEvent> event = 1177 new AccStateChangeEvent(accessible, states::UNAVAILABLE); 1178 FireDelayedEvent(event); 1179 event = new AccStateChangeEvent(accessible, states::ENABLED); 1180 FireDelayedEvent(event); 1181 // This likely changes focusability as well. 1182 event = new AccStateChangeEvent(accessible, states::FOCUSABLE); 1183 FireDelayedEvent(event); 1184 } 1185 } 1186 1187 void DocAccessible::CharacterDataWillChange(nsIContent* aContent, 1188 const CharacterDataChangeInfo&) {} 1189 1190 void DocAccessible::CharacterDataChanged(nsIContent* aContent, 1191 const CharacterDataChangeInfo&) { 1192 MaybeHandleChangeToHiddenNameOrDescription(aContent); 1193 } 1194 1195 void DocAccessible::ContentInserted(nsIContent* aChild, 1196 const ContentInsertInfo&) { 1197 MaybeHandleChangeToHiddenNameOrDescription(aChild); 1198 } 1199 1200 void DocAccessible::ContentWillBeRemoved(nsIContent* aChildNode, 1201 const ContentRemoveInfo&) { 1202 #ifdef A11Y_LOG 1203 if (logging::IsEnabled(logging::eTree)) { 1204 logging::MsgBegin("TREE", "DOM content removed; doc: %p", this); 1205 logging::Node("container node", aChildNode->GetParent()); 1206 logging::Node("content node", aChildNode); 1207 logging::MsgEnd(); 1208 } 1209 #endif 1210 ContentRemoved(aChildNode); 1211 } 1212 1213 void DocAccessible::ParentChainChanged(nsIContent* aContent) {} 1214 1215 //////////////////////////////////////////////////////////////////////////////// 1216 // LocalAccessible 1217 1218 #ifdef A11Y_LOG 1219 nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) { 1220 if (logging::IsEnabled(logging::eDocLoad)) { 1221 logging::DocLoadEventHandled(aEvent); 1222 } 1223 1224 return HyperTextAccessible::HandleAccEvent(aEvent); 1225 } 1226 #endif 1227 1228 //////////////////////////////////////////////////////////////////////////////// 1229 // Public members 1230 1231 nsPresContext* DocAccessible::PresContext() const { 1232 return mPresShell->GetPresContext(); 1233 } 1234 1235 void* DocAccessible::GetNativeWindow() const { 1236 if (!mPresShell) { 1237 return nullptr; 1238 } 1239 if (nsIWidget* widget = mPresShell->GetRootWidget()) { 1240 return widget->GetNativeData(NS_NATIVE_WINDOW); 1241 } 1242 return nullptr; 1243 } 1244 1245 LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree( 1246 void* aUniqueID) { 1247 LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID); 1248 if (child) return child; 1249 1250 uint32_t childDocCount = mChildDocuments.Length(); 1251 for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) { 1252 DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx); 1253 child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID); 1254 if (child) return child; 1255 } 1256 1257 return nullptr; 1258 } 1259 1260 LocalAccessible* DocAccessible::GetAccessibleOrContainer( 1261 nsINode* aNode, bool aNoContainerIfPruned) const { 1262 if (!aNode || !aNode->GetComposedDoc()) { 1263 return nullptr; 1264 } 1265 1266 nsINode* start = aNode; 1267 if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) { 1268 // This can happen, for example, when called within 1269 // SelectionManager::ProcessSelectionChanged due to focusing a direct 1270 // child of a shadow root. 1271 // GetFlattenedTreeParent works on children of a shadow root, but not the 1272 // shadow root itself. 1273 start = shadowRoot->GetHost(); 1274 if (!start) { 1275 return nullptr; 1276 } 1277 } 1278 1279 for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) { 1280 // No container if is inside of aria-hidden subtree. 1281 if (aNoContainerIfPruned && currNode->IsElement() && 1282 aria::IsValidARIAHidden(currNode->AsElement())) { 1283 return nullptr; 1284 } 1285 1286 // Check if node is in zero-sized map 1287 if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) { 1288 if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) { 1289 if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent()) 1290 .IsEmpty()) { 1291 return nullptr; 1292 } 1293 } 1294 } 1295 1296 if (LocalAccessible* accessible = GetAccessible(currNode)) { 1297 return accessible; 1298 } 1299 } 1300 1301 return nullptr; 1302 } 1303 1304 LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const { 1305 return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode()) 1306 : nullptr; 1307 } 1308 1309 LocalAccessible* DocAccessible::GetAccessibleOrDescendant( 1310 nsINode* aNode) const { 1311 LocalAccessible* acc = GetAccessible(aNode); 1312 if (acc) return acc; 1313 1314 if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) { 1315 // If the node is the doc's body or root element, return the doc accessible. 1316 return const_cast<DocAccessible*>(this); 1317 } 1318 1319 acc = GetContainerAccessible(aNode); 1320 if (acc) { 1321 TreeWalker walker(acc, aNode->AsContent(), 1322 TreeWalker::eWalkCache | TreeWalker::eScoped); 1323 return walker.Next(); 1324 } 1325 1326 return nullptr; 1327 } 1328 1329 void DocAccessible::BindToDocument(LocalAccessible* aAccessible, 1330 const nsRoleMapEntry* aRoleMapEntry) { 1331 // Put into DOM node cache. 1332 if (aAccessible->IsNodeMapEntry()) { 1333 mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible); 1334 } 1335 1336 // Put into unique ID cache. 1337 mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible}); 1338 1339 aAccessible->SetRoleMapEntry(aRoleMapEntry); 1340 1341 if (aAccessible->HasOwnContent()) { 1342 AddDependentIDsFor(aAccessible); 1343 AddDependentElementsFor(aAccessible); 1344 1345 nsIContent* content = aAccessible->GetContent(); 1346 if (content->IsElement() && 1347 nsAccUtils::HasARIAAttr(content->AsElement(), nsGkAtoms::aria_owns)) { 1348 mNotificationController->ScheduleRelocation(aAccessible); 1349 } 1350 } 1351 1352 if (mIPCDoc && HasLoadState(eTreeConstructed)) { 1353 // Child process and not in initial tree construction phase. 1354 // We need to track inserted accessibles so we don't mark them as moved 1355 // before their initial show event. 1356 // If this is the initial tree construction, we will push the tree to the 1357 // parent process before we process moves, so we will always need to mark a 1358 // relocated accessible as moved. 1359 mInsertedAccessibles.EnsureInserted(aAccessible); 1360 } 1361 1362 QueueCacheUpdateForDependentRelations(aAccessible); 1363 } 1364 1365 void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) { 1366 NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()), 1367 "Unbinding the unbound accessible!"); 1368 1369 // Fire focus event on accessible having DOM focus if last focus was removed 1370 // from the tree. 1371 if (FocusMgr()->WasLastFocused(aAccessible)) { 1372 FocusMgr()->ActiveItemChanged(nullptr); 1373 #ifdef A11Y_LOG 1374 if (logging::IsEnabled(logging::eFocus)) { 1375 logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible); 1376 } 1377 #endif 1378 } 1379 1380 // Remove an accessible from node-to-accessible map if it exists there. 1381 if (aAccessible->IsNodeMapEntry() && 1382 mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) { 1383 mNodeToAccessibleMap.Remove(aAccessible->GetNode()); 1384 } 1385 1386 aAccessible->mStateFlags |= eIsNotInDocument; 1387 1388 // Update XPCOM part. 1389 xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this); 1390 if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible); 1391 1392 void* uniqueID = aAccessible->UniqueID(); 1393 1394 NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!"); 1395 aAccessible->Shutdown(); 1396 1397 mAccessibleCache.Remove(uniqueID); 1398 } 1399 1400 void DocAccessible::ContentInserted(nsIContent* aStartChildNode, 1401 nsIContent* aEndChildNode) { 1402 // Ignore content insertions until we constructed accessible tree. Otherwise 1403 // schedule tree update on content insertion after layout. 1404 if (!mNotificationController || !HasLoadState(eTreeConstructed)) { 1405 return; 1406 } 1407 1408 // The frame constructor guarantees that only ranges with the same parent 1409 // arrive here in presence of dynamic changes to the page, see 1410 // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers. 1411 nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode(); 1412 if (!parent) { 1413 return; 1414 } 1415 1416 LocalAccessible* container = AccessibleOrTrueContainer(parent); 1417 if (!container) { 1418 return; 1419 } 1420 1421 AutoTArray<nsCOMPtr<nsIContent>, 10> list; 1422 for (nsIContent* node = aStartChildNode; node != aEndChildNode; 1423 node = node->GetNextSibling()) { 1424 MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode()); 1425 if (PruneOrInsertSubtree(node)) { 1426 list.AppendElement(node); 1427 } 1428 } 1429 1430 mNotificationController->ScheduleContentInsertion(container, list); 1431 } 1432 1433 void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) { 1434 if (mPendingUpdates.Contains(aContent)) { 1435 return; 1436 } 1437 mPendingUpdates.AppendElement(aContent); 1438 mNotificationController->ScheduleProcessing(); 1439 } 1440 1441 void DocAccessible::ProcessPendingUpdates() { 1442 auto updates = std::move(mPendingUpdates); 1443 for (auto update : updates) { 1444 if (update->GetComposedDoc() != mDocumentNode) { 1445 continue; 1446 } 1447 // The pruning logic will take care of avoiding unnecessary notifications. 1448 ContentInserted(update, update->GetNextSibling()); 1449 } 1450 } 1451 1452 bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) { 1453 AUTO_PROFILER_MARKER_TEXT("DocAccessible::PruneOrInsertSubtree", A11Y, {}, 1454 ""_ns); 1455 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_PruneOrInsertSubtree> 1456 autoRecording; 1457 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 1458 1459 bool insert = false; 1460 1461 // In the case that we are, or are in, a shadow host, we need to assure 1462 // some accessibles are removed if they are not rendered anymore. 1463 nsIContent* shadowHost = 1464 aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost(); 1465 if (shadowHost) { 1466 // Check all explicit children in the host, if they are not slotted 1467 // then remove their accessibles and subtrees. 1468 for (nsIContent* childNode = shadowHost->GetFirstChild(); childNode; 1469 childNode = childNode->GetNextSibling()) { 1470 if (!childNode->GetPrimaryFrame() && 1471 !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) { 1472 ContentRemoved(childNode); 1473 } 1474 } 1475 1476 // If this is a slot, check to see if its fallback content is rendered, 1477 // if not - remove it. 1478 if (aRoot->IsHTMLElement(nsGkAtoms::slot)) { 1479 for (nsIContent* childNode = aRoot->GetFirstChild(); childNode; 1480 childNode = childNode->GetNextSibling()) { 1481 if (!childNode->GetPrimaryFrame() && 1482 !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) { 1483 ContentRemoved(childNode); 1484 } 1485 } 1486 } 1487 } 1488 1489 // If we already have an accessible, check if we need to remove it, recreate 1490 // it, or keep it in place. 1491 LocalAccessible* acc = GetAccessible(aRoot); 1492 if (acc) { 1493 MOZ_ASSERT(aRoot == acc->GetContent(), 1494 "LocalAccessible has differing content!"); 1495 #ifdef A11Y_LOG 1496 if (logging::IsEnabled(logging::eTree)) { 1497 logging::MsgBegin( 1498 "TREE", "inserted content already has accessible; doc: %p", this); 1499 logging::Node("content node", aRoot); 1500 logging::AccessibleInfo("accessible node", acc); 1501 logging::MsgEnd(); 1502 } 1503 #endif 1504 1505 nsIFrame* frame = acc->GetFrame(); 1506 if (frame) { 1507 acc->MaybeQueueCacheUpdateForStyleChanges(); 1508 } 1509 1510 // LocalAccessible has no frame and it's not display:contents. Remove it. 1511 // As well as removing the a11y subtree, we must also remove Accessibles 1512 // for DOM descendants, since some of these might be relocated Accessibles 1513 // and their DOM nodes are now hidden as well. 1514 if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) { 1515 ContentRemoved(aRoot); 1516 return false; 1517 } 1518 1519 if (!frame && aRoot->IsElement() && 1520 aRoot->AsElement()->IsDisplayContents() && acc->mOldComputedStyle) { 1521 // This element has probably just become display: contents. We won't be 1522 // notified of a computed style change in this case. Also, the bounds we 1523 // cached previously are now invalid, but our normal bounds change 1524 // notifications won't fire either. Therefore, queue cache updates for 1525 // both. 1526 QueueCacheUpdate(acc, CacheDomain::Style | CacheDomain::Bounds); 1527 } 1528 1529 // If the frame is hidden because its ancestor is specified with 1530 // `content-visibility: hidden`, remove its Accessible. 1531 if (frame && frame->IsHiddenByContentVisibilityOnAnyAncestor( 1532 nsIFrame::IncludeContentVisibility::Hidden)) { 1533 ContentRemoved(aRoot); 1534 return false; 1535 } 1536 1537 // If it's a XULLabel it was probably reframed because a `value` attribute 1538 // was added. The accessible creates its text leaf upon construction, so we 1539 // need to recreate. Remove it, and schedule for reconstruction. 1540 if (acc->IsXULLabel()) { 1541 ContentRemoved(acc); 1542 return true; 1543 } 1544 1545 if (frame && frame->IsReplaced() && frame->AccessibleType() == eImageType && 1546 !aRoot->IsHTMLElement(nsGkAtoms::img)) { 1547 // This is an image specified using the CSS content property which 1548 // replaces the content of the node. Its frame might be reconstructed, 1549 // which means its alt text might have changed. We expose the alt text 1550 // as the name, so fire a name change event. 1551 // We will schedule this for reinsertion below, and prune any children if 1552 // they exist. 1553 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, acc); 1554 } else if (frame && 1555 (acc->IsImage() != (frame->AccessibleType() == eImageType))) { 1556 // It is a broken image that is being reframed because it either got 1557 // or lost an `alt` tag that would rerender this node as text. 1558 ContentRemoved(aRoot); 1559 return true; 1560 } 1561 1562 // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible, 1563 // we need to recreate the LocalAccessible. This can happen for embed or 1564 // object elements if their embedded content changes to be web content. 1565 if (frame && !acc->IsOuterDoc() && 1566 frame->AccessibleType() == eOuterDocType) { 1567 ContentRemoved(aRoot); 1568 return true; 1569 } 1570 1571 // If the content is focused, and is being re-framed, reset the selection 1572 // listener for the node because the previous selection listener is on the 1573 // old frame. 1574 if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) { 1575 SelectionMgr()->SetControlSelectionListener(aRoot->AsElement()); 1576 } 1577 1578 // If the accessible is a table, or table part, its layout table 1579 // status may have changed. We need to invalidate the associated 1580 // mac table cache, which listens for the following event. We don't 1581 // use this cache when the core cache is enabled, so to minimise event 1582 // traffic only fire this event when that cache is off. 1583 if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) { 1584 LocalAccessible* table = nsAccUtils::TableFor(acc); 1585 if (table && table->IsTable()) { 1586 QueueCacheUpdate(table, CacheDomain::Table); 1587 } 1588 } 1589 1590 // The accessible can be reparented or reordered in its parent. 1591 // We schedule it for reinsertion. For example, a slotted element 1592 // can change its slot attribute to a different slot. 1593 insert = true; 1594 1595 // If the frame is invisible, remove it. 1596 // Normally, layout sends explicit a11y notifications for visibility 1597 // changes (see SendA11yNotifications in RestyleManager). However, if a 1598 // visibility change also reconstructs the frame, we must handle it here. 1599 if (frame && !frame->StyleVisibility()->IsVisible()) { 1600 ContentRemoved(aRoot); 1601 // There might be visible descendants, so we want to walk the subtree. 1602 // However, we know we don't want to reinsert this node, so we set insert 1603 // to false. 1604 insert = false; 1605 } 1606 } else { 1607 // If there is no current accessible, and the node has a frame, or is 1608 // display:contents, schedule it for insertion. 1609 if (aRoot->GetPrimaryFrame() || 1610 nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) { 1611 // This may be a new subtree, the insertion process will recurse through 1612 // its descendants. 1613 if (!GetAccessibleOrDescendant(aRoot)) { 1614 return true; 1615 } 1616 1617 // Content is not an accessible, but has accessible descendants. 1618 // We schedule this container for insertion strictly for the case where it 1619 // itself now needs an accessible. We will still need to recurse into the 1620 // descendant content to prune accessibles, and in all likelyness to 1621 // insert accessibles since accessible insertions will likeley get missed 1622 // in an existing subtree. 1623 insert = true; 1624 } 1625 } 1626 1627 if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) { 1628 AutoTArray<nsCOMPtr<nsIContent>, 10> list; 1629 dom::AllChildrenIterator iter = 1630 dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true); 1631 while (nsIContent* childNode = iter.GetNextChild()) { 1632 if (PruneOrInsertSubtree(childNode)) { 1633 list.AppendElement(childNode); 1634 } 1635 } 1636 1637 if (!list.IsEmpty()) { 1638 mNotificationController->ScheduleContentInsertion(container, list); 1639 } 1640 } 1641 1642 return insert; 1643 } 1644 1645 void DocAccessible::RecreateAccessible(nsIContent* aContent) { 1646 #ifdef A11Y_LOG 1647 if (logging::IsEnabled(logging::eTree)) { 1648 logging::MsgBegin("TREE", "accessible recreated"); 1649 logging::Node("content", aContent); 1650 logging::MsgEnd(); 1651 } 1652 #endif 1653 1654 // XXX: we shouldn't recreate whole accessible subtree, instead we should 1655 // subclass hide and show events to handle them separately and implement their 1656 // coalescence with normal hide and show events. Note, in this case they 1657 // should be coalesced with normal show/hide events. 1658 ContentRemoved(aContent); 1659 ContentInserted(aContent, aContent->GetNextSibling()); 1660 } 1661 1662 void DocAccessible::ProcessInvalidationList() { 1663 // Invalidate children of container accessible for each element in 1664 // invalidation list. Allow invalidation list insertions while container 1665 // children are recached. 1666 for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) { 1667 nsIContent* content = mInvalidationList[idx]; 1668 if (!HasAccessible(content) && content->HasID()) { 1669 LocalAccessible* container = GetContainerAccessible(content); 1670 if (container) { 1671 // Check if the node is a target of aria-owns, and if so, don't process 1672 // it here and let DoARIAOwnsRelocation process it. 1673 AttrRelProviders* list = GetRelProviders( 1674 content->AsElement(), nsDependentAtomString(content->GetID())); 1675 bool shouldProcess = !!list; 1676 if (shouldProcess) { 1677 for (uint32_t idx = 0; idx < list->Length(); idx++) { 1678 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) { 1679 shouldProcess = false; 1680 break; 1681 } 1682 } 1683 1684 if (shouldProcess) { 1685 ProcessContentInserted(container, content); 1686 } 1687 } 1688 } 1689 } 1690 } 1691 1692 mInvalidationList.Clear(); 1693 } 1694 1695 void DocAccessible::ProcessQueuedCacheUpdates(uint64_t aInitialDomains) { 1696 AUTO_PROFILER_MARKER_UNTYPED("DocAccessible::ProcessQueuedCacheUpdates", A11Y, 1697 {}); 1698 PerfStats::AutoMetricRecording< 1699 PerfStats::Metric::A11Y_ProcessQueuedCacheUpdate> 1700 autoRecording; 1701 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 1702 1703 nsTArray<CacheData> data; 1704 for (auto [acc, domain] : mQueuedCacheUpdatesArray) { 1705 if (acc && acc->IsInDocument() && !acc->IsDefunct()) { 1706 RefPtr<AccAttributes> fields = acc->BundleFieldsForCache( 1707 domain, CacheUpdateType::Update, aInitialDomains); 1708 1709 if (fields->Count()) { 1710 data.AppendElement(CacheData( 1711 acc->IsDoc() ? 0 : reinterpret_cast<uint64_t>(acc->UniqueID()), 1712 fields)); 1713 } 1714 } 1715 } 1716 1717 mQueuedCacheUpdatesArray.Clear(); 1718 mQueuedCacheUpdatesHash.Clear(); 1719 1720 if (mViewportCacheDirty) { 1721 RefPtr<AccAttributes> fields = 1722 BundleFieldsForCache(CacheDomain::Viewport, CacheUpdateType::Update); 1723 if (fields->Count()) { 1724 data.AppendElement(CacheData(0, fields)); 1725 } 1726 mViewportCacheDirty = false; 1727 } 1728 1729 if (data.Length()) { 1730 IPCDoc()->SendCache(CacheUpdateType::Update, data); 1731 } 1732 } 1733 1734 void DocAccessible::SendAccessiblesWillMove() { 1735 if (!mIPCDoc) { 1736 return; 1737 } 1738 nsTArray<uint64_t> ids; 1739 for (LocalAccessible* acc : mMovedAccessibles) { 1740 // If acc is defunct or not in a document, it was removed after it was 1741 // moved. 1742 if (!acc->IsDefunct() && acc->IsInDocument()) { 1743 ids.AppendElement(reinterpret_cast<uintptr_t>(acc->UniqueID())); 1744 // acc might have been re-parented. Since we cache bounds relative to the 1745 // parent, we need to update the cache. 1746 QueueCacheUpdate(acc, CacheDomain::Bounds); 1747 } 1748 } 1749 if (!ids.IsEmpty()) { 1750 mIPCDoc->SendAccessiblesWillMove(ids); 1751 } 1752 } 1753 1754 LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap( 1755 nsINode* aNode) const { 1756 if (!aNode->IsContent() || 1757 !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) { 1758 return GetAccessible(aNode); 1759 } 1760 1761 // XXX Bug 135040, incorrect when multiple images use the same map. 1762 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); 1763 nsImageFrame* imageFrame = do_QueryFrame(frame); 1764 if (imageFrame) { 1765 LocalAccessible* parent = GetAccessible(imageFrame->GetContent()); 1766 if (parent) { 1767 if (HTMLImageMapAccessible* imageMap = parent->AsImageMap()) { 1768 return imageMap->GetChildAccessibleFor(aNode); 1769 } 1770 return nullptr; 1771 } 1772 } 1773 1774 return GetAccessible(aNode); 1775 } 1776 1777 //////////////////////////////////////////////////////////////////////////////// 1778 // Protected members 1779 1780 void DocAccessible::NotifyOfLoading(bool aIsReloading) { 1781 // Mark the document accessible as loading, if it stays alive then we'll mark 1782 // it as loaded when we receive proper notification. 1783 mLoadState &= ~eDOMLoaded; 1784 1785 if (!IsLoadEventTarget()) return; 1786 1787 if (aIsReloading && !mLoadEventType && 1788 // We can't fire events on a document whose tree isn't constructed yet. 1789 HasLoadState(eTreeConstructed)) { 1790 // Fire reload and state busy events on existing document accessible while 1791 // event from user input flag can be calculated properly and accessible 1792 // is alive. When new document gets loaded then this one is destroyed. 1793 RefPtr<AccEvent> reloadEvent = 1794 new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this); 1795 nsEventShell::FireEvent(reloadEvent); 1796 } 1797 1798 // Fire state busy change event. Use delayed event since we don't care 1799 // actually if event isn't delivered when the document goes away like a shot. 1800 RefPtr<AccEvent> stateEvent = 1801 new AccStateChangeEvent(this, states::BUSY, true); 1802 FireDelayedEvent(stateEvent); 1803 } 1804 1805 void DocAccessible::DoInitialUpdate() { 1806 AUTO_PROFILER_MARKER_UNTYPED("DocAccessible::DoInitialUpdate", A11Y, {}); 1807 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_DoInitialUpdate> 1808 autoRecording; 1809 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 1810 1811 if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) { 1812 mDocFlags |= eTopLevelContentDocInProcess; 1813 if (IPCAccessibilityActive()) { 1814 nsIDocShell* docShell = mDocumentNode->GetDocShell(); 1815 if (RefPtr<dom::BrowserChild> browserChild = 1816 dom::BrowserChild::GetFrom(docShell)) { 1817 // In content processes, top level content documents are always 1818 // RootAccessibles. 1819 MOZ_ASSERT(IsRoot()); 1820 DocAccessibleChild* ipcDoc = IPCDoc(); 1821 if (!ipcDoc) { 1822 ipcDoc = new DocAccessibleChild(this, browserChild); 1823 MOZ_RELEASE_ASSERT(browserChild->SendPDocAccessibleConstructor( 1824 ipcDoc, nullptr, 0, mDocumentNode->GetBrowsingContext())); 1825 // trying to recover from this failing is problematic 1826 SetIPCDoc(ipcDoc); 1827 } 1828 } 1829 } 1830 } 1831 1832 // Set up a root element and ARIA role mapping. 1833 UpdateRootElIfNeeded(); 1834 1835 // Build initial tree. 1836 CacheChildrenInSubtree(this); 1837 1838 mLoadState |= eTreeConstructed; 1839 1840 #ifdef A11Y_LOG 1841 if (logging::IsEnabled(logging::eVerbose)) { 1842 logging::Tree("TREE", "Initial subtree", this); 1843 } 1844 if (logging::IsEnabled(logging::eTreeSize)) { 1845 logging::TreeSize("TREE SIZE", "Initial subtree", this); 1846 } 1847 #endif 1848 1849 // Fire reorder event after the document tree is constructed. Note, since 1850 // this reorder event is processed by parent document then events targeted to 1851 // this document may be fired prior to this reorder event. If this is 1852 // a problem then consider to keep event processing per tab document. 1853 if (!IsRoot()) { 1854 RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent()); 1855 ParentDocument()->FireDelayedEvent(reorderEvent); 1856 } 1857 1858 if (AppShutdown::IsShutdownImpending()) { 1859 return; 1860 } 1861 if (IPCAccessibilityActive()) { 1862 DocAccessibleChild* ipcDoc = IPCDoc(); 1863 MOZ_ASSERT(ipcDoc); 1864 if (ipcDoc) { 1865 // Send an initial update for this document and its attributes. Each acc 1866 // contained in this doc will have its initial update sent in 1867 // `InsertIntoIpcTree`. 1868 SendCache(nsAccessibilityService::GetActiveCacheDomains(), 1869 CacheUpdateType::Initial); 1870 1871 for (auto idx = 0U; idx < mChildren.Length(); idx++) { 1872 ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true); 1873 } 1874 ipcDoc->SendQueuedMutationEvents(); 1875 } 1876 } 1877 } 1878 1879 void DocAccessible::ProcessLoad() { 1880 mLoadState |= eCompletelyLoaded; 1881 1882 #ifdef A11Y_LOG 1883 if (logging::IsEnabled(logging::eDocLoad)) { 1884 logging::DocCompleteLoad(this, IsLoadEventTarget()); 1885 } 1886 #endif 1887 1888 // Do not fire document complete/stop events for root chrome document 1889 // accessibles because screen readers start working on focus event in the case 1890 // of root chrome documents. 1891 if (!IsLoadEventTarget()) return; 1892 1893 // Fire complete/load stopped if the load event type is given. 1894 if (mLoadEventType) { 1895 RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this); 1896 FireDelayedEvent(loadEvent); 1897 1898 mLoadEventType = 0; 1899 } 1900 1901 // Fire busy state change event. 1902 RefPtr<AccEvent> stateEvent = 1903 new AccStateChangeEvent(this, states::BUSY, false); 1904 FireDelayedEvent(stateEvent); 1905 } 1906 1907 void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider, 1908 nsAtom* aRelAttr) { 1909 dom::Element* relProviderEl = aRelProvider->Elm(); 1910 if (!relProviderEl) return; 1911 1912 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { 1913 nsStaticAtom* relAttr = kRelationAttrs[idx]; 1914 if (aRelAttr && aRelAttr != relAttr) continue; 1915 1916 if (relAttr == nsGkAtoms::_for) { 1917 if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label, 1918 nsGkAtoms::output)) { 1919 continue; 1920 } 1921 1922 } else if (relAttr == nsGkAtoms::control) { 1923 if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label, 1924 nsGkAtoms::description)) { 1925 continue; 1926 } 1927 } 1928 1929 AssociatedElementsIterator iter(this, relProviderEl, relAttr); 1930 while (true) { 1931 const nsDependentSubstring id = iter.NextID(); 1932 if (id.IsEmpty()) break; 1933 1934 AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id); 1935 if (providers) { 1936 AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl); 1937 if (provider) { 1938 providers->AppendElement(provider); 1939 1940 // We've got here during the children caching. If the referenced 1941 // content is not accessible then store it to pend its container 1942 // children invalidation (this happens immediately after the caching 1943 // is finished). 1944 nsIContent* dependentContent = iter.GetElem(id); 1945 if (dependentContent) { 1946 if (!HasAccessible(dependentContent)) { 1947 mInvalidationList.AppendElement(dependentContent); 1948 } 1949 } 1950 } 1951 } 1952 } 1953 1954 // If the relation attribute is given then we don't have anything else to 1955 // check. 1956 if (aRelAttr) break; 1957 } 1958 1959 // Make sure to schedule the tree update if needed. 1960 mNotificationController->ScheduleProcessing(); 1961 } 1962 1963 void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider, 1964 nsAtom* aRelAttr) { 1965 dom::Element* relProviderElm = aRelProvider->Elm(); 1966 if (!relProviderElm) return; 1967 1968 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) { 1969 nsStaticAtom* relAttr = kRelationAttrs[idx]; 1970 if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue; 1971 1972 AssociatedElementsIterator iter(this, relProviderElm, relAttr); 1973 while (true) { 1974 const nsDependentSubstring id = iter.NextID(); 1975 if (id.IsEmpty()) break; 1976 1977 AttrRelProviders* providers = GetRelProviders(relProviderElm, id); 1978 if (providers) { 1979 providers->RemoveElementsBy( 1980 [relAttr, relProviderElm](const auto& provider) { 1981 return provider->mRelAttr == relAttr && 1982 provider->mContent == relProviderElm; 1983 }); 1984 1985 RemoveRelProvidersIfEmpty(relProviderElm, id); 1986 } 1987 } 1988 1989 // If the relation attribute is given then we don't have anything else to 1990 // check. 1991 if (aRelAttr) break; 1992 } 1993 } 1994 1995 void DocAccessible::AddDependentElementsFor(LocalAccessible* aRelProvider, 1996 nsAtom* aRelAttr) { 1997 dom::Element* providerEl = aRelProvider->Elm(); 1998 if (!providerEl) { 1999 return; 2000 } 2001 for (nsStaticAtom* attr : kSingleElementRelationIdlAttrs) { 2002 if (aRelAttr && aRelAttr != attr) { 2003 continue; 2004 } 2005 if (dom::Element* targetEl = 2006 providerEl->GetExplicitlySetAttrElement(attr)) { 2007 AttrRelProviders& providers = 2008 mDependentElementsMap.LookupOrInsert(targetEl); 2009 AttrRelProvider* provider = new AttrRelProvider(attr, providerEl); 2010 providers.AppendElement(provider); 2011 } 2012 // If the relation attribute was given, we've already handled it. We don't 2013 // have anything else to check. 2014 if (aRelAttr) { 2015 break; 2016 } 2017 } 2018 2019 aria::AttrWithCharacteristicsIterator multipleElementsRelationIter( 2020 ATTR_REFLECT_ELEMENTS); 2021 while (multipleElementsRelationIter.Next()) { 2022 nsStaticAtom* attr = multipleElementsRelationIter.AttrName(); 2023 if (aRelAttr && aRelAttr != attr) { 2024 continue; 2025 } 2026 nsTArray<dom::Element*> elements; 2027 nsAccUtils::GetARIAElementsAttr(providerEl, attr, elements); 2028 for (dom::Element* targetEl : elements) { 2029 AttrRelProviders& providers = 2030 mDependentElementsMap.LookupOrInsert(targetEl); 2031 AttrRelProvider* provider = new AttrRelProvider(attr, providerEl); 2032 providers.AppendElement(provider); 2033 } 2034 // If the relation attribute was given, we've already handled it. We don't 2035 // have anything else to check. 2036 if (aRelAttr) { 2037 break; 2038 } 2039 } 2040 } 2041 2042 void DocAccessible::RemoveDependentElementsFor(LocalAccessible* aRelProvider, 2043 nsAtom* aRelAttr) { 2044 dom::Element* providerEl = aRelProvider->Elm(); 2045 if (!providerEl) { 2046 return; 2047 } 2048 for (nsStaticAtom* attr : kSingleElementRelationIdlAttrs) { 2049 if (aRelAttr && aRelAttr != attr) { 2050 continue; 2051 } 2052 if (dom::Element* targetEl = 2053 providerEl->GetExplicitlySetAttrElement(attr)) { 2054 if (auto providers = mDependentElementsMap.Lookup(targetEl)) { 2055 providers.Data().RemoveElementsBy([attr, 2056 providerEl](const auto& provider) { 2057 return provider->mRelAttr == attr && provider->mContent == providerEl; 2058 }); 2059 if (providers.Data().IsEmpty()) { 2060 providers.Remove(); 2061 } 2062 } 2063 } 2064 // If the relation attribute was given, we've already handled it. We don't 2065 // have anything else to check. 2066 if (aRelAttr) { 2067 break; 2068 } 2069 } 2070 2071 aria::AttrWithCharacteristicsIterator multipleElementsRelationIter( 2072 ATTR_REFLECT_ELEMENTS); 2073 while (multipleElementsRelationIter.Next()) { 2074 nsStaticAtom* attr = multipleElementsRelationIter.AttrName(); 2075 if (aRelAttr && aRelAttr != attr) { 2076 continue; 2077 } 2078 nsTArray<dom::Element*> elements; 2079 nsAccUtils::GetARIAElementsAttr(providerEl, attr, elements); 2080 for (dom::Element* targetEl : elements) { 2081 if (auto providers = mDependentElementsMap.Lookup(targetEl)) { 2082 providers.Data().RemoveElementsBy([attr, 2083 providerEl](const auto& provider) { 2084 return provider->mRelAttr == attr && provider->mContent == providerEl; 2085 }); 2086 if (providers.Data().IsEmpty()) { 2087 providers.Remove(); 2088 } 2089 } 2090 } 2091 2092 // If the relation attribute was given, we've already handled it. We don't 2093 // have anything else to check. 2094 if (aRelAttr) { 2095 break; 2096 } 2097 } 2098 } 2099 2100 bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement, 2101 nsAtom* aAttribute) { 2102 if (aAttribute == nsGkAtoms::role) { 2103 // It is common for js libraries to set the role on the body element after 2104 // the document has loaded. In this case we just update the role map entry. 2105 if (mContent == aElement) { 2106 SetRoleMapEntryForDoc(aElement); 2107 if (mIPCDoc) { 2108 mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex); 2109 } 2110 2111 return true; 2112 } 2113 2114 // Recreate the accessible when role is changed because we might require a 2115 // different accessible class for the new role or the accessible may expose 2116 // a different sets of interfaces (COM restriction). 2117 RecreateAccessible(aElement); 2118 2119 return true; 2120 } 2121 2122 if (aAttribute == nsGkAtoms::multiple) { 2123 if (dom::HTMLSelectElement* select = 2124 dom::HTMLSelectElement::FromNode(aElement)) { 2125 if (select->Size() <= 1) { 2126 // Adding the 'multiple' attribute to a select that has a size of 1 2127 // creates a listbox as opposed to a combobox with a popup combobox 2128 // list. Removing the attribute does the opposite. 2129 RecreateAccessible(aElement); 2130 return true; 2131 } 2132 } 2133 } 2134 2135 if (aAttribute == nsGkAtoms::size && 2136 aElement->IsHTMLElement(nsGkAtoms::select)) { 2137 // Changing the size of a select element can potentially change it from a 2138 // combobox button to a listbox with different underlying implementations. 2139 RecreateAccessible(aElement); 2140 return true; 2141 } 2142 2143 if (aAttribute == nsGkAtoms::type) { 2144 // If the input[type] changes, we should recreate the accessible. 2145 RecreateAccessible(aElement); 2146 return true; 2147 } 2148 2149 if (aAttribute == nsGkAtoms::href && 2150 !nsCoreUtils::HasClickListener(aElement)) { 2151 // If the href is added or removed for a or area elements without click 2152 // listeners, we need to recreate the accessible since the role might have 2153 // changed. Without an href or click listener, the accessible must be a 2154 // generic. 2155 if (aElement->IsHTMLElement(nsGkAtoms::a)) { 2156 LocalAccessible* acc = GetAccessible(aElement); 2157 if (!acc) { 2158 return false; 2159 } 2160 if (acc->IsHTMLLink() != aElement->HasAttr(nsGkAtoms::href)) { 2161 RecreateAccessible(aElement); 2162 return true; 2163 } 2164 } else if (aElement->IsHTMLElement(nsGkAtoms::area)) { 2165 // For area accessibles, we have to recreate the entire image map, since 2166 // the image map accessible manages the tree itself. 2167 LocalAccessible* areaAcc = GetAccessibleEvenIfNotInMap(aElement); 2168 if (!areaAcc || !areaAcc->LocalParent()) { 2169 return false; 2170 } 2171 RecreateAccessible(areaAcc->LocalParent()->GetContent()); 2172 return true; 2173 } 2174 } 2175 2176 if (aElement->IsHTMLElement(nsGkAtoms::img) && aAttribute == nsGkAtoms::alt) { 2177 // If alt text changes on an img element, we may want to create or remove an 2178 // accessible for that img. 2179 if (nsAccessibilityService::ShouldCreateImgAccessible(aElement, this)) { 2180 if (GetAccessible(aElement)) { 2181 // If the accessible already exists, there's no need to create one. 2182 return false; 2183 } 2184 ContentInserted(aElement, aElement->GetNextSibling()); 2185 } else { 2186 ContentRemoved(aElement); 2187 } 2188 return true; 2189 } 2190 2191 if (aAttribute == nsGkAtoms::popover && aElement->IsHTMLElement()) { 2192 // Changing the popover attribute might change the role. 2193 RecreateAccessible(aElement); 2194 return true; 2195 } 2196 2197 return false; 2198 } 2199 2200 void DocAccessible::UpdateRootElIfNeeded() { 2201 dom::Element* rootEl = mDocumentNode->GetBodyElement(); 2202 if (!rootEl) { 2203 rootEl = mDocumentNode->GetRootElement(); 2204 } 2205 if (rootEl != mContent) { 2206 mContent = rootEl; 2207 SetRoleMapEntryForDoc(rootEl); 2208 if (mIPCDoc) { 2209 mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex); 2210 } 2211 } 2212 } 2213 2214 /** 2215 * Content insertion helper. 2216 */ 2217 class InsertIterator final { 2218 public: 2219 InsertIterator(LocalAccessible* aContext, 2220 const nsTArray<nsCOMPtr<nsIContent>>* aNodes) 2221 : mChild(nullptr), 2222 mChildBefore(nullptr), 2223 mWalker(aContext), 2224 mNodes(aNodes), 2225 mNodesIdx(0) { 2226 MOZ_ASSERT(aContext, "No context"); 2227 MOZ_ASSERT(aNodes, "No nodes to search for accessible elements"); 2228 MOZ_COUNT_CTOR(InsertIterator); 2229 } 2230 MOZ_COUNTED_DTOR(InsertIterator) 2231 2232 LocalAccessible* Context() const { return mWalker.Context(); } 2233 LocalAccessible* Child() const { return mChild; } 2234 LocalAccessible* ChildBefore() const { return mChildBefore; } 2235 DocAccessible* Document() const { return mWalker.Document(); } 2236 2237 /** 2238 * Iterates to a next accessible within the inserted content. 2239 */ 2240 bool Next(); 2241 2242 void Rejected() { 2243 mChild = nullptr; 2244 mChildBefore = nullptr; 2245 } 2246 2247 private: 2248 LocalAccessible* mChild; 2249 LocalAccessible* mChildBefore; 2250 TreeWalker mWalker; 2251 2252 const nsTArray<nsCOMPtr<nsIContent>>* mNodes; 2253 nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes; 2254 uint32_t mNodesIdx; 2255 }; 2256 2257 bool InsertIterator::Next() { 2258 if (mNodesIdx > 0) { 2259 // If we already processed the first node in the mNodes list, 2260 // check if we can just use the walker to get its next sibling. 2261 LocalAccessible* nextChild = mWalker.Next(); 2262 if (nextChild) { 2263 mChildBefore = mChild; 2264 mChild = nextChild; 2265 return true; 2266 } 2267 } 2268 2269 while (mNodesIdx < mNodes->Length()) { 2270 nsIContent* node = mNodes->ElementAt(mNodesIdx++); 2271 // Check to see if we already processed this node with this iterator. 2272 // this can happen if we get two redundant insertions in the case of a 2273 // text and frame insertion. 2274 if (!mProcessedNodes.EnsureInserted(node)) { 2275 continue; 2276 } 2277 2278 LocalAccessible* container = Document()->AccessibleOrTrueContainer( 2279 node->GetFlattenedTreeParentNode(), true); 2280 // Ignore nodes that are not contained by the container anymore. 2281 // The container might be changed, for example, because of the subsequent 2282 // overlapping content insertion (i.e. other content was inserted between 2283 // this inserted content and its container or the content was reinserted 2284 // into different container of unrelated part of tree). To avoid a double 2285 // processing of the content insertion ignore this insertion notification. 2286 // Note, the inserted content might be not in tree at all at this point 2287 // what means there's no container. Ignore the insertion too. 2288 if (container != Context()) { 2289 continue; 2290 } 2291 2292 // HTML comboboxes have no-content list accessible as an intermediate 2293 // containing all options. 2294 if (container->IsHTMLCombobox()) { 2295 container = container->LocalFirstChild(); 2296 } 2297 2298 if (!container->IsAcceptableChild(node)) { 2299 continue; 2300 } 2301 2302 #ifdef A11Y_LOG 2303 logging::TreeInfo("traversing an inserted node", logging::eVerbose, 2304 "container", container, "node", node); 2305 #endif 2306 2307 nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr; 2308 if (prevNode && prevNode->GetNextSibling() == node) { 2309 // If inserted nodes are siblings then just move the walker next. 2310 LocalAccessible* nextChild = mWalker.Scope(node); 2311 if (nextChild) { 2312 mChildBefore = mChild; 2313 mChild = nextChild; 2314 return true; 2315 } 2316 } else { 2317 // Otherwise use a new walker to find this node in the container's 2318 // subtree, and retrieve its preceding sibling. 2319 TreeWalker finder(container); 2320 if (finder.Seek(node)) { 2321 mChild = mWalker.Scope(node); 2322 if (mChild) { 2323 MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned"); 2324 mChildBefore = finder.Prev(); 2325 return true; 2326 } 2327 } 2328 } 2329 } 2330 2331 return false; 2332 } 2333 2334 void DocAccessible::MaybeFireEventsForChangedPopover(LocalAccessible* aAcc) { 2335 dom::Element* el = aAcc->Elm(); 2336 if (!el || !el->IsHTMLElement() || !el->HasAttr(nsGkAtoms::popover)) { 2337 return; // Not a popover. 2338 } 2339 // A popover has just been inserted into or removed from the a11y tree, which 2340 // means it just appeared or disappeared. Fire expanded state changes on its 2341 // popovertarget invokers. 2342 Relation invokers(new RelatedAccIterator(mDoc, el, nsGkAtoms::popovertarget)); 2343 // Additionally iterate over any commandfor invokers. 2344 invokers.AppendIter(new RelatedAccIterator(mDoc, el, nsGkAtoms::commandfor)); 2345 while (Accessible* invoker = invokers.LocalNext()) { 2346 RefPtr<AccEvent> expandedChangeEvent = 2347 new AccStateChangeEvent(invoker->AsLocal(), states::EXPANDED); 2348 FireDelayedEvent(expandedChangeEvent); 2349 } 2350 } 2351 2352 void DocAccessible::ProcessContentInserted( 2353 LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) { 2354 // Process insertions if the container accessible is still in tree. 2355 if (!aContainer->IsInDocument()) { 2356 return; 2357 } 2358 2359 // If new root content has been inserted then update it. 2360 if (aContainer == this) { 2361 UpdateRootElIfNeeded(); 2362 } 2363 2364 InsertIterator iter(aContainer, aNodes); 2365 if (!iter.Next()) { 2366 return; 2367 } 2368 2369 #ifdef A11Y_LOG 2370 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer); 2371 #endif 2372 2373 TreeMutation mt(aContainer); 2374 bool inserted = false; 2375 do { 2376 LocalAccessible* parent = iter.Child()->LocalParent(); 2377 if (parent) { 2378 LocalAccessible* previousSibling = iter.ChildBefore(); 2379 if (parent != aContainer || 2380 iter.Child()->LocalPrevSibling() != previousSibling) { 2381 if (previousSibling && previousSibling->LocalParent() != aContainer) { 2382 // previousSibling hasn't been moved into aContainer yet. 2383 // previousSibling should be later in the insertion list, so the tree 2384 // will get adjusted when we process it later. 2385 MOZ_DIAGNOSTIC_ASSERT(parent == aContainer, 2386 "Child moving to new parent, but previous " 2387 "sibling in wrong parent"); 2388 continue; 2389 } 2390 #ifdef A11Y_LOG 2391 logging::TreeInfo("relocating accessible", 0, "old parent", parent, 2392 "new parent", aContainer, "child", iter.Child(), 2393 nullptr); 2394 #endif 2395 MoveChild(iter.Child(), aContainer, 2396 previousSibling ? previousSibling->IndexInParent() + 1 : 0); 2397 inserted = true; 2398 } 2399 continue; 2400 } 2401 2402 if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) { 2403 #ifdef A11Y_LOG 2404 logging::TreeInfo("accessible was inserted", 0, "container", aContainer, 2405 "child", iter.Child(), nullptr); 2406 #endif 2407 2408 CreateSubtree(iter.Child()); 2409 mt.AfterInsertion(iter.Child()); 2410 inserted = true; 2411 MaybeFireEventsForChangedPopover(iter.Child()); 2412 continue; 2413 } 2414 2415 MOZ_ASSERT_UNREACHABLE("accessible was rejected"); 2416 iter.Rejected(); 2417 } while (iter.Next()); 2418 2419 mt.Done(); 2420 2421 #ifdef A11Y_LOG 2422 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer); 2423 #endif 2424 2425 // We might not have actually inserted anything if layout frame reconstruction 2426 // occurred. 2427 if (inserted) { 2428 FireEventsOnInsertion(aContainer); 2429 } 2430 } 2431 2432 void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer, 2433 nsIContent* aNode) { 2434 if (!aContainer->IsInDocument()) { 2435 return; 2436 } 2437 2438 #ifdef A11Y_LOG 2439 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer); 2440 #endif 2441 2442 #ifdef A11Y_LOG 2443 logging::TreeInfo("traversing an inserted node", logging::eVerbose, 2444 "container", aContainer, "node", aNode); 2445 #endif 2446 2447 TreeWalker walker(aContainer); 2448 if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) { 2449 LocalAccessible* child = GetAccessible(aNode); 2450 if (!child) { 2451 child = GetAccService()->CreateAccessible(aNode, aContainer); 2452 } 2453 2454 if (child) { 2455 TreeMutation mt(aContainer); 2456 if (!aContainer->InsertAfter(child, walker.Prev())) { 2457 return; 2458 } 2459 CreateSubtree(child); 2460 mt.AfterInsertion(child); 2461 mt.Done(); 2462 2463 FireEventsOnInsertion(aContainer); 2464 } 2465 } 2466 2467 #ifdef A11Y_LOG 2468 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer); 2469 #endif 2470 } 2471 2472 void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) { 2473 // Check to see if change occurred inside an alert, and fire an EVENT_ALERT 2474 // if it did. 2475 if (aContainer->IsAlert() || aContainer->IsInsideAlert()) { 2476 LocalAccessible* ancestor = aContainer; 2477 do { 2478 if (ancestor->IsAlert()) { 2479 FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor); 2480 break; 2481 } 2482 } while ((ancestor = ancestor->LocalParent())); 2483 } 2484 } 2485 2486 void DocAccessible::ContentRemoved(LocalAccessible* aChild) { 2487 AUTO_PROFILER_MARKER_TEXT("DocAccessible::ContentRemovedAcc", A11Y, {}, 2488 ""_ns); 2489 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_ContentRemovedAcc> 2490 autoRecording; 2491 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 2492 2493 MOZ_DIAGNOSTIC_ASSERT(aChild != this, "Should never be called for the doc"); 2494 LocalAccessible* parent = aChild->LocalParent(); 2495 MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree"); 2496 2497 #ifdef A11Y_LOG 2498 logging::TreeInfo("process content removal", 0, "container", parent, "child", 2499 aChild, nullptr); 2500 #endif 2501 2502 // XXX: event coalescence may kill us 2503 RefPtr<LocalAccessible> kungFuDeathGripChild(aChild); 2504 2505 TreeMutation mt(parent); 2506 mt.BeforeRemoval(aChild); 2507 2508 if (aChild->IsDefunct()) { 2509 MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible"); 2510 mt.Done(); 2511 return; 2512 } 2513 2514 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1"); 2515 2516 if (aChild->IsRelocated()) { 2517 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent); 2518 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); 2519 owned->RemoveElement(aChild); 2520 if (owned->Length() == 0) { 2521 mARIAOwnsHash.Remove(parent); 2522 } 2523 } 2524 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2"); 2525 UncacheChildrenInSubtree(aChild); 2526 parent->RemoveChild(aChild); 2527 2528 mt.Done(); 2529 } 2530 2531 void DocAccessible::ContentRemoved(nsIContent* aContentNode) { 2532 AUTO_PROFILER_MARKER_TEXT("DocAccessible::ContentRemovedNode", A11Y, {}, 2533 ""_ns); 2534 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_ContentRemovedNode> 2535 autoRecording; 2536 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 2537 2538 if (!mRemovedNodes.EnsureInserted(aContentNode)) { 2539 return; 2540 } 2541 2542 // If child node is not accessible then look for its accessible children. 2543 LocalAccessible* acc = GetAccessible(aContentNode); 2544 if (acc) { 2545 ContentRemoved(acc); 2546 } 2547 2548 dom::AllChildrenIterator iter = 2549 dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true); 2550 while (nsIContent* childNode = iter.GetNextChild()) { 2551 ContentRemoved(childNode); 2552 } 2553 2554 // If this node has a shadow root, remove its explicit children too. 2555 // The host node may be removed after the shadow root was attached, and 2556 // before we asynchronously prune the light DOM and construct the shadow DOM. 2557 // If this is a case where the node does not have its own accessible, we will 2558 // not recurse into its current children, so we need to use an 2559 // ExplicitChildIterator in order to get its accessible children in the light 2560 // DOM, since they are not accessible anymore via AllChildrenIterator. 2561 if (aContentNode->GetShadowRoot()) { 2562 for (nsIContent* childNode = aContentNode->GetFirstChild(); childNode; 2563 childNode = childNode->GetNextSibling()) { 2564 ContentRemoved(childNode); 2565 } 2566 } 2567 } 2568 2569 bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) { 2570 RelatedAccIterator owners(mDoc, aElement, nsGkAtoms::aria_owns); 2571 if (Accessible* owner = owners.Next()) { 2572 mNotificationController->ScheduleRelocation(owner->AsLocal()); 2573 return true; 2574 } 2575 2576 return false; 2577 } 2578 2579 void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) { 2580 MOZ_ASSERT(aOwner, "aOwner must be a valid pointer"); 2581 MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer"); 2582 2583 #ifdef A11Y_LOG 2584 logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner); 2585 #endif 2586 2587 const nsRoleMapEntry* roleMap = aOwner->ARIARoleMap(); 2588 if (roleMap && roleMap->role == roles::EDITCOMBOBOX) { 2589 // The READWRITE state of a combobox may sever aria-owns relations 2590 // we fallback to "controls" relations. 2591 QueueCacheUpdate(aOwner, CacheDomain::Relations); 2592 } 2593 2594 nsTArray<RefPtr<LocalAccessible>>* owned = 2595 mARIAOwnsHash.GetOrInsertNew(aOwner); 2596 2597 if (aOwner->Elm()->State().HasState(dom::ElementState::READWRITE)) { 2598 // The container is editable. 2599 PutChildrenBack(owned, 0); 2600 return; 2601 } 2602 2603 AssociatedElementsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns); 2604 uint32_t idx = 0; 2605 while (dom::Element* childEl = iter.NextElem()) { 2606 if (childEl->State().HasState(dom::ElementState::READWRITE)) { 2607 dom::Element* parentEl = childEl->GetFlattenedTreeParentElement(); 2608 if (parentEl && 2609 parentEl->State().HasState(dom::ElementState::READWRITE)) { 2610 // The child is inside of an editable subtree, don't relocate it. 2611 continue; 2612 } 2613 } 2614 2615 LocalAccessible* child = GetAccessible(childEl); 2616 auto insertIdx = aOwner->ChildCount() - owned->Length() + idx; 2617 2618 // Make an attempt to create an accessible if it wasn't created yet. 2619 if (!child) { 2620 // An owned child cannot be an ancestor of the owner. 2621 bool ok = true; 2622 bool check = true; 2623 for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc(); 2624 parent = parent->LocalParent()) { 2625 if (check) { 2626 if (parent->Elm()->IsInclusiveDescendantOf(childEl)) { 2627 ok = false; 2628 break; 2629 } 2630 } 2631 // We need to do the DOM descendant check again whenever the DOM 2632 // lineage changes. If parent is relocated, that means the next 2633 // ancestor will have a different DOM lineage. 2634 check = parent->IsRelocated(); 2635 } 2636 if (!ok) { 2637 continue; 2638 } 2639 2640 if (aOwner->IsAcceptableChild(childEl)) { 2641 child = GetAccService()->CreateAccessible(childEl, aOwner); 2642 if (child) { 2643 TreeMutation imut(aOwner); 2644 aOwner->InsertChildAt(insertIdx, child); 2645 imut.AfterInsertion(child); 2646 imut.Done(); 2647 2648 child->SetRelocated(true); 2649 owned->InsertElementAt(idx, child); 2650 idx++; 2651 2652 // Create subtree before adjusting the insertion index, since subtree 2653 // creation may alter children in the container. 2654 CreateSubtree(child); 2655 FireEventsOnInsertion(aOwner); 2656 } 2657 } 2658 continue; 2659 } 2660 2661 #ifdef A11Y_LOG 2662 logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate", 2663 child, nullptr); 2664 #endif 2665 2666 if (owned->IndexOf(child) < idx) { 2667 continue; // ignore second entry of same ID 2668 } 2669 2670 // Same child on same position, no change. 2671 if (child->LocalParent() == aOwner) { 2672 int32_t indexInParent = child->IndexInParent(); 2673 2674 // The child is being placed in its current index, 2675 // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'. 2676 if (indexInParent == static_cast<int32_t>(insertIdx)) { 2677 MOZ_ASSERT(child->IsRelocated(), 2678 "A child, having an index in parent from aria ownded " 2679 "indices range, has to be aria owned"); 2680 MOZ_ASSERT(owned->ElementAt(idx) == child, 2681 "Unexpected child in ARIA owned array"); 2682 idx++; 2683 continue; 2684 } 2685 2686 // The child is being inserted directly after its current index, 2687 // resulting in a no-move case. This will happen when a parent aria-owns 2688 // its last ordinal child: 2689 // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul> 2690 if (indexInParent == static_cast<int32_t>(insertIdx) - 1) { 2691 MOZ_ASSERT(!child->IsRelocated(), 2692 "Child should be in its ordinal position"); 2693 child->SetRelocated(true); 2694 owned->InsertElementAt(idx, child); 2695 idx++; 2696 continue; 2697 } 2698 } 2699 2700 MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!"); 2701 2702 // A new child is found, check for loops. 2703 if (child->LocalParent() != aOwner) { 2704 // Child is aria-owned by another container, skip. 2705 if (child->IsRelocated()) { 2706 continue; 2707 } 2708 2709 LocalAccessible* parent = aOwner; 2710 while (parent && parent != child && !parent->IsDoc()) { 2711 parent = parent->LocalParent(); 2712 } 2713 // A referred child cannot be a parent of the owner. 2714 if (parent == child) { 2715 continue; 2716 } 2717 } 2718 2719 if (MoveChild(child, aOwner, insertIdx)) { 2720 child->SetRelocated(true); 2721 MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner)); 2722 owned = mARIAOwnsHash.GetOrInsertNew(aOwner); 2723 owned->InsertElementAt(idx, child); 2724 idx++; 2725 } 2726 } 2727 2728 // Put back children that are not seized anymore. 2729 PutChildrenBack(owned, idx); 2730 if (owned->Length() == 0) { 2731 mARIAOwnsHash.Remove(aOwner); 2732 } 2733 } 2734 2735 void DocAccessible::PutChildrenBack( 2736 nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) { 2737 MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index"); 2738 2739 for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) { 2740 LocalAccessible* child = aChildren->ElementAt(idx); 2741 if (!child->IsInDocument()) { 2742 continue; 2743 } 2744 2745 // Remove the child from the owner 2746 LocalAccessible* owner = child->LocalParent(); 2747 if (!owner) { 2748 NS_ERROR("Cannot put the child back. No parent, a broken tree."); 2749 continue; 2750 } 2751 2752 #ifdef A11Y_LOG 2753 logging::TreeInfo("aria owns put child back", 0, "old parent", owner, 2754 "child", child, nullptr); 2755 #endif 2756 2757 // Unset relocated flag to find an insertion point for the child. 2758 child->SetRelocated(false); 2759 2760 nsIContent* content = child->GetContent(); 2761 int32_t idxInParent = -1; 2762 LocalAccessible* origContainer = 2763 AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode()); 2764 // This node has probably been detached or removed from the DOM, so we have 2765 // nowhere to move it. 2766 if (!origContainer) { 2767 continue; 2768 } 2769 2770 // If the target container or any of its ancestors aren't in the document, 2771 // there's no need to determine where the child should go for relocation 2772 // since the target tree is going away. 2773 bool origContainerHasOutOfDocAncestor = false; 2774 LocalAccessible* ancestor = origContainer; 2775 while (ancestor) { 2776 if (ancestor->IsDoc()) { 2777 break; 2778 } 2779 if (!ancestor->IsInDocument()) { 2780 origContainerHasOutOfDocAncestor = true; 2781 break; 2782 } 2783 ancestor = ancestor->LocalParent(); 2784 } 2785 if (origContainerHasOutOfDocAncestor) { 2786 continue; 2787 } 2788 2789 TreeWalker walker(origContainer); 2790 if (!walker.Seek(content)) { 2791 continue; 2792 } 2793 LocalAccessible* prevChild = walker.Prev(); 2794 if (prevChild) { 2795 idxInParent = prevChild->IndexInParent() + 1; 2796 MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(), 2797 "Broken tree"); 2798 origContainer = prevChild->LocalParent(); 2799 } else { 2800 idxInParent = 0; 2801 } 2802 2803 // The child may have already be in its ordinal place for 2 reasons: 2804 // 1. It was the last ordinal child, and the first aria-owned child. 2805 // given: <ul id="list" aria-owns="b"><li id="a"></li><li 2806 // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", ""); 2807 // 2. The preceding adopted children were just reclaimed, eg: 2808 // given: <ul id="list"><li id="b"></li></ul> 2809 // after load: $("list").setAttribute("aria-owns", "a b"); 2810 // later: $("list").setAttribute("aria-owns", ""); 2811 if (origContainer != owner || child->IndexInParent() != idxInParent) { 2812 // Only attempt to move the child if the target container would accept it. 2813 // Otherwise, just allow it to be removed from the tree, since it would 2814 // not be allowed in normal tree creation. 2815 if (origContainer->IsAcceptableChild(child->GetContent())) { 2816 DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent); 2817 MOZ_ASSERT(moved, "Failed to put child back."); 2818 } 2819 } else { 2820 MOZ_ASSERT(!child->LocalPrevSibling() || 2821 !child->LocalPrevSibling()->IsRelocated(), 2822 "No relocated child should appear before this one"); 2823 MOZ_ASSERT(!child->LocalNextSibling() || 2824 child->LocalNextSibling()->IsRelocated(), 2825 "No ordinal child should appear after this one"); 2826 } 2827 } 2828 2829 aChildren->RemoveLastElements(aChildren->Length() - aStartIdx); 2830 } 2831 2832 void DocAccessible::TrackMovedAccessible(LocalAccessible* aAcc) { 2833 MOZ_ASSERT(aAcc->mDoc == this); 2834 // If an Accessible is inserted and moved during the same tick, don't track 2835 // it as a move because it hasn't been shown yet. 2836 if (!mInsertedAccessibles.Contains(aAcc)) { 2837 mMovedAccessibles.EnsureInserted(aAcc); 2838 } 2839 // When we move an Accessible, we're also moving its descendants. 2840 if (aAcc->IsOuterDoc()) { 2841 // Don't descend into other documents. 2842 return; 2843 } 2844 for (uint32_t c = 0, count = aAcc->ContentChildCount(); c < count; ++c) { 2845 TrackMovedAccessible(aAcc->ContentChildAt(c)); 2846 } 2847 } 2848 2849 bool DocAccessible::MoveChild(LocalAccessible* aChild, 2850 LocalAccessible* aNewParent, 2851 int32_t aIdxInParent) { 2852 MOZ_ASSERT(aChild, "No child"); 2853 MOZ_ASSERT(aChild->LocalParent(), "No parent"); 2854 // We can't guarantee MoveChild works correctly for accessibilities storing 2855 // children outside mChildren. 2856 MOZ_ASSERT( 2857 aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()), 2858 "Wrong insertion point for a moving child"); 2859 2860 LocalAccessible* curParent = aChild->LocalParent(); 2861 2862 if (!aNewParent->IsAcceptableChild(aChild->GetContent())) { 2863 return false; 2864 } 2865 2866 #ifdef A11Y_LOG 2867 logging::TreeInfo("move child", 0, "old parent", curParent, "new parent", 2868 aNewParent, "child", aChild, nullptr); 2869 #endif 2870 2871 // Forget aria-owns info in case of ARIA owned element. The caller is expected 2872 // to update it if needed. 2873 if (aChild->IsRelocated()) { 2874 aChild->SetRelocated(false); 2875 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent); 2876 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash"); 2877 owned->RemoveElement(aChild); 2878 if (owned->Length() == 0) { 2879 mARIAOwnsHash.Remove(curParent); 2880 } 2881 } 2882 2883 if (curParent == aNewParent) { 2884 MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case"); 2885 curParent->RelocateChild(aIdxInParent, aChild); 2886 if (mIPCDoc) { 2887 TrackMovedAccessible(aChild); 2888 } 2889 2890 #ifdef A11Y_LOG 2891 logging::TreeInfo("move child: parent tree after", logging::eVerbose, 2892 curParent); 2893 #endif 2894 return true; 2895 } 2896 2897 // If the child cannot be re-inserted into the tree, then make sure to remove 2898 // it from its present parent and then shutdown it. 2899 bool hasInsertionPoint = 2900 (aIdxInParent >= 0) && 2901 (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length())); 2902 2903 TreeMutation rmut(curParent); 2904 rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown); 2905 curParent->RemoveChild(aChild); 2906 rmut.Done(); 2907 2908 // No insertion point for the child. 2909 if (!hasInsertionPoint) { 2910 return true; 2911 } 2912 2913 TreeMutation imut(aNewParent); 2914 aNewParent->InsertChildAt(aIdxInParent, aChild); 2915 if (mIPCDoc) { 2916 TrackMovedAccessible(aChild); 2917 } 2918 imut.AfterInsertion(aChild); 2919 imut.Done(); 2920 2921 #ifdef A11Y_LOG 2922 logging::TreeInfo("move child: old parent tree after", logging::eVerbose, 2923 curParent); 2924 logging::TreeInfo("move child: new parent tree after", logging::eVerbose, 2925 aNewParent); 2926 #endif 2927 2928 return true; 2929 } 2930 2931 void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot, 2932 LocalAccessible** aFocusedAcc) { 2933 // If the accessible is focused then report a focus event after all related 2934 // mutation events. 2935 if (aFocusedAcc && !*aFocusedAcc && 2936 FocusMgr()->HasDOMFocus(aRoot->GetContent())) { 2937 *aFocusedAcc = aRoot; 2938 } 2939 2940 LocalAccessible* root = 2941 aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot; 2942 if (root->KidsFromDOM()) { 2943 TreeMutation mt(root, TreeMutation::kNoEvents); 2944 TreeWalker walker(root); 2945 while (LocalAccessible* child = walker.Next()) { 2946 if (child->IsBoundToParent()) { 2947 MoveChild(child, root, root->mChildren.Length()); 2948 continue; 2949 } 2950 2951 root->AppendChild(child); 2952 mt.AfterInsertion(child); 2953 2954 CacheChildrenInSubtree(child, aFocusedAcc); 2955 } 2956 mt.Done(); 2957 } 2958 2959 // Fire events for ARIA elements. 2960 if (!aRoot->HasARIARole()) { 2961 return; 2962 } 2963 2964 roles::Role role = aRoot->ARIARole(); 2965 if (!aRoot->IsDoc()) { 2966 if (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT) { 2967 // XXX: we should delay document load complete event if the ARIA document 2968 // has aria-busy. 2969 FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot); 2970 } else if (role == roles::MENUPOPUP && HasLoadState(eCompletelyLoaded)) { 2971 FireDelayedEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_START, aRoot); 2972 } else if (role == roles::ALERT && HasLoadState(eCompletelyLoaded)) { 2973 FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, aRoot); 2974 } 2975 } 2976 } 2977 2978 void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) { 2979 MaybeFireEventsForChangedPopover(aRoot); 2980 if (aRoot->ARIARole() == roles::MENUPOPUP) { 2981 nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_MENUPOPUP_END, aRoot); 2982 } 2983 2984 aRoot->mStateFlags |= eIsNotInDocument; 2985 RemoveDependentIDsFor(aRoot); 2986 RemoveDependentElementsFor(aRoot); 2987 2988 // The parent of the removed subtree is about to be cleared, so we must do 2989 // this here rather than in LocalAccessible::UnbindFromParent because we need 2990 // the ancestry for this to work. 2991 if (aRoot->IsTable() || aRoot->IsTableCell()) { 2992 CachedTableAccessible::Invalidate(aRoot); 2993 } 2994 2995 // Put relocated children back in their original places instead of removing 2996 // them from the tree. 2997 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot); 2998 if (owned) { 2999 PutChildrenBack(owned, 0); 3000 MOZ_ASSERT(owned->IsEmpty(), 3001 "Owned Accessibles should be cleared after PutChildrenBack."); 3002 mARIAOwnsHash.Remove(aRoot); 3003 owned = nullptr; 3004 } 3005 3006 const uint32_t count = aRoot->ContentChildCount(); 3007 for (uint32_t idx = 0; idx < count; ++idx) { 3008 LocalAccessible* child = aRoot->ContentChildAt(idx); 3009 3010 MOZ_ASSERT(!child->IsRelocated(), 3011 "No children should be relocated here. They should all have " 3012 "been relocated by PutChildrenBack."); 3013 3014 // Removing this accessible from the document doesn't mean anything about 3015 // accessibles for subdocuments, so skip removing those from the tree. 3016 if (!child->IsDoc()) { 3017 UncacheChildrenInSubtree(child); 3018 } 3019 } 3020 3021 if (aRoot->IsNodeMapEntry() && 3022 mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) { 3023 mNodeToAccessibleMap.Remove(aRoot->GetNode()); 3024 } 3025 } 3026 3027 void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) { 3028 AUTO_PROFILER_MARKER_TEXT("DocAccessible::ShutdownChildrenInSubtree", A11Y, 3029 {}, ""_ns); 3030 PerfStats::AutoMetricRecording< 3031 PerfStats::Metric::A11Y_ShutdownChildrenInSubtree> 3032 autoRecording; 3033 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS. 3034 3035 MOZ_ASSERT(!nsAccessibilityService::IsShutdown()); 3036 // Traverse through children and shutdown them before this accessible. When 3037 // child gets shutdown then it removes itself from children array of its 3038 // parent. Use jdx index to process the cases if child is not attached to the 3039 // parent and as result doesn't remove itself from its children. 3040 uint32_t count = aAccessible->ContentChildCount(); 3041 for (uint32_t idx = 0, jdx = 0; idx < count; idx++) { 3042 LocalAccessible* child = aAccessible->ContentChildAt(jdx); 3043 if (!child->IsBoundToParent()) { 3044 NS_ERROR("Parent refers to a child, child doesn't refer to parent!"); 3045 jdx++; 3046 } 3047 3048 // Don't cross document boundaries. The outerdoc shutdown takes care about 3049 // its subdocument. 3050 if (!child->IsDoc()) { 3051 ShutdownChildrenInSubtree(child); 3052 if (nsAccessibilityService::IsShutdown()) { 3053 // If XPCOM is the only consumer (devtools & mochitests), shutting down 3054 // the child's subtree can cause a11y to shut down because the last 3055 // xpcom accessibles will be removed. In that case, return early, our 3056 // work is done. 3057 return; 3058 } 3059 } 3060 } 3061 3062 UnbindFromDocument(aAccessible); 3063 } 3064 3065 bool DocAccessible::IsLoadEventTarget() const { 3066 return mDocumentNode->GetBrowsingContext()->IsContent(); 3067 } 3068 3069 void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) { 3070 MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!"); 3071 mIPCDoc = aIPCDoc; 3072 } 3073 3074 void DocAccessible::DispatchScrollingEvent(nsINode* aTarget, 3075 uint32_t aEventType) { 3076 LocalAccessible* acc = GetAccessible(aTarget); 3077 if (!acc) { 3078 return; 3079 } 3080 3081 nsIFrame* frame = acc->GetFrame(); 3082 if (!frame) { 3083 // Although the accessible had a frame at scroll time, it may now be gone 3084 // because of display: contents. 3085 return; 3086 } 3087 3088 auto [scrollPoint, scrollRange] = ComputeScrollData(acc); 3089 3090 int32_t appUnitsPerDevPixel = 3091 mPresShell->GetPresContext()->AppUnitsPerDevPixel(); 3092 3093 LayoutDeviceIntPoint scrollPointDP = LayoutDevicePoint::FromAppUnitsToNearest( 3094 scrollPoint, appUnitsPerDevPixel); 3095 LayoutDeviceIntRect scrollRangeDP = 3096 LayoutDeviceRect::FromAppUnitsToNearest(scrollRange, appUnitsPerDevPixel); 3097 3098 RefPtr<AccEvent> event = 3099 new AccScrollingEvent(aEventType, acc, scrollPointDP.x, scrollPointDP.y, 3100 scrollRangeDP.width, scrollRangeDP.height); 3101 nsEventShell::FireEvent(event); 3102 } 3103 3104 void DocAccessible::ARIAActiveDescendantIDMaybeMoved( 3105 LocalAccessible* aAccessible) { 3106 LocalAccessible* widget = nullptr; 3107 if (aAccessible->IsActiveDescendant(&widget) && widget) { 3108 // The active descendant might have just been inserted and may not be in the 3109 // tree yet. Therefore, schedule this async to ensure the tree is up to 3110 // date. 3111 mNotificationController 3112 ->ScheduleNotification<DocAccessible, LocalAccessible>( 3113 this, &DocAccessible::ARIAActiveDescendantChanged, widget); 3114 } 3115 } 3116 3117 void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) { 3118 const nsRoleMapEntry* entry = aria::GetRoleMap(aElement); 3119 if (!entry || entry->role == roles::APPLICATION || 3120 entry->role == roles::DIALOG || 3121 // Role alert isn't valid on the body element according to the ARIA spec, 3122 // but it's useful for our UI; e.g. the WebRTC sharing indicator. 3123 (entry->role == roles::ALERT && !mDocumentNode->IsContentDocument())) { 3124 SetRoleMapEntry(entry); 3125 return; 3126 } 3127 // No other ARIA roles are valid on body elements. 3128 SetRoleMapEntry(nullptr); 3129 } 3130 3131 LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const { 3132 return aNode == mDocumentNode ? const_cast<DocAccessible*>(this) 3133 : mNodeToAccessibleMap.Get(aNode); 3134 } 3135 3136 bool DocAccessible::HasPrimaryAction() const { 3137 if (HyperTextAccessible::HasPrimaryAction()) { 3138 return true; 3139 } 3140 // mContent is normally the body, but there might be a click listener on the 3141 // root. 3142 dom::Element* root = mDocumentNode->GetRootElement(); 3143 if (mContent != root) { 3144 return nsCoreUtils::HasClickListener(root); 3145 } 3146 return false; 3147 } 3148 3149 void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) { 3150 aName.Truncate(); 3151 if (aIndex != 0) { 3152 return; 3153 } 3154 if (HasPrimaryAction()) { 3155 aName.AssignLiteral("click"); 3156 } 3157 } 3158 3159 void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription( 3160 nsIContent* aChild) { 3161 if (!HasLoadState(eTreeConstructed)) { 3162 return; 3163 } 3164 for (nsIContent* content = aChild; content; content = content->GetParent()) { 3165 if (HasAccessible(content)) { 3166 // This node isn't hidden. Events for name/description dependents will be 3167 // fired elsewhere. 3168 // ... but we do need to handle firing an event for text value changes on 3169 // meters, since inner meter text is never rendered by layout. 3170 if (content->IsHTMLElement(nsGkAtoms::meter)) { 3171 FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, 3172 GetAccessible(content)); 3173 } 3174 break; 3175 } 3176 nsAtom* id = content->GetID(); 3177 if (!id) { 3178 continue; 3179 } 3180 auto* providers = 3181 GetRelProviders(content->AsElement(), nsDependentAtomString(id)); 3182 if (!providers) { 3183 continue; 3184 } 3185 for (auto& provider : *providers) { 3186 if (provider->mRelAttr != nsGkAtoms::aria_labelledby && 3187 provider->mRelAttr != nsGkAtoms::aria_describedby) { 3188 continue; 3189 } 3190 LocalAccessible* dependentAcc = GetAccessible(provider->mContent); 3191 if (!dependentAcc) { 3192 continue; 3193 } 3194 FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby 3195 ? nsIAccessibleEvent::EVENT_NAME_CHANGE 3196 : nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, 3197 dependentAcc); 3198 } 3199 } 3200 } 3201 3202 void DocAccessible::MaybeHandleChangeToAriaActions(LocalAccessible* aAcc, 3203 const nsAtom* aAttribute) { 3204 if (aAttribute == nsGkAtoms::aria_actions && 3205 nsTextEquivUtils::HasNameRule(aAcc, eNameFromSubtreeIfReqRule)) { 3206 // Search for action targets in subtree, and fire a name change event 3207 // on aAcc if any are found. 3208 AssociatedElementsIterator iter(mDoc, aAcc->Elm(), nsGkAtoms::aria_actions); 3209 while (LocalAccessible* target = iter.Next()) { 3210 if (aAcc->IsAncestorOf(target)) { 3211 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAcc); 3212 break; 3213 } 3214 } 3215 } 3216 3217 if (aAttribute == nsGkAtoms::id) { 3218 RelatedAccIterator iter(mDoc, aAcc->Elm(), nsGkAtoms::aria_actions); 3219 while (LocalAccessible* host = iter.Next()) { 3220 // Search for any ancestor action hosts and fire a name change 3221 // if any are found that calculate their name from the subtree. 3222 if (host->IsAncestorOf(aAcc) && 3223 nsTextEquivUtils::HasNameRule(host, eNameFromSubtreeIfReqRule)) { 3224 mDoc->FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, host); 3225 } 3226 } 3227 } 3228 } 3229 3230 void DocAccessible::AttrElementWillChange(dom::Element* aElement, 3231 nsAtom* aAttr) { 3232 MOZ_ASSERT(!sIsAttrElementChanging); 3233 AttributeWillChange(aElement, kNameSpaceID_None, aAttr, 3234 AttrModType::Modification); 3235 // We might get notified about a related content attribute change. Ignore 3236 // it. 3237 sIsAttrElementChanging = true; 3238 } 3239 3240 void DocAccessible::AttrElementChanged(dom::Element* aElement, nsAtom* aAttr) { 3241 MOZ_ASSERT(sIsAttrElementChanging); 3242 // The element has changed and the content attribute change notifications 3243 // (if any) have been sent. 3244 sIsAttrElementChanging = false; 3245 AttributeChanged(aElement, kNameSpaceID_None, aAttr, 3246 AttrModType::Modification, nullptr); 3247 } 3248 3249 bool DocAccessible::ProcessAnchorJump() { 3250 if (!mAnchorJumpElm) { 3251 return true; 3252 } 3253 LocalAccessible* target = GetAccessibleOrContainer(mAnchorJumpElm); 3254 if (!target) { 3255 // This node isn't in the tree. 3256 mAnchorJumpElm = nullptr; 3257 return true; 3258 } 3259 const Accessible* focusedAcc = FocusMgr()->FocusedAccessible(); 3260 if (!focusedAcc || (focusedAcc != this && !focusedAcc->IsNonInteractive())) { 3261 // Focus is nowhere or on an interactive element. Ignore the anchor jump for 3262 // now. 3263 return false; 3264 } 3265 nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START, target); 3266 // We've processed this anchor jump now. Clear it so it isn't processed again. 3267 mAnchorJumpElm = nullptr; 3268 return true; 3269 } 3270 3271 void DocAccessible::RefreshAnchorRelationCacheForTarget( 3272 LocalAccessible* aTarget) { 3273 nsIFrame* frame = aTarget->GetFrame(); 3274 if (!frame || !frame->HasProperty(nsIFrame::AnchorPosReferences())) { 3275 return; 3276 } 3277 3278 AnchorPosReferenceData* referencedAnchors = 3279 frame->GetProperty(nsIFrame::AnchorPosReferences()); 3280 for (auto& entry : *referencedAnchors) { 3281 const auto& anchorName = entry.GetKey(); 3282 if (const nsIFrame* anchorFrame = 3283 mPresShell->GetAnchorPosAnchor(anchorName, frame)) { 3284 if (LocalAccessible* anchorAcc = 3285 GetAccessible(anchorFrame->GetContent())) { 3286 if (!mInsertedAccessibles.Contains(anchorAcc)) { 3287 QueueCacheUpdate(anchorAcc, CacheDomain::Relations); 3288 } 3289 } 3290 } 3291 } 3292 }