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/CHANGELOG.md +57 -0
- package/README.md +53 -47
- package/dist/focus-trap.esm.js +114 -33
- package/dist/focus-trap.esm.js.map +1 -1
- package/dist/focus-trap.esm.min.js +2 -2
- package/dist/focus-trap.esm.min.js.map +1 -1
- package/dist/focus-trap.js +114 -33
- package/dist/focus-trap.js.map +1 -1
- package/dist/focus-trap.min.js +2 -2
- package/dist/focus-trap.min.js.map +1 -1
- package/dist/focus-trap.umd.js +114 -33
- package/dist/focus-trap.umd.js.map +1 -1
- package/dist/focus-trap.umd.min.js +2 -2
- package/dist/focus-trap.umd.min.js.map +1 -1
- package/index.d.ts +62 -13
- package/index.js +117 -24
- package/package.json +32 -30
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
|
481
|
-
|
|
482
|
-
|
|
558
|
+
const returnFocus = getOption(
|
|
559
|
+
deactivateOptions,
|
|
560
|
+
'returnFocus',
|
|
561
|
+
'returnFocusOnDeactivate'
|
|
562
|
+
);
|
|
483
563
|
|
|
484
|
-
|
|
485
|
-
delay(
|
|
486
|
-
|
|
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
|
+
"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
|
|
21
|
-
"format": "prettier --write \"{*,src/**/*,test/**/*,
|
|
22
|
-
"format:check": "prettier --check \"{*,src/**/*,test/**/*,
|
|
23
|
-
"format:watch": "onchange \"{*,src/**/*,test/**/*,
|
|
24
|
-
"lint": "eslint \"*.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
|
|
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
|
|
36
|
-
"test": "
|
|
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.
|
|
63
|
+
"tabbable": "^5.2.0"
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
|
-
"@babel/cli": "^7.
|
|
66
|
-
"@babel/core": "^7.
|
|
67
|
-
"@babel/preset-env": "^7.
|
|
68
|
-
"@changesets/cli": "^2.
|
|
69
|
-
"@rollup/plugin-babel": "^5.
|
|
70
|
-
"@rollup/plugin-commonjs": "^
|
|
71
|
-
"@rollup/plugin-node-resolve": "^
|
|
72
|
-
"@testing-library/cypress": "^7.0.
|
|
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.
|
|
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
|
-
"
|
|
81
|
+
"cross-env": "^7.0.3",
|
|
82
|
+
"cypress": "^7.6.0",
|
|
81
83
|
"cypress-plugin-tab": "^1.0.5",
|
|
82
|
-
"eslint": "^7.
|
|
83
|
-
"eslint-config-prettier": "^
|
|
84
|
-
"eslint-plugin-cypress": "^2.11.
|
|
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
|
|
87
|
-
"rollup": "^2.
|
|
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.
|
|
91
|
-
"typescript": "^4.
|
|
92
|
+
"start-server-and-test": "^1.12.5",
|
|
93
|
+
"typescript": "^4.3.4"
|
|
92
94
|
}
|
|
93
95
|
}
|