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