1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 cr.define('cr.ui', function() {
7 * Constructor for FocusManager singleton. Checks focus of elements to ensure
8 * that elements in "background" pages (i.e., those in a dialog that is not
9 * the topmost overlay) do not receive focus.
12 function FocusManager() {
15 FocusManager.prototype = {
17 * Whether focus is being transferred backward or forward through the DOM.
21 focusDirBackwards_: false,
24 * Determines whether the |child| is a descendant of |parent| in the page's
26 * @param {Element} parent The parent element to test.
27 * @param {Element} child The child element to test.
28 * @return {boolean} True if |child| is a descendant of |parent|.
31 isDescendantOf_: function(parent, child) {
35 current = current.parentNode;
36 if (typeof(current) == 'undefined' ||
37 typeof(current) == 'null' ||
38 current === document.body) {
40 } else if (current === parent) {
49 * Returns the parent element containing all elements which should be
50 * allowed to receive focus.
51 * @return {Element} The element containing focusable elements.
53 getFocusParent: function() {
58 * Returns the elements on the page capable of receiving focus.
59 * @return {Array.Element} The focusable elements.
61 getFocusableElements_: function() {
62 var focusableDiv = this.getFocusParent();
64 // Create a TreeWalker object to traverse the DOM from |focusableDiv|.
65 var treeWalker = document.createTreeWalker(
67 NodeFilter.SHOW_ELEMENT,
68 { acceptNode: function(node) {
69 var style = window.getComputedStyle(node);
70 // Reject all hidden nodes. FILTER_REJECT also rejects these
71 // nodes' children, so non-hidden elements that are descendants of
72 // hidden <div>s will correctly be rejected.
73 if (node.hidden || style.display == 'none' ||
74 style.visibility == 'hidden') {
75 return NodeFilter.FILTER_REJECT;
78 // Skip nodes that cannot receive focus. FILTER_SKIP does not
79 // cause this node's children also to be skipped.
80 if (node.disabled || node.tabIndex < 0)
81 return NodeFilter.FILTER_SKIP;
83 // Accept nodes that are non-hidden and focusable.
84 return NodeFilter.FILTER_ACCEPT;
90 while (treeWalker.nextNode())
91 focusable.push(treeWalker.currentNode);
97 * Dispatches an 'elementFocused' event to notify an element that it has
98 * received focus. When focus wraps around within the a page, only the
99 * element that has focus after the wrapping receives an 'elementFocused'
100 * event. This differs from the native 'focus' event which is received by
101 * an element outside the page first, followed by a 'focus' on an element
102 * within the page after the FocusManager has intervened.
103 * @param {Element} element The element that has received focus.
106 dispatchFocusEvent_: function(element) {
107 cr.dispatchSimpleEvent(element, 'elementFocused', true, false);
111 * Attempts to focus the appropriate element in the current dialog.
114 setFocus_: function() {
115 // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
116 // and has caused the focus to be transferred backward, outside of the
117 // current dialog. In this case, loop around and try to focus the last
118 // element of the dialog; otherwise, try to focus the first element of the
120 var focusableElements = this.getFocusableElements_();
121 var element = this.focusDirBackwards_ ? focusableElements.pop() :
122 focusableElements.shift();
125 this.dispatchFocusEvent_(element);
130 * Attempts to focus the first element in the current dialog.
132 focusFirstElement: function() {
133 this.focusFirstElement_();
137 * Handler for focus events on the page.
138 * @param {Event} event The focus event.
141 onDocumentFocus_: function(event) {
142 // If the element being focused is a descendant of the currently visible
143 // page, focus is valid.
144 if (this.isDescendantOf_(this.getFocusParent(), event.target)) {
145 this.dispatchFocusEvent_(event.target);
149 // The target of the focus event is not in the topmost visible page and
150 // should not be focused.
153 // Attempt to wrap around focus within the current page.
158 * Handler for keydown events on the page.
159 * @param {Event} event The keydown event.
162 onDocumentKeyDown_: function(event) {
163 /** @const */ var tabKeyCode = 9;
165 if (event.keyCode == tabKeyCode) {
166 // If the "Shift" key is held, focus is being transferred backward in
168 this.focusDirBackwards_ = event.shiftKey ? true : false;
173 * Initializes the FocusManager by listening for events in the document.
175 initialize: function() {
176 document.addEventListener('focus', this.onDocumentFocus_.bind(this),
178 document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this),
184 * Disable mouse-focus for button controls.
185 * Button form controls are mouse-focusable since Chromium 30. We want the
186 * old behavior in some WebUI pages.
188 FocusManager.disableMouseFocusOnButtons = function() {
189 document.addEventListener('mousedown', function(event) {
190 var node = event.target;
191 var tagName = node.tagName;
192 if (tagName != 'BUTTON' && tagName != 'INPUT') {
194 node = node.parentNode;
195 if (!node || node.nodeType != Node.ELEMENT_NODE)
197 } while (node.tagName != 'BUTTON');
199 var type = node.type;
200 if (type == 'button' || type == 'reset' || type == 'submit' ||
201 type == 'radio' || type == 'checkbox') {
202 if (document.activeElement != node)
203 document.activeElement.blur();
205 // Focus the current window so that if the active element is in another
206 // window, it is deactivated.
208 event.preventDefault();
214 FocusManager: FocusManager,