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/. */
5 #include "FocusManager.h"
7 #include "LocalAccessible-inl.h"
8 #include "DocAccessible-inl.h"
9 #include "nsAccessibilityService.h"
10 #include "nsEventShell.h"
12 #include "nsFocusManager.h"
13 #include "mozilla/a11y/DocAccessibleParent.h"
14 #include "mozilla/EventStateManager.h"
15 #include "mozilla/dom/Element.h"
16 #include "mozilla/dom/BrowsingContext.h"
17 #include "mozilla/dom/BrowserParent.h"
22 FocusManager::FocusManager() {}
24 FocusManager::~FocusManager() {}
26 LocalAccessible
* FocusManager::FocusedLocalAccessible() const {
27 MOZ_ASSERT(NS_IsMainThread());
29 if (mActiveItem
->IsDefunct()) {
30 MOZ_ASSERT_UNREACHABLE("Stored active item is unbound from document");
37 if (nsAccessibilityService::IsShutdown()) {
38 // We might try to get or create a DocAccessible below, which isn't safe (or
39 // useful) if the accessibility service is shutting down.
43 nsINode
* focusedNode
= FocusedDOMNode();
46 GetAccService()->GetDocAccessible(focusedNode
->OwnerDoc());
47 return doc
? doc
->GetAccessibleEvenIfNotInMapOrContainer(focusedNode
)
54 Accessible
* FocusManager::FocusedAccessible() const {
56 // It's not safe to call FocusedLocalAccessible() except on the main thread.
57 // Android might query RemoteAccessibles on the UI thread, which might call
58 // FocusedAccessible(). Never try to get the focused LocalAccessible in this
60 if (NS_IsMainThread()) {
61 if (Accessible
* focusedAcc
= FocusedLocalAccessible()) {
65 nsAccessibilityService::GetAndroidMonitor().AssertCurrentThreadOwns();
67 return mFocusedRemoteDoc
? mFocusedRemoteDoc
->GetFocusedAcc() : nullptr;
69 if (Accessible
* focusedAcc
= FocusedLocalAccessible()) {
73 if (!XRE_IsParentProcess()) {
74 // DocAccessibleParent's don't exist in the content
75 // process, so we can't return anything useful if this
80 nsFocusManager
* focusManagerDOM
= nsFocusManager::GetFocusManager();
81 if (!focusManagerDOM
) {
85 // If we call GetFocusedBrowsingContext from the chrome process
86 // it returns the BrowsingContext for the focused _window_, which
87 // is not helpful here. Instead use GetFocusedBrowsingContextInChrome
88 // which returns the content BrowsingContext that has focus.
89 dom::BrowsingContext
* focusedContext
=
90 focusManagerDOM
->GetFocusedBrowsingContextInChrome();
92 DocAccessibleParent
* focusedDoc
=
93 DocAccessibleParent::GetFrom(focusedContext
);
94 return focusedDoc
? focusedDoc
->GetFocusedAcc() : nullptr;
95 #endif // defined(ANDROID)
98 bool FocusManager::IsFocusWithin(const Accessible
* aContainer
) const {
99 Accessible
* child
= FocusedAccessible();
101 if (child
== aContainer
) return true;
103 child
= child
->Parent();
108 FocusManager::FocusDisposition
FocusManager::IsInOrContainsFocus(
109 const LocalAccessible
* aAccessible
) const {
110 LocalAccessible
* focus
= FocusedLocalAccessible();
111 if (!focus
) return eNone
;
114 if (focus
== aAccessible
) return eFocused
;
116 // If contains the focus.
117 LocalAccessible
* child
= focus
->LocalParent();
119 if (child
== aAccessible
) return eContainsFocus
;
121 child
= child
->LocalParent();
124 // If contained by focus.
125 child
= aAccessible
->LocalParent();
127 if (child
== focus
) return eContainedByFocus
;
129 child
= child
->LocalParent();
135 bool FocusManager::WasLastFocused(const LocalAccessible
* aAccessible
) const {
136 return mLastFocus
== aAccessible
;
139 void FocusManager::NotifyOfDOMFocus(nsISupports
* aTarget
) {
141 if (logging::IsEnabled(logging::eFocus
)) {
142 logging::FocusNotificationTarget("DOM focus", "Target", aTarget
);
146 mActiveItem
= nullptr;
148 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(aTarget
));
150 DocAccessible
* document
=
151 GetAccService()->GetDocAccessible(targetNode
->OwnerDoc());
153 // Set selection listener for focused element.
154 if (targetNode
->IsElement()) {
155 SelectionMgr()->SetControlSelectionListener(targetNode
->AsElement());
158 document
->HandleNotification
<FocusManager
, nsINode
>(
159 this, &FocusManager::ProcessDOMFocus
, targetNode
);
164 void FocusManager::NotifyOfDOMBlur(nsISupports
* aTarget
) {
166 if (logging::IsEnabled(logging::eFocus
)) {
167 logging::FocusNotificationTarget("DOM blur", "Target", aTarget
);
171 mActiveItem
= nullptr;
173 // If DOM document stays focused then fire accessible focus event to process
174 // the case when no element within this DOM document will be focused.
175 nsCOMPtr
<nsINode
> targetNode(do_QueryInterface(aTarget
));
176 if (targetNode
&& targetNode
->OwnerDoc() == FocusedDOMDocument()) {
177 dom::Document
* DOMDoc
= targetNode
->OwnerDoc();
178 DocAccessible
* document
= GetAccService()->GetDocAccessible(DOMDoc
);
180 // Clear selection listener for previously focused element.
181 if (targetNode
->IsElement()) {
182 SelectionMgr()->ClearControlSelectionListener();
185 document
->HandleNotification
<FocusManager
, nsINode
>(
186 this, &FocusManager::ProcessDOMFocus
, DOMDoc
);
191 void FocusManager::ActiveItemChanged(LocalAccessible
* aItem
,
192 bool aCheckIfActive
) {
194 if (logging::IsEnabled(logging::eFocus
)) {
195 logging::FocusNotificationTarget("active item changed", "Item", aItem
);
199 // Nothing changed, happens for XUL trees and HTML selects.
200 if (aItem
&& aItem
== mActiveItem
) {
204 mActiveItem
= nullptr;
206 if (aItem
&& aCheckIfActive
) {
207 LocalAccessible
* widget
= aItem
->ContainerWidget();
209 if (logging::IsEnabled(logging::eFocus
)) logging::ActiveWidget(widget
);
211 if (!widget
|| !widget
->IsActiveWidget() || !widget
->AreItemsOperable()) {
217 // If mActiveItem is null we may need to shift a11y focus back to a remote
218 // document. For example, when combobox popup is closed, then
219 // the focus should be moved back to the combobox.
220 if (!mActiveItem
&& XRE_IsParentProcess()) {
221 dom::BrowserParent
* browser
= dom::BrowserParent::GetFocused();
223 a11y::DocAccessibleParent
* dap
= browser
->GetTopLevelDocAccessible();
225 Unused
<< dap
->SendRestoreFocus();
230 // If active item is changed then fire accessible focus event on it, otherwise
231 // if there's no an active item then fire focus event to accessible having
233 LocalAccessible
* target
= FocusedLocalAccessible();
235 DispatchFocusEvent(target
->Document(), target
);
239 void FocusManager::ForceFocusEvent() {
240 nsINode
* focusedNode
= FocusedDOMNode();
242 DocAccessible
* document
=
243 GetAccService()->GetDocAccessible(focusedNode
->OwnerDoc());
245 document
->HandleNotification
<FocusManager
, nsINode
>(
246 this, &FocusManager::ProcessDOMFocus
, focusedNode
);
251 void FocusManager::DispatchFocusEvent(DocAccessible
* aDocument
,
252 LocalAccessible
* aTarget
) {
253 MOZ_ASSERT(aDocument
, "No document for focused accessible!");
255 RefPtr
<AccEvent
> event
=
256 new AccEvent(nsIAccessibleEvent::EVENT_FOCUS
, aTarget
, eAutoDetect
,
257 AccEvent::eCoalesceOfSameType
);
258 aDocument
->FireDelayedEvent(event
);
259 mLastFocus
= aTarget
;
260 if (mActiveItem
!= aTarget
) {
261 // This new focus overrides the stored active item, so clear the active
262 // item. Among other things, the old active item might die.
263 mActiveItem
= nullptr;
267 if (logging::IsEnabled(logging::eFocus
)) logging::FocusDispatched(aTarget
);
272 void FocusManager::ProcessDOMFocus(nsINode
* aTarget
) {
274 if (logging::IsEnabled(logging::eFocus
)) {
275 logging::FocusNotificationTarget("process DOM focus", "Target", aTarget
);
279 DocAccessible
* document
=
280 GetAccService()->GetDocAccessible(aTarget
->OwnerDoc());
281 if (!document
) return;
283 LocalAccessible
* target
=
284 document
->GetAccessibleEvenIfNotInMapOrContainer(aTarget
);
286 // Check if still focused. Otherwise we can end up with storing the active
287 // item for control that isn't focused anymore.
288 nsINode
* focusedNode
= FocusedDOMNode();
289 if (!focusedNode
) return;
291 LocalAccessible
* DOMFocus
=
292 document
->GetAccessibleEvenIfNotInMapOrContainer(focusedNode
);
293 if (target
!= DOMFocus
) return;
295 LocalAccessible
* activeItem
= target
->CurrentItem();
297 mActiveItem
= activeItem
;
301 DispatchFocusEvent(document
, target
);
305 void FocusManager::ProcessFocusEvent(AccEvent
* aEvent
) {
306 MOZ_ASSERT(aEvent
->GetEventType() == nsIAccessibleEvent::EVENT_FOCUS
,
307 "Focus event is expected!");
309 // Emit focus event if event target is the active item. Otherwise then check
310 // if it's still focused and then update active item and emit focus event.
311 LocalAccessible
* target
= aEvent
->GetAccessible();
312 MOZ_ASSERT(!target
->IsDefunct());
313 if (target
!= mActiveItem
) {
314 // Check if still focused. Otherwise we can end up with storing the active
315 // item for control that isn't focused anymore.
316 DocAccessible
* document
= aEvent
->Document();
317 nsINode
* focusedNode
= FocusedDOMNode();
318 if (!focusedNode
) return;
320 LocalAccessible
* DOMFocus
=
321 document
->GetAccessibleEvenIfNotInMapOrContainer(focusedNode
);
322 if (target
!= DOMFocus
) return;
324 LocalAccessible
* activeItem
= target
->CurrentItem();
326 mActiveItem
= activeItem
;
328 MOZ_ASSERT(!target
->IsDefunct());
332 // Fire menu start/end events for ARIA menus.
333 if (target
->IsARIARole(nsGkAtoms::menuitem
)) {
334 // The focus was moved into menu.
335 LocalAccessible
* ARIAMenubar
= nullptr;
336 for (LocalAccessible
* parent
= target
->LocalParent(); parent
;
337 parent
= parent
->LocalParent()) {
338 if (parent
->IsARIARole(nsGkAtoms::menubar
)) {
339 ARIAMenubar
= parent
;
343 // Go up in the parent chain of the menu hierarchy.
344 if (!parent
->IsARIARole(nsGkAtoms::menuitem
) &&
345 !parent
->IsARIARole(nsGkAtoms::menu
)) {
350 if (ARIAMenubar
!= mActiveARIAMenubar
) {
351 // Leaving ARIA menu. Fire menu_end event on current menubar.
352 if (mActiveARIAMenubar
) {
353 RefPtr
<AccEvent
> menuEndEvent
=
354 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END
, mActiveARIAMenubar
,
355 aEvent
->FromUserInput());
356 nsEventShell::FireEvent(menuEndEvent
);
359 mActiveARIAMenubar
= ARIAMenubar
;
361 // Entering ARIA menu. Fire menu_start event.
362 if (mActiveARIAMenubar
) {
363 RefPtr
<AccEvent
> menuStartEvent
=
364 new AccEvent(nsIAccessibleEvent::EVENT_MENU_START
,
365 mActiveARIAMenubar
, aEvent
->FromUserInput());
366 nsEventShell::FireEvent(menuStartEvent
);
369 } else if (mActiveARIAMenubar
) {
370 // Focus left a menu. Fire menu_end event.
371 RefPtr
<AccEvent
> menuEndEvent
=
372 new AccEvent(nsIAccessibleEvent::EVENT_MENU_END
, mActiveARIAMenubar
,
373 aEvent
->FromUserInput());
374 nsEventShell::FireEvent(menuEndEvent
);
376 mActiveARIAMenubar
= nullptr;
380 if (logging::IsEnabled(logging::eFocus
)) {
381 logging::FocusNotificationTarget("fire focus event", "Target", target
);
385 // Reset cached caret value. The cache will be updated upon processing the
386 // next caret move event. This ensures that we will return the correct caret
387 // offset before the caret move event is handled.
388 SelectionMgr()->ResetCaretOffset();
390 RefPtr
<AccEvent
> focusEvent
= new AccEvent(nsIAccessibleEvent::EVENT_FOCUS
,
391 target
, aEvent
->FromUserInput());
392 nsEventShell::FireEvent(focusEvent
);
394 if (NS_WARN_IF(target
->IsDefunct())) {
395 // target died during nsEventShell::FireEvent.
399 // Fire scrolling_start event when the document receives the focus if it has
400 // an anchor jump. If an accessible within the document receive the focus
401 // then null out the anchor jump because it no longer applies.
402 DocAccessible
* targetDocument
= target
->Document();
403 MOZ_ASSERT(targetDocument
);
404 LocalAccessible
* anchorJump
= targetDocument
->AnchorJump();
406 if (target
== targetDocument
|| target
->IsNonInteractive()) {
407 // XXX: bug 625699, note in some cases the node could go away before we
408 // we receive focus event, for example if the node is removed from DOM.
409 nsEventShell::FireEvent(nsIAccessibleEvent::EVENT_SCROLLING_START
,
410 anchorJump
, aEvent
->FromUserInput());
412 targetDocument
->SetAnchorJump(nullptr);
416 nsINode
* FocusManager::FocusedDOMNode() const {
417 nsFocusManager
* DOMFocusManager
= nsFocusManager::GetFocusManager();
418 nsIContent
* focusedElm
= DOMFocusManager
->GetFocusedElement();
419 nsIFrame
* focusedFrame
= focusedElm
? focusedElm
->GetPrimaryFrame() : nullptr;
420 // DOM elements retain their focused state when they get styled as display:
421 // none/content or visibility: hidden. We should treat those cases as if those
422 // elements were removed, and focus on doc.
423 if (focusedFrame
&& focusedFrame
->StyleVisibility()->IsVisible()) {
424 // Print preview documents don't get DocAccessibles, but we still want a11y
425 // focus to go somewhere useful. Therefore, we allow a11y focus to land on
426 // the OuterDocAccessible in this case.
427 // Note that this code only handles remote print preview documents.
428 if (EventStateManager::IsTopLevelRemoteTarget(focusedElm
) &&
429 focusedElm
->AsElement()->HasAttribute(u
"printpreview"_ns
)) {
432 // No focus on remote target elements like xul:browser having DOM focus and
433 // residing in chrome process because it means an element in content process
434 // keeps the focus. Similarly, suppress focus on OOP iframes because an
435 // element in another content process should now have the focus.
436 if (EventStateManager::IsRemoteTarget(focusedElm
)) {
442 // Otherwise the focus can be on DOM document.
443 dom::BrowsingContext
* context
= DOMFocusManager
->GetFocusedBrowsingContext();
445 // GetDocShell will return null if the document isn't in our process.
446 nsIDocShell
* shell
= context
->GetDocShell();
448 return shell
->GetDocument();
452 // Focus isn't in this process.
456 dom::Document
* FocusManager::FocusedDOMDocument() const {
457 nsINode
* focusedNode
= FocusedDOMNode();
458 return focusedNode
? focusedNode
->OwnerDoc() : nullptr;
462 } // namespace mozilla