focus-trap 6.6.1 → 6.7.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 +13 -0
- package/README.md +25 -9
- package/dist/focus-trap.esm.js +80 -40
- 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 +80 -40
- 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 +83 -43
- 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 +89 -43
- package/package.json +22 -20
package/index.js
CHANGED
|
@@ -82,8 +82,21 @@ 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
|
-
const doc = document;
|
|
99
|
+
const doc = userOptions.document || document;
|
|
87
100
|
|
|
88
101
|
const config = {
|
|
89
102
|
returnFocusOnDeactivate: true,
|
|
@@ -125,28 +138,51 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
125
138
|
};
|
|
126
139
|
|
|
127
140
|
const containersContain = function (element) {
|
|
128
|
-
return
|
|
141
|
+
return !!(
|
|
142
|
+
element &&
|
|
143
|
+
state.containers.some((container) => container.contains(element))
|
|
144
|
+
);
|
|
129
145
|
};
|
|
130
146
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
147
|
+
/**
|
|
148
|
+
* Gets the node for the given option, which is expected to be an option that
|
|
149
|
+
* can be either a DOM node, a string that is a selector to get a node, `false`
|
|
150
|
+
* (if a node is explicitly NOT given), or a function that returns any of these
|
|
151
|
+
* values.
|
|
152
|
+
* @param {string} optionName
|
|
153
|
+
* @returns {undefined | false | HTMLElement | SVGElement} Returns
|
|
154
|
+
* `undefined` if the option is not specified; `false` if the option
|
|
155
|
+
* resolved to `false` (node explicitly not given); otherwise, the resolved
|
|
156
|
+
* DOM node.
|
|
157
|
+
* @throws {Error} If the option is set, not `false`, and is not, or does not
|
|
158
|
+
* resolve to a node.
|
|
159
|
+
*/
|
|
160
|
+
const getNodeForOption = function (optionName, ...params) {
|
|
161
|
+
let optionValue = config[optionName];
|
|
136
162
|
|
|
137
|
-
|
|
163
|
+
if (typeof optionValue === 'function') {
|
|
164
|
+
optionValue = optionValue(...params);
|
|
165
|
+
}
|
|
138
166
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
throw new Error(`\`${optionName}\` refers to no known node`);
|
|
167
|
+
if (!optionValue) {
|
|
168
|
+
if (optionValue === undefined || optionValue === false) {
|
|
169
|
+
return optionValue;
|
|
143
170
|
}
|
|
171
|
+
// else, empty string (invalid), null (invalid), 0 (invalid)
|
|
172
|
+
|
|
173
|
+
throw new Error(
|
|
174
|
+
`\`${optionName}\` was specified but was not a node, or did not return a node`
|
|
175
|
+
);
|
|
144
176
|
}
|
|
145
177
|
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
let node = optionValue; // could be HTMLElement, SVGElement, or non-empty string at this point
|
|
179
|
+
|
|
180
|
+
if (typeof optionValue === 'string') {
|
|
181
|
+
node = doc.querySelector(optionValue); // resolve to node, or null if fails
|
|
148
182
|
if (!node) {
|
|
149
|
-
throw new Error(
|
|
183
|
+
throw new Error(
|
|
184
|
+
`\`${optionName}\` as selector refers to no known node`
|
|
185
|
+
);
|
|
150
186
|
}
|
|
151
187
|
}
|
|
152
188
|
|
|
@@ -154,22 +190,25 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
154
190
|
};
|
|
155
191
|
|
|
156
192
|
const getInitialFocusNode = function () {
|
|
157
|
-
let node;
|
|
193
|
+
let node = getNodeForOption('initialFocus');
|
|
158
194
|
|
|
159
|
-
// false indicates we want no initialFocus at all
|
|
160
|
-
if (
|
|
195
|
+
// false explicitly indicates we want no initialFocus at all
|
|
196
|
+
if (node === false) {
|
|
161
197
|
return false;
|
|
162
198
|
}
|
|
163
199
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
if (node === undefined) {
|
|
201
|
+
// option not specified: use fallback options
|
|
202
|
+
if (containersContain(doc.activeElement)) {
|
|
203
|
+
node = doc.activeElement;
|
|
204
|
+
} else {
|
|
205
|
+
const firstTabbableGroup = state.tabbableGroups[0];
|
|
206
|
+
const firstTabbableNode =
|
|
207
|
+
firstTabbableGroup && firstTabbableGroup.firstTabbableNode;
|
|
208
|
+
|
|
209
|
+
// NOTE: `fallbackFocus` option function cannot return `false` (not supported)
|
|
210
|
+
node = firstTabbableNode || getNodeForOption('fallbackFocus');
|
|
211
|
+
}
|
|
173
212
|
}
|
|
174
213
|
|
|
175
214
|
if (!node) {
|
|
@@ -201,7 +240,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
201
240
|
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
|
202
241
|
if (
|
|
203
242
|
state.tabbableGroups.length <= 0 &&
|
|
204
|
-
!getNodeForOption('fallbackFocus')
|
|
243
|
+
!getNodeForOption('fallbackFocus') // returning false not supported for this option
|
|
205
244
|
) {
|
|
206
245
|
throw new Error(
|
|
207
246
|
'Your focus-trap must have at least one container with at least one tabbable node in it at all times'
|
|
@@ -232,15 +271,16 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
232
271
|
};
|
|
233
272
|
|
|
234
273
|
const getReturnFocusNode = function (previousActiveElement) {
|
|
235
|
-
const node = getNodeForOption('setReturnFocus');
|
|
236
|
-
|
|
237
|
-
return node ? node : previousActiveElement;
|
|
274
|
+
const node = getNodeForOption('setReturnFocus', previousActiveElement);
|
|
275
|
+
return node ? node : node === false ? false : previousActiveElement;
|
|
238
276
|
};
|
|
239
277
|
|
|
240
278
|
// This needs to be done on mousedown and touchstart instead of click
|
|
241
279
|
// so that it precedes the focus event.
|
|
242
280
|
const checkPointerDown = function (e) {
|
|
243
|
-
|
|
281
|
+
const target = getActualTarget(e);
|
|
282
|
+
|
|
283
|
+
if (containersContain(target)) {
|
|
244
284
|
// allow the click since it ocurred inside the trap
|
|
245
285
|
return;
|
|
246
286
|
}
|
|
@@ -259,7 +299,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
259
299
|
// that was clicked, whether it's focusable or not; by setting
|
|
260
300
|
// `returnFocus: true`, we'll attempt to re-focus the node originally-focused
|
|
261
301
|
// on activation (or the configured `setReturnFocus` node)
|
|
262
|
-
returnFocus: config.returnFocusOnDeactivate && !isFocusable(
|
|
302
|
+
returnFocus: config.returnFocusOnDeactivate && !isFocusable(target),
|
|
263
303
|
});
|
|
264
304
|
return;
|
|
265
305
|
}
|
|
@@ -278,11 +318,13 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
278
318
|
|
|
279
319
|
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
280
320
|
const checkFocusIn = function (e) {
|
|
281
|
-
const
|
|
321
|
+
const target = getActualTarget(e);
|
|
322
|
+
const targetContained = containersContain(target);
|
|
323
|
+
|
|
282
324
|
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
283
|
-
if (targetContained ||
|
|
325
|
+
if (targetContained || target instanceof Document) {
|
|
284
326
|
if (targetContained) {
|
|
285
|
-
state.mostRecentlyFocusedNode =
|
|
327
|
+
state.mostRecentlyFocusedNode = target;
|
|
286
328
|
}
|
|
287
329
|
} else {
|
|
288
330
|
// escaped! pull it back in to where it just left
|
|
@@ -296,6 +338,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
296
338
|
// moment it can end up scrolling the page and causing confusion so we
|
|
297
339
|
// kind of need to capture the action at the keydown phase.
|
|
298
340
|
const checkTab = function (e) {
|
|
341
|
+
const target = getActualTarget(e);
|
|
299
342
|
updateTabbableNodes();
|
|
300
343
|
|
|
301
344
|
let destinationNode = null;
|
|
@@ -305,7 +348,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
305
348
|
// NOTE: the target may also be the container itself if it's tabbable
|
|
306
349
|
// with tabIndex='-1' and was given initial focus
|
|
307
350
|
const containerIndex = findIndex(state.tabbableGroups, ({ container }) =>
|
|
308
|
-
container.contains(
|
|
351
|
+
container.contains(target)
|
|
309
352
|
);
|
|
310
353
|
|
|
311
354
|
if (containerIndex < 0) {
|
|
@@ -326,12 +369,12 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
326
369
|
// is the target the first tabbable node in a group?
|
|
327
370
|
let startOfGroupIndex = findIndex(
|
|
328
371
|
state.tabbableGroups,
|
|
329
|
-
({ firstTabbableNode }) =>
|
|
372
|
+
({ firstTabbableNode }) => target === firstTabbableNode
|
|
330
373
|
);
|
|
331
374
|
|
|
332
375
|
if (
|
|
333
376
|
startOfGroupIndex < 0 &&
|
|
334
|
-
state.tabbableGroups[containerIndex].container ===
|
|
377
|
+
state.tabbableGroups[containerIndex].container === target
|
|
335
378
|
) {
|
|
336
379
|
// an exception case where the target is the container itself, in which
|
|
337
380
|
// case, we should handle shift+tab as if focus were on the container's
|
|
@@ -357,12 +400,12 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
357
400
|
// is the target the last tabbable node in a group?
|
|
358
401
|
let lastOfGroupIndex = findIndex(
|
|
359
402
|
state.tabbableGroups,
|
|
360
|
-
({ lastTabbableNode }) =>
|
|
403
|
+
({ lastTabbableNode }) => target === lastTabbableNode
|
|
361
404
|
);
|
|
362
405
|
|
|
363
406
|
if (
|
|
364
407
|
lastOfGroupIndex < 0 &&
|
|
365
|
-
state.tabbableGroups[containerIndex].container ===
|
|
408
|
+
state.tabbableGroups[containerIndex].container === target
|
|
366
409
|
) {
|
|
367
410
|
// an exception case where the target is the container itself, in which
|
|
368
411
|
// case, we should handle tab as if focus were on the container's
|
|
@@ -384,6 +427,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
384
427
|
}
|
|
385
428
|
}
|
|
386
429
|
} else {
|
|
430
|
+
// NOTE: the fallbackFocus option does not support returning false to opt-out
|
|
387
431
|
destinationNode = getNodeForOption('fallbackFocus');
|
|
388
432
|
}
|
|
389
433
|
|
|
@@ -397,7 +441,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
397
441
|
const checkKey = function (e) {
|
|
398
442
|
if (
|
|
399
443
|
isEscapeEvent(e) &&
|
|
400
|
-
valueOrHandler(config.escapeDeactivates) !== false
|
|
444
|
+
valueOrHandler(config.escapeDeactivates, e) !== false
|
|
401
445
|
) {
|
|
402
446
|
e.preventDefault();
|
|
403
447
|
trap.deactivate();
|
|
@@ -415,7 +459,9 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
415
459
|
return;
|
|
416
460
|
}
|
|
417
461
|
|
|
418
|
-
|
|
462
|
+
const target = getActualTarget(e);
|
|
463
|
+
|
|
464
|
+
if (containersContain(target)) {
|
|
419
465
|
return;
|
|
420
466
|
}
|
|
421
467
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.7.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,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",
|
|
@@ -63,33 +65,33 @@
|
|
|
63
65
|
"tabbable": "^5.2.1"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
|
-
"@babel/cli": "^7.
|
|
67
|
-
"@babel/core": "^7.15.
|
|
68
|
-
"@babel/preset-env": "^7.15.
|
|
69
|
-
"@changesets/cli": "^2.
|
|
68
|
+
"@babel/cli": "^7.15.7",
|
|
69
|
+
"@babel/core": "^7.15.5",
|
|
70
|
+
"@babel/preset-env": "^7.15.6",
|
|
71
|
+
"@changesets/cli": "^2.17.0",
|
|
70
72
|
"@rollup/plugin-babel": "^5.3.0",
|
|
71
73
|
"@rollup/plugin-commonjs": "^20.0.0",
|
|
72
|
-
"@rollup/plugin-node-resolve": "^13.0.
|
|
73
|
-
"@testing-library/cypress": "^8.0.
|
|
74
|
+
"@rollup/plugin-node-resolve": "^13.0.5",
|
|
75
|
+
"@testing-library/cypress": "^8.0.1",
|
|
74
76
|
"@types/jquery": "^3.5.6",
|
|
75
77
|
"all-contributors-cli": "^6.20.0",
|
|
76
78
|
"babel-eslint": "^10.1.0",
|
|
77
79
|
"babel-loader": "^8.2.2",
|
|
78
|
-
"babelify": "^10.0.0",
|
|
79
|
-
"browserify": "^17.0.0",
|
|
80
|
-
"budo": "^11.6.4",
|
|
81
80
|
"cross-env": "^7.0.3",
|
|
82
|
-
"cypress": "^8.
|
|
81
|
+
"cypress": "^8.4.1",
|
|
83
82
|
"cypress-plugin-tab": "^1.0.5",
|
|
84
83
|
"eslint": "^7.32.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.4.1",
|
|
88
|
+
"rollup": "^2.57.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.3
|
|
94
|
+
"start-server-and-test": "^1.14.0",
|
|
95
|
+
"typescript": "^4.4.3"
|
|
94
96
|
}
|
|
95
97
|
}
|