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