From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- accessible/generic/DocAccessible.cpp | 2771 ++++++++++++++++++++++++++++++++++ 1 file changed, 2771 insertions(+) create mode 100644 accessible/generic/DocAccessible.cpp (limited to 'accessible/generic/DocAccessible.cpp') diff --git a/accessible/generic/DocAccessible.cpp b/accessible/generic/DocAccessible.cpp new file mode 100644 index 0000000000..fda332ebe0 --- /dev/null +++ b/accessible/generic/DocAccessible.cpp @@ -0,0 +1,2771 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LocalAccessible-inl.h" +#include "AccIterator.h" +#include "AccAttributes.h" +#include "CachedTableAccessible.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleChild.h" +#include "EventTree.h" +#include "HTMLImageMapAccessible.h" +#include "mozilla/ProfilerMarkers.h" +#include "nsAccCache.h" +#include "nsAccessiblePivot.h" +#include "nsAccUtils.h" +#include "nsEventShell.h" +#include "nsIIOService.h" +#include "nsLayoutUtils.h" +#include "nsTextEquivUtils.h" +#include "Pivot.h" +#include "Role.h" +#include "RootAccessible.h" +#include "TreeWalker.h" +#include "xpcAccessibleDocument.h" + +#include "nsCommandManager.h" +#include "nsContentUtils.h" +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsPIDOMWindow.h" +#include "nsIContentInlines.h" +#include "nsIEditingSession.h" +#include "nsIFrame.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsImageFrame.h" +#include "nsViewManager.h" +#include "nsIScrollableFrame.h" +#include "nsUnicharUtils.h" +#include "nsIURI.h" +#include "nsIWebNavigation.h" +#include "nsFocusManager.h" +#include "nsTHashSet.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/Components.h" // for mozilla::components +#include "mozilla/EditorBase.h" +#include "mozilla/HTMLEditor.h" +#include "mozilla/ipc/ProcessChild.h" +#include "mozilla/PresShell.h" +#include "nsAccessibilityService.h" +#include "mozilla/a11y/DocAccessibleParent.h" +#include "mozilla/dom/AncestorIterator.h" +#include "mozilla/dom/BrowserChild.h" +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/DocumentType.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/HTMLSelectElement.h" +#include "mozilla/dom/MutationEventBinding.h" +#include "mozilla/dom/UserActivation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// Static member initialization + +static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby, + nsGkAtoms::aria_describedby, + nsGkAtoms::aria_details, + nsGkAtoms::aria_owns, + nsGkAtoms::aria_controls, + nsGkAtoms::aria_flowto, + nsGkAtoms::aria_errormessage, + nsGkAtoms::_for, + nsGkAtoms::control}; + +static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); + +//////////////////////////////////////////////////////////////////////////////// +// Constructor/desctructor + +DocAccessible::DocAccessible(dom::Document* aDocument, + PresShell* aPresShell) + : // XXX don't pass a document to the LocalAccessible constructor so that + // we don't set mDoc until our vtable is fully setup. If we set mDoc + // before setting up the vtable we will call LocalAccessible::AddRef() + // but not the overrides of it for subclasses. It is important to call + // those overrides to avoid confusing leak checking machinary. + HyperTextAccessibleWrap(nullptr, nullptr), + // XXX aaronl should we use an algorithm for the initial cache size? + mAccessibleCache(kDefaultCacheLength), + mNodeToAccessibleMap(kDefaultCacheLength), + mDocumentNode(aDocument), + mLoadState(eTreeConstructionPending), + mDocFlags(0), + mViewportCacheDirty(false), + mLoadEventType(0), + mPrevStateBits(0), + mVirtualCursor(nullptr), + mPresShell(aPresShell), + mIPCDoc(nullptr) { + mGenericTypes |= eDocument; + mStateFlags |= eNotNodeMapEntry; + mDoc = this; + + MOZ_ASSERT(mPresShell, "should have been given a pres shell"); + mPresShell->SetDocAccessible(this); +} + +DocAccessible::~DocAccessible() { + NS_ASSERTION(!mPresShell, "LastRelease was never called!?!"); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible) + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible, + LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments) + for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) { + for (const auto& providers : hashEntry->Values()) { + for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME( + cb, "content of dependent ids hash entry of document accessible"); + + const auto& provider = (*providers)[provIdx]; + cb.NoteXPCOMChild(provider->mContent); + } + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingUpdates) + for (const auto& ar : tmp->mARIAOwnsHash.Values()) { + for (uint32_t i = 0; i < ar->Length(); i++) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item"); + cb.NoteXPCOMChild(ar->ElementAt(i)); + } + } +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments) + tmp->mDependentIDsHashes.Clear(); + tmp->mNodeToAccessibleMap.Clear(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingUpdates) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + tmp->mARIAOwnsHash.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver) +NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible) + +NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible) +NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible) + +//////////////////////////////////////////////////////////////////////////////// +// nsIAccessible + +ENameValueFlag DocAccessible::Name(nsString& aName) const { + aName.Truncate(); + + if (mParent) { + mParent->Name(aName); // Allow owning iframe to override the name + } + if (aName.IsEmpty()) { + // Allow name via aria-labelledby or title attribute + LocalAccessible::Name(aName); + } + if (aName.IsEmpty()) { + Title(aName); // Try title element + } + if (aName.IsEmpty()) { // Last resort: use URL + URL(aName); + } + + return eNameOK; +} + +// LocalAccessible public method +role DocAccessible::NativeRole() const { + nsCOMPtr docShell = nsCoreUtils::GetDocShellFor(mDocumentNode); + if (docShell) { + nsCOMPtr sameTypeRoot; + docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot)); + int32_t itemType = docShell->ItemType(); + if (sameTypeRoot == docShell) { + // Root of content or chrome tree + if (itemType == nsIDocShellTreeItem::typeChrome) { + return roles::CHROME_WINDOW; + } + + if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } else if (itemType == nsIDocShellTreeItem::typeContent) { + return roles::DOCUMENT; + } + } + + return roles::PANE; // Fall back; +} + +void DocAccessible::Description(nsString& aDescription) const { + if (mParent) mParent->Description(aDescription); + + if (HasOwnContent() && aDescription.IsEmpty()) { + nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby, + aDescription); + } +} + +// LocalAccessible public method +uint64_t DocAccessible::NativeState() const { + // Document is always focusable. + uint64_t state = + states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl + if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED; + + // Expose stale state until the document is ready (DOM is loaded and tree is + // constructed). + if (!HasLoadState(eReady)) state |= states::STALE; + + // Expose state busy until the document and all its subdocuments is completely + // loaded. + if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY; + + nsIFrame* frame = GetFrame(); + if (!frame || !frame->IsVisibleConsideringAncestors( + nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) { + state |= states::INVISIBLE | states::OFFSCREEN; + } + + RefPtr editorBase = GetEditor(); + state |= editorBase ? states::EDITABLE : states::READONLY; + + return state; +} + +uint64_t DocAccessible::NativeInteractiveState() const { + // Document is always focusable. + return states::FOCUSABLE; +} + +bool DocAccessible::NativelyUnavailable() const { return false; } + +// LocalAccessible public method +void DocAccessible::ApplyARIAState(uint64_t* aState) const { + // Grab states from content element. + if (mContent) LocalAccessible::ApplyARIAState(aState); + + // Allow iframe/frame etc. to have final state override via ARIA. + if (mParent) mParent->ApplyARIAState(aState); +} + +Accessible* DocAccessible::FocusedChild() { + // Return an accessible for the current global focus, which does not have to + // be contained within the current document. + return FocusMgr()->FocusedAccessible(); +} + +void DocAccessible::TakeFocus() const { + // Focus the document. + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + RefPtr newFocus; + dom::AutoHandlingUserInputStatePusher inputStatePusher(true); + fm->MoveFocus(mDocumentNode->GetWindow(), nullptr, + nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus)); +} + +// HyperTextAccessible method +already_AddRefed DocAccessible::GetEditor() const { + // Check if document is editable (designMode="on" case). Otherwise check if + // the html:body (for HTML document case) or document element is editable. + if (!mDocumentNode->IsInDesignMode() && + (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) { + return nullptr; + } + + nsCOMPtr docShell = mDocumentNode->GetDocShell(); + if (!docShell) { + return nullptr; + } + + nsCOMPtr editingSession; + docShell->GetEditingSession(getter_AddRefs(editingSession)); + if (!editingSession) return nullptr; // No editing session interface + + RefPtr htmlEditor = + editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow()); + if (!htmlEditor) { + return nullptr; + } + + bool isEditable = false; + htmlEditor->GetIsDocumentEditable(&isEditable); + if (isEditable) { + return htmlEditor.forget(); + } + + return nullptr; +} + +// DocAccessible public method + +void DocAccessible::URL(nsAString& aURL) const { + aURL.Truncate(); + nsCOMPtr container = mDocumentNode->GetContainer(); + nsCOMPtr webNav(do_GetInterface(container)); + if (MOZ_UNLIKELY(!webNav)) { + return; + } + + nsCOMPtr uri; + webNav->GetCurrentURI(getter_AddRefs(uri)); + if (MOZ_UNLIKELY(!uri)) { + return; + } + // Let's avoid treating too long URI in the main process for avoiding + // memory fragmentation as far as possible. + if (uri->SchemeIs("data") || uri->SchemeIs("blob")) { + return; + } + + nsCOMPtr io = mozilla::components::IO::Service(); + if (NS_WARN_IF(!io)) { + return; + } + nsCOMPtr exposableURI; + if (NS_FAILED(io->CreateExposableURI(uri, getter_AddRefs(exposableURI))) || + MOZ_UNLIKELY(!exposableURI)) { + return; + } + nsAutoCString theURL; + if (NS_SUCCEEDED(exposableURI->GetSpec(theURL))) { + CopyUTF8toUTF16(theURL, aURL); + } +} + +void DocAccessible::Title(nsString& aTitle) const { + mDocumentNode->GetTitle(aTitle); +} + +void DocAccessible::MimeType(nsAString& aType) const { + mDocumentNode->GetContentType(aType); +} + +void DocAccessible::DocType(nsAString& aType) const { + dom::DocumentType* docType = mDocumentNode->GetDoctype(); + if (docType) docType->GetPublicId(aType); +} + +void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc, + uint64_t aNewDomain) { + if (!mIPCDoc) { + return; + } + uint64_t& domain = mQueuedCacheUpdates.LookupOrInsert(aAcc, 0); + domain |= aNewDomain; + Controller()->ScheduleProcessing(); +} + +void DocAccessible::QueueCacheUpdateForDependentRelations( + LocalAccessible* aAcc) { + if (!mIPCDoc || !aAcc || !aAcc->Elm() || !aAcc->IsInDocument() || + aAcc->IsDefunct()) { + return; + } + nsAutoString ID; + aAcc->DOMNodeID(ID); + if (AttrRelProviders* list = GetRelProviders(aAcc->Elm(), ID)) { + // We call this function when we've noticed an ID change, or when an acc + // is getting bound to its document. We need to ensure any existing accs + // that depend on this acc's ID have their rel cache entries updated. + for (const auto& provider : *list) { + LocalAccessible* relatedAcc = GetAccessible(provider->mContent); + if (!relatedAcc || relatedAcc->IsDefunct() || + !relatedAcc->IsInDocument() || + mInsertedAccessibles.Contains(relatedAcc)) { + continue; + } + QueueCacheUpdate(relatedAcc, CacheDomain::Relations); + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +// LocalAccessible + +void DocAccessible::Init() { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("document initialize", mDocumentNode, this); + } +#endif + + // Initialize notification controller. + mNotificationController = new NotificationController(this, mPresShell); + + // Mark the DocAccessible as loaded if its DOM document is already loaded at + // this point. This can happen for one of three reasons: + // 1. A11y was started late. + // 2. DOM loading for a document (probably an in-process iframe) completed + // before its Accessible container was created. + // 3. The PresShell for the document was created after DOM loading completed. + // In that case, we tried to create the DocAccessible when DOM loading + // completed, but we can't create a DocAccessible without a PresShell, so + // this failed. The DocAccessible was subsequently created due to a layout + // notification. + if (mDocumentNode->GetReadyStateEnum() == + dom::Document::READYSTATE_COMPLETE) { + mLoadState |= eDOMLoaded; + // If this happened due to reasons 1 or 2, it isn't *necessary* to fire a + // doc load complete event. If it happened due to reason 3, we need to fire + // doc load complete because clients (especially tests) might be waiting + // for the document to load using this event. We can't distinguish why this + // happened at this point, so just fire it regardless. It won't do any + // harm even if it isn't necessary. We set mLoadEventType here and it will + // be fired in ProcessLoad as usual. + mLoadEventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; + } else if (mDocumentNode->IsInitialDocument()) { + // The initial about:blank document will never finish loading, so we can + // immediately mark it loaded to avoid waiting for its load. + mLoadState |= eDOMLoaded; + } + + AddEventListeners(); +} + +void DocAccessible::Shutdown() { + if (!mPresShell) { // already shutdown + return; + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("document shutdown", mDocumentNode, this); + } +#endif + + // Mark the document as shutdown before AT is notified about the document + // removal from its container (valid for root documents on ATK and due to + // some reason for MSAA, refer to bug 757392 for details). + mStateFlags |= eIsDefunct; + + if (mNotificationController) { + mNotificationController->Shutdown(); + mNotificationController = nullptr; + } + + RemoveEventListeners(); + + // mParent->RemoveChild clears mParent, but we need to know whether we were a + // child later, so use a flag. + const bool isChild = !!mParent; + if (mParent) { + DocAccessible* parentDocument = mParent->Document(); + if (parentDocument) parentDocument->RemoveChildDocument(this); + + mParent->RemoveChild(this); + MOZ_ASSERT(!mParent, "Parent has to be null!"); + } + + mPresShell->SetDocAccessible(nullptr); + mPresShell = nullptr; // Avoid reentrancy + + // Walk the array backwards because child documents remove themselves from the + // array as they are shutdown. + int32_t childDocCount = mChildDocuments.Length(); + for (int32_t idx = childDocCount - 1; idx >= 0; idx--) { + mChildDocuments[idx]->Shutdown(); + } + + mChildDocuments.Clear(); + // mQueuedCacheUpdates can contain a reference to this document (ex. if the + // doc is scrollable and we're sending a scroll position update). Clear the + // map here to avoid creating ref cycles. + mQueuedCacheUpdates.Clear(); + + // XXX thinking about ordering? + if (mIPCDoc) { + MOZ_ASSERT(IPCAccessibilityActive()); + mIPCDoc->Shutdown(); + MOZ_ASSERT(!mIPCDoc); + } + + if (mVirtualCursor) { + mVirtualCursor->RemoveObserver(this); + mVirtualCursor = nullptr; + } + + mDependentIDsHashes.Clear(); + mNodeToAccessibleMap.Clear(); + + mAnchorJumpElm = nullptr; + mInvalidationList.Clear(); + mPendingUpdates.Clear(); + + for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) { + LocalAccessible* accessible = iter.Data(); + MOZ_ASSERT(accessible); + if (accessible) { + // This might have been focused with FocusManager::ActiveItemChanged. In + // that case, we must notify FocusManager so that it clears the active + // item. Otherwise, it will hold on to a defunct Accessible. Normally, + // this happens in UnbindFromDocument, but we don't call that when the + // whole document shuts down. + if (FocusMgr()->WasLastFocused(accessible)) { + FocusMgr()->ActiveItemChanged(nullptr); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eFocus)) { + logging::ActiveItemChangeCausedBy("doc shutdown", accessible); + } +#endif + } + if (!accessible->IsDefunct()) { + // Unlink parent to avoid its cleaning overhead in shutdown. + accessible->mParent = nullptr; + accessible->Shutdown(); + } + } + iter.Remove(); + } + + HyperTextAccessibleWrap::Shutdown(); + + MOZ_ASSERT(GetAccService()); + GetAccService()->NotifyOfDocumentShutdown( + this, mDocumentNode, + // Make sure we don't shut down AccService while a parent document is + // still shutting down. The parent will allow service shutdown when it + // reaches this point. + /* aAllowServiceShutdown */ !isChild); + mDocumentNode = nullptr; +} + +nsIFrame* DocAccessible::GetFrame() const { + nsIFrame* root = nullptr; + if (mPresShell) { + root = mPresShell->GetRootFrame(); + } + + return root; +} + +nsINode* DocAccessible::GetNode() const { return mDocumentNode; } + +// DocAccessible protected member +nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const { + *aRelativeFrame = GetFrame(); + + dom::Document* document = mDocumentNode; + dom::Document* parentDoc = nullptr; + + nsRect bounds; + while (document) { + PresShell* presShell = document->GetPresShell(); + if (!presShell) { + return nsRect(); + } + + nsRect scrollPort; + nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable(); + if (sf) { + scrollPort = sf->GetScrollPortRect(); + } else { + nsIFrame* rootFrame = presShell->GetRootFrame(); + if (!rootFrame) return nsRect(); + + scrollPort = rootFrame->GetRect(); + } + + if (parentDoc) { // After first time thru loop + // XXXroc bogus code! scrollPort is relative to the viewport of + // this document, but we're intersecting rectangles derived from + // multiple documents and assuming they're all in the same coordinate + // system. See bug 514117. + bounds.IntersectRect(scrollPort, bounds); + } else { // First time through loop + bounds = scrollPort; + } + + document = parentDoc = document->GetInProcessParentDocument(); + } + + return bounds; +} + +// DocAccessible protected member +nsresult DocAccessible::AddEventListeners() { + SelectionMgr()->AddDocSelectionListener(mPresShell); + + // Add document observer. + mDocumentNode->AddObserver(this); + return NS_OK; +} + +// DocAccessible protected member +nsresult DocAccessible::RemoveEventListeners() { + // Remove listeners associated with content documents + NS_ASSERTION(mDocumentNode, "No document during removal of listeners."); + + if (mDocumentNode) { + mDocumentNode->RemoveObserver(this); + } + + if (mScrollWatchTimer) { + mScrollWatchTimer->Cancel(); + mScrollWatchTimer = nullptr; + NS_RELEASE_THIS(); // Kung fu death grip + } + + SelectionMgr()->RemoveDocSelectionListener(mPresShell); + return NS_OK; +} + +void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) { + DocAccessible* docAcc = reinterpret_cast(aClosure); + + if (docAcc) { + // Dispatch a scroll-end for all entries in table. They have not + // been scrolled in at least `kScrollEventInterval`. + for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done(); + iter.Next()) { + docAcc->DispatchScrollingEvent(iter.Key(), + nsIAccessibleEvent::EVENT_SCROLLING_END); + iter.Remove(); + } + + if (docAcc->mScrollWatchTimer) { + docAcc->mScrollWatchTimer = nullptr; + NS_RELEASE(docAcc); // Release kung fu death grip + } + } +} + +void DocAccessible::HandleScroll(nsINode* aTarget) { + nsINode* target = aTarget; + LocalAccessible* targetAcc = GetAccessible(target); + if (!targetAcc && target->IsInNativeAnonymousSubtree()) { + // The scroll event for textareas comes from a native anonymous div. We need + // the closest non-anonymous ancestor to get the right Accessible. + target = target->GetClosestNativeAnonymousSubtreeRootParentOrHost(); + targetAcc = GetAccessible(target); + } + // Regardless of our scroll timer, we need to send a cache update + // to ensure the next Bounds() query accurately reflects our position + // after scrolling. + if (targetAcc) { + QueueCacheUpdate(targetAcc, CacheDomain::ScrollPosition); + } + + const uint32_t kScrollEventInterval = 100; + // If we haven't dispatched a scrolling event for a target in at least + // kScrollEventInterval milliseconds, dispatch one now. + mLastScrollingDispatch.WithEntryHandle(target, [&](auto&& lastDispatch) { + const TimeStamp now = TimeStamp::Now(); + + if (!lastDispatch || + (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) { + // We can't fire events on a document whose tree isn't constructed yet. + if (HasLoadState(eTreeConstructed)) { + DispatchScrollingEvent(target, nsIAccessibleEvent::EVENT_SCROLLING); + } + lastDispatch.InsertOrUpdate(now); + } + }); + + // If timer callback is still pending, push it 100ms into the future. + // When scrolling ends and we don't fire this callback anymore, the + // timer callback will fire and dispatch an EVENT_SCROLLING_END. + if (mScrollWatchTimer) { + mScrollWatchTimer->SetDelay(kScrollEventInterval); + } else { + NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer), + ScrollTimerCallback, this, kScrollEventInterval, + nsITimer::TYPE_ONE_SHOT, + "a11y::DocAccessible::ScrollPositionDidChange"); + if (mScrollWatchTimer) { + NS_ADDREF_THIS(); // Kung fu death grip + } + } +} + +std::pair DocAccessible::ComputeScrollData( + LocalAccessible* aAcc) { + nsPoint scrollPoint; + nsRect scrollRange; + + if (nsIFrame* frame = aAcc->GetFrame()) { + nsIScrollableFrame* sf = aAcc == this + ? mPresShell->GetRootScrollFrameAsScrollable() + : frame->GetScrollTargetFrame(); + + // If there is no scrollable frame, it's likely a scroll in a popup, like + //