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