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