focus-trap 6.2.0 → 6.3.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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * focus-trap 6.2.0
2
+ * focus-trap 6.3.0
3
3
  * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
4
4
  */
5
5
  (function (global, factory) {
@@ -100,7 +100,54 @@
100
100
  };
101
101
  }();
102
102
 
103
- function createFocusTrap(elements, userOptions) {
103
+ var isSelectableInput = function isSelectableInput(node) {
104
+ return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
105
+ };
106
+
107
+ var isEscapeEvent = function isEscapeEvent(e) {
108
+ return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
109
+ };
110
+
111
+ var isTabEvent = function isTabEvent(e) {
112
+ return e.key === 'Tab' || e.keyCode === 9;
113
+ };
114
+
115
+ var delay = function delay(fn) {
116
+ return setTimeout(fn, 0);
117
+ }; // Array.find/findIndex() are not supported on IE; this replicates enough
118
+ // of Array.findIndex() for our needs
119
+
120
+
121
+ var findIndex = function findIndex(arr, fn) {
122
+ var idx = -1;
123
+ arr.every(function (value, i) {
124
+ if (fn(value)) {
125
+ idx = i;
126
+ return false; // break
127
+ }
128
+
129
+ return true; // next
130
+ });
131
+ return idx;
132
+ };
133
+ /**
134
+ * Get an option's value when it could be a plain value, or a handler that provides
135
+ * the value.
136
+ * @param {*} value Option's value to check.
137
+ * @param {...*} [params] Any parameters to pass to the handler, if `value` is a function.
138
+ * @returns {*} The `value`, or the handler's returned value.
139
+ */
140
+
141
+
142
+ var valueOrHandler = function valueOrHandler(value) {
143
+ for (var _len = arguments.length, params = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
144
+ params[_key - 1] = arguments[_key];
145
+ }
146
+
147
+ return typeof value === 'function' ? value.apply(void 0, params) : value;
148
+ };
149
+
150
+ var createFocusTrap = function createFocusTrap(elements, userOptions) {
104
151
  var doc = document;
105
152
 
106
153
  var config = _objectSpread2({
@@ -112,143 +159,41 @@
112
159
  var state = {
113
160
  // @type {Array<HTMLElement>}
114
161
  containers: [],
115
- // @type {{ firstTabbableNode: HTMLElement, lastTabbableNode: HTMLElement }}
162
+ // list of objects identifying the first and last tabbable nodes in all containers/groups in
163
+ // the trap
164
+ // NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
165
+ // is active, but the trap should never get to a state where there isn't at least one group
166
+ // with at least one tabbable node in it (that would lead to an error condition that would
167
+ // result in an error being thrown)
168
+ // @type {Array<{ container: HTMLElement, firstTabbableNode: HTMLElement|null, lastTabbableNode: HTMLElement|null }>}
116
169
  tabbableGroups: [],
117
170
  nodeFocusedBeforeActivation: null,
118
171
  mostRecentlyFocusedNode: null,
119
172
  active: false,
120
173
  paused: false
121
174
  };
122
- var trap = {
123
- activate: activate,
124
- deactivate: deactivate,
125
- pause: pause,
126
- unpause: unpause,
127
- updateContainerElements: updateContainerElements
128
- };
129
- updateContainerElements(elements);
130
- return trap;
131
-
132
- function updateContainerElements(containerElements) {
133
- var elementsAsArray = [].concat(containerElements).filter(Boolean);
134
- state.containers = elementsAsArray.map(function (element) {
135
- return typeof element === 'string' ? doc.querySelector(element) : element;
136
- });
137
-
138
- if (state.active) {
139
- updateTabbableNodes();
140
- }
141
-
142
- return trap;
143
- }
144
-
145
- function activate(activateOptions) {
146
- if (state.active) return;
147
- updateTabbableNodes();
148
- state.active = true;
149
- state.paused = false;
150
- state.nodeFocusedBeforeActivation = doc.activeElement;
151
- var onActivate = activateOptions && activateOptions.onActivate ? activateOptions.onActivate : config.onActivate;
152
-
153
- if (onActivate) {
154
- onActivate();
155
- }
156
-
157
- addListeners();
158
- return trap;
159
- }
175
+ var trap; // eslint-disable-line prefer-const -- some private functions reference it, and its methods reference private functions, so we must declare here and define later
160
176
 
161
- function deactivate(deactivateOptions) {
162
- if (!state.active) return;
163
- clearTimeout(activeFocusDelay);
164
- removeListeners();
165
- state.active = false;
166
- state.paused = false;
167
- activeFocusTraps.deactivateTrap(trap);
168
- var onDeactivate = deactivateOptions && deactivateOptions.onDeactivate !== undefined ? deactivateOptions.onDeactivate : config.onDeactivate;
169
-
170
- if (onDeactivate) {
171
- onDeactivate();
172
- }
173
-
174
- var returnFocus = deactivateOptions && deactivateOptions.returnFocus !== undefined ? deactivateOptions.returnFocus : config.returnFocusOnDeactivate;
175
-
176
- if (returnFocus) {
177
- delay(function () {
178
- tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
179
- });
180
- }
181
-
182
- return trap;
183
- }
184
-
185
- function pause() {
186
- if (state.paused || !state.active) return trap;
187
- state.paused = true;
188
- removeListeners();
189
- return trap;
190
- }
191
-
192
- function unpause() {
193
- if (!state.paused || !state.active) return trap;
194
- state.paused = false;
195
- updateTabbableNodes();
196
- addListeners();
197
- return trap;
198
- }
199
-
200
- function addListeners() {
201
- if (!state.active) return; // There can be only one listening focus trap at a time
202
-
203
- activeFocusTraps.activateTrap(trap); // Delay ensures that the focused element doesn't capture the event
204
- // that caused the focus trap activation.
205
-
206
- activeFocusDelay = config.delayInitialFocus ? delay(function () {
207
- tryFocus(getInitialFocusNode());
208
- }) : tryFocus(getInitialFocusNode());
209
- doc.addEventListener('focusin', checkFocusIn, true);
210
- doc.addEventListener('mousedown', checkPointerDown, {
211
- capture: true,
212
- passive: false
213
- });
214
- doc.addEventListener('touchstart', checkPointerDown, {
215
- capture: true,
216
- passive: false
217
- });
218
- doc.addEventListener('click', checkClick, {
219
- capture: true,
220
- passive: false
221
- });
222
- doc.addEventListener('keydown', checkKey, {
223
- capture: true,
224
- passive: false
177
+ var containersContain = function containersContain(element) {
178
+ return state.containers.some(function (container) {
179
+ return container.contains(element);
225
180
  });
226
- return trap;
227
- }
228
-
229
- function removeListeners() {
230
- if (!state.active) return;
231
- doc.removeEventListener('focusin', checkFocusIn, true);
232
- doc.removeEventListener('mousedown', checkPointerDown, true);
233
- doc.removeEventListener('touchstart', checkPointerDown, true);
234
- doc.removeEventListener('click', checkClick, true);
235
- doc.removeEventListener('keydown', checkKey, true);
236
- return trap;
237
- }
181
+ };
238
182
 
239
- function getNodeForOption(optionName) {
183
+ var getNodeForOption = function getNodeForOption(optionName) {
240
184
  var optionValue = config[optionName];
241
- var node = optionValue;
242
185
 
243
186
  if (!optionValue) {
244
187
  return null;
245
188
  }
246
189
 
190
+ var node = optionValue;
191
+
247
192
  if (typeof optionValue === 'string') {
248
193
  node = doc.querySelector(optionValue);
249
194
 
250
195
  if (!node) {
251
- throw new Error('`' + optionName + '` refers to no known node');
196
+ throw new Error("`".concat(optionName, "` refers to no known node"));
252
197
  }
253
198
  }
254
199
 
@@ -256,14 +201,14 @@
256
201
  node = optionValue();
257
202
 
258
203
  if (!node) {
259
- throw new Error('`' + optionName + '` did not return a node');
204
+ throw new Error("`".concat(optionName, "` did not return a node"));
260
205
  }
261
206
  }
262
207
 
263
208
  return node;
264
- }
209
+ };
265
210
 
266
- function getInitialFocusNode() {
211
+ var getInitialFocusNode = function getInitialFocusNode() {
267
212
  var node;
268
213
 
269
214
  if (getNodeForOption('initialFocus') !== null) {
@@ -281,24 +226,67 @@
281
226
  }
282
227
 
283
228
  return node;
284
- }
229
+ };
230
+
231
+ var updateTabbableNodes = function updateTabbableNodes() {
232
+ state.tabbableGroups = state.containers.map(function (container) {
233
+ var tabbableNodes = tabbable.tabbable(container);
285
234
 
286
- function getReturnFocusNode(previousActiveElement) {
235
+ if (tabbableNodes.length > 0) {
236
+ return {
237
+ container: container,
238
+ firstTabbableNode: tabbableNodes[0],
239
+ lastTabbableNode: tabbableNodes[tabbableNodes.length - 1]
240
+ };
241
+ }
242
+
243
+ return undefined;
244
+ }).filter(function (group) {
245
+ return !!group;
246
+ }); // remove groups with no tabbable nodes
247
+ // throw if no groups have tabbable nodes and we don't have a fallback focus node either
248
+
249
+ if (state.tabbableGroups.length <= 0 && !getNodeForOption('fallbackFocus')) {
250
+ throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
251
+ }
252
+ };
253
+
254
+ var tryFocus = function tryFocus(node) {
255
+ if (node === doc.activeElement) {
256
+ return;
257
+ }
258
+
259
+ if (!node || !node.focus) {
260
+ tryFocus(getInitialFocusNode());
261
+ return;
262
+ }
263
+
264
+ node.focus({
265
+ preventScroll: !!config.preventScroll
266
+ });
267
+ state.mostRecentlyFocusedNode = node;
268
+
269
+ if (isSelectableInput(node)) {
270
+ node.select();
271
+ }
272
+ };
273
+
274
+ var getReturnFocusNode = function getReturnFocusNode(previousActiveElement) {
287
275
  var node = getNodeForOption('setReturnFocus');
288
276
  return node ? node : previousActiveElement;
289
- } // This needs to be done on mousedown and touchstart instead of click
277
+ }; // This needs to be done on mousedown and touchstart instead of click
290
278
  // so that it precedes the focus event.
291
279
 
292
280
 
293
- function checkPointerDown(e) {
281
+ var checkPointerDown = function checkPointerDown(e) {
294
282
  if (containersContain(e.target)) {
295
283
  // allow the click since it ocurred inside the trap
296
284
  return;
297
285
  }
298
286
 
299
- if (config.clickOutsideDeactivates) {
287
+ if (valueOrHandler(config.clickOutsideDeactivates, e)) {
300
288
  // immediately deactivate the trap
301
- deactivate({
289
+ trap.deactivate({
302
290
  // if, on deactivation, we should return focus to the node originally-focused
303
291
  // when the trap was activated (or the configured `setReturnFocus` node),
304
292
  // then assume it's also OK to return focus to the outside node that was
@@ -318,140 +306,253 @@
318
306
  // then on mobile they will be blocked anyways if `touchstart` is blocked.)
319
307
 
320
308
 
321
- if (config.allowOutsideClick && (typeof config.allowOutsideClick === 'boolean' ? config.allowOutsideClick : config.allowOutsideClick(e))) {
309
+ if (valueOrHandler(config.allowOutsideClick, e)) {
322
310
  // allow the click outside the trap to take place
323
311
  return;
324
312
  } // otherwise, prevent the click
325
313
 
326
314
 
327
315
  e.preventDefault();
328
- } // In case focus escapes the trap for some strange reason, pull it back in.
316
+ }; // In case focus escapes the trap for some strange reason, pull it back in.
329
317
 
330
318
 
331
- function checkFocusIn(e) {
332
- // In Firefox when you Tab out of an iframe the Document is briefly focused.
333
- if (containersContain(e.target) || e.target instanceof Document) {
334
- return;
335
- }
319
+ var checkFocusIn = function checkFocusIn(e) {
320
+ var targetContained = containersContain(e.target); // In Firefox when you Tab out of an iframe the Document is briefly focused.
336
321
 
337
- e.stopImmediatePropagation();
338
- tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
339
- }
340
-
341
- function checkKey(e) {
342
- if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
343
- e.preventDefault();
344
- deactivate();
345
- return;
346
- }
347
-
348
- if (isTabEvent(e)) {
349
- checkTab(e);
350
- return;
322
+ if (targetContained || e.target instanceof Document) {
323
+ if (targetContained) {
324
+ state.mostRecentlyFocusedNode = e.target;
325
+ }
326
+ } else {
327
+ // escaped! pull it back in to where it just left
328
+ e.stopImmediatePropagation();
329
+ tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
351
330
  }
352
- } // Hijack Tab events on the first and last focusable nodes of the trap,
331
+ }; // Hijack Tab events on the first and last focusable nodes of the trap,
353
332
  // in order to prevent focus from escaping. If it escapes for even a
354
333
  // moment it can end up scrolling the page and causing confusion so we
355
334
  // kind of need to capture the action at the keydown phase.
356
335
 
357
336
 
358
- function checkTab(e) {
337
+ var checkTab = function checkTab(e) {
359
338
  updateTabbableNodes();
360
339
  var destinationNode = null;
361
340
 
362
- if (e.shiftKey) {
363
- var startOfGroupIndex = state.tabbableGroups.findIndex(function (_ref) {
364
- var firstTabbableNode = _ref.firstTabbableNode;
365
- return e.target === firstTabbableNode;
341
+ if (state.tabbableGroups.length > 0) {
342
+ // make sure the target is actually contained in a group
343
+ var containerIndex = findIndex(state.tabbableGroups, function (_ref) {
344
+ var container = _ref.container;
345
+ return container.contains(e.target);
366
346
  });
367
347
 
368
- if (startOfGroupIndex >= 0) {
369
- var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
370
- var destinationGroup = state.tabbableGroups[destinationGroupIndex];
371
- destinationNode = destinationGroup.lastTabbableNode;
372
- }
373
- } else {
374
- var lastOfGroupIndex = state.tabbableGroups.findIndex(function (_ref2) {
375
- var lastTabbableNode = _ref2.lastTabbableNode;
376
- return e.target === lastTabbableNode;
377
- });
348
+ if (containerIndex < 0) {
349
+ // target not found in any group: quite possible focus has escaped the trap,
350
+ // so bring it back in to...
351
+ if (e.shiftKey) {
352
+ // ...the last node in the last group
353
+ destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
354
+ } else {
355
+ // ...the first node in the first group
356
+ destinationNode = state.tabbableGroups[0].firstTabbableNode;
357
+ }
358
+ } else if (e.shiftKey) {
359
+ // REVERSE
360
+ var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
361
+ var firstTabbableNode = _ref2.firstTabbableNode;
362
+ return e.target === firstTabbableNode;
363
+ });
364
+
365
+ if (startOfGroupIndex >= 0) {
366
+ var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
367
+ var destinationGroup = state.tabbableGroups[destinationGroupIndex];
368
+ destinationNode = destinationGroup.lastTabbableNode;
369
+ }
370
+ } else {
371
+ // FORWARD
372
+ var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
373
+ var lastTabbableNode = _ref3.lastTabbableNode;
374
+ return e.target === lastTabbableNode;
375
+ });
378
376
 
379
- if (lastOfGroupIndex >= 0) {
380
- var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
377
+ if (lastOfGroupIndex >= 0) {
378
+ var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
381
379
 
382
- var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
383
- destinationNode = _destinationGroup.firstTabbableNode;
380
+ var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
381
+ destinationNode = _destinationGroup.firstTabbableNode;
382
+ }
384
383
  }
384
+ } else {
385
+ destinationNode = getNodeForOption('fallbackFocus');
385
386
  }
386
387
 
387
388
  if (destinationNode) {
388
389
  e.preventDefault();
389
390
  tryFocus(destinationNode);
390
391
  }
391
- }
392
+ };
392
393
 
393
- function checkClick(e) {
394
- if (config.clickOutsideDeactivates) return;
395
- if (containersContain(e.target)) return;
394
+ var checkKey = function checkKey(e) {
395
+ if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
396
+ e.preventDefault();
397
+ trap.deactivate();
398
+ return;
399
+ }
396
400
 
397
- if (config.allowOutsideClick && (typeof config.allowOutsideClick === 'boolean' ? config.allowOutsideClick : config.allowOutsideClick(e))) {
401
+ if (isTabEvent(e)) {
402
+ checkTab(e);
403
+ return;
404
+ }
405
+ };
406
+
407
+ var checkClick = function checkClick(e) {
408
+ if (valueOrHandler(config.clickOutsideDeactivates, e)) {
409
+ return;
410
+ }
411
+
412
+ if (containersContain(e.target)) {
413
+ return;
414
+ }
415
+
416
+ if (valueOrHandler(config.allowOutsideClick, e)) {
398
417
  return;
399
418
  }
400
419
 
401
420
  e.preventDefault();
402
421
  e.stopImmediatePropagation();
403
- }
422
+ }; //
423
+ // EVENT LISTENERS
424
+ //
404
425
 
405
- function updateTabbableNodes() {
406
- state.tabbableGroups = state.containers.map(function (container) {
407
- var tabbableNodes = tabbable.tabbable(container);
408
- return {
409
- firstTabbableNode: tabbableNodes[0],
410
- lastTabbableNode: tabbableNodes[tabbableNodes.length - 1]
411
- };
412
- });
413
- }
414
426
 
415
- function tryFocus(node) {
416
- if (node === doc.activeElement) return;
427
+ var addListeners = function addListeners() {
428
+ if (!state.active) {
429
+ return;
430
+ } // There can be only one listening focus trap at a time
417
431
 
418
- if (!node || !node.focus) {
432
+
433
+ activeFocusTraps.activateTrap(trap); // Delay ensures that the focused element doesn't capture the event
434
+ // that caused the focus trap activation.
435
+
436
+ activeFocusDelay = config.delayInitialFocus ? delay(function () {
419
437
  tryFocus(getInitialFocusNode());
438
+ }) : tryFocus(getInitialFocusNode());
439
+ doc.addEventListener('focusin', checkFocusIn, true);
440
+ doc.addEventListener('mousedown', checkPointerDown, {
441
+ capture: true,
442
+ passive: false
443
+ });
444
+ doc.addEventListener('touchstart', checkPointerDown, {
445
+ capture: true,
446
+ passive: false
447
+ });
448
+ doc.addEventListener('click', checkClick, {
449
+ capture: true,
450
+ passive: false
451
+ });
452
+ doc.addEventListener('keydown', checkKey, {
453
+ capture: true,
454
+ passive: false
455
+ });
456
+ return trap;
457
+ };
458
+
459
+ var removeListeners = function removeListeners() {
460
+ if (!state.active) {
420
461
  return;
421
462
  }
422
463
 
423
- node.focus({
424
- preventScroll: !!config.preventScroll
425
- });
426
- state.mostRecentlyFocusedNode = node;
464
+ doc.removeEventListener('focusin', checkFocusIn, true);
465
+ doc.removeEventListener('mousedown', checkPointerDown, true);
466
+ doc.removeEventListener('touchstart', checkPointerDown, true);
467
+ doc.removeEventListener('click', checkClick, true);
468
+ doc.removeEventListener('keydown', checkKey, true);
469
+ return trap;
470
+ }; //
471
+ // TRAP DEFINITION
472
+ //
427
473
 
428
- if (isSelectableInput(node)) {
429
- node.select();
430
- }
431
- }
432
474
 
433
- function containersContain(element) {
434
- return state.containers.some(function (container) {
435
- return container.contains(element);
436
- });
437
- }
438
- }
475
+ trap = {
476
+ activate: function activate(activateOptions) {
477
+ if (state.active) {
478
+ return this;
479
+ }
439
480
 
440
- function isSelectableInput(node) {
441
- return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
442
- }
481
+ updateTabbableNodes();
482
+ state.active = true;
483
+ state.paused = false;
484
+ state.nodeFocusedBeforeActivation = doc.activeElement;
485
+ var onActivate = activateOptions && activateOptions.onActivate ? activateOptions.onActivate : config.onActivate;
443
486
 
444
- function isEscapeEvent(e) {
445
- return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
446
- }
487
+ if (onActivate) {
488
+ onActivate();
489
+ }
447
490
 
448
- function isTabEvent(e) {
449
- return e.key === 'Tab' || e.keyCode === 9;
450
- }
491
+ addListeners();
492
+ return this;
493
+ },
494
+ deactivate: function deactivate(deactivateOptions) {
495
+ if (!state.active) {
496
+ return this;
497
+ }
451
498
 
452
- function delay(fn) {
453
- return setTimeout(fn, 0);
454
- }
499
+ clearTimeout(activeFocusDelay);
500
+ removeListeners();
501
+ state.active = false;
502
+ state.paused = false;
503
+ activeFocusTraps.deactivateTrap(trap);
504
+ var onDeactivate = deactivateOptions && deactivateOptions.onDeactivate !== undefined ? deactivateOptions.onDeactivate : config.onDeactivate;
505
+
506
+ if (onDeactivate) {
507
+ onDeactivate();
508
+ }
509
+
510
+ var returnFocus = deactivateOptions && deactivateOptions.returnFocus !== undefined ? deactivateOptions.returnFocus : config.returnFocusOnDeactivate;
511
+
512
+ if (returnFocus) {
513
+ delay(function () {
514
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
515
+ });
516
+ }
517
+
518
+ return this;
519
+ },
520
+ pause: function pause() {
521
+ if (state.paused || !state.active) {
522
+ return this;
523
+ }
524
+
525
+ state.paused = true;
526
+ removeListeners();
527
+ return this;
528
+ },
529
+ unpause: function unpause() {
530
+ if (!state.paused || !state.active) {
531
+ return this;
532
+ }
533
+
534
+ state.paused = false;
535
+ updateTabbableNodes();
536
+ addListeners();
537
+ return this;
538
+ },
539
+ updateContainerElements: function updateContainerElements(containerElements) {
540
+ var elementsAsArray = [].concat(containerElements).filter(Boolean);
541
+ state.containers = elementsAsArray.map(function (element) {
542
+ return typeof element === 'string' ? doc.querySelector(element) : element;
543
+ });
544
+
545
+ if (state.active) {
546
+ updateTabbableNodes();
547
+ }
548
+
549
+ return this;
550
+ }
551
+ }; // initialize container elements
552
+
553
+ trap.updateContainerElements(elements);
554
+ return trap;
555
+ };
455
556
 
456
557
  exports.createFocusTrap = createFocusTrap;
457
558