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