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