1<!doctype html> 2<!-- 3@license 4Copyright (c) 2016 The Polymer Project Authors. All rights reserved. 5This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 6The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 7The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 8Code distributed by Google as part of the polymer project is also 9subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 10--> 11<link rel="import" href="../polymer/polymer.html"> 12 13<script> 14 (function() { 15 'use strict'; 16 17 var p = Element.prototype; 18 var matches = p.matches || p.matchesSelector || p.mozMatchesSelector || 19 p.msMatchesSelector || p.oMatchesSelector || p.webkitMatchesSelector; 20 21 Polymer.IronFocusablesHelper = { 22 23 /** 24 * Returns a sorted array of tabbable nodes, including the root node. 25 * It searches the tabbable nodes in the light and shadow dom of the chidren, 26 * sorting the result by tabindex. 27 * @param {!Node} node 28 * @return {Array<HTMLElement>} 29 */ 30 getTabbableNodes: function(node) { 31 var result = []; 32 // If there is at least one element with tabindex > 0, we need to sort 33 // the final array by tabindex. 34 var needsSortByTabIndex = this._collectTabbableNodes(node, result); 35 if (needsSortByTabIndex) { 36 return this._sortByTabIndex(result); 37 } 38 return result; 39 }, 40 41 /** 42 * Returns if a element is focusable. 43 * @param {!HTMLElement} element 44 * @return {boolean} 45 */ 46 isFocusable: function(element) { 47 // From http://stackoverflow.com/a/1600194/4228703: 48 // There isn't a definite list, it's up to the browser. The only 49 // standard we have is DOM Level 2 HTML https://www.w3.org/TR/DOM-Level-2-HTML/html.html, 50 // according to which the only elements that have a focus() method are 51 // HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement and 52 // HTMLAnchorElement. This notably omits HTMLButtonElement and 53 // HTMLAreaElement. 54 // Referring to these tests with tabbables in different browsers 55 // http://allyjs.io/data-tables/focusable.html 56 57 // Elements that cannot be focused if they have [disabled] attribute. 58 if (matches.call(element, 'input, select, textarea, button, object')) { 59 return matches.call(element, ':not([disabled])'); 60 } 61 // Elements that can be focused even if they have [disabled] attribute. 62 return matches.call(element, 63 'a[href], area[href], iframe, [tabindex], [contentEditable]'); 64 }, 65 66 /** 67 * Returns if a element is tabbable. To be tabbable, a element must be 68 * focusable, visible, and with a tabindex !== -1. 69 * @param {!HTMLElement} element 70 * @return {boolean} 71 */ 72 isTabbable: function(element) { 73 return this.isFocusable(element) && 74 matches.call(element, ':not([tabindex="-1"])') && 75 this._isVisible(element); 76 }, 77 78 /** 79 * Returns the normalized element tabindex. If not focusable, returns -1. 80 * It checks for the attribute "tabindex" instead of the element property 81 * `tabIndex` since browsers assign different values to it. 82 * e.g. in Firefox `<div contenteditable>` has `tabIndex = -1` 83 * @param {!HTMLElement} element 84 * @return {!number} 85 * @private 86 */ 87 _normalizedTabIndex: function(element) { 88 if (this.isFocusable(element)) { 89 var tabIndex = element.getAttribute('tabindex') || 0; 90 return Number(tabIndex); 91 } 92 return -1; 93 }, 94 95 /** 96 * Searches for nodes that are tabbable and adds them to the `result` array. 97 * Returns if the `result` array needs to be sorted by tabindex. 98 * @param {!Node} node The starting point for the search; added to `result` 99 * if tabbable. 100 * @param {!Array<HTMLElement>} result 101 * @return {boolean} 102 * @private 103 */ 104 _collectTabbableNodes: function(node, result) { 105 // If not an element or not visible, no need to explore children. 106 if (node.nodeType !== Node.ELEMENT_NODE || !this._isVisible(node)) { 107 return false; 108 } 109 var element = /** @type {HTMLElement} */ (node); 110 var tabIndex = this._normalizedTabIndex(element); 111 var needsSortByTabIndex = tabIndex > 0; 112 if (tabIndex >= 0) { 113 result.push(element); 114 } 115 116 // In ShadowDOM v1, tab order is affected by the order of distrubution. 117 // E.g. getTabbableNodes(#root) in ShadowDOM v1 should return [#A, #B]; 118 // in ShadowDOM v0 tab order is not affected by the distrubution order, 119 // in fact getTabbableNodes(#root) returns [#B, #A]. 120 // <div id="root"> 121 // <!-- shadow --> 122 // <slot name="a"> 123 // <slot name="b"> 124 // <!-- /shadow --> 125 // <input id="A" slot="a"> 126 // <input id="B" slot="b" tabindex="1"> 127 // </div> 128 // TODO(valdrin) support ShadowDOM v1 when upgrading to Polymer v2.0. 129 var children; 130 if (element.localName === 'content') { 131 children = Polymer.dom(element).getDistributedNodes(); 132 } else { 133 // Use shadow root if possible, will check for distributed nodes. 134 children = Polymer.dom(element.root || element).children; 135 } 136 for (var i = 0; i < children.length; i++) { 137 // Ensure method is always invoked to collect tabbable children. 138 var needsSort = this._collectTabbableNodes(children[i], result); 139 needsSortByTabIndex = needsSortByTabIndex || needsSort; 140 } 141 return needsSortByTabIndex; 142 }, 143 144 /** 145 * Returns false if the element has `visibility: hidden` or `display: none` 146 * @param {!HTMLElement} element 147 * @return {boolean} 148 * @private 149 */ 150 _isVisible: function(element) { 151 // Check inline style first to save a re-flow. If looks good, check also 152 // computed style. 153 var style = element.style; 154 if (style.visibility !== 'hidden' && style.display !== 'none') { 155 style = window.getComputedStyle(element); 156 return (style.visibility !== 'hidden' && style.display !== 'none'); 157 } 158 return false; 159 }, 160 161 /** 162 * Sorts an array of tabbable elements by tabindex. Returns a new array. 163 * @param {!Array<HTMLElement>} tabbables 164 * @return {Array<HTMLElement>} 165 * @private 166 */ 167 _sortByTabIndex: function(tabbables) { 168 // Implement a merge sort as Array.prototype.sort does a non-stable sort 169 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort 170 var len = tabbables.length; 171 if (len < 2) { 172 return tabbables; 173 } 174 var pivot = Math.ceil(len / 2); 175 var left = this._sortByTabIndex(tabbables.slice(0, pivot)); 176 var right = this._sortByTabIndex(tabbables.slice(pivot)); 177 return this._mergeSortByTabIndex(left, right); 178 }, 179 180 /** 181 * Merge sort iterator, merges the two arrays into one, sorted by tab index. 182 * @param {!Array<HTMLElement>} left 183 * @param {!Array<HTMLElement>} right 184 * @return {Array<HTMLElement>} 185 * @private 186 */ 187 _mergeSortByTabIndex: function(left, right) { 188 var result = []; 189 while ((left.length > 0) && (right.length > 0)) { 190 if (this._hasLowerTabOrder(left[0], right[0])) { 191 result.push(right.shift()); 192 } else { 193 result.push(left.shift()); 194 } 195 } 196 197 return result.concat(left, right); 198 }, 199 200 /** 201 * Returns if element `a` has lower tab order compared to element `b` 202 * (both elements are assumed to be focusable and tabbable). 203 * Elements with tabindex = 0 have lower tab order compared to elements 204 * with tabindex > 0. 205 * If both have same tabindex, it returns false. 206 * @param {!HTMLElement} a 207 * @param {!HTMLElement} b 208 * @return {boolean} 209 * @private 210 */ 211 _hasLowerTabOrder: function(a, b) { 212 // Normalize tabIndexes 213 // e.g. in Firefox `<div contenteditable>` has `tabIndex = -1` 214 var ati = Math.max(a.tabIndex, 0); 215 var bti = Math.max(b.tabIndex, 0); 216 return (ati === 0 || bti === 0) ? bti > ati : ati > bti; 217 } 218 }; 219 })(); 220</script> 221