focus-trap 6.7.2 → 6.7.3

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/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 = [];
@@ -117,7 +117,12 @@ const createFocusTrap = function (elements, userOptions) {
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<{ container: HTMLElement, firstTabbableNode: HTMLElement|null, lastTabbableNode: HTMLElement|null }>}
120
+ // @type {Array<{
121
+ // container: HTMLElement,
122
+ // firstTabbableNode: HTMLElement|null,
123
+ // lastTabbableNode: HTMLElement|null,
124
+ // nextTabbableNode: (node: HTMLElement, forward: boolean) => HTMLElement|undefined
125
+ // }>}
121
126
  tabbableGroups: [],
122
127
 
123
128
  nodeFocusedBeforeActivation: null,
@@ -227,11 +232,46 @@ const createFocusTrap = function (elements, userOptions) {
227
232
  .map((container) => {
228
233
  const tabbableNodes = tabbable(container);
229
234
 
235
+ // NOTE: if we have tabbable nodes, we must have focusable nodes; focusable nodes
236
+ // are a superset of tabbable nodes
237
+ const focusableNodes = focusable(container);
238
+
230
239
  if (tabbableNodes.length > 0) {
231
240
  return {
232
241
  container,
233
242
  firstTabbableNode: tabbableNodes[0],
234
243
  lastTabbableNode: tabbableNodes[tabbableNodes.length - 1],
244
+
245
+ /**
246
+ * Finds the __tabbable__ node that follows the given node in the specified direction,
247
+ * in this container, if any.
248
+ * @param {HTMLElement} node
249
+ * @param {boolean} [forward] True if going in forward tab order; false if going
250
+ * in reverse.
251
+ * @returns {HTMLElement|undefined} The next tabbable node, if any.
252
+ */
253
+ nextTabbableNode(node, forward = true) {
254
+ // NOTE: If tabindex is positive (in order to manipulate the tab order separate
255
+ // from the DOM order), this __will not work__ because the list of focusableNodes,
256
+ // while it contains tabbable nodes, does not sort its nodes in any order other
257
+ // than DOM order, because it can't: Where would you place focusable (but not
258
+ // tabbable) nodes in that order? They have no order, because they aren't tabbale...
259
+ // Support for positive tabindex is already broken and hard to manage (possibly
260
+ // not supportable, TBD), so this isn't going to make things worse than they
261
+ // already are, and at least makes things better for the majority of cases where
262
+ // tabindex is either 0/unset or negative.
263
+ // FYI, positive tabindex issue: https://github.com/focus-trap/focus-trap/issues/375
264
+ const nodeIdx = focusableNodes.findIndex((n) => n === node);
265
+ if (forward) {
266
+ return focusableNodes
267
+ .slice(nodeIdx + 1)
268
+ .find((n) => isTabbable(n));
269
+ }
270
+ return focusableNodes
271
+ .slice(0, nodeIdx)
272
+ .reverse()
273
+ .find((n) => isTabbable(n));
274
+ },
235
275
  };
236
276
  }
237
277
 
@@ -352,6 +392,8 @@ const createFocusTrap = function (elements, userOptions) {
352
392
  const containerIndex = findIndex(state.tabbableGroups, ({ container }) =>
353
393
  container.contains(target)
354
394
  );
395
+ const containerGroup =
396
+ containerIndex >= 0 ? state.tabbableGroups[containerIndex] : undefined;
355
397
 
356
398
  if (containerIndex < 0) {
357
399
  // target not found in any group: quite possible focus has escaped the trap,
@@ -376,12 +418,15 @@ const createFocusTrap = function (elements, userOptions) {
376
418
 
377
419
  if (
378
420
  startOfGroupIndex < 0 &&
379
- (state.tabbableGroups[containerIndex].container === target ||
380
- (isFocusable(target) && !isTabbable(target)))
421
+ (containerGroup.container === target ||
422
+ (isFocusable(target) &&
423
+ !isTabbable(target) &&
424
+ !containerGroup.nextTabbableNode(target, false)))
381
425
  ) {
382
426
  // an exception case where the target is either the container itself, or
383
427
  // 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
428
+ // and user clicked on it or node was programmatically given focus)
429
+ // and is not followed by any other tabbable node, in which
385
430
  // case, we should handle shift+tab as if focus were on the container's
386
431
  // first tabbable node, and go to the last tabbable node of the LAST group
387
432
  startOfGroupIndex = containerIndex;
@@ -410,12 +455,15 @@ const createFocusTrap = function (elements, userOptions) {
410
455
 
411
456
  if (
412
457
  lastOfGroupIndex < 0 &&
413
- (state.tabbableGroups[containerIndex].container === target ||
414
- (isFocusable(target) && !isTabbable(target)))
458
+ (containerGroup.container === target ||
459
+ (isFocusable(target) &&
460
+ !isTabbable(target) &&
461
+ !containerGroup.nextTabbableNode(target)))
415
462
  ) {
416
463
  // an exception case where the target is the container itself, or
417
464
  // 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
465
+ // and user clicked on it or node was programmatically given focus)
466
+ // and is not followed by any other tabbable node, in which
419
467
  // case, we should handle tab as if focus were on the container's
420
468
  // last tabbable node, and go to the first tabbable node of the FIRST group
421
469
  lastOfGroupIndex = containerIndex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "focus-trap",
3
- "version": "6.7.2",
3
+ "version": "6.7.3",
4
4
  "description": "Trap focus within a DOM node.",
5
5
  "main": "dist/focus-trap.js",
6
6
  "module": "dist/focus-trap.esm.js",
@@ -65,11 +65,11 @@
65
65
  "tabbable": "^5.2.1"
66
66
  },
67
67
  "devDependencies": {
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",
68
+ "@babel/cli": "^7.17.0",
69
+ "@babel/core": "^7.17.2",
70
+ "@babel/eslint-parser": "^7.17.0",
71
+ "@babel/preset-env": "^7.16.11",
72
+ "@changesets/cli": "^2.20.0",
73
73
  "@rollup/plugin-babel": "^5.3.0",
74
74
  "@rollup/plugin-commonjs": "^21.0.1",
75
75
  "@rollup/plugin-node-resolve": "^13.1.3",
@@ -78,20 +78,20 @@
78
78
  "all-contributors-cli": "^6.20.0",
79
79
  "babel-loader": "^8.2.3",
80
80
  "cross-env": "^7.0.3",
81
- "cypress": "^9.2.1",
81
+ "cypress": "^9.4.1",
82
82
  "cypress-plugin-tab": "^1.0.5",
83
- "eslint": "^8.6.0",
83
+ "eslint": "^8.8.0",
84
84
  "eslint-config-prettier": "^8.3.0",
85
85
  "eslint-plugin-cypress": "^2.12.1",
86
86
  "onchange": "^7.1.0",
87
87
  "prettier": "^2.5.1",
88
- "rollup": "^2.63.0",
88
+ "rollup": "^2.67.1",
89
89
  "rollup-plugin-inject-process-env": "^1.3.1",
90
90
  "rollup-plugin-livereload": "^2.0.5",
91
91
  "rollup-plugin-serve": "^1.1.0",
92
92
  "rollup-plugin-sourcemaps": "^0.6.3",
93
93
  "rollup-plugin-terser": "^7.0.1",
94
94
  "start-server-and-test": "^1.14.0",
95
- "typescript": "^4.5.4"
95
+ "typescript": "^4.5.5"
96
96
  }
97
97
  }