Bug 1869043 assert that graph set access is main thread only r=padenot
[gecko.git] / accessible / generic / DocAccessible.cpp
blob34ba3bc50abbf4b2e78c4b5c332a1732885fd6c2
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/. */
7 #include "LocalAccessible-inl.h"
8 #include "AccIterator.h"
9 #include "AccAttributes.h"
10 #include "CachedTableAccessible.h"
11 #include "DocAccessible-inl.h"
12 #include "EventTree.h"
13 #include "HTMLImageMapAccessible.h"
14 #include "mozilla/ProfilerMarkers.h"
15 #include "nsAccUtils.h"
16 #include "nsEventShell.h"
17 #include "nsIIOService.h"
18 #include "nsLayoutUtils.h"
19 #include "nsTextEquivUtils.h"
20 #include "mozilla/a11y/Role.h"
21 #include "TreeWalker.h"
22 #include "xpcAccessibleDocument.h"
24 #include "nsIDocShell.h"
25 #include "mozilla/dom/Document.h"
26 #include "nsPIDOMWindow.h"
27 #include "nsIContentInlines.h"
28 #include "nsIEditingSession.h"
29 #include "nsIFrame.h"
30 #include "nsIInterfaceRequestorUtils.h"
31 #include "nsImageFrame.h"
32 #include "nsViewManager.h"
33 #include "nsIScrollableFrame.h"
34 #include "nsIURI.h"
35 #include "nsIWebNavigation.h"
36 #include "nsFocusManager.h"
37 #include "mozilla/ArrayUtils.h"
38 #include "mozilla/Assertions.h"
39 #include "mozilla/Components.h" // for mozilla::components
40 #include "mozilla/EditorBase.h"
41 #include "mozilla/HTMLEditor.h"
42 #include "mozilla/ipc/ProcessChild.h"
43 #include "mozilla/PerfStats.h"
44 #include "mozilla/PresShell.h"
45 #include "nsAccessibilityService.h"
46 #include "mozilla/a11y/DocAccessibleChild.h"
47 #include "mozilla/dom/AncestorIterator.h"
48 #include "mozilla/dom/BrowserChild.h"
49 #include "mozilla/dom/DocumentType.h"
50 #include "mozilla/dom/Element.h"
51 #include "mozilla/dom/HTMLSelectElement.h"
52 #include "mozilla/dom/MutationEventBinding.h"
53 #include "mozilla/dom/UserActivation.h"
55 using namespace mozilla;
56 using namespace mozilla::a11y;
58 ////////////////////////////////////////////////////////////////////////////////
59 // Static member initialization
61 static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby,
62 nsGkAtoms::aria_describedby,
63 nsGkAtoms::aria_details,
64 nsGkAtoms::aria_owns,
65 nsGkAtoms::aria_controls,
66 nsGkAtoms::aria_flowto,
67 nsGkAtoms::aria_errormessage,
68 nsGkAtoms::_for,
69 nsGkAtoms::control};
71 static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
73 ////////////////////////////////////////////////////////////////////////////////
74 // Constructor/desctructor
76 DocAccessible::DocAccessible(dom::Document* aDocument,
77 PresShell* aPresShell)
78 : // XXX don't pass a document to the LocalAccessible constructor so that
79 // we don't set mDoc until our vtable is fully setup. If we set mDoc
80 // before setting up the vtable we will call LocalAccessible::AddRef()
81 // but not the overrides of it for subclasses. It is important to call
82 // those overrides to avoid confusing leak checking machinary.
83 HyperTextAccessible(nullptr, nullptr),
84 // XXX aaronl should we use an algorithm for the initial cache size?
85 mAccessibleCache(kDefaultCacheLength),
86 mNodeToAccessibleMap(kDefaultCacheLength),
87 mDocumentNode(aDocument),
88 mLoadState(eTreeConstructionPending),
89 mDocFlags(0),
90 mViewportCacheDirty(false),
91 mLoadEventType(0),
92 mPrevStateBits(0),
93 mPresShell(aPresShell),
94 mIPCDoc(nullptr) {
95 mGenericTypes |= eDocument;
96 mStateFlags |= eNotNodeMapEntry;
97 mDoc = this;
99 MOZ_ASSERT(mPresShell, "should have been given a pres shell");
100 mPresShell->SetDocAccessible(this);
103 DocAccessible::~DocAccessible() {
104 NS_ASSERTION(!mPresShell, "LastRelease was never called!?!");
107 ////////////////////////////////////////////////////////////////////////////////
108 // nsISupports
110 NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible)
112 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible,
113 LocalAccessible)
114 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController)
115 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments)
116 for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) {
117 for (const auto& providers : hashEntry->Values()) {
118 for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) {
119 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
120 cb, "content of dependent ids hash entry of document accessible");
122 const auto& provider = (*providers)[provIdx];
123 cb.NoteXPCOMChild(provider->mContent);
127 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
128 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
129 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
130 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates)
131 for (const auto& ar : tmp->mARIAOwnsHash.Values()) {
132 for (uint32_t i = 0; i < ar->Length(); i++) {
133 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
134 cb.NoteXPCOMChild(ar->ElementAt(i));
137 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
139 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
140 NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController)
141 NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments)
142 tmp->mDependentIDsHashes.Clear();
143 tmp->mNodeToAccessibleMap.Clear();
144 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
145 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
146 NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
147 NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates)
148 NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
149 tmp->mARIAOwnsHash.Clear();
150 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
152 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
153 NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
154 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
155 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
156 NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
158 NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible)
159 NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible)
161 ////////////////////////////////////////////////////////////////////////////////
162 // nsIAccessible
164 ENameValueFlag DocAccessible::Name(nsString& aName) const {
165 aName.Truncate();
167 if (mParent) {
168 mParent->Name(aName); // Allow owning iframe to override the name
170 if (aName.IsEmpty()) {
171 // Allow name via aria-labelledby or title attribute
172 LocalAccessible::Name(aName);
174 if (aName.IsEmpty()) {
175 Title(aName); // Try title element
177 if (aName.IsEmpty()) { // Last resort: use URL
178 URL(aName);
181 return eNameOK;
184 // LocalAccessible public method
185 role DocAccessible::NativeRole() const {
186 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
187 if (docShell) {
188 nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
189 docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
190 int32_t itemType = docShell->ItemType();
191 if (sameTypeRoot == docShell) {
192 // Root of content or chrome tree
193 if (itemType == nsIDocShellTreeItem::typeChrome) {
194 return roles::CHROME_WINDOW;
197 if (itemType == nsIDocShellTreeItem::typeContent) {
198 return roles::DOCUMENT;
200 } else if (itemType == nsIDocShellTreeItem::typeContent) {
201 return roles::DOCUMENT;
205 return roles::PANE; // Fall back;
208 void DocAccessible::Description(nsString& aDescription) const {
209 if (mParent) mParent->Description(aDescription);
211 if (HasOwnContent() && aDescription.IsEmpty()) {
212 nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby,
213 aDescription);
217 // LocalAccessible public method
218 uint64_t DocAccessible::NativeState() const {
219 // Document is always focusable.
220 uint64_t state =
221 states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl
222 if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
224 // Expose stale state until the document is ready (DOM is loaded and tree is
225 // constructed).
226 if (!HasLoadState(eReady)) state |= states::STALE;
228 // Expose state busy until the document and all its subdocuments is completely
229 // loaded.
230 if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY;
232 nsIFrame* frame = GetFrame();
233 if (!frame || !frame->IsVisibleConsideringAncestors(
234 nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
235 state |= states::INVISIBLE | states::OFFSCREEN;
238 RefPtr<EditorBase> editorBase = GetEditor();
239 state |= editorBase ? states::EDITABLE : states::READONLY;
241 return state;
244 uint64_t DocAccessible::NativeInteractiveState() const {
245 // Document is always focusable.
246 return states::FOCUSABLE;
249 bool DocAccessible::NativelyUnavailable() const { return false; }
251 // LocalAccessible public method
252 void DocAccessible::ApplyARIAState(uint64_t* aState) const {
253 // Grab states from content element.
254 if (mContent) LocalAccessible::ApplyARIAState(aState);
256 // Allow iframe/frame etc. to have final state override via ARIA.
257 if (mParent) mParent->ApplyARIAState(aState);
260 Accessible* DocAccessible::FocusedChild() {
261 // Return an accessible for the current global focus, which does not have to
262 // be contained within the current document.
263 return FocusMgr()->FocusedAccessible();
266 void DocAccessible::TakeFocus() const {
267 // Focus the document.
268 nsFocusManager* fm = nsFocusManager::GetFocusManager();
269 RefPtr<dom::Element> newFocus;
270 dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
271 fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
272 nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
275 // HyperTextAccessible method
276 already_AddRefed<EditorBase> DocAccessible::GetEditor() const {
277 // Check if document is editable (designMode="on" case). Otherwise check if
278 // the html:body (for HTML document case) or document element is editable.
279 if (!mDocumentNode->IsInDesignMode() &&
280 (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) {
281 return nullptr;
284 nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
285 if (!docShell) {
286 return nullptr;
289 nsCOMPtr<nsIEditingSession> editingSession;
290 docShell->GetEditingSession(getter_AddRefs(editingSession));
291 if (!editingSession) return nullptr; // No editing session interface
293 RefPtr<HTMLEditor> htmlEditor =
294 editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow());
295 if (!htmlEditor) {
296 return nullptr;
299 bool isEditable = false;
300 htmlEditor->GetIsDocumentEditable(&isEditable);
301 if (isEditable) {
302 return htmlEditor.forget();
305 return nullptr;
308 // DocAccessible public method
310 void DocAccessible::URL(nsAString& aURL) const {
311 aURL.Truncate();
312 nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer();
313 nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
314 if (MOZ_UNLIKELY(!webNav)) {
315 return;
318 nsCOMPtr<nsIURI> uri;
319 webNav->GetCurrentURI(getter_AddRefs(uri));
320 if (MOZ_UNLIKELY(!uri)) {
321 return;
323 // Let's avoid treating too long URI in the main process for avoiding
324 // memory fragmentation as far as possible.
325 if (uri->SchemeIs("data") || uri->SchemeIs("blob")) {
326 return;
329 nsCOMPtr<nsIIOService> io = mozilla::components::IO::Service();
330 if (NS_WARN_IF(!io)) {
331 return;
333 nsCOMPtr<nsIURI> exposableURI;
334 if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) ||
335 MOZ_UNLIKELY(!exposableURI)) {
336 return;
338 nsAutoCString theURL;
339 if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) {
340 CopyUTF8toUTF16(theURL, aURL);
344 void DocAccessible::Title(nsString& aTitle) const {
345 mDocumentNode->GetTitle(aTitle);
348 void DocAccessible::MimeType(nsAString& aType) const {
349 mDocumentNode->GetContentType(aType);
352 void DocAccessible::DocType(nsAString& aType) const {
353 dom::DocumentType* docType = mDocumentNode->GetDoctype();
354 if (docType) docType->GetPublicId(aType);
357 void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc,
358 uint64_t aNewDomain) {
359 if (!mIPCDoc) {
360 return;
362 // These strong references aren't necessary because WithEntryHandle is
363 // guaranteed to run synchronously. However, static analysis complains without
364 // them.
365 RefPtr<DocAccessible> self = this;
366 RefPtr<LocalAccessible> acc = aAcc;
367 size_t arrayIndex =
368 mQueuedCacheUpdatesHash.WithEntryHandle(aAcc, [self, acc](auto&& entry) {
369 if (entry.HasEntry()) {
370 // This LocalAccessible has already been queued. Return its index in
371 // the queue array so we can update its queued domains.
372 return entry.Data();
374 // Add this LocalAccessible to the queue array.
375 size_t index = self->mQueuedCacheUpdatesArray.Length();
376 self->mQueuedCacheUpdatesArray.EmplaceBack(std::make_pair(acc, 0));
377 // Also add it to the hash map so we can avoid processing the same
378 // LocalAccessible twice.
379 return entry.Insert(index);
381 auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex];
382 MOZ_ASSERT(arrayAcc == aAcc);
383 domain |= aNewDomain;
384 Controller()->ScheduleProcessing();
387 void DocAccessible::QueueCacheUpdateForDependentRelations(
388 LocalAccessible* aAcc) {
389 if (!mIPCDoc || !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() ||
390 aAcc->IsDefunct()) {
391 return;
393 nsAutoString ID;
394 aAcc->DOMNodeID(ID);
395 if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) {
396 // We call this function when we've noticed an ID change, or when an acc
397 // is getting bound to its document. We need to ensure any existing accs
398 // that depend on this acc's ID have their rel cache entries updated.
399 for (const auto& provider : *list) {
400 LocalAccessible* relatedAcc = GetAccessible(provider->mContent);
401 if (!relatedAcc || relatedAcc->IsDefunct() ||
402 !relatedAcc->IsInDocument() ||
403 mInsertedAccessibles.Contains(relatedAcc)) {
404 continue;
406 QueueCacheUpdate(relatedAcc, CacheDomain::Relations);
411 ////////////////////////////////////////////////////////////////////////////////
412 // LocalAccessible
414 void DocAccessible::Init() {
415 #ifdef A11Y_LOG
416 if (logging::IsEnabled(logging::eDocCreate)) {
417 logging::DocCreate("document initialize", mDocumentNode, this);
419 #endif
421 // Initialize notification controller.
422 mNotificationController = new NotificationController(this, mPresShell);
424 // Mark the DocAccessible as loaded if its DOM document is already loaded at
425 // this point. This can happen for one of three reasons:
426 // 1. A11y was started late.
427 // 2. DOM loading for a document (probably an in-process iframe) completed
428 // before its Accessible container was created.
429 // 3. The PresShell for the document was created after DOM loading completed.
430 // In that case, we tried to create the DocAccessible when DOM loading
431 // completed, but we can't create a DocAccessible without a PresShell, so
432 // this failed. The DocAccessible was subsequently created due to a layout
433 // notification.
434 if (mDocumentNode->GetReadyStateEnum() ==
435 dom::Document::READYSTATE_COMPLETE) {
436 mLoadState |= eDOMLoaded;
437 // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a
438 // doc load complete event. If it happened due to reason 3, we need to fire
439 // doc load complete because clients (especially tests) might be waiting
440 // for the document to load using this event. We can't distinguish why this
441 // happened at this point, so just fire it regardless. It won't do any
442 // harm even if it isn't necessary. We set mLoadEventType here and it will
443 // be fired in ProcessLoad as usual.
444 mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE;
445 } else if (mDocumentNode->IsInitialDocument()) {
446 // The initial about:blank document will never finish loading, so we can
447 // immediately mark it loaded to avoid waiting for its load.
448 mLoadState |= eDOMLoaded;
451 AddEventListeners();
454 void DocAccessible::Shutdown() {
455 if (!mPresShell) { // already shutdown
456 return;
459 #ifdef A11Y_LOG
460 if (logging::IsEnabled(logging::eDocDestroy)) {
461 logging::DocDestroy("document shutdown", mDocumentNode, this);
463 #endif
465 // Mark the document as shutdown before AT is notified about the document
466 // removal from its container (valid for root documents on ATK and due to
467 // some reason for MSAA, refer to bug 757392 for details).
468 mStateFlags |= eIsDefunct;
470 if (mNotificationController) {
471 mNotificationController->Shutdown();
472 mNotificationController = nullptr;
475 RemoveEventListeners();
477 // mParent->RemoveChild clears mParent, but we need to know whether we were a
478 // child later, so use a flag.
479 const bool isChild = !!mParent;
480 if (mParent) {
481 DocAccessible* parentDocument = mParent->Document();
482 if (parentDocument) parentDocument->RemoveChildDocument(this);
484 mParent->RemoveChild(this);
485 MOZ_ASSERT(!mParent, "Parent has to be null!");
488 mPresShell->SetDocAccessible(nullptr);
489 mPresShell = nullptr; // Avoid reentrancy
491 // Walk the array backwards because child documents remove themselves from the
492 // array as they are shutdown.
493 int32_t childDocCount = mChildDocuments.Length();
494 for (int32_t idx = childDocCount - 1; idx >= 0; idx--) {
495 mChildDocuments[idx]->Shutdown();
498 mChildDocuments.Clear();
499 // mQueuedCacheUpdates* can contain a reference to this document (ex. if the
500 // doc is scrollable and we're sending a scroll position update). Clear the
501 // map here to avoid creating ref cycles.
502 mQueuedCacheUpdatesArray.Clear();
503 mQueuedCacheUpdatesHash.Clear();
505 // XXX thinking about ordering?
506 if (mIPCDoc) {
507 MOZ_ASSERT(IPCAccessibilityActive());
508 mIPCDoc->Shutdown();
509 MOZ_ASSERT(!mIPCDoc);
512 mDependentIDsHashes.Clear();
513 mNodeToAccessibleMap.Clear();
515 mAnchorJumpElm = nullptr;
516 mInvalidationList.Clear();
517 mPendingUpdates.Clear();
519 for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
520 LocalAccessible* accessible = iter.Data();
521 MOZ_ASSERT(accessible);
522 if (accessible) {
523 // This might have been focused with FocusManager::ActiveItemChanged. In
524 // that case, we must notify FocusManager so that it clears the active
525 // item. Otherwise, it will hold on to a defunct Accessible. Normally,
526 // this happens in UnbindFromDocument, but we don't call that when the
527 // whole document shuts down.
528 if (FocusMgr()->WasLastFocused(accessible)) {
529 FocusMgr()->ActiveItemChanged(nullptr);
530 #ifdef A11Y_LOG
531 if (logging::IsEnabled(logging::eFocus)) {
532 logging::ActiveItemChangeCausedBy("doc shutdown", accessible);
534 #endif
536 if (!accessible->IsDefunct()) {
537 // Unlink parent to avoid its cleaning overhead in shutdown.
538 accessible->mParent = nullptr;
539 accessible->Shutdown();
542 iter.Remove();
545 HyperTextAccessible::Shutdown();
547 MOZ_ASSERT(GetAccService());
548 GetAccService()->NotifyOfDocumentShutdown(
549 this, mDocumentNode,
550 // Make sure we don't shut down AccService while a parent document is
551 // still shutting down. The parent will allow service shutdown when it
552 // reaches this point.
553 /* aAllowServiceShutdown */ !isChild);
554 mDocumentNode = nullptr;
557 nsIFrame* DocAccessible::GetFrame() const {
558 nsIFrame* root = nullptr;
559 if (mPresShell) {
560 root = mPresShell->GetRootFrame();
563 return root;
566 nsINode* DocAccessible::GetNode() const { return mDocumentNode; }
568 // DocAccessible protected member
569 nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
570 *aRelativeFrame = GetFrame();
572 dom::Document* document = mDocumentNode;
573 dom::Document* parentDoc = nullptr;
575 nsRect bounds;
576 while (document) {
577 PresShell* presShell = document->GetPresShell();
578 if (!presShell) {
579 return nsRect();
582 nsRect scrollPort;
583 nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
584 if (sf) {
585 scrollPort = sf->GetScrollPortRect();
586 } else {
587 nsIFrame* rootFrame = presShell->GetRootFrame();
588 if (!rootFrame) return nsRect();
590 scrollPort = rootFrame->GetRect();
593 if (parentDoc) { // After first time thru loop
594 // XXXroc bogus code! scrollPort is relative to the viewport of
595 // this document, but we're intersecting rectangles derived from
596 // multiple documents and assuming they're all in the same coordinate
597 // system. See bug 514117.
598 bounds.IntersectRect(scrollPort, bounds);
599 } else { // First time through loop
600 bounds = scrollPort;
603 document = parentDoc = document->GetInProcessParentDocument();
606 return bounds;
609 // DocAccessible protected member
610 nsresult DocAccessible::AddEventListeners() {
611 SelectionMgr()->AddDocSelectionListener(mPresShell);
613 // Add document observer.
614 mDocumentNode->AddObserver(this);
615 return NS_OK;
618 // DocAccessible protected member
619 nsresult DocAccessible::RemoveEventListeners() {
620 // Remove listeners associated with content documents
621 NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
623 if (mDocumentNode) {
624 mDocumentNode->RemoveObserver(this);
627 if (mScrollWatchTimer) {
628 mScrollWatchTimer->Cancel();
629 mScrollWatchTimer = nullptr;
630 NS_RELEASE_THIS(); // Kung fu death grip
633 SelectionMgr()->RemoveDocSelectionListener(mPresShell);
634 return NS_OK;
637 void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) {
638 DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
640 if (docAcc) {
641 // Dispatch a scroll-end for all entries in table. They have not
642 // been scrolled in at least `kScrollEventInterval`.
643 for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done();
644 iter.Next()) {
645 docAcc->DispatchScrollingEvent(iter.Key(),
646 nsIAccessibleEvent::EVENT_SCROLLING_END);
647 iter.Remove();
650 if (docAcc->mScrollWatchTimer) {
651 docAcc->mScrollWatchTimer = nullptr;
652 NS_RELEASE(docAcc); // Release kung fu death grip
657 void DocAccessible::HandleScroll(nsINode* aTarget) {
658 nsINode* target = aTarget;
659 LocalAccessible* targetAcc = GetAccessible(target);
660 if (!targetAcc && target->IsInNativeAnonymousSubtree()) {
661 // The scroll event for textareas comes from a native anonymous div. We need
662 // the closest non-anonymous ancestor to get the right Accessible.
663 target = target->GetClosestNativeAnonymousSubtreeRootParentOrHost();
664 targetAcc = GetAccessible(target);
666 // Regardless of our scroll timer, we need to send a cache update
667 // to ensure the next Bounds() query accurately reflects our position
668 // after scrolling.
669 if (targetAcc) {
670 QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition);
673 const uint32_t kScrollEventInterval = 100;
674 // If we haven't dispatched a scrolling event for a target in at least
675 // kScrollEventInterval milliseconds, dispatch one now.
676 mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) {
677 const TimeStamp now = TimeStamp::Now();
679 if (!lastDispatch ||
680 (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) {
681 // We can't fire events on a document whose tree isn't constructed yet.
682 if (HasLoadState(eTreeConstructed)) {
683 DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING);
685 lastDispatch.InsertOrUpdate(now);
689 // If timer callback is still pending, push it 100ms into the future.
690 // When scrolling ends and we don't fire this callback anymore, the
691 // timer callback will fire and dispatch an EVENT_SCROLLING_END.
692 if (mScrollWatchTimer) {
693 mScrollWatchTimer->SetDelay(kScrollEventInterval);
694 } else {
695 NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
696 ScrollTimerCallback, this, kScrollEventInterval,
697 nsITimer::TYPE_ONE_SHOT,
698 "a11y::DocAccessible::ScrollPositionDidChange");
699 if (mScrollWatchTimer) {
700 NS_ADDREF_THIS(); // Kung fu death grip
705 std::pair<nsPoint, nsRect> DocAccessible::ComputeScrollData(
706 LocalAccessible* aAcc) {
707 nsPoint scrollPoint;
708 nsRect scrollRange;
710 if (nsIFrame* frame = aAcc->GetFrame()) {
711 nsIScrollableFrame* sf = aAcc == this
712 ? mPresShell->GetRootScrollFrameAsScrollable()
713 : frame->GetScrollTargetFrame();
715 // If there is no scrollable frame, it's likely a scroll in a popup, like
716 // <select>. Return a scroll offset and range of 0. The scroll info
717 // is currently only used on Android, and popups are rendered natively
718 // there.
719 if (sf) {
720 scrollPoint = sf->GetScrollPosition() * mPresShell->GetResolution();
721 scrollRange = sf->GetScrollRange();
722 scrollRange.ScaleRoundOut(mPresShell->GetResolution());
726 return {scrollPoint, scrollRange};
729 ////////////////////////////////////////////////////////////////////////////////
730 // nsIDocumentObserver
732 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
733 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
735 void DocAccessible::AttributeWillChange(dom::Element* aElement,
736 int32_t aNameSpaceID,
737 nsAtom* aAttribute, int32_t aModType) {
738 LocalAccessible* accessible = GetAccessible(aElement);
739 if (!accessible) {
740 if (aElement != mContent) return;
742 accessible = this;
745 // Update dependent IDs cache. Take care of elements that are accessible
746 // because dependent IDs cache doesn't contain IDs from non accessible
747 // elements.
748 if (aModType != dom::MutationEvent_Binding::ADDITION) {
749 RemoveDependentIDsFor(accessible, aAttribute);
752 if (aAttribute == nsGkAtoms::id) {
753 if (accessible->IsActiveDescendant()) {
754 RefPtr<AccEvent> event =
755 new AccStateChangeEvent(accessible, states::ACTIVE, false);
756 FireDelayedEvent(event);
759 RelocateARIAOwnedIfNeeded(aElement);
762 if (aAttribute == nsGkAtoms::aria_activedescendant) {
763 if (LocalAccessible* activeDescendant = accessible->CurrentItem()) {
764 RefPtr<AccEvent> event =
765 new AccStateChangeEvent(activeDescendant, states::ACTIVE, false);
766 FireDelayedEvent(event);
770 // If attribute affects accessible's state, store the old state so we can
771 // later compare it against the state of the accessible after the attribute
772 // change.
773 if (accessible->AttributeChangesState(aAttribute)) {
774 mPrevStateBits = accessible->State();
775 } else {
776 mPrevStateBits = 0;
780 void DocAccessible::AttributeChanged(dom::Element* aElement,
781 int32_t aNameSpaceID, nsAtom* aAttribute,
782 int32_t aModType,
783 const nsAttrValue* aOldValue) {
784 NS_ASSERTION(!IsDefunct(),
785 "Attribute changed called on defunct document accessible!");
787 // Proceed even if the element is not accessible because element may become
788 // accessible if it gets certain attribute.
789 if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
791 // Update the accessible tree on aria-hidden change. Make sure to not create
792 // a tree under aria-hidden='true'.
793 if (aAttribute == nsGkAtoms::aria_hidden) {
794 if (aria::HasDefinedARIAHidden(aElement)) {
795 ContentRemoved(aElement);
796 } else {
797 ContentInserted(aElement, aElement->GetNextSibling());
799 return;
802 LocalAccessible* accessible = GetAccessible(aElement);
803 if (!accessible) {
804 if (mContent == aElement) {
805 // The attribute change occurred on the root content of this
806 // DocAccessible, so handle it as an attribute change on this.
807 accessible = this;
808 } else {
809 if (aModType == dom::MutationEvent_Binding::ADDITION &&
810 aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) {
811 // The element doesn't have an Accessible, but a global ARIA attribute
812 // was just added, which means we should probably create an Accessible.
813 ContentInserted(aElement, aElement->GetNextSibling());
814 return;
816 // The element doesn't have an Accessible, so ignore the attribute
817 // change.
818 return;
822 MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
823 "DOM attribute change on an accessible detached from the tree");
825 if (aAttribute == nsGkAtoms::id) {
826 dom::Element* elm = accessible->Elm();
827 RelocateARIAOwnedIfNeeded(elm);
828 ARIAActiveDescendantIDMaybeMoved(accessible);
829 QueueCacheUpdate(accessible, CacheDomain::DOMNodeIDAndClass);
830 QueueCacheUpdateForDependentRelations(accessible);
833 // The activedescendant universal property redirects accessible focus events
834 // to the element with the id that activedescendant points to. Make sure
835 // the tree up to date before processing. In other words, when a node has just
836 // been inserted, the tree won't be up to date yet, so we must always schedule
837 // an async notification so that a newly inserted node will be present in
838 // the tree.
839 if (aAttribute == nsGkAtoms::aria_activedescendant) {
840 mNotificationController
841 ->ScheduleNotification<DocAccessible, LocalAccessible>(
842 this, &DocAccessible::ARIAActiveDescendantChanged, accessible);
843 return;
846 // Defer to accessible any needed actions like changing states or emiting
847 // events.
848 accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue,
849 mPrevStateBits);
851 // Update dependent IDs cache. We handle elements with accessibles.
852 // If the accessible or element with the ID doesn't exist yet the cache will
853 // be updated when they are added.
854 if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
855 aModType == dom::MutationEvent_Binding::ADDITION) {
856 AddDependentIDsFor(accessible, aAttribute);
860 void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement,
861 nsAtom* aAttribute,
862 int32_t aModType) {
863 NS_ASSERTION(!IsDefunct(),
864 "Attribute changed called on defunct document accessible!");
866 if (aElement->HasAttr(aAttribute)) {
867 return;
870 AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType);
873 void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement,
874 nsAtom* aAttribute,
875 int32_t aModType) {
876 NS_ASSERTION(!IsDefunct(),
877 "Attribute changed called on defunct document accessible!");
879 if (aElement->HasAttr(aAttribute)) {
880 return;
883 AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr);
886 void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
887 if (dom::Element* elm = aAccessible->Elm()) {
888 nsAutoString id;
889 if (elm->GetAttr(nsGkAtoms::aria_activedescendant, id)) {
890 dom::Element* activeDescendantElm = IDRefsIterator::GetElem(elm, id);
891 if (activeDescendantElm) {
892 LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm);
893 if (activeDescendant) {
894 RefPtr<AccEvent> event =
895 new AccStateChangeEvent(activeDescendant, states::ACTIVE, true);
896 FireDelayedEvent(event);
897 if (aAccessible->IsActiveWidget()) {
898 FocusMgr()->ActiveItemChanged(activeDescendant, false);
899 #ifdef A11Y_LOG
900 if (logging::IsEnabled(logging::eFocus)) {
901 logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
902 activeDescendant);
904 #endif
906 return;
911 // aria-activedescendant was cleared or changed to a non-existent node.
912 // Move focus back to the element itself if it has DOM focus.
913 if (aAccessible->IsActiveWidget()) {
914 FocusMgr()->ActiveItemChanged(aAccessible, false);
915 #ifdef A11Y_LOG
916 if (logging::IsEnabled(logging::eFocus)) {
917 logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared",
918 aAccessible);
920 #endif
925 void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {
926 MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
929 void DocAccessible::ElementStateChanged(dom::Document* aDocument,
930 dom::Element* aElement,
931 dom::ElementState aStateMask) {
932 if (aStateMask.HasState(dom::ElementState::READWRITE) &&
933 aElement == mDocumentNode->GetRootElement()) {
934 // This handles changes to designMode. contentEditable is handled by
935 // LocalAccessible::AttributeChangesState and
936 // LocalAccessible::DOMAttributeChanged.
937 const bool isEditable =
938 aElement->State().HasState(dom::ElementState::READWRITE);
939 RefPtr<AccEvent> event =
940 new AccStateChangeEvent(this, states::EDITABLE, isEditable);
941 FireDelayedEvent(event);
942 event = new AccStateChangeEvent(this, states::READONLY, !isEditable);
943 FireDelayedEvent(event);
946 LocalAccessible* accessible = GetAccessible(aElement);
947 if (!accessible) return;
949 if (aStateMask.HasState(dom::ElementState::CHECKED)) {
950 LocalAccessible* widget = accessible->ContainerWidget();
951 if (widget && widget->IsSelect()) {
952 // Changing selection here changes what we cache for
953 // the viewport.
954 SetViewportCacheDirty(true);
955 AccSelChangeEvent::SelChangeType selChangeType =
956 aElement->State().HasState(dom::ElementState::CHECKED)
957 ? AccSelChangeEvent::eSelectionAdd
958 : AccSelChangeEvent::eSelectionRemove;
959 RefPtr<AccEvent> event =
960 new AccSelChangeEvent(widget, accessible, selChangeType);
961 FireDelayedEvent(event);
962 return;
965 RefPtr<AccEvent> event = new AccStateChangeEvent(
966 accessible, states::CHECKED,
967 aElement->State().HasState(dom::ElementState::CHECKED));
968 FireDelayedEvent(event);
971 if (aStateMask.HasState(dom::ElementState::INVALID)) {
972 RefPtr<AccEvent> event =
973 new AccStateChangeEvent(accessible, states::INVALID);
974 FireDelayedEvent(event);
977 if (aStateMask.HasState(dom::ElementState::REQUIRED)) {
978 RefPtr<AccEvent> event =
979 new AccStateChangeEvent(accessible, states::REQUIRED);
980 FireDelayedEvent(event);
983 if (aStateMask.HasState(dom::ElementState::VISITED)) {
984 RefPtr<AccEvent> event =
985 new AccStateChangeEvent(accessible, states::TRAVERSED, true);
986 FireDelayedEvent(event);
989 // We only expose dom::ElementState::DEFAULT on buttons, but we can get
990 // notifications for other controls like checkboxes.
991 if (aStateMask.HasState(dom::ElementState::DEFAULT) &&
992 accessible->IsButton()) {
993 RefPtr<AccEvent> event =
994 new AccStateChangeEvent(accessible, states::DEFAULT);
995 FireDelayedEvent(event);
999 void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
1000 const CharacterDataChangeInfo&) {}
1002 void DocAccessible::CharacterDataChanged(nsIContent* aContent,
1003 const CharacterDataChangeInfo&) {}
1005 void DocAccessible::ContentInserted(nsIContent* aChild) {
1006 MaybeHandleChangeToHiddenNameOrDescription(aChild);
1009 void DocAccessible::ContentRemoved(nsIContent* aChildNode,
1010 nsIContent* aPreviousSiblingNode) {
1011 #ifdef A11Y_LOG
1012 if (logging::IsEnabled(logging::eTree)) {
1013 logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
1014 logging::Node("container node", aChildNode->GetParent());
1015 logging::Node("content node", aChildNode);
1016 logging::MsgEnd();
1018 #endif
1019 ContentRemoved(aChildNode);
1022 void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
1024 ////////////////////////////////////////////////////////////////////////////////
1025 // LocalAccessible
1027 #ifdef A11Y_LOG
1028 nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
1029 if (logging::IsEnabled(logging::eDocLoad)) {
1030 logging::DocLoadEventHandled(aEvent);
1033 return HyperTextAccessible::HandleAccEvent(aEvent);
1035 #endif
1037 ////////////////////////////////////////////////////////////////////////////////
1038 // Public members
1040 nsPresContext* DocAccessible::PresContext() const {
1041 return mPresShell->GetPresContext();
1044 void* DocAccessible::GetNativeWindow() const {
1045 if (!mPresShell) {
1046 return nullptr;
1049 nsViewManager* vm = mPresShell->GetViewManager();
1050 if (!vm) return nullptr;
1052 nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
1053 if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
1055 return nullptr;
1058 LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(
1059 void* aUniqueID) {
1060 LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID);
1061 if (child) return child;
1063 uint32_t childDocCount = mChildDocuments.Length();
1064 for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
1065 DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
1066 child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
1067 if (child) return child;
1070 return nullptr;
1073 LocalAccessible* DocAccessible::GetAccessibleOrContainer(
1074 nsINode* aNode, bool aNoContainerIfPruned) const {
1075 if (!aNode || !aNode->GetComposedDoc()) {
1076 return nullptr;
1079 nsINode* start = aNode;
1080 if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) {
1081 // This can happen, for example, when called within
1082 // SelectionManager::ProcessSelectionChanged due to focusing a direct
1083 // child of a shadow root.
1084 // GetFlattenedTreeParent works on children of a shadow root, but not the
1085 // shadow root itself.
1086 start = shadowRoot->GetHost();
1087 if (!start) {
1088 return nullptr;
1092 for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) {
1093 // No container if is inside of aria-hidden subtree.
1094 if (aNoContainerIfPruned && currNode->IsElement() &&
1095 aria::HasDefinedARIAHidden(currNode->AsElement())) {
1096 return nullptr;
1099 // Check if node is in zero-sized map
1100 if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) {
1101 if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
1102 if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
1103 .IsEmpty()) {
1104 return nullptr;
1109 if (LocalAccessible* accessible = GetAccessible(currNode)) {
1110 return accessible;
1114 return nullptr;
1117 LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const {
1118 return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode())
1119 : nullptr;
1122 LocalAccessible* DocAccessible::GetAccessibleOrDescendant(
1123 nsINode* aNode) const {
1124 LocalAccessible* acc = GetAccessible(aNode);
1125 if (acc) return acc;
1127 if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) {
1128 // If the node is the doc's body or root element, return the doc accessible.
1129 return const_cast<DocAccessible*>(this);
1132 acc = GetContainerAccessible(aNode);
1133 if (acc) {
1134 TreeWalker walker(acc, aNode->AsContent(),
1135 TreeWalker::eWalkCache | TreeWalker::eScoped);
1136 return walker.Next();
1139 return nullptr;
1142 void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
1143 const nsRoleMapEntry* aRoleMapEntry) {
1144 // Put into DOM node cache.
1145 if (aAccessible->IsNodeMapEntry()) {
1146 mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible);
1149 // Put into unique ID cache.
1150 mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible});
1152 aAccessible->SetRoleMapEntry(aRoleMapEntry);
1154 if (aAccessible->HasOwnContent()) {
1155 AddDependentIDsFor(aAccessible);
1157 nsIContent* content = aAccessible->GetContent();
1158 if (content->IsElement() &&
1159 content->AsElement()->HasAttr(nsGkAtoms::aria_owns)) {
1160 mNotificationController->ScheduleRelocation(aAccessible);
1164 if (mIPCDoc) {
1165 mInsertedAccessibles.EnsureInserted(aAccessible);
1168 QueueCacheUpdateForDependentRelations(aAccessible);
1171 void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) {
1172 NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
1173 "Unbinding the unbound accessible!");
1175 // Fire focus event on accessible having DOM focus if last focus was removed
1176 // from the tree.
1177 if (FocusMgr()->WasLastFocused(aAccessible)) {
1178 FocusMgr()->ActiveItemChanged(nullptr);
1179 #ifdef A11Y_LOG
1180 if (logging::IsEnabled(logging::eFocus)) {
1181 logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
1183 #endif
1186 // Remove an accessible from node-to-accessible map if it exists there.
1187 if (aAccessible->IsNodeMapEntry() &&
1188 mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) {
1189 mNodeToAccessibleMap.Remove(aAccessible->GetNode());
1192 aAccessible->mStateFlags |= eIsNotInDocument;
1194 // Update XPCOM part.
1195 xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
1196 if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);
1198 void* uniqueID = aAccessible->UniqueID();
1200 NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!");
1201 aAccessible->Shutdown();
1203 mAccessibleCache.Remove(uniqueID);
1206 void DocAccessible::ContentInserted(nsIContent* aStartChildNode,
1207 nsIContent* aEndChildNode) {
1208 // Ignore content insertions until we constructed accessible tree. Otherwise
1209 // schedule tree update on content insertion after layout.
1210 if (!mNotificationController || !HasLoadState(eTreeConstructed)) {
1211 return;
1214 // The frame constructor guarantees that only ranges with the same parent
1215 // arrive here in presence of dynamic changes to the page, see
1216 // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers.
1217 nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode();
1218 if (!parent) {
1219 return;
1222 LocalAccessible* container = AccessibleOrTrueContainer(parent);
1223 if (!container) {
1224 return;
1227 AutoTArray<nsCOMPtr<nsIContent>, 10> list;
1228 for (nsIContent* node = aStartChildNode; node != aEndChildNode;
1229 node = node->GetNextSibling()) {
1230 MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode());
1231 if (PruneOrInsertSubtree(node)) {
1232 list.AppendElement(node);
1236 mNotificationController->ScheduleContentInsertion(container, list);
1239 void DocAccessible::ScheduleTreeUpdate(nsIContent* aContent) {
1240 if (mPendingUpdates.Contains(aContent)) {
1241 return;
1243 mPendingUpdates.AppendElement(aContent);
1244 mNotificationController->ScheduleProcessing();
1247 void DocAccessible::ProcessPendingUpdates() {
1248 auto updates = std::move(mPendingUpdates);
1249 for (auto update : updates) {
1250 if (update->GetComposedDoc() != mDocumentNode) {
1251 continue;
1253 // The pruning logic will take care of avoiding unnecessary notifications.
1254 ContentInserted(update, update->GetNextSibling());
1258 bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
1259 bool insert = false;
1261 // In the case that we are, or are in, a shadow host, we need to assure
1262 // some accessibles are removed if they are not rendered anymore.
1263 nsIContent* shadowHost =
1264 aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost();
1265 if (shadowHost) {
1266 // Check all explicit children in the host, if they are not slotted
1267 // then remove their accessibles and subtrees.
1268 for (nsIContent* childNode = shadowHost->GetFirstChild(); childNode;
1269 childNode = childNode->GetNextSibling()) {
1270 if (!childNode->GetPrimaryFrame() &&
1271 !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
1272 ContentRemoved(childNode);
1276 // If this is a slot, check to see if its fallback content is rendered,
1277 // if not - remove it.
1278 if (aRoot->IsHTMLElement(nsGkAtoms::slot)) {
1279 for (nsIContent* childNode = aRoot->GetFirstChild(); childNode;
1280 childNode = childNode->GetNextSibling()) {
1281 if (!childNode->GetPrimaryFrame() &&
1282 !nsCoreUtils::CanCreateAccessibleWithoutFrame(childNode)) {
1283 ContentRemoved(childNode);
1289 // If we already have an accessible, check if we need to remove it, recreate
1290 // it, or keep it in place.
1291 LocalAccessible* acc = GetAccessible(aRoot);
1292 if (acc) {
1293 MOZ_ASSERT(aRoot == acc->GetContent(),
1294 "LocalAccessible has differing content!");
1295 #ifdef A11Y_LOG
1296 if (logging::IsEnabled(logging::eTree)) {
1297 logging::MsgBegin(
1298 "TREE", "inserted content already has accessible; doc: %p", this);
1299 logging::Node("content node", aRoot);
1300 logging::AccessibleInfo("accessible node", acc);
1301 logging::MsgEnd();
1303 #endif
1305 nsIFrame* frame = acc->GetFrame();
1306 if (frame) {
1307 acc->MaybeQueueCacheUpdateForStyleChanges();
1310 // LocalAccessible has no frame and it's not display:contents. Remove it.
1311 // As well as removing the a11y subtree, we must also remove Accessibles
1312 // for DOM descendants, since some of these might be relocated Accessibles
1313 // and their DOM nodes are now hidden as well.
1314 if (!frame && !nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
1315 ContentRemoved(aRoot);
1316 return false;
1319 // If the frame is hidden because its ancestor is specified with
1320 // `content-visibility: hidden`, remove its Accessible.
1321 if (frame && frame->IsHiddenByContentVisibilityOnAnyAncestor(
1322 nsIFrame::IncludeContentVisibility::Hidden)) {
1323 ContentRemoved(aRoot);
1324 return false;
1327 // If it's a XULLabel it was probably reframed because a `value` attribute
1328 // was added. The accessible creates its text leaf upon construction, so we
1329 // need to recreate. Remove it, and schedule for reconstruction.
1330 if (acc->IsXULLabel()) {
1331 ContentRemoved(acc);
1332 return true;
1335 // It is a broken image that is being reframed because it either got
1336 // or lost an `alt` tag that would rerender this node as text.
1337 if (frame && (acc->IsImage() != (frame->AccessibleType() == eImageType))) {
1338 ContentRemoved(aRoot);
1339 return true;
1342 // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible,
1343 // we need to recreate the LocalAccessible. This can happen for embed or
1344 // object elements if their embedded content changes to be web content.
1345 if (frame && !acc->IsOuterDoc() &&
1346 frame->AccessibleType() == eOuterDocType) {
1347 ContentRemoved(aRoot);
1348 return true;
1351 // If the content is focused, and is being re-framed, reset the selection
1352 // listener for the node because the previous selection listener is on the
1353 // old frame.
1354 if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) {
1355 SelectionMgr()->SetControlSelectionListener(aRoot->AsElement());
1358 // If the accessible is a table, or table part, its layout table
1359 // status may have changed. We need to invalidate the associated
1360 // mac table cache, which listens for the following event. We don't
1361 // use this cache when the core cache is enabled, so to minimise event
1362 // traffic only fire this event when that cache is off.
1363 if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) {
1364 LocalAccessible* table = nsAccUtils::TableFor(acc);
1365 if (table && table->IsTable()) {
1366 QueueCacheUpdate(table, CacheDomain::Table);
1370 // The accessible can be reparented or reordered in its parent.
1371 // We schedule it for reinsertion. For example, a slotted element
1372 // can change its slot attribute to a different slot.
1373 insert = true;
1375 // If the frame is invisible, remove it.
1376 // Normally, layout sends explicit a11y notifications for visibility
1377 // changes (see SendA11yNotifications in RestyleManager). However, if a
1378 // visibility change also reconstructs the frame, we must handle it here.
1379 if (frame && !frame->StyleVisibility()->IsVisible()) {
1380 ContentRemoved(aRoot);
1381 // There might be visible descendants, so we want to walk the subtree.
1382 // However, we know we don't want to reinsert this node, so we set insert
1383 // to false.
1384 insert = false;
1386 } else {
1387 // If there is no current accessible, and the node has a frame, or is
1388 // display:contents, schedule it for insertion.
1389 if (aRoot->GetPrimaryFrame() ||
1390 nsCoreUtils::CanCreateAccessibleWithoutFrame(aRoot)) {
1391 // This may be a new subtree, the insertion process will recurse through
1392 // its descendants.
1393 if (!GetAccessibleOrDescendant(aRoot)) {
1394 return true;
1397 // Content is not an accessible, but has accessible descendants.
1398 // We schedule this container for insertion strictly for the case where it
1399 // itself now needs an accessible. We will still need to recurse into the
1400 // descendant content to prune accessibles, and in all likelyness to
1401 // insert accessibles since accessible insertions will likeley get missed
1402 // in an existing subtree.
1403 insert = true;
1407 if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) {
1408 AutoTArray<nsCOMPtr<nsIContent>, 10> list;
1409 dom::AllChildrenIterator iter =
1410 dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true);
1411 while (nsIContent* childNode = iter.GetNextChild()) {
1412 if (PruneOrInsertSubtree(childNode)) {
1413 list.AppendElement(childNode);
1417 if (!list.IsEmpty()) {
1418 mNotificationController->ScheduleContentInsertion(container, list);
1422 return insert;
1425 void DocAccessible::RecreateAccessible(nsIContent* aContent) {
1426 #ifdef A11Y_LOG
1427 if (logging::IsEnabled(logging::eTree)) {
1428 logging::MsgBegin("TREE", "accessible recreated");
1429 logging::Node("content", aContent);
1430 logging::MsgEnd();
1432 #endif
1434 // XXX: we shouldn't recreate whole accessible subtree, instead we should
1435 // subclass hide and show events to handle them separately and implement their
1436 // coalescence with normal hide and show events. Note, in this case they
1437 // should be coalesced with normal show/hide events.
1438 ContentRemoved(aContent);
1439 ContentInserted(aContent, aContent->GetNextSibling());
1442 void DocAccessible::ProcessInvalidationList() {
1443 // Invalidate children of container accessible for each element in
1444 // invalidation list. Allow invalidation list insertions while container
1445 // children are recached.
1446 for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) {
1447 nsIContent* content = mInvalidationList[idx];
1448 if (!HasAccessible(content) && content->HasID()) {
1449 LocalAccessible* container = GetContainerAccessible(content);
1450 if (container) {
1451 // Check if the node is a target of aria-owns, and if so, don't process
1452 // it here and let DoARIAOwnsRelocation process it.
1453 AttrRelProviders* list = GetRelProviders(
1454 content->AsElement(), nsDependentAtomString(content->GetID()));
1455 bool shouldProcess = !!list;
1456 if (shouldProcess) {
1457 for (uint32_t idx = 0; idx < list->Length(); idx++) {
1458 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
1459 shouldProcess = false;
1460 break;
1464 if (shouldProcess) {
1465 ProcessContentInserted(container, content);
1472 mInvalidationList.Clear();
1475 void DocAccessible::ProcessQueuedCacheUpdates() {
1476 AUTO_PROFILER_MARKER_TEXT("DocAccessible::ProcessQueuedCacheUpdates", A11Y,
1477 {}, ""_ns);
1478 PerfStats::AutoMetricRecording<
1479 PerfStats::Metric::A11Y_ProcessQueuedCacheUpdate>
1480 autoRecording;
1481 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
1483 nsTArray<CacheData> data;
1484 for (auto [acc, domain] : mQueuedCacheUpdatesArray) {
1485 if (acc && acc->IsInDocument() && !acc->IsDefunct()) {
1486 RefPtr<AccAttributes> fields =
1487 acc->BundleFieldsForCache(domain, CacheUpdateType::Update);
1489 if (fields->Count()) {
1490 data.AppendElement(CacheData(
1491 acc->IsDoc() ? 0 : reinterpret_cast<uint64_t>(acc->UniqueID()),
1492 fields));
1497 mQueuedCacheUpdatesArray.Clear();
1498 mQueuedCacheUpdatesHash.Clear();
1500 if (mViewportCacheDirty) {
1501 RefPtr<AccAttributes> fields =
1502 BundleFieldsForCache(CacheDomain::Viewport, CacheUpdateType::Update);
1503 if (fields->Count()) {
1504 data.AppendElement(CacheData(0, fields));
1506 mViewportCacheDirty = false;
1509 if (data.Length()) {
1510 IPCDoc()->SendCache(CacheUpdateType::Update, data);
1514 void DocAccessible::SendAccessiblesWillMove() {
1515 if (!mIPCDoc) {
1516 return;
1518 nsTArray<uint64_t> ids;
1519 for (LocalAccessible* acc : mMovedAccessibles) {
1520 // If acc is defunct or not in a document, it was removed after it was
1521 // moved.
1522 if (!acc->IsDefunct() && acc->IsInDocument()) {
1523 ids.AppendElement(reinterpret_cast<uintptr_t>(acc->UniqueID()));
1524 // acc might have been re-parented. Since we cache bounds relative to the
1525 // parent, we need to update the cache.
1526 QueueCacheUpdate(acc, CacheDomain::Bounds);
1529 if (!ids.IsEmpty()) {
1530 mIPCDoc->SendAccessiblesWillMove(ids);
1534 LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap(
1535 nsINode* aNode) const {
1536 if (!aNode->IsContent() ||
1537 !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) {
1538 return GetAccessible(aNode);
1541 // XXX Bug 135040, incorrect when multiple images use the same map.
1542 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
1543 nsImageFrame* imageFrame = do_QueryFrame(frame);
1544 if (imageFrame) {
1545 LocalAccessible* parent = GetAccessible(imageFrame->GetContent());
1546 if (parent) {
1547 if (HTMLImageMapAccessible* imageMap = parent->AsImageMap()) {
1548 return imageMap->GetChildAccessibleFor(aNode);
1550 return nullptr;
1554 return GetAccessible(aNode);
1557 ////////////////////////////////////////////////////////////////////////////////
1558 // Protected members
1560 void DocAccessible::NotifyOfLoading(bool aIsReloading) {
1561 // Mark the document accessible as loading, if it stays alive then we'll mark
1562 // it as loaded when we receive proper notification.
1563 mLoadState &= ~eDOMLoaded;
1565 if (!IsLoadEventTarget()) return;
1567 if (aIsReloading && !mLoadEventType &&
1568 // We can't fire events on a document whose tree isn't constructed yet.
1569 HasLoadState(eTreeConstructed)) {
1570 // Fire reload and state busy events on existing document accessible while
1571 // event from user input flag can be calculated properly and accessible
1572 // is alive. When new document gets loaded then this one is destroyed.
1573 RefPtr<AccEvent> reloadEvent =
1574 new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this);
1575 nsEventShell::FireEvent(reloadEvent);
1578 // Fire state busy change event. Use delayed event since we don't care
1579 // actually if event isn't delivered when the document goes away like a shot.
1580 RefPtr<AccEvent> stateEvent =
1581 new AccStateChangeEvent(this, states::BUSY, true);
1582 FireDelayedEvent(stateEvent);
1585 void DocAccessible::DoInitialUpdate() {
1586 AUTO_PROFILER_MARKER_TEXT("DocAccessible::DoInitialUpdate", A11Y, {}, ""_ns);
1587 PerfStats::AutoMetricRecording<PerfStats::Metric::A11Y_DoInitialUpdate>
1588 autoRecording;
1589 // DO NOT ADD CODE ABOVE THIS BLOCK: THIS CODE IS MEASURING TIMINGS.
1591 if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) {
1592 mDocFlags |= eTopLevelContentDocInProcess;
1593 if (IPCAccessibilityActive()) {
1594 nsIDocShell* docShell = mDocumentNode->GetDocShell();
1595 if (RefPtr<dom::BrowserChild> browserChild =
1596 dom::BrowserChild::GetFrom(docShell)) {
1597 // In content processes, top level content documents are always
1598 // RootAccessibles.
1599 MOZ_ASSERT(IsRoot());
1600 DocAccessibleChild* ipcDoc = IPCDoc();
1601 if (!ipcDoc) {
1602 ipcDoc = new DocAccessibleChild(this, browserChild);
1603 MOZ_RELEASE_ASSERT(browserChild->SendPDocAccessibleConstructor(
1604 ipcDoc, nullptr, 0, mDocumentNode->GetBrowsingContext()));
1605 // trying to recover from this failing is problematic
1606 SetIPCDoc(ipcDoc);
1612 mLoadState |= eTreeConstructed;
1614 // Set up a root element and ARIA role mapping.
1615 UpdateRootElIfNeeded();
1617 // Build initial tree.
1618 CacheChildrenInSubtree(this);
1619 #ifdef A11Y_LOG
1620 if (logging::IsEnabled(logging::eVerbose)) {
1621 logging::Tree("TREE", "Initial subtree", this);
1623 if (logging::IsEnabled(logging::eTreeSize)) {
1624 logging::TreeSize("TREE SIZE", "Initial subtree", this);
1626 #endif
1628 // Fire reorder event after the document tree is constructed. Note, since
1629 // this reorder event is processed by parent document then events targeted to
1630 // this document may be fired prior to this reorder event. If this is
1631 // a problem then consider to keep event processing per tab document.
1632 if (!IsRoot()) {
1633 RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent());
1634 ParentDocument()->FireDelayedEvent(reorderEvent);
1637 if (ipc::ProcessChild::ExpectingShutdown()) {
1638 return;
1640 if (IPCAccessibilityActive()) {
1641 DocAccessibleChild* ipcDoc = IPCDoc();
1642 MOZ_ASSERT(ipcDoc);
1643 if (ipcDoc) {
1644 // Send an initial update for this document and its attributes. Each acc
1645 // contained in this doc will have its initial update sent in
1646 // `InsertIntoIpcTree`.
1647 SendCache(CacheDomain::All, CacheUpdateType::Initial);
1649 for (auto idx = 0U; idx < mChildren.Length(); idx++) {
1650 ipcDoc->InsertIntoIpcTree(mChildren.ElementAt(idx), true);
1656 void DocAccessible::ProcessLoad() {
1657 mLoadState |= eCompletelyLoaded;
1659 #ifdef A11Y_LOG
1660 if (logging::IsEnabled(logging::eDocLoad)) {
1661 logging::DocCompleteLoad(this, IsLoadEventTarget());
1663 #endif
1665 // Do not fire document complete/stop events for root chrome document
1666 // accessibles and for frame/iframe documents because
1667 // a) screen readers start working on focus event in the case of root chrome
1668 // documents
1669 // b) document load event on sub documents causes screen readers to act is if
1670 // entire page is reloaded.
1671 if (!IsLoadEventTarget()) return;
1673 // Fire complete/load stopped if the load event type is given.
1674 if (mLoadEventType) {
1675 RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this);
1676 FireDelayedEvent(loadEvent);
1678 mLoadEventType = 0;
1681 // Fire busy state change event.
1682 RefPtr<AccEvent> stateEvent =
1683 new AccStateChangeEvent(this, states::BUSY, false);
1684 FireDelayedEvent(stateEvent);
1687 void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider,
1688 nsAtom* aRelAttr) {
1689 dom::Element* relProviderEl = aRelProvider->Elm();
1690 if (!relProviderEl) return;
1692 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1693 nsStaticAtom* relAttr = kRelationAttrs[idx];
1694 if (aRelAttr && aRelAttr != relAttr) continue;
1696 if (relAttr == nsGkAtoms::_for) {
1697 if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
1698 nsGkAtoms::output)) {
1699 continue;
1702 } else if (relAttr == nsGkAtoms::control) {
1703 if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
1704 nsGkAtoms::description)) {
1705 continue;
1709 IDRefsIterator iter(this, relProviderEl, relAttr);
1710 while (true) {
1711 const nsDependentSubstring id = iter.NextID();
1712 if (id.IsEmpty()) break;
1714 AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id);
1715 if (providers) {
1716 AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl);
1717 if (provider) {
1718 providers->AppendElement(provider);
1720 // We've got here during the children caching. If the referenced
1721 // content is not accessible then store it to pend its container
1722 // children invalidation (this happens immediately after the caching
1723 // is finished).
1724 nsIContent* dependentContent = iter.GetElem(id);
1725 if (dependentContent) {
1726 if (!HasAccessible(dependentContent)) {
1727 mInvalidationList.AppendElement(dependentContent);
1734 // If the relation attribute is given then we don't have anything else to
1735 // check.
1736 if (aRelAttr) break;
1739 // Make sure to schedule the tree update if needed.
1740 mNotificationController->ScheduleProcessing();
1743 void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider,
1744 nsAtom* aRelAttr) {
1745 dom::Element* relProviderElm = aRelProvider->Elm();
1746 if (!relProviderElm) return;
1748 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1749 nsStaticAtom* relAttr = kRelationAttrs[idx];
1750 if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue;
1752 IDRefsIterator iter(this, relProviderElm, relAttr);
1753 while (true) {
1754 const nsDependentSubstring id = iter.NextID();
1755 if (id.IsEmpty()) break;
1757 AttrRelProviders* providers = GetRelProviders(relProviderElm, id);
1758 if (providers) {
1759 providers->RemoveElementsBy(
1760 [relAttr, relProviderElm](const auto& provider) {
1761 return provider->mRelAttr == relAttr &&
1762 provider->mContent == relProviderElm;
1765 RemoveRelProvidersIfEmpty(relProviderElm, id);
1769 // If the relation attribute is given then we don't have anything else to
1770 // check.
1771 if (aRelAttr) break;
1775 bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
1776 nsAtom* aAttribute) {
1777 if (aAttribute == nsGkAtoms::role) {
1778 // It is common for js libraries to set the role on the body element after
1779 // the document has loaded. In this case we just update the role map entry.
1780 if (mContent == aElement) {
1781 SetRoleMapEntryForDoc(aElement);
1782 if (mIPCDoc) {
1783 mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
1786 return true;
1789 // Recreate the accessible when role is changed because we might require a
1790 // different accessible class for the new role or the accessible may expose
1791 // a different sets of interfaces (COM restriction).
1792 RecreateAccessible(aElement);
1794 return true;
1797 if (aAttribute == nsGkAtoms::multiple) {
1798 if (dom::HTMLSelectElement* select =
1799 dom::HTMLSelectElement::FromNode(aElement)) {
1800 if (select->Size() <= 1) {
1801 // Adding the 'multiple' attribute to a select that has a size of 1
1802 // creates a listbox as opposed to a combobox with a popup combobox
1803 // list. Removing the attribute does the opposite.
1804 RecreateAccessible(aElement);
1805 return true;
1810 if (aAttribute == nsGkAtoms::size &&
1811 aElement->IsHTMLElement(nsGkAtoms::select)) {
1812 // Changing the size of a select element can potentially change it from a
1813 // combobox button to a listbox with different underlying implementations.
1814 RecreateAccessible(aElement);
1815 return true;
1818 if (aAttribute == nsGkAtoms::type) {
1819 // If the input[type] changes, we should recreate the accessible.
1820 RecreateAccessible(aElement);
1821 return true;
1824 if (aAttribute == nsGkAtoms::href &&
1825 !nsCoreUtils::HasClickListener(aElement)) {
1826 // If the href is added or removed for a or area elements without click
1827 // listeners, we need to recreate the accessible since the role might have
1828 // changed. Without an href or click listener, the accessible must be a
1829 // generic.
1830 if (aElement->IsHTMLElement(nsGkAtoms::a)) {
1831 LocalAccessible* acc = GetAccessible(aElement);
1832 if (!acc) {
1833 return false;
1835 if (acc->IsHTMLLink() != aElement->HasAttr(nsGkAtoms::href)) {
1836 RecreateAccessible(aElement);
1837 return true;
1839 } else if (aElement->IsHTMLElement(nsGkAtoms::area)) {
1840 // For area accessibles, we have to recreate the entire image map, since
1841 // the image map accessible manages the tree itself.
1842 LocalAccessible* areaAcc = GetAccessibleEvenIfNotInMap(aElement);
1843 if (!areaAcc || !areaAcc->LocalParent()) {
1844 return false;
1846 RecreateAccessible(areaAcc->LocalParent()->GetContent());
1847 return true;
1851 if (aElement->IsHTMLElement(nsGkAtoms::img) && aAttribute == nsGkAtoms::alt) {
1852 // If alt text changes on an img element, we may want to create or remove an
1853 // accessible for that img.
1854 if (nsAccessibilityService::ShouldCreateImgAccessible(aElement, this)) {
1855 if (GetAccessible(aElement)) {
1856 // If the accessible already exists, there's no need to create one.
1857 return false;
1859 ContentInserted(aElement, aElement->GetNextSibling());
1860 } else {
1861 ContentRemoved(aElement);
1863 return true;
1866 return false;
1869 void DocAccessible::UpdateRootElIfNeeded() {
1870 dom::Element* rootEl = mDocumentNode->GetBodyElement();
1871 if (!rootEl) {
1872 rootEl = mDocumentNode->GetRootElement();
1874 if (rootEl != mContent) {
1875 mContent = rootEl;
1876 SetRoleMapEntryForDoc(rootEl);
1877 if (mIPCDoc) {
1878 mIPCDoc->SendRoleChangedEvent(Role(), mRoleMapEntryIndex);
1884 * Content insertion helper.
1886 class InsertIterator final {
1887 public:
1888 InsertIterator(LocalAccessible* aContext,
1889 const nsTArray<nsCOMPtr<nsIContent>>* aNodes)
1890 : mChild(nullptr),
1891 mChildBefore(nullptr),
1892 mWalker(aContext),
1893 mNodes(aNodes),
1894 mNodesIdx(0) {
1895 MOZ_ASSERT(aContext, "No context");
1896 MOZ_ASSERT(aNodes, "No nodes to search for accessible elements");
1897 MOZ_COUNT_CTOR(InsertIterator);
1899 MOZ_COUNTED_DTOR(InsertIterator)
1901 LocalAccessible* Context() const { return mWalker.Context(); }
1902 LocalAccessible* Child() const { return mChild; }
1903 LocalAccessible* ChildBefore() const { return mChildBefore; }
1904 DocAccessible* Document() const { return mWalker.Document(); }
1907 * Iterates to a next accessible within the inserted content.
1909 bool Next();
1911 void Rejected() {
1912 mChild = nullptr;
1913 mChildBefore = nullptr;
1916 private:
1917 LocalAccessible* mChild;
1918 LocalAccessible* mChildBefore;
1919 TreeWalker mWalker;
1921 const nsTArray<nsCOMPtr<nsIContent>>* mNodes;
1922 nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes;
1923 uint32_t mNodesIdx;
1926 bool InsertIterator::Next() {
1927 if (mNodesIdx > 0) {
1928 // If we already processed the first node in the mNodes list,
1929 // check if we can just use the walker to get its next sibling.
1930 LocalAccessible* nextChild = mWalker.Next();
1931 if (nextChild) {
1932 mChildBefore = mChild;
1933 mChild = nextChild;
1934 return true;
1938 while (mNodesIdx < mNodes->Length()) {
1939 nsIContent* node = mNodes->ElementAt(mNodesIdx++);
1940 // Check to see if we already processed this node with this iterator.
1941 // this can happen if we get two redundant insertions in the case of a
1942 // text and frame insertion.
1943 if (!mProcessedNodes.EnsureInserted(node)) {
1944 continue;
1947 LocalAccessible* container = Document()->AccessibleOrTrueContainer(
1948 node->GetFlattenedTreeParentNode(), true);
1949 // Ignore nodes that are not contained by the container anymore.
1950 // The container might be changed, for example, because of the subsequent
1951 // overlapping content insertion (i.e. other content was inserted between
1952 // this inserted content and its container or the content was reinserted
1953 // into different container of unrelated part of tree). To avoid a double
1954 // processing of the content insertion ignore this insertion notification.
1955 // Note, the inserted content might be not in tree at all at this point
1956 // what means there's no container. Ignore the insertion too.
1957 if (container != Context()) {
1958 continue;
1961 // HTML comboboxes have no-content list accessible as an intermediate
1962 // containing all options.
1963 if (container->IsHTMLCombobox()) {
1964 container = container->LocalFirstChild();
1967 if (!container->IsAcceptableChild(node)) {
1968 continue;
1971 #ifdef A11Y_LOG
1972 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
1973 "container", container, "node", node);
1974 #endif
1976 nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr;
1977 if (prevNode && prevNode->GetNextSibling() == node) {
1978 // If inserted nodes are siblings then just move the walker next.
1979 LocalAccessible* nextChild = mWalker.Scope(node);
1980 if (nextChild) {
1981 mChildBefore = mChild;
1982 mChild = nextChild;
1983 return true;
1985 } else {
1986 // Otherwise use a new walker to find this node in the container's
1987 // subtree, and retrieve its preceding sibling.
1988 TreeWalker finder(container);
1989 if (finder.Seek(node)) {
1990 mChild = mWalker.Scope(node);
1991 if (mChild) {
1992 MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned");
1993 mChildBefore = finder.Prev();
1994 return true;
2000 return false;
2003 void DocAccessible::ProcessContentInserted(
2004 LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) {
2005 // Process insertions if the container accessible is still in tree.
2006 if (!aContainer->IsInDocument()) {
2007 return;
2010 // If new root content has been inserted then update it.
2011 if (aContainer == this) {
2012 UpdateRootElIfNeeded();
2015 InsertIterator iter(aContainer, aNodes);
2016 if (!iter.Next()) {
2017 return;
2020 #ifdef A11Y_LOG
2021 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
2022 #endif
2024 TreeMutation mt(aContainer);
2025 bool inserted = false;
2026 do {
2027 LocalAccessible* parent = iter.Child()->LocalParent();
2028 if (parent) {
2029 LocalAccessible* previousSibling = iter.ChildBefore();
2030 if (parent != aContainer ||
2031 iter.Child()->LocalPrevSibling() != previousSibling) {
2032 if (previousSibling && previousSibling->LocalParent() != aContainer) {
2033 // previousSibling hasn't been moved into aContainer yet.
2034 // previousSibling should be later in the insertion list, so the tree
2035 // will get adjusted when we process it later.
2036 MOZ_DIAGNOSTIC_ASSERT(parent == aContainer,
2037 "Child moving to new parent, but previous "
2038 "sibling in wrong parent");
2039 continue;
2041 #ifdef A11Y_LOG
2042 logging::TreeInfo("relocating accessible", 0, "old parent", parent,
2043 "new parent", aContainer, "child", iter.Child(),
2044 nullptr);
2045 #endif
2046 MoveChild(iter.Child(), aContainer,
2047 previousSibling ? previousSibling->IndexInParent() + 1 : 0);
2048 inserted = true;
2050 continue;
2053 if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) {
2054 #ifdef A11Y_LOG
2055 logging::TreeInfo("accessible was inserted", 0, "container", aContainer,
2056 "child", iter.Child(), nullptr);
2057 #endif
2059 CreateSubtree(iter.Child());
2060 mt.AfterInsertion(iter.Child());
2061 inserted = true;
2062 continue;
2065 MOZ_ASSERT_UNREACHABLE("accessible was rejected");
2066 iter.Rejected();
2067 } while (iter.Next());
2069 mt.Done();
2071 #ifdef A11Y_LOG
2072 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
2073 #endif
2075 // We might not have actually inserted anything if layout frame reconstruction
2076 // occurred.
2077 if (inserted) {
2078 FireEventsOnInsertion(aContainer);
2082 void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer,
2083 nsIContent* aNode) {
2084 if (!aContainer->IsInDocument()) {
2085 return;
2088 #ifdef A11Y_LOG
2089 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
2090 #endif
2092 #ifdef A11Y_LOG
2093 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
2094 "container", aContainer, "node", aNode);
2095 #endif
2097 TreeWalker walker(aContainer);
2098 if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) {
2099 LocalAccessible* child = GetAccessible(aNode);
2100 if (!child) {
2101 child = GetAccService()->CreateAccessible(aNode, aContainer);
2104 if (child) {
2105 TreeMutation mt(aContainer);
2106 if (!aContainer->InsertAfter(child, walker.Prev())) {
2107 return;
2109 CreateSubtree(child);
2110 mt.AfterInsertion(child);
2111 mt.Done();
2113 FireEventsOnInsertion(aContainer);
2117 #ifdef A11Y_LOG
2118 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
2119 #endif
2122 void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) {
2123 // Check to see if change occurred inside an alert, and fire an EVENT_ALERT
2124 // if it did.
2125 if (aContainer->IsAlert() || aContainer->IsInsideAlert()) {
2126 LocalAccessible* ancestor = aContainer;
2127 do {
2128 if (ancestor->IsAlert()) {
2129 FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor);
2130 break;
2132 } while ((ancestor = ancestor->LocalParent()));
2136 void DocAccessible::ContentRemoved(LocalAccessible* aChild) {
2137 MOZ_DIAGNOSTIC_ASSERT(aChild != this, "Should never be called for the doc");
2138 LocalAccessible* parent = aChild->LocalParent();
2139 MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree");
2141 #ifdef A11Y_LOG
2142 logging::TreeInfo("process content removal", 0, "container", parent, "child",
2143 aChild, nullptr);
2144 #endif
2146 // XXX: event coalescence may kill us
2147 RefPtr<LocalAccessible> kungFuDeathGripChild(aChild);
2149 TreeMutation mt(parent);
2150 mt.BeforeRemoval(aChild);
2152 if (aChild->IsDefunct()) {
2153 MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible");
2154 mt.Done();
2155 return;
2158 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1");
2160 if (aChild->IsRelocated()) {
2161 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent);
2162 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2163 owned->RemoveElement(aChild);
2164 if (owned->Length() == 0) {
2165 mARIAOwnsHash.Remove(parent);
2168 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2");
2169 UncacheChildrenInSubtree(aChild);
2170 parent->RemoveChild(aChild);
2172 mt.Done();
2175 void DocAccessible::ContentRemoved(nsIContent* aContentNode) {
2176 if (!mRemovedNodes.EnsureInserted(aContentNode)) {
2177 return;
2180 // If child node is not accessible then look for its accessible children.
2181 LocalAccessible* acc = GetAccessible(aContentNode);
2182 if (acc) {
2183 ContentRemoved(acc);
2186 dom::AllChildrenIterator iter =
2187 dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true);
2188 while (nsIContent* childNode = iter.GetNextChild()) {
2189 ContentRemoved(childNode);
2192 // If this node has a shadow root, remove its explicit children too.
2193 // The host node may be removed after the shadow root was attached, and
2194 // before we asynchronously prune the light DOM and construct the shadow DOM.
2195 // If this is a case where the node does not have its own accessible, we will
2196 // not recurse into its current children, so we need to use an
2197 // ExplicitChildIterator in order to get its accessible children in the light
2198 // DOM, since they are not accessible anymore via AllChildrenIterator.
2199 if (aContentNode->GetShadowRoot()) {
2200 for (nsIContent* childNode = aContentNode->GetFirstChild(); childNode;
2201 childNode = childNode->GetNextSibling()) {
2202 ContentRemoved(childNode);
2207 bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) {
2208 if (!aElement->HasID()) return false;
2210 AttrRelProviders* list = GetRelProviders(
2211 aElement->AsElement(), nsDependentAtomString(aElement->GetID()));
2212 if (list) {
2213 for (uint32_t idx = 0; idx < list->Length(); idx++) {
2214 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
2215 LocalAccessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
2216 if (owner) {
2217 mNotificationController->ScheduleRelocation(owner);
2218 return true;
2224 return false;
2227 void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
2228 MOZ_ASSERT(aOwner, "aOwner must be a valid pointer");
2229 MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer");
2231 #ifdef A11Y_LOG
2232 logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
2233 #endif
2235 nsTArray<RefPtr<LocalAccessible>>* owned =
2236 mARIAOwnsHash.GetOrInsertNew(aOwner);
2238 IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
2239 uint32_t idx = 0;
2240 while (nsIContent* childEl = iter.NextElem()) {
2241 LocalAccessible* child = GetAccessible(childEl);
2242 auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
2244 // Make an attempt to create an accessible if it wasn't created yet.
2245 if (!child) {
2246 // An owned child cannot be an ancestor of the owner.
2247 bool ok = true;
2248 bool check = true;
2249 for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc();
2250 parent = parent->LocalParent()) {
2251 if (check) {
2252 if (parent->Elm()->IsInclusiveDescendantOf(childEl)) {
2253 ok = false;
2254 break;
2257 // We need to do the DOM descendant check again whenever the DOM
2258 // lineage changes. If parent is relocated, that means the next
2259 // ancestor will have a different DOM lineage.
2260 check = parent->IsRelocated();
2262 if (!ok) {
2263 continue;
2266 if (aOwner->IsAcceptableChild(childEl)) {
2267 child = GetAccService()->CreateAccessible(childEl, aOwner);
2268 if (child) {
2269 TreeMutation imut(aOwner);
2270 aOwner->InsertChildAt(insertIdx, child);
2271 imut.AfterInsertion(child);
2272 imut.Done();
2274 child->SetRelocated(true);
2275 owned->InsertElementAt(idx, child);
2276 idx++;
2278 // Create subtree before adjusting the insertion index, since subtree
2279 // creation may alter children in the container.
2280 CreateSubtree(child);
2281 FireEventsOnInsertion(aOwner);
2284 continue;
2287 #ifdef A11Y_LOG
2288 logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate",
2289 child, nullptr);
2290 #endif
2292 if (owned->IndexOf(child) < idx) {
2293 continue; // ignore second entry of same ID
2296 // Same child on same position, no change.
2297 if (child->LocalParent() == aOwner) {
2298 int32_t indexInParent = child->IndexInParent();
2300 // The child is being placed in its current index,
2301 // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'.
2302 if (indexInParent == static_cast<int32_t>(insertIdx)) {
2303 MOZ_ASSERT(child->IsRelocated(),
2304 "A child, having an index in parent from aria ownded "
2305 "indices range, has to be aria owned");
2306 MOZ_ASSERT(owned->ElementAt(idx) == child,
2307 "Unexpected child in ARIA owned array");
2308 idx++;
2309 continue;
2312 // The child is being inserted directly after its current index,
2313 // resulting in a no-move case. This will happen when a parent aria-owns
2314 // its last ordinal child:
2315 // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul>
2316 if (indexInParent == static_cast<int32_t>(insertIdx) - 1) {
2317 MOZ_ASSERT(!child->IsRelocated(),
2318 "Child should be in its ordinal position");
2319 child->SetRelocated(true);
2320 owned->InsertElementAt(idx, child);
2321 idx++;
2322 continue;
2326 MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!");
2328 // A new child is found, check for loops.
2329 if (child->LocalParent() != aOwner) {
2330 // Child is aria-owned by another container, skip.
2331 if (child->IsRelocated()) {
2332 continue;
2335 LocalAccessible* parent = aOwner;
2336 while (parent && parent != child && !parent->IsDoc()) {
2337 parent = parent->LocalParent();
2339 // A referred child cannot be a parent of the owner.
2340 if (parent == child) {
2341 continue;
2345 if (MoveChild(child, aOwner, insertIdx)) {
2346 child->SetRelocated(true);
2347 MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner));
2348 owned = mARIAOwnsHash.GetOrInsertNew(aOwner);
2349 owned->InsertElementAt(idx, child);
2350 idx++;
2354 // Put back children that are not seized anymore.
2355 PutChildrenBack(owned, idx);
2356 if (owned->Length() == 0) {
2357 mARIAOwnsHash.Remove(aOwner);
2361 void DocAccessible::PutChildrenBack(
2362 nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) {
2363 MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index");
2365 for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
2366 LocalAccessible* child = aChildren->ElementAt(idx);
2367 if (!child->IsInDocument()) {
2368 continue;
2371 // Remove the child from the owner
2372 LocalAccessible* owner = child->LocalParent();
2373 if (!owner) {
2374 NS_ERROR("Cannot put the child back. No parent, a broken tree.");
2375 continue;
2378 #ifdef A11Y_LOG
2379 logging::TreeInfo("aria owns put child back", 0, "old parent", owner,
2380 "child", child, nullptr);
2381 #endif
2383 // Unset relocated flag to find an insertion point for the child.
2384 child->SetRelocated(false);
2386 nsIContent* content = child->GetContent();
2387 int32_t idxInParent = -1;
2388 LocalAccessible* origContainer =
2389 AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode());
2390 // This node has probably been detached or removed from the DOM, so we have
2391 // nowhere to move it.
2392 if (!origContainer) {
2393 continue;
2396 // If the target container or any of its ancestors aren't in the document,
2397 // there's no need to determine where the child should go for relocation
2398 // since the target tree is going away.
2399 bool origContainerHasOutOfDocAncestor = false;
2400 LocalAccessible* ancestor = origContainer;
2401 while (ancestor) {
2402 if (ancestor->IsDoc()) {
2403 break;
2405 if (!ancestor->IsInDocument()) {
2406 origContainerHasOutOfDocAncestor = true;
2407 break;
2409 ancestor = ancestor->LocalParent();
2411 if (origContainerHasOutOfDocAncestor) {
2412 continue;
2415 TreeWalker walker(origContainer);
2416 if (!walker.Seek(content)) {
2417 continue;
2419 LocalAccessible* prevChild = walker.Prev();
2420 if (prevChild) {
2421 idxInParent = prevChild->IndexInParent() + 1;
2422 MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(),
2423 "Broken tree");
2424 origContainer = prevChild->LocalParent();
2425 } else {
2426 idxInParent = 0;
2429 // The child may have already be in its ordinal place for 2 reasons:
2430 // 1. It was the last ordinal child, and the first aria-owned child.
2431 // given: <ul id="list" aria-owns="b"><li id="a"></li><li
2432 // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", "");
2433 // 2. The preceding adopted children were just reclaimed, eg:
2434 // given: <ul id="list"><li id="b"></li></ul>
2435 // after load: $("list").setAttribute("aria-owns", "a b");
2436 // later: $("list").setAttribute("aria-owns", "");
2437 if (origContainer != owner || child->IndexInParent() != idxInParent) {
2438 // Only attempt to move the child if the target container would accept it.
2439 // Otherwise, just allow it to be removed from the tree, since it would
2440 // not be allowed in normal tree creation.
2441 if (origContainer->IsAcceptableChild(child->GetContent())) {
2442 DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent);
2443 MOZ_ASSERT(moved, "Failed to put child back.");
2445 } else {
2446 MOZ_ASSERT(!child->LocalPrevSibling() ||
2447 !child->LocalPrevSibling()->IsRelocated(),
2448 "No relocated child should appear before this one");
2449 MOZ_ASSERT(!child->LocalNextSibling() ||
2450 child->LocalNextSibling()->IsRelocated(),
2451 "No ordinal child should appear after this one");
2455 aChildren->RemoveLastElements(aChildren->Length() - aStartIdx);
2458 void DocAccessible::TrackMovedAccessible(LocalAccessible* aAcc) {
2459 MOZ_ASSERT(aAcc->mDoc == this);
2460 // If an Accessible is inserted and moved during the same tick, don't track
2461 // it as a move because it hasn't been shown yet.
2462 if (!mInsertedAccessibles.Contains(aAcc)) {
2463 mMovedAccessibles.EnsureInserted(aAcc);
2465 // When we move an Accessible, we're also moving its descendants.
2466 if (aAcc->IsOuterDoc()) {
2467 // Don't descend into other documents.
2468 return;
2470 for (uint32_t c = 0, count = aAcc->ContentChildCount(); c < count; ++c) {
2471 TrackMovedAccessible(aAcc->ContentChildAt(c));
2475 bool DocAccessible::MoveChild(LocalAccessible* aChild,
2476 LocalAccessible* aNewParent,
2477 int32_t aIdxInParent) {
2478 MOZ_ASSERT(aChild, "No child");
2479 MOZ_ASSERT(aChild->LocalParent(), "No parent");
2480 // We can't guarantee MoveChild works correctly for accessibilities storing
2481 // children outside mChildren.
2482 MOZ_ASSERT(
2483 aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()),
2484 "Wrong insertion point for a moving child");
2486 LocalAccessible* curParent = aChild->LocalParent();
2488 if (!aNewParent->IsAcceptableChild(aChild->GetContent())) {
2489 return false;
2492 #ifdef A11Y_LOG
2493 logging::TreeInfo("move child", 0, "old parent", curParent, "new parent",
2494 aNewParent, "child", aChild, nullptr);
2495 #endif
2497 // Forget aria-owns info in case of ARIA owned element. The caller is expected
2498 // to update it if needed.
2499 if (aChild->IsRelocated()) {
2500 aChild->SetRelocated(false);
2501 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent);
2502 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2503 owned->RemoveElement(aChild);
2504 if (owned->Length() == 0) {
2505 mARIAOwnsHash.Remove(curParent);
2509 if (curParent == aNewParent) {
2510 MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case");
2511 curParent->RelocateChild(aIdxInParent, aChild);
2512 if (mIPCDoc) {
2513 TrackMovedAccessible(aChild);
2516 #ifdef A11Y_LOG
2517 logging::TreeInfo("move child: parent tree after", logging::eVerbose,
2518 curParent);
2519 #endif
2520 return true;
2523 // If the child cannot be re-inserted into the tree, then make sure to remove
2524 // it from its present parent and then shutdown it.
2525 bool hasInsertionPoint =
2526 (aIdxInParent >= 0) &&
2527 (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()));
2529 TreeMutation rmut(curParent);
2530 rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown);
2531 curParent->RemoveChild(aChild);
2532 rmut.Done();
2534 // No insertion point for the child.
2535 if (!hasInsertionPoint) {
2536 return true;
2539 TreeMutation imut(aNewParent);
2540 aNewParent->InsertChildAt(aIdxInParent, aChild);
2541 if (mIPCDoc) {
2542 TrackMovedAccessible(aChild);
2544 imut.AfterInsertion(aChild);
2545 imut.Done();
2547 #ifdef A11Y_LOG
2548 logging::TreeInfo("move child: old parent tree after", logging::eVerbose,
2549 curParent);
2550 logging::TreeInfo("move child: new parent tree after", logging::eVerbose,
2551 aNewParent);
2552 #endif
2554 return true;
2557 void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot,
2558 LocalAccessible** aFocusedAcc) {
2559 // If the accessible is focused then report a focus event after all related
2560 // mutation events.
2561 if (aFocusedAcc && !*aFocusedAcc &&
2562 FocusMgr()->HasDOMFocus(aRoot->GetContent())) {
2563 *aFocusedAcc = aRoot;
2566 LocalAccessible* root =
2567 aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot;
2568 if (root->KidsFromDOM()) {
2569 TreeMutation mt(root, TreeMutation::kNoEvents);
2570 TreeWalker walker(root);
2571 while (LocalAccessible* child = walker.Next()) {
2572 if (child->IsBoundToParent()) {
2573 MoveChild(child, root, root->mChildren.Length());
2574 continue;
2577 root->AppendChild(child);
2578 mt.AfterInsertion(child);
2580 CacheChildrenInSubtree(child, aFocusedAcc);
2582 mt.Done();
2585 // Fire events for ARIA elements.
2586 if (!aRoot->HasARIARole()) {
2587 return;
2590 // XXX: we should delay document load complete event if the ARIA document
2591 // has aria-busy.
2592 roles::Role role = aRoot->ARIARole();
2593 if (!aRoot->IsDoc() &&
2594 (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) {
2595 FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
2599 void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
2600 aRoot->mStateFlags |= eIsNotInDocument;
2601 RemoveDependentIDsFor(aRoot);
2603 // The parent of the removed subtree is about to be cleared, so we must do
2604 // this here rather than in LocalAccessible::UnbindFromParent because we need
2605 // the ancestry for this to work.
2606 if (aRoot->IsTable() || aRoot->IsTableCell()) {
2607 CachedTableAccessible::Invalidate(aRoot);
2610 // Put relocated children back in their original places instead of removing
2611 // them from the tree.
2612 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot);
2613 if (owned) {
2614 PutChildrenBack(owned, 0);
2615 MOZ_ASSERT(owned->IsEmpty(),
2616 "Owned Accessibles should be cleared after PutChildrenBack.");
2617 mARIAOwnsHash.Remove(aRoot);
2618 owned = nullptr;
2621 const uint32_t count = aRoot->ContentChildCount();
2622 for (uint32_t idx = 0; idx < count; ++idx) {
2623 LocalAccessible* child = aRoot->ContentChildAt(idx);
2625 MOZ_ASSERT(!child->IsRelocated(),
2626 "No children should be relocated here. They should all have "
2627 "been relocated by PutChildrenBack.");
2629 // Removing this accessible from the document doesn't mean anything about
2630 // accessibles for subdocuments, so skip removing those from the tree.
2631 if (!child->IsDoc()) {
2632 UncacheChildrenInSubtree(child);
2636 if (aRoot->IsNodeMapEntry() &&
2637 mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) {
2638 mNodeToAccessibleMap.Remove(aRoot->GetNode());
2642 void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) {
2643 MOZ_ASSERT(!nsAccessibilityService::IsShutdown());
2644 // Traverse through children and shutdown them before this accessible. When
2645 // child gets shutdown then it removes itself from children array of its
2646 // parent. Use jdx index to process the cases if child is not attached to the
2647 // parent and as result doesn't remove itself from its children.
2648 uint32_t count = aAccessible->ContentChildCount();
2649 for (uint32_t idx = 0, jdx = 0; idx < count; idx++) {
2650 LocalAccessible* child = aAccessible->ContentChildAt(jdx);
2651 if (!child->IsBoundToParent()) {
2652 NS_ERROR("Parent refers to a child, child doesn't refer to parent!");
2653 jdx++;
2656 // Don't cross document boundaries. The outerdoc shutdown takes care about
2657 // its subdocument.
2658 if (!child->IsDoc()) {
2659 ShutdownChildrenInSubtree(child);
2660 if (nsAccessibilityService::IsShutdown()) {
2661 // If XPCOM is the only consumer (devtools & mochitests), shutting down
2662 // the child's subtree can cause a11y to shut down because the last
2663 // xpcom accessibles will be removed. In that case, return early, our
2664 // work is done.
2665 return;
2670 UnbindFromDocument(aAccessible);
2673 bool DocAccessible::IsLoadEventTarget() const {
2674 nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
2675 if (!treeItem) {
2676 return false;
2679 nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
2680 treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
2682 // Not a root document.
2683 if (parentTreeItem) {
2684 // Return true if it's either:
2685 // a) tab document;
2686 nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
2687 treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
2688 if (parentTreeItem == rootTreeItem) return true;
2690 // b) frame/iframe document and its parent document is not in loading state
2691 // Note: we can get notifications while document is loading (and thus
2692 // while there's no parent document yet).
2693 DocAccessible* parentDoc = ParentDocument();
2694 return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
2697 // It's content (not chrome) root document.
2698 return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
2701 void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) {
2702 MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!");
2703 mIPCDoc = aIPCDoc;
2706 void DocAccessible::DispatchScrollingEvent(nsINode* aTarget,
2707 uint32_t aEventType) {
2708 LocalAccessible* acc = GetAccessible(aTarget);
2709 if (!acc) {
2710 return;
2713 nsIFrame* frame = acc->GetFrame();
2714 if (!frame) {
2715 // Although the accessible had a frame at scroll time, it may now be gone
2716 // because of display: contents.
2717 return;
2720 auto [scrollPoint, scrollRange] = ComputeScrollData(acc);
2722 int32_t appUnitsPerDevPixel =
2723 mPresShell->GetPresContext()->AppUnitsPerDevPixel();
2725 LayoutDeviceIntPoint scrollPointDP = LayoutDevicePoint::FromAppUnitsToNearest(
2726 scrollPoint, appUnitsPerDevPixel);
2727 LayoutDeviceIntRect scrollRangeDP =
2728 LayoutDeviceRect::FromAppUnitsToNearest(scrollRange, appUnitsPerDevPixel);
2730 RefPtr<AccEvent> event =
2731 new AccScrollingEvent(aEventType, acc, scrollPointDP.x, scrollPointDP.y,
2732 scrollRangeDP.width, scrollRangeDP.height);
2733 nsEventShell::FireEvent(event);
2736 void DocAccessible::ARIAActiveDescendantIDMaybeMoved(
2737 LocalAccessible* aAccessible) {
2738 LocalAccessible* widget = nullptr;
2739 if (aAccessible->IsActiveDescendant(&widget) && widget) {
2740 // The active descendant might have just been inserted and may not be in the
2741 // tree yet. Therefore, schedule this async to ensure the tree is up to
2742 // date.
2743 mNotificationController
2744 ->ScheduleNotification<DocAccessible, LocalAccessible>(
2745 this, &DocAccessible::ARIAActiveDescendantChanged, widget);
2749 void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) {
2750 const nsRoleMapEntry* entry = aria::GetRoleMap(aElement);
2751 if (!entry || entry->role == roles::APPLICATION ||
2752 entry->role == roles::DIALOG ||
2753 // Role alert isn't valid on the body element according to the ARIA spec,
2754 // but it's useful for our UI; e.g. the WebRTC sharing indicator.
2755 (entry->role == roles::ALERT && !mDocumentNode->IsContentDocument())) {
2756 SetRoleMapEntry(entry);
2757 return;
2759 // No other ARIA roles are valid on body elements.
2760 SetRoleMapEntry(nullptr);
2763 LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const {
2764 return aNode == mDocumentNode ? const_cast<DocAccessible*>(this)
2765 : mNodeToAccessibleMap.Get(aNode);
2768 bool DocAccessible::HasPrimaryAction() const {
2769 if (HyperTextAccessible::HasPrimaryAction()) {
2770 return true;
2772 // mContent is normally the body, but there might be a click listener on the
2773 // root.
2774 dom::Element* root = mDocumentNode->GetRootElement();
2775 if (mContent != root) {
2776 return nsCoreUtils::HasClickListener(root);
2778 return false;
2781 void DocAccessible::ActionNameAt(uint8_t aIndex, nsAString& aName) {
2782 aName.Truncate();
2783 if (aIndex != 0) {
2784 return;
2786 if (HasPrimaryAction()) {
2787 aName.AssignLiteral("click");
2791 void DocAccessible::MaybeHandleChangeToHiddenNameOrDescription(
2792 nsIContent* aChild) {
2793 if (!HasLoadState(eTreeConstructed)) {
2794 return;
2796 for (nsIContent* content = aChild; content; content = content->GetParent()) {
2797 if (HasAccessible(content)) {
2798 // This node isn't hidden. Events for name/description dependents will be
2799 // fired elsewhere.
2800 break;
2802 nsAtom* id = content->GetID();
2803 if (!id) {
2804 continue;
2806 auto* providers =
2807 GetRelProviders(content->AsElement(), nsDependentAtomString(id));
2808 if (!providers) {
2809 continue;
2811 for (auto& provider : *providers) {
2812 if (provider->mRelAttr != nsGkAtoms::aria_labelledby &&
2813 provider->mRelAttr != nsGkAtoms::aria_describedby) {
2814 continue;
2816 LocalAccessible* dependentAcc = GetAccessible(provider->mContent);
2817 if (!dependentAcc) {
2818 continue;
2820 FireDelayedEvent(provider->mRelAttr == nsGkAtoms::aria_labelledby
2821 ? nsIAccessibleEvent::EVENT_NAME_CHANGE
2822 : nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
2823 dependentAcc);