focus-trap 6.3.0 → 6.6.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,7 +1,5 @@
1
1
  import { tabbable, isFocusable } from 'tabbable';
2
2
 
3
- let activeFocusDelay;
4
-
5
3
  const activeFocusTraps = (function () {
6
4
  const trapQueue = [];
7
5
  return {
@@ -111,10 +109,21 @@ const createFocusTrap = function (elements, userOptions) {
111
109
  mostRecentlyFocusedNode: null,
112
110
  active: false,
113
111
  paused: false,
112
+
113
+ // timer ID for when delayInitialFocus is true and initial focus in this trap
114
+ // has been delayed during activation
115
+ delayInitialFocusTimer: undefined,
114
116
  };
115
117
 
116
118
  let 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
117
119
 
120
+ const getOption = (configOverrideOptions, optionName, configOptionName) => {
121
+ return configOverrideOptions &&
122
+ configOverrideOptions[optionName] !== undefined
123
+ ? configOverrideOptions[optionName]
124
+ : config[configOptionName || optionName];
125
+ };
126
+
118
127
  const containersContain = function (element) {
119
128
  return state.containers.some((container) => container.contains(element));
120
129
  };
@@ -147,6 +156,11 @@ const createFocusTrap = function (elements, userOptions) {
147
156
  const getInitialFocusNode = function () {
148
157
  let node;
149
158
 
159
+ // false indicates we want no initialFocus at all
160
+ if (getOption({}, 'initialFocus') === false) {
161
+ return false;
162
+ }
163
+
150
164
  if (getNodeForOption('initialFocus') !== null) {
151
165
  node = getNodeForOption('initialFocus');
152
166
  } else if (containersContain(doc.activeElement)) {
@@ -196,9 +210,14 @@ const createFocusTrap = function (elements, userOptions) {
196
210
  };
197
211
 
198
212
  const tryFocus = function (node) {
213
+ if (node === false) {
214
+ return;
215
+ }
216
+
199
217
  if (node === doc.activeElement) {
200
218
  return;
201
219
  }
220
+
202
221
  if (!node || !node.focus) {
203
222
  tryFocus(getInitialFocusNode());
204
223
  return;
@@ -283,6 +302,8 @@ const createFocusTrap = function (elements, userOptions) {
283
302
 
284
303
  if (state.tabbableGroups.length > 0) {
285
304
  // make sure the target is actually contained in a group
305
+ // NOTE: the target may also be the container itself if it's tabbable
306
+ // with tabIndex='-1' and was given initial focus
286
307
  const containerIndex = findIndex(state.tabbableGroups, ({ container }) =>
287
308
  container.contains(e.target)
288
309
  );
@@ -301,12 +322,27 @@ const createFocusTrap = function (elements, userOptions) {
301
322
  }
302
323
  } else if (e.shiftKey) {
303
324
  // REVERSE
304
- const startOfGroupIndex = findIndex(
325
+
326
+ // is the target the first tabbable node in a group?
327
+ let startOfGroupIndex = findIndex(
305
328
  state.tabbableGroups,
306
329
  ({ firstTabbableNode }) => e.target === firstTabbableNode
307
330
  );
308
331
 
332
+ if (
333
+ startOfGroupIndex < 0 &&
334
+ state.tabbableGroups[containerIndex].container === e.target
335
+ ) {
336
+ // an exception case where the target is the container itself, in which
337
+ // case, we should handle shift+tab as if focus were on the container's
338
+ // first tabbable node, and go to the last tabbable node of the LAST group
339
+ startOfGroupIndex = containerIndex;
340
+ }
341
+
309
342
  if (startOfGroupIndex >= 0) {
343
+ // YES: then shift+tab should go to the last tabbable node in the
344
+ // previous group (and wrap around to the last tabbable node of
345
+ // the LAST group if it's the first tabbable node of the FIRST group)
310
346
  const destinationGroupIndex =
311
347
  startOfGroupIndex === 0
312
348
  ? state.tabbableGroups.length - 1
@@ -317,12 +353,27 @@ const createFocusTrap = function (elements, userOptions) {
317
353
  }
318
354
  } else {
319
355
  // FORWARD
320
- const lastOfGroupIndex = findIndex(
356
+
357
+ // is the target the last tabbable node in a group?
358
+ let lastOfGroupIndex = findIndex(
321
359
  state.tabbableGroups,
322
360
  ({ lastTabbableNode }) => e.target === lastTabbableNode
323
361
  );
324
362
 
363
+ if (
364
+ lastOfGroupIndex < 0 &&
365
+ state.tabbableGroups[containerIndex].container === e.target
366
+ ) {
367
+ // an exception case where the target is the container itself, in which
368
+ // case, we should handle tab as if focus were on the container's
369
+ // last tabbable node, and go to the first tabbable node of the FIRST group
370
+ lastOfGroupIndex = containerIndex;
371
+ }
372
+
325
373
  if (lastOfGroupIndex >= 0) {
374
+ // YES: then tab should go to the first tabbable node in the next
375
+ // group (and wrap around to the first tabbable node of the FIRST
376
+ // group if it's the last tabbable node of the LAST group)
326
377
  const destinationGroupIndex =
327
378
  lastOfGroupIndex === state.tabbableGroups.length - 1
328
379
  ? 0
@@ -340,10 +391,14 @@ const createFocusTrap = function (elements, userOptions) {
340
391
  e.preventDefault();
341
392
  tryFocus(destinationNode);
342
393
  }
394
+ // else, let the browser take care of [shift+]tab and move the focus
343
395
  };
344
396
 
345
397
  const checkKey = function (e) {
346
- if (config.escapeDeactivates !== false && isEscapeEvent(e)) {
398
+ if (
399
+ isEscapeEvent(e) &&
400
+ valueOrHandler(config.escapeDeactivates) !== false
401
+ ) {
347
402
  e.preventDefault();
348
403
  trap.deactivate();
349
404
  return;
@@ -386,7 +441,7 @@ const createFocusTrap = function (elements, userOptions) {
386
441
 
387
442
  // Delay ensures that the focused element doesn't capture the event
388
443
  // that caused the focus trap activation.
389
- activeFocusDelay = config.delayInitialFocus
444
+ state.delayInitialFocusTimer = config.delayInitialFocus
390
445
  ? delay(function () {
391
446
  tryFocus(getInitialFocusNode());
392
447
  })
@@ -437,21 +492,41 @@ const createFocusTrap = function (elements, userOptions) {
437
492
  return this;
438
493
  }
439
494
 
440
- updateTabbableNodes();
495
+ const onActivate = getOption(activateOptions, 'onActivate');
496
+ const onPostActivate = getOption(activateOptions, 'onPostActivate');
497
+ const checkCanFocusTrap = getOption(activateOptions, 'checkCanFocusTrap');
498
+
499
+ if (!checkCanFocusTrap) {
500
+ updateTabbableNodes();
501
+ }
441
502
 
442
503
  state.active = true;
443
504
  state.paused = false;
444
505
  state.nodeFocusedBeforeActivation = doc.activeElement;
445
506
 
446
- const onActivate =
447
- activateOptions && activateOptions.onActivate
448
- ? activateOptions.onActivate
449
- : config.onActivate;
450
507
  if (onActivate) {
451
508
  onActivate();
452
509
  }
453
510
 
454
- addListeners();
511
+ const finishActivation = () => {
512
+ if (checkCanFocusTrap) {
513
+ updateTabbableNodes();
514
+ }
515
+ addListeners();
516
+ if (onPostActivate) {
517
+ onPostActivate();
518
+ }
519
+ };
520
+
521
+ if (checkCanFocusTrap) {
522
+ checkCanFocusTrap(state.containers.concat()).then(
523
+ finishActivation,
524
+ finishActivation
525
+ );
526
+ return this;
527
+ }
528
+
529
+ finishActivation();
455
530
  return this;
456
531
  },
457
532
 
@@ -460,7 +535,8 @@ const createFocusTrap = function (elements, userOptions) {
460
535
  return this;
461
536
  }
462
537
 
463
- clearTimeout(activeFocusDelay);
538
+ clearTimeout(state.delayInitialFocusTimer); // noop if undefined
539
+ state.delayInitialFocusTimer = undefined;
464
540
 
465
541
  removeListeners();
466
542
  state.active = false;
@@ -468,25 +544,42 @@ const createFocusTrap = function (elements, userOptions) {
468
544
 
469
545
  activeFocusTraps.deactivateTrap(trap);
470
546
 
471
- const onDeactivate =
472
- deactivateOptions && deactivateOptions.onDeactivate !== undefined
473
- ? deactivateOptions.onDeactivate
474
- : config.onDeactivate;
547
+ const onDeactivate = getOption(deactivateOptions, 'onDeactivate');
548
+ const onPostDeactivate = getOption(deactivateOptions, 'onPostDeactivate');
549
+ const checkCanReturnFocus = getOption(
550
+ deactivateOptions,
551
+ 'checkCanReturnFocus'
552
+ );
553
+
475
554
  if (onDeactivate) {
476
555
  onDeactivate();
477
556
  }
478
557
 
479
- const returnFocus =
480
- deactivateOptions && deactivateOptions.returnFocus !== undefined
481
- ? deactivateOptions.returnFocus
482
- : config.returnFocusOnDeactivate;
558
+ const returnFocus = getOption(
559
+ deactivateOptions,
560
+ 'returnFocus',
561
+ 'returnFocusOnDeactivate'
562
+ );
483
563
 
484
- if (returnFocus) {
485
- delay(function () {
486
- tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
564
+ const finishDeactivation = () => {
565
+ delay(() => {
566
+ if (returnFocus) {
567
+ tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
568
+ }
569
+ if (onPostDeactivate) {
570
+ onPostDeactivate();
571
+ }
487
572
  });
573
+ };
574
+
575
+ if (returnFocus && checkCanReturnFocus) {
576
+ checkCanReturnFocus(
577
+ getReturnFocusNode(state.nodeFocusedBeforeActivation)
578
+ ).then(finishDeactivation, finishDeactivation);
579
+ return this;
488
580
  }
489
581
 
582
+ finishDeactivation();
490
583
  return this;
491
584
  },
492
585
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focus-trap",
3
- "version": "6.3.0",
3
+ "version": "6.6.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",
@@ -17,23 +17,24 @@
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "demo-bundle": "browserify demo/js/index.js -o demo/demo-bundle.js",
21
- "format": "prettier --write \"{*,src/**/*,test/**/*,demo/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
22
- "format:check": "prettier --check \"{*,src/**/*,test/**/*,demo/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
23
- "format:watch": "onchange \"{*,src/**/*,test/**/*,demo/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\" -- prettier --write {{changed}}",
24
- "lint": "eslint \"*.js\" \"demo/**/*.js\" \"cypress/**/*.js\"",
20
+ "demo-bundle": "browserify docs/js/index.js -o docs/demo-bundle.js",
21
+ "format": "prettier --write \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
22
+ "format:check": "prettier --check \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
23
+ "format:watch": "onchange \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\" -- prettier --write {{changed}}",
24
+ "lint": "eslint \"*.js\" \"docs/js/**/*.js\" \"cypress/**/*.js\"",
25
25
  "clean": "rm -rf ./dist",
26
- "compile:esm": "BUILD_ENV=esm BABEL_ENV=esm rollup -c",
27
- "compile:cjs": "BUILD_ENV=cjs BABEL_ENV=es5 rollup -c",
28
- "compile:umd": "BUILD_ENV=umd BABEL_ENV=es5 rollup -c",
26
+ "compile:esm": "cross-env BUILD_ENV=esm BABEL_ENV=esm rollup -c",
27
+ "compile:cjs": "cross-env BUILD_ENV=cjs BABEL_ENV=es5 rollup -c",
28
+ "compile:umd": "cross-env BUILD_ENV=umd BABEL_ENV=es5 rollup -c",
29
29
  "compile": "yarn compile:esm && yarn compile:cjs && yarn compile:umd",
30
30
  "build": "yarn clean && yarn compile",
31
- "start": "yarn compile:cjs && budo demo/js/index.js:demo-bundle.js --dir demo --live -- -t babelify",
31
+ "start": "yarn compile:cjs && budo docs/js/index.js:demo-bundle.js --dir docs --live -- -t babelify",
32
32
  "test:types": "tsc index.d.ts",
33
33
  "test:unit": "echo \"No unit tests to run!\"",
34
34
  "test:cypress": "start-server-and-test start 9966 'cypress open'",
35
- "test:cypress-ci": "start-server-and-test start 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
36
- "test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress-ci",
35
+ "test:cypress:ci": "start-server-and-test start 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
36
+ "test:chrome": "CYPRESS_BROWSER=chrome yarn test:cypress:ci",
37
+ "test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress:ci",
37
38
  "prepare": "yarn build",
38
39
  "release": "yarn build && changeset publish"
39
40
  },
@@ -59,35 +60,36 @@
59
60
  },
60
61
  "homepage": "https://github.com/focus-trap/focus-trap#readme",
61
62
  "dependencies": {
62
- "tabbable": "^5.1.5"
63
+ "tabbable": "^5.2.0"
63
64
  },
64
65
  "devDependencies": {
65
- "@babel/cli": "^7.12.10",
66
- "@babel/core": "^7.12.10",
67
- "@babel/preset-env": "^7.12.11",
68
- "@changesets/cli": "^2.12.0",
69
- "@rollup/plugin-babel": "^5.2.2",
70
- "@rollup/plugin-commonjs": "^17.0.0",
71
- "@rollup/plugin-node-resolve": "^11.0.1",
72
- "@testing-library/cypress": "^7.0.3",
66
+ "@babel/cli": "^7.14.5",
67
+ "@babel/core": "^7.14.6",
68
+ "@babel/preset-env": "^7.14.7",
69
+ "@changesets/cli": "^2.16.0",
70
+ "@rollup/plugin-babel": "^5.3.0",
71
+ "@rollup/plugin-commonjs": "^19.0.0",
72
+ "@rollup/plugin-node-resolve": "^13.0.0",
73
+ "@testing-library/cypress": "^7.0.6",
73
74
  "@types/jquery": "^3.5.5",
74
- "all-contributors-cli": "^6.19.0",
75
+ "all-contributors-cli": "^6.20.0",
75
76
  "babel-eslint": "^10.1.0",
76
77
  "babel-loader": "^8.2.2",
77
78
  "babelify": "^10.0.0",
78
79
  "browserify": "^17.0.0",
79
80
  "budo": "^11.6.4",
80
- "cypress": "^6.2.1",
81
+ "cross-env": "^7.0.3",
82
+ "cypress": "^7.6.0",
81
83
  "cypress-plugin-tab": "^1.0.5",
82
- "eslint": "^7.17.0",
83
- "eslint-config-prettier": "^7.1.0",
84
- "eslint-plugin-cypress": "^2.11.2",
84
+ "eslint": "^7.29.0",
85
+ "eslint-config-prettier": "^8.3.0",
86
+ "eslint-plugin-cypress": "^2.11.3",
85
87
  "onchange": "^7.1.0",
86
- "prettier": "^2.2.1",
87
- "rollup": "^2.36.1",
88
+ "prettier": "^2.3.2",
89
+ "rollup": "^2.52.4",
88
90
  "rollup-plugin-sourcemaps": "^0.6.3",
89
91
  "rollup-plugin-terser": "^7.0.1",
90
- "start-server-and-test": "^1.11.7",
91
- "typescript": "^4.1.3"
92
+ "start-server-and-test": "^1.12.5",
93
+ "typescript": "^4.3.4"
92
94
  }
93
95
  }