1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "LocalAccessible-inl.h"
8 #include "AccIterator.h"
9 #include "AccAttributes.h"
10 #include "DocAccessible-inl.h"
11 #include "DocAccessibleChild.h"
12 #include "HTMLImageMapAccessible.h"
13 #include "nsAccCache.h"
14 #include "nsAccessiblePivot.h"
15 #include "nsAccUtils.h"
16 #include "nsDeckFrame.h"
17 #include "nsEventShell.h"
18 #include "nsLayoutUtils.h"
19 #include "nsTextEquivUtils.h"
20 #include "Pivot.h"
21 #include "Role.h"
22 #include "RootAccessible.h"
23 #include "TreeWalker.h"
24 #include "xpcAccessibleDocument.h"
25
26 #include "nsCommandManager.h"
27 #include "nsContentUtils.h"
28 #include "nsIDocShell.h"
29 #include "mozilla/dom/Document.h"
30 #include "nsPIDOMWindow.h"
31 #include "nsIEditingSession.h"
32 #include "nsIFrame.h"
33 #include "nsIInterfaceRequestorUtils.h"
34 #include "nsImageFrame.h"
35 #include "nsViewManager.h"
36 #include "nsIScrollableFrame.h"
37 #include "nsUnicharUtils.h"
38 #include "nsIURI.h"
39 #include "nsIWebNavigation.h"
40 #include "nsFocusManager.h"
41 #include "nsTHashSet.h"
42 #include "mozilla/ArrayUtils.h"
43 #include "mozilla/Assertions.h"
44 #include "mozilla/EditorBase.h"
45 #include "mozilla/EventStates.h"
46 #include "mozilla/HTMLEditor.h"
47 #include "mozilla/PresShell.h"
48 #include "mozilla/StaticPrefs_accessibility.h"
49 #include "mozilla/dom/AncestorIterator.h"
50 #include "mozilla/dom/BrowserChild.h"
51 #include "mozilla/dom/DocumentType.h"
52 #include "mozilla/dom/Element.h"
53 #include "mozilla/dom/MutationEventBinding.h"
54 #include "mozilla/dom/UserActivation.h"
55 #include "HTMLElementAccessibles.h"
56
57 using namespace mozilla;
58 using namespace mozilla::a11y;
59
60 ////////////////////////////////////////////////////////////////////////////////
61 // Static member initialization
62
63 static nsStaticAtom* const kRelationAttrs[] = {nsGkAtoms::aria_labelledby,
64 nsGkAtoms::aria_describedby,
65 nsGkAtoms::aria_details,
66 nsGkAtoms::aria_owns,
67 nsGkAtoms::aria_controls,
68 nsGkAtoms::aria_flowto,
69 nsGkAtoms::aria_errormessage,
70 nsGkAtoms::_for,
71 nsGkAtoms::control};
72
73 static const uint32_t kRelationAttrsLen = ArrayLength(kRelationAttrs);
74
75 ////////////////////////////////////////////////////////////////////////////////
76 // Constructor/desctructor
77
DocAccessible(dom::Document * aDocument,PresShell * aPresShell)78 DocAccessible::DocAccessible(dom::Document* aDocument,
79 PresShell* aPresShell)
80 : // XXX don't pass a document to the LocalAccessible constructor so that
81 // we don't set mDoc until our vtable is fully setup. If we set mDoc
82 // before setting up the vtable we will call LocalAccessible::AddRef()
83 // but not the overrides of it for subclasses. It is important to call
84 // those overrides to avoid confusing leak checking machinary.
85 HyperTextAccessibleWrap(nullptr, nullptr),
86 // XXX aaronl should we use an algorithm for the initial cache size?
87 mAccessibleCache(kDefaultCacheLength),
88 mNodeToAccessibleMap(kDefaultCacheLength),
89 mDocumentNode(aDocument),
90 mLoadState(eTreeConstructionPending),
91 mDocFlags(0),
92 mLoadEventType(0),
93 mARIAAttrOldValue{nullptr},
94 mVirtualCursor(nullptr),
95 mPresShell(aPresShell),
96 mIPCDoc(nullptr) {
97 mGenericTypes |= eDocument;
98 mStateFlags |= eNotNodeMapEntry;
99 mDoc = this;
100
101 MOZ_ASSERT(mPresShell, "should have been given a pres shell");
102 mPresShell->SetDocAccessible(this);
103 }
104
~DocAccessible()105 DocAccessible::~DocAccessible() {
106 NS_ASSERTION(!mPresShell, "LastRelease was never called!?!");
107 }
108
109 ////////////////////////////////////////////////////////////////////////////////
110 // nsISupports
111
112 NS_IMPL_CYCLE_COLLECTION_CLASS(DocAccessible)
113
114 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DocAccessible,
115 LocalAccessible)
116 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNotificationController)
117 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mVirtualCursor)
118 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChildDocuments)
119 for (const auto& hashEntry : tmp->mDependentIDsHashes.Values()) {
120 for (const auto& providers : hashEntry->Values()) {
121 for (int32_t provIdx = providers->Length() - 1; provIdx >= 0; provIdx--) {
122 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(
123 cb, "content of dependent ids hash entry of document accessible");
124
125 const auto& provider = (*providers)[provIdx];
126 cb.NoteXPCOMChild(provider->mContent);
127 }
128 }
129 }
130 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAccessibleCache)
131 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorJumpElm)
132 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInvalidationList)
133 for (const auto& ar : tmp->mARIAOwnsHash.Values()) {
134 for (uint32_t i = 0; i < ar->Length(); i++) {
135 NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "mARIAOwnsHash entry item");
136 cb.NoteXPCOMChild(ar->ElementAt(i));
137 }
138 }
139 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
140
141 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DocAccessible, LocalAccessible)
142 NS_IMPL_CYCLE_COLLECTION_UNLINK(mNotificationController)
143 NS_IMPL_CYCLE_COLLECTION_UNLINK(mVirtualCursor)
144 NS_IMPL_CYCLE_COLLECTION_UNLINK(mChildDocuments)
145 tmp->mDependentIDsHashes.Clear();
146 tmp->mNodeToAccessibleMap.Clear();
147 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAccessibleCache)
148 NS_IMPL_CYCLE_COLLECTION_UNLINK(mAnchorJumpElm)
149 NS_IMPL_CYCLE_COLLECTION_UNLINK(mInvalidationList)
150 NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
151 tmp->mARIAOwnsHash.Clear();
152 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
153
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)154 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DocAccessible)
155 NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
156 NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
157 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
158 NS_INTERFACE_MAP_ENTRY(nsIObserver)
159 NS_INTERFACE_MAP_ENTRY(nsIAccessiblePivotObserver)
160 NS_INTERFACE_MAP_END_INHERITING(HyperTextAccessible)
161
162 NS_IMPL_ADDREF_INHERITED(DocAccessible, HyperTextAccessible)
163 NS_IMPL_RELEASE_INHERITED(DocAccessible, HyperTextAccessible)
164
165 ////////////////////////////////////////////////////////////////////////////////
166 // nsIAccessible
167
168 ENameValueFlag DocAccessible::Name(nsString& aName) const {
169 aName.Truncate();
170
171 if (mParent) {
172 mParent->Name(aName); // Allow owning iframe to override the name
173 }
174 if (aName.IsEmpty()) {
175 // Allow name via aria-labelledby or title attribute
176 LocalAccessible::Name(aName);
177 }
178 if (aName.IsEmpty()) {
179 Title(aName); // Try title element
180 }
181 if (aName.IsEmpty()) { // Last resort: use URL
182 URL(aName);
183 }
184
185 return eNameOK;
186 }
187
188 // LocalAccessible public method
NativeRole() const189 role DocAccessible::NativeRole() const {
190 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mDocumentNode);
191 if (docShell) {
192 nsCOMPtr<nsIDocShellTreeItem> sameTypeRoot;
193 docShell->GetInProcessSameTypeRootTreeItem(getter_AddRefs(sameTypeRoot));
194 int32_t itemType = docShell->ItemType();
195 if (sameTypeRoot == docShell) {
196 // Root of content or chrome tree
197 if (itemType == nsIDocShellTreeItem::typeChrome) {
198 return roles::CHROME_WINDOW;
199 }
200
201 if (itemType == nsIDocShellTreeItem::typeContent) {
202 return roles::DOCUMENT;
203 }
204 } else if (itemType == nsIDocShellTreeItem::typeContent) {
205 return roles::DOCUMENT;
206 }
207 }
208
209 return roles::PANE; // Fall back;
210 }
211
Description(nsString & aDescription)212 void DocAccessible::Description(nsString& aDescription) {
213 if (mParent) mParent->Description(aDescription);
214
215 if (HasOwnContent() && aDescription.IsEmpty()) {
216 nsTextEquivUtils::GetTextEquivFromIDRefs(this, nsGkAtoms::aria_describedby,
217 aDescription);
218 }
219 }
220
221 // LocalAccessible public method
NativeState() const222 uint64_t DocAccessible::NativeState() const {
223 // Document is always focusable.
224 uint64_t state =
225 states::FOCUSABLE; // keep in sync with NativeInteractiveState() impl
226 if (FocusMgr()->IsFocused(this)) state |= states::FOCUSED;
227
228 // Expose stale state until the document is ready (DOM is loaded and tree is
229 // constructed).
230 if (!HasLoadState(eReady)) state |= states::STALE;
231
232 // Expose state busy until the document and all its subdocuments is completely
233 // loaded.
234 if (!HasLoadState(eCompletelyLoaded)) state |= states::BUSY;
235
236 nsIFrame* frame = GetFrame();
237 if (!frame || !frame->IsVisibleConsideringAncestors(
238 nsIFrame::VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY)) {
239 state |= states::INVISIBLE | states::OFFSCREEN;
240 }
241
242 RefPtr<EditorBase> editorBase = GetEditor();
243 state |= editorBase ? states::EDITABLE : states::READONLY;
244
245 return state;
246 }
247
NativeInteractiveState() const248 uint64_t DocAccessible::NativeInteractiveState() const {
249 // Document is always focusable.
250 return states::FOCUSABLE;
251 }
252
NativelyUnavailable() const253 bool DocAccessible::NativelyUnavailable() const { return false; }
254
255 // LocalAccessible public method
ApplyARIAState(uint64_t * aState) const256 void DocAccessible::ApplyARIAState(uint64_t* aState) const {
257 // Grab states from content element.
258 if (mContent) LocalAccessible::ApplyARIAState(aState);
259
260 // Allow iframe/frame etc. to have final state override via ARIA.
261 if (mParent) mParent->ApplyARIAState(aState);
262 }
263
Attributes()264 already_AddRefed<AccAttributes> DocAccessible::Attributes() {
265 RefPtr<AccAttributes> attributes = HyperTextAccessibleWrap::Attributes();
266
267 // No attributes if document is not attached to the tree or if it's a root
268 // document.
269 if (!mParent || IsRoot()) return attributes.forget();
270
271 // Override ARIA object attributes from outerdoc.
272 aria::AttrIterator attribIter(mParent->GetContent());
273 while (attribIter.Next()) {
274 nsAutoString value;
275 attribIter.AttrValue(value);
276 attributes->SetAttribute(attribIter.AttrName(), value);
277 }
278
279 return attributes.forget();
280 }
281
FocusedChild()282 LocalAccessible* DocAccessible::FocusedChild() {
283 // Return an accessible for the current global focus, which does not have to
284 // be contained within the current document.
285 return FocusMgr()->FocusedAccessible();
286 }
287
TakeFocus() const288 void DocAccessible::TakeFocus() const {
289 // Focus the document.
290 nsFocusManager* fm = nsFocusManager::GetFocusManager();
291 RefPtr<dom::Element> newFocus;
292 dom::AutoHandlingUserInputStatePusher inputStatePusher(true);
293 fm->MoveFocus(mDocumentNode->GetWindow(), nullptr,
294 nsFocusManager::MOVEFOCUS_ROOT, 0, getter_AddRefs(newFocus));
295 }
296
297 // HyperTextAccessible method
GetEditor() const298 already_AddRefed<EditorBase> DocAccessible::GetEditor() const {
299 // Check if document is editable (designMode="on" case). Otherwise check if
300 // the html:body (for HTML document case) or document element is editable.
301 if (!mDocumentNode->HasFlag(NODE_IS_EDITABLE) &&
302 (!mContent || !mContent->HasFlag(NODE_IS_EDITABLE))) {
303 return nullptr;
304 }
305
306 nsCOMPtr<nsIDocShell> docShell = mDocumentNode->GetDocShell();
307 if (!docShell) {
308 return nullptr;
309 }
310
311 nsCOMPtr<nsIEditingSession> editingSession;
312 docShell->GetEditingSession(getter_AddRefs(editingSession));
313 if (!editingSession) return nullptr; // No editing session interface
314
315 RefPtr<HTMLEditor> htmlEditor =
316 editingSession->GetHTMLEditorForWindow(mDocumentNode->GetWindow());
317 if (!htmlEditor) {
318 return nullptr;
319 }
320
321 bool isEditable = false;
322 htmlEditor->GetIsDocumentEditable(&isEditable);
323 if (isEditable) {
324 return htmlEditor.forget();
325 }
326
327 return nullptr;
328 }
329
330 // DocAccessible public method
331
URL(nsAString & aURL) const332 void DocAccessible::URL(nsAString& aURL) const {
333 nsCOMPtr<nsISupports> container = mDocumentNode->GetContainer();
334 nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(container));
335 nsAutoCString theURL;
336 if (webNav) {
337 nsCOMPtr<nsIURI> pURI;
338 webNav->GetCurrentURI(getter_AddRefs(pURI));
339 if (pURI) pURI->GetSpec(theURL);
340 }
341 CopyUTF8toUTF16(theURL, aURL);
342 }
343
Title(nsString & aTitle) const344 void DocAccessible::Title(nsString& aTitle) const {
345 mDocumentNode->GetTitle(aTitle);
346 }
347
MimeType(nsAString & aType) const348 void DocAccessible::MimeType(nsAString& aType) const {
349 mDocumentNode->GetContentType(aType);
350 }
351
DocType(nsAString & aType) const352 void DocAccessible::DocType(nsAString& aType) const {
353 dom::DocumentType* docType = mDocumentNode->GetDoctype();
354 if (docType) docType->GetPublicId(aType);
355 }
356
357 ////////////////////////////////////////////////////////////////////////////////
358 // LocalAccessible
359
Init()360 void DocAccessible::Init() {
361 #ifdef A11Y_LOG
362 if (logging::IsEnabled(logging::eDocCreate)) {
363 logging::DocCreate("document initialize", mDocumentNode, this);
364 }
365 #endif
366
367 // Initialize notification controller.
368 mNotificationController = new NotificationController(this, mPresShell);
369
370 // Mark the document accessible as loaded if its DOM document was loaded at
371 // this point (this can happen because a11y is started late or DOM document
372 // having no container was loaded.
373 if (mDocumentNode->GetReadyStateEnum() ==
374 dom::Document::READYSTATE_COMPLETE) {
375 mLoadState |= eDOMLoaded;
376 }
377
378 AddEventListeners();
379 }
380
Shutdown()381 void DocAccessible::Shutdown() {
382 if (!mPresShell) { // already shutdown
383 return;
384 }
385
386 #ifdef A11Y_LOG
387 if (logging::IsEnabled(logging::eDocDestroy)) {
388 logging::DocDestroy("document shutdown", mDocumentNode, this);
389 }
390 #endif
391
392 // Mark the document as shutdown before AT is notified about the document
393 // removal from its container (valid for root documents on ATK and due to
394 // some reason for MSAA, refer to bug 757392 for details).
395 mStateFlags |= eIsDefunct;
396
397 if (mNotificationController) {
398 mNotificationController->Shutdown();
399 mNotificationController = nullptr;
400 }
401
402 RemoveEventListeners();
403
404 // mParent->RemoveChild clears mParent, but we need to know whether we were a
405 // child later, so use a flag.
406 const bool isChild = !!mParent;
407 if (mParent) {
408 DocAccessible* parentDocument = mParent->Document();
409 if (parentDocument) parentDocument->RemoveChildDocument(this);
410
411 mParent->RemoveChild(this);
412 MOZ_ASSERT(!mParent, "Parent has to be null!");
413 }
414
415 mPresShell->SetDocAccessible(nullptr);
416 mPresShell = nullptr; // Avoid reentrancy
417
418 // Walk the array backwards because child documents remove themselves from the
419 // array as they are shutdown.
420 int32_t childDocCount = mChildDocuments.Length();
421 for (int32_t idx = childDocCount - 1; idx >= 0; idx--) {
422 mChildDocuments[idx]->Shutdown();
423 }
424
425 mChildDocuments.Clear();
426
427 // XXX thinking about ordering?
428 if (mIPCDoc) {
429 MOZ_ASSERT(IPCAccessibilityActive());
430 mIPCDoc->Shutdown();
431 MOZ_ASSERT(!mIPCDoc);
432 }
433
434 if (mVirtualCursor) {
435 mVirtualCursor->RemoveObserver(this);
436 mVirtualCursor = nullptr;
437 }
438
439 mDependentIDsHashes.Clear();
440 mNodeToAccessibleMap.Clear();
441
442 mAnchorJumpElm = nullptr;
443 mInvalidationList.Clear();
444
445 for (auto iter = mAccessibleCache.Iter(); !iter.Done(); iter.Next()) {
446 LocalAccessible* accessible = iter.Data();
447 MOZ_ASSERT(accessible);
448 if (accessible && !accessible->IsDefunct()) {
449 // Unlink parent to avoid its cleaning overhead in shutdown.
450 accessible->mParent = nullptr;
451 accessible->Shutdown();
452 }
453 iter.Remove();
454 }
455
456 HyperTextAccessibleWrap::Shutdown();
457
458 MOZ_ASSERT(GetAccService());
459 GetAccService()->NotifyOfDocumentShutdown(
460 this, mDocumentNode,
461 // Make sure we don't shut down AccService while a parent document is
462 // still shutting down. The parent will allow service shutdown when it
463 // reaches this point.
464 /* aAllowServiceShutdown */ !isChild);
465 mDocumentNode = nullptr;
466 }
467
GetFrame() const468 nsIFrame* DocAccessible::GetFrame() const {
469 nsIFrame* root = nullptr;
470 if (mPresShell) {
471 root = mPresShell->GetRootFrame();
472 }
473
474 return root;
475 }
476
GetNode() const477 nsINode* DocAccessible::GetNode() const { return mDocumentNode; }
478
479 // DocAccessible protected member
RelativeBounds(nsIFrame ** aRelativeFrame) const480 nsRect DocAccessible::RelativeBounds(nsIFrame** aRelativeFrame) const {
481 *aRelativeFrame = GetFrame();
482
483 dom::Document* document = mDocumentNode;
484 dom::Document* parentDoc = nullptr;
485
486 nsRect bounds;
487 while (document) {
488 PresShell* presShell = document->GetPresShell();
489 if (!presShell) {
490 return nsRect();
491 }
492
493 nsRect scrollPort;
494 nsIScrollableFrame* sf = presShell->GetRootScrollFrameAsScrollable();
495 if (sf) {
496 scrollPort = sf->GetScrollPortRect();
497 } else {
498 nsIFrame* rootFrame = presShell->GetRootFrame();
499 if (!rootFrame) return nsRect();
500
501 scrollPort = rootFrame->GetRect();
502 }
503
504 if (parentDoc) { // After first time thru loop
505 // XXXroc bogus code! scrollPort is relative to the viewport of
506 // this document, but we're intersecting rectangles derived from
507 // multiple documents and assuming they're all in the same coordinate
508 // system. See bug 514117.
509 bounds.IntersectRect(scrollPort, bounds);
510 } else { // First time through loop
511 bounds = scrollPort;
512 }
513
514 document = parentDoc = document->GetInProcessParentDocument();
515 }
516
517 return bounds;
518 }
519
520 // DocAccessible protected member
AddEventListeners()521 nsresult DocAccessible::AddEventListeners() {
522 nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell());
523
524 // We want to add a command observer only if the document is content and has
525 // an editor.
526 if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) {
527 RefPtr<nsCommandManager> commandManager = docShell->GetCommandManager();
528 if (commandManager) {
529 commandManager->AddCommandObserver(this, "obs_documentCreated");
530 }
531 }
532
533 SelectionMgr()->AddDocSelectionListener(mPresShell);
534
535 // Add document observer.
536 mDocumentNode->AddObserver(this);
537 return NS_OK;
538 }
539
540 // DocAccessible protected member
RemoveEventListeners()541 nsresult DocAccessible::RemoveEventListeners() {
542 // Remove listeners associated with content documents
543 NS_ASSERTION(mDocumentNode, "No document during removal of listeners.");
544
545 if (mDocumentNode) {
546 mDocumentNode->RemoveObserver(this);
547
548 nsCOMPtr<nsIDocShell> docShell(mDocumentNode->GetDocShell());
549 NS_ASSERTION(docShell, "doc should support nsIDocShellTreeItem.");
550
551 if (docShell) {
552 if (docShell->ItemType() == nsIDocShellTreeItem::typeContent) {
553 RefPtr<nsCommandManager> commandManager = docShell->GetCommandManager();
554 if (commandManager) {
555 commandManager->RemoveCommandObserver(this, "obs_documentCreated");
556 }
557 }
558 }
559 }
560
561 if (mScrollWatchTimer) {
562 mScrollWatchTimer->Cancel();
563 mScrollWatchTimer = nullptr;
564 NS_RELEASE_THIS(); // Kung fu death grip
565 }
566
567 SelectionMgr()->RemoveDocSelectionListener(mPresShell);
568 return NS_OK;
569 }
570
ScrollTimerCallback(nsITimer * aTimer,void * aClosure)571 void DocAccessible::ScrollTimerCallback(nsITimer* aTimer, void* aClosure) {
572 DocAccessible* docAcc = reinterpret_cast<DocAccessible*>(aClosure);
573
574 if (docAcc) {
575 // Dispatch a scroll-end for all entries in table. They have not
576 // been scrolled in at least `kScrollEventInterval`.
577 for (auto iter = docAcc->mLastScrollingDispatch.Iter(); !iter.Done();
578 iter.Next()) {
579 docAcc->DispatchScrollingEvent(iter.Key(),
580 nsIAccessibleEvent::EVENT_SCROLLING_END);
581 iter.Remove();
582 }
583
584 if (docAcc->mScrollWatchTimer) {
585 docAcc->mScrollWatchTimer = nullptr;
586 NS_RELEASE(docAcc); // Release kung fu death grip
587 }
588 }
589 }
590
HandleScroll(nsINode * aTarget)591 void DocAccessible::HandleScroll(nsINode* aTarget) {
592 const uint32_t kScrollEventInterval = 100;
593 // If we haven't dispatched a scrolling event for a target in at least
594 // kScrollEventInterval milliseconds, dispatch one now.
595 mLastScrollingDispatch.WithEntryHandle(aTarget, [&](auto&& lastDispatch) {
596 const TimeStamp now = TimeStamp::Now();
597
598 if (!lastDispatch ||
599 (now - lastDispatch.Data()).ToMilliseconds() >= kScrollEventInterval) {
600 // We can't fire events on a document whose tree isn't constructed yet.
601 if (HasLoadState(eTreeConstructed)) {
602 DispatchScrollingEvent(aTarget, nsIAccessibleEvent::EVENT_SCROLLING);
603 }
604 lastDispatch.InsertOrUpdate(now);
605 }
606 });
607
608 // If timer callback is still pending, push it 100ms into the future.
609 // When scrolling ends and we don't fire this callback anymore, the
610 // timer callback will fire and dispatch an EVENT_SCROLLING_END.
611 if (mScrollWatchTimer) {
612 mScrollWatchTimer->SetDelay(kScrollEventInterval);
613 } else {
614 NS_NewTimerWithFuncCallback(getter_AddRefs(mScrollWatchTimer),
615 ScrollTimerCallback, this, kScrollEventInterval,
616 nsITimer::TYPE_ONE_SHOT,
617 "a11y::DocAccessible::ScrollPositionDidChange");
618 if (mScrollWatchTimer) {
619 NS_ADDREF_THIS(); // Kung fu death grip
620 }
621 }
622 }
623
624 ////////////////////////////////////////////////////////////////////////////////
625 // nsIObserver
626
627 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)628 DocAccessible::Observe(nsISupports* aSubject, const char* aTopic,
629 const char16_t* aData) {
630 if (!nsCRT::strcmp(aTopic, "obs_documentCreated")) {
631 // State editable will now be set, readonly is now clear
632 // Normally we only fire delayed events created from the node, not an
633 // accessible object. See the AccStateChangeEvent constructor for details
634 // about this exceptional case.
635 RefPtr<AccEvent> event =
636 new AccStateChangeEvent(this, states::EDITABLE, true);
637 FireDelayedEvent(event);
638 }
639
640 return NS_OK;
641 }
642
643 ////////////////////////////////////////////////////////////////////////////////
644 // nsIAccessiblePivotObserver
645
646 NS_IMETHODIMP
OnPivotChanged(nsIAccessiblePivot * aPivot,nsIAccessible * aOldAccessible,int32_t aOldStart,int32_t aOldEnd,nsIAccessible * aNewAccessible,int32_t aNewStart,int32_t aNewEnd,PivotMoveReason aReason,TextBoundaryType aBoundaryType,bool aIsFromUserInput)647 DocAccessible::OnPivotChanged(nsIAccessiblePivot* aPivot,
648 nsIAccessible* aOldAccessible, int32_t aOldStart,
649 int32_t aOldEnd, nsIAccessible* aNewAccessible,
650 int32_t aNewStart, int32_t aNewEnd,
651 PivotMoveReason aReason,
652 TextBoundaryType aBoundaryType,
653 bool aIsFromUserInput) {
654 RefPtr<AccEvent> event = new AccVCChangeEvent(
655 this, (aOldAccessible ? aOldAccessible->ToInternalAccessible() : nullptr),
656 aOldStart, aOldEnd,
657 (aNewAccessible ? aNewAccessible->ToInternalAccessible() : nullptr),
658 aNewStart, aNewEnd, aReason, aBoundaryType,
659 aIsFromUserInput ? eFromUserInput : eNoUserInput);
660 nsEventShell::FireEvent(event);
661
662 return NS_OK;
663 }
664
665 ////////////////////////////////////////////////////////////////////////////////
666 // nsIDocumentObserver
667
668 NS_IMPL_NSIDOCUMENTOBSERVER_CORE_STUB(DocAccessible)
NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)669 NS_IMPL_NSIDOCUMENTOBSERVER_LOAD_STUB(DocAccessible)
670
671 void DocAccessible::AttributeWillChange(dom::Element* aElement,
672 int32_t aNameSpaceID,
673 nsAtom* aAttribute, int32_t aModType) {
674 LocalAccessible* accessible = GetAccessible(aElement);
675 if (!accessible) {
676 if (aElement != mContent) return;
677
678 accessible = this;
679 }
680
681 // Update dependent IDs cache. Take care of elements that are accessible
682 // because dependent IDs cache doesn't contain IDs from non accessible
683 // elements.
684 if (aModType != dom::MutationEvent_Binding::ADDITION) {
685 RemoveDependentIDsFor(accessible, aAttribute);
686 }
687
688 if (aAttribute == nsGkAtoms::id) {
689 RelocateARIAOwnedIfNeeded(aElement);
690 }
691
692 // Store the ARIA attribute old value so that it can be used after
693 // attribute change. Note, we assume there's no nested ARIA attribute
694 // changes. If this happens then we should end up with keeping a stack of
695 // old values.
696
697 // XXX TODO: bugs 472142, 472143.
698 // Here we will want to cache whatever attribute values we are interested
699 // in, such as the existence of aria-pressed for button (so we know if we
700 // need to newly expose it as a toggle button) etc.
701 if (aAttribute == nsGkAtoms::aria_checked ||
702 aAttribute == nsGkAtoms::aria_pressed) {
703 mARIAAttrOldValue = (aModType != dom::MutationEvent_Binding::ADDITION)
704 ? nsAccUtils::GetARIAToken(aElement, aAttribute)
705 : nullptr;
706 return;
707 }
708
709 if (aAttribute == nsGkAtoms::aria_disabled || aAttribute == nsGkAtoms::href ||
710 aAttribute == nsGkAtoms::disabled || aAttribute == nsGkAtoms::tabindex ||
711 aAttribute == nsGkAtoms::contenteditable) {
712 mPrevStateBits = accessible->State();
713 }
714 }
715
NativeAnonymousChildListChange(nsIContent * aContent,bool aIsRemove)716 void DocAccessible::NativeAnonymousChildListChange(nsIContent* aContent,
717 bool aIsRemove) {
718 if (aIsRemove) {
719 #ifdef A11Y_LOG
720 if (logging::IsEnabled(logging::eTree)) {
721 logging::MsgBegin("TREE", "Anonymous content removed; doc: %p", this);
722 logging::Node("node", aContent);
723 logging::MsgEnd();
724 }
725 #endif
726
727 ContentRemoved(aContent);
728 }
729 }
730
AttributeChanged(dom::Element * aElement,int32_t aNameSpaceID,nsAtom * aAttribute,int32_t aModType,const nsAttrValue * aOldValue)731 void DocAccessible::AttributeChanged(dom::Element* aElement,
732 int32_t aNameSpaceID, nsAtom* aAttribute,
733 int32_t aModType,
734 const nsAttrValue* aOldValue) {
735 NS_ASSERTION(!IsDefunct(),
736 "Attribute changed called on defunct document accessible!");
737
738 // Proceed even if the element is not accessible because element may become
739 // accessible if it gets certain attribute.
740 if (UpdateAccessibleOnAttrChange(aElement, aAttribute)) return;
741
742 // Update the accessible tree on aria-hidden change. Make sure to not create
743 // a tree under aria-hidden='true'.
744 if (aAttribute == nsGkAtoms::aria_hidden) {
745 if (aria::HasDefinedARIAHidden(aElement)) {
746 ContentRemoved(aElement);
747 } else {
748 ContentInserted(aElement, aElement->GetNextSibling());
749 }
750 return;
751 }
752
753 // Ignore attribute change if the element doesn't have an accessible (at all
754 // or still) iff the element is not a root content of this document accessible
755 // (which is treated as attribute change on this document accessible).
756 // Note: we don't bail if all the content hasn't finished loading because
757 // these attributes are changing for a loaded part of the content.
758 LocalAccessible* accessible = GetAccessible(aElement);
759 if (!accessible) {
760 if (mContent != aElement) return;
761
762 accessible = this;
763 }
764
765 MOZ_ASSERT(accessible->IsBoundToParent() || accessible->IsDoc(),
766 "DOM attribute change on an accessible detached from the tree");
767
768 // Fire accessible events iff there's an accessible, otherwise we consider
769 // the accessible state wasn't changed, i.e. its state is initial state.
770 AttributeChangedImpl(accessible, aNameSpaceID, aAttribute, aModType);
771
772 // Update dependent IDs cache. Take care of accessible elements because no
773 // accessible element means either the element is not accessible at all or
774 // its accessible will be created later. It doesn't make sense to keep
775 // dependent IDs for non accessible elements. For the second case we'll update
776 // dependent IDs cache when its accessible is created.
777 if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
778 aModType == dom::MutationEvent_Binding::ADDITION) {
779 AddDependentIDsFor(accessible, aAttribute);
780 }
781 }
782
783 // DocAccessible protected member
AttributeChangedImpl(LocalAccessible * aAccessible,int32_t aNameSpaceID,nsAtom * aAttribute,int32_t aModType)784 void DocAccessible::AttributeChangedImpl(LocalAccessible* aAccessible,
785 int32_t aNameSpaceID,
786 nsAtom* aAttribute, int32_t aModType) {
787 // Fire accessible event after short timer, because we need to wait for
788 // DOM attribute & resulting layout to actually change. Otherwise,
789 // assistive technology will retrieve the wrong state/value/selection info.
790
791 // XXX todo
792 // We still need to handle special HTML cases here
793 // For example, if an <img>'s usemap attribute is modified
794 // Otherwise it may just be a state change, for example an object changing
795 // its visibility
796 //
797 // XXX todo: report aria state changes for "undefined" literal value changes
798 // filed as bug 472142
799 //
800 // XXX todo: invalidate accessible when aria state changes affect exposed
801 // role filed as bug 472143
802
803 // Universal boolean properties that don't require a role. Fire the state
804 // change when disabled or aria-disabled attribute is set.
805 // Note. Checking the XUL or HTML namespace would not seem to gain us
806 // anything, because disabled attribute really is going to mean the same
807 // thing in any namespace.
808 // Note. We use the attribute instead of the disabled state bit because
809 // ARIA's aria-disabled does not affect the disabled state bit.
810 if (aAttribute == nsGkAtoms::disabled ||
811 aAttribute == nsGkAtoms::aria_disabled) {
812 // disabled can affect focusable state
813 aAccessible->MaybeFireFocusableStateChange(
814 (mPrevStateBits & states::FOCUSABLE) != 0);
815
816 // Do nothing if state wasn't changed (like @aria-disabled was removed but
817 // @disabled is still presented).
818 uint64_t unavailableState = (aAccessible->State() & states::UNAVAILABLE);
819 if ((mPrevStateBits & states::UNAVAILABLE) == unavailableState) {
820 return;
821 }
822
823 RefPtr<AccEvent> enabledChangeEvent = new AccStateChangeEvent(
824 aAccessible, states::ENABLED, !unavailableState);
825 FireDelayedEvent(enabledChangeEvent);
826
827 RefPtr<AccEvent> sensitiveChangeEvent = new AccStateChangeEvent(
828 aAccessible, states::SENSITIVE, !unavailableState);
829 FireDelayedEvent(sensitiveChangeEvent);
830
831 return;
832 }
833
834 if (aAttribute == nsGkAtoms::tabindex) {
835 // Fire a focusable state change event if the previous state was different.
836 // It may be the same if tabindex is on a redundantly focusable element.
837 aAccessible->MaybeFireFocusableStateChange(
838 (mPrevStateBits & states::FOCUSABLE));
839 return;
840 }
841
842 // When a details object has its open attribute changed
843 // we should fire a state-change event on the accessible of
844 // its main summary
845 if (aAttribute == nsGkAtoms::open) {
846 // FromDetails checks if the given accessible belongs to
847 // a details frame and also locates the accessible of its
848 // main summary.
849 if (HTMLSummaryAccessible* summaryAccessible =
850 HTMLSummaryAccessible::FromDetails(aAccessible)) {
851 RefPtr<AccEvent> expandedChangeEvent =
852 new AccStateChangeEvent(summaryAccessible, states::EXPANDED);
853 FireDelayedEvent(expandedChangeEvent);
854 return;
855 }
856 }
857
858 // Check for namespaced ARIA attribute
859 if (aNameSpaceID == kNameSpaceID_None) {
860 // Check for hyphenated aria-foo property?
861 if (StringBeginsWith(nsDependentAtomString(aAttribute), u"aria-"_ns)) {
862 ARIAAttributeChanged(aAccessible, aAttribute);
863 }
864 }
865
866 // Fire name change and description change events. XXX: it's not complete and
867 // dupes the code logic of accessible name and description calculation, we do
868 // that for performance reasons.
869 if (aAttribute == nsGkAtoms::aria_label) {
870 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
871 return;
872 }
873
874 dom::Element* elm = aAccessible->GetContent()->AsElement();
875 if (aAttribute == nsGkAtoms::aria_describedby) {
876 FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE, aAccessible);
877 if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
878 aModType == dom::MutationEvent_Binding::ADDITION) {
879 // The subtrees of the new aria-describedby targets might be used to
880 // compute the description for aAccessible. Therefore, we need to set
881 // the eHasDescriptionDependent flag on all Accessibles in these subtrees.
882 IDRefsIterator iter(this, aAccessible->Elm(),
883 nsGkAtoms::aria_describedby);
884 while (LocalAccessible* target = iter.Next()) {
885 Pivot pivot(target);
886 LocalAccInSameDocRule rule;
887 for (AccessibleOrProxy anchor(target); !anchor.IsNull();
888 anchor = pivot.Next(anchor, rule)) {
889 LocalAccessible* acc = anchor.AsAccessible();
890 MOZ_ASSERT(acc);
891 acc->mContextFlags |= eHasDescriptionDependent;
892 }
893 }
894 }
895 return;
896 }
897
898 if (aAttribute == nsGkAtoms::aria_labelledby &&
899 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label)) {
900 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
901 if (aModType == dom::MutationEvent_Binding::MODIFICATION ||
902 aModType == dom::MutationEvent_Binding::ADDITION) {
903 // The subtrees of the new aria-labelledby targets might be used to
904 // compute the name for aAccessible. Therefore, we need to set
905 // the eHasNameDependent flag on all Accessibles in these subtrees.
906 IDRefsIterator iter(this, aAccessible->Elm(), nsGkAtoms::aria_labelledby);
907 while (LocalAccessible* target = iter.Next()) {
908 Pivot pivot(target);
909 LocalAccInSameDocRule rule;
910 for (AccessibleOrProxy anchor(target); !anchor.IsNull();
911 anchor = pivot.Next(anchor, rule)) {
912 LocalAccessible* acc = anchor.AsAccessible();
913 MOZ_ASSERT(acc);
914 acc->mContextFlags |= eHasNameDependent;
915 }
916 }
917 }
918 return;
919 }
920
921 if (aAttribute == nsGkAtoms::alt &&
922 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) &&
923 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby)) {
924 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
925 return;
926 }
927
928 if (aAttribute == nsGkAtoms::title) {
929 if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_label) &&
930 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_labelledby) &&
931 !elm->HasAttr(kNameSpaceID_None, nsGkAtoms::alt)) {
932 FireDelayedEvent(nsIAccessibleEvent::EVENT_NAME_CHANGE, aAccessible);
933 return;
934 }
935
936 if (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_describedby)) {
937 FireDelayedEvent(nsIAccessibleEvent::EVENT_DESCRIPTION_CHANGE,
938 aAccessible);
939 }
940
941 return;
942 }
943
944 // These attributes can change whether or not a table is a layout table.
945 // We currently cache that information on Mac, so we fire a
946 // EVENT_OBJECT_ATTRIBUTE_CHANGED, which Mac listens for, to invalidate.
947 if (aAccessible->IsTable() || aAccessible->IsTableRow() ||
948 aAccessible->IsTableCell()) {
949 if (aAttribute == nsGkAtoms::summary || aAttribute == nsGkAtoms::headers ||
950 aAttribute == nsGkAtoms::scope || aAttribute == nsGkAtoms::abbr) {
951 FireDelayedEvent(nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED,
952 aAccessible);
953 }
954 }
955
956 if (aAttribute == nsGkAtoms::aria_busy) {
957 bool isOn = elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true,
958 eCaseMatters);
959 RefPtr<AccEvent> event =
960 new AccStateChangeEvent(aAccessible, states::BUSY, isOn);
961 FireDelayedEvent(event);
962 return;
963 }
964
965 if (aAttribute == nsGkAtoms::aria_multiline) {
966 bool isOn = elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true,
967 eCaseMatters);
968 RefPtr<AccEvent> event =
969 new AccStateChangeEvent(aAccessible, states::MULTI_LINE, isOn);
970 FireDelayedEvent(event);
971 return;
972 }
973
974 if (aAttribute == nsGkAtoms::id) {
975 RelocateARIAOwnedIfNeeded(elm);
976 ARIAActiveDescendantIDMaybeMoved(elm);
977 }
978
979 // ARIA or XUL selection
980 if ((aAccessible->GetContent()->IsXULElement() &&
981 aAttribute == nsGkAtoms::selected) ||
982 aAttribute == nsGkAtoms::aria_selected) {
983 LocalAccessible* widget =
984 nsAccUtils::GetSelectableContainer(aAccessible, aAccessible->State());
985 if (widget) {
986 AccSelChangeEvent::SelChangeType selChangeType =
987 elm->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true,
988 eCaseMatters)
989 ? AccSelChangeEvent::eSelectionAdd
990 : AccSelChangeEvent::eSelectionRemove;
991
992 RefPtr<AccEvent> event =
993 new AccSelChangeEvent(widget, aAccessible, selChangeType);
994 FireDelayedEvent(event);
995 }
996
997 return;
998 }
999
1000 if (aAttribute == nsGkAtoms::contenteditable) {
1001 RefPtr<AccEvent> editableChangeEvent =
1002 new AccStateChangeEvent(aAccessible, states::EDITABLE);
1003 FireDelayedEvent(editableChangeEvent);
1004 // Fire a focusable state change event if the previous state was different.
1005 // It may be the same if contenteditable is set on a node that doesn't
1006 // support it. Like an <input>.
1007 aAccessible->MaybeFireFocusableStateChange(
1008 (mPrevStateBits & states::FOCUSABLE));
1009 return;
1010 }
1011
1012 if (aAttribute == nsGkAtoms::value) {
1013 if (aAccessible->IsProgress()) {
1014 FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible);
1015 }
1016 return;
1017 }
1018
1019 if (aModType == dom::MutationEvent_Binding::REMOVAL ||
1020 aModType == dom::MutationEvent_Binding::ADDITION) {
1021 if (aAttribute == nsGkAtoms::href) {
1022 if (aAccessible->IsHTMLLink() &&
1023 !nsCoreUtils::HasClickListener(aAccessible->GetContent())) {
1024 RefPtr<AccEvent> linkedChangeEvent =
1025 new AccStateChangeEvent(aAccessible, states::LINKED);
1026 FireDelayedEvent(linkedChangeEvent);
1027 // Fire a focusable state change event if the previous state was
1028 // different. It may be the same if there is tabindex on this link.
1029 aAccessible->MaybeFireFocusableStateChange(
1030 (mPrevStateBits & states::FOCUSABLE));
1031 }
1032 }
1033 }
1034 }
1035
1036 // DocAccessible protected member
ARIAAttributeChanged(LocalAccessible * aAccessible,nsAtom * aAttribute)1037 void DocAccessible::ARIAAttributeChanged(LocalAccessible* aAccessible,
1038 nsAtom* aAttribute) {
1039 // Note: For universal/global ARIA states and properties we don't care if
1040 // there is an ARIA role present or not.
1041
1042 if (aAttribute == nsGkAtoms::aria_required) {
1043 RefPtr<AccEvent> event =
1044 new AccStateChangeEvent(aAccessible, states::REQUIRED);
1045 FireDelayedEvent(event);
1046 return;
1047 }
1048
1049 if (aAttribute == nsGkAtoms::aria_invalid) {
1050 RefPtr<AccEvent> event =
1051 new AccStateChangeEvent(aAccessible, states::INVALID);
1052 FireDelayedEvent(event);
1053 return;
1054 }
1055
1056 // The activedescendant universal property redirects accessible focus events
1057 // to the element with the id that activedescendant points to. Make sure
1058 // the tree up to date before processing. In other words, when a node has just
1059 // been inserted, the tree won't be up to date yet, so we must always schedule
1060 // an async notification so that a newly inserted node will be present in
1061 // the tree.
1062 if (aAttribute == nsGkAtoms::aria_activedescendant) {
1063 mNotificationController
1064 ->ScheduleNotification<DocAccessible, LocalAccessible>(
1065 this, &DocAccessible::ARIAActiveDescendantChanged, aAccessible);
1066 return;
1067 }
1068
1069 // We treat aria-expanded as a global ARIA state for historical reasons
1070 if (aAttribute == nsGkAtoms::aria_expanded) {
1071 RefPtr<AccEvent> event =
1072 new AccStateChangeEvent(aAccessible, states::EXPANDED);
1073 FireDelayedEvent(event);
1074 return;
1075 }
1076
1077 // For aria attributes like drag and drop changes we fire a generic attribute
1078 // change event; at least until native API comes up with a more meaningful
1079 // event.
1080 uint8_t attrFlags = aria::AttrCharacteristicsFor(aAttribute);
1081 if (!(attrFlags & ATTR_BYPASSOBJ)) {
1082 RefPtr<AccEvent> event =
1083 new AccObjectAttrChangedEvent(aAccessible, aAttribute);
1084 FireDelayedEvent(event);
1085 }
1086
1087 dom::Element* elm = aAccessible->GetContent()->AsElement();
1088
1089 if (aAttribute == nsGkAtoms::aria_checked ||
1090 (aAccessible->IsButton() && aAttribute == nsGkAtoms::aria_pressed)) {
1091 const uint64_t kState = (aAttribute == nsGkAtoms::aria_checked)
1092 ? states::CHECKED
1093 : states::PRESSED;
1094 RefPtr<AccEvent> event = new AccStateChangeEvent(aAccessible, kState);
1095 FireDelayedEvent(event);
1096
1097 bool wasMixed = (mARIAAttrOldValue == nsGkAtoms::mixed);
1098 bool isMixed = elm->AttrValueIs(kNameSpaceID_None, aAttribute,
1099 nsGkAtoms::mixed, eCaseMatters);
1100 if (isMixed != wasMixed) {
1101 RefPtr<AccEvent> event =
1102 new AccStateChangeEvent(aAccessible, states::MIXED, isMixed);
1103 FireDelayedEvent(event);
1104 }
1105 return;
1106 }
1107
1108 if (aAttribute == nsGkAtoms::aria_readonly) {
1109 RefPtr<AccEvent> event =
1110 new AccStateChangeEvent(aAccessible, states::READONLY);
1111 FireDelayedEvent(event);
1112 return;
1113 }
1114
1115 // Fire text value change event whenever aria-valuetext is changed.
1116 if (aAttribute == nsGkAtoms::aria_valuetext) {
1117 FireDelayedEvent(nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE, aAccessible);
1118 return;
1119 }
1120
1121 // Fire numeric value change event when aria-valuenow is changed and
1122 // aria-valuetext is empty
1123 if (aAttribute == nsGkAtoms::aria_valuenow &&
1124 (!elm->HasAttr(kNameSpaceID_None, nsGkAtoms::aria_valuetext) ||
1125 elm->AttrValueIs(kNameSpaceID_None, nsGkAtoms::aria_valuetext,
1126 nsGkAtoms::_empty, eCaseMatters))) {
1127 FireDelayedEvent(nsIAccessibleEvent::EVENT_VALUE_CHANGE, aAccessible);
1128 return;
1129 }
1130
1131 if (aAttribute == nsGkAtoms::aria_current) {
1132 RefPtr<AccEvent> event =
1133 new AccStateChangeEvent(aAccessible, states::CURRENT);
1134 FireDelayedEvent(event);
1135 return;
1136 }
1137
1138 if (aAttribute == nsGkAtoms::aria_haspopup) {
1139 RefPtr<AccEvent> event =
1140 new AccStateChangeEvent(aAccessible, states::HASPOPUP);
1141 FireDelayedEvent(event);
1142 return;
1143 }
1144
1145 if (aAttribute == nsGkAtoms::aria_owns) {
1146 mNotificationController->ScheduleRelocation(aAccessible);
1147 }
1148 }
1149
ARIAActiveDescendantChanged(LocalAccessible * aAccessible)1150 void DocAccessible::ARIAActiveDescendantChanged(LocalAccessible* aAccessible) {
1151 nsIContent* elm = aAccessible->GetContent();
1152 if (elm && elm->IsElement() && aAccessible->IsActiveWidget()) {
1153 nsAutoString id;
1154 if (elm->AsElement()->GetAttr(kNameSpaceID_None,
1155 nsGkAtoms::aria_activedescendant, id)) {
1156 dom::Element* activeDescendantElm = IDRefsIterator::GetElem(elm, id);
1157 if (activeDescendantElm) {
1158 LocalAccessible* activeDescendant = GetAccessible(activeDescendantElm);
1159 if (activeDescendant) {
1160 FocusMgr()->ActiveItemChanged(activeDescendant, false);
1161 #ifdef A11Y_LOG
1162 if (logging::IsEnabled(logging::eFocus)) {
1163 logging::ActiveItemChangeCausedBy("ARIA activedescedant changed",
1164 activeDescendant);
1165 }
1166 #endif
1167 return;
1168 }
1169 }
1170 }
1171
1172 // aria-activedescendant was cleared or changed to a non-existent node.
1173 // Move focus back to the element itself.
1174 FocusMgr()->ActiveItemChanged(aAccessible, false);
1175 #ifdef A11Y_LOG
1176 if (logging::IsEnabled(logging::eFocus)) {
1177 logging::ActiveItemChangeCausedBy("ARIA activedescedant cleared",
1178 aAccessible);
1179 }
1180 #endif
1181 }
1182 }
1183
ContentAppended(nsIContent * aFirstNewContent)1184 void DocAccessible::ContentAppended(nsIContent* aFirstNewContent) {}
1185
ContentStateChanged(dom::Document * aDocument,nsIContent * aContent,EventStates aStateMask)1186 void DocAccessible::ContentStateChanged(dom::Document* aDocument,
1187 nsIContent* aContent,
1188 EventStates aStateMask) {
1189 LocalAccessible* accessible = GetAccessible(aContent);
1190 if (!accessible) return;
1191
1192 if (aStateMask.HasState(NS_EVENT_STATE_CHECKED)) {
1193 LocalAccessible* widget = accessible->ContainerWidget();
1194 if (widget && widget->IsSelect()) {
1195 AccSelChangeEvent::SelChangeType selChangeType =
1196 aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED)
1197 ? AccSelChangeEvent::eSelectionAdd
1198 : AccSelChangeEvent::eSelectionRemove;
1199 RefPtr<AccEvent> event =
1200 new AccSelChangeEvent(widget, accessible, selChangeType);
1201 FireDelayedEvent(event);
1202 return;
1203 }
1204
1205 RefPtr<AccEvent> event = new AccStateChangeEvent(
1206 accessible, states::CHECKED,
1207 aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED));
1208 FireDelayedEvent(event);
1209 }
1210
1211 if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) {
1212 RefPtr<AccEvent> event =
1213 new AccStateChangeEvent(accessible, states::INVALID, true);
1214 FireDelayedEvent(event);
1215 }
1216
1217 if (aStateMask.HasState(NS_EVENT_STATE_REQUIRED)) {
1218 RefPtr<AccEvent> event =
1219 new AccStateChangeEvent(accessible, states::REQUIRED);
1220 FireDelayedEvent(event);
1221 }
1222
1223 if (aStateMask.HasState(NS_EVENT_STATE_VISITED)) {
1224 RefPtr<AccEvent> event =
1225 new AccStateChangeEvent(accessible, states::TRAVERSED, true);
1226 FireDelayedEvent(event);
1227 }
1228 }
1229
CharacterDataWillChange(nsIContent * aContent,const CharacterDataChangeInfo &)1230 void DocAccessible::CharacterDataWillChange(nsIContent* aContent,
1231 const CharacterDataChangeInfo&) {}
1232
CharacterDataChanged(nsIContent * aContent,const CharacterDataChangeInfo &)1233 void DocAccessible::CharacterDataChanged(nsIContent* aContent,
1234 const CharacterDataChangeInfo&) {}
1235
ContentInserted(nsIContent * aChild)1236 void DocAccessible::ContentInserted(nsIContent* aChild) {}
1237
ContentRemoved(nsIContent * aChildNode,nsIContent * aPreviousSiblingNode)1238 void DocAccessible::ContentRemoved(nsIContent* aChildNode,
1239 nsIContent* aPreviousSiblingNode) {
1240 #ifdef A11Y_LOG
1241 if (logging::IsEnabled(logging::eTree)) {
1242 logging::MsgBegin("TREE", "DOM content removed; doc: %p", this);
1243 logging::Node("container node", aChildNode->GetParent());
1244 logging::Node("content node", aChildNode);
1245 logging::MsgEnd();
1246 }
1247 #endif
1248 // This one and content removal notification from layout may result in
1249 // double processing of same subtrees. If it pops up in profiling, then
1250 // consider reusing a document node cache to reject these notifications early.
1251 ContentRemoved(aChildNode);
1252 }
1253
ParentChainChanged(nsIContent * aContent)1254 void DocAccessible::ParentChainChanged(nsIContent* aContent) {}
1255
1256 ////////////////////////////////////////////////////////////////////////////////
1257 // LocalAccessible
1258
1259 #ifdef A11Y_LOG
HandleAccEvent(AccEvent * aEvent)1260 nsresult DocAccessible::HandleAccEvent(AccEvent* aEvent) {
1261 if (logging::IsEnabled(logging::eDocLoad)) {
1262 logging::DocLoadEventHandled(aEvent);
1263 }
1264
1265 return HyperTextAccessible::HandleAccEvent(aEvent);
1266 }
1267 #endif
1268
1269 ////////////////////////////////////////////////////////////////////////////////
1270 // Public members
1271
PresContext() const1272 nsPresContext* DocAccessible::PresContext() const {
1273 return mPresShell->GetPresContext();
1274 }
1275
GetNativeWindow() const1276 void* DocAccessible::GetNativeWindow() const {
1277 if (!mPresShell) {
1278 return nullptr;
1279 }
1280
1281 nsViewManager* vm = mPresShell->GetViewManager();
1282 if (!vm) return nullptr;
1283
1284 nsCOMPtr<nsIWidget> widget;
1285 vm->GetRootWidget(getter_AddRefs(widget));
1286 if (widget) return widget->GetNativeData(NS_NATIVE_WINDOW);
1287
1288 return nullptr;
1289 }
1290
GetAccessibleByUniqueIDInSubtree(void * aUniqueID)1291 LocalAccessible* DocAccessible::GetAccessibleByUniqueIDInSubtree(
1292 void* aUniqueID) {
1293 LocalAccessible* child = GetAccessibleByUniqueID(aUniqueID);
1294 if (child) return child;
1295
1296 uint32_t childDocCount = mChildDocuments.Length();
1297 for (uint32_t childDocIdx = 0; childDocIdx < childDocCount; childDocIdx++) {
1298 DocAccessible* childDocument = mChildDocuments.ElementAt(childDocIdx);
1299 child = childDocument->GetAccessibleByUniqueIDInSubtree(aUniqueID);
1300 if (child) return child;
1301 }
1302
1303 return nullptr;
1304 }
1305
GetAccessibleOrContainer(nsINode * aNode,bool aNoContainerIfPruned) const1306 LocalAccessible* DocAccessible::GetAccessibleOrContainer(
1307 nsINode* aNode, bool aNoContainerIfPruned) const {
1308 if (!aNode || !aNode->GetComposedDoc()) {
1309 return nullptr;
1310 }
1311
1312 nsINode* start = aNode;
1313 if (auto* shadowRoot = dom::ShadowRoot::FromNode(aNode)) {
1314 // This can happen, for example, when called within
1315 // SelectionManager::ProcessSelectionChanged due to focusing a direct
1316 // child of a shadow root.
1317 // GetFlattenedTreeParent works on children of a shadow root, but not the
1318 // shadow root itself.
1319 start = shadowRoot->GetHost();
1320 if (!start) {
1321 return nullptr;
1322 }
1323 }
1324
1325 for (nsINode* currNode : dom::InclusiveFlatTreeAncestors(*start)) {
1326 // No container if is inside of aria-hidden subtree.
1327 if (aNoContainerIfPruned && currNode->IsElement() &&
1328 aria::HasDefinedARIAHidden(currNode->AsElement())) {
1329 return nullptr;
1330 }
1331
1332 // Check if node is in an unselected deck panel
1333 if (aNoContainerIfPruned && currNode->IsXULElement()) {
1334 if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
1335 nsDeckFrame* deckFrame = do_QueryFrame(frame->GetParent());
1336 if (deckFrame && deckFrame->GetSelectedBox() != frame) {
1337 // If deck is not a <tabpanels>, return null
1338 nsIContent* parentFrameContent = deckFrame->GetContent();
1339 if (!parentFrameContent ||
1340 !parentFrameContent->IsXULElement(nsGkAtoms::tabpanels)) {
1341 return nullptr;
1342 }
1343 }
1344 }
1345 }
1346
1347 // Check if node is in zero-sized map
1348 if (aNoContainerIfPruned && currNode->IsHTMLElement(nsGkAtoms::map)) {
1349 if (nsIFrame* frame = currNode->AsContent()->GetPrimaryFrame()) {
1350 if (nsLayoutUtils::GetAllInFlowRectsUnion(frame, frame->GetParent())
1351 .IsEmpty()) {
1352 return nullptr;
1353 }
1354 }
1355 }
1356
1357 if (LocalAccessible* accessible = GetAccessible(currNode)) {
1358 return accessible;
1359 }
1360 }
1361
1362 return nullptr;
1363 }
1364
GetContainerAccessible(nsINode * aNode) const1365 LocalAccessible* DocAccessible::GetContainerAccessible(nsINode* aNode) const {
1366 return aNode ? GetAccessibleOrContainer(aNode->GetFlattenedTreeParentNode())
1367 : nullptr;
1368 }
1369
GetAccessibleOrDescendant(nsINode * aNode) const1370 LocalAccessible* DocAccessible::GetAccessibleOrDescendant(
1371 nsINode* aNode) const {
1372 LocalAccessible* acc = GetAccessible(aNode);
1373 if (acc) return acc;
1374
1375 if (aNode == mContent || aNode == mDocumentNode->GetRootElement()) {
1376 // If the node is the doc's body or root element, return the doc accessible.
1377 return const_cast<DocAccessible*>(this);
1378 }
1379
1380 acc = GetContainerAccessible(aNode);
1381 if (acc) {
1382 TreeWalker walker(acc, aNode->AsContent(),
1383 TreeWalker::eWalkCache | TreeWalker::eScoped);
1384 return walker.Next();
1385 }
1386
1387 return nullptr;
1388 }
1389
BindToDocument(LocalAccessible * aAccessible,const nsRoleMapEntry * aRoleMapEntry)1390 void DocAccessible::BindToDocument(LocalAccessible* aAccessible,
1391 const nsRoleMapEntry* aRoleMapEntry) {
1392 // Put into DOM node cache.
1393 if (aAccessible->IsNodeMapEntry()) {
1394 mNodeToAccessibleMap.InsertOrUpdate(aAccessible->GetNode(), aAccessible);
1395 }
1396
1397 // Put into unique ID cache.
1398 mAccessibleCache.InsertOrUpdate(aAccessible->UniqueID(), RefPtr{aAccessible});
1399
1400 aAccessible->SetRoleMapEntry(aRoleMapEntry);
1401
1402 if (aAccessible->HasOwnContent()) {
1403 AddDependentIDsFor(aAccessible);
1404
1405 nsIContent* content = aAccessible->GetContent();
1406 if (content->IsElement() && content->AsElement()->HasAttr(
1407 kNameSpaceID_None, nsGkAtoms::aria_owns)) {
1408 mNotificationController->ScheduleRelocation(aAccessible);
1409 }
1410 }
1411 }
1412
UnbindFromDocument(LocalAccessible * aAccessible)1413 void DocAccessible::UnbindFromDocument(LocalAccessible* aAccessible) {
1414 NS_ASSERTION(mAccessibleCache.GetWeak(aAccessible->UniqueID()),
1415 "Unbinding the unbound accessible!");
1416
1417 // Fire focus event on accessible having DOM focus if last focus was removed
1418 // from the tree.
1419 if (FocusMgr()->WasLastFocused(aAccessible)) {
1420 FocusMgr()->ActiveItemChanged(nullptr);
1421 #ifdef A11Y_LOG
1422 if (logging::IsEnabled(logging::eFocus)) {
1423 logging::ActiveItemChangeCausedBy("tree shutdown", aAccessible);
1424 }
1425 #endif
1426 }
1427
1428 // Remove an accessible from node-to-accessible map if it exists there.
1429 if (aAccessible->IsNodeMapEntry() &&
1430 mNodeToAccessibleMap.Get(aAccessible->GetNode()) == aAccessible) {
1431 mNodeToAccessibleMap.Remove(aAccessible->GetNode());
1432 }
1433
1434 aAccessible->mStateFlags |= eIsNotInDocument;
1435
1436 // Update XPCOM part.
1437 xpcAccessibleDocument* xpcDoc = GetAccService()->GetCachedXPCDocument(this);
1438 if (xpcDoc) xpcDoc->NotifyOfShutdown(aAccessible);
1439
1440 void* uniqueID = aAccessible->UniqueID();
1441
1442 NS_ASSERTION(!aAccessible->IsDefunct(), "Shutdown the shutdown accessible!");
1443 aAccessible->Shutdown();
1444
1445 mAccessibleCache.Remove(uniqueID);
1446 }
1447
ContentInserted(nsIContent * aStartChildNode,nsIContent * aEndChildNode)1448 void DocAccessible::ContentInserted(nsIContent* aStartChildNode,
1449 nsIContent* aEndChildNode) {
1450 // Ignore content insertions until we constructed accessible tree. Otherwise
1451 // schedule tree update on content insertion after layout.
1452 if (!mNotificationController || !HasLoadState(eTreeConstructed)) {
1453 return;
1454 }
1455
1456 // The frame constructor guarantees that only ranges with the same parent
1457 // arrive here in presence of dynamic changes to the page, see
1458 // nsCSSFrameConstructor::IssueSingleInsertNotifications' callers.
1459 nsINode* parent = aStartChildNode->GetFlattenedTreeParentNode();
1460 if (!parent) {
1461 return;
1462 }
1463
1464 LocalAccessible* container = AccessibleOrTrueContainer(parent);
1465 if (!container) {
1466 return;
1467 }
1468
1469 AutoTArray<nsCOMPtr<nsIContent>, 10> list;
1470 for (nsIContent* node = aStartChildNode; node != aEndChildNode;
1471 node = node->GetNextSibling()) {
1472 MOZ_ASSERT(parent == node->GetFlattenedTreeParentNode());
1473 if (PruneOrInsertSubtree(node)) {
1474 list.AppendElement(node);
1475 }
1476 }
1477
1478 mNotificationController->ScheduleContentInsertion(container, list);
1479 }
1480
PruneOrInsertSubtree(nsIContent * aRoot)1481 bool DocAccessible::PruneOrInsertSubtree(nsIContent* aRoot) {
1482 bool insert = false;
1483
1484 // In the case that we are, or are in, a shadow host, we need to assure
1485 // some accessibles are removed if they are not rendered anymore.
1486 nsIContent* shadowHost =
1487 aRoot->GetShadowRoot() ? aRoot : aRoot->GetContainingShadowHost();
1488 if (shadowHost) {
1489 dom::ExplicitChildIterator iter(shadowHost);
1490
1491 // Check all explicit children in the host, if they are not slotted
1492 // then remove their accessibles and subtrees.
1493 while (nsIContent* childNode = iter.GetNextChild()) {
1494 if (!childNode->GetPrimaryFrame() &&
1495 !nsCoreUtils::IsDisplayContents(childNode)) {
1496 ContentRemoved(childNode);
1497 }
1498 }
1499
1500 // If this is a slot, check to see if its fallback content is rendered,
1501 // if not - remove it.
1502 if (aRoot->IsHTMLElement(nsGkAtoms::slot)) {
1503 for (nsIContent* childNode = aRoot->GetFirstChild(); childNode;
1504 childNode = childNode->GetNextSibling()) {
1505 if (!childNode->GetPrimaryFrame() &&
1506 !nsCoreUtils::IsDisplayContents(childNode)) {
1507 ContentRemoved(childNode);
1508 }
1509 }
1510 }
1511 }
1512
1513 // If we already have an accessible, check if we need to remove it, recreate
1514 // it, or keep it in place.
1515 LocalAccessible* acc = GetAccessible(aRoot);
1516 if (acc) {
1517 MOZ_ASSERT(aRoot == acc->GetContent(),
1518 "LocalAccessible has differing content!");
1519 #ifdef A11Y_LOG
1520 if (logging::IsEnabled(logging::eTree)) {
1521 logging::MsgBegin(
1522 "TREE", "inserted content already has accessible; doc: %p", this);
1523 logging::Node("content node", aRoot);
1524 logging::AccessibleInfo("accessible node", acc);
1525 logging::MsgEnd();
1526 }
1527 #endif
1528
1529 nsIFrame* frame = acc->GetFrame();
1530
1531 // LocalAccessible has no frame and it's not display:contents. Remove it.
1532 // As well as removing the a11y subtree, we must also remove Accessibles
1533 // for DOM descendants, since some of these might be relocated Accessibles
1534 // and their DOM nodes are now hidden as well.
1535 if (!frame && !nsCoreUtils::IsDisplayContents(aRoot)) {
1536 ContentRemoved(aRoot);
1537 return false;
1538 }
1539
1540 // If it's a XULLabel it was probably reframed because a `value` attribute
1541 // was added. The accessible creates its text leaf upon construction, so we
1542 // need to recreate. Remove it, and schedule for reconstruction.
1543 if (acc->IsXULLabel()) {
1544 ContentRemoved(acc);
1545 return true;
1546 }
1547
1548 // It is a broken image that is being reframed because it either got
1549 // or lost an `alt` tag that would rerender this node as text.
1550 if (frame && (acc->IsImage() != (frame->AccessibleType() == eImageType))) {
1551 ContentRemoved(aRoot);
1552 return true;
1553 }
1554
1555 // If the frame is an OuterDoc frame but this isn't an OuterDocAccessible,
1556 // we need to recreate the LocalAccessible. This can happen for embed or
1557 // object elements if their embedded content changes to be web content.
1558 if (frame && !acc->IsOuterDoc() &&
1559 frame->AccessibleType() == eOuterDocType) {
1560 ContentRemoved(aRoot);
1561 return true;
1562 }
1563
1564 // If the content is focused, and is being re-framed, reset the selection
1565 // listener for the node because the previous selection listener is on the
1566 // old frame.
1567 if (aRoot->IsElement() && FocusMgr()->HasDOMFocus(aRoot)) {
1568 SelectionMgr()->SetControlSelectionListener(aRoot->AsElement());
1569 }
1570
1571 // If the accessible is a table, or table part, its layout table
1572 // status may have changed. We need to invalidate the associated
1573 // cache, which listens for the following event.
1574 if (acc->IsTable() || acc->IsTableRow() || acc->IsTableCell()) {
1575 FireDelayedEvent(nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED, acc);
1576 }
1577
1578 // The accessible can be reparented or reordered in its parent.
1579 // We schedule it for reinsertion. For example, a slotted element
1580 // can change its slot attribute to a different slot.
1581 insert = true;
1582
1583 // If the frame is invisible, remove it.
1584 // Normally, layout sends explicit a11y notifications for visibility
1585 // changes (see SendA11yNotifications in RestyleManager). However, if a
1586 // visibility change also reconstructs the frame, we must handle it here.
1587 if (frame && !frame->StyleVisibility()->IsVisible()) {
1588 ContentRemoved(aRoot);
1589 // There might be visible descendants, so we want to walk the subtree.
1590 // However, we know we don't want to reinsert this node, so we set insert
1591 // to false.
1592 insert = false;
1593 }
1594 } else {
1595 // If there is no current accessible, and the node has a frame, or is
1596 // display:contents, schedule it for insertion.
1597 if (aRoot->GetPrimaryFrame() || nsCoreUtils::IsDisplayContents(aRoot)) {
1598 // This may be a new subtree, the insertion process will recurse through
1599 // its descendants.
1600 if (!GetAccessibleOrDescendant(aRoot)) {
1601 return true;
1602 }
1603
1604 // Content is not an accessible, but has accessible descendants.
1605 // We schedule this container for insertion strictly for the case where it
1606 // itself now needs an accessible. We will still need to recurse into the
1607 // descendant content to prune accessibles, and in all likelyness to
1608 // insert accessibles since accessible insertions will likeley get missed
1609 // in an existing subtree.
1610 insert = true;
1611 }
1612 }
1613
1614 if (LocalAccessible* container = AccessibleOrTrueContainer(aRoot)) {
1615 AutoTArray<nsCOMPtr<nsIContent>, 10> list;
1616 dom::AllChildrenIterator iter =
1617 dom::AllChildrenIterator(aRoot, nsIContent::eAllChildren, true);
1618 while (nsIContent* childNode = iter.GetNextChild()) {
1619 if (PruneOrInsertSubtree(childNode)) {
1620 list.AppendElement(childNode);
1621 }
1622 }
1623
1624 if (!list.IsEmpty()) {
1625 mNotificationController->ScheduleContentInsertion(container, list);
1626 }
1627 }
1628
1629 return insert;
1630 }
1631
RecreateAccessible(nsIContent * aContent)1632 void DocAccessible::RecreateAccessible(nsIContent* aContent) {
1633 #ifdef A11Y_LOG
1634 if (logging::IsEnabled(logging::eTree)) {
1635 logging::MsgBegin("TREE", "accessible recreated");
1636 logging::Node("content", aContent);
1637 logging::MsgEnd();
1638 }
1639 #endif
1640
1641 // XXX: we shouldn't recreate whole accessible subtree, instead we should
1642 // subclass hide and show events to handle them separately and implement their
1643 // coalescence with normal hide and show events. Note, in this case they
1644 // should be coalesced with normal show/hide events.
1645 ContentRemoved(aContent);
1646 ContentInserted(aContent, aContent->GetNextSibling());
1647 }
1648
ProcessInvalidationList()1649 void DocAccessible::ProcessInvalidationList() {
1650 // Invalidate children of container accessible for each element in
1651 // invalidation list. Allow invalidation list insertions while container
1652 // children are recached.
1653 for (uint32_t idx = 0; idx < mInvalidationList.Length(); idx++) {
1654 nsIContent* content = mInvalidationList[idx];
1655 if (!HasAccessible(content) && content->HasID()) {
1656 LocalAccessible* container = GetContainerAccessible(content);
1657 if (container) {
1658 // Check if the node is a target of aria-owns, and if so, don't process
1659 // it here and let DoARIAOwnsRelocation process it.
1660 AttrRelProviders* list = GetRelProviders(
1661 content->AsElement(), nsDependentAtomString(content->GetID()));
1662 bool shouldProcess = !!list;
1663 if (shouldProcess) {
1664 for (uint32_t idx = 0; idx < list->Length(); idx++) {
1665 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
1666 shouldProcess = false;
1667 break;
1668 }
1669 }
1670
1671 if (shouldProcess) {
1672 ProcessContentInserted(container, content);
1673 }
1674 }
1675 }
1676 }
1677 }
1678
1679 mInvalidationList.Clear();
1680 }
1681
GetAccessibleEvenIfNotInMap(nsINode * aNode) const1682 LocalAccessible* DocAccessible::GetAccessibleEvenIfNotInMap(
1683 nsINode* aNode) const {
1684 if (!aNode->IsContent() ||
1685 !aNode->AsContent()->IsHTMLElement(nsGkAtoms::area)) {
1686 return GetAccessible(aNode);
1687 }
1688
1689 // XXX Bug 135040, incorrect when multiple images use the same map.
1690 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
1691 nsImageFrame* imageFrame = do_QueryFrame(frame);
1692 if (imageFrame) {
1693 LocalAccessible* parent = GetAccessible(imageFrame->GetContent());
1694 if (parent) {
1695 LocalAccessible* area =
1696 parent->AsImageMap()->GetChildAccessibleFor(aNode);
1697 if (area) return area;
1698
1699 return nullptr;
1700 }
1701 }
1702
1703 return GetAccessible(aNode);
1704 }
1705
1706 ////////////////////////////////////////////////////////////////////////////////
1707 // Protected members
1708
NotifyOfLoading(bool aIsReloading)1709 void DocAccessible::NotifyOfLoading(bool aIsReloading) {
1710 // Mark the document accessible as loading, if it stays alive then we'll mark
1711 // it as loaded when we receive proper notification.
1712 mLoadState &= ~eDOMLoaded;
1713
1714 if (!IsLoadEventTarget()) return;
1715
1716 if (aIsReloading && !mLoadEventType &&
1717 // We can't fire events on a document whose tree isn't constructed yet.
1718 HasLoadState(eTreeConstructed)) {
1719 // Fire reload and state busy events on existing document accessible while
1720 // event from user input flag can be calculated properly and accessible
1721 // is alive. When new document gets loaded then this one is destroyed.
1722 RefPtr<AccEvent> reloadEvent =
1723 new AccEvent(nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD, this);
1724 nsEventShell::FireEvent(reloadEvent);
1725 }
1726
1727 // Fire state busy change event. Use delayed event since we don't care
1728 // actually if event isn't delivered when the document goes away like a shot.
1729 RefPtr<AccEvent> stateEvent =
1730 new AccStateChangeEvent(this, states::BUSY, true);
1731 FireDelayedEvent(stateEvent);
1732 }
1733
DoInitialUpdate()1734 void DocAccessible::DoInitialUpdate() {
1735 if (nsCoreUtils::IsTopLevelContentDocInProcess(mDocumentNode)) {
1736 mDocFlags |= eTopLevelContentDocInProcess;
1737 if (IPCAccessibilityActive()) {
1738 nsIDocShell* docShell = mDocumentNode->GetDocShell();
1739 if (RefPtr<dom::BrowserChild> browserChild =
1740 dom::BrowserChild::GetFrom(docShell)) {
1741 // In content processes, top level content documents are always
1742 // RootAccessibles.
1743 MOZ_ASSERT(IsRoot());
1744 DocAccessibleChild* ipcDoc = IPCDoc();
1745 if (ipcDoc) {
1746 browserChild->SetTopLevelDocAccessibleChild(ipcDoc);
1747 } else {
1748 ipcDoc = new DocAccessibleChild(this, browserChild);
1749 SetIPCDoc(ipcDoc);
1750 // Subsequent initialization might depend on being able to get the
1751 // top level DocAccessibleChild, so set that as early as possible.
1752 browserChild->SetTopLevelDocAccessibleChild(ipcDoc);
1753
1754 #if defined(XP_WIN)
1755 IAccessibleHolder holder;
1756 int32_t childID;
1757 if (StaticPrefs::accessibility_cache_enabled_AtStartup()) {
1758 childID = 0;
1759 } else {
1760 holder = CreateHolderFromAccessible(WrapNotNull(this));
1761 MOZ_ASSERT(!holder.IsNull());
1762 childID = MsaaAccessible::GetChildIDFor(this);
1763 }
1764 #else
1765 int32_t holder = 0, childID = 0;
1766 #endif
1767 browserChild->SendPDocAccessibleConstructor(ipcDoc, nullptr, 0,
1768 childID, holder);
1769 #if !defined(XP_WIN)
1770 ipcDoc->SendPDocAccessiblePlatformExtConstructor();
1771 #endif
1772 }
1773 #if !defined(XP_WIN)
1774 // It's safe for us to mark top level documents as constructed in the
1775 // parent process without receiving an explicit message, since we can
1776 // never get queries for this document or descendants before parent
1777 // process construction is complete.
1778 ipcDoc->SetConstructedInParentProcess();
1779 #endif
1780 }
1781 }
1782 }
1783
1784 mLoadState |= eTreeConstructed;
1785
1786 // Set up a root element and ARIA role mapping.
1787 UpdateRootElIfNeeded();
1788
1789 // Build initial tree.
1790 CacheChildrenInSubtree(this);
1791 #ifdef A11Y_LOG
1792 if (logging::IsEnabled(logging::eVerbose)) {
1793 logging::Tree("TREE", "Initial subtree", this);
1794 }
1795 if (logging::IsEnabled(logging::eTreeSize)) {
1796 logging::TreeSize("TREE SIZE", "Initial subtree", this);
1797 }
1798 #endif
1799
1800 // Fire reorder event after the document tree is constructed. Note, since
1801 // this reorder event is processed by parent document then events targeted to
1802 // this document may be fired prior to this reorder event. If this is
1803 // a problem then consider to keep event processing per tab document.
1804 if (!IsRoot()) {
1805 RefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(LocalParent());
1806 ParentDocument()->FireDelayedEvent(reorderEvent);
1807 }
1808
1809 if (IPCAccessibilityActive()) {
1810 DocAccessibleChild* ipcDoc = IPCDoc();
1811 MOZ_ASSERT(ipcDoc);
1812 if (ipcDoc) {
1813 for (auto idx = 0U; idx < mChildren.Length(); idx++) {
1814 ipcDoc->InsertIntoIpcTree(this, mChildren.ElementAt(idx), idx);
1815 }
1816 }
1817 }
1818 }
1819
ProcessLoad()1820 void DocAccessible::ProcessLoad() {
1821 mLoadState |= eCompletelyLoaded;
1822
1823 #ifdef A11Y_LOG
1824 if (logging::IsEnabled(logging::eDocLoad)) {
1825 logging::DocCompleteLoad(this, IsLoadEventTarget());
1826 }
1827 #endif
1828
1829 // Do not fire document complete/stop events for root chrome document
1830 // accessibles and for frame/iframe documents because
1831 // a) screen readers start working on focus event in the case of root chrome
1832 // documents
1833 // b) document load event on sub documents causes screen readers to act is if
1834 // entire page is reloaded.
1835 if (!IsLoadEventTarget()) return;
1836
1837 // Fire complete/load stopped if the load event type is given.
1838 if (mLoadEventType) {
1839 RefPtr<AccEvent> loadEvent = new AccEvent(mLoadEventType, this);
1840 FireDelayedEvent(loadEvent);
1841
1842 mLoadEventType = 0;
1843 }
1844
1845 // Fire busy state change event.
1846 RefPtr<AccEvent> stateEvent =
1847 new AccStateChangeEvent(this, states::BUSY, false);
1848 FireDelayedEvent(stateEvent);
1849 }
1850
AddDependentIDsFor(LocalAccessible * aRelProvider,nsAtom * aRelAttr)1851 void DocAccessible::AddDependentIDsFor(LocalAccessible* aRelProvider,
1852 nsAtom* aRelAttr) {
1853 dom::Element* relProviderEl = aRelProvider->Elm();
1854 if (!relProviderEl) return;
1855
1856 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1857 nsStaticAtom* relAttr = kRelationAttrs[idx];
1858 if (aRelAttr && aRelAttr != relAttr) continue;
1859
1860 if (relAttr == nsGkAtoms::_for) {
1861 if (!relProviderEl->IsAnyOfHTMLElements(nsGkAtoms::label,
1862 nsGkAtoms::output)) {
1863 continue;
1864 }
1865
1866 } else if (relAttr == nsGkAtoms::control) {
1867 if (!relProviderEl->IsAnyOfXULElements(nsGkAtoms::label,
1868 nsGkAtoms::description)) {
1869 continue;
1870 }
1871 }
1872
1873 IDRefsIterator iter(this, relProviderEl, relAttr);
1874 while (true) {
1875 const nsDependentSubstring id = iter.NextID();
1876 if (id.IsEmpty()) break;
1877
1878 AttrRelProviders* providers = GetOrCreateRelProviders(relProviderEl, id);
1879 if (providers) {
1880 AttrRelProvider* provider = new AttrRelProvider(relAttr, relProviderEl);
1881 if (provider) {
1882 providers->AppendElement(provider);
1883
1884 // We've got here during the children caching. If the referenced
1885 // content is not accessible then store it to pend its container
1886 // children invalidation (this happens immediately after the caching
1887 // is finished).
1888 nsIContent* dependentContent = iter.GetElem(id);
1889 if (dependentContent) {
1890 if (!HasAccessible(dependentContent)) {
1891 mInvalidationList.AppendElement(dependentContent);
1892 }
1893 }
1894 }
1895 }
1896 }
1897
1898 // If the relation attribute is given then we don't have anything else to
1899 // check.
1900 if (aRelAttr) break;
1901 }
1902
1903 // Make sure to schedule the tree update if needed.
1904 mNotificationController->ScheduleProcessing();
1905 }
1906
RemoveDependentIDsFor(LocalAccessible * aRelProvider,nsAtom * aRelAttr)1907 void DocAccessible::RemoveDependentIDsFor(LocalAccessible* aRelProvider,
1908 nsAtom* aRelAttr) {
1909 dom::Element* relProviderElm = aRelProvider->Elm();
1910 if (!relProviderElm) return;
1911
1912 for (uint32_t idx = 0; idx < kRelationAttrsLen; idx++) {
1913 nsStaticAtom* relAttr = kRelationAttrs[idx];
1914 if (aRelAttr && aRelAttr != kRelationAttrs[idx]) continue;
1915
1916 IDRefsIterator iter(this, relProviderElm, relAttr);
1917 while (true) {
1918 const nsDependentSubstring id = iter.NextID();
1919 if (id.IsEmpty()) break;
1920
1921 AttrRelProviders* providers = GetRelProviders(relProviderElm, id);
1922 if (providers) {
1923 providers->RemoveElementsBy(
1924 [relAttr, relProviderElm](const auto& provider) {
1925 return provider->mRelAttr == relAttr &&
1926 provider->mContent == relProviderElm;
1927 });
1928
1929 RemoveRelProvidersIfEmpty(relProviderElm, id);
1930 }
1931 }
1932
1933 // If the relation attribute is given then we don't have anything else to
1934 // check.
1935 if (aRelAttr) break;
1936 }
1937 }
1938
UpdateAccessibleOnAttrChange(dom::Element * aElement,nsAtom * aAttribute)1939 bool DocAccessible::UpdateAccessibleOnAttrChange(dom::Element* aElement,
1940 nsAtom* aAttribute) {
1941 if (aAttribute == nsGkAtoms::role) {
1942 // It is common for js libraries to set the role on the body element after
1943 // the document has loaded. In this case we just update the role map entry.
1944 if (mContent == aElement) {
1945 SetRoleMapEntryForDoc(aElement);
1946 if (mIPCDoc) {
1947 mIPCDoc->SendRoleChangedEvent(Role());
1948 }
1949
1950 return true;
1951 }
1952
1953 // Recreate the accessible when role is changed because we might require a
1954 // different accessible class for the new role or the accessible may expose
1955 // a different sets of interfaces (COM restriction).
1956 RecreateAccessible(aElement);
1957
1958 return true;
1959 }
1960
1961 if (aAttribute == nsGkAtoms::aria_multiselectable &&
1962 aElement->HasAttr(kNameSpaceID_None, nsGkAtoms::role)) {
1963 // This affects whether the accessible supports SelectAccessible.
1964 // COM says we cannot change what interfaces are supported on-the-fly,
1965 // so invalidate this object. A new one will be created on demand.
1966 RecreateAccessible(aElement);
1967
1968 return true;
1969 }
1970
1971 if (aAttribute == nsGkAtoms::type) {
1972 // If the input[type] changes, we should recreate the accessible.
1973 RecreateAccessible(aElement);
1974 return true;
1975 }
1976
1977 return false;
1978 }
1979
UpdateRootElIfNeeded()1980 void DocAccessible::UpdateRootElIfNeeded() {
1981 dom::Element* rootEl = mDocumentNode->GetBodyElement();
1982 if (!rootEl) {
1983 rootEl = mDocumentNode->GetRootElement();
1984 }
1985 if (rootEl != mContent) {
1986 mContent = rootEl;
1987 SetRoleMapEntryForDoc(rootEl);
1988 if (mIPCDoc) {
1989 mIPCDoc->SendRoleChangedEvent(Role());
1990 }
1991 }
1992 }
1993
1994 /**
1995 * Content insertion helper.
1996 */
1997 class InsertIterator final {
1998 public:
InsertIterator(LocalAccessible * aContext,const nsTArray<nsCOMPtr<nsIContent>> * aNodes)1999 InsertIterator(LocalAccessible* aContext,
2000 const nsTArray<nsCOMPtr<nsIContent>>* aNodes)
2001 : mChild(nullptr),
2002 mChildBefore(nullptr),
2003 mWalker(aContext),
2004 mNodes(aNodes),
2005 mNodesIdx(0) {
2006 MOZ_ASSERT(aContext, "No context");
2007 MOZ_ASSERT(aNodes, "No nodes to search for accessible elements");
2008 MOZ_COUNT_CTOR(InsertIterator);
2009 }
MOZ_COUNTED_DTOR(InsertIterator)2010 MOZ_COUNTED_DTOR(InsertIterator)
2011
2012 LocalAccessible* Context() const { return mWalker.Context(); }
Child() const2013 LocalAccessible* Child() const { return mChild; }
ChildBefore() const2014 LocalAccessible* ChildBefore() const { return mChildBefore; }
Document() const2015 DocAccessible* Document() const { return mWalker.Document(); }
2016
2017 /**
2018 * Iterates to a next accessible within the inserted content.
2019 */
2020 bool Next();
2021
Rejected()2022 void Rejected() {
2023 mChild = nullptr;
2024 mChildBefore = nullptr;
2025 }
2026
2027 private:
2028 LocalAccessible* mChild;
2029 LocalAccessible* mChildBefore;
2030 TreeWalker mWalker;
2031
2032 const nsTArray<nsCOMPtr<nsIContent>>* mNodes;
2033 nsTHashSet<nsPtrHashKey<const nsIContent>> mProcessedNodes;
2034 uint32_t mNodesIdx;
2035 };
2036
Next()2037 bool InsertIterator::Next() {
2038 if (mNodesIdx > 0) {
2039 // If we already processed the first node in the mNodes list,
2040 // check if we can just use the walker to get its next sibling.
2041 LocalAccessible* nextChild = mWalker.Next();
2042 if (nextChild) {
2043 mChildBefore = mChild;
2044 mChild = nextChild;
2045 return true;
2046 }
2047 }
2048
2049 while (mNodesIdx < mNodes->Length()) {
2050 nsIContent* node = mNodes->ElementAt(mNodesIdx++);
2051 // Check to see if we already processed this node with this iterator.
2052 // this can happen if we get two redundant insertions in the case of a
2053 // text and frame insertion.
2054 if (!mProcessedNodes.EnsureInserted(node)) {
2055 continue;
2056 }
2057
2058 LocalAccessible* container = Document()->AccessibleOrTrueContainer(
2059 node->GetFlattenedTreeParentNode(), true);
2060 // Ignore nodes that are not contained by the container anymore.
2061 // The container might be changed, for example, because of the subsequent
2062 // overlapping content insertion (i.e. other content was inserted between
2063 // this inserted content and its container or the content was reinserted
2064 // into different container of unrelated part of tree). To avoid a double
2065 // processing of the content insertion ignore this insertion notification.
2066 // Note, the inserted content might be not in tree at all at this point
2067 // what means there's no container. Ignore the insertion too.
2068 if (container != Context()) {
2069 continue;
2070 }
2071
2072 // HTML comboboxes have no-content list accessible as an intermediate
2073 // containing all options.
2074 if (container->IsHTMLCombobox()) {
2075 container = container->LocalFirstChild();
2076 }
2077
2078 if (!container->IsAcceptableChild(node)) {
2079 continue;
2080 }
2081
2082 #ifdef A11Y_LOG
2083 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
2084 "container", container, "node", node);
2085 #endif
2086
2087 nsIContent* prevNode = mChild ? mChild->GetContent() : nullptr;
2088 if (prevNode && prevNode->GetNextSibling() == node) {
2089 // If inserted nodes are siblings then just move the walker next.
2090 LocalAccessible* nextChild = mWalker.Scope(node);
2091 if (nextChild) {
2092 mChildBefore = mChild;
2093 mChild = nextChild;
2094 return true;
2095 }
2096 } else {
2097 // Otherwise use a new walker to find this node in the container's
2098 // subtree, and retrieve its preceding sibling.
2099 TreeWalker finder(container);
2100 if (finder.Seek(node)) {
2101 mChild = mWalker.Scope(node);
2102 if (mChild) {
2103 MOZ_ASSERT(!mChild->IsRelocated(), "child cannot be aria owned");
2104 mChildBefore = finder.Prev();
2105 return true;
2106 }
2107 }
2108 }
2109 }
2110
2111 return false;
2112 }
2113
ProcessContentInserted(LocalAccessible * aContainer,const nsTArray<nsCOMPtr<nsIContent>> * aNodes)2114 void DocAccessible::ProcessContentInserted(
2115 LocalAccessible* aContainer, const nsTArray<nsCOMPtr<nsIContent>>* aNodes) {
2116 // Process insertions if the container accessible is still in tree.
2117 if (!aContainer->IsInDocument()) {
2118 return;
2119 }
2120
2121 // If new root content has been inserted then update it.
2122 if (aContainer == this) {
2123 UpdateRootElIfNeeded();
2124 }
2125
2126 InsertIterator iter(aContainer, aNodes);
2127 if (!iter.Next()) {
2128 return;
2129 }
2130
2131 #ifdef A11Y_LOG
2132 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
2133 #endif
2134
2135 TreeMutation mt(aContainer);
2136 do {
2137 LocalAccessible* parent = iter.Child()->LocalParent();
2138 if (parent) {
2139 LocalAccessible* previousSibling = iter.ChildBefore();
2140 if (parent != aContainer ||
2141 iter.Child()->LocalPrevSibling() != previousSibling) {
2142 if (previousSibling && previousSibling->LocalParent() != aContainer) {
2143 // previousSibling hasn't been moved into aContainer yet.
2144 // previousSibling should be later in the insertion list, so the tree
2145 // will get adjusted when we process it later.
2146 MOZ_DIAGNOSTIC_ASSERT(parent == aContainer,
2147 "Child moving to new parent, but previous "
2148 "sibling in wrong parent");
2149 continue;
2150 }
2151 #ifdef A11Y_LOG
2152 logging::TreeInfo("relocating accessible", 0, "old parent", parent,
2153 "new parent", aContainer, "child", iter.Child(),
2154 nullptr);
2155 #endif
2156 MoveChild(iter.Child(), aContainer,
2157 previousSibling ? previousSibling->IndexInParent() + 1 : 0);
2158 }
2159 continue;
2160 }
2161
2162 if (aContainer->InsertAfter(iter.Child(), iter.ChildBefore())) {
2163 #ifdef A11Y_LOG
2164 logging::TreeInfo("accessible was inserted", 0, "container", aContainer,
2165 "child", iter.Child(), nullptr);
2166 #endif
2167
2168 CreateSubtree(iter.Child());
2169 mt.AfterInsertion(iter.Child());
2170 continue;
2171 }
2172
2173 MOZ_ASSERT_UNREACHABLE("accessible was rejected");
2174 iter.Rejected();
2175 } while (iter.Next());
2176
2177 mt.Done();
2178
2179 #ifdef A11Y_LOG
2180 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
2181 #endif
2182
2183 FireEventsOnInsertion(aContainer);
2184 }
2185
ProcessContentInserted(LocalAccessible * aContainer,nsIContent * aNode)2186 void DocAccessible::ProcessContentInserted(LocalAccessible* aContainer,
2187 nsIContent* aNode) {
2188 if (!aContainer->IsInDocument()) {
2189 return;
2190 }
2191
2192 #ifdef A11Y_LOG
2193 logging::TreeInfo("children before insertion", logging::eVerbose, aContainer);
2194 #endif
2195
2196 #ifdef A11Y_LOG
2197 logging::TreeInfo("traversing an inserted node", logging::eVerbose,
2198 "container", aContainer, "node", aNode);
2199 #endif
2200
2201 TreeWalker walker(aContainer);
2202 if (aContainer->IsAcceptableChild(aNode) && walker.Seek(aNode)) {
2203 LocalAccessible* child = GetAccessible(aNode);
2204 if (!child) {
2205 child = GetAccService()->CreateAccessible(aNode, aContainer);
2206 }
2207
2208 if (child) {
2209 TreeMutation mt(aContainer);
2210 if (!aContainer->InsertAfter(child, walker.Prev())) {
2211 return;
2212 }
2213 CreateSubtree(child);
2214 mt.AfterInsertion(child);
2215 mt.Done();
2216
2217 FireEventsOnInsertion(aContainer);
2218 }
2219 }
2220
2221 #ifdef A11Y_LOG
2222 logging::TreeInfo("children after insertion", logging::eVerbose, aContainer);
2223 #endif
2224 }
2225
FireEventsOnInsertion(LocalAccessible * aContainer)2226 void DocAccessible::FireEventsOnInsertion(LocalAccessible* aContainer) {
2227 // Check to see if change occurred inside an alert, and fire an EVENT_ALERT
2228 // if it did.
2229 if (aContainer->IsAlert() || aContainer->IsInsideAlert()) {
2230 LocalAccessible* ancestor = aContainer;
2231 do {
2232 if (ancestor->IsAlert()) {
2233 FireDelayedEvent(nsIAccessibleEvent::EVENT_ALERT, ancestor);
2234 break;
2235 }
2236 } while ((ancestor = ancestor->LocalParent()));
2237 }
2238 }
2239
ContentRemoved(LocalAccessible * aChild)2240 void DocAccessible::ContentRemoved(LocalAccessible* aChild) {
2241 LocalAccessible* parent = aChild->LocalParent();
2242 MOZ_DIAGNOSTIC_ASSERT(parent, "Unattached accessible from tree");
2243
2244 #ifdef A11Y_LOG
2245 logging::TreeInfo("process content removal", 0, "container", parent, "child",
2246 aChild, nullptr);
2247 #endif
2248
2249 // XXX: event coalescence may kill us
2250 RefPtr<LocalAccessible> kungFuDeathGripChild(aChild);
2251
2252 TreeMutation mt(parent);
2253 mt.BeforeRemoval(aChild);
2254
2255 if (aChild->IsDefunct()) {
2256 MOZ_ASSERT_UNREACHABLE("Event coalescence killed the accessible");
2257 mt.Done();
2258 return;
2259 }
2260
2261 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Alive but unparented #1");
2262
2263 if (aChild->IsRelocated()) {
2264 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(parent);
2265 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2266 owned->RemoveElement(aChild);
2267 if (owned->Length() == 0) {
2268 mARIAOwnsHash.Remove(parent);
2269 }
2270 }
2271 MOZ_DIAGNOSTIC_ASSERT(aChild->LocalParent(), "Unparented #2");
2272 parent->RemoveChild(aChild);
2273 UncacheChildrenInSubtree(aChild);
2274
2275 mt.Done();
2276 }
2277
ContentRemoved(nsIContent * aContentNode)2278 void DocAccessible::ContentRemoved(nsIContent* aContentNode) {
2279 // If child node is not accessible then look for its accessible children.
2280 LocalAccessible* acc = GetAccessible(aContentNode);
2281 if (acc) {
2282 ContentRemoved(acc);
2283 }
2284
2285 dom::AllChildrenIterator iter =
2286 dom::AllChildrenIterator(aContentNode, nsIContent::eAllChildren, true);
2287 while (nsIContent* childNode = iter.GetNextChild()) {
2288 ContentRemoved(childNode);
2289 }
2290
2291 // If this node has a shadow root, remove its explicit children too.
2292 // The host node may be removed after the shadow root was attached, and
2293 // before we asynchronously prune the light DOM and construct the shadow DOM.
2294 // If this is a case where the node does not have its own accessible, we will
2295 // not recurse into its current children, so we need to use an
2296 // ExplicitChildIterator in order to get its accessible children in the light
2297 // DOM, since they are not accessible anymore via AllChildrenIterator.
2298 if (aContentNode->GetShadowRoot()) {
2299 dom::ExplicitChildIterator iter = dom::ExplicitChildIterator(aContentNode);
2300 while (nsIContent* childNode = iter.GetNextChild()) {
2301 ContentRemoved(childNode);
2302 }
2303 }
2304 }
2305
RelocateARIAOwnedIfNeeded(nsIContent * aElement)2306 bool DocAccessible::RelocateARIAOwnedIfNeeded(nsIContent* aElement) {
2307 if (!aElement->HasID()) return false;
2308
2309 AttrRelProviders* list = GetRelProviders(
2310 aElement->AsElement(), nsDependentAtomString(aElement->GetID()));
2311 if (list) {
2312 for (uint32_t idx = 0; idx < list->Length(); idx++) {
2313 if (list->ElementAt(idx)->mRelAttr == nsGkAtoms::aria_owns) {
2314 LocalAccessible* owner = GetAccessible(list->ElementAt(idx)->mContent);
2315 if (owner) {
2316 mNotificationController->ScheduleRelocation(owner);
2317 return true;
2318 }
2319 }
2320 }
2321 }
2322
2323 return false;
2324 }
2325
DoARIAOwnsRelocation(LocalAccessible * aOwner)2326 void DocAccessible::DoARIAOwnsRelocation(LocalAccessible* aOwner) {
2327 MOZ_ASSERT(aOwner, "aOwner must be a valid pointer");
2328 MOZ_ASSERT(aOwner->Elm(), "aOwner->Elm() must be a valid pointer");
2329
2330 #ifdef A11Y_LOG
2331 logging::TreeInfo("aria owns relocation", logging::eVerbose, aOwner);
2332 #endif
2333
2334 nsTArray<RefPtr<LocalAccessible>>* owned =
2335 mARIAOwnsHash.GetOrInsertNew(aOwner);
2336
2337 IDRefsIterator iter(this, aOwner->Elm(), nsGkAtoms::aria_owns);
2338 uint32_t idx = 0;
2339 while (nsIContent* childEl = iter.NextElem()) {
2340 LocalAccessible* child = GetAccessible(childEl);
2341 auto insertIdx = aOwner->ChildCount() - owned->Length() + idx;
2342
2343 // Make an attempt to create an accessible if it wasn't created yet.
2344 if (!child) {
2345 // An owned child cannot be an ancestor of the owner.
2346 bool ok = true;
2347 bool check = true;
2348 for (LocalAccessible* parent = aOwner; parent && !parent->IsDoc();
2349 parent = parent->LocalParent()) {
2350 if (check) {
2351 if (parent->Elm()->IsInclusiveDescendantOf(childEl)) {
2352 ok = false;
2353 break;
2354 }
2355 }
2356 // We need to do the DOM descendant check again whenever the DOM
2357 // lineage changes. If parent is relocated, that means the next
2358 // ancestor will have a different DOM lineage.
2359 check = parent->IsRelocated();
2360 }
2361 if (!ok) {
2362 continue;
2363 }
2364
2365 if (aOwner->IsAcceptableChild(childEl)) {
2366 child = GetAccService()->CreateAccessible(childEl, aOwner);
2367 if (child) {
2368 TreeMutation imut(aOwner);
2369 aOwner->InsertChildAt(insertIdx, child);
2370 imut.AfterInsertion(child);
2371 imut.Done();
2372
2373 child->SetRelocated(true);
2374 owned->InsertElementAt(idx, child);
2375 idx++;
2376
2377 // Create subtree before adjusting the insertion index, since subtree
2378 // creation may alter children in the container.
2379 CreateSubtree(child);
2380 FireEventsOnInsertion(aOwner);
2381 }
2382 }
2383 continue;
2384 }
2385
2386 #ifdef A11Y_LOG
2387 logging::TreeInfo("aria owns traversal", logging::eVerbose, "candidate",
2388 child, nullptr);
2389 #endif
2390
2391 if (owned->IndexOf(child) < idx) {
2392 continue; // ignore second entry of same ID
2393 }
2394
2395 // Same child on same position, no change.
2396 if (child->LocalParent() == aOwner) {
2397 int32_t indexInParent = child->IndexInParent();
2398
2399 // The child is being placed in its current index,
2400 // eg. aria-owns='id1 id2 id3' is changed to aria-owns='id3 id2 id1'.
2401 if (indexInParent == static_cast<int32_t>(insertIdx)) {
2402 MOZ_ASSERT(child->IsRelocated(),
2403 "A child, having an index in parent from aria ownded "
2404 "indices range, has to be aria owned");
2405 MOZ_ASSERT(owned->ElementAt(idx) == child,
2406 "Unexpected child in ARIA owned array");
2407 idx++;
2408 continue;
2409 }
2410
2411 // The child is being inserted directly after its current index,
2412 // resulting in a no-move case. This will happen when a parent aria-owns
2413 // its last ordinal child:
2414 // <ul aria-owns='id2'><li id='id1'></li><li id='id2'></li></ul>
2415 if (indexInParent == static_cast<int32_t>(insertIdx) - 1) {
2416 MOZ_ASSERT(!child->IsRelocated(),
2417 "Child should be in its ordinal position");
2418 child->SetRelocated(true);
2419 owned->InsertElementAt(idx, child);
2420 idx++;
2421 continue;
2422 }
2423 }
2424
2425 MOZ_ASSERT(owned->SafeElementAt(idx) != child, "Already in place!");
2426
2427 // A new child is found, check for loops.
2428 if (child->LocalParent() != aOwner) {
2429 // Child is aria-owned by another container, skip.
2430 if (child->IsRelocated()) {
2431 continue;
2432 }
2433
2434 LocalAccessible* parent = aOwner;
2435 while (parent && parent != child && !parent->IsDoc()) {
2436 parent = parent->LocalParent();
2437 }
2438 // A referred child cannot be a parent of the owner.
2439 if (parent == child) {
2440 continue;
2441 }
2442 }
2443
2444 if (MoveChild(child, aOwner, insertIdx)) {
2445 child->SetRelocated(true);
2446 MOZ_ASSERT(owned == mARIAOwnsHash.Get(aOwner));
2447 owned = mARIAOwnsHash.GetOrInsertNew(aOwner);
2448 owned->InsertElementAt(idx, child);
2449 idx++;
2450 }
2451 }
2452
2453 // Put back children that are not seized anymore.
2454 PutChildrenBack(owned, idx);
2455 if (owned->Length() == 0) {
2456 mARIAOwnsHash.Remove(aOwner);
2457 }
2458 }
2459
PutChildrenBack(nsTArray<RefPtr<LocalAccessible>> * aChildren,uint32_t aStartIdx)2460 void DocAccessible::PutChildrenBack(
2461 nsTArray<RefPtr<LocalAccessible>>* aChildren, uint32_t aStartIdx) {
2462 MOZ_ASSERT(aStartIdx <= aChildren->Length(), "Wrong removal index");
2463
2464 for (auto idx = aStartIdx; idx < aChildren->Length(); idx++) {
2465 LocalAccessible* child = aChildren->ElementAt(idx);
2466 if (!child->IsInDocument()) {
2467 continue;
2468 }
2469
2470 // Remove the child from the owner
2471 LocalAccessible* owner = child->LocalParent();
2472 if (!owner) {
2473 NS_ERROR("Cannot put the child back. No parent, a broken tree.");
2474 continue;
2475 }
2476
2477 #ifdef A11Y_LOG
2478 logging::TreeInfo("aria owns put child back", 0, "old parent", owner,
2479 "child", child, nullptr);
2480 #endif
2481
2482 // Unset relocated flag to find an insertion point for the child.
2483 child->SetRelocated(false);
2484
2485 nsIContent* content = child->GetContent();
2486 int32_t idxInParent = -1;
2487 LocalAccessible* origContainer =
2488 AccessibleOrTrueContainer(content->GetFlattenedTreeParentNode());
2489 if (origContainer) {
2490 TreeWalker walker(origContainer);
2491 if (walker.Seek(content)) {
2492 LocalAccessible* prevChild = walker.Prev();
2493 if (prevChild) {
2494 idxInParent = prevChild->IndexInParent() + 1;
2495 MOZ_DIAGNOSTIC_ASSERT(origContainer == prevChild->LocalParent(),
2496 "Broken tree");
2497 origContainer = prevChild->LocalParent();
2498 } else {
2499 idxInParent = 0;
2500 }
2501 }
2502 }
2503
2504 // The child may have already be in its ordinal place for 2 reasons:
2505 // 1. It was the last ordinal child, and the first aria-owned child.
2506 // given: <ul id="list" aria-owns="b"><li id="a"></li><li
2507 // id="b"></li></ul> after load: $("list").setAttribute("aria-owns", "");
2508 // 2. The preceding adopted children were just reclaimed, eg:
2509 // given: <ul id="list"><li id="b"></li></ul>
2510 // after load: $("list").setAttribute("aria-owns", "a b");
2511 // later: $("list").setAttribute("aria-owns", "");
2512 if (origContainer != owner || child->IndexInParent() != idxInParent) {
2513 DebugOnly<bool> moved = MoveChild(child, origContainer, idxInParent);
2514 MOZ_ASSERT(moved, "Failed to put child back.");
2515 } else {
2516 MOZ_ASSERT(!child->LocalPrevSibling() ||
2517 !child->LocalPrevSibling()->IsRelocated(),
2518 "No relocated child should appear before this one");
2519 MOZ_ASSERT(!child->LocalNextSibling() ||
2520 child->LocalNextSibling()->IsRelocated(),
2521 "No ordinal child should appear after this one");
2522 }
2523 }
2524
2525 aChildren->RemoveLastElements(aChildren->Length() - aStartIdx);
2526 }
2527
MoveChild(LocalAccessible * aChild,LocalAccessible * aNewParent,int32_t aIdxInParent)2528 bool DocAccessible::MoveChild(LocalAccessible* aChild,
2529 LocalAccessible* aNewParent,
2530 int32_t aIdxInParent) {
2531 MOZ_ASSERT(aChild, "No child");
2532 MOZ_ASSERT(aChild->LocalParent(), "No parent");
2533 // We can't guarantee MoveChild works correctly for accessibilities storing
2534 // children outside mChildren.
2535 MOZ_ASSERT(
2536 aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()),
2537 "Wrong insertion point for a moving child");
2538
2539 LocalAccessible* curParent = aChild->LocalParent();
2540
2541 if (!aNewParent->IsAcceptableChild(aChild->GetContent())) {
2542 return false;
2543 }
2544
2545 #ifdef A11Y_LOG
2546 logging::TreeInfo("move child", 0, "old parent", curParent, "new parent",
2547 aNewParent, "child", aChild, nullptr);
2548 #endif
2549
2550 // Forget aria-owns info in case of ARIA owned element. The caller is expected
2551 // to update it if needed.
2552 if (aChild->IsRelocated()) {
2553 aChild->SetRelocated(false);
2554 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(curParent);
2555 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2556 owned->RemoveElement(aChild);
2557 if (owned->Length() == 0) {
2558 mARIAOwnsHash.Remove(curParent);
2559 }
2560 }
2561
2562 NotificationController::MoveGuard mguard(mNotificationController);
2563
2564 if (curParent == aNewParent) {
2565 MOZ_ASSERT(aChild->IndexInParent() != aIdxInParent, "No move case");
2566 curParent->RelocateChild(aIdxInParent, aChild);
2567
2568 #ifdef A11Y_LOG
2569 logging::TreeInfo("move child: parent tree after", logging::eVerbose,
2570 curParent);
2571 #endif
2572 return true;
2573 }
2574
2575 // If the child cannot be re-inserted into the tree, then make sure to remove
2576 // it from its present parent and then shutdown it.
2577 bool hasInsertionPoint =
2578 (aIdxInParent >= 0) &&
2579 (aIdxInParent <= static_cast<int32_t>(aNewParent->mChildren.Length()));
2580
2581 TreeMutation rmut(curParent);
2582 rmut.BeforeRemoval(aChild, hasInsertionPoint && TreeMutation::kNoShutdown);
2583 curParent->RemoveChild(aChild);
2584 rmut.Done();
2585
2586 // No insertion point for the child.
2587 if (!hasInsertionPoint) {
2588 return true;
2589 }
2590
2591 TreeMutation imut(aNewParent);
2592 aNewParent->InsertChildAt(aIdxInParent, aChild);
2593 imut.AfterInsertion(aChild);
2594 imut.Done();
2595
2596 #ifdef A11Y_LOG
2597 logging::TreeInfo("move child: old parent tree after", logging::eVerbose,
2598 curParent);
2599 logging::TreeInfo("move child: new parent tree after", logging::eVerbose,
2600 aNewParent);
2601 #endif
2602
2603 return true;
2604 }
2605
CacheChildrenInSubtree(LocalAccessible * aRoot,LocalAccessible ** aFocusedAcc)2606 void DocAccessible::CacheChildrenInSubtree(LocalAccessible* aRoot,
2607 LocalAccessible** aFocusedAcc) {
2608 // If the accessible is focused then report a focus event after all related
2609 // mutation events.
2610 if (aFocusedAcc && !*aFocusedAcc &&
2611 FocusMgr()->HasDOMFocus(aRoot->GetContent())) {
2612 *aFocusedAcc = aRoot;
2613 }
2614
2615 LocalAccessible* root =
2616 aRoot->IsHTMLCombobox() ? aRoot->LocalFirstChild() : aRoot;
2617 if (root->KidsFromDOM()) {
2618 TreeMutation mt(root, TreeMutation::kNoEvents);
2619 TreeWalker walker(root);
2620 while (LocalAccessible* child = walker.Next()) {
2621 if (child->IsBoundToParent()) {
2622 MoveChild(child, root, root->mChildren.Length());
2623 continue;
2624 }
2625
2626 root->AppendChild(child);
2627 mt.AfterInsertion(child);
2628
2629 CacheChildrenInSubtree(child, aFocusedAcc);
2630 }
2631 mt.Done();
2632 }
2633
2634 // Fire events for ARIA elements.
2635 if (!aRoot->HasARIARole()) {
2636 return;
2637 }
2638
2639 // XXX: we should delay document load complete event if the ARIA document
2640 // has aria-busy.
2641 roles::Role role = aRoot->ARIARole();
2642 if (!aRoot->IsDoc() &&
2643 (role == roles::DIALOG || role == roles::NON_NATIVE_DOCUMENT)) {
2644 FireDelayedEvent(nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE, aRoot);
2645 }
2646 }
2647
UncacheChildrenInSubtree(LocalAccessible * aRoot)2648 void DocAccessible::UncacheChildrenInSubtree(LocalAccessible* aRoot) {
2649 aRoot->mStateFlags |= eIsNotInDocument;
2650 RemoveDependentIDsFor(aRoot);
2651
2652 nsTArray<RefPtr<LocalAccessible>>* owned = mARIAOwnsHash.Get(aRoot);
2653 uint32_t count = aRoot->ContentChildCount();
2654 for (uint32_t idx = 0; idx < count; idx++) {
2655 LocalAccessible* child = aRoot->ContentChildAt(idx);
2656
2657 if (child->IsRelocated()) {
2658 MOZ_ASSERT(owned, "IsRelocated flag is out of sync with mARIAOwnsHash");
2659 owned->RemoveElement(child);
2660 if (owned->Length() == 0) {
2661 mARIAOwnsHash.Remove(aRoot);
2662 owned = nullptr;
2663 }
2664 }
2665
2666 // Removing this accessible from the document doesn't mean anything about
2667 // accessibles for subdocuments, so skip removing those from the tree.
2668 if (!child->IsDoc()) {
2669 UncacheChildrenInSubtree(child);
2670 }
2671 }
2672
2673 if (aRoot->IsNodeMapEntry() &&
2674 mNodeToAccessibleMap.Get(aRoot->GetNode()) == aRoot) {
2675 mNodeToAccessibleMap.Remove(aRoot->GetNode());
2676 }
2677 }
2678
ShutdownChildrenInSubtree(LocalAccessible * aAccessible)2679 void DocAccessible::ShutdownChildrenInSubtree(LocalAccessible* aAccessible) {
2680 // Traverse through children and shutdown them before this accessible. When
2681 // child gets shutdown then it removes itself from children array of its
2682 // parent. Use jdx index to process the cases if child is not attached to the
2683 // parent and as result doesn't remove itself from its children.
2684 uint32_t count = aAccessible->ContentChildCount();
2685 for (uint32_t idx = 0, jdx = 0; idx < count; idx++) {
2686 LocalAccessible* child = aAccessible->ContentChildAt(jdx);
2687 if (!child->IsBoundToParent()) {
2688 NS_ERROR("Parent refers to a child, child doesn't refer to parent!");
2689 jdx++;
2690 }
2691
2692 // Don't cross document boundaries. The outerdoc shutdown takes care about
2693 // its subdocument.
2694 if (!child->IsDoc()) ShutdownChildrenInSubtree(child);
2695 }
2696
2697 UnbindFromDocument(aAccessible);
2698 }
2699
IsLoadEventTarget() const2700 bool DocAccessible::IsLoadEventTarget() const {
2701 nsCOMPtr<nsIDocShellTreeItem> treeItem = mDocumentNode->GetDocShell();
2702 NS_ASSERTION(treeItem, "No document shell for document!");
2703
2704 nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
2705 treeItem->GetInProcessParent(getter_AddRefs(parentTreeItem));
2706
2707 // Not a root document.
2708 if (parentTreeItem) {
2709 // Return true if it's either:
2710 // a) tab document;
2711 nsCOMPtr<nsIDocShellTreeItem> rootTreeItem;
2712 treeItem->GetInProcessRootTreeItem(getter_AddRefs(rootTreeItem));
2713 if (parentTreeItem == rootTreeItem) return true;
2714
2715 // b) frame/iframe document and its parent document is not in loading state
2716 // Note: we can get notifications while document is loading (and thus
2717 // while there's no parent document yet).
2718 DocAccessible* parentDoc = ParentDocument();
2719 return parentDoc && parentDoc->HasLoadState(eCompletelyLoaded);
2720 }
2721
2722 // It's content (not chrome) root document.
2723 return (treeItem->ItemType() == nsIDocShellTreeItem::typeContent);
2724 }
2725
SetIPCDoc(DocAccessibleChild * aIPCDoc)2726 void DocAccessible::SetIPCDoc(DocAccessibleChild* aIPCDoc) {
2727 MOZ_ASSERT(!mIPCDoc || !aIPCDoc, "Clobbering an attached IPCDoc!");
2728 mIPCDoc = aIPCDoc;
2729 }
2730
DispatchScrollingEvent(nsINode * aTarget,uint32_t aEventType)2731 void DocAccessible::DispatchScrollingEvent(nsINode* aTarget,
2732 uint32_t aEventType) {
2733 LocalAccessible* acc = GetAccessible(aTarget);
2734 if (!acc) {
2735 return;
2736 }
2737
2738 nsIFrame* frame = acc->GetFrame();
2739 if (!frame) {
2740 // Although the accessible had a frame at scroll time, it may now be gone
2741 // because of display: contents.
2742 return;
2743 }
2744
2745 LayoutDevicePoint scrollPoint;
2746 LayoutDeviceRect scrollRange;
2747 nsIScrollableFrame* sf = acc == this
2748 ? mPresShell->GetRootScrollFrameAsScrollable()
2749 : frame->GetScrollTargetFrame();
2750
2751 // If there is no scrollable frame, it's likely a scroll in a popup, like
2752 // <select>. Just send an event with no scroll info. The scroll info
2753 // is currently only used on Android, and popups are rendered natively
2754 // there.
2755 if (sf) {
2756 int32_t appUnitsPerDevPixel =
2757 mPresShell->GetPresContext()->AppUnitsPerDevPixel();
2758 scrollPoint = LayoutDevicePoint::FromAppUnits(sf->GetScrollPosition(),
2759 appUnitsPerDevPixel) *
2760 mPresShell->GetResolution();
2761
2762 scrollRange = LayoutDeviceRect::FromAppUnits(sf->GetScrollRange(),
2763 appUnitsPerDevPixel);
2764 scrollRange.ScaleRoundOut(mPresShell->GetResolution());
2765 }
2766
2767 RefPtr<AccEvent> event =
2768 new AccScrollingEvent(aEventType, acc, scrollPoint.x, scrollPoint.y,
2769 scrollRange.width, scrollRange.height);
2770 nsEventShell::FireEvent(event);
2771 }
2772
ARIAActiveDescendantIDMaybeMoved(dom::Element * aElm)2773 void DocAccessible::ARIAActiveDescendantIDMaybeMoved(dom::Element* aElm) {
2774 nsINode* focusNode = FocusMgr()->FocusedDOMNode();
2775 // The focused element must be within this document.
2776 if (!focusNode || focusNode->OwnerDoc() != mDocumentNode) {
2777 return;
2778 }
2779
2780 dom::Element* focusElm = nullptr;
2781 if (focusNode == mDocumentNode) {
2782 // The document is focused, so look for aria-activedescendant on the
2783 // body/root.
2784 focusElm = Elm();
2785 if (!focusElm) {
2786 return;
2787 }
2788 } else {
2789 MOZ_ASSERT(focusNode->IsElement());
2790 focusElm = focusNode->AsElement();
2791 }
2792
2793 // Check if the focus has aria-activedescendant and whether
2794 // it refers to the id just set on aElm.
2795 nsAutoString id;
2796 aElm->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id);
2797 if (!focusElm->AttrValueIs(kNameSpaceID_None,
2798 nsGkAtoms::aria_activedescendant, id,
2799 eCaseMatters)) {
2800 return;
2801 }
2802
2803 // The aria-activedescendant target has probably changed.
2804 LocalAccessible* acc = GetAccessibleEvenIfNotInMapOrContainer(focusNode);
2805 if (!acc) {
2806 return;
2807 }
2808
2809 // The active descendant might have just been inserted and may not be in the
2810 // tree yet. Therefore, schedule this async to ensure the tree is up to date.
2811 mNotificationController->ScheduleNotification<DocAccessible, LocalAccessible>(
2812 this, &DocAccessible::ARIAActiveDescendantChanged, acc);
2813 }
2814
SetRoleMapEntryForDoc(dom::Element * aElement)2815 void DocAccessible::SetRoleMapEntryForDoc(dom::Element* aElement) {
2816 const nsRoleMapEntry* entry = aria::GetRoleMap(aElement);
2817 if (!entry || entry->role == roles::APPLICATION ||
2818 entry->role == roles::DIALOG ||
2819 // Role alert isn't valid on the body element according to the ARIA spec,
2820 // but it's useful for our UI; e.g. the WebRTC sharing indicator.
2821 (entry->role == roles::ALERT &&
2822 !nsCoreUtils::IsContentDocument(mDocumentNode))) {
2823 SetRoleMapEntry(entry);
2824 return;
2825 }
2826 // No other ARIA roles are valid on body elements.
2827 SetRoleMapEntry(nullptr);
2828 }
2829
GetAccessible(nsINode * aNode) const2830 LocalAccessible* DocAccessible::GetAccessible(nsINode* aNode) const {
2831 return aNode == mDocumentNode ? const_cast<DocAccessible*>(this)
2832 : mNodeToAccessibleMap.Get(aNode);
2833 }
2834