focus-trap 6.8.0-beta.0 → 6.8.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 +23 -0
- package/README.md +7 -3
- package/dist/focus-trap.esm.js +108 -35
- 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 +107 -34
- 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 +107 -34
- 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.js +113 -36
- package/package.json +18 -17
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tabbable, isFocusable, isTabbable } from 'tabbable';
|
|
1
|
+
import { tabbable, focusable, isFocusable, isTabbable } from 'tabbable';
|
|
2
2
|
|
|
3
3
|
const activeFocusTraps = (function () {
|
|
4
4
|
const trapQueue = [];
|
|
@@ -108,16 +108,29 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
const state = {
|
|
111
|
+
// containers given to createFocusTrap()
|
|
111
112
|
// @type {Array<HTMLElement>}
|
|
112
113
|
containers: [],
|
|
113
114
|
|
|
114
|
-
// list of objects identifying
|
|
115
|
-
// the trap
|
|
115
|
+
// list of objects identifying tabbable nodes in `containers` in the trap
|
|
116
116
|
// NOTE: it's possible that a group has no tabbable nodes if nodes get removed while the trap
|
|
117
117
|
// is active, but the trap should never get to a state where there isn't at least one group
|
|
118
118
|
// with at least one tabbable node in it (that would lead to an error condition that would
|
|
119
119
|
// result in an error being thrown)
|
|
120
|
-
// @type {Array<{
|
|
120
|
+
// @type {Array<{
|
|
121
|
+
// container: HTMLElement,
|
|
122
|
+
// tabbableNodes: Array<HTMLElement>, // empty if none
|
|
123
|
+
// focusableNodes: Array<HTMLElement>, // empty if none
|
|
124
|
+
// firstTabbableNode: HTMLElement|null,
|
|
125
|
+
// lastTabbableNode: HTMLElement|null,
|
|
126
|
+
// nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
|
|
127
|
+
// }>}
|
|
128
|
+
containerGroups: [], // same order/length as `containers` list
|
|
129
|
+
|
|
130
|
+
// references to objects in `containerGroups`, but only those that actually have
|
|
131
|
+
// tabbable nodes in them
|
|
132
|
+
// NOTE: same order as `containers` and `containerGroups`, but __not necessarily__
|
|
133
|
+
// the same length
|
|
121
134
|
tabbableGroups: [],
|
|
122
135
|
|
|
123
136
|
nodeFocusedBeforeActivation: null,
|
|
@@ -147,10 +160,25 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
147
160
|
: config[configOptionName || optionName];
|
|
148
161
|
};
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Finds the index of the container that contains the element.
|
|
165
|
+
* @param {HTMLElement} element
|
|
166
|
+
* @returns {number} Index of the container in either `state.containers` or
|
|
167
|
+
* `state.containerGroups` (the order/length of these lists are the same); -1
|
|
168
|
+
* if the element isn't found.
|
|
169
|
+
*/
|
|
170
|
+
const findContainerIndex = function (element) {
|
|
171
|
+
// NOTE: search `containerGroups` because it's possible a group contains no tabbable
|
|
172
|
+
// nodes, but still contains focusable nodes (e.g. if they all have `tabindex=-1`)
|
|
173
|
+
// and we still need to find the element in there
|
|
174
|
+
return state.containerGroups.findIndex(
|
|
175
|
+
({ container, tabbableNodes }) =>
|
|
176
|
+
container.contains(element) ||
|
|
177
|
+
// fall back to explicit tabbable search which will take into consideration any
|
|
178
|
+
// web components if the `tabbableOptions.getShadowRoot` option was used for
|
|
179
|
+
// the trap, enabling shadow DOM support in tabbable (`Node.contains()` doesn't
|
|
180
|
+
// look inside web components even if open)
|
|
181
|
+
tabbableNodes.find((node) => node === element)
|
|
154
182
|
);
|
|
155
183
|
};
|
|
156
184
|
|
|
@@ -209,7 +237,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
209
237
|
|
|
210
238
|
if (node === undefined) {
|
|
211
239
|
// option not specified: use fallback options
|
|
212
|
-
if (
|
|
240
|
+
if (findContainerIndex(doc.activeElement) >= 0) {
|
|
213
241
|
node = doc.activeElement;
|
|
214
242
|
} else {
|
|
215
243
|
const firstTabbableGroup = state.tabbableGroups[0];
|
|
@@ -231,23 +259,66 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
231
259
|
};
|
|
232
260
|
|
|
233
261
|
const updateTabbableNodes = function () {
|
|
234
|
-
state.
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
});
|
|
262
|
+
state.containerGroups = state.containers.map((container) => {
|
|
263
|
+
const tabbableNodes = tabbable(container, {
|
|
264
|
+
getShadowRoot: config.tabbableOptions?.getShadowRoot,
|
|
265
|
+
});
|
|
239
266
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
};
|
|
246
|
-
}
|
|
267
|
+
// NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
|
|
268
|
+
// are a superset of tabbable nodes
|
|
269
|
+
const focusableNodes = focusable(container, {
|
|
270
|
+
getShadowRoot: config.tabbableOptions?.getShadowRoot,
|
|
271
|
+
});
|
|
247
272
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
return {
|
|
274
|
+
container,
|
|
275
|
+
tabbableNodes,
|
|
276
|
+
focusableNodes,
|
|
277
|
+
firstTabbableNode: tabbableNodes.length > 0 ? tabbableNodes[0] : null,
|
|
278
|
+
lastTabbableNode:
|
|
279
|
+
tabbableNodes.length > 0
|
|
280
|
+
? tabbableNodes[tabbableNodes.length - 1]
|
|
281
|
+
: null,
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Finds the __tabbable__ node that follows the given node in the specified direction,
|
|
285
|
+
* in this container, if any.
|
|
286
|
+
* @param {HTMLElement} node
|
|
287
|
+
* @param {boolean} [forward] True if going in forward tab order; false if going
|
|
288
|
+
* in reverse.
|
|
289
|
+
* @returns {HTMLElement|undefined} The next tabbable node, if any.
|
|
290
|
+
*/
|
|
291
|
+
nextTabbableNode(node, forward = true) {
|
|
292
|
+
// NOTE: If tabindex is positive (in order to manipulate the tab order separate
|
|
293
|
+
// from the DOM order), this __will not work__ because the list of focusableNodes,
|
|
294
|
+
// while it contains tabbable nodes, does not sort its nodes in any order other
|
|
295
|
+
// than DOM order, because it can't: Where would you place focusable (but not
|
|
296
|
+
// tabbable) nodes in that order? They have no order, because they aren't tabbale...
|
|
297
|
+
// Support for positive tabindex is already broken and hard to manage (possibly
|
|
298
|
+
// not supportable, TBD), so this isn't going to make things worse than they
|
|
299
|
+
// already are, and at least makes things better for the majority of cases where
|
|
300
|
+
// tabindex is either 0/unset or negative.
|
|
301
|
+
// FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
|
|
302
|
+
const nodeIdx = focusableNodes.findIndex((n) => n === node);
|
|
303
|
+
if (nodeIdx < 0) {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (forward) {
|
|
308
|
+
return focusableNodes.slice(nodeIdx + 1).find((n) => isTabbable(n));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return focusableNodes
|
|
312
|
+
.slice(0, nodeIdx)
|
|
313
|
+
.reverse()
|
|
314
|
+
.find((n) => isTabbable(n));
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
state.tabbableGroups = state.containerGroups.filter(
|
|
320
|
+
(group) => group.tabbableNodes.length > 0
|
|
321
|
+
);
|
|
251
322
|
|
|
252
323
|
// throw if no groups have tabbable nodes and we don't have a fallback focus node either
|
|
253
324
|
if (
|
|
@@ -292,7 +363,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
292
363
|
const checkPointerDown = function (e) {
|
|
293
364
|
const target = getActualTarget(e);
|
|
294
365
|
|
|
295
|
-
if (
|
|
366
|
+
if (findContainerIndex(target) >= 0) {
|
|
296
367
|
// allow the click since it ocurred inside the trap
|
|
297
368
|
return;
|
|
298
369
|
}
|
|
@@ -331,7 +402,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
331
402
|
// In case focus escapes the trap for some strange reason, pull it back in.
|
|
332
403
|
const checkFocusIn = function (e) {
|
|
333
404
|
const target = getActualTarget(e);
|
|
334
|
-
const targetContained =
|
|
405
|
+
const targetContained = findContainerIndex(target) >= 0;
|
|
335
406
|
|
|
336
407
|
// In Firefox when you Tab out of an iframe the Document is briefly focused.
|
|
337
408
|
if (targetContained || target instanceof Document) {
|
|
@@ -359,9 +430,9 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
359
430
|
// make sure the target is actually contained in a group
|
|
360
431
|
// NOTE: the target may also be the container itself if it's focusable
|
|
361
432
|
// with tabIndex='-1' and was given initial focus
|
|
362
|
-
const containerIndex =
|
|
363
|
-
|
|
364
|
-
|
|
433
|
+
const containerIndex = findContainerIndex(target);
|
|
434
|
+
const containerGroup =
|
|
435
|
+
containerIndex >= 0 ? state.containerGroups[containerIndex] : undefined;
|
|
365
436
|
|
|
366
437
|
if (containerIndex < 0) {
|
|
367
438
|
// target not found in any group: quite possible focus has escaped the trap,
|
|
@@ -386,12 +457,15 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
386
457
|
|
|
387
458
|
if (
|
|
388
459
|
startOfGroupIndex < 0 &&
|
|
389
|
-
(
|
|
390
|
-
(isFocusable(target) &&
|
|
460
|
+
(containerGroup.container === target ||
|
|
461
|
+
(isFocusable(target) &&
|
|
462
|
+
!isTabbable(target) &&
|
|
463
|
+
!containerGroup.nextTabbableNode(target, false)))
|
|
391
464
|
) {
|
|
392
465
|
// an exception case where the target is either the container itself, or
|
|
393
466
|
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
394
|
-
// and user clicked on it or node was programmatically given focus)
|
|
467
|
+
// and user clicked on it or node was programmatically given focus)
|
|
468
|
+
// and is not followed by any other tabbable node, in which
|
|
395
469
|
// case, we should handle shift+tab as if focus were on the container's
|
|
396
470
|
// first tabbable node, and go to the last tabbable node of the LAST group
|
|
397
471
|
startOfGroupIndex = containerIndex;
|
|
@@ -420,12 +494,15 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
420
494
|
|
|
421
495
|
if (
|
|
422
496
|
lastOfGroupIndex < 0 &&
|
|
423
|
-
(
|
|
424
|
-
(isFocusable(target) &&
|
|
497
|
+
(containerGroup.container === target ||
|
|
498
|
+
(isFocusable(target) &&
|
|
499
|
+
!isTabbable(target) &&
|
|
500
|
+
!containerGroup.nextTabbableNode(target)))
|
|
425
501
|
) {
|
|
426
502
|
// an exception case where the target is the container itself, or
|
|
427
503
|
// a non-tabbable node that was given focus (i.e. tabindex is negative
|
|
428
|
-
// and user clicked on it or node was programmatically given focus)
|
|
504
|
+
// and user clicked on it or node was programmatically given focus)
|
|
505
|
+
// and is not followed by any other tabbable node, in which
|
|
429
506
|
// case, we should handle tab as if focus were on the container's
|
|
430
507
|
// last tabbable node, and go to the first tabbable node of the FIRST group
|
|
431
508
|
lastOfGroupIndex = containerIndex;
|
|
@@ -479,7 +556,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
479
556
|
|
|
480
557
|
const target = getActualTarget(e);
|
|
481
558
|
|
|
482
|
-
if (
|
|
559
|
+
if (findContainerIndex(target) >= 0) {
|
|
483
560
|
return;
|
|
484
561
|
}
|
|
485
562
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap",
|
|
3
|
-
"version": "6.8.0
|
|
3
|
+
"version": "6.8.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",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"test:chrome": "CYPRESS_BROWSER=chrome yarn test:cypress:ci",
|
|
39
39
|
"test": "yarn format:check && yarn lint && yarn test:unit && yarn test:types && CYPRESS_BROWSER=chrome yarn test:cypress:ci",
|
|
40
40
|
"prepare": "yarn build",
|
|
41
|
+
"prepublishOnly": "yarn test && yarn build",
|
|
41
42
|
"release": "yarn build && changeset publish"
|
|
42
43
|
},
|
|
43
44
|
"repository": {
|
|
@@ -62,36 +63,36 @@
|
|
|
62
63
|
},
|
|
63
64
|
"homepage": "https://github.com/focus-trap/focus-trap#readme",
|
|
64
65
|
"dependencies": {
|
|
65
|
-
"tabbable": "5.3.0
|
|
66
|
+
"tabbable": "5.3.0"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"@babel/cli": "^7.
|
|
69
|
-
"@babel/core": "^7.
|
|
70
|
-
"@babel/eslint-parser": "^7.
|
|
69
|
+
"@babel/cli": "^7.17.6",
|
|
70
|
+
"@babel/core": "^7.17.9",
|
|
71
|
+
"@babel/eslint-parser": "^7.17.0",
|
|
71
72
|
"@babel/preset-env": "^7.16.11",
|
|
72
|
-
"@changesets/cli": "^2.
|
|
73
|
-
"@rollup/plugin-babel": "^5.3.
|
|
74
|
-
"@rollup/plugin-commonjs": "^21.0
|
|
75
|
-
"@rollup/plugin-node-resolve": "^13.1
|
|
73
|
+
"@changesets/cli": "^2.22.0",
|
|
74
|
+
"@rollup/plugin-babel": "^5.3.1",
|
|
75
|
+
"@rollup/plugin-commonjs": "^21.1.0",
|
|
76
|
+
"@rollup/plugin-node-resolve": "^13.2.1",
|
|
76
77
|
"@testing-library/cypress": "^8.0.2",
|
|
77
|
-
"@types/jquery": "^3.5.
|
|
78
|
+
"@types/jquery": "^3.5.14",
|
|
78
79
|
"all-contributors-cli": "^6.20.0",
|
|
79
|
-
"babel-loader": "^8.2.
|
|
80
|
+
"babel-loader": "^8.2.5",
|
|
80
81
|
"cross-env": "^7.0.3",
|
|
81
|
-
"cypress": "^9.
|
|
82
|
+
"cypress": "^9.5.4",
|
|
82
83
|
"cypress-plugin-tab": "^1.0.5",
|
|
83
|
-
"eslint": "^8.
|
|
84
|
-
"eslint-config-prettier": "^8.
|
|
84
|
+
"eslint": "^8.13.0",
|
|
85
|
+
"eslint-config-prettier": "^8.5.0",
|
|
85
86
|
"eslint-plugin-cypress": "^2.12.1",
|
|
86
87
|
"onchange": "^7.1.0",
|
|
87
|
-
"prettier": "^2.
|
|
88
|
-
"rollup": "^2.
|
|
88
|
+
"prettier": "^2.6.2",
|
|
89
|
+
"rollup": "^2.70.2",
|
|
89
90
|
"rollup-plugin-inject-process-env": "^1.3.1",
|
|
90
91
|
"rollup-plugin-livereload": "^2.0.5",
|
|
91
92
|
"rollup-plugin-serve": "^1.1.0",
|
|
92
93
|
"rollup-plugin-sourcemaps": "^0.6.3",
|
|
93
94
|
"rollup-plugin-terser": "^7.0.1",
|
|
94
95
|
"start-server-and-test": "^1.14.0",
|
|
95
|
-
"typescript": "^4.
|
|
96
|
+
"typescript": "^4.6.3"
|
|
96
97
|
}
|
|
97
98
|
}
|