Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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
#include "LocalAccessible-inl.h"
#include "AccIterator.h"
#include "AccAttributes.h"
#include "ARIAMap.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 "nsIURI.h"
#include "nsIWebNavigation.h"
#include "nsFocusManager.h"
#include "mozilla/AppShutdown.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/PerfStats.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.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/ElementInlines.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 = std::size(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<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
if (docShell) {
nsCOMPtr<nsIDocShellTreeItem> 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> 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<dom::Element> newFocus;
dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
}
// HyperTextAccessible method
already_AddRefed<EditorBase> 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<nsIDocShell> docShell = mDocumentNode->GetDocShell();
if (!docShell) {
return nullptr;
}
nsCOMPtr<nsIEditingSession> editingSession;
docShell->GetEditingSession(getter_AddRefs(editingSession));
if (!editingSession) return nullptr; // No editing session interface
RefPtr<HTMLEditor> 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<nsISupports> container = mDocumentNode->GetContainer();
nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
if (MOZ_UNLIKELY(!webNav)) {
return;
}
nsCOMPtr<nsIURI> 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<nsIIOService> io = mozilla::components::IO::Service();
if (NS_WARN_IF(!io)) {
return;
}
nsCOMPtr<nsIURI> 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);
}
// Certain cache domain updates might require updating other cache domains.
// This function takes the given cache domains and returns those cache domains
// plus any other required associated cache domains. Made for use with
// QueueCacheUpdate.
static uint64_t GetCacheDomainsQueueUpdateSuperset(uint64_t aCacheDomains) {
// Text domain updates imply updates to the TextOffsetAttributes and
// TextBounds domains.
if (aCacheDomains & CacheDomain::Text) {
aCacheDomains |= CacheDomain::TextOffsetAttributes;
aCacheDomains |= CacheDomain::TextBounds;
}
// Bounds domain updates imply updates to the TextBounds domain.
if (aCacheDomains & CacheDomain::Bounds) {
aCacheDomains |= CacheDomain::TextBounds;
}
return aCacheDomains;
}
void DocAccessible::QueueCacheUpdate(LocalAccessible* aAcc, uint64_t aNewDomain,
bool aBypassActiveDomains) {
if (!mIPCDoc) {
return;
}
// These strong references aren't necessary because WithEntryHandle is
// guaranteed to run synchronously. However, static analysis complains without
// them.
RefPtr<DocAccessible> self = this;
RefPtr<LocalAccessible> 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);
});
// We may need to bypass the active domain restriction when populating domains
// for the first time. In that case, queue cache updates regardless of domain.
if (aBypassActiveDomains) {
auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex];
MOZ_ASSERT(arrayAcc == aAcc);
domain |= aNewDomain;
Controller()->ScheduleProcessing();
return;
}
// Potentially queue updates for required related domains.
const uint64_t newDomains = GetCacheDomainsQueueUpdateSuperset(aNewDomain);
// Only queue cache updates for domains that are active.
const uint64_t domainsToUpdate =
nsAccessibilityService::GetActiveCacheDomains() & newDomains;
// Avoid queueing cache updates if we have no domains to update.
if (domainsToUpdate == CacheDomain::None) {
return;
}
auto& [arrayAcc, domain] = mQueuedCacheUpdatesArray[arrayIndex];
MOZ_ASSERT(arrayAcc == aAcc);
domain |= domainsToUpdate;
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
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;
ScrollContainerFrame* sf = presShell->GetRootScrollContainerFrame();
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
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<DocAccessible*>(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<nsPoint, nsRect> DocAccessible::ComputeScrollData(
LocalAccessible* aAcc) {
nsPoint scrollPoint;
nsRect scrollRange;
if (nsIFrame* frame = aAcc->GetFrame()) {
ScrollContainerFrame* sf = aAcc == this
? mPresShell->GetRootScrollContainerFrame()
: frame->GetScrollTargetFrame();
// If there is no scrollable frame, it's likely a scroll in a popup, like
// <select>. Return a scroll offset and range of 0. The scroll info
// is currently only used on Android, and popups are rendered natively
// there.
if (sf) {
scrollPoint = sf->GetScrollPosition() * mPresShell->GetResolution();
scrollRange = sf->GetScrollRange();
scrollRange.ScaleRoundOut(mPresShell->GetResolution());
}
}
return {scrollPoint, scrollRange};
}
////////////////////////////////////////////////////////////////////////////////
// nsIDocumentObserver
NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
// When a reflected element IDL attribute changes, we might get the following
// synchronous calls:
// 1. AttributeWillChange for the element.
// 2. AttributeWillChange for the content attribute.
// 3. AttributeChanged for the content attribute.
// 4. AttributeChanged for the element.
// Since the content attribute value is "" for any element, we won't always get
// 2 or 3. Even if we do, they might occur after the element has already
// changed, which means we can't detect any relevant state changes there; e.g.
// mPrevStateBits. Thus, we need 1 and 4, and we must ignore 2 and 3. To
// facilitate this, sIsAttrElementChanging will be set to true for 2 and 3.
static bool sIsAttrElementChanging = false;
void DocAccessible::AttributeWillChange(dom::Element* aElement,
int32_t aNameSpaceID,
nsAtom* aAttribute, int32_t aModType) {
if (sIsAttrElementChanging) {
// See the comment above the definition of sIsAttrElementChanging.
return;
}
LocalAccessible* accessible = GetAccessible(aElement);
if (!accessible) {
if (aElement != mContent) return;
accessible = this;
}
// Update dependent IDs cache. Take care of elements that are accessible
// because dependent IDs cache doesn't contain IDs from non accessible
// elements. We do this for attribute additions as well because there might
// be an ElementInternals default value.
RemoveDependentIDsFor(accessible, aAttribute);
RemoveDependentElementsFor(accessible, aAttribute);
if (aAttribute == nsGkAtoms::id) {
if (accessible->IsActiveDescendantId()) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::ACTIVE, false);
FireDelayedEvent(event);
}
RelocateARIAOwnedIfNeeded(aElement);
}
if (aAttribute == nsGkAtoms::aria_activedescendant) {
if (LocalAccessible* activeDescendant = accessible->CurrentItem()) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(activeDescendant, states::ACTIVE, false);
FireDelayedEvent(event);
}
}
// If attribute affects accessible's state, store the old state so we can
// later compare it against the state of the accessible after the attribute
// change.
if (accessible->AttributeChangesState(aAttribute)) {
mPrevStateBits = accessible->State();
} else {
mPrevStateBits = 0;
}
}
void DocAccessible::AttributeChanged(dom::Element* aElement,
int32_t aNameSpaceID, nsAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aOldValue) {
if (sIsAttrElementChanging) {
// See the comment above the definition of sIsAttrElementChanging.
return;
}
NS_ASSERTION(!IsDefunct(),
"Attribute changed called on defunct document accessible!");
// Proceed even if the element is not accessible because element may become
// accessible if it gets certain attribute.
if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
// Update the accessible tree on aria-hidden change. Make sure to not create
// a tree under aria-hidden='true'.
if (aAttribute == nsGkAtoms::aria_hidden) {
if (aria::HasDefinedARIAHidden(aElement)) {
ContentRemoved(aElement);
} else {
ContentInserted(aElement, aElement->GetNextSibling());
}
return;
}
if (aAttribute == nsGkAtoms::slot &&
!aElement->GetFlattenedTreeParentNode() && aElement != mContent) {
// Element is inside a shadow host but is no longer slotted.
mDoc->ContentRemoved(aElement);
return;
}
LocalAccessible* accessible = GetAccessible(aElement);
if (!accessible) {
if (mContent == aElement) {
// The attribute change occurred on the root content of this
// DocAccessible, so handle it as an attribute change on this.
accessible = this;
} else {
if (aModType == dom::MutationEvent_Binding::ADDITION &&
aria::AttrCharacteristicsFor(aAttribute) & ATTR_GLOBAL) {
// The element doesn't have an Accessible, but a global ARIA attribute
// was just added, which means we should probably create an Accessible.
ContentInserted(aElement, aElement->GetNextSibling());
return;
}
// The element doesn't have an Accessible, so ignore the attribute
// change.
return;
}
}
MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
"DOM attribute change on an accessible detached from the tree");
if (aAttribute == nsGkAtoms::id) {
dom::Element* elm = accessible->Elm();
RelocateARIAOwnedIfNeeded(elm);
ARIAActiveDescendantIDMaybeMoved(accessible);
QueueCacheUpdate(accessible, CacheDomain::DOMNodeIDAndClass);
QueueCacheUpdateForDependentRelations(accessible);
}
// The activedescendant universal property redirects accessible focus events
// to the element with the id that activedescendant points to. Make sure
// the tree up to date before processing. In other words, when a node has just
// been inserted, the tree won't be up to date yet, so we must always schedule
// an async notification so that a newly inserted node will be present in
// the tree.
if (aAttribute == nsGkAtoms::aria_activedescendant) {
mNotificationController
->ScheduleNotification<DocAccessible, LocalAccessible>(
this, &DocAccessible::ARIAActiveDescendantChanged, accessible);
return;
}
// Defer to accessible any needed actions like changing states or emiting
// events.
accessible->DOMAttributeChanged(aNameSpaceID, aAttribute, aModType, aOldValue,
mPrevStateBits);
// Update dependent IDs cache. We handle elements with accessibles.
// If the accessible or element with the ID doesn't exist yet the cache will
// be updated when they are added.
if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
aModType == dom::MutationEvent_Binding::ADDITION) {
AddDependentIDsFor(accessible, aAttribute);
AddDependentElementsFor(accessible, aAttribute);
}
}
void DocAccessible::ARIAAttributeDefaultWillChange(dom::Element* aElement,
nsAtom* aAttribute,
int32_t aModType) {
NS_ASSERTION(!IsDefunct(),
"Attribute changed called on defunct document accessible!");
if (aElement->HasAttr(aAttribute)) {
return;
}
AttributeWillChange(aElement, kNameSpaceID_None, aAttribute, aModType);
}
void DocAccessible::ARIAAttributeDefaultChanged(dom::Element* aElement,
nsAtom* aAttribute,
int32_t aModType) {
NS_ASSERTION(!IsDefunct(),
"Attribute changed called on defunct document accessible!");
if (aElement->HasAttr(aAttribute)) {
return;
}
AttributeChanged(aElement, kNameSpaceID_None, aAttribute, aModType, nullptr);
}
void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
if (dom::Element* elm = aAccessible->Elm()) {
nsAutoString id;
if (dom::Element* activeDescendantElm =
nsCoreUtils::GetAriaActiveDescendantElement(elm)) {
LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm);
if (activeDescendant) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(activeDescendant, states::ACTIVE, true);
FireDelayedEvent(event);
if (aAccessible->IsActiveWidget()) {
FocusMgr()->ActiveItemChanged(activeDescendant, false);
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eFocus)) {
logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
activeDescendant);
}
#endif
}
return;
}
}
// aria-activedescendant was cleared or changed to a non-existent node.
// Move focus back to the element itself if it has DOM focus.
if (aAccessible->IsActiveWidget()) {
FocusMgr()->ActiveItemChanged(aAccessible, false);
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eFocus)) {
logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared",
aAccessible);
}
#endif
}
}
}
void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {
MaybeHandleChangeToHiddenNameOrDescription(aFirstNewContent);
}
void DocAccessible::ElementStateChanged(dom::Document* aDocument,
dom::Element* aElement,
dom::ElementState aStateMask) {
LocalAccessible* accessible =
aElement == mContent ? this : GetAccessible(aElement);
if (!accessible) {
return;
}
if (aStateMask.HasState(dom::ElementState::READWRITE) &&
!accessible->IsTextField()) {
const bool isEditable =
aElement->State().HasState(dom::ElementState::READWRITE);
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::EDITABLE, isEditable);
FireDelayedEvent(event);
if (accessible == this || aElement->IsHTMLElement(nsGkAtoms::article)) {
// We want <article> to behave like a document in terms of readonly state.
event =
new AccStateChangeEvent(accessible, states::READONLY, !isEditable);
FireDelayedEvent(event);
}
if (aElement->HasAttr(nsGkAtoms::aria_owns)) {
// If this has aria-owns, update children that are relocated into here.
// If we are becoming editable, put them back into their original
// containers, if we are becoming readonly, acquire them.
mNotificationController->ScheduleRelocation(accessible);
}
// If this is a node inside of a newly editable subtree, it needs to be
// un-aria-owned. And inversely, if the node becomes uneditable, allow the
// node to be aria-owned.
RelocateARIAOwnedIfNeeded(aElement);
}
if (aStateMask.HasState(dom::ElementState::CHECKED)) {
LocalAccessible* widget = accessible->ContainerWidget();
if (widget && widget->IsSelect()) {
// Changing selection here changes what we cache for
// the viewport.
SetViewportCacheDirty(true);
AccSelChangeEvent::SelChangeType selChangeType =
aElement->State().HasState(dom::ElementState::CHECKED)
? AccSelChangeEvent::eSelectionAdd
: AccSelChangeEvent::eSelectionRemove;
RefPtr<AccEvent> event =
new AccSelChangeEvent(widget, accessible, selChangeType);
FireDelayedEvent(event);
return;
}
RefPtr<AccEvent> event = new AccStateChangeEvent(
accessible, states::CHECKED,
aElement->State().HasState(dom::ElementState::CHECKED));
FireDelayedEvent(event);
}
if (aStateMask.HasState(dom::ElementState::INVALID)) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::INVALID);
FireDelayedEvent(event);
}
if (aStateMask.HasState(dom::ElementState::REQUIRED)) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::REQUIRED);
FireDelayedEvent(event);
}
if (aStateMask.HasState(dom::ElementState::VISITED)) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::TRAVERSED, true);
FireDelayedEvent(event);
}
// We only expose dom::ElementState::DEFAULT on buttons, but we can get
// notifications for other controls like checkboxes.
if (aStateMask.HasState(dom::ElementState::DEFAULT) &&
accessible->IsButton()) {
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::DEFAULT);
FireDelayedEvent(event);
}
if (aStateMask.HasState(dom::ElementState::INDETERMINATE)) {
RefPtr<AccEvent> event = new AccStateChangeEvent(accessible, states::MIXED);
FireDelayedEvent(event);
}
if (aStateMask.HasState(dom::ElementState::DISABLED) &&
!nsAccUtils::ARIAAttrValueIs(aElement, nsGkAtoms::aria_disabled,
nsGkAtoms::_true, eCaseMatters)) {
// The DOM disabled state has changed and there is no aria-disabled="true"
// taking precedence.
RefPtr<AccEvent> event =
new AccStateChangeEvent(accessible, states::UNAVAILABLE);
FireDelayedEvent(event);
event = new AccStateChangeEvent(accessible, states::ENABLED);
FireDelayedEvent(event);
// This likely changes focusability as well.
event = new AccStateChangeEvent(accessible, states::FOCUSABLE);
FireDelayedEvent(event);
}
}
void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
const CharacterDataChangeInfo&) {}
void DocAccessible::CharacterDataChanged(nsIContent* aContent,
const CharacterDataChangeInfo&) {}
void DocAccessible::ContentInserted(nsIContent* aChild) {
MaybeHandleChangeToHiddenNameOrDescription(aChild);
}
void DocAccessible::ContentWillBeRemoved(nsIContent* aChildNode,
const BatchRemovalState*) {
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eTree)) {
logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
logging::Node("container node", aChildNode->GetParent());
logging::Node("content node", aChildNode);
logging::MsgEnd();
}
#endif
ContentRemoved(aChildNode);
}
void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
////////////////////////////////////////////////////////////////////////////////
// LocalAccessible
#ifdef A11Y_LOG
nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
if (logging::IsEnabled(logging::eDocLoad)) {
logging::DocLoadEventHandled(aEvent);
}
return HyperTextAccessible::HandleAccEvent(aEvent);
}
#endif
////////////////////////////////////////////////////////////////////////////////
// Public members
nsPresContext* DocAccessible::PresContext() const {
return mPresShell->GetPresContext();
}
void* DocAccessible::GetNativeWindow() const {
if (!mPresShell) {
return nullptr;
}
nsViewManager* vm = mPresShell->GetViewManager();
if (!vm) return nullptr;
nsCOMPtr<nsIWidget> widget = vm->GetRootWidget();
if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
return nullptr;
}
LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(
void* aUniqueID) {
LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID);
if (child) return child;
uint32_t childDocCount = mChildDocuments.Length();
for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
if (child) return child;
}
return nullptr;
}
LocalAccessible* DocAccessible::GetAccessibleOrContainer(
nsINode* aNode, bool aNoContainerIfPruned) const {
if (!aNode || !aNode->GetComposedDoc()) {
return nullptr;
}
nsINode* start = aNode;
if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) {
// This can happen, for example, when called within
// SelectionManager::ProcessSelectionChanged due to focusing a direct
// child of a shadow root.
// GetFlattenedTreeParent works on children of a shadow root, but not the
// shadow root itself.
start = shadowRoot->GetHost();
if (!start) {
return nullptr;
}
}
for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) {
// No container if is inside of aria-hidden subtree.
if (aNoContainerIfPruned && currNode->IsElement() &&
aria::HasDefinedARIAHidden(currNode->AsElement())) {
return nullptr;
}
// Check if node is in zero-sized map
if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) {
if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
.IsEmpty()) {
return nullptr;
}
}
}
if (LocalAccessible* accessible = GetAccessible(currNode)) {
return accessible;
}
}
return nullptr;
}
LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const {
return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode())
: nullptr;
}
LocalAccessible* DocAccessible::GetAccessibleOrDescendant(
nsINode* aNode) const {
LocalAccessible* acc = GetAccessible(aNode);
if (acc) return acc;
if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) {
// If the node is the doc's body or root element, return the doc accessible.
return const_cast<DocAccessible*>(this);
}
acc = GetContainerAccessible(aNode);
if (acc) {
TreeWalker walker(acc, aNode->AsContent(),
TreeWalker::eWalkCache | TreeWalker::eScoped);
return walker.Next();
}
return nullptr;
}
void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
const nsRoleMapEntry* aRoleMapEntry) {
// Put into DOM node cache.
if (aAccessible->IsNodeMapEntry()) {
mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible);
}
// Put into unique ID cache.
mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible});
aAccessible->SetRoleMapEntry(aRoleMapEntry);
if (aAccessible->HasOwnContent()) {
AddDependentIDsFor(aAccessible);
AddDependentElementsFor(aAccessible);
nsIContent* content = aAccessible->GetContent();
if (content->IsElement() &&
nsAccUtils::HasARIAAttr(content->AsElement(), nsGkAtoms::aria_owns)) {
mNotificationController->ScheduleRelocation(aAccessible);
}
}
if (mIPCDoc) {
mInsertedAccessibles.EnsureInserted(aAccessible);
}
QueueCacheUpdateForDependentRelations(aAccessible);
}
void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) {
NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
"Unbinding the unbound accessible!");
// Fire focus event on accessible having DOM focus if last focus was removed
// from the tree.
if (FocusMgr()->WasLastFocused(aAccessible)) {
FocusMgr()->ActiveItemChanged(nullptr);
#ifdef A11Y_LOG
if (logging::IsEnabled(logging::eFocus)) {
logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
}
#endif
}
// Remove an accessible from node-to-accessible map if it exists there.
if (aAccessible->IsNodeMapEntry() &&
mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) {
mNodeToAccessibleMap.Remove(aAccessible->GetNode());
}
aAccessible->mStateFlags |= eIsNotInDocument;
// Update XPCOM part.
xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);