• 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 {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|.
29     * @private
30     */
31    isDescendantOf_: function(parent, child) {
32      var current = child;
33
34      while (current) {
35        current = current.parentNode;
36        if (typeof(current) == 'undefined' ||
37            typeof(current) == 'null' ||
38            current === document.body) {
39          return false;
40        } else if (current === parent) {
41          return true;
42        }
43      }
44
45      return false;
46    },
47
48    /**
49     * Returns the parent element containing all elements which should be
50     * allowed to receive focus.
51     * @return {Element} The element containing focusable elements.
52     */
53    getFocusParent: function() {
54      return document.body;
55    },
56
57    /**
58     * Returns the elements on the page capable of receiving focus.
59     * @return {Array.Element} The focusable elements.
60     */
61    getFocusableElements_: function() {
62      var focusableDiv = this.getFocusParent();
63
64      // Create a TreeWalker object to traverse the DOM from |focusableDiv|.
65      var treeWalker = document.createTreeWalker(
66          focusableDiv,
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;
76              }
77
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;
82
83              // Accept nodes that are non-hidden and focusable.
84              return NodeFilter.FILTER_ACCEPT;
85            }
86          },
87          false);
88
89      var focusable = [];
90      while (treeWalker.nextNode())
91        focusable.push(treeWalker.currentNode);
92
93      return focusable;
94    },
95
96    /**
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.
104     * @private
105     */
106    dispatchFocusEvent_: function(element) {
107      cr.dispatchSimpleEvent(element, 'elementFocused', true, false);
108    },
109
110    /**
111     * Attempts to focus the appropriate element in the current dialog.
112     * @private
113     */
114    setFocus_: function() {
115      var element = this.selectFocusableElement_();
116      if (element) {
117        element.focus();
118        this.dispatchFocusEvent_(element);
119      }
120    },
121
122    /**
123     * Attempts to focus the first element in the current dialog.
124     */
125    focusFirstElement: function() {
126      this.focusFirstElement_();
127    },
128
129    /**
130     * Selects first appropriate focusable element according to the
131     * current focus direction and element type.  If it is a radio button,
132     * checked one is selected from the group.
133     * @private
134     */
135    selectFocusableElement_: function() {
136      // If |this.focusDirBackwards_| is true, the user has pressed "Shift+Tab"
137      // and has caused the focus to be transferred backward, outside of the
138      // current dialog. In this case, loop around and try to focus the last
139      // element of the dialog; otherwise, try to focus the first element of the
140      // dialog.
141      var focusableElements = this.getFocusableElements_();
142      var element = this.focusDirBackwards_ ? focusableElements.pop() :
143                                              focusableElements.shift();
144      if (!element)
145        return null;
146      if (element.tagName != 'INPUT' || element.type != 'radio' ||
147          element.name == '') {
148        return element;
149      }
150      if (!element.checked) {
151        for (var i = 0; i < focusableElements.length; i++) {
152          var e = focusableElements[i];
153          if (e && e.tagName == 'INPUT' && e.type == 'radio' &&
154              e.name == element.name && e.checked) {
155            element = e;
156            break;
157          }
158        }
159      }
160      return element;
161    },
162
163    /**
164     * Handler for focus events on the page.
165     * @param {Event} event The focus event.
166     * @private
167     */
168    onDocumentFocus_: function(event) {
169      // If the element being focused is a descendant of the currently visible
170      // page, focus is valid.
171      if (this.isDescendantOf_(this.getFocusParent(), event.target)) {
172        this.dispatchFocusEvent_(event.target);
173        return;
174      }
175
176      // Focus event handlers for descendant elements might dispatch another
177      // focus event.
178      event.stopPropagation();
179
180      // The target of the focus event is not in the topmost visible page and
181      // should not be focused.
182      event.target.blur();
183
184      // Attempt to wrap around focus within the current page.
185      this.setFocus_();
186    },
187
188    /**
189     * Handler for keydown events on the page.
190     * @param {Event} event The keydown event.
191     * @private
192     */
193    onDocumentKeyDown_: function(event) {
194      /** @const */ var tabKeyCode = 9;
195
196      if (event.keyCode == tabKeyCode) {
197        // If the "Shift" key is held, focus is being transferred backward in
198        // the page.
199        this.focusDirBackwards_ = event.shiftKey ? true : false;
200      }
201    },
202
203    /**
204     * Initializes the FocusManager by listening for events in the document.
205     */
206    initialize: function() {
207      document.addEventListener('focus', this.onDocumentFocus_.bind(this),
208          true);
209      document.addEventListener('keydown', this.onDocumentKeyDown_.bind(this),
210          true);
211    },
212  };
213
214  /**
215   * Disable mouse-focus for button controls.
216   * Button form controls are mouse-focusable since Chromium 30.  We want the
217   * old behavior in some WebUI pages.
218   */
219  FocusManager.disableMouseFocusOnButtons = function() {
220    document.addEventListener('mousedown', function(event) {
221      var node = event.target;
222      var tagName = node.tagName;
223      if (tagName != 'BUTTON' && tagName != 'INPUT') {
224        do {
225          node = node.parentNode;
226          if (!node || node.nodeType != Node.ELEMENT_NODE)
227            return;
228        } while (node.tagName != 'BUTTON');
229      }
230      var type = node.type;
231      if (type == 'button' || type == 'reset' || type == 'submit' ||
232          type == 'radio' || type == 'checkbox') {
233        if (document.activeElement != node)
234          document.activeElement.blur();
235
236        // Focus the current window so that if the active element is in another
237        // window, it is deactivated.
238        window.focus();
239        event.preventDefault();
240      }
241    }, false);
242  };
243
244  return {
245    FocusManager: FocusManager,
246  };
247});
248