1// Copyright 2014 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 5/** 6 * @fileoverview Implements the setFocus function. 7 */ 8 9goog.provide('cvox.Focuser'); 10 11goog.require('cvox.ChromeVoxEventSuspender'); 12goog.require('cvox.DomUtil'); 13 14 15/** 16 * Sets the browser focus to the targetNode or its closest ancestor that is 17 * focusable. 18 * 19 * @param {Node} targetNode The node to move the browser focus to. 20 * @param {boolean=} opt_focusDescendants Whether or not we check descendants 21 * of the target node to see if they are focusable. If true, sets focus on the 22 * first focusable descendant. If false, only sets focus on the targetNode or 23 * its closest ancestor. Default is false. 24 */ 25cvox.Focuser.setFocus = function(targetNode, opt_focusDescendants) { 26 // Save the selection because Chrome will lose it if there's a focus or blur. 27 var sel = window.getSelection(); 28 var range; 29 if (sel.rangeCount > 0) { 30 range = sel.getRangeAt(0); 31 } 32 // Blur the currently-focused element if the target node is not a descendant. 33 if (document.activeElement && 34 !cvox.DomUtil.isDescendantOfNode(targetNode, document.activeElement)) { 35 document.activeElement.blur(); 36 } 37 38 // Video elements should always be focusable. 39 if (targetNode && (targetNode.constructor == HTMLVideoElement)) { 40 if (!cvox.DomUtil.isFocusable(targetNode)) { 41 targetNode.setAttribute('tabIndex', 0); 42 } 43 } 44 45 if (opt_focusDescendants && !cvox.DomUtil.isFocusable(targetNode)) { 46 var focusableDescendant = cvox.DomUtil.findFocusableDescendant(targetNode); 47 if (focusableDescendant) { 48 targetNode = focusableDescendant; 49 } 50 } else { 51 // Search up the parent chain until a focusable node is found. 52 while (targetNode && !cvox.DomUtil.isFocusable(targetNode)) { 53 targetNode = targetNode.parentNode; 54 } 55 } 56 57 // If we found something focusable, focus it - otherwise, blur it. 58 if (cvox.DomUtil.isFocusable(targetNode)) { 59 // Don't let the instance of ChromeVox in the parent focus iframe children 60 // - instead, let the instance of ChromeVox in the iframe focus itself to 61 // avoid getting trapped in iframes that have no ChromeVox in them. 62 // This self focusing is performed by calling window.focus() in 63 // cvox.NavigationManager.prototype.addInterframeListener_ 64 if (targetNode.tagName != 'IFRAME') { 65 // setTimeout must be used because there's a bug (in Chrome, I think) 66 // with .focus() which causes the page to be redrawn incorrectly if 67 // not in setTimeout. 68 if (cvox.ChromeVoxEventSuspender.areEventsSuspended()) { 69 if (cvox.Focuser.shouldEnterSuspendEvents_(targetNode)) { 70 cvox.ChromeVoxEventSuspender.enterSuspendEvents(); 71 } 72 window.setTimeout(function() { 73 targetNode.focus(); 74 cvox.ChromeVoxEventSuspender.exitSuspendEvents(); 75 }, 0); 76 } 77 else { 78 window.setTimeout(function() { 79 targetNode.focus(); 80 }, 0); 81 } 82 } 83 } else if (document.activeElement && 84 document.activeElement.tagName != 'BODY') { 85 document.activeElement.blur(); 86 } 87 88 // Restore the selection, unless the focused item is a text box. 89 if (cvox.DomUtil.isInputTypeText(targetNode)) { 90 targetNode.select(); 91 } else if (range) { 92 sel.removeAllRanges(); 93 sel.addRange(range); 94 } 95}; 96 97/** 98 * Rules for whether or not enterSuspendEvents should be called. 99 * In general, we should not enterSuspendEvents if the targetNode will get some 100 * special handlers attached when a focus event is received for it; otherwise, 101 * the special handlers will not get attached. 102 * 103 * @param {Node} targetNode The node that is being focused. 104 * @return {boolean} True if enterSuspendEvents should be called. 105 */ 106cvox.Focuser.shouldEnterSuspendEvents_ = function(targetNode){ 107 if (targetNode.constructor && targetNode.constructor == HTMLVideoElement) { 108 return false; 109 } 110 if (targetNode.hasAttribute) { 111 switch (targetNode.getAttribute('type')) { 112 case 'time': 113 case 'date': 114 case 'week': 115 case 'month': 116 return false; 117 } 118 } 119 return true; 120}; 121