focus-trap 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +43 -31
- package/dist/focus-trap.esm.js +138 -166
- package/dist/focus-trap.esm.js.map +1 -1
- package/dist/focus-trap.esm.min.js +2 -2
- package/dist/focus-trap.esm.min.js.map +1 -1
- package/dist/focus-trap.js +138 -166
- package/dist/focus-trap.js.map +1 -1
- package/dist/focus-trap.min.js +2 -2
- package/dist/focus-trap.min.js.map +1 -1
- package/dist/focus-trap.umd.js +138 -166
- package/dist/focus-trap.umd.js.map +1 -1
- package/dist/focus-trap.umd.min.js +2 -2
- package/dist/focus-trap.umd.min.js.map +1 -1
- package/index.d.ts +37 -5
- package/index.js +74 -45
- package/package.json +20 -20
package/dist/focus-trap.umd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* focus-trap 7.
|
|
2
|
+
* focus-trap 7.2.0
|
|
3
3
|
* @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
|
|
4
4
|
*/
|
|
5
5
|
(function (global, factory) {
|
|
@@ -15,17 +15,14 @@
|
|
|
15
15
|
|
|
16
16
|
function ownKeys(object, enumerableOnly) {
|
|
17
17
|
var keys = Object.keys(object);
|
|
18
|
-
|
|
19
18
|
if (Object.getOwnPropertySymbols) {
|
|
20
19
|
var symbols = Object.getOwnPropertySymbols(object);
|
|
21
20
|
enumerableOnly && (symbols = symbols.filter(function (sym) {
|
|
22
21
|
return Object.getOwnPropertyDescriptor(object, sym).enumerable;
|
|
23
22
|
})), keys.push.apply(keys, symbols);
|
|
24
23
|
}
|
|
25
|
-
|
|
26
24
|
return keys;
|
|
27
25
|
}
|
|
28
|
-
|
|
29
26
|
function _objectSpread2(target) {
|
|
30
27
|
for (var i = 1; i < arguments.length; i++) {
|
|
31
28
|
var source = null != arguments[i] ? arguments[i] : {};
|
|
@@ -35,11 +32,10 @@
|
|
|
35
32
|
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
|
36
33
|
});
|
|
37
34
|
}
|
|
38
|
-
|
|
39
35
|
return target;
|
|
40
36
|
}
|
|
41
|
-
|
|
42
37
|
function _defineProperty(obj, key, value) {
|
|
38
|
+
key = _toPropertyKey(key);
|
|
43
39
|
if (key in obj) {
|
|
44
40
|
Object.defineProperty(obj, key, {
|
|
45
41
|
value: value,
|
|
@@ -50,64 +46,75 @@
|
|
|
50
46
|
} else {
|
|
51
47
|
obj[key] = value;
|
|
52
48
|
}
|
|
53
|
-
|
|
54
49
|
return obj;
|
|
55
50
|
}
|
|
51
|
+
function _toPrimitive(input, hint) {
|
|
52
|
+
if (typeof input !== "object" || input === null) return input;
|
|
53
|
+
var prim = input[Symbol.toPrimitive];
|
|
54
|
+
if (prim !== undefined) {
|
|
55
|
+
var res = prim.call(input, hint || "default");
|
|
56
|
+
if (typeof res !== "object") return res;
|
|
57
|
+
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
58
|
+
}
|
|
59
|
+
return (hint === "string" ? String : Number)(input);
|
|
60
|
+
}
|
|
61
|
+
function _toPropertyKey(arg) {
|
|
62
|
+
var key = _toPrimitive(arg, "string");
|
|
63
|
+
return typeof key === "symbol" ? key : String(key);
|
|
64
|
+
}
|
|
56
65
|
|
|
57
|
-
var activeFocusTraps =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (activeTrap !== trap) {
|
|
65
|
-
activeTrap.pause();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
var trapIndex = trapQueue.indexOf(trap);
|
|
70
|
-
|
|
71
|
-
if (trapIndex === -1) {
|
|
72
|
-
trapQueue.push(trap);
|
|
73
|
-
} else {
|
|
74
|
-
// move this existing trap to the front of the queue
|
|
75
|
-
trapQueue.splice(trapIndex, 1);
|
|
76
|
-
trapQueue.push(trap);
|
|
77
|
-
}
|
|
78
|
-
},
|
|
79
|
-
deactivateTrap: function deactivateTrap(trap) {
|
|
80
|
-
var trapIndex = trapQueue.indexOf(trap);
|
|
81
|
-
|
|
82
|
-
if (trapIndex !== -1) {
|
|
83
|
-
trapQueue.splice(trapIndex, 1);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (trapQueue.length > 0) {
|
|
87
|
-
trapQueue[trapQueue.length - 1].unpause();
|
|
66
|
+
var activeFocusTraps = {
|
|
67
|
+
activateTrap: function activateTrap(trapStack, trap) {
|
|
68
|
+
if (trapStack.length > 0) {
|
|
69
|
+
var activeTrap = trapStack[trapStack.length - 1];
|
|
70
|
+
if (activeTrap !== trap) {
|
|
71
|
+
activeTrap.pause();
|
|
88
72
|
}
|
|
89
73
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
74
|
+
var trapIndex = trapStack.indexOf(trap);
|
|
75
|
+
if (trapIndex === -1) {
|
|
76
|
+
trapStack.push(trap);
|
|
77
|
+
} else {
|
|
78
|
+
// move this existing trap to the front of the queue
|
|
79
|
+
trapStack.splice(trapIndex, 1);
|
|
80
|
+
trapStack.push(trap);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
deactivateTrap: function deactivateTrap(trapStack, trap) {
|
|
84
|
+
var trapIndex = trapStack.indexOf(trap);
|
|
85
|
+
if (trapIndex !== -1) {
|
|
86
|
+
trapStack.splice(trapIndex, 1);
|
|
87
|
+
}
|
|
88
|
+
if (trapStack.length > 0) {
|
|
89
|
+
trapStack[trapStack.length - 1].unpause();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
93
|
var isSelectableInput = function isSelectableInput(node) {
|
|
94
94
|
return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
|
|
95
95
|
};
|
|
96
|
-
|
|
97
96
|
var isEscapeEvent = function isEscapeEvent(e) {
|
|
98
97
|
return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
|
|
99
98
|
};
|
|
100
|
-
|
|
101
99
|
var isTabEvent = function isTabEvent(e) {
|
|
102
100
|
return e.key === 'Tab' || e.keyCode === 9;
|
|
103
101
|
};
|
|
104
102
|
|
|
103
|
+
// checks for TAB by default
|
|
104
|
+
var isKeyForward = function isKeyForward(e) {
|
|
105
|
+
return isTabEvent(e) && !e.shiftKey;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// checks for SHIFT+TAB by default
|
|
109
|
+
var isKeyBackward = function isKeyBackward(e) {
|
|
110
|
+
return isTabEvent(e) && e.shiftKey;
|
|
111
|
+
};
|
|
105
112
|
var delay = function delay(fn) {
|
|
106
113
|
return setTimeout(fn, 0);
|
|
107
|
-
};
|
|
108
|
-
// of Array.findIndex() for our needs
|
|
109
|
-
|
|
114
|
+
};
|
|
110
115
|
|
|
116
|
+
// Array.find/findIndex() are not supported on IE; this replicates enough
|
|
117
|
+
// of Array.findIndex() for our needs
|
|
111
118
|
var findIndex = function findIndex(arr, fn) {
|
|
112
119
|
var idx = -1;
|
|
113
120
|
arr.every(function (value, i) {
|
|
@@ -118,8 +125,10 @@
|
|
|
118
125
|
|
|
119
126
|
return true; // next
|
|
120
127
|
});
|
|
128
|
+
|
|
121
129
|
return idx;
|
|
122
130
|
};
|
|
131
|
+
|
|
123
132
|
/**
|
|
124
133
|
* Get an option's value when it could be a plain value, or a handler that provides
|
|
125
134
|
* the value.
|
|
@@ -127,16 +136,12 @@
|
|
|
127
136
|
* @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
|
|
128
137
|
* @returns {*} The `value`, or the handler's returned value.
|
|
129
138
|
*/
|
|
130
|
-
|
|
131
|
-
|
|
132
139
|
var valueOrHandler = function valueOrHandler(value) {
|
|
133
140
|
for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
134
141
|
params[_key - 1] = arguments[_key];
|
|
135
142
|
}
|
|
136
|
-
|
|
137
143
|
return typeof value === 'function' ? value.apply(void 0, params) : value;
|
|
138
144
|
};
|
|
139
|
-
|
|
140
145
|
var getActualTarget = function getActualTarget(event) {
|
|
141
146
|
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
|
142
147
|
// shadow host. However, event.target.composedPath() will be an array of
|
|
@@ -148,17 +153,21 @@
|
|
|
148
153
|
return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
|
|
149
154
|
};
|
|
150
155
|
|
|
156
|
+
// NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
|
|
157
|
+
// current instance use the same stack if `userOptions.trapStack` isn't specified
|
|
158
|
+
var internalTrapStack = [];
|
|
151
159
|
var createFocusTrap = function createFocusTrap(elements, userOptions) {
|
|
152
160
|
// SSR: a live trap shouldn't be created in this type of environment so this
|
|
153
161
|
// should be safe code to execute if the `document` option isn't specified
|
|
154
162
|
var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
|
|
155
|
-
|
|
163
|
+
var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
|
|
156
164
|
var config = _objectSpread2({
|
|
157
165
|
returnFocusOnDeactivate: true,
|
|
158
166
|
escapeDeactivates: true,
|
|
159
|
-
delayInitialFocus: true
|
|
167
|
+
delayInitialFocus: true,
|
|
168
|
+
isKeyForward: isKeyForward,
|
|
169
|
+
isKeyBackward: isKeyBackward
|
|
160
170
|
}, userOptions);
|
|
161
|
-
|
|
162
171
|
var state = {
|
|
163
172
|
// containers given to createFocusTrap()
|
|
164
173
|
// @type {Array<HTMLElement>}
|
|
@@ -178,6 +187,7 @@
|
|
|
178
187
|
// }>}
|
|
179
188
|
containerGroups: [],
|
|
180
189
|
// same order/length as `containers` list
|
|
190
|
+
|
|
181
191
|
// references to objects in `containerGroups`, but only those that actually have
|
|
182
192
|
// tabbable nodes in them
|
|
183
193
|
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
|
@@ -201,10 +211,10 @@
|
|
|
201
211
|
* @param {string|undefined} [configOptionName] Name of option to use __instead of__ `optionName`
|
|
202
212
|
* IIF `configOverrideOptions` is not defined. Otherwise, `optionName` is used.
|
|
203
213
|
*/
|
|
204
|
-
|
|
205
214
|
var getOption = function getOption(configOverrideOptions, optionName, configOptionName) {
|
|
206
215
|
return configOverrideOptions && configOverrideOptions[optionName] !== undefined ? configOverrideOptions[optionName] : config[configOptionName || optionName];
|
|
207
216
|
};
|
|
217
|
+
|
|
208
218
|
/**
|
|
209
219
|
* Finds the index of the container that contains the element.
|
|
210
220
|
* @param {HTMLElement} element
|
|
@@ -212,16 +222,15 @@
|
|
|
212
222
|
* `state.containerGroups` (the order/length of these lists are the same); -1
|
|
213
223
|
* if the element isn't found.
|
|
214
224
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
225
|
var findContainerIndex = function findContainerIndex(element) {
|
|
218
226
|
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
|
219
227
|
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
|
220
228
|
// and we still need to find the element in there
|
|
221
229
|
return state.containerGroups.findIndex(function (_ref) {
|
|
222
230
|
var container = _ref.container,
|
|
223
|
-
|
|
224
|
-
return container.contains(element) ||
|
|
231
|
+
tabbableNodes = _ref.tabbableNodes;
|
|
232
|
+
return container.contains(element) ||
|
|
233
|
+
// fall back to explicit tabbable search which will take into consideration any
|
|
225
234
|
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
|
226
235
|
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
|
227
236
|
// look inside web components even if open)
|
|
@@ -230,6 +239,7 @@
|
|
|
230
239
|
});
|
|
231
240
|
});
|
|
232
241
|
};
|
|
242
|
+
|
|
233
243
|
/**
|
|
234
244
|
* Gets the node for the given option, which is expected to be an option that
|
|
235
245
|
* can be either a DOM node, a string that is a selector to get a node, `false`
|
|
@@ -243,19 +253,14 @@
|
|
|
243
253
|
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
|
244
254
|
* resolve to a node.
|
|
245
255
|
*/
|
|
246
|
-
|
|
247
|
-
|
|
248
256
|
var getNodeForOption = function getNodeForOption(optionName) {
|
|
249
257
|
var optionValue = config[optionName];
|
|
250
|
-
|
|
251
258
|
if (typeof optionValue === 'function') {
|
|
252
259
|
for (var _len2 = arguments.length, params = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
|
|
253
260
|
params[_key2 - 1] = arguments[_key2];
|
|
254
261
|
}
|
|
255
|
-
|
|
256
262
|
optionValue = optionValue.apply(void 0, params);
|
|
257
263
|
}
|
|
258
|
-
|
|
259
264
|
if (optionValue === true) {
|
|
260
265
|
optionValue = undefined; // use default value
|
|
261
266
|
}
|
|
@@ -263,56 +268,51 @@
|
|
|
263
268
|
if (!optionValue) {
|
|
264
269
|
if (optionValue === undefined || optionValue === false) {
|
|
265
270
|
return optionValue;
|
|
266
|
-
}
|
|
267
|
-
|
|
271
|
+
}
|
|
272
|
+
// else, empty string (invalid), null (invalid), 0 (invalid)
|
|
268
273
|
|
|
269
274
|
throw new Error("`".concat(optionName, "` was specified but was not a node, or did not return a node"));
|
|
270
275
|
}
|
|
271
|
-
|
|
272
276
|
var node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
|
273
277
|
|
|
274
278
|
if (typeof optionValue === 'string') {
|
|
275
279
|
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
|
276
|
-
|
|
277
280
|
if (!node) {
|
|
278
281
|
throw new Error("`".concat(optionName, "` as selector refers to no known node"));
|
|
279
282
|
}
|
|
280
283
|
}
|
|
281
|
-
|
|
282
284
|
return node;
|
|
283
285
|
};
|
|
284
|
-
|
|
285
286
|
var getInitialFocusNode = function getInitialFocusNode() {
|
|
286
|
-
var node = getNodeForOption('initialFocus');
|
|
287
|
+
var node = getNodeForOption('initialFocus');
|
|
287
288
|
|
|
289
|
+
// false explicitly indicates we want no initialFocus at all
|
|
288
290
|
if (node === false) {
|
|
289
291
|
return false;
|
|
290
292
|
}
|
|
291
|
-
|
|
292
293
|
if (node === undefined) {
|
|
293
294
|
// option not specified: use fallback options
|
|
294
295
|
if (findContainerIndex(doc.activeElement) >= 0) {
|
|
295
296
|
node = doc.activeElement;
|
|
296
297
|
} else {
|
|
297
298
|
var firstTabbableGroup = state.tabbableGroups[0];
|
|
298
|
-
var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
|
299
|
+
var firstTabbableNode = firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
|
299
300
|
|
|
301
|
+
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
|
300
302
|
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
301
303
|
}
|
|
302
304
|
}
|
|
303
|
-
|
|
304
305
|
if (!node) {
|
|
305
306
|
throw new Error('Your focus-trap needs to have at least one focusable element');
|
|
306
307
|
}
|
|
307
|
-
|
|
308
308
|
return node;
|
|
309
309
|
};
|
|
310
|
-
|
|
311
310
|
var updateTabbableNodes = function updateTabbableNodes() {
|
|
312
311
|
state.containerGroups = state.containers.map(function (container) {
|
|
313
|
-
var tabbableNodes = tabbable.tabbable(container, config.tabbableOptions);
|
|
314
|
-
// are a superset of tabbable nodes
|
|
312
|
+
var tabbableNodes = tabbable.tabbable(container, config.tabbableOptions);
|
|
315
313
|
|
|
314
|
+
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
|
315
|
+
// are a superset of tabbable nodes
|
|
316
316
|
var focusableNodes = tabbable.focusable(container, config.tabbableOptions);
|
|
317
317
|
return {
|
|
318
318
|
container: container,
|
|
@@ -320,7 +320,6 @@
|
|
|
320
320
|
focusableNodes: focusableNodes,
|
|
321
321
|
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
|
322
322
|
lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
|
|
323
|
-
|
|
324
323
|
/**
|
|
325
324
|
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
|
326
325
|
* in this container, if any.
|
|
@@ -344,17 +343,14 @@
|
|
|
344
343
|
var nodeIdx = focusableNodes.findIndex(function (n) {
|
|
345
344
|
return n === node;
|
|
346
345
|
});
|
|
347
|
-
|
|
348
346
|
if (nodeIdx < 0) {
|
|
349
347
|
return undefined;
|
|
350
348
|
}
|
|
351
|
-
|
|
352
349
|
if (forward) {
|
|
353
350
|
return focusableNodes.slice(nodeIdx + 1).find(function (n) {
|
|
354
351
|
return tabbable.isTabbable(n, config.tabbableOptions);
|
|
355
352
|
});
|
|
356
353
|
}
|
|
357
|
-
|
|
358
354
|
return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
|
|
359
355
|
return tabbable.isTabbable(n, config.tabbableOptions);
|
|
360
356
|
});
|
|
@@ -363,53 +359,46 @@
|
|
|
363
359
|
});
|
|
364
360
|
state.tabbableGroups = state.containerGroups.filter(function (group) {
|
|
365
361
|
return group.tabbableNodes.length > 0;
|
|
366
|
-
});
|
|
362
|
+
});
|
|
367
363
|
|
|
364
|
+
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
|
368
365
|
if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus') // returning false not supported for this option
|
|
369
366
|
) {
|
|
370
367
|
throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
|
|
371
368
|
}
|
|
372
369
|
};
|
|
373
|
-
|
|
374
370
|
var tryFocus = function tryFocus(node) {
|
|
375
371
|
if (node === false) {
|
|
376
372
|
return;
|
|
377
373
|
}
|
|
378
|
-
|
|
379
374
|
if (node === doc.activeElement) {
|
|
380
375
|
return;
|
|
381
376
|
}
|
|
382
|
-
|
|
383
377
|
if (!node || !node.focus) {
|
|
384
378
|
tryFocus(getInitialFocusNode());
|
|
385
379
|
return;
|
|
386
380
|
}
|
|
387
|
-
|
|
388
381
|
node.focus({
|
|
389
382
|
preventScroll: !!config.preventScroll
|
|
390
383
|
});
|
|
391
384
|
state.mostRecentlyFocusedNode = node;
|
|
392
|
-
|
|
393
385
|
if (isSelectableInput(node)) {
|
|
394
386
|
node.select();
|
|
395
387
|
}
|
|
396
388
|
};
|
|
397
|
-
|
|
398
389
|
var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
|
|
399
390
|
var node = getNodeForOption('setReturnFocus', previousActiveElement);
|
|
400
391
|
return node ? node : node === false ? false : previousActiveElement;
|
|
401
|
-
};
|
|
402
|
-
// so that it precedes the focus event.
|
|
403
|
-
|
|
392
|
+
};
|
|
404
393
|
|
|
394
|
+
// This needs to be done on mousedown and touchstart instead of click
|
|
395
|
+
// so that it precedes the focus event.
|
|
405
396
|
var checkPointerDown = function checkPointerDown(e) {
|
|
406
397
|
var target = getActualTarget(e);
|
|
407
|
-
|
|
408
398
|
if (findContainerIndex(target) >= 0) {
|
|
409
399
|
// allow the click since it ocurred inside the trap
|
|
410
400
|
return;
|
|
411
401
|
}
|
|
412
|
-
|
|
413
402
|
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
|
414
403
|
// immediately deactivate the trap
|
|
415
404
|
trap.deactivate({
|
|
@@ -427,25 +416,26 @@
|
|
|
427
416
|
returnFocus: config.returnFocusOnDeactivate && !tabbable.isFocusable(target, config.tabbableOptions)
|
|
428
417
|
});
|
|
429
418
|
return;
|
|
430
|
-
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// This is needed for mobile devices.
|
|
431
422
|
// (If we'll only let `click` events through,
|
|
432
423
|
// then on mobile they will be blocked anyways if `touchstart` is blocked.)
|
|
433
|
-
|
|
434
|
-
|
|
435
424
|
if (valueOrHandler(config.allowOutsideClick, e)) {
|
|
436
425
|
// allow the click outside the trap to take place
|
|
437
426
|
return;
|
|
438
|
-
}
|
|
439
|
-
|
|
427
|
+
}
|
|
440
428
|
|
|
429
|
+
// otherwise, prevent the click
|
|
441
430
|
e.preventDefault();
|
|
442
|
-
};
|
|
443
|
-
|
|
431
|
+
};
|
|
444
432
|
|
|
433
|
+
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
445
434
|
var checkFocusIn = function checkFocusIn(e) {
|
|
446
435
|
var target = getActualTarget(e);
|
|
447
|
-
var targetContained = findContainerIndex(target) >= 0;
|
|
436
|
+
var targetContained = findContainerIndex(target) >= 0;
|
|
448
437
|
|
|
438
|
+
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
449
439
|
if (targetContained || target instanceof Document) {
|
|
450
440
|
if (targetContained) {
|
|
451
441
|
state.mostRecentlyFocusedNode = target;
|
|
@@ -455,42 +445,41 @@
|
|
|
455
445
|
e.stopImmediatePropagation();
|
|
456
446
|
tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
|
|
457
447
|
}
|
|
458
|
-
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Hijack key nav events on the first and last focusable nodes of the trap,
|
|
459
451
|
// in order to prevent focus from escaping. If it escapes for even a
|
|
460
452
|
// moment it can end up scrolling the page and causing confusion so we
|
|
461
453
|
// kind of need to capture the action at the keydown phase.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
var target = getActualTarget(e);
|
|
454
|
+
var checkKeyNav = function checkKeyNav(event) {
|
|
455
|
+
var isBackward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
|
456
|
+
var target = getActualTarget(event);
|
|
466
457
|
updateTabbableNodes();
|
|
467
458
|
var destinationNode = null;
|
|
468
|
-
|
|
469
459
|
if (state.tabbableGroups.length > 0) {
|
|
470
460
|
// make sure the target is actually contained in a group
|
|
471
461
|
// NOTE: the target may also be the container itself if it's focusable
|
|
472
462
|
// with tabIndex='-1' and was given initial focus
|
|
473
463
|
var containerIndex = findContainerIndex(target);
|
|
474
464
|
var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
|
475
|
-
|
|
476
465
|
if (containerIndex < 0) {
|
|
477
466
|
// target not found in any group: quite possible focus has escaped the trap,
|
|
478
|
-
// so bring it back
|
|
479
|
-
if (
|
|
467
|
+
// so bring it back into...
|
|
468
|
+
if (isBackward) {
|
|
480
469
|
// ...the last node in the last group
|
|
481
470
|
destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
|
|
482
471
|
} else {
|
|
483
472
|
// ...the first node in the first group
|
|
484
473
|
destinationNode = state.tabbableGroups[0].firstTabbableNode;
|
|
485
474
|
}
|
|
486
|
-
} else if (
|
|
475
|
+
} else if (isBackward) {
|
|
487
476
|
// REVERSE
|
|
477
|
+
|
|
488
478
|
// is the target the first tabbable node in a group?
|
|
489
479
|
var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
|
|
490
480
|
var firstTabbableNode = _ref2.firstTabbableNode;
|
|
491
481
|
return target === firstTabbableNode;
|
|
492
482
|
});
|
|
493
|
-
|
|
494
483
|
if (startOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
|
|
495
484
|
// an exception case where the target is either the container itself, or
|
|
496
485
|
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
@@ -500,7 +489,6 @@
|
|
|
500
489
|
// first tabbable node, and go to the last tabbable node of the LAST group
|
|
501
490
|
startOfGroupIndex = containerIndex;
|
|
502
491
|
}
|
|
503
|
-
|
|
504
492
|
if (startOfGroupIndex >= 0) {
|
|
505
493
|
// YES: then shift+tab should go to the last tabbable node in the
|
|
506
494
|
// previous group (and wrap around to the last tabbable node of
|
|
@@ -508,15 +496,19 @@
|
|
|
508
496
|
var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
|
|
509
497
|
var destinationGroup = state.tabbableGroups[destinationGroupIndex];
|
|
510
498
|
destinationNode = destinationGroup.lastTabbableNode;
|
|
499
|
+
} else if (!isTabEvent(event)) {
|
|
500
|
+
// user must have customized the nav keys so we have to move focus manually _within_
|
|
501
|
+
// the active group: do this based on the order determined by tabbable()
|
|
502
|
+
destinationNode = containerGroup.nextTabbableNode(target, false);
|
|
511
503
|
}
|
|
512
504
|
} else {
|
|
513
505
|
// FORWARD
|
|
506
|
+
|
|
514
507
|
// is the target the last tabbable node in a group?
|
|
515
508
|
var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
|
|
516
509
|
var lastTabbableNode = _ref3.lastTabbableNode;
|
|
517
510
|
return target === lastTabbableNode;
|
|
518
511
|
});
|
|
519
|
-
|
|
520
512
|
if (lastOfGroupIndex < 0 && (containerGroup.container === target || tabbable.isFocusable(target, config.tabbableOptions) && !tabbable.isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
|
|
521
513
|
// an exception case where the target is the container itself, or
|
|
522
514
|
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
@@ -526,73 +518,76 @@
|
|
|
526
518
|
// last tabbable node, and go to the first tabbable node of the FIRST group
|
|
527
519
|
lastOfGroupIndex = containerIndex;
|
|
528
520
|
}
|
|
529
|
-
|
|
530
521
|
if (lastOfGroupIndex >= 0) {
|
|
531
522
|
// YES: then tab should go to the first tabbable node in the next
|
|
532
523
|
// group (and wrap around to the first tabbable node of the FIRST
|
|
533
524
|
// group if it's the last tabbable node of the LAST group)
|
|
534
525
|
var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
|
|
535
|
-
|
|
536
526
|
var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
|
|
537
527
|
destinationNode = _destinationGroup.firstTabbableNode;
|
|
528
|
+
} else if (!isTabEvent(event)) {
|
|
529
|
+
// user must have customized the nav keys so we have to move focus manually _within_
|
|
530
|
+
// the active group: do this based on the order determined by tabbable()
|
|
531
|
+
destinationNode = containerGroup.nextTabbableNode(target);
|
|
538
532
|
}
|
|
539
533
|
}
|
|
540
534
|
} else {
|
|
535
|
+
// no groups available
|
|
541
536
|
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
|
542
537
|
destinationNode = getNodeForOption('fallbackFocus');
|
|
543
538
|
}
|
|
544
|
-
|
|
545
539
|
if (destinationNode) {
|
|
546
|
-
|
|
540
|
+
if (isTabEvent(event)) {
|
|
541
|
+
// since tab natively moves focus, we wouldn't have a destination node unless we
|
|
542
|
+
// were on the edge of a container and had to move to the next/previous edge, in
|
|
543
|
+
// which case we want to prevent default to keep the browser from moving focus
|
|
544
|
+
// to where it normally would
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
}
|
|
547
547
|
tryFocus(destinationNode);
|
|
548
|
-
}
|
|
549
|
-
|
|
548
|
+
}
|
|
549
|
+
// else, let the browser take care of [shift+]tab and move the focus
|
|
550
550
|
};
|
|
551
551
|
|
|
552
|
-
var checkKey = function checkKey(
|
|
553
|
-
if (isEscapeEvent(
|
|
554
|
-
|
|
552
|
+
var checkKey = function checkKey(event) {
|
|
553
|
+
if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
|
|
554
|
+
event.preventDefault();
|
|
555
555
|
trap.deactivate();
|
|
556
556
|
return;
|
|
557
557
|
}
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
checkTab(e);
|
|
561
|
-
return;
|
|
558
|
+
if (config.isKeyForward(event) || config.isKeyBackward(event)) {
|
|
559
|
+
checkKeyNav(event, config.isKeyBackward(event));
|
|
562
560
|
}
|
|
563
561
|
};
|
|
564
|
-
|
|
565
562
|
var checkClick = function checkClick(e) {
|
|
566
563
|
var target = getActualTarget(e);
|
|
567
|
-
|
|
568
564
|
if (findContainerIndex(target) >= 0) {
|
|
569
565
|
return;
|
|
570
566
|
}
|
|
571
|
-
|
|
572
567
|
if (valueOrHandler(config.clickOutsideDeactivates, e)) {
|
|
573
568
|
return;
|
|
574
569
|
}
|
|
575
|
-
|
|
576
570
|
if (valueOrHandler(config.allowOutsideClick, e)) {
|
|
577
571
|
return;
|
|
578
572
|
}
|
|
579
|
-
|
|
580
573
|
e.preventDefault();
|
|
581
574
|
e.stopImmediatePropagation();
|
|
582
|
-
};
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
//
|
|
583
578
|
// EVENT LISTENERS
|
|
584
579
|
//
|
|
585
580
|
|
|
586
|
-
|
|
587
581
|
var addListeners = function addListeners() {
|
|
588
582
|
if (!state.active) {
|
|
589
583
|
return;
|
|
590
|
-
}
|
|
584
|
+
}
|
|
591
585
|
|
|
586
|
+
// There can be only one listening focus trap at a time
|
|
587
|
+
activeFocusTraps.activateTrap(trapStack, trap);
|
|
592
588
|
|
|
593
|
-
|
|
589
|
+
// Delay ensures that the focused element doesn't capture the event
|
|
594
590
|
// that caused the focus trap activation.
|
|
595
|
-
|
|
596
591
|
state.delayInitialFocusTimer = config.delayInitialFocus ? delay(function () {
|
|
597
592
|
tryFocus(getInitialFocusNode());
|
|
598
593
|
}) : tryFocus(getInitialFocusNode());
|
|
@@ -615,70 +610,58 @@
|
|
|
615
610
|
});
|
|
616
611
|
return trap;
|
|
617
612
|
};
|
|
618
|
-
|
|
619
613
|
var removeListeners = function removeListeners() {
|
|
620
614
|
if (!state.active) {
|
|
621
615
|
return;
|
|
622
616
|
}
|
|
623
|
-
|
|
624
617
|
doc.removeEventListener('focusin', checkFocusIn, true);
|
|
625
618
|
doc.removeEventListener('mousedown', checkPointerDown, true);
|
|
626
619
|
doc.removeEventListener('touchstart', checkPointerDown, true);
|
|
627
620
|
doc.removeEventListener('click', checkClick, true);
|
|
628
621
|
doc.removeEventListener('keydown', checkKey, true);
|
|
629
622
|
return trap;
|
|
630
|
-
};
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
//
|
|
631
626
|
// TRAP DEFINITION
|
|
632
627
|
//
|
|
633
628
|
|
|
634
|
-
|
|
635
629
|
trap = {
|
|
636
630
|
get active() {
|
|
637
631
|
return state.active;
|
|
638
632
|
},
|
|
639
|
-
|
|
640
633
|
get paused() {
|
|
641
634
|
return state.paused;
|
|
642
635
|
},
|
|
643
|
-
|
|
644
636
|
activate: function activate(activateOptions) {
|
|
645
637
|
if (state.active) {
|
|
646
638
|
return this;
|
|
647
639
|
}
|
|
648
|
-
|
|
649
640
|
var onActivate = getOption(activateOptions, 'onActivate');
|
|
650
641
|
var onPostActivate = getOption(activateOptions, 'onPostActivate');
|
|
651
642
|
var checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
|
|
652
|
-
|
|
653
643
|
if (!checkCanFocusTrap) {
|
|
654
644
|
updateTabbableNodes();
|
|
655
645
|
}
|
|
656
|
-
|
|
657
646
|
state.active = true;
|
|
658
647
|
state.paused = false;
|
|
659
648
|
state.nodeFocusedBeforeActivation = doc.activeElement;
|
|
660
|
-
|
|
661
649
|
if (onActivate) {
|
|
662
650
|
onActivate();
|
|
663
651
|
}
|
|
664
|
-
|
|
665
652
|
var finishActivation = function finishActivation() {
|
|
666
653
|
if (checkCanFocusTrap) {
|
|
667
654
|
updateTabbableNodes();
|
|
668
655
|
}
|
|
669
|
-
|
|
670
656
|
addListeners();
|
|
671
|
-
|
|
672
657
|
if (onPostActivate) {
|
|
673
658
|
onPostActivate();
|
|
674
659
|
}
|
|
675
660
|
};
|
|
676
|
-
|
|
677
661
|
if (checkCanFocusTrap) {
|
|
678
662
|
checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
|
|
679
663
|
return this;
|
|
680
664
|
}
|
|
681
|
-
|
|
682
665
|
finishActivation();
|
|
683
666
|
return this;
|
|
684
667
|
},
|
|
@@ -686,46 +669,38 @@
|
|
|
686
669
|
if (!state.active) {
|
|
687
670
|
return this;
|
|
688
671
|
}
|
|
689
|
-
|
|
690
672
|
var options = _objectSpread2({
|
|
691
673
|
onDeactivate: config.onDeactivate,
|
|
692
674
|
onPostDeactivate: config.onPostDeactivate,
|
|
693
675
|
checkCanReturnFocus: config.checkCanReturnFocus
|
|
694
676
|
}, deactivateOptions);
|
|
695
|
-
|
|
696
677
|
clearTimeout(state.delayInitialFocusTimer); // noop if undefined
|
|
697
|
-
|
|
698
678
|
state.delayInitialFocusTimer = undefined;
|
|
699
679
|
removeListeners();
|
|
700
680
|
state.active = false;
|
|
701
681
|
state.paused = false;
|
|
702
|
-
activeFocusTraps.deactivateTrap(trap);
|
|
682
|
+
activeFocusTraps.deactivateTrap(trapStack, trap);
|
|
703
683
|
var onDeactivate = getOption(options, 'onDeactivate');
|
|
704
684
|
var onPostDeactivate = getOption(options, 'onPostDeactivate');
|
|
705
685
|
var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
|
706
686
|
var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
|
|
707
|
-
|
|
708
687
|
if (onDeactivate) {
|
|
709
688
|
onDeactivate();
|
|
710
689
|
}
|
|
711
|
-
|
|
712
690
|
var finishDeactivation = function finishDeactivation() {
|
|
713
691
|
delay(function () {
|
|
714
692
|
if (returnFocus) {
|
|
715
693
|
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
|
716
694
|
}
|
|
717
|
-
|
|
718
695
|
if (onPostDeactivate) {
|
|
719
696
|
onPostDeactivate();
|
|
720
697
|
}
|
|
721
698
|
});
|
|
722
699
|
};
|
|
723
|
-
|
|
724
700
|
if (returnFocus && checkCanReturnFocus) {
|
|
725
701
|
checkCanReturnFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation)).then(finishDeactivation, finishDeactivation);
|
|
726
702
|
return this;
|
|
727
703
|
}
|
|
728
|
-
|
|
729
704
|
finishDeactivation();
|
|
730
705
|
return this;
|
|
731
706
|
},
|
|
@@ -733,7 +708,6 @@
|
|
|
733
708
|
if (state.paused || !state.active) {
|
|
734
709
|
return this;
|
|
735
710
|
}
|
|
736
|
-
|
|
737
711
|
state.paused = true;
|
|
738
712
|
removeListeners();
|
|
739
713
|
return this;
|
|
@@ -742,7 +716,6 @@
|
|
|
742
716
|
if (!state.paused || !state.active) {
|
|
743
717
|
return this;
|
|
744
718
|
}
|
|
745
|
-
|
|
746
719
|
state.paused = false;
|
|
747
720
|
updateTabbableNodes();
|
|
748
721
|
addListeners();
|
|
@@ -753,15 +726,14 @@
|
|
|
753
726
|
state.containers = elementsAsArray.map(function (element) {
|
|
754
727
|
return typeof element === 'string' ? doc.querySelector(element) : element;
|
|
755
728
|
});
|
|
756
|
-
|
|
757
729
|
if (state.active) {
|
|
758
730
|
updateTabbableNodes();
|
|
759
731
|
}
|
|
760
|
-
|
|
761
732
|
return this;
|
|
762
733
|
}
|
|
763
|
-
};
|
|
734
|
+
};
|
|
764
735
|
|
|
736
|
+
// initialize container elements
|
|
765
737
|
trap.updateContainerElements(elements);
|
|
766
738
|
return trap;
|
|
767
739
|
};
|