@getflip/swirl-components 0.172.0 → 0.172.1

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,15 +1,64 @@
1
1
  /*!
2
- * tabbable 6.0.1
2
+ * tabbable 6.2.0
3
3
  * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
4
4
  */
5
- var candidateSelectors = ['input', 'select', 'textarea', 'a[href]', 'button', '[tabindex]:not(slot)', 'audio[controls]', 'video[controls]', '[contenteditable]:not([contenteditable="false"])', 'details>summary:first-of-type', 'details'];
5
+ // NOTE: separate `:not()` selectors has broader browser support than the newer
6
+ // `:not([inert], [inert] *)` (Feb 2023)
7
+ // CAREFUL: JSDom does not support `:not([inert] *)` as a selector; using it causes
8
+ // the entire query to fail, resulting in no nodes found, which will break a lot
9
+ // of things... so we have to rely on JS to identify nodes inside an inert container
10
+ var candidateSelectors = ['input:not([inert])', 'select:not([inert])', 'textarea:not([inert])', 'a[href]:not([inert])', 'button:not([inert])', '[tabindex]:not(slot):not([inert])', 'audio[controls]:not([inert])', 'video[controls]:not([inert])', '[contenteditable]:not([contenteditable="false"]):not([inert])', 'details>summary:first-of-type:not([inert])', 'details:not([inert])'];
6
11
  var candidateSelector = /* #__PURE__ */candidateSelectors.join(',');
7
12
  var NoElement = typeof Element === 'undefined';
8
13
  var matches = NoElement ? function () {} : Element.prototype.matches || Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
9
14
  var getRootNode = !NoElement && Element.prototype.getRootNode ? function (element) {
10
- return element.getRootNode();
15
+ var _element$getRootNode;
16
+ return element === null || element === void 0 ? void 0 : (_element$getRootNode = element.getRootNode) === null || _element$getRootNode === void 0 ? void 0 : _element$getRootNode.call(element);
11
17
  } : function (element) {
12
- return element.ownerDocument;
18
+ return element === null || element === void 0 ? void 0 : element.ownerDocument;
19
+ };
20
+
21
+ /**
22
+ * Determines if a node is inert or in an inert ancestor.
23
+ * @param {Element} [node]
24
+ * @param {boolean} [lookUp] If true and `node` is not inert, looks up at ancestors to
25
+ * see if any of them are inert. If false, only `node` itself is considered.
26
+ * @returns {boolean} True if inert itself or by way of being in an inert ancestor.
27
+ * False if `node` is falsy.
28
+ */
29
+ var isInert = function isInert(node, lookUp) {
30
+ var _node$getAttribute;
31
+ if (lookUp === void 0) {
32
+ lookUp = true;
33
+ }
34
+ // CAREFUL: JSDom does not support inert at all, so we can't use the `HTMLElement.inert`
35
+ // JS API property; we have to check the attribute, which can either be empty or 'true';
36
+ // if it's `null` (not specified) or 'false', it's an active element
37
+ var inertAtt = node === null || node === void 0 ? void 0 : (_node$getAttribute = node.getAttribute) === null || _node$getAttribute === void 0 ? void 0 : _node$getAttribute.call(node, 'inert');
38
+ var inert = inertAtt === '' || inertAtt === 'true';
39
+
40
+ // NOTE: this could also be handled with `node.matches('[inert], :is([inert] *)')`
41
+ // if it weren't for `matches()` not being a function on shadow roots; the following
42
+ // code works for any kind of node
43
+ // CAREFUL: JSDom does not appear to support certain selectors like `:not([inert] *)`
44
+ // so it likely would not support `:is([inert] *)` either...
45
+ var result = inert || lookUp && node && isInert(node.parentNode); // recursive
46
+
47
+ return result;
48
+ };
49
+
50
+ /**
51
+ * Determines if a node's content is editable.
52
+ * @param {Element} [node]
53
+ * @returns True if it's content-editable; false if it's not or `node` is falsy.
54
+ */
55
+ var isContentEditable = function isContentEditable(node) {
56
+ var _node$getAttribute2;
57
+ // CAREFUL: JSDom does not support the `HTMLElement.isContentEditable` API so we have
58
+ // to use the attribute directly to check for this, which can either be empty or 'true';
59
+ // if it's `null` (not specified) or 'false', it's a non-editable element
60
+ var attValue = node === null || node === void 0 ? void 0 : (_node$getAttribute2 = node.getAttribute) === null || _node$getAttribute2 === void 0 ? void 0 : _node$getAttribute2.call(node, 'contenteditable');
61
+ return attValue === '' || attValue === 'true';
13
62
  };
14
63
 
15
64
  /**
@@ -19,6 +68,11 @@ var getRootNode = !NoElement && Element.prototype.getRootNode ? function (elemen
19
68
  * @returns {Element[]}
20
69
  */
21
70
  var getCandidates = function getCandidates(el, includeContainer, filter) {
71
+ // even if `includeContainer=false`, we still have to check it for inertness because
72
+ // if it's inert, all its children are inert
73
+ if (isInert(el)) {
74
+ return [];
75
+ }
22
76
  var candidates = Array.prototype.slice.apply(el.querySelectorAll(candidateSelector));
23
77
  if (includeContainer && matches.call(el, candidateSelector)) {
24
78
  candidates.unshift(el);
@@ -66,6 +120,11 @@ var getCandidatesIteratively = function getCandidatesIteratively(elements, inclu
66
120
  var elementsToCheck = Array.from(elements);
67
121
  while (elementsToCheck.length) {
68
122
  var element = elementsToCheck.shift();
123
+ if (isInert(element, false)) {
124
+ // no need to look up since we're drilling down
125
+ // anything inside this container will also be inert
126
+ continue;
127
+ }
69
128
  if (element.tagName === 'SLOT') {
70
129
  // add shadow dom slot scope (slot itself cannot be focusable)
71
130
  var assigned = element.assignedElements();
@@ -90,7 +149,11 @@ var getCandidatesIteratively = function getCandidatesIteratively(elements, inclu
90
149
  var shadowRoot = element.shadowRoot ||
91
150
  // check for an undisclosed shadow
92
151
  typeof options.getShadowRoot === 'function' && options.getShadowRoot(element);
93
- var validShadowRoot = !options.shadowRootFilter || options.shadowRootFilter(element);
152
+
153
+ // no inert look up because we're already drilling down and checking for inertness
154
+ // on the way down, so all containers to this root node should have already been
155
+ // vetted as non-inert
156
+ var validShadowRoot = !isInert(shadowRoot, false) && (!options.shadowRootFilter || options.shadowRootFilter(element));
94
157
  if (shadowRoot && validShadowRoot) {
95
158
  // add shadow dom scope IIF a shadow root node was given; otherwise, an undisclosed
96
159
  // shadow exists, so look at light dom children as fallback BUT create a scope for any
@@ -116,7 +179,27 @@ var getCandidatesIteratively = function getCandidatesIteratively(elements, inclu
116
179
  }
117
180
  return candidates;
118
181
  };
119
- var getTabindex = function getTabindex(node, isScope) {
182
+
183
+ /**
184
+ * @private
185
+ * Determines if the node has an explicitly specified `tabindex` attribute.
186
+ * @param {HTMLElement} node
187
+ * @returns {boolean} True if so; false if not.
188
+ */
189
+ var hasTabIndex = function hasTabIndex(node) {
190
+ return !isNaN(parseInt(node.getAttribute('tabindex'), 10));
191
+ };
192
+
193
+ /**
194
+ * Determine the tab index of a given node.
195
+ * @param {HTMLElement} node
196
+ * @returns {number} Tab order (negative, 0, or positive number).
197
+ * @throws {Error} If `node` is falsy.
198
+ */
199
+ var getTabIndex = function getTabIndex(node) {
200
+ if (!node) {
201
+ throw new Error('No node provided');
202
+ }
120
203
  if (node.tabIndex < 0) {
121
204
  // in Chrome, <details/>, <audio controls/> and <video controls/> elements get a default
122
205
  // `tabIndex` of -1 when the 'tabindex' attribute isn't specified in the DOM,
@@ -125,16 +208,28 @@ var getTabindex = function getTabindex(node, isScope) {
125
208
  // order, consider their tab index to be 0.
126
209
  // Also browsers do not return `tabIndex` correctly for contentEditable nodes;
127
210
  // so if they don't have a tabindex attribute specifically set, assume it's 0.
128
- //
129
- // isScope is positive for custom element with shadow root or slot that by default
130
- // have tabIndex -1, but need to be sorted by document order in order for their
131
- // content to be inserted in the correct position
132
- if ((isScope || /^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || node.isContentEditable) && isNaN(parseInt(node.getAttribute('tabindex'), 10))) {
211
+ if ((/^(AUDIO|VIDEO|DETAILS)$/.test(node.tagName) || isContentEditable(node)) && !hasTabIndex(node)) {
133
212
  return 0;
134
213
  }
135
214
  }
136
215
  return node.tabIndex;
137
216
  };
217
+
218
+ /**
219
+ * Determine the tab index of a given node __for sort order purposes__.
220
+ * @param {HTMLElement} node
221
+ * @param {boolean} [isScope] True for a custom element with shadow root or slot that, by default,
222
+ * has tabIndex -1, but needs to be sorted by document order in order for its content to be
223
+ * inserted into the correct sort position.
224
+ * @returns {number} Tab order (negative, 0, or positive number).
225
+ */
226
+ var getSortOrderTabIndex = function getSortOrderTabIndex(node, isScope) {
227
+ var tabIndex = getTabIndex(node);
228
+ if (tabIndex < 0 && isScope && !hasTabIndex(node)) {
229
+ return 0;
230
+ }
231
+ return tabIndex;
232
+ };
138
233
  var sortOrderedTabbables = function sortOrderedTabbables(a, b) {
139
234
  return a.tabIndex === b.tabIndex ? a.documentOrder - b.documentOrder : a.tabIndex - b.tabIndex;
140
235
  };
@@ -189,7 +284,7 @@ var isNonTabbableRadio = function isNonTabbableRadio(node) {
189
284
 
190
285
  // determines if a node is ultimately attached to the window's document
191
286
  var isNodeAttached = function isNodeAttached(node) {
192
- var _nodeRootHost;
287
+ var _nodeRoot;
193
288
  // The root node is the shadow root if the node is in a shadow DOM; some document otherwise
194
289
  // (but NOT _the_ document; see second 'If' comment below for more).
195
290
  // If rootNode is shadow root, it'll have a host, which is the element to which the shadow
@@ -209,15 +304,28 @@ var isNodeAttached = function isNodeAttached(node) {
209
304
  // to ignore the rootNode at this point, and use `node.ownerDocument`. Otherwise,
210
305
  // using `rootNode.contains(node)` will _always_ be true we'll get false-positives when
211
306
  // node is actually detached.
212
- var nodeRootHost = getRootNode(node).host;
213
- var attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && _nodeRootHost.ownerDocument.contains(nodeRootHost) || node.ownerDocument.contains(node));
214
- while (!attached && nodeRootHost) {
215
- var _nodeRootHost2;
216
- // since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
217
- // which means we need to get the host's host and check if that parent host is contained
218
- // in (i.e. attached to) the document
219
- nodeRootHost = getRootNode(nodeRootHost).host;
220
- attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && _nodeRootHost2.ownerDocument.contains(nodeRootHost));
307
+ // NOTE: If `nodeRootHost` or `node` happens to be the `document` itself (which is possible
308
+ // if a tabbable/focusable node was quickly added to the DOM, focused, and then removed
309
+ // from the DOM as in https://github.com/focus-trap/focus-trap-react/issues/905), then
310
+ // `ownerDocument` will be `null`, hence the optional chaining on it.
311
+ var nodeRoot = node && getRootNode(node);
312
+ var nodeRootHost = (_nodeRoot = nodeRoot) === null || _nodeRoot === void 0 ? void 0 : _nodeRoot.host;
313
+
314
+ // in some cases, a detached node will return itself as the root instead of a document or
315
+ // shadow root object, in which case, we shouldn't try to look further up the host chain
316
+ var attached = false;
317
+ if (nodeRoot && nodeRoot !== node) {
318
+ var _nodeRootHost, _nodeRootHost$ownerDo, _node$ownerDocument;
319
+ attached = !!((_nodeRootHost = nodeRootHost) !== null && _nodeRootHost !== void 0 && (_nodeRootHost$ownerDo = _nodeRootHost.ownerDocument) !== null && _nodeRootHost$ownerDo !== void 0 && _nodeRootHost$ownerDo.contains(nodeRootHost) || node !== null && node !== void 0 && (_node$ownerDocument = node.ownerDocument) !== null && _node$ownerDocument !== void 0 && _node$ownerDocument.contains(node));
320
+ while (!attached && nodeRootHost) {
321
+ var _nodeRoot2, _nodeRootHost2, _nodeRootHost2$ownerD;
322
+ // since it's not attached and we have a root host, the node MUST be in a nested shadow DOM,
323
+ // which means we need to get the host's host and check if that parent host is contained
324
+ // in (i.e. attached to) the document
325
+ nodeRoot = getRootNode(nodeRootHost);
326
+ nodeRootHost = (_nodeRoot2 = nodeRoot) === null || _nodeRoot2 === void 0 ? void 0 : _nodeRoot2.host;
327
+ attached = !!((_nodeRootHost2 = nodeRootHost) !== null && _nodeRootHost2 !== void 0 && (_nodeRootHost2$ownerD = _nodeRootHost2.ownerDocument) !== null && _nodeRootHost2$ownerD !== void 0 && _nodeRootHost2$ownerD.contains(nodeRootHost));
328
+ }
221
329
  }
222
330
  return attached;
223
331
  };
@@ -352,7 +460,11 @@ var isDisabledFromFieldset = function isDisabledFromFieldset(node) {
352
460
  return false;
353
461
  };
354
462
  var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(options, node) {
355
- if (node.disabled || isHiddenInput(node) || isHidden(node, options) ||
463
+ if (node.disabled ||
464
+ // we must do an inert look up to filter out any elements inside an inert ancestor
465
+ // because we're limited in the type of selectors we can use in JSDom (see related
466
+ // note related to `candidateSelectors`)
467
+ isInert(node) || isHiddenInput(node) || isHidden(node, options) ||
356
468
  // For a details element with a summary, the summary element gets the focus
357
469
  isDetailsWithSummary(node) || isDisabledFromFieldset(node)) {
358
470
  return false;
@@ -360,7 +472,7 @@ var isNodeMatchingSelectorFocusable = function isNodeMatchingSelectorFocusable(o
360
472
  return true;
361
473
  };
362
474
  var isNodeMatchingSelectorTabbable = function isNodeMatchingSelectorTabbable(options, node) {
363
- if (isNonTabbableRadio(node) || getTabindex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
475
+ if (isNonTabbableRadio(node) || getTabIndex(node) < 0 || !isNodeMatchingSelectorFocusable(options, node)) {
364
476
  return false;
365
477
  }
366
478
  return true;
@@ -385,7 +497,7 @@ var sortByOrder = function sortByOrder(candidates) {
385
497
  candidates.forEach(function (item, i) {
386
498
  var isScope = !!item.scopeParent;
387
499
  var element = isScope ? item.scopeParent : item;
388
- var candidateTabindex = getTabindex(element, isScope);
500
+ var candidateTabindex = getSortOrderTabIndex(element, isScope);
389
501
  var elements = isScope ? sortByOrder(item.candidates) : element;
390
502
  if (candidateTabindex === 0) {
391
503
  isScope ? regularTabbables.push.apply(regularTabbables, elements) : regularTabbables.push(element);
@@ -404,32 +516,32 @@ var sortByOrder = function sortByOrder(candidates) {
404
516
  return acc;
405
517
  }, []).concat(regularTabbables);
406
518
  };
407
- var tabbable = function tabbable(el, options) {
519
+ var tabbable = function tabbable(container, options) {
408
520
  options = options || {};
409
521
  var candidates;
410
522
  if (options.getShadowRoot) {
411
- candidates = getCandidatesIteratively([el], options.includeContainer, {
523
+ candidates = getCandidatesIteratively([container], options.includeContainer, {
412
524
  filter: isNodeMatchingSelectorTabbable.bind(null, options),
413
525
  flatten: false,
414
526
  getShadowRoot: options.getShadowRoot,
415
527
  shadowRootFilter: isValidShadowRootTabbable
416
528
  });
417
529
  } else {
418
- candidates = getCandidates(el, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
530
+ candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorTabbable.bind(null, options));
419
531
  }
420
532
  return sortByOrder(candidates);
421
533
  };
422
- var focusable = function focusable(el, options) {
534
+ var focusable = function focusable(container, options) {
423
535
  options = options || {};
424
536
  var candidates;
425
537
  if (options.getShadowRoot) {
426
- candidates = getCandidatesIteratively([el], options.includeContainer, {
538
+ candidates = getCandidatesIteratively([container], options.includeContainer, {
427
539
  filter: isNodeMatchingSelectorFocusable.bind(null, options),
428
540
  flatten: true,
429
541
  getShadowRoot: options.getShadowRoot
430
542
  });
431
543
  } else {
432
- candidates = getCandidates(el, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
544
+ candidates = getCandidates(container, options.includeContainer, isNodeMatchingSelectorFocusable.bind(null, options));
433
545
  }
434
546
  return candidates;
435
547
  };
@@ -456,32 +568,33 @@ var isFocusable = function isFocusable(node, options) {
456
568
  };
457
569
 
458
570
  /*!
459
- * focus-trap 7.1.0
571
+ * focus-trap 7.5.4
460
572
  * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
461
573
  */
462
574
 
463
- function ownKeys(object, enumerableOnly) {
464
- var keys = Object.keys(object);
575
+ function ownKeys(e, r) {
576
+ var t = Object.keys(e);
465
577
  if (Object.getOwnPropertySymbols) {
466
- var symbols = Object.getOwnPropertySymbols(object);
467
- enumerableOnly && (symbols = symbols.filter(function (sym) {
468
- return Object.getOwnPropertyDescriptor(object, sym).enumerable;
469
- })), keys.push.apply(keys, symbols);
578
+ var o = Object.getOwnPropertySymbols(e);
579
+ r && (o = o.filter(function (r) {
580
+ return Object.getOwnPropertyDescriptor(e, r).enumerable;
581
+ })), t.push.apply(t, o);
470
582
  }
471
- return keys;
583
+ return t;
472
584
  }
473
- function _objectSpread2(target) {
474
- for (var i = 1; i < arguments.length; i++) {
475
- var source = null != arguments[i] ? arguments[i] : {};
476
- i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
477
- _defineProperty(target, key, source[key]);
478
- }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
479
- Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
585
+ function _objectSpread2(e) {
586
+ for (var r = 1; r < arguments.length; r++) {
587
+ var t = null != arguments[r] ? arguments[r] : {};
588
+ r % 2 ? ownKeys(Object(t), !0).forEach(function (r) {
589
+ _defineProperty(e, r, t[r]);
590
+ }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) {
591
+ Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r));
480
592
  });
481
593
  }
482
- return target;
594
+ return e;
483
595
  }
484
596
  function _defineProperty(obj, key, value) {
597
+ key = _toPropertyKey(key);
485
598
  if (key in obj) {
486
599
  Object.defineProperty(obj, key, {
487
600
  value: value,
@@ -494,8 +607,21 @@ function _defineProperty(obj, key, value) {
494
607
  }
495
608
  return obj;
496
609
  }
610
+ function _toPrimitive(input, hint) {
611
+ if (typeof input !== "object" || input === null) return input;
612
+ var prim = input[Symbol.toPrimitive];
613
+ if (prim !== undefined) {
614
+ var res = prim.call(input, hint || "default");
615
+ if (typeof res !== "object") return res;
616
+ throw new TypeError("@@toPrimitive must return a primitive value.");
617
+ }
618
+ return (hint === "string" ? String : Number)(input);
619
+ }
620
+ function _toPropertyKey(arg) {
621
+ var key = _toPrimitive(arg, "string");
622
+ return typeof key === "symbol" ? key : String(key);
623
+ }
497
624
 
498
- var rooTrapStack = [];
499
625
  var activeFocusTraps = {
500
626
  activateTrap: function activateTrap(trapStack, trap) {
501
627
  if (trapStack.length > 0) {
@@ -527,10 +653,20 @@ var isSelectableInput = function isSelectableInput(node) {
527
653
  return node.tagName && node.tagName.toLowerCase() === 'input' && typeof node.select === 'function';
528
654
  };
529
655
  var isEscapeEvent = function isEscapeEvent(e) {
530
- return e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27;
656
+ return (e === null || e === void 0 ? void 0 : e.key) === 'Escape' || (e === null || e === void 0 ? void 0 : e.key) === 'Esc' || (e === null || e === void 0 ? void 0 : e.keyCode) === 27;
531
657
  };
532
658
  var isTabEvent = function isTabEvent(e) {
533
- return e.key === 'Tab' || e.keyCode === 9;
659
+ return (e === null || e === void 0 ? void 0 : e.key) === 'Tab' || (e === null || e === void 0 ? void 0 : e.keyCode) === 9;
660
+ };
661
+
662
+ // checks for TAB by default
663
+ var isKeyForward = function isKeyForward(e) {
664
+ return isTabEvent(e) && !e.shiftKey;
665
+ };
666
+
667
+ // checks for SHIFT+TAB by default
668
+ var isKeyBackward = function isKeyBackward(e) {
669
+ return isTabEvent(e) && e.shiftKey;
534
670
  };
535
671
  var delay = function delay(fn) {
536
672
  return setTimeout(fn, 0);
@@ -575,15 +711,21 @@ var getActualTarget = function getActualTarget(event) {
575
711
  // composedPath()[0] === event.target always).
576
712
  return event.target.shadowRoot && typeof event.composedPath === 'function' ? event.composedPath()[0] : event.target;
577
713
  };
714
+
715
+ // NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
716
+ // current instance use the same stack if `userOptions.trapStack` isn't specified
717
+ var internalTrapStack = [];
578
718
  var createFocusTrap = function createFocusTrap(elements, userOptions) {
579
719
  // SSR: a live trap shouldn't be created in this type of environment so this
580
720
  // should be safe code to execute if the `document` option isn't specified
581
721
  var doc = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.document) || document;
582
- var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || rooTrapStack;
722
+ var trapStack = (userOptions === null || userOptions === void 0 ? void 0 : userOptions.trapStack) || internalTrapStack;
583
723
  var config = _objectSpread2({
584
724
  returnFocusOnDeactivate: true,
585
725
  escapeDeactivates: true,
586
- delayInitialFocus: true
726
+ delayInitialFocus: true,
727
+ isKeyForward: isKeyForward,
728
+ isKeyBackward: isKeyBackward
587
729
  }, userOptions);
588
730
  var state = {
589
731
  // containers given to createFocusTrap()
@@ -598,8 +740,11 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
598
740
  // container: HTMLElement,
599
741
  // tabbableNodes: Array<HTMLElement>, // empty if none
600
742
  // focusableNodes: Array<HTMLElement>, // empty if none
601
- // firstTabbableNode: HTMLElement|null,
602
- // lastTabbableNode: HTMLElement|null,
743
+ // posTabIndexesFound: boolean,
744
+ // firstTabbableNode: HTMLElement|undefined,
745
+ // lastTabbableNode: HTMLElement|undefined,
746
+ // firstDomTabbableNode: HTMLElement|undefined,
747
+ // lastDomTabbableNode: HTMLElement|undefined,
603
748
  // nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
604
749
  // }>}
605
750
  containerGroups: [],
@@ -616,7 +761,9 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
616
761
  paused: false,
617
762
  // timer ID for when delayInitialFocus is true and initial focus in this trap
618
763
  // has been delayed during activation
619
- delayInitialFocusTimer: undefined
764
+ delayInitialFocusTimer: undefined,
765
+ // the most recent KeyboardEvent for the configured nav key (typically [SHIFT+]TAB), if any
766
+ recentNavEvent: undefined
620
767
  };
621
768
  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
622
769
 
@@ -635,23 +782,26 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
635
782
  /**
636
783
  * Finds the index of the container that contains the element.
637
784
  * @param {HTMLElement} element
785
+ * @param {Event} [event] If available, and `element` isn't directly found in any container,
786
+ * the event's composed path is used to see if includes any known trap containers in the
787
+ * case where the element is inside a Shadow DOM.
638
788
  * @returns {number} Index of the container in either `state.containers` or
639
789
  * `state.containerGroups` (the order/length of these lists are the same); -1
640
790
  * if the element isn't found.
641
791
  */
642
- var findContainerIndex = function findContainerIndex(element) {
792
+ var findContainerIndex = function findContainerIndex(element, event) {
793
+ var composedPath = typeof (event === null || event === void 0 ? void 0 : event.composedPath) === 'function' ? event.composedPath() : undefined;
643
794
  // NOTE: search `containerGroups` because it's possible a group contains no tabbable
644
795
  // nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
645
796
  // and we still need to find the element in there
646
797
  return state.containerGroups.findIndex(function (_ref) {
647
798
  var container = _ref.container,
648
799
  tabbableNodes = _ref.tabbableNodes;
649
- return container.contains(element) ||
650
- // fall back to explicit tabbable search which will take into consideration any
800
+ return container.contains(element) || ( // fall back to explicit tabbable search which will take into consideration any
651
801
  // web components if the `tabbableOptions.getShadowRoot` option was used for
652
802
  // the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
653
803
  // look inside web components even if open)
654
- tabbableNodes.find(function (node) {
804
+ composedPath === null || composedPath === void 0 ? void 0 : composedPath.includes(container)) || tabbableNodes.find(function (node) {
655
805
  return node === element;
656
806
  });
657
807
  });
@@ -707,8 +857,8 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
707
857
  if (node === false) {
708
858
  return false;
709
859
  }
710
- if (node === undefined) {
711
- // option not specified: use fallback options
860
+ if (node === undefined || !isFocusable(node, config.tabbableOptions)) {
861
+ // option not specified nor focusable: use fallback options
712
862
  if (findContainerIndex(doc.activeElement) >= 0) {
713
863
  node = doc.activeElement;
714
864
  } else {
@@ -729,14 +879,41 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
729
879
  var tabbableNodes = tabbable(container, config.tabbableOptions);
730
880
 
731
881
  // NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
732
- // are a superset of tabbable nodes
882
+ // are a superset of tabbable nodes since nodes with negative `tabindex` attributes
883
+ // are focusable but not tabbable
733
884
  var focusableNodes = focusable(container, config.tabbableOptions);
885
+ var firstTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[0] : undefined;
886
+ var lastTabbableNode = tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : undefined;
887
+ var firstDomTabbableNode = focusableNodes.find(function (node) {
888
+ return isTabbable(node);
889
+ });
890
+ var lastDomTabbableNode = focusableNodes.slice().reverse().find(function (node) {
891
+ return isTabbable(node);
892
+ });
893
+ var posTabIndexesFound = !!tabbableNodes.find(function (node) {
894
+ return getTabIndex(node) > 0;
895
+ });
734
896
  return {
735
897
  container: container,
736
898
  tabbableNodes: tabbableNodes,
737
899
  focusableNodes: focusableNodes,
738
- firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
739
- lastTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[tabbableNodes.length - 1] : null,
900
+ /** True if at least one node with positive `tabindex` was found in this container. */
901
+ posTabIndexesFound: posTabIndexesFound,
902
+ /** First tabbable node in container, __tabindex__ order; `undefined` if none. */
903
+ firstTabbableNode: firstTabbableNode,
904
+ /** Last tabbable node in container, __tabindex__ order; `undefined` if none. */
905
+ lastTabbableNode: lastTabbableNode,
906
+ // NOTE: DOM order is NOT NECESSARILY "document position" order, but figuring that out
907
+ // would require more than just https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition
908
+ // because that API doesn't work with Shadow DOM as well as it should (@see
909
+ // https://github.com/whatwg/dom/issues/320) and since this first/last is only needed, so far,
910
+ // to address an edge case related to positive tabindex support, this seems like a much easier,
911
+ // "close enough most of the time" alternative for positive tabindexes which should generally
912
+ // be avoided anyway...
913
+ /** First tabbable node in container, __DOM__ order; `undefined` if none. */
914
+ firstDomTabbableNode: firstDomTabbableNode,
915
+ /** Last tabbable node in container, __DOM__ order; `undefined` if none. */
916
+ lastDomTabbableNode: lastDomTabbableNode,
740
917
  /**
741
918
  * Finds the __tabbable__ node that follows the given node in the specified direction,
742
919
  * in this container, if any.
@@ -747,30 +924,24 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
747
924
  */
748
925
  nextTabbableNode: function nextTabbableNode(node) {
749
926
  var forward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
750
- // NOTE: If tabindex is positive (in order to manipulate the tab order separate
751
- // from the DOM order), this __will not work__ because the list of focusableNodes,
752
- // while it contains tabbable nodes, does not sort its nodes in any order other
753
- // than DOM order, because it can't: Where would you place focusable (but not
754
- // tabbable) nodes in that order? They have no order, because they aren't tabbale...
755
- // Support for positive tabindex is already broken and hard to manage (possibly
756
- // not supportable, TBD), so this isn't going to make things worse than they
757
- // already are, and at least makes things better for the majority of cases where
758
- // tabindex is either 0/unset or negative.
759
- // FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
760
- var nodeIdx = focusableNodes.findIndex(function (n) {
761
- return n === node;
762
- });
927
+ var nodeIdx = tabbableNodes.indexOf(node);
763
928
  if (nodeIdx < 0) {
764
- return undefined;
765
- }
766
- if (forward) {
767
- return focusableNodes.slice(nodeIdx + 1).find(function (n) {
768
- return isTabbable(n, config.tabbableOptions);
929
+ // either not tabbable nor focusable, or was focused but not tabbable (negative tabindex):
930
+ // since `node` should at least have been focusable, we assume that's the case and mimic
931
+ // what browsers do, which is set focus to the next node in __document position order__,
932
+ // regardless of positive tabindexes, if any -- and for reasons explained in the NOTE
933
+ // above related to `firstDomTabbable` and `lastDomTabbable` properties, we fall back to
934
+ // basic DOM order
935
+ if (forward) {
936
+ return focusableNodes.slice(focusableNodes.indexOf(node) + 1).find(function (el) {
937
+ return isTabbable(el);
938
+ });
939
+ }
940
+ return focusableNodes.slice(0, focusableNodes.indexOf(node)).reverse().find(function (el) {
941
+ return isTabbable(el);
769
942
  });
770
943
  }
771
- return focusableNodes.slice(0, nodeIdx).reverse().find(function (n) {
772
- return isTabbable(n, config.tabbableOptions);
773
- });
944
+ return tabbableNodes[nodeIdx + (forward ? 1 : -1)];
774
945
  }
775
946
  };
776
947
  });
@@ -783,12 +954,44 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
783
954
  ) {
784
955
  throw new Error('Your focus-trap must have at least one container with at least one tabbable node in it at all times');
785
956
  }
957
+
958
+ // NOTE: Positive tabindexes are only properly supported in single-container traps because
959
+ // doing it across multiple containers where tabindexes could be all over the place
960
+ // would require Tabbable to support multiple containers, would require additional
961
+ // specialized Shadow DOM support, and would require Tabbable's multi-container support
962
+ // to look at those containers in document position order rather than user-provided
963
+ // order (as they are treated in Focus-trap, for legacy reasons). See discussion on
964
+ // https://github.com/focus-trap/focus-trap/issues/375 for more details.
965
+ if (state.containerGroups.find(function (g) {
966
+ return g.posTabIndexesFound;
967
+ }) && state.containerGroups.length > 1) {
968
+ throw new Error("At least one node with a positive tabindex was found in one of your focus-trap's multiple containers. Positive tabindexes are only supported in single-container focus-traps.");
969
+ }
970
+ };
971
+
972
+ /**
973
+ * Gets the current activeElement. If it's a web-component and has open shadow-root
974
+ * it will recursively search inside shadow roots for the "true" activeElement.
975
+ *
976
+ * @param {Document | ShadowRoot} el
977
+ *
978
+ * @returns {HTMLElement} The element that currently has the focus
979
+ **/
980
+ var getActiveElement = function getActiveElement(el) {
981
+ var activeElement = el.activeElement;
982
+ if (!activeElement) {
983
+ return;
984
+ }
985
+ if (activeElement.shadowRoot && activeElement.shadowRoot.activeElement !== null) {
986
+ return getActiveElement(activeElement.shadowRoot);
987
+ }
988
+ return activeElement;
786
989
  };
787
990
  var tryFocus = function tryFocus(node) {
788
991
  if (node === false) {
789
992
  return;
790
993
  }
791
- if (node === doc.activeElement) {
994
+ if (node === getActiveElement(document)) {
792
995
  return;
793
996
  }
794
997
  if (!node || !node.focus) {
@@ -798,6 +1001,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
798
1001
  node.focus({
799
1002
  preventScroll: !!config.preventScroll
800
1003
  });
1004
+ // NOTE: focus() API does not trigger focusIn event so set MRU node manually
801
1005
  state.mostRecentlyFocusedNode = node;
802
1006
  if (isSelectableInput(node)) {
803
1007
  node.select();
@@ -808,92 +1012,47 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
808
1012
  return node ? node : node === false ? false : previousActiveElement;
809
1013
  };
810
1014
 
811
- // This needs to be done on mousedown and touchstart instead of click
812
- // so that it precedes the focus event.
813
- var checkPointerDown = function checkPointerDown(e) {
814
- var target = getActualTarget(e);
815
- if (findContainerIndex(target) >= 0) {
816
- // allow the click since it ocurred inside the trap
817
- return;
818
- }
819
- if (valueOrHandler(config.clickOutsideDeactivates, e)) {
820
- // immediately deactivate the trap
821
- trap.deactivate({
822
- // if, on deactivation, we should return focus to the node originally-focused
823
- // when the trap was activated (or the configured `setReturnFocus` node),
824
- // then assume it's also OK to return focus to the outside node that was
825
- // just clicked, causing deactivation, as long as that node is focusable;
826
- // if it isn't focusable, then return focus to the original node focused
827
- // on activation (or the configured `setReturnFocus` node)
828
- // NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
829
- // which will result in the outside click setting focus to the node
830
- // that was clicked, whether it's focusable or not; by setting
831
- // `returnFocus: true`, we'll attempt to re-focus the node originally-focused
832
- // on activation (or the configured `setReturnFocus` node)
833
- returnFocus: config.returnFocusOnDeactivate && !isFocusable(target, config.tabbableOptions)
834
- });
835
- return;
836
- }
837
-
838
- // This is needed for mobile devices.
839
- // (If we'll only let `click` events through,
840
- // then on mobile they will be blocked anyways if `touchstart` is blocked.)
841
- if (valueOrHandler(config.allowOutsideClick, e)) {
842
- // allow the click outside the trap to take place
843
- return;
844
- }
845
-
846
- // otherwise, prevent the click
847
- e.preventDefault();
848
- };
849
-
850
- // In case focus escapes the trap for some strange reason, pull it back in.
851
- var checkFocusIn = function checkFocusIn(e) {
852
- var target = getActualTarget(e);
853
- var targetContained = findContainerIndex(target) >= 0;
854
-
855
- // In Firefox when you Tab out of an iframe the Document is briefly focused.
856
- if (targetContained || target instanceof Document) {
857
- if (targetContained) {
858
- state.mostRecentlyFocusedNode = target;
859
- }
860
- } else {
861
- // escaped! pull it back in to where it just left
862
- e.stopImmediatePropagation();
863
- tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
864
- }
865
- };
866
-
867
- // Hijack Tab events on the first and last focusable nodes of the trap,
868
- // in order to prevent focus from escaping. If it escapes for even a
869
- // moment it can end up scrolling the page and causing confusion so we
870
- // kind of need to capture the action at the keydown phase.
871
- var checkTab = function checkTab(e) {
872
- var target = getActualTarget(e);
1015
+ /**
1016
+ * Finds the next node (in either direction) where focus should move according to a
1017
+ * keyboard focus-in event.
1018
+ * @param {Object} params
1019
+ * @param {Node} [params.target] Known target __from which__ to navigate, if any.
1020
+ * @param {KeyboardEvent|FocusEvent} [params.event] Event to use if `target` isn't known (event
1021
+ * will be used to determine the `target`). Ignored if `target` is specified.
1022
+ * @param {boolean} [params.isBackward] True if focus should move backward.
1023
+ * @returns {Node|undefined} The next node, or `undefined` if a next node couldn't be
1024
+ * determined given the current state of the trap.
1025
+ */
1026
+ var findNextNavNode = function findNextNavNode(_ref2) {
1027
+ var target = _ref2.target,
1028
+ event = _ref2.event,
1029
+ _ref2$isBackward = _ref2.isBackward,
1030
+ isBackward = _ref2$isBackward === void 0 ? false : _ref2$isBackward;
1031
+ target = target || getActualTarget(event);
873
1032
  updateTabbableNodes();
874
1033
  var destinationNode = null;
875
1034
  if (state.tabbableGroups.length > 0) {
876
1035
  // make sure the target is actually contained in a group
877
1036
  // NOTE: the target may also be the container itself if it's focusable
878
1037
  // with tabIndex='-1' and was given initial focus
879
- var containerIndex = findContainerIndex(target);
1038
+ var containerIndex = findContainerIndex(target, event);
880
1039
  var containerGroup = containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
881
1040
  if (containerIndex < 0) {
882
1041
  // target not found in any group: quite possible focus has escaped the trap,
883
- // so bring it back in to...
884
- if (e.shiftKey) {
1042
+ // so bring it back into...
1043
+ if (isBackward) {
885
1044
  // ...the last node in the last group
886
1045
  destinationNode = state.tabbableGroups[state.tabbableGroups.length - 1].lastTabbableNode;
887
1046
  } else {
888
1047
  // ...the first node in the first group
889
1048
  destinationNode = state.tabbableGroups[0].firstTabbableNode;
890
1049
  }
891
- } else if (e.shiftKey) {
1050
+ } else if (isBackward) {
892
1051
  // REVERSE
893
1052
 
894
1053
  // is the target the first tabbable node in a group?
895
- var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref2) {
896
- var firstTabbableNode = _ref2.firstTabbableNode;
1054
+ var startOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
1055
+ var firstTabbableNode = _ref3.firstTabbableNode;
897
1056
  return target === firstTabbableNode;
898
1057
  });
899
1058
  if (startOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target, false))) {
@@ -911,14 +1070,18 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
911
1070
  // the LAST group if it's the first tabbable node of the FIRST group)
912
1071
  var destinationGroupIndex = startOfGroupIndex === 0 ? state.tabbableGroups.length - 1 : startOfGroupIndex - 1;
913
1072
  var destinationGroup = state.tabbableGroups[destinationGroupIndex];
914
- destinationNode = destinationGroup.lastTabbableNode;
1073
+ destinationNode = getTabIndex(target) >= 0 ? destinationGroup.lastTabbableNode : destinationGroup.lastDomTabbableNode;
1074
+ } else if (!isTabEvent(event)) {
1075
+ // user must have customized the nav keys so we have to move focus manually _within_
1076
+ // the active group: do this based on the order determined by tabbable()
1077
+ destinationNode = containerGroup.nextTabbableNode(target, false);
915
1078
  }
916
1079
  } else {
917
1080
  // FORWARD
918
1081
 
919
1082
  // is the target the last tabbable node in a group?
920
- var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref3) {
921
- var lastTabbableNode = _ref3.lastTabbableNode;
1083
+ var lastOfGroupIndex = findIndex(state.tabbableGroups, function (_ref4) {
1084
+ var lastTabbableNode = _ref4.lastTabbableNode;
922
1085
  return target === lastTabbableNode;
923
1086
  });
924
1087
  if (lastOfGroupIndex < 0 && (containerGroup.container === target || isFocusable(target, config.tabbableOptions) && !isTabbable(target, config.tabbableOptions) && !containerGroup.nextTabbableNode(target))) {
@@ -936,34 +1099,191 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
936
1099
  // group if it's the last tabbable node of the LAST group)
937
1100
  var _destinationGroupIndex = lastOfGroupIndex === state.tabbableGroups.length - 1 ? 0 : lastOfGroupIndex + 1;
938
1101
  var _destinationGroup = state.tabbableGroups[_destinationGroupIndex];
939
- destinationNode = _destinationGroup.firstTabbableNode;
1102
+ destinationNode = getTabIndex(target) >= 0 ? _destinationGroup.firstTabbableNode : _destinationGroup.firstDomTabbableNode;
1103
+ } else if (!isTabEvent(event)) {
1104
+ // user must have customized the nav keys so we have to move focus manually _within_
1105
+ // the active group: do this based on the order determined by tabbable()
1106
+ destinationNode = containerGroup.nextTabbableNode(target);
940
1107
  }
941
1108
  }
942
1109
  } else {
1110
+ // no groups available
943
1111
  // NOTE: the fallbackFocus option does not support returning false to opt-out
944
1112
  destinationNode = getNodeForOption('fallbackFocus');
945
1113
  }
1114
+ return destinationNode;
1115
+ };
1116
+
1117
+ // This needs to be done on mousedown and touchstart instead of click
1118
+ // so that it precedes the focus event.
1119
+ var checkPointerDown = function checkPointerDown(e) {
1120
+ var target = getActualTarget(e);
1121
+ if (findContainerIndex(target, e) >= 0) {
1122
+ // allow the click since it ocurred inside the trap
1123
+ return;
1124
+ }
1125
+ if (valueOrHandler(config.clickOutsideDeactivates, e)) {
1126
+ // immediately deactivate the trap
1127
+ trap.deactivate({
1128
+ // NOTE: by setting `returnFocus: false`, deactivate() will do nothing,
1129
+ // which will result in the outside click setting focus to the node
1130
+ // that was clicked (and if not focusable, to "nothing"); by setting
1131
+ // `returnFocus: true`, we'll attempt to re-focus the node originally-focused
1132
+ // on activation (or the configured `setReturnFocus` node), whether the
1133
+ // outside click was on a focusable node or not
1134
+ returnFocus: config.returnFocusOnDeactivate
1135
+ });
1136
+ return;
1137
+ }
1138
+
1139
+ // This is needed for mobile devices.
1140
+ // (If we'll only let `click` events through,
1141
+ // then on mobile they will be blocked anyways if `touchstart` is blocked.)
1142
+ if (valueOrHandler(config.allowOutsideClick, e)) {
1143
+ // allow the click outside the trap to take place
1144
+ return;
1145
+ }
1146
+
1147
+ // otherwise, prevent the click
1148
+ e.preventDefault();
1149
+ };
1150
+
1151
+ // In case focus escapes the trap for some strange reason, pull it back in.
1152
+ // NOTE: the focusIn event is NOT cancelable, so if focus escapes, it may cause unexpected
1153
+ // scrolling if the node that got focused was out of view; there's nothing we can do to
1154
+ // prevent that from happening by the time we discover that focus escaped
1155
+ var checkFocusIn = function checkFocusIn(event) {
1156
+ var target = getActualTarget(event);
1157
+ var targetContained = findContainerIndex(target, event) >= 0;
1158
+
1159
+ // In Firefox when you Tab out of an iframe the Document is briefly focused.
1160
+ if (targetContained || target instanceof Document) {
1161
+ if (targetContained) {
1162
+ state.mostRecentlyFocusedNode = target;
1163
+ }
1164
+ } else {
1165
+ // escaped! pull it back in to where it just left
1166
+ event.stopImmediatePropagation();
1167
+
1168
+ // focus will escape if the MRU node had a positive tab index and user tried to nav forward;
1169
+ // it will also escape if the MRU node had a 0 tab index and user tried to nav backward
1170
+ // toward a node with a positive tab index
1171
+ var nextNode; // next node to focus, if we find one
1172
+ var navAcrossContainers = true;
1173
+ if (state.mostRecentlyFocusedNode) {
1174
+ if (getTabIndex(state.mostRecentlyFocusedNode) > 0) {
1175
+ // MRU container index must be >=0 otherwise we wouldn't have it as an MRU node...
1176
+ var mruContainerIdx = findContainerIndex(state.mostRecentlyFocusedNode);
1177
+ // there MAY not be any tabbable nodes in the container if there are at least 2 containers
1178
+ // and the MRU node is focusable but not tabbable (focus-trap requires at least 1 container
1179
+ // with at least one tabbable node in order to function, so this could be the other container
1180
+ // with nothing tabbable in it)
1181
+ var tabbableNodes = state.containerGroups[mruContainerIdx].tabbableNodes;
1182
+ if (tabbableNodes.length > 0) {
1183
+ // MRU tab index MAY not be found if the MRU node is focusable but not tabbable
1184
+ var mruTabIdx = tabbableNodes.findIndex(function (node) {
1185
+ return node === state.mostRecentlyFocusedNode;
1186
+ });
1187
+ if (mruTabIdx >= 0) {
1188
+ if (config.isKeyForward(state.recentNavEvent)) {
1189
+ if (mruTabIdx + 1 < tabbableNodes.length) {
1190
+ nextNode = tabbableNodes[mruTabIdx + 1];
1191
+ navAcrossContainers = false;
1192
+ }
1193
+ // else, don't wrap within the container as focus should move to next/previous
1194
+ // container
1195
+ } else {
1196
+ if (mruTabIdx - 1 >= 0) {
1197
+ nextNode = tabbableNodes[mruTabIdx - 1];
1198
+ navAcrossContainers = false;
1199
+ }
1200
+ // else, don't wrap within the container as focus should move to next/previous
1201
+ // container
1202
+ }
1203
+ // else, don't find in container order without considering direction too
1204
+ }
1205
+ }
1206
+ // else, no tabbable nodes in that container (which means we must have at least one other
1207
+ // container with at least one tabbable node in it, otherwise focus-trap would've thrown
1208
+ // an error the last time updateTabbableNodes() was run): find next node among all known
1209
+ // containers
1210
+ } else {
1211
+ // check to see if there's at least one tabbable node with a positive tab index inside
1212
+ // the trap because focus seems to escape when navigating backward from a tabbable node
1213
+ // with tabindex=0 when this is the case (instead of wrapping to the tabbable node with
1214
+ // the greatest positive tab index like it should)
1215
+ if (!state.containerGroups.some(function (g) {
1216
+ return g.tabbableNodes.some(function (n) {
1217
+ return getTabIndex(n) > 0;
1218
+ });
1219
+ })) {
1220
+ // no containers with tabbable nodes with positive tab indexes which means the focus
1221
+ // escaped for some other reason and we should just execute the fallback to the
1222
+ // MRU node or initial focus node, if any
1223
+ navAcrossContainers = false;
1224
+ }
1225
+ }
1226
+ } else {
1227
+ // no MRU node means we're likely in some initial condition when the trap has just
1228
+ // been activated and initial focus hasn't been given yet, in which case we should
1229
+ // fall through to trying to focus the initial focus node, which is what should
1230
+ // happen below at this point in the logic
1231
+ navAcrossContainers = false;
1232
+ }
1233
+ if (navAcrossContainers) {
1234
+ nextNode = findNextNavNode({
1235
+ // move FROM the MRU node, not event-related node (which will be the node that is
1236
+ // outside the trap causing the focus escape we're trying to fix)
1237
+ target: state.mostRecentlyFocusedNode,
1238
+ isBackward: config.isKeyBackward(state.recentNavEvent)
1239
+ });
1240
+ }
1241
+ if (nextNode) {
1242
+ tryFocus(nextNode);
1243
+ } else {
1244
+ tryFocus(state.mostRecentlyFocusedNode || getInitialFocusNode());
1245
+ }
1246
+ }
1247
+ state.recentNavEvent = undefined; // clear
1248
+ };
1249
+
1250
+ // Hijack key nav events on the first and last focusable nodes of the trap,
1251
+ // in order to prevent focus from escaping. If it escapes for even a
1252
+ // moment it can end up scrolling the page and causing confusion so we
1253
+ // kind of need to capture the action at the keydown phase.
1254
+ var checkKeyNav = function checkKeyNav(event) {
1255
+ var isBackward = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
1256
+ state.recentNavEvent = event;
1257
+ var destinationNode = findNextNavNode({
1258
+ event: event,
1259
+ isBackward: isBackward
1260
+ });
946
1261
  if (destinationNode) {
947
- e.preventDefault();
1262
+ if (isTabEvent(event)) {
1263
+ // since tab natively moves focus, we wouldn't have a destination node unless we
1264
+ // were on the edge of a container and had to move to the next/previous edge, in
1265
+ // which case we want to prevent default to keep the browser from moving focus
1266
+ // to where it normally would
1267
+ event.preventDefault();
1268
+ }
948
1269
  tryFocus(destinationNode);
949
1270
  }
950
1271
  // else, let the browser take care of [shift+]tab and move the focus
951
1272
  };
952
1273
 
953
- var checkKey = function checkKey(e) {
954
- if (isEscapeEvent(e) && valueOrHandler(config.escapeDeactivates, e) !== false) {
955
- e.preventDefault();
1274
+ var checkKey = function checkKey(event) {
1275
+ if (isEscapeEvent(event) && valueOrHandler(config.escapeDeactivates, event) !== false) {
1276
+ event.preventDefault();
956
1277
  trap.deactivate();
957
1278
  return;
958
1279
  }
959
- if (isTabEvent(e)) {
960
- checkTab(e);
961
- return;
1280
+ if (config.isKeyForward(event) || config.isKeyBackward(event)) {
1281
+ checkKeyNav(event, config.isKeyBackward(event));
962
1282
  }
963
1283
  };
964
1284
  var checkClick = function checkClick(e) {
965
1285
  var target = getActualTarget(e);
966
- if (findContainerIndex(target) >= 0) {
1286
+ if (findContainerIndex(target, e) >= 0) {
967
1287
  return;
968
1288
  }
969
1289
  if (valueOrHandler(config.clickOutsideDeactivates, e)) {
@@ -1024,6 +1344,43 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
1024
1344
  return trap;
1025
1345
  };
1026
1346
 
1347
+ //
1348
+ // MUTATION OBSERVER
1349
+ //
1350
+
1351
+ var checkDomRemoval = function checkDomRemoval(mutations) {
1352
+ var isFocusedNodeRemoved = mutations.some(function (mutation) {
1353
+ var removedNodes = Array.from(mutation.removedNodes);
1354
+ return removedNodes.some(function (node) {
1355
+ return node === state.mostRecentlyFocusedNode;
1356
+ });
1357
+ });
1358
+
1359
+ // If the currently focused is removed then browsers will move focus to the
1360
+ // <body> element. If this happens, try to move focus back into the trap.
1361
+ if (isFocusedNodeRemoved) {
1362
+ tryFocus(getInitialFocusNode());
1363
+ }
1364
+ };
1365
+
1366
+ // Use MutationObserver - if supported - to detect if focused node is removed
1367
+ // from the DOM.
1368
+ var mutationObserver = typeof window !== 'undefined' && 'MutationObserver' in window ? new MutationObserver(checkDomRemoval) : undefined;
1369
+ var updateObservedNodes = function updateObservedNodes() {
1370
+ if (!mutationObserver) {
1371
+ return;
1372
+ }
1373
+ mutationObserver.disconnect();
1374
+ if (state.active && !state.paused) {
1375
+ state.containers.map(function (container) {
1376
+ mutationObserver.observe(container, {
1377
+ subtree: true,
1378
+ childList: true
1379
+ });
1380
+ });
1381
+ }
1382
+ };
1383
+
1027
1384
  //
1028
1385
  // TRAP DEFINITION
1029
1386
  //
@@ -1048,17 +1405,14 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
1048
1405
  state.active = true;
1049
1406
  state.paused = false;
1050
1407
  state.nodeFocusedBeforeActivation = doc.activeElement;
1051
- if (onActivate) {
1052
- onActivate();
1053
- }
1408
+ onActivate === null || onActivate === void 0 || onActivate();
1054
1409
  var finishActivation = function finishActivation() {
1055
1410
  if (checkCanFocusTrap) {
1056
1411
  updateTabbableNodes();
1057
1412
  }
1058
1413
  addListeners();
1059
- if (onPostActivate) {
1060
- onPostActivate();
1061
- }
1414
+ updateObservedNodes();
1415
+ onPostActivate === null || onPostActivate === void 0 || onPostActivate();
1062
1416
  };
1063
1417
  if (checkCanFocusTrap) {
1064
1418
  checkCanFocusTrap(state.containers.concat()).then(finishActivation, finishActivation);
@@ -1081,22 +1435,19 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
1081
1435
  removeListeners();
1082
1436
  state.active = false;
1083
1437
  state.paused = false;
1438
+ updateObservedNodes();
1084
1439
  activeFocusTraps.deactivateTrap(trapStack, trap);
1085
1440
  var onDeactivate = getOption(options, 'onDeactivate');
1086
1441
  var onPostDeactivate = getOption(options, 'onPostDeactivate');
1087
1442
  var checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
1088
1443
  var returnFocus = getOption(options, 'returnFocus', 'returnFocusOnDeactivate');
1089
- if (onDeactivate) {
1090
- onDeactivate();
1091
- }
1444
+ onDeactivate === null || onDeactivate === void 0 || onDeactivate();
1092
1445
  var finishDeactivation = function finishDeactivation() {
1093
1446
  delay(function () {
1094
1447
  if (returnFocus) {
1095
1448
  tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
1096
1449
  }
1097
- if (onPostDeactivate) {
1098
- onPostDeactivate();
1099
- }
1450
+ onPostDeactivate === null || onPostDeactivate === void 0 || onPostDeactivate();
1100
1451
  });
1101
1452
  };
1102
1453
  if (returnFocus && checkCanReturnFocus) {
@@ -1106,21 +1457,31 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
1106
1457
  finishDeactivation();
1107
1458
  return this;
1108
1459
  },
1109
- pause: function pause() {
1460
+ pause: function pause(pauseOptions) {
1110
1461
  if (state.paused || !state.active) {
1111
1462
  return this;
1112
1463
  }
1464
+ var onPause = getOption(pauseOptions, 'onPause');
1465
+ var onPostPause = getOption(pauseOptions, 'onPostPause');
1113
1466
  state.paused = true;
1467
+ onPause === null || onPause === void 0 || onPause();
1114
1468
  removeListeners();
1469
+ updateObservedNodes();
1470
+ onPostPause === null || onPostPause === void 0 || onPostPause();
1115
1471
  return this;
1116
1472
  },
1117
- unpause: function unpause() {
1473
+ unpause: function unpause(unpauseOptions) {
1118
1474
  if (!state.paused || !state.active) {
1119
1475
  return this;
1120
1476
  }
1477
+ var onUnpause = getOption(unpauseOptions, 'onUnpause');
1478
+ var onPostUnpause = getOption(unpauseOptions, 'onPostUnpause');
1121
1479
  state.paused = false;
1480
+ onUnpause === null || onUnpause === void 0 || onUnpause();
1122
1481
  updateTabbableNodes();
1123
1482
  addListeners();
1483
+ updateObservedNodes();
1484
+ onPostUnpause === null || onPostUnpause === void 0 || onPostUnpause();
1124
1485
  return this;
1125
1486
  },
1126
1487
  updateContainerElements: function updateContainerElements(containerElements) {
@@ -1131,6 +1492,7 @@ var createFocusTrap = function createFocusTrap(elements, userOptions) {
1131
1492
  if (state.active) {
1132
1493
  updateTabbableNodes();
1133
1494
  }
1495
+ updateObservedNodes();
1134
1496
  return this;
1135
1497
  }
1136
1498
  };