• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5cr.define('cr.ui', function() {
6  /**
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.
10   * @constructor
11   */
12  function FocusManager() {
13  }
14
15  FocusManager.prototype = {
16    /**
17     * Whether focus is being transferred backward or forward through the DOM.
18     * @type {boolean}
19     * @private
20     */
21    focusDirBackwards_: false,
22
23    /**
24     * Determines whether the |child| is a descendant of |parent| in the page's
25     * DOM.
26     * @param {Node} parent The parent element to test.
27     * @param {Node} child The child element to test.
28     * @return {boolean} True if |child| is a descendant of |parent|.
29     * @private
30     */
31    isDescendantOf_: function(parent, child) {
32      return !!parent && !(parent === child) && parent.contains(child);
33    },
34
35    /**
36     * Returns the parent element containing all elements which should be
37     * allowed to receive focus.
38     * @return {Element} The element containing focusable elements.
39     */
40    getFocusParent: function() {
41      return document.body;
42    },
43
44    /**
45     * Returns the elements on the page capable of receiving focus.
46     * @return {Array.<Element>} The focusable elements.
47     */
48    getFocusableElements_: function() {
49      var focusableDiv = this.getFocusParent();
50
51      // Create a TreeWalker object to traverse the DOM from |focusableDiv|.
52      var treeWalker = document.createTreeWalker(
53          focusableDiv,
54          NodeFilter.SHOW_ELEMENT,
55          /** @type {NodeFilter} */
56          ({
57            acceptNode: function(node) {
58              var style = window.getComputedStyle(node);
59              // Reject all hidden nodes. FILTER_REJECT also rejects these
60              // nodes' children, so non-hidden elements that are descendants of
61              // hidden <div>s will correctly be rejected.
62              if (node.hidden || style.display == 'none' ||
63                  style.visibility == 'hidden') {
64                return NodeFilter.FILTER_REJECT;
65              }
66
67              // Skip nodes that cannot receive focus. FILTER_SKIP does not
68              // cause this node's children also to be skipped.
69              if (node.disabled || node.tabIndex < 0)
70                return NodeFilter.FILTER_SKIP;
71
72              // Accept nodes that are non-hidden and focusable.
73              return NodeFilter.FILTER_ACCEPT;
74            }
75          }),
76          false);
77
78      var focusable = [];
79      while (treeWalker.nextNode())
80        focusable.push(treeWalker.currentNode);
81
82      return focusable;
83    },
84
85    /**
86     * Dispatches an 'elementFocused' event to notify an element that it has
87     * received focus. When focus wraps around within the a page, only the
88     * element that has focus after the wrapping receives an 'elementFocused'
89     * event. This differs from the native 'focus' event which is received by
90     * an element outside the page first, followed by a 'focus' on an element
91     * within the page after the FocusManager has intervened.
92     * @param {EventTarget} element The element that has received focus.
93     * @private
94     */
95    dispatchFocusEvent_: function(element) {
96      cr.dispatchSimpleEvent(element, 'elementFocused', true, false);
97    },
98
99    /**
100     * Attempts to focus the appropriate element in the current dialog.
101     * @private
102     */
103    setFocus_: function() {
104      var element = this.selectFocusableElement_();
105      if (element) {
106        element.focus();
107        this.dispatchFocusEvent_(element);
108      }
109    },
110
111    /**
112     * Selects first appropriate focusable element according to the
113     * current focus direction and element type.  If it is a radio button,
114     * checked one is selected from the group.
115     * @private
116     */
117    selectFocusableElement_: function() {
118      // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
119      // and has caused the focus to be transferred backward, outside of the
120      // current dialog. In this case, loop around and try to focus the last
121      // element of the dialog; otherwise, try to focus the first element of the
122      // dialog.
123      var focusableElements = this.getFocusableElements_();
124      var element = this.focusDirBackwards_ ? focusableElements.pop() :
125                                              focusableElements.shift();
126      if (!element)
127        return null;
128      if (element.tagName != 'INPUT' || element.type != 'radio' ||
129          element.name == '') {
130        return element;
131      }
132      if (!element.checked) {
133        for (var i = 0; i < focusableElements.length; i++) {
134          var e = focusableElements[i];
135          if (e && e.tagName == 'INPUT' && e.type == 'radio' &&
136              e.name == element.name && e.checked) {
137            element = e;
138            break;
139          }
140        }
141      }
142      return element;
143    },
144
145    /**
146     * Handler for focus events on the page.
147     * @param {Event} event The focus event.
148     * @private
149     */
150    onDocumentFocus_: function(event) {
151      // If the element being focused is a descendant of the currently visible
152      // page, focus is valid.
153      var targetNode = /** @type {Node} */(event.target);
154      if (this.isDescendantOf_(this.getFocusParent(), targetNode)) {
155        this.dispatchFocusEvent_(event.target);
156        return;
157      }
158
159      // Focus event handlers for descendant elements might dispatch another
160      // focus event.
161      event.stopPropagation();
162
163      // The target of the focus event is not in the topmost visible page and
164      // should not be focused.
165      event.target.blur();
166
167      // Attempt to wrap around focus within the current page.
168      this.setFocus_();
169    },
170
171    /**
172     * Handler for keydown events on the page.
173     * @param {Event} event The keydown event.
174     * @private
175     */
176    onDocumentKeyDown_: function(event) {
177      /** @const */ var tabKeyCode = 9;
178
179      if (event.keyCode == tabKeyCode) {
180        // If the "Shift" key is held, focus is being transferred backward in
181        // the page.
182        this.focusDirBackwards_ = event.shiftKey ? true : false;
183      }
184    },
185
186    /**
187     * Initializes the FocusManager by listening for events in the document.
188     */
189    initialize: function() {
190      document.addEventListener('focus', this.onDocumentFocus_.bind(this),
191          true);
192      document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this),
193          true);
194    },
195  };
196
197  /**
198   * Disable mouse-focus for button controls.
199   * Button form controls are mouse-focusable since Chromium 30.  We want the
200   * old behavior in some WebUI pages.
201   */
202  FocusManager.disableMouseFocusOnButtons = function() {
203    document.addEventListener('mousedown', function(event) {
204      if (event.defaultPrevented)
205        return;
206      var node = event.target;
207      var tagName = node.tagName;
208      if (tagName != 'BUTTON' && tagName != 'INPUT') {
209        do {
210          node = node.parentNode;
211          if (!node || node.nodeType != Node.ELEMENT_NODE)
212            return;
213        } while (node.tagName != 'BUTTON');
214      }
215      var type = node.type;
216      if (type == 'button' || type == 'reset' || type == 'submit' ||
217          type == 'radio' || type == 'checkbox') {
218        if (document.activeElement != node)
219          document.activeElement.blur();
220
221        // Focus the current window so that if the active element is in another
222        // window, it is deactivated.
223        window.focus();
224        event.preventDefault();
225      }
226    }, false);
227  };
228
229  return {
230    FocusManager: FocusManager,
231  };
232});
233