focus-trap 7.0.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,38 +1,35 @@
1
1
  import { tabbable, focusable, isFocusable, isTabbable } from 'tabbable';
2
2
 
3
- const activeFocusTraps = (function () {
4
- const trapQueue = [];
5
- return {
6
- activateTrap(trap) {
7
- if (trapQueue.length > 0) {
8
- const activeTrap = trapQueue[trapQueue.length - 1];
9
- if (activeTrap !== trap) {
10
- activeTrap.pause();
11
- }
3
+ const activeFocusTraps = {
4
+ activateTrap(trapStack, trap) {
5
+ if (trapStack.length > 0) {
6
+ const activeTrap = trapStack[trapStack.length - 1];
7
+ if (activeTrap !== trap) {
8
+ activeTrap.pause();
12
9
  }
10
+ }
13
11
 
14
- const trapIndex = trapQueue.indexOf(trap);
15
- if (trapIndex === -1) {
16
- trapQueue.push(trap);
17
- } else {
18
- // move this existing trap to the front of the queue
19
- trapQueue.splice(trapIndex, 1);
20
- trapQueue.push(trap);
21
- }
22
- },
12
+ const trapIndex = trapStack.indexOf(trap);
13
+ if (trapIndex === -1) {
14
+ trapStack.push(trap);
15
+ } else {
16
+ // move this existing trap to the front of the queue
17
+ trapStack.splice(trapIndex, 1);
18
+ trapStack.push(trap);
19
+ }
20
+ },
23
21
 
24
- deactivateTrap(trap) {
25
- const trapIndex = trapQueue.indexOf(trap);
26
- if (trapIndex !== -1) {
27
- trapQueue.splice(trapIndex, 1);
28
- }
22
+ deactivateTrap(trapStack, trap) {
23
+ const trapIndex = trapStack.indexOf(trap);
24
+ if (trapIndex !== -1) {
25
+ trapStack.splice(trapIndex, 1);
26
+ }
29
27
 
30
- if (trapQueue.length > 0) {
31
- trapQueue[trapQueue.length - 1].unpause();
32
- }
33
- },
34
- };
35
- })();
28
+ if (trapStack.length > 0) {
29
+ trapStack[trapStack.length - 1].unpause();
30
+ }
31
+ },
32
+ };
36
33
 
37
34
  const isSelectableInput = function (node) {
38
35
  return (
@@ -50,6 +47,16 @@ const isTabEvent = function (e) {
50
47
  return e.key === 'Tab' || e.keyCode === 9;
51
48
  };
52
49
 
50
+ // checks for TAB by default
51
+ const isKeyForward = function (e) {
52
+ return isTabEvent(e) && !e.shiftKey;
53
+ };
54
+
55
+ // checks for SHIFT+TAB by default
56
+ const isKeyBackward = function (e) {
57
+ return isTabEvent(e) && e.shiftKey;
58
+ };
59
+
53
60
  const delay = function (fn) {
54
61
  return setTimeout(fn, 0);
55
62
  };
@@ -95,15 +102,23 @@ const getActualTarget = function (event) {
95
102
  : event.target;
96
103
  };
97
104
 
105
+ // NOTE: this must be _outside_ `createFocusTrap()` to make sure all traps in this
106
+ // current instance use the same stack if `userOptions.trapStack` isn't specified
107
+ const internalTrapStack = [];
108
+
98
109
  const createFocusTrap = function (elements, userOptions) {
99
110
  // SSR: a live trap shouldn't be created in this type of environment so this
100
111
  // should be safe code to execute if the `document` option isn't specified
101
112
  const doc = userOptions?.document || document;
102
113
 
114
+ const trapStack = userOptions?.trapStack || internalTrapStack;
115
+
103
116
  const config = {
104
117
  returnFocusOnDeactivate: true,
105
118
  escapeDeactivates: true,
106
119
  delayInitialFocus: true,
120
+ isKeyForward,
121
+ isKeyBackward,
107
122
  ...userOptions,
108
123
  };
109
124
 
@@ -420,12 +435,12 @@ const createFocusTrap = function (elements, userOptions) {
420
435
  }
421
436
  };
422
437
 
423
- // Hijack Tab events on the first and last focusable nodes of the trap,
438
+ // Hijack key nav events on the first and last focusable nodes of the trap,
424
439
  // in order to prevent focus from escaping. If it escapes for even a
425
440
  // moment it can end up scrolling the page and causing confusion so we
426
441
  // kind of need to capture the action at the keydown phase.
427
- const checkTab = function (e) {
428
- const target = getActualTarget(e);
442
+ const checkKeyNav = function (event, isBackward = false) {
443
+ const target = getActualTarget(event);
429
444
  updateTabbableNodes();
430
445
 
431
446
  let destinationNode = null;
@@ -440,8 +455,8 @@ const createFocusTrap = function (elements, userOptions) {
440
455
 
441
456
  if (containerIndex < 0) {
442
457
  // target not found in any group: quite possible focus has escaped the trap,
443
- // so bring it back in to...
444
- if (e.shiftKey) {
458
+ // so bring it back into...
459
+ if (isBackward) {
445
460
  // ...the last node in the last group
446
461
  destinationNode =
447
462
  state.tabbableGroups[state.tabbableGroups.length - 1]
@@ -450,7 +465,7 @@ const createFocusTrap = function (elements, userOptions) {
450
465
  // ...the first node in the first group
451
466
  destinationNode = state.tabbableGroups[0].firstTabbableNode;
452
467
  }
453
- } else if (e.shiftKey) {
468
+ } else if (isBackward) {
454
469
  // REVERSE
455
470
 
456
471
  // is the target the first tabbable node in a group?
@@ -486,6 +501,10 @@ const createFocusTrap = function (elements, userOptions) {
486
501
 
487
502
  const destinationGroup = state.tabbableGroups[destinationGroupIndex];
488
503
  destinationNode = destinationGroup.lastTabbableNode;
504
+ } else if (!isTabEvent(event)) {
505
+ // user must have customized the nav keys so we have to move focus manually _within_
506
+ // the active group: do this based on the order determined by tabbable()
507
+ destinationNode = containerGroup.nextTabbableNode(target, false);
489
508
  }
490
509
  } else {
491
510
  // FORWARD
@@ -523,33 +542,43 @@ const createFocusTrap = function (elements, userOptions) {
523
542
 
524
543
  const destinationGroup = state.tabbableGroups[destinationGroupIndex];
525
544
  destinationNode = destinationGroup.firstTabbableNode;
545
+ } else if (!isTabEvent(event)) {
546
+ // user must have customized the nav keys so we have to move focus manually _within_
547
+ // the active group: do this based on the order determined by tabbable()
548
+ destinationNode = containerGroup.nextTabbableNode(target);
526
549
  }
527
550
  }
528
551
  } else {
552
+ // no groups available
529
553
  // NOTE: the fallbackFocus option does not support returning false to opt-out
530
554
  destinationNode = getNodeForOption('fallbackFocus');
531
555
  }
532
556
 
533
557
  if (destinationNode) {
534
- e.preventDefault();
558
+ if (isTabEvent(event)) {
559
+ // since tab natively moves focus, we wouldn't have a destination node unless we
560
+ // were on the edge of a container and had to move to the next/previous edge, in
561
+ // which case we want to prevent default to keep the browser from moving focus
562
+ // to where it normally would
563
+ event.preventDefault();
564
+ }
535
565
  tryFocus(destinationNode);
536
566
  }
537
567
  // else, let the browser take care of [shift+]tab and move the focus
538
568
  };
539
569
 
540
- const checkKey = function (e) {
570
+ const checkKey = function (event) {
541
571
  if (
542
- isEscapeEvent(e) &&
543
- valueOrHandler(config.escapeDeactivates, e) !== false
572
+ isEscapeEvent(event) &&
573
+ valueOrHandler(config.escapeDeactivates, event) !== false
544
574
  ) {
545
- e.preventDefault();
575
+ event.preventDefault();
546
576
  trap.deactivate();
547
577
  return;
548
578
  }
549
579
 
550
- if (isTabEvent(e)) {
551
- checkTab(e);
552
- return;
580
+ if (config.isKeyForward(event) || config.isKeyBackward(event)) {
581
+ checkKeyNav(event, config.isKeyBackward(event));
553
582
  }
554
583
  };
555
584
 
@@ -582,7 +611,7 @@ const createFocusTrap = function (elements, userOptions) {
582
611
  }
583
612
 
584
613
  // There can be only one listening focus trap at a time
585
- activeFocusTraps.activateTrap(trap);
614
+ activeFocusTraps.activateTrap(trapStack, trap);
586
615
 
587
616
  // Delay ensures that the focused element doesn't capture the event
588
617
  // that caused the focus trap activation.
@@ -702,7 +731,7 @@ const createFocusTrap = function (elements, userOptions) {
702
731
  state.active = false;
703
732
  state.paused = false;
704
733
 
705
- activeFocusTraps.deactivateTrap(trap);
734
+ activeFocusTraps.deactivateTrap(trapStack, trap);
706
735
 
707
736
  const onDeactivate = getOption(options, 'onDeactivate');
708
737
  const onPostDeactivate = getOption(options, 'onPostDeactivate');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focus-trap",
3
- "version": "7.0.0",
3
+ "version": "7.2.0",
4
4
  "description": "Trap focus within a DOM node.",
5
5
  "main": "dist/focus-trap.js",
6
6
  "module": "dist/focus-trap.esm.js",
@@ -63,37 +63,37 @@
63
63
  },
64
64
  "homepage": "https://github.com/focus-trap/focus-trap#readme",
65
65
  "dependencies": {
66
- "tabbable": "^6.0.0"
66
+ "tabbable": "^6.0.1"
67
67
  },
68
68
  "devDependencies": {
69
- "@babel/cli": "^7.18.10",
70
- "@babel/core": "^7.18.10",
71
- "@babel/eslint-parser": "^7.18.9",
72
- "@babel/preset-env": "^7.18.10",
73
- "@changesets/cli": "^2.24.3",
74
- "@rollup/plugin-babel": "^5.3.1",
75
- "@rollup/plugin-commonjs": "^22.0.2",
76
- "@rollup/plugin-node-resolve": "^13.3.0",
77
- "@testing-library/cypress": "^8.0.3",
69
+ "@babel/cli": "^7.19.3",
70
+ "@babel/core": "^7.20.5",
71
+ "@babel/eslint-parser": "^7.19.1",
72
+ "@babel/preset-env": "^7.20.2",
73
+ "@changesets/cli": "^2.25.2",
74
+ "@rollup/plugin-babel": "^6.0.3",
75
+ "@rollup/plugin-commonjs": "^23.0.3",
76
+ "@rollup/plugin-node-resolve": "^15.0.1",
77
+ "@testing-library/cypress": "^8.0.7",
78
78
  "@types/jquery": "^3.5.14",
79
- "all-contributors-cli": "^6.20.0",
80
- "babel-loader": "^8.2.5",
79
+ "all-contributors-cli": "^6.24.0",
80
+ "babel-loader": "^9.1.0",
81
81
  "cross-env": "^7.0.3",
82
- "cypress": "^10.6.0",
82
+ "cypress": "^11.2.0",
83
83
  "cypress-plugin-tab": "^1.0.5",
84
- "eslint": "^8.22.0",
84
+ "eslint": "^8.28.0",
85
85
  "eslint-config-prettier": "^8.5.0",
86
86
  "eslint-plugin-cypress": "^2.12.1",
87
- "eslint-plugin-jest": "^26.8.3",
87
+ "eslint-plugin-jest": "^27.1.6",
88
88
  "onchange": "^7.1.0",
89
- "prettier": "^2.7.1",
90
- "rollup": "^2.78.0",
89
+ "prettier": "^2.8.0",
90
+ "rollup": "^2.79.1",
91
91
  "rollup-plugin-inject-process-env": "^1.3.1",
92
92
  "rollup-plugin-livereload": "^2.0.5",
93
- "rollup-plugin-serve": "^2.0.1",
93
+ "rollup-plugin-serve": "^2.0.2",
94
94
  "rollup-plugin-sourcemaps": "^0.6.3",
95
95
  "rollup-plugin-terser": "^7.0.1",
96
96
  "start-server-and-test": "^1.14.0",
97
- "typescript": "^4.7.4"
97
+ "typescript": "^4.9.3"
98
98
  }
99
99
  }