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