• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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