This repository has been archived by the owner on Dec 29, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 48
/
inert-polyfill.js
269 lines (249 loc) · 8.97 KB
/
inert-polyfill.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
/*
* Copyright 2015 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
if (!('inert' in HTMLElement.prototype)) {
Object.defineProperty(HTMLElement.prototype, 'inert', {
enumerable: true,
/**
* @return {boolean}
* @this {Element}
*/
get: function() { return this.hasAttribute('inert'); },
/**
* @param {boolean} inert
* @this {Element}
*/
set: function(inert) {
if (inert) {
this.setAttribute('inert', '');
} else {
this.removeAttribute('inert');
}
}
});
window.addEventListener('load', function() {
function applyStyle(css) {
var style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
document.head.appendChild(style);
}
var css = "/*[inert]*/*[inert]{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}";
applyStyle(css);
/**
* Sends a fake tab event. This is only supported by some browsers.
*
* @param {boolean=} opt_shiftKey whether to send this tab with shiftKey
*/
function dispatchTabEvent(opt_shiftKey) {
var ev = null;
try {
ev = new KeyboardEvent('keydown', {
keyCode: 9,
which: 9,
key: 'Tab',
code: 'Tab',
keyIdentifier: 'U+0009',
shiftKey: !!opt_shiftKey,
bubbles: true
});
} catch (e) {
try {
// Internet Explorer
ev = document.createEvent('KeyboardEvent');
ev.initKeyboardEvent(
'keydown',
true,
true,
window,
'Tab',
0,
opt_shiftKey ? 'Shift' : '',
false,
'en'
)
} catch (e) {}
}
if (ev) {
try {
Object.defineProperty(ev, 'keyCode', { value: 9 });
} catch (e) {}
document.dispatchEvent(ev);
}
}
/**
* Determines whether the specified element is inert, and returns the element
* which caused this state. This is limited to, but may include, the body
* element.
*
* @param {Element} e to check
* @return {Element} element is made inert by, if any
*/
function madeInertBy(e) {
while (e && e !== document.documentElement) {
if (e.hasAttribute('inert')) {
return e;
}
e = e.parentElement;
}
return null;
}
/**
* Finds the nearest shadow root from an element that's within said shadow root.
*
* TODO(samthor): We probably want to find the highest shadow root.
*
* @param {Element} e to check
* @return {Node} shadow root, if any
*/
var findShadowRoot = function(e) { return null; };
if (window.ShadowRoot) {
findShadowRoot = function(e) {
while (e && e !== document.documentElement) {
if (e instanceof window.ShadowRoot) { return e; }
e = e.parentNode;
}
return null;
}
}
/**
* Returns the target of the passed event. If there's a path (shadow DOM only), then prefer it.
*
* @param {!Event} event
* @return {Element} target of event
*/
function targetForEvent(event) {
var p = event.path;
return /** @type {Element} */ (p && p[0] || event.target);
}
// Hold onto the last tab direction: next (tab) or previous (shift-tab). This
// can be used to step over inert elements in the correct direction. Mouse
// or non-tab events should reset this and inert events should focus nothing.
var lastTabDirection = 0;
document.addEventListener('keydown', function(ev) {
if (ev.keyCode === 9) {
lastTabDirection = ev.shiftKey ? -1 : +1;
} else {
lastTabDirection = 0;
}
});
document.addEventListener('mousedown', function(ev) {
lastTabDirection = 0;
});
// Retain the currently focused shadowRoot.
var focusedShadowRoot = null;
function updateFocusedShadowRoot(root) {
if (root == focusedShadowRoot) { return; }
if (focusedShadowRoot) {
if (!(focusedShadowRoot instanceof window.ShadowRoot)) {
throw new Error('not shadow root: ' + focusedShadowRoot);
}
focusedShadowRoot.removeEventListener('focusin', shadowFocusHandler, true); // remove
}
if (root) {
root.addEventListener('focusin', shadowFocusHandler, true); // add
}
focusedShadowRoot = root;
}
/**
* Focus handler on a Shadow DOM host. This traps focus events within that root.
*
* @param {!Event} ev
*/
function shadowFocusHandler(ev) {
// ignore "direct" focus, we only want shadow root focus
var last = ev.path[ev.path.length - 1];
if (last === /** @type {*} */ (window)) { return; }
sharedFocusHandler(targetForEvent(ev));
ev.preventDefault();
ev.stopPropagation();
}
/**
* Called indirectly by both the regular focus handler and Shadow DOM host focus handler. This
* is the bulk of the polyfill which prevents focus.
*
* @param {Element} target focused on
*/
function sharedFocusHandler(target) {
var inertElement = madeInertBy(target);
if (!inertElement) { return; }
// If the page has been tabbed recently, then focus the next element
// in the known direction (if available).
if (document.hasFocus() && lastTabDirection !== 0) {
function getFocused() {
return (focusedShadowRoot || document).activeElement;
}
// Send a fake tab event to enumerate through the browser's view of
// focusable elements. This is supported in some browsers (not Firefox).
var previous = getFocused();
dispatchTabEvent(lastTabDirection < 0 ? true : false);
if (previous != getFocused()) { return; }
// Otherwise, enumerate through adjacent elements to find the next
// focusable element. This won't respect any custom tabIndex.
var filter = /** @type {NodeFilter} */ ({
/**
* @param {Node} node
* @return {number}
*/
acceptNode: function(node) {
if (!node || !node.focus || node.tabIndex < 0) {
return NodeFilter.FILTER_SKIP; // look at descendants
}
var contained = inertElement.contains(node);
return contained ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT;
},
});
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, filter);
walker.currentNode = inertElement;
var nextFunc = Math.sign(lastTabDirection) === -1 ? walker.previousNode : walker.nextNode
var next = nextFunc.bind(walker);
for (var candidate; candidate = next(); ) {
candidate.focus();
if (getFocused() !== previous) { return; }
}
// FIXME: If a focusable element can't be found here, it's likely to mean
// that this is the start or end of the page. Blurring is then not quite
// right, as it prevents access to the browser chrome.
}
// Otherwise, immediately blur the targeted element. Technically, this
// still generates focus and blur events on the element. This is (probably)
// the price to pay for this polyfill.
target.blur();
}
// The 'focusin' event bubbles, but instead, use 'focus' with useCapture set
// to true as this is supported in Firefox. Additionally, target the body so
// this doesn't generate superfluous events on document itself.
document.body.addEventListener('focus', function(ev) {
var target = targetForEvent(ev);
updateFocusedShadowRoot((target == ev.target ? null : findShadowRoot(target)));
sharedFocusHandler(target); // either real DOM node or shadow node
}, true);
// Use a capturing click listener as both a safety fallback where pointer-events is not
// available (IE10 and below), and to prevent accessKey access to inert elements.
// TODO(samthor): Note that pointer-events polyfills trap more mouse events, e.g.-
// https://github.com/kmewhort/pointer_events_polyfill
document.addEventListener('click', function(ev) {
var target = targetForEvent(ev);
if (madeInertBy(target)) {
ev.preventDefault();
ev.stopPropagation();
}
}, true);
});
}