1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 #include "FocusManager.h"
6 
7 #include "LocalAccessible-inl.h"
8 #include "AccIterator.h"
9 #include "DocAccessible-inl.h"
10 #include "nsAccessibilityService.h"
11 #include "nsAccUtils.h"
12 #include "nsEventShell.h"
13 #include "Role.h"
14 
15 #include "nsFocusManager.h"
16 #include "mozilla/a11y/DocAccessibleParent.h"
17 #include "mozilla/a11y/DocManager.h"
18 #include "mozilla/EventStateManager.h"
19 #include "mozilla/dom/Element.h"
20 #include "mozilla/dom/BrowsingContext.h"
21 #include "mozilla/dom/BrowserParent.h"
22 
23 namespace mozilla {
24 namespace a11y {
25 
FocusManager()26 FocusManager::FocusManager() {}
27 
~FocusManager()28 FocusManager::~FocusManager() {}
29 
FocusedAccessible() const30 LocalAccessible* FocusManager::FocusedAccessible() const {
31   if (mActiveItem) {
32     if (mActiveItem->IsDefunct()) {
33       MOZ_ASSERT_UNREACHABLE("Stored active item is unbound from document");
34       return nullptr;
35     }
36 
37     return mActiveItem;
38   }
39 
40   nsINode* focusedNode = FocusedDOMNode();
41   if (focusedNode) {
42     DocAccessible* doc =
43         GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
44     return doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode)
45                : nullptr;
46   }
47 
48   return nullptr;
49 }
50 
IsFocused(const LocalAccessible * aAccessible) const51 bool FocusManager::IsFocused(const LocalAccessible* aAccessible) const {
52   if (mActiveItem) return mActiveItem == aAccessible;
53 
54   nsINode* focusedNode = FocusedDOMNode();
55   if (focusedNode) {
56     // XXX: Before getting an accessible for node having a DOM focus make sure
57     // they belong to the same document because it can trigger unwanted document
58     // accessible creation for temporary about:blank document. Without this
59     // peculiarity we would end up with plain implementation based on
60     // FocusedAccessible() method call. Make sure this issue is fixed in
61     // bug 638465.
62     if (focusedNode->OwnerDoc() == aAccessible->GetNode()->OwnerDoc()) {
63       DocAccessible* doc =
64           GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
65       return aAccessible ==
66              (doc ? doc->GetAccessibleEvenIfNotInMapOrContainer(focusedNode)
67                   : nullptr);
68     }
69   }
70   return false;
71 }
72 
IsFocusWithin(const LocalAccessible * aContainer) const73 bool FocusManager::IsFocusWithin(const LocalAccessible* aContainer) const {
74   LocalAccessible* child = FocusedAccessible();
75   while (child) {
76     if (child == aContainer) return true;
77 
78     child = child->LocalParent();
79   }
80   return false;
81 }
82 
IsInOrContainsFocus(const LocalAccessible * aAccessible) const83 FocusManager::FocusDisposition FocusManager::IsInOrContainsFocus(
84     const LocalAccessible* aAccessible) const {
85   LocalAccessible* focus = FocusedAccessible();
86   if (!focus) return eNone;
87 
88   // If focused.
89   if (focus == aAccessible) return eFocused;
90 
91   // If contains the focus.
92   LocalAccessible* child = focus->LocalParent();
93   while (child) {
94     if (child == aAccessible) return eContainsFocus;
95 
96     child = child->LocalParent();
97   }
98 
99   // If contained by focus.
100   child = aAccessible->LocalParent();
101   while (child) {
102     if (child == focus) return eContainedByFocus;
103 
104     child = child->LocalParent();
105   }
106 
107   return eNone;
108 }
109 
WasLastFocused(const LocalAccessible * aAccessible) const110 bool FocusManager::WasLastFocused(const LocalAccessible* aAccessible) const {
111   return mLastFocus == aAccessible;
112 }
113 
NotifyOfDOMFocus(nsISupports * aTarget)114 void FocusManager::NotifyOfDOMFocus(nsISupports* aTarget) {
115 #ifdef A11Y_LOG
116   if (logging::IsEnabled(logging::eFocus)) {
117     logging::FocusNotificationTarget("DOM focus", "Target", aTarget);
118   }
119 #endif
120 
121   mActiveItem = nullptr;
122 
123   nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget));
124   if (targetNode) {
125     DocAccessible* document =
126         GetAccService()->GetDocAccessible(targetNode->OwnerDoc());
127     if (document) {
128       // Set selection listener for focused element.
129       if (targetNode->IsElement()) {
130         SelectionMgr()->SetControlSelectionListener(targetNode->AsElement());
131       }
132 
133       document->HandleNotification<FocusManager, nsINode>(
134           this, &FocusManager::ProcessDOMFocus, targetNode);
135     }
136   }
137 }
138 
NotifyOfDOMBlur(nsISupports * aTarget)139 void FocusManager::NotifyOfDOMBlur(nsISupports* aTarget) {
140 #ifdef A11Y_LOG
141   if (logging::IsEnabled(logging::eFocus)) {
142     logging::FocusNotificationTarget("DOM blur", "Target", aTarget);
143   }
144 #endif
145 
146   mActiveItem = nullptr;
147 
148   // If DOM document stays focused then fire accessible focus event to process
149   // the case when no element within this DOM document will be focused.
150   nsCOMPtr<nsINode> targetNode(do_QueryInterface(aTarget));
151   if (targetNode && targetNode->OwnerDoc() == FocusedDOMDocument()) {
152     dom::Document* DOMDoc = targetNode->OwnerDoc();
153     DocAccessible* document = GetAccService()->GetDocAccessible(DOMDoc);
154     if (document) {
155       // Clear selection listener for previously focused element.
156       if (targetNode->IsElement()) {
157         SelectionMgr()->ClearControlSelectionListener();
158       }
159 
160       document->HandleNotification<FocusManager, nsINode>(
161           this, &FocusManager::ProcessDOMFocus, DOMDoc);
162     }
163   }
164 }
165 
ActiveItemChanged(LocalAccessible * aItem,bool aCheckIfActive)166 void FocusManager::ActiveItemChanged(LocalAccessible* aItem,
167                                      bool aCheckIfActive) {
168 #ifdef A11Y_LOG
169   if (logging::IsEnabled(logging::eFocus)) {
170     logging::FocusNotificationTarget("active item changed", "Item", aItem);
171   }
172 #endif
173 
174   // Nothing changed, happens for XUL trees and HTML selects.
175   if (aItem && aItem == mActiveItem) return;
176 
177   mActiveItem = nullptr;
178 
179   if (aItem && aCheckIfActive) {
180     LocalAccessible* widget = aItem->ContainerWidget();
181 #ifdef A11Y_LOG
182     if (logging::IsEnabled(logging::eFocus)) logging::ActiveWidget(widget);
183 #endif
184     if (!widget || !widget->IsActiveWidget() || !widget->AreItemsOperable()) {
185       return;
186     }
187   }
188   mActiveItem = aItem;
189 
190   // If mActiveItem is null we may need to shift a11y focus back to a remote
191   // document. For example, when combobox popup is closed, then
192   // the focus should be moved back to the combobox.
193   if (!mActiveItem && XRE_IsParentProcess()) {
194     dom::BrowserParent* browser = dom::BrowserParent::GetFocused();
195     if (browser) {
196       a11y::DocAccessibleParent* dap = browser->GetTopLevelDocAccessible();
197       if (dap) {
198         Unused << dap->SendRestoreFocus();
199       }
200     }
201   }
202 
203   // If active item is changed then fire accessible focus event on it, otherwise
204   // if there's no an active item then fire focus event to accessible having
205   // DOM focus.
206   LocalAccessible* target = FocusedAccessible();
207   if (target) {
208     DispatchFocusEvent(target->Document(), target);
209   }
210 }
211 
ForceFocusEvent()212 void FocusManager::ForceFocusEvent() {
213   nsINode* focusedNode = FocusedDOMNode();
214   if (focusedNode) {
215     DocAccessible* document =
216         GetAccService()->GetDocAccessible(focusedNode->OwnerDoc());
217     if (document) {
218       document->HandleNotification<FocusManager, nsINode>(
219           this, &FocusManager::ProcessDOMFocus, focusedNode);
220     }
221   }
222 }
223 
DispatchFocusEvent(DocAccessible * aDocument,LocalAccessible * aTarget)224 void FocusManager::DispatchFocusEvent(DocAccessible* aDocument,
225                                       LocalAccessible* aTarget) {
226   MOZ_ASSERT(aDocument, "No document for focused accessible!");
227   if (aDocument) {
228     RefPtr<AccEvent> event =
229         new AccEvent(nsIAccessibleEvent::EVENT_FOCUS, aTarget, eAutoDetect,
230                      AccEvent::eCoalesceOfSameType);
231     aDocument->FireDelayedEvent(event);
232     mLastFocus = aTarget;
233 
234 #ifdef A11Y_LOG
235     if (logging::IsEnabled(logging::eFocus)) logging::FocusDispatched(aTarget);
236 #endif
237   }
238 }
239 
ProcessDOMFocus(nsINode * aTarget)240 void FocusManager::ProcessDOMFocus(nsINode* aTarget) {
241 #ifdef A11Y_LOG
242   if (logging::IsEnabled(logging::eFocus)) {
243     logging::FocusNotificationTarget("process DOM focus", "Target", aTarget);
244   }
245 #endif
246 
247   DocAccessible* document =
248       GetAccService()->GetDocAccessible(aTarget->OwnerDoc());
249   if (!document) return;
250 
251   LocalAccessible* target =
252       document->GetAccessibleEvenIfNotInMapOrContainer(aTarget);
253   if (target) {
254     // Check if still focused. Otherwise we can end up with storing the active
255     // item for control that isn't focused anymore.
256     nsINode* focusedNode = FocusedDOMNode();
257     if (!focusedNode) return;
258 
259     LocalAccessible* DOMFocus =
260         document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode);
261     if (target != DOMFocus) return;
262 
263     LocalAccessible* activeItem = target->CurrentItem();
264     if (activeItem) {
265       mActiveItem = activeItem;
266       target = activeItem;
267     }
268 
269     DispatchFocusEvent(document, target);
270   }
271 }
272 
ProcessFocusEvent(AccEvent * aEvent)273 void FocusManager::ProcessFocusEvent(AccEvent* aEvent) {
274   MOZ_ASSERT(aEvent->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS,
275              "Focus event is expected!");
276 
277   // Emit focus event if event target is the active item. Otherwise then check
278   // if it's still focused and then update active item and emit focus event.
279   LocalAccessible* target = aEvent->GetAccessible();
280   MOZ_ASSERT(!target->IsDefunct());
281   if (target != mActiveItem) {
282     // Check if still focused. Otherwise we can end up with storing the active
283     // item for control that isn't focused anymore.
284     DocAccessible* document = aEvent->Document();
285     nsINode* focusedNode = FocusedDOMNode();
286     if (!focusedNode) return;
287 
288     LocalAccessible* DOMFocus =
289         document->GetAccessibleEvenIfNotInMapOrContainer(focusedNode);
290     if (target != DOMFocus) return;
291 
292     LocalAccessible* activeItem = target->CurrentItem();
293     if (activeItem) {
294       mActiveItem = activeItem;
295       target = activeItem;
296       MOZ_ASSERT(!target->IsDefunct());
297     }
298   }
299 
300   // Fire menu start/end events for ARIA menus.
301   if (target->IsARIARole(nsGkAtoms::menuitem)) {
302     // The focus was moved into menu.
303     LocalAccessible* ARIAMenubar = nullptr;
304     for (LocalAccessible* parent = target->LocalParent(); parent;
305          parent = parent->LocalParent()) {
306       if (parent->IsARIARole(nsGkAtoms::menubar)) {
307         ARIAMenubar = parent;
308         break;
309       }
310 
311       // Go up in the parent chain of the menu hierarchy.
312       if (!parent->IsARIARole(nsGkAtoms::menuitem) &&
313           !parent->IsARIARole(nsGkAtoms::menu)) {
314         break;
315       }
316     }
317 
318     if (ARIAMenubar != mActiveARIAMenubar) {
319       // Leaving ARIA menu. Fire menu_end event on current menubar.
320       if (mActiveARIAMenubar) {
321         RefPtr<AccEvent> menuEndEvent =
322             new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar,
323                          aEvent->FromUserInput());
324         nsEventShell::FireEvent(menuEndEvent);
325       }
326 
327       mActiveARIAMenubar = ARIAMenubar;
328 
329       // Entering ARIA menu. Fire menu_start event.
330       if (mActiveARIAMenubar) {
331         RefPtr<AccEvent> menuStartEvent =
332             new AccEvent(nsIAccessibleEvent::EVENT_MENU_START,
333                          mActiveARIAMenubar, aEvent->FromUserInput());
334         nsEventShell::FireEvent(menuStartEvent);
335       }
336     }
337   } else if (mActiveARIAMenubar) {
338     // Focus left a menu. Fire menu_end event.
339     RefPtr<AccEvent> menuEndEvent =
340         new AccEvent(nsIAccessibleEvent::EVENT_MENU_END, mActiveARIAMenubar,
341                      aEvent->FromUserInput());
342     nsEventShell::FireEvent(menuEndEvent);
343 
344     mActiveARIAMenubar = nullptr;
345   }
346 
347 #ifdef A11Y_LOG
348   if (logging::IsEnabled(logging::eFocus)) {
349     logging::FocusNotificationTarget("fire focus event", "Target", target);
350   }
351 #endif
352 
353   // Reset cached caret value. The cache will be updated upon processing the
354   // next caret move event. This ensures that we will return the correct caret
355   // offset before the caret move event is handled.
356   SelectionMgr()->ResetCaretOffset();
357 
358   RefPtr<AccEvent> focusEvent = new AccEvent(nsIAccessibleEvent::EVENT_FOCUS,
359                                              target, aEvent->FromUserInput());
360   nsEventShell::FireEvent(focusEvent);
361 
362   if (NS_WARN_IF(target->IsDefunct())) {
363     // target died during nsEventShell::FireEvent.
364     return;
365   }
366 
367   // Fire scrolling_start event when the document receives the focus if it has
368   // an anchor jump. If an accessible within the document receive the focus
369   // then null out the anchor jump because it no longer applies.
370   DocAccessible* targetDocument = target->Document();
371   MOZ_ASSERT(targetDocument);
372   LocalAccessible* anchorJump = targetDocument->AnchorJump();
373   if (anchorJump) {
374     if (target == targetDocument) {
375       // XXX: bug 625699, note in some cases the node could go away before we
376       // we receive focus event, for example if the node is removed from DOM.
377       nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START,
378                               anchorJump, aEvent->FromUserInput());
379     }
380     targetDocument->SetAnchorJump(nullptr);
381   }
382 }
383 
FocusedDOMNode() const384 nsINode* FocusManager::FocusedDOMNode() const {
385   nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
386   nsIContent* focusedElm = DOMFocusManager->GetFocusedElement();
387   nsIFrame* focusedFrame = focusedElm ? focusedElm->GetPrimaryFrame() : nullptr;
388   // DOM elements retain their focused state when they get styled as display:
389   // none/content or visibility: hidden. We should treat those cases as if those
390   // elements were removed, and focus on doc.
391   if (focusedFrame && focusedFrame->StyleVisibility()->IsVisible()) {
392     // Print preview documents don't get DocAccessibles, but we still want a11y
393     // focus to go somewhere useful. Therefore, we allow a11y focus to land on
394     // the OuterDocAccessible in this case.
395     // Note that this code only handles remote print preview documents.
396     if (EventStateManager::IsTopLevelRemoteTarget(focusedElm) &&
397         focusedElm->AsElement()->HasAttribute(u"printpreview"_ns)) {
398       return focusedElm;
399     }
400     // No focus on remote target elements like xul:browser having DOM focus and
401     // residing in chrome process because it means an element in content process
402     // keeps the focus. Similarly, suppress focus on OOP iframes because an
403     // element in another content process should now have the focus.
404     if (EventStateManager::IsRemoteTarget(focusedElm)) {
405       return nullptr;
406     }
407     return focusedElm;
408   }
409 
410   // Otherwise the focus can be on DOM document.
411   dom::BrowsingContext* context = DOMFocusManager->GetFocusedBrowsingContext();
412   if (context) {
413     // GetDocShell will return null if the document isn't in our process.
414     nsIDocShell* shell = context->GetDocShell();
415     if (shell) {
416       return shell->GetDocument();
417     }
418   }
419 
420   // Focus isn't in this process.
421   return nullptr;
422 }
423 
FocusedDOMDocument() const424 dom::Document* FocusManager::FocusedDOMDocument() const {
425   nsINode* focusedNode = FocusedDOMNode();
426   return focusedNode ? focusedNode->OwnerDoc() : nullptr;
427 }
428 
429 }  // namespace a11y
430 }  // namespace mozilla
431