/* -*- 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 "EventTree.h" #include "HTMLImageMapAccessible.h" #include "mozilla/ProfilerMarkers.h" #include "nsAccUtils.h" #include "nsEventShell.h" #include "nsIIOService.h" #include "nsLayoutUtils.h" #include "nsTextEquivUtils.h" #include "mozilla/a11y/Role.h" #include "TreeWalker.h" #include "xpcAccessibleDocument.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 "nsIURI.h" #include "nsIWebNavigation.h" #include "nsFocusManager.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/PerfStats.h" #include "mozilla/PresShell.h" #include "nsAccessibilityService.h" #include "mozilla/a11y/DocAccessibleChild.h" #include "mozilla/dom/AncestorIterator.h" #include "mozilla/dom/BrowserChild.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, nsGkAtoms::popovertarget}; static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs); static nsStaticAtom* const kSingleElementRelationIdlAttrs[] = { nsGkAtoms::popovertarget}; //////////////////////////////////////////////////////////////////////////////// // 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. HyperTextAccessible(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), 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(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(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_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; } // These strong references aren't necessary because WithEntryHandle is // guaranteed to run synchronously. However, static analysis complains without // them. RefPtr self = this; RefPtr acc = aAcc; size_t arrayIndex = mQueuedCacheUpdatesHash.WithEntryHandle(aAcc, [self, acc](auto&& entry) { if (entry.HasEntry()) { // This LocalAccessible has already been queued. Return its index in // the queue array so we can update its queued domains. return entry.Data(); } // Add this LocalAccessible to the queue array. size_t index = self->mQueuedCacheUpdatesArray.Length(); self->mQueuedCacheUpdatesArray.EmplaceBack(std::make_pair(acc, 0)); // Also add it to the hash map so we can avoid processing the same // LocalAccessible twice. return entry.Insert(index); }); auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex]; MOZ_ASSERT(arrayAcc == aAcc); domain |= aNewDomain; Controller()->ScheduleProcessing(); } void DocAccessible::QueueCacheUpdateForDependentRelations( LocalAccessible* aAcc) { if (!mIPCDoc || !aAcc || !aAcc->IsInDocument() || aAcc->IsDefunct()) { return; } dom::Element* el = aAcc->Elm(); if (!el) { return; } // 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 or Element have their relation cache entries // updated. RelatedAccIterator iter(this, el, nullptr); while (LocalAccessible* relatedAcc = iter.Next()) { if (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. mQueuedCacheUpdatesArray.Clear(); mQueuedCacheUpdatesHash.Clear(); // XXX thinking about ordering? if (mIPCDoc) { MOZ_ASSERT(IPCAccessibilityActive()); mIPCDoc->Shutdown(); MOZ_ASSERT(!mIPCDoc); } mDependentIDsHashes.Clear(); mDependentElementsMap.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(); } HyperTextAccessible::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 //