focus-trap 6.6.0 → 6.7.2
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 +31 -0
- package/README.md +25 -9
- package/dist/focus-trap.esm.js +99 -65
- 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 +98 -64
- 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 +101 -67
- 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 +25 -4
- package/index.js +101 -47
- package/package.json +28 -26
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tabbable, isFocusable } from 'tabbable';
|
|
1
|
+
import { tabbable, isFocusable, isTabbable } from 'tabbable';
|
|
2
2
|
|
|
3
3
|
const activeFocusTraps = (function () {
|
|
4
4
|
const trapQueue = [];
|
|
@@ -82,8 +82,23 @@ const valueOrHandler = function (value, ...params) {
|
|
|
82
82
|
return typeof value === 'function' ? value(...params) : value;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
+
const getActualTarget = function (event) {
|
|
86
|
+
// NOTE: If the trap is _inside_ a shadow DOM, event.target will always be the
|
|
87
|
+
// shadow host. However, event.target.composedPath() will be an array of
|
|
88
|
+
// nodes "clicked" from inner-most (the actual element inside the shadow) to
|
|
89
|
+
// outer-most (the host HTML document). If we have access to composedPath(),
|
|
90
|
+
// then use its first element; otherwise, fall back to event.target (and
|
|
91
|
+
// this only works for an _open_ shadow DOM; otherwise,
|
|
92
|
+
// composedPath()[0] === event.target always).
|
|
93
|
+
return event.target.shadowRoot && typeof event.composedPath === 'function'
|
|
94
|
+
? event.composedPath()[0]
|
|
95
|
+
: event.target;
|
|
96
|
+
};
|
|
97
|
+
|
|
85
98
|
const createFocusTrap = function (elements, userOptions) {
|
|
86
|
-
|
|
99
|
+
// SSR: a live trap shouldn't be created in this type of environment so this
|
|
100
|
+
// should be safe code to execute if the `document` option isn't specified
|
|
101
|
+
const doc = userOptions?.document || document;
|
|
87
102
|
|
|
88
103
|
const config = {
|
|
89
104
|
returnFocusOnDeactivate: true,
|
|
@@ -125,28 +140,51 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
125
140
|
};
|
|
126
141
|
|
|
127
142
|
const containersContain = function (element) {
|
|
128
|
-
return
|
|
143
|
+
return !!(
|
|
144
|
+
element &&
|
|
145
|
+
state.containers.some((container) => container.contains(element))
|
|
146
|
+
);
|
|
129
147
|
};
|
|
130
148
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Gets the node for the given option, which is expected to be an option that
|
|
151
|
+
* can be either a DOM node, a string that is a selector to get a node, `false`
|
|
152
|
+
* (if a node is explicitly NOT given), or a function that returns any of these
|
|
153
|
+
* values.
|
|
154
|
+
* @param {string} optionName
|
|
155
|
+
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
|
156
|
+
* `undefined` if the option is not specified; `false` if the option
|
|
157
|
+
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
|
158
|
+
* DOM node.
|
|
159
|
+
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
|
160
|
+
* resolve to a node.
|
|
161
|
+
*/
|
|
162
|
+
const getNodeForOption = function (optionName, ...params) {
|
|
163
|
+
let optionValue = config[optionName];
|
|
136
164
|
|
|
137
|
-
|
|
165
|
+
if (typeof optionValue === 'function') {
|
|
166
|
+
optionValue = optionValue(...params);
|
|
167
|
+
}
|
|
138
168
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
throw new Error(`\`${optionName}\` refers to no known node`);
|
|
169
|
+
if (!optionValue) {
|
|
170
|
+
if (optionValue === undefined || optionValue === false) {
|
|
171
|
+
return optionValue;
|
|
143
172
|
}
|
|
173
|
+
// else, empty string (invalid), null (invalid), 0 (invalid)
|
|
174
|
+
|
|
175
|
+
throw new Error(
|
|
176
|
+
`\`${optionName}\` was specified but was not a node, or did not return a node`
|
|
177
|
+
);
|
|
144
178
|
}
|
|
145
179
|
|
|
146
|
-
|
|
147
|
-
|
|
180
|
+
let node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
|
181
|
+
|
|
182
|
+
if (typeof optionValue === 'string') {
|
|
183
|
+
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
|
148
184
|
if (!node) {
|
|
149
|
-
throw new Error(
|
|
185
|
+
throw new Error(
|
|
186
|
+
`\`${optionName}\` as selector refers to no known node`
|
|
187
|
+
);
|
|
150
188
|
}
|
|
151
189
|
}
|
|
152
190
|
|
|
@@ -154,22 +192,25 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
154
192
|
};
|
|
155
193
|
|
|
156
194
|
const getInitialFocusNode = function () {
|
|
157
|
-
let node;
|
|
195
|
+
let node = getNodeForOption('initialFocus');
|
|
158
196
|
|
|
159
|
-
// false indicates we want no initialFocus at all
|
|
160
|
-
if (
|
|
197
|
+
// false explicitly indicates we want no initialFocus at all
|
|
198
|
+
if (node === false) {
|
|
161
199
|
return false;
|
|
162
200
|
}
|
|
163
201
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
202
|
+
if (node === undefined) {
|
|
203
|
+
// option not specified: use fallback options
|
|
204
|
+
if (containersContain(doc.activeElement)) {
|
|
205
|
+
node = doc.activeElement;
|
|
206
|
+
} else {
|
|
207
|
+
const firstTabbableGroup = state.tabbableGroups[0];
|
|
208
|
+
const firstTabbableNode =
|
|
209
|
+
firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
|
210
|
+
|
|
211
|
+
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
|
212
|
+
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
213
|
+
}
|
|
173
214
|
}
|
|
174
215
|
|
|
175
216
|
if (!node) {
|
|
@@ -201,7 +242,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
201
242
|
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
|
202
243
|
if (
|
|
203
244
|
state.tabbableGroups.length <= 0 &&
|
|
204
|
-
!getNodeForOption('fallbackFocus')
|
|
245
|
+
!getNodeForOption('fallbackFocus') // returning false not supported for this option
|
|
205
246
|
) {
|
|
206
247
|
throw new Error(
|
|
207
248
|
'Your focus-trap must have at least one container with at least one tabbable node in it at all times'
|
|
@@ -232,15 +273,16 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
232
273
|
};
|
|
233
274
|
|
|
234
275
|
const getReturnFocusNode = function (previousActiveElement) {
|
|
235
|
-
const node = getNodeForOption('setReturnFocus');
|
|
236
|
-
|
|
237
|
-
return node ? node : previousActiveElement;
|
|
276
|
+
const node = getNodeForOption('setReturnFocus', previousActiveElement);
|
|
277
|
+
return node ? node : node === false ? false : previousActiveElement;
|
|
238
278
|
};
|
|
239
279
|
|
|
240
280
|
// This needs to be done on mousedown and touchstart instead of click
|
|
241
281
|
// so that it precedes the focus event.
|
|
242
282
|
const checkPointerDown = function (e) {
|
|
243
|
-
|
|
283
|
+
const target = getActualTarget(e);
|
|
284
|
+
|
|
285
|
+
if (containersContain(target)) {
|
|
244
286
|
// allow the click since it ocurred inside the trap
|
|
245
287
|
return;
|
|
246
288
|
}
|
|
@@ -259,7 +301,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
259
301
|
// that was clicked, whether it's focusable or not; by setting
|
|
260
302
|
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
|
261
303
|
// on activation (or the configured `setReturnFocus` node)
|
|
262
|
-
returnFocus: config.returnFocusOnDeactivate && !isFocusable(
|
|
304
|
+
returnFocus: config.returnFocusOnDeactivate && !isFocusable(target),
|
|
263
305
|
});
|
|
264
306
|
return;
|
|
265
307
|
}
|
|
@@ -278,11 +320,13 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
278
320
|
|
|
279
321
|
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
280
322
|
const checkFocusIn = function (e) {
|
|
281
|
-
const
|
|
323
|
+
const target = getActualTarget(e);
|
|
324
|
+
const targetContained = containersContain(target);
|
|
325
|
+
|
|
282
326
|
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
283
|
-
if (targetContained ||
|
|
327
|
+
if (targetContained || target instanceof Document) {
|
|
284
328
|
if (targetContained) {
|
|
285
|
-
state.mostRecentlyFocusedNode =
|
|
329
|
+
state.mostRecentlyFocusedNode = target;
|
|
286
330
|
}
|
|
287
331
|
} else {
|
|
288
332
|
// escaped! pull it back in to where it just left
|
|
@@ -296,16 +340,17 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
296
340
|
// moment it can end up scrolling the page and causing confusion so we
|
|
297
341
|
// kind of need to capture the action at the keydown phase.
|
|
298
342
|
const checkTab = function (e) {
|
|
343
|
+
const target = getActualTarget(e);
|
|
299
344
|
updateTabbableNodes();
|
|
300
345
|
|
|
301
346
|
let destinationNode = null;
|
|
302
347
|
|
|
303
348
|
if (state.tabbableGroups.length > 0) {
|
|
304
349
|
// make sure the target is actually contained in a group
|
|
305
|
-
// NOTE: the target may also be the container itself if it's
|
|
350
|
+
// NOTE: the target may also be the container itself if it's focusable
|
|
306
351
|
// with tabIndex='-1' and was given initial focus
|
|
307
352
|
const containerIndex = findIndex(state.tabbableGroups, ({ container }) =>
|
|
308
|
-
container.contains(
|
|
353
|
+
container.contains(target)
|
|
309
354
|
);
|
|
310
355
|
|
|
311
356
|
if (containerIndex < 0) {
|
|
@@ -326,14 +371,17 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
326
371
|
// is the target the first tabbable node in a group?
|
|
327
372
|
let startOfGroupIndex = findIndex(
|
|
328
373
|
state.tabbableGroups,
|
|
329
|
-
({ firstTabbableNode }) =>
|
|
374
|
+
({ firstTabbableNode }) => target === firstTabbableNode
|
|
330
375
|
);
|
|
331
376
|
|
|
332
377
|
if (
|
|
333
378
|
startOfGroupIndex < 0 &&
|
|
334
|
-
state.tabbableGroups[containerIndex].container ===
|
|
379
|
+
(state.tabbableGroups[containerIndex].container === target ||
|
|
380
|
+
(isFocusable(target) && !isTabbable(target)))
|
|
335
381
|
) {
|
|
336
|
-
// an exception case where the target is the container itself,
|
|
382
|
+
// an exception case where the target is either the container itself, or
|
|
383
|
+
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
384
|
+
// and user clicked on it or node was programmatically given focus), in which
|
|
337
385
|
// case, we should handle shift+tab as if focus were on the container's
|
|
338
386
|
// first tabbable node, and go to the last tabbable node of the LAST group
|
|
339
387
|
startOfGroupIndex = containerIndex;
|
|
@@ -357,14 +405,17 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
357
405
|
// is the target the last tabbable node in a group?
|
|
358
406
|
let lastOfGroupIndex = findIndex(
|
|
359
407
|
state.tabbableGroups,
|
|
360
|
-
({ lastTabbableNode }) =>
|
|
408
|
+
({ lastTabbableNode }) => target === lastTabbableNode
|
|
361
409
|
);
|
|
362
410
|
|
|
363
411
|
if (
|
|
364
412
|
lastOfGroupIndex < 0 &&
|
|
365
|
-
state.tabbableGroups[containerIndex].container ===
|
|
413
|
+
(state.tabbableGroups[containerIndex].container === target ||
|
|
414
|
+
(isFocusable(target) && !isTabbable(target)))
|
|
366
415
|
) {
|
|
367
|
-
// an exception case where the target is the container itself,
|
|
416
|
+
// an exception case where the target is the container itself, or
|
|
417
|
+
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
418
|
+
// and user clicked on it or node was programmatically given focus), in which
|
|
368
419
|
// case, we should handle tab as if focus were on the container's
|
|
369
420
|
// last tabbable node, and go to the first tabbable node of the FIRST group
|
|
370
421
|
lastOfGroupIndex = containerIndex;
|
|
@@ -384,6 +435,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
384
435
|
}
|
|
385
436
|
}
|
|
386
437
|
} else {
|
|
438
|
+
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
|
387
439
|
destinationNode = getNodeForOption('fallbackFocus');
|
|
388
440
|
}
|
|
389
441
|
|
|
@@ -397,7 +449,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
397
449
|
const checkKey = function (e) {
|
|
398
450
|
if (
|
|
399
451
|
isEscapeEvent(e) &&
|
|
400
|
-
valueOrHandler(config.escapeDeactivates) !== false
|
|
452
|
+
valueOrHandler(config.escapeDeactivates, e) !== false
|
|
401
453
|
) {
|
|
402
454
|
e.preventDefault();
|
|
403
455
|
trap.deactivate();
|
|
@@ -415,7 +467,9 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
415
467
|
return;
|
|
416
468
|
}
|
|
417
469
|
|
|
418
|
-
|
|
470
|
+
const target = getActualTarget(e);
|
|
471
|
+
|
|
472
|
+
if (containersContain(target)) {
|
|
419
473
|
return;
|
|
420
474
|
}
|
|
421
475
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.2",
|
|
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,7 +17,7 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"demo-bundle": "
|
|
20
|
+
"demo-bundle": "yarn compile:demo",
|
|
21
21
|
"format": "prettier --write \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
|
|
22
22
|
"format:check": "prettier --check \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\"",
|
|
23
23
|
"format:watch": "onchange \"{*,src/**/*,test/**/*,docs/js/**/*,.github/workflows/*,cypress/**/*}.+(js|yml)\" -- prettier --write {{changed}}",
|
|
@@ -26,13 +26,15 @@
|
|
|
26
26
|
"compile:esm": "cross-env BUILD_ENV=esm BABEL_ENV=esm rollup -c",
|
|
27
27
|
"compile:cjs": "cross-env BUILD_ENV=cjs BABEL_ENV=es5 rollup -c",
|
|
28
28
|
"compile:umd": "cross-env BUILD_ENV=umd BABEL_ENV=es5 rollup -c",
|
|
29
|
+
"compile:demo": "cross-env BUILD_ENV=demo BABEL_ENV=es5 rollup -c",
|
|
29
30
|
"compile": "yarn compile:esm && yarn compile:cjs && yarn compile:umd",
|
|
30
31
|
"build": "yarn clean && yarn compile",
|
|
31
|
-
"start": "yarn compile:
|
|
32
|
+
"start": "yarn compile:demo --watch --environment SERVE,RELOAD,IS_CYPRESS_ENV:''",
|
|
33
|
+
"start:cypress": "yarn compile:demo --environment SERVE,IS_CYPRESS_ENV:\"$CYPRESS_BROWSER\"",
|
|
32
34
|
"test:types": "tsc index.d.ts",
|
|
33
35
|
"test:unit": "echo \"No unit tests to run!\"",
|
|
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:cypress": "CYPRESS_BROWSER=ANY start-server-and-test start:cypress 9966 'cypress open'",
|
|
37
|
+
"test:cypress:ci": "start-server-and-test start:cypress 9966 'cypress run --browser $CYPRESS_BROWSER --headless'",
|
|
36
38
|
"test:chrome": "CYPRESS_BROWSER=chrome yarn test:cypress:ci",
|
|
37
39
|
"test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress:ci",
|
|
38
40
|
"prepare": "yarn build",
|
|
@@ -60,36 +62,36 @@
|
|
|
60
62
|
},
|
|
61
63
|
"homepage": "https://github.com/focus-trap/focus-trap#readme",
|
|
62
64
|
"dependencies": {
|
|
63
|
-
"tabbable": "^5.2.
|
|
65
|
+
"tabbable": "^5.2.1"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
|
-
"@babel/cli": "^7.
|
|
67
|
-
"@babel/core": "^7.
|
|
68
|
-
"@babel/
|
|
69
|
-
"@
|
|
68
|
+
"@babel/cli": "^7.16.8",
|
|
69
|
+
"@babel/core": "^7.16.7",
|
|
70
|
+
"@babel/eslint-parser": "^7.16.5",
|
|
71
|
+
"@babel/preset-env": "^7.16.8",
|
|
72
|
+
"@changesets/cli": "^2.19.0",
|
|
70
73
|
"@rollup/plugin-babel": "^5.3.0",
|
|
71
|
-
"@rollup/plugin-commonjs": "^
|
|
72
|
-
"@rollup/plugin-node-resolve": "^13.
|
|
73
|
-
"@testing-library/cypress": "^
|
|
74
|
-
"@types/jquery": "^3.5.
|
|
74
|
+
"@rollup/plugin-commonjs": "^21.0.1",
|
|
75
|
+
"@rollup/plugin-node-resolve": "^13.1.3",
|
|
76
|
+
"@testing-library/cypress": "^8.0.2",
|
|
77
|
+
"@types/jquery": "^3.5.13",
|
|
75
78
|
"all-contributors-cli": "^6.20.0",
|
|
76
|
-
"babel-
|
|
77
|
-
"babel-loader": "^8.2.2",
|
|
78
|
-
"babelify": "^10.0.0",
|
|
79
|
-
"browserify": "^17.0.0",
|
|
80
|
-
"budo": "^11.6.4",
|
|
79
|
+
"babel-loader": "^8.2.3",
|
|
81
80
|
"cross-env": "^7.0.3",
|
|
82
|
-
"cypress": "^
|
|
81
|
+
"cypress": "^9.2.1",
|
|
83
82
|
"cypress-plugin-tab": "^1.0.5",
|
|
84
|
-
"eslint": "^
|
|
83
|
+
"eslint": "^8.6.0",
|
|
85
84
|
"eslint-config-prettier": "^8.3.0",
|
|
86
|
-
"eslint-plugin-cypress": "^2.
|
|
85
|
+
"eslint-plugin-cypress": "^2.12.1",
|
|
87
86
|
"onchange": "^7.1.0",
|
|
88
|
-
"prettier": "^2.
|
|
89
|
-
"rollup": "^2.
|
|
87
|
+
"prettier": "^2.5.1",
|
|
88
|
+
"rollup": "^2.63.0",
|
|
89
|
+
"rollup-plugin-inject-process-env": "^1.3.1",
|
|
90
|
+
"rollup-plugin-livereload": "^2.0.5",
|
|
91
|
+
"rollup-plugin-serve": "^1.1.0",
|
|
90
92
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
91
93
|
"rollup-plugin-terser": "^7.0.1",
|
|
92
|
-
"start-server-and-test": "^1.
|
|
93
|
-
"typescript": "^4.
|
|
94
|
+
"start-server-and-test": "^1.14.0",
|
|
95
|
+
"typescript": "^4.5.4"
|
|
94
96
|
}
|
|
95
97
|
}
|