focus-trap 8.1.0 → 8.2.1
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 +17 -0
- package/README.md +20 -2
- package/dist/focus-trap.esm.js +103 -231
- package/dist/focus-trap.esm.js.map +1 -1
- package/dist/focus-trap.esm.min.js +2 -4
- package/dist/focus-trap.esm.min.js.map +1 -1
- package/dist/focus-trap.js +102 -230
- package/dist/focus-trap.js.map +1 -1
- package/dist/focus-trap.min.js +2 -4
- package/dist/focus-trap.min.js.map +1 -1
- package/dist/focus-trap.umd.js +102 -230
- package/dist/focus-trap.umd.js.map +1 -1
- package/dist/focus-trap.umd.min.js +2 -4
- package/dist/focus-trap.umd.min.js.map +1 -1
- package/index.d.ts +15 -0
- package/index.js +102 -63
- package/package.json +14 -14
package/index.js
CHANGED
|
@@ -128,6 +128,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
128
128
|
returnFocusOnDeactivate: true,
|
|
129
129
|
escapeDeactivates: true,
|
|
130
130
|
delayInitialFocus: true,
|
|
131
|
+
delayReturnFocus: true,
|
|
131
132
|
isolateSubtrees: false,
|
|
132
133
|
isKeyForward,
|
|
133
134
|
isKeyBackward,
|
|
@@ -300,6 +301,31 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
300
301
|
return node;
|
|
301
302
|
};
|
|
302
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Gets the current activeElement. If it's a web-component and has open shadow-root
|
|
306
|
+
* it will recursively search inside shadow roots for the "true" activeElement.
|
|
307
|
+
*
|
|
308
|
+
* @param {Document | ShadowRoot} el
|
|
309
|
+
*
|
|
310
|
+
* @returns {HTMLElement|null} The element that currently has the focus. `null` if a focused element isn't found.
|
|
311
|
+
**/
|
|
312
|
+
const getActiveElement = function (el) {
|
|
313
|
+
const activeElement = el.activeElement;
|
|
314
|
+
|
|
315
|
+
if (!activeElement) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (
|
|
320
|
+
activeElement.shadowRoot &&
|
|
321
|
+
activeElement.shadowRoot.activeElement !== null
|
|
322
|
+
) {
|
|
323
|
+
return getActiveElement(activeElement.shadowRoot);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return activeElement;
|
|
327
|
+
};
|
|
328
|
+
|
|
303
329
|
const getInitialFocusNode = function () {
|
|
304
330
|
let node = getNodeForOption('initialFocus', { hasFallback: true });
|
|
305
331
|
|
|
@@ -312,9 +338,11 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
312
338
|
node === undefined ||
|
|
313
339
|
(node && !isFocusable(node, config.tabbableOptions))
|
|
314
340
|
) {
|
|
341
|
+
const activeElement = getActiveElement(doc);
|
|
342
|
+
|
|
315
343
|
// option not specified nor focusable: use fallback options
|
|
316
|
-
if (findContainerIndex(
|
|
317
|
-
node =
|
|
344
|
+
if (findContainerIndex(activeElement) >= 0) {
|
|
345
|
+
node = activeElement;
|
|
318
346
|
} else {
|
|
319
347
|
const firstTabbableGroup = state.tabbableGroups[0];
|
|
320
348
|
const firstTabbableNode =
|
|
@@ -456,31 +484,6 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
456
484
|
}
|
|
457
485
|
};
|
|
458
486
|
|
|
459
|
-
/**
|
|
460
|
-
* Gets the current activeElement. If it's a web-component and has open shadow-root
|
|
461
|
-
* it will recursively search inside shadow roots for the "true" activeElement.
|
|
462
|
-
*
|
|
463
|
-
* @param {Document | ShadowRoot} el
|
|
464
|
-
*
|
|
465
|
-
* @returns {HTMLElement} The element that currently has the focus
|
|
466
|
-
**/
|
|
467
|
-
const getActiveElement = function (el) {
|
|
468
|
-
const activeElement = el.activeElement;
|
|
469
|
-
|
|
470
|
-
if (!activeElement) {
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (
|
|
475
|
-
activeElement.shadowRoot &&
|
|
476
|
-
activeElement.shadowRoot.activeElement !== null
|
|
477
|
-
) {
|
|
478
|
-
return getActiveElement(activeElement.shadowRoot);
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
return activeElement;
|
|
482
|
-
};
|
|
483
|
-
|
|
484
487
|
const tryFocus = function (node) {
|
|
485
488
|
if (node === false) {
|
|
486
489
|
return;
|
|
@@ -850,12 +853,13 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
850
853
|
/**
|
|
851
854
|
* Adds listeners to the document necessary for trapping focus and attempts to set focus
|
|
852
855
|
* to the configured initial focus node. Does nothing if the trap isn't active.
|
|
853
|
-
* @returns {Promise<void>}
|
|
854
|
-
*
|
|
856
|
+
* @returns {Promise<void> | undefined} A promise resolved once the initial focus node has
|
|
857
|
+
* been focused when `delayInitialFocus=true`; `undefined` when focus is set synchronously
|
|
858
|
+
* or the trap isn't active.
|
|
855
859
|
*/
|
|
856
860
|
const addListeners = function () {
|
|
857
861
|
if (!state.active) {
|
|
858
|
-
return
|
|
862
|
+
return;
|
|
859
863
|
}
|
|
860
864
|
|
|
861
865
|
// There can be only one listening focus trap at a time
|
|
@@ -863,7 +867,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
863
867
|
|
|
864
868
|
// Delay ensures that the focused element doesn't capture the event
|
|
865
869
|
// that caused the focus trap activation.
|
|
866
|
-
/** @type {Promise<void>} */
|
|
870
|
+
/** @type {Promise<void> | undefined} */
|
|
867
871
|
let promise;
|
|
868
872
|
if (config.delayInitialFocus) {
|
|
869
873
|
// NOTE: Promise constructor callback is called synchronously, which is what we want
|
|
@@ -875,7 +879,6 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
875
879
|
});
|
|
876
880
|
});
|
|
877
881
|
} else {
|
|
878
|
-
promise = Promise.resolve();
|
|
879
882
|
tryFocus(getInitialFocusNode());
|
|
880
883
|
}
|
|
881
884
|
|
|
@@ -979,17 +982,33 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
979
982
|
//
|
|
980
983
|
|
|
981
984
|
const checkDomRemoval = function (mutations) {
|
|
985
|
+
const focusedNode = state.mostRecentlyFocusedNode;
|
|
986
|
+
if (!focusedNode) {
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
982
989
|
const isFocusedNodeRemoved = mutations.some(function (mutation) {
|
|
983
990
|
const removedNodes = Array.from(mutation.removedNodes);
|
|
984
991
|
return removedNodes.some(function (node) {
|
|
985
|
-
return
|
|
992
|
+
return (
|
|
993
|
+
node === focusedNode ||
|
|
994
|
+
(typeof node.contains === 'function' && node.contains(focusedNode))
|
|
995
|
+
);
|
|
986
996
|
});
|
|
987
997
|
});
|
|
988
998
|
|
|
989
|
-
// If the currently focused is removed then browsers will move focus to the
|
|
999
|
+
// If the currently focused node is removed then browsers will move focus to the
|
|
990
1000
|
// <body> element. If this happens, try to move focus back into the trap.
|
|
991
|
-
if (
|
|
992
|
-
|
|
1001
|
+
if (
|
|
1002
|
+
isFocusedNodeRemoved &&
|
|
1003
|
+
state.containers.some(function (container) {
|
|
1004
|
+
return container?.isConnected;
|
|
1005
|
+
})
|
|
1006
|
+
) {
|
|
1007
|
+
// Refresh tabbable state before resolving initial focus because
|
|
1008
|
+
// getInitialFocusNode() may fall back to the first tabbable node in the trap.
|
|
1009
|
+
updateTabbableNodes();
|
|
1010
|
+
const initialFocusNode = getInitialFocusNode();
|
|
1011
|
+
tryFocus(initialFocusNode);
|
|
993
1012
|
}
|
|
994
1013
|
};
|
|
995
1014
|
|
|
@@ -1061,21 +1080,28 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
1061
1080
|
|
|
1062
1081
|
onActivate?.({ trap });
|
|
1063
1082
|
|
|
1064
|
-
const finishActivation =
|
|
1083
|
+
const finishActivation = () => {
|
|
1065
1084
|
if (checkCanFocusTrap) {
|
|
1066
1085
|
updateTabbableNodes();
|
|
1067
1086
|
}
|
|
1068
1087
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
await addListeners();
|
|
1088
|
+
const afterListeners = () => {
|
|
1089
|
+
trap._setSubtreeIsolation(true);
|
|
1090
|
+
updateObservedNodes();
|
|
1091
|
+
onPostActivate?.({ trap });
|
|
1092
|
+
};
|
|
1075
1093
|
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1094
|
+
// NOTE: wait for initial focus node to get focused (whether activation is fully,
|
|
1095
|
+
// partially, or not asynchronous) before we potentially isolate the subtrees
|
|
1096
|
+
// with aria-hidden while focus is still in some other subtree and not yet in
|
|
1097
|
+
// the trap, resulting in some browsers (e.g. Chrome) logging to the console that
|
|
1098
|
+
// they, "Blocked aria-hidden on an element because its descendant retained focus..."
|
|
1099
|
+
const listenersPromise = addListeners();
|
|
1100
|
+
if (listenersPromise) {
|
|
1101
|
+
listenersPromise.then(afterListeners);
|
|
1102
|
+
} else {
|
|
1103
|
+
afterListeners();
|
|
1104
|
+
}
|
|
1079
1105
|
};
|
|
1080
1106
|
|
|
1081
1107
|
if (checkCanFocusTrap) {
|
|
@@ -1137,6 +1163,7 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
1137
1163
|
const onDeactivate = getOption(options, 'onDeactivate');
|
|
1138
1164
|
const onPostDeactivate = getOption(options, 'onPostDeactivate');
|
|
1139
1165
|
const checkCanReturnFocus = getOption(options, 'checkCanReturnFocus');
|
|
1166
|
+
const delayReturnFocus = getOption(options, 'delayReturnFocus');
|
|
1140
1167
|
const returnFocus = getOption(
|
|
1141
1168
|
options,
|
|
1142
1169
|
'returnFocus',
|
|
@@ -1144,14 +1171,19 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
1144
1171
|
);
|
|
1145
1172
|
|
|
1146
1173
|
onDeactivate?.({ trap });
|
|
1174
|
+
const completeDeactivation = () => {
|
|
1175
|
+
if (returnFocus) {
|
|
1176
|
+
tryFocus(getReturnFocusNode(state.nodeFocusedBeforeActivation));
|
|
1177
|
+
}
|
|
1178
|
+
onPostDeactivate?.({ trap });
|
|
1179
|
+
};
|
|
1147
1180
|
|
|
1148
1181
|
const finishDeactivation = () => {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
});
|
|
1182
|
+
if (delayReturnFocus && returnFocus) {
|
|
1183
|
+
delay(completeDeactivation);
|
|
1184
|
+
} else {
|
|
1185
|
+
completeDeactivation();
|
|
1186
|
+
}
|
|
1155
1187
|
};
|
|
1156
1188
|
|
|
1157
1189
|
if (returnFocus && checkCanReturnFocus) {
|
|
@@ -1244,19 +1276,26 @@ const createFocusTrap = function (elements, userOptions) {
|
|
|
1244
1276
|
|
|
1245
1277
|
onUnpause?.({ trap });
|
|
1246
1278
|
|
|
1247
|
-
const finishUnpause =
|
|
1279
|
+
const finishUnpause = () => {
|
|
1248
1280
|
updateTabbableNodes();
|
|
1249
1281
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1282
|
+
const afterListeners = () => {
|
|
1283
|
+
trap._setSubtreeIsolation(true);
|
|
1284
|
+
updateObservedNodes();
|
|
1285
|
+
onPostUnpause?.({ trap });
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// NOTE: wait for initial focus node to get focused (whether activation is fully,
|
|
1289
|
+
// partially, or not asynchronous) before we potentially isolate the subtrees
|
|
1290
|
+
// with aria-hidden while focus is still in some other subtree and not yet in
|
|
1291
|
+
// the trap, resulting in some browsers (e.g. Chrome) logging to the console that
|
|
1292
|
+
// they, "Blocked aria-hidden on an element because its descendant retained focus..."
|
|
1293
|
+
const listenersPromise = addListeners();
|
|
1294
|
+
if (listenersPromise) {
|
|
1295
|
+
listenersPromise.then(afterListeners);
|
|
1296
|
+
} else {
|
|
1297
|
+
afterListeners();
|
|
1298
|
+
}
|
|
1260
1299
|
};
|
|
1261
1300
|
|
|
1262
1301
|
finishUnpause();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "focus-trap",
|
|
3
|
-
"version": "8.1
|
|
3
|
+
"version": "8.2.1",
|
|
4
4
|
"description": "Trap focus within a DOM node.",
|
|
5
5
|
"main": "dist/focus-trap.js",
|
|
6
6
|
"module": "dist/focus-trap.esm.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"@babel/core": "^7.29.0",
|
|
73
73
|
"@babel/eslint-parser": "^7.28.6",
|
|
74
74
|
"@babel/eslint-plugin": "^7.27.1",
|
|
75
|
-
"@babel/preset-env": "^7.29.
|
|
75
|
+
"@babel/preset-env": "^7.29.5",
|
|
76
76
|
"@changesets/cli": "^2.31.0",
|
|
77
77
|
"@eslint/js": "^9.39.2",
|
|
78
78
|
"@rollup/plugin-babel": "^7.0.0",
|
|
@@ -80,40 +80,40 @@
|
|
|
80
80
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
81
81
|
"@rollup/plugin-replace": "^6.0.3",
|
|
82
82
|
"@rollup/plugin-terser": "^1.0.0",
|
|
83
|
-
"@testing-library/cypress": "^10.1.
|
|
83
|
+
"@testing-library/cypress": "^10.1.3",
|
|
84
84
|
"@testing-library/dom": "^10.4.1",
|
|
85
85
|
"@testing-library/jest-dom": "^6.9.1",
|
|
86
86
|
"@testing-library/user-event": "^14.6.1",
|
|
87
87
|
"@types/jest": "^30.0.0",
|
|
88
88
|
"@types/jquery": "^4.0.0",
|
|
89
|
-
"@types/node": "^25.
|
|
90
|
-
"@typescript-eslint/eslint-plugin": "^8.59.
|
|
91
|
-
"@typescript-eslint/parser": "^8.
|
|
89
|
+
"@types/node": "^25.7.0",
|
|
90
|
+
"@typescript-eslint/eslint-plugin": "^8.59.2",
|
|
91
|
+
"@typescript-eslint/parser": "^8.59.3",
|
|
92
92
|
"all-contributors-cli": "^6.26.1",
|
|
93
|
-
"babel-jest": "^30.
|
|
93
|
+
"babel-jest": "^30.4.1",
|
|
94
94
|
"babel-loader": "^10.1.1",
|
|
95
95
|
"cross-env": "^10.1.0",
|
|
96
|
-
"cypress": "^15.
|
|
96
|
+
"cypress": "^15.15.0",
|
|
97
97
|
"cypress-plugin-tab": "^1.0.5",
|
|
98
98
|
"eslint": "^9.39.2",
|
|
99
99
|
"eslint-config-prettier": "^10.1.8",
|
|
100
100
|
"eslint-import-resolver-node": "^0.3.10",
|
|
101
101
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
102
|
-
"eslint-plugin-cypress": "^6.
|
|
102
|
+
"eslint-plugin-cypress": "^6.4.1",
|
|
103
103
|
"eslint-plugin-import": "^2.32.0",
|
|
104
104
|
"eslint-plugin-jest": "^29.15.2",
|
|
105
105
|
"eslint-plugin-jest-dom": "^5.5.0",
|
|
106
106
|
"eslint-plugin-testing-library": "^7.16.2",
|
|
107
|
-
"globals": "^17.
|
|
108
|
-
"jest": "^30.
|
|
109
|
-
"jest-environment-jsdom": "^30.
|
|
107
|
+
"globals": "^17.6.0",
|
|
108
|
+
"jest": "^30.4.2",
|
|
109
|
+
"jest-environment-jsdom": "^30.4.1",
|
|
110
110
|
"jest-watch-typeahead": "^3.0.1",
|
|
111
111
|
"onchange": "^7.1.0",
|
|
112
112
|
"prettier": "^3.8.3",
|
|
113
|
-
"rollup": "^4.60.
|
|
113
|
+
"rollup": "^4.60.3",
|
|
114
114
|
"rollup-plugin-livereload": "^2.0.5",
|
|
115
115
|
"rollup-plugin-serve": "^3.0.0",
|
|
116
|
-
"start-server-and-test": "^3.0.
|
|
116
|
+
"start-server-and-test": "^3.0.4",
|
|
117
117
|
"typescript": "^6.0.3"
|
|
118
118
|
}
|
|
119
119
|
}
|