@react-aria/focus 3.14.3 → 3.15.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.
@@ -50,22 +50,22 @@ export interface FocusManagerOptions {
50
50
 
51
51
  export interface FocusManager {
52
52
  /** Moves focus to the next focusable or tabbable element in the focus scope. */
53
- focusNext(opts?: FocusManagerOptions): FocusableElement,
53
+ focusNext(opts?: FocusManagerOptions): FocusableElement | null,
54
54
  /** Moves focus to the previous focusable or tabbable element in the focus scope. */
55
- focusPrevious(opts?: FocusManagerOptions): FocusableElement,
55
+ focusPrevious(opts?: FocusManagerOptions): FocusableElement | null,
56
56
  /** Moves focus to the first focusable or tabbable element in the focus scope. */
57
- focusFirst(opts?: FocusManagerOptions): FocusableElement,
57
+ focusFirst(opts?: FocusManagerOptions): FocusableElement | null,
58
58
  /** Moves focus to the last focusable or tabbable element in the focus scope. */
59
- focusLast(opts?: FocusManagerOptions): FocusableElement
59
+ focusLast(opts?: FocusManagerOptions): FocusableElement | null
60
60
  }
61
61
 
62
- type ScopeRef = RefObject<Element[]>;
62
+ type ScopeRef = RefObject<Element[]> | null;
63
63
  interface IFocusContext {
64
64
  focusManager: FocusManager,
65
65
  parentNode: TreeNode | null
66
66
  }
67
67
 
68
- const FocusContext = React.createContext<IFocusContext>(null);
68
+ const FocusContext = React.createContext<IFocusContext | null>(null);
69
69
 
70
70
  let activeScope: ScopeRef = null;
71
71
 
@@ -81,8 +81,8 @@ let activeScope: ScopeRef = null;
81
81
  */
82
82
  export function FocusScope(props: FocusScopeProps) {
83
83
  let {children, contain, restoreFocus, autoFocus} = props;
84
- let startRef = useRef<HTMLSpanElement>();
85
- let endRef = useRef<HTMLSpanElement>();
84
+ let startRef = useRef<HTMLSpanElement>(null);
85
+ let endRef = useRef<HTMLSpanElement>(null);
86
86
  let scopeRef = useRef<Element[]>([]);
87
87
  let {parentNode} = useContext(FocusContext) || {};
88
88
 
@@ -109,16 +109,18 @@ export function FocusScope(props: FocusScopeProps) {
109
109
 
110
110
  useLayoutEffect(() => {
111
111
  let node = focusScopeTree.getTreeNode(scopeRef);
112
- node.contain = contain;
112
+ if (node) {
113
+ node.contain = !!contain;
114
+ }
113
115
  }, [contain]);
114
116
 
115
117
  useLayoutEffect(() => {
116
118
  // Find all rendered nodes between the sentinels and add them to the scope.
117
- let node = startRef.current.nextSibling;
118
- let nodes = [];
119
+ let node = startRef.current?.nextSibling!;
120
+ let nodes: Element[] = [];
119
121
  while (node && node !== endRef.current) {
120
- nodes.push(node);
121
- node = node.nextSibling;
122
+ nodes.push(node as Element);
123
+ node = node.nextSibling as Element;
122
124
  }
123
125
 
124
126
  scopeRef.current = nodes;
@@ -129,43 +131,42 @@ export function FocusScope(props: FocusScopeProps) {
129
131
  useRestoreFocus(scopeRef, restoreFocus, contain);
130
132
  useAutoFocus(scopeRef, autoFocus);
131
133
 
132
- // this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
134
+ // This needs to be an effect so that activeScope is updated after the FocusScope tree is complete.
135
+ // It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet.
133
136
  useEffect(() => {
134
- if (scopeRef) {
135
- let activeElement = document.activeElement;
136
- let scope = null;
137
- // In strict mode, active scope is incorrectly updated since cleanup will run even though scope hasn't unmounted.
138
- // To fix this, we need to update the actual activeScope here
139
- if (isElementInScope(activeElement, scopeRef.current)) {
140
- // Since useLayoutEffect runs for children first, we need to traverse the focusScope tree and find the bottom most scope that
141
- // contains the active element and set that as the activeScope
142
- for (let node of focusScopeTree.traverse()) {
143
- if (isElementInScope(activeElement, node.scopeRef.current)) {
144
- scope = node;
145
- }
137
+ let activeElement = document.activeElement;
138
+ let scope: TreeNode | null = null;
139
+
140
+ if (isElementInScope(activeElement, scopeRef.current)) {
141
+ // We need to traverse the focusScope tree and find the bottom most scope that
142
+ // contains the active element and set that as the activeScope.
143
+ for (let node of focusScopeTree.traverse()) {
144
+ if (node.scopeRef && isElementInScope(activeElement, node.scopeRef.current)) {
145
+ scope = node;
146
146
  }
147
+ }
147
148
 
148
- if (scope === focusScopeTree.getTreeNode(scopeRef)) {
149
- activeScope = scope.scopeRef;
150
- }
149
+ if (scope === focusScopeTree.getTreeNode(scopeRef)) {
150
+ activeScope = scope.scopeRef;
151
151
  }
152
+ }
153
+ }, [scopeRef]);
152
154
 
153
- return () => {
154
- // Scope may have been re-parented.
155
- let parentScope = focusScopeTree.getTreeNode(scopeRef).parent.scopeRef;
155
+ // This layout effect cleanup is so that the tree node is removed synchronously with react before the RAF
156
+ // in useRestoreFocus cleanup runs.
157
+ useLayoutEffect(() => {
158
+ return () => {
159
+ // Scope may have been re-parented.
160
+ let parentScope = focusScopeTree.getTreeNode(scopeRef)?.parent?.scopeRef ?? null;
156
161
 
157
- // Restore the active scope on unmount if this scope or a descendant scope is active.
158
- // Parent effect cleanups run before children, so we need to check if the
159
- // parent scope actually still exists before restoring the active scope to it.
160
- if (
161
- (scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)) &&
162
- (!parentScope || focusScopeTree.getTreeNode(parentScope))
163
- ) {
164
- activeScope = parentScope;
165
- }
166
- focusScopeTree.removeTreeNode(scopeRef);
167
- };
168
- }
162
+ if (
163
+ (scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)) &&
164
+ (!parentScope || focusScopeTree.getTreeNode(parentScope))
165
+ ) {
166
+ activeScope = parentScope;
167
+ }
168
+ focusScopeTree.removeTreeNode(scopeRef);
169
+ };
169
170
  }, [scopeRef]);
170
171
 
171
172
  let focusManager = useMemo(() => createFocusManagerForScope(scopeRef), []);
@@ -188,18 +189,19 @@ export function FocusScope(props: FocusScopeProps) {
188
189
  * A FocusManager can be used to programmatically move focus within
189
190
  * a FocusScope, e.g. in response to user events like keyboard navigation.
190
191
  */
191
- export function useFocusManager(): FocusManager {
192
+ export function useFocusManager(): FocusManager | undefined {
192
193
  return useContext(FocusContext)?.focusManager;
193
194
  }
194
195
 
195
196
  function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): FocusManager {
196
197
  return {
197
198
  focusNext(opts: FocusManagerOptions = {}) {
198
- let scope = scopeRef.current;
199
+ let scope = scopeRef.current!;
199
200
  let {from, tabbable, wrap, accept} = opts;
200
- let node = from || document.activeElement;
201
- let sentinel = scope[0].previousElementSibling;
202
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
201
+ let node = from || document.activeElement!;
202
+ let sentinel = scope[0].previousElementSibling!;
203
+ let scopeRoot = getScopeRoot(scope);
204
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
203
205
  walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
204
206
  let nextNode = walker.nextNode() as FocusableElement;
205
207
  if (!nextNode && wrap) {
@@ -212,11 +214,12 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
212
214
  return nextNode;
213
215
  },
214
216
  focusPrevious(opts: FocusManagerOptions = {}) {
215
- let scope = scopeRef.current;
217
+ let scope = scopeRef.current!;
216
218
  let {from, tabbable, wrap, accept} = opts;
217
- let node = from || document.activeElement;
218
- let sentinel = scope[scope.length - 1].nextElementSibling;
219
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
219
+ let node = from || document.activeElement!;
220
+ let sentinel = scope[scope.length - 1].nextElementSibling!;
221
+ let scopeRoot = getScopeRoot(scope);
222
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
220
223
  walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
221
224
  let previousNode = walker.previousNode() as FocusableElement;
222
225
  if (!previousNode && wrap) {
@@ -229,10 +232,11 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
229
232
  return previousNode;
230
233
  },
231
234
  focusFirst(opts = {}) {
232
- let scope = scopeRef.current;
235
+ let scope = scopeRef.current!;
233
236
  let {tabbable, accept} = opts;
234
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
235
- walker.currentNode = scope[0].previousElementSibling;
237
+ let scopeRoot = getScopeRoot(scope);
238
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
239
+ walker.currentNode = scope[0].previousElementSibling!;
236
240
  let nextNode = walker.nextNode() as FocusableElement;
237
241
  if (nextNode) {
238
242
  focusElement(nextNode, true);
@@ -240,10 +244,11 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
240
244
  return nextNode;
241
245
  },
242
246
  focusLast(opts = {}) {
243
- let scope = scopeRef.current;
247
+ let scope = scopeRef.current!;
244
248
  let {tabbable, accept} = opts;
245
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
246
- walker.currentNode = scope[scope.length - 1].nextElementSibling;
249
+ let scopeRoot = getScopeRoot(scope);
250
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
251
+ walker.currentNode = scope[scope.length - 1].nextElementSibling!;
247
252
  let previousNode = walker.previousNode() as FocusableElement;
248
253
  if (previousNode) {
249
254
  focusElement(previousNode, true);
@@ -275,7 +280,7 @@ focusableElements.push('[tabindex]:not([tabindex="-1"]):not([disabled])');
275
280
  const TABBABLE_ELEMENT_SELECTOR = focusableElements.join(':not([hidden]):not([tabindex="-1"]),');
276
281
 
277
282
  function getScopeRoot(scope: Element[]) {
278
- return scope[0].parentElement;
283
+ return scope[0].parentElement!;
279
284
  }
280
285
 
281
286
  function shouldContainFocus(scopeRef: ScopeRef) {
@@ -291,17 +296,17 @@ function shouldContainFocus(scopeRef: ScopeRef) {
291
296
  return true;
292
297
  }
293
298
 
294
- function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
299
+ function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean) {
295
300
  let focusedNode = useRef<FocusableElement>();
296
301
 
297
- let raf = useRef(null);
302
+ let raf = useRef<ReturnType<typeof requestAnimationFrame>>();
298
303
  useLayoutEffect(() => {
299
304
  let scope = scopeRef.current;
300
305
  if (!contain) {
301
306
  // if contain was changed, then we should cancel any ongoing waits to pull focus back into containment
302
307
  if (raf.current) {
303
308
  cancelAnimationFrame(raf.current);
304
- raf.current = null;
309
+ raf.current = undefined;
305
310
  }
306
311
  return;
307
312
  }
@@ -314,16 +319,20 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
314
319
 
315
320
  let focusedElement = document.activeElement;
316
321
  let scope = scopeRef.current;
317
- if (!isElementInScope(focusedElement, scope)) {
322
+ if (!scope || !isElementInScope(focusedElement, scope)) {
318
323
  return;
319
324
  }
320
325
 
321
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable: true}, scope);
326
+ let scopeRoot = getScopeRoot(scope);
327
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable: true}, scope);
328
+ if (!focusedElement) {
329
+ return;
330
+ }
322
331
  walker.currentNode = focusedElement;
323
332
  let nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
324
333
  if (!nextElement) {
325
- walker.currentNode = e.shiftKey ? scope[scope.length - 1].nextElementSibling : scope[0].previousElementSibling;
326
- nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
334
+ walker.currentNode = e.shiftKey ? scope[scope.length - 1].nextElementSibling! : scope[0].previousElementSibling!;
335
+ nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
327
336
  }
328
337
 
329
338
  e.preventDefault();
@@ -343,7 +352,7 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
343
352
  // restore focus to the previously focused node or the first tabbable element in the active scope.
344
353
  if (focusedNode.current) {
345
354
  focusedNode.current.focus();
346
- } else if (activeScope) {
355
+ } else if (activeScope && activeScope.current) {
347
356
  focusFirstInScope(activeScope.current);
348
357
  }
349
358
  } else if (shouldContainFocus(scopeRef)) {
@@ -358,12 +367,12 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
358
367
  }
359
368
  raf.current = requestAnimationFrame(() => {
360
369
  // Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe
361
- if (shouldContainFocus(scopeRef) && !isElementInChildScope(document.activeElement, scopeRef)) {
370
+ if (document.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(document.activeElement, scopeRef)) {
362
371
  activeScope = scopeRef;
363
372
  if (document.body.contains(e.target)) {
364
373
  focusedNode.current = e.target;
365
- focusedNode.current.focus();
366
- } else if (activeScope) {
374
+ focusedNode.current?.focus();
375
+ } else if (activeScope.current) {
367
376
  focusFirstInScope(activeScope.current);
368
377
  }
369
378
  }
@@ -372,13 +381,13 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
372
381
 
373
382
  document.addEventListener('keydown', onKeyDown, false);
374
383
  document.addEventListener('focusin', onFocus, false);
375
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
376
- scope.forEach(element => element.addEventListener('focusout', onBlur, false));
384
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
385
+ scope?.forEach(element => element.addEventListener('focusout', onBlur, false));
377
386
  return () => {
378
387
  document.removeEventListener('keydown', onKeyDown, false);
379
388
  document.removeEventListener('focusin', onFocus, false);
380
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
381
- scope.forEach(element => element.removeEventListener('focusout', onBlur, false));
389
+ scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
390
+ scope?.forEach(element => element.removeEventListener('focusout', onBlur, false));
382
391
  };
383
392
  }, [scopeRef, contain]);
384
393
 
@@ -397,7 +406,13 @@ function isElementInAnyScope(element: Element) {
397
406
  return isElementInChildScope(element);
398
407
  }
399
408
 
400
- function isElementInScope(element: Element, scope: Element[]) {
409
+ function isElementInScope(element?: Element | null, scope?: Element[] | null) {
410
+ if (!element) {
411
+ return false;
412
+ }
413
+ if (!scope) {
414
+ return false;
415
+ }
401
416
  return scope.some(node => node.contains(element));
402
417
  }
403
418
 
@@ -410,7 +425,7 @@ function isElementInChildScope(element: Element, scope: ScopeRef = null) {
410
425
  // node.contains in isElementInScope covers child scopes that are also DOM children,
411
426
  // but does not cover child scopes in portals.
412
427
  for (let {scopeRef: s} of focusScopeTree.traverse(focusScopeTree.getTreeNode(scope))) {
413
- if (isElementInScope(element, s.current)) {
428
+ if (s && isElementInScope(element, s.current)) {
414
429
  return true;
415
430
  }
416
431
  }
@@ -451,14 +466,16 @@ function focusElement(element: FocusableElement | null, scroll = false) {
451
466
  }
452
467
 
453
468
  function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
454
- let sentinel = scope[0].previousElementSibling;
455
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable}, scope);
469
+ let sentinel = scope[0].previousElementSibling!;
470
+ let scopeRoot = getScopeRoot(scope);
471
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable}, scope);
456
472
  walker.currentNode = sentinel;
457
473
  let nextNode = walker.nextNode();
458
474
 
459
475
  // If the scope does not contain a tabbable element, use the first focusable element.
460
476
  if (tabbable && !nextNode) {
461
- walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable: false}, scope);
477
+ scopeRoot = getScopeRoot(scope);
478
+ walker = getFocusableTreeWalker(scopeRoot, {tabbable: false}, scope);
462
479
  walker.currentNode = sentinel;
463
480
  nextNode = walker.nextNode();
464
481
  }
@@ -466,12 +483,12 @@ function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
466
483
  focusElement(nextNode as FocusableElement);
467
484
  }
468
485
 
469
- function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
486
+ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus?: boolean) {
470
487
  const autoFocusRef = React.useRef(autoFocus);
471
488
  useEffect(() => {
472
489
  if (autoFocusRef.current) {
473
490
  activeScope = scopeRef;
474
- if (!isElementInScope(document.activeElement, activeScope.current)) {
491
+ if (!isElementInScope(document.activeElement, activeScope.current) && scopeRef.current) {
475
492
  focusFirstInScope(scopeRef.current);
476
493
  }
477
494
  }
@@ -479,7 +496,7 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
479
496
  }, [scopeRef]);
480
497
  }
481
498
 
482
- function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean, contain: boolean) {
499
+ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore?: boolean, contain?: boolean) {
483
500
  // tracks the active scope, in case restore and contain are both false.
484
501
  // if either are true, this is tracked in useRestoreFocus or useFocusContainment.
485
502
  useLayoutEffect(() => {
@@ -489,7 +506,7 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean,
489
506
 
490
507
  let scope = scopeRef.current;
491
508
 
492
- let onFocus = (e: FocusEvent) => {
509
+ let onFocus = (e) => {
493
510
  let target = e.target as Element;
494
511
  if (isElementInScope(target, scopeRef.current)) {
495
512
  activeScope = scopeRef;
@@ -499,10 +516,10 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean,
499
516
  };
500
517
 
501
518
  document.addEventListener('focusin', onFocus, false);
502
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
519
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
503
520
  return () => {
504
521
  document.removeEventListener('focusin', onFocus, false);
505
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
522
+ scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
506
523
  };
507
524
  }, [scopeRef, restore, contain]);
508
525
  }
@@ -520,7 +537,7 @@ function shouldRestoreFocus(scopeRef: ScopeRef) {
520
537
  return scope?.scopeRef === scopeRef;
521
538
  }
522
539
 
523
- function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean, contain: boolean) {
540
+ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean, contain?: boolean) {
524
541
  // create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
525
542
  const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? document.activeElement as FocusableElement : null);
526
543
 
@@ -543,10 +560,10 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
543
560
  };
544
561
 
545
562
  document.addEventListener('focusin', onFocus, false);
546
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
563
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
547
564
  return () => {
548
565
  document.removeEventListener('focusin', onFocus, false);
549
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
566
+ scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
550
567
  };
551
568
  // eslint-disable-next-line react-hooks/exhaustive-deps
552
569
  }, [scopeRef, contain]);
@@ -569,7 +586,11 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
569
586
  if (!isElementInScope(focusedElement, scopeRef.current)) {
570
587
  return;
571
588
  }
572
- let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
589
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
590
+ if (!treeNode) {
591
+ return;
592
+ }
593
+ let nodeToRestore = treeNode.nodeToRestore;
573
594
 
574
595
  // Create a DOM tree walker that matches all tabbable elements
575
596
  let walker = getFocusableTreeWalker(document.body, {tabbable: true});
@@ -578,9 +599,9 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
578
599
  walker.currentNode = focusedElement;
579
600
  let nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
580
601
 
581
- if (!document.body.contains(nodeToRestore) || nodeToRestore === document.body) {
582
- nodeToRestore = null;
583
- focusScopeTree.getTreeNode(scopeRef).nodeToRestore = null;
602
+ if (!nodeToRestore || !document.body.contains(nodeToRestore) || nodeToRestore === document.body) {
603
+ nodeToRestore = undefined;
604
+ treeNode.nodeToRestore = undefined;
584
605
  }
585
606
 
586
607
  // If there is no next element, or it is outside the current scope, move focus to the
@@ -627,10 +648,18 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
627
648
  return;
628
649
  }
629
650
 
630
- focusScopeTree.getTreeNode(scopeRef).nodeToRestore = nodeToRestoreRef.current;
651
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
652
+ if (!treeNode) {
653
+ return;
654
+ }
655
+ treeNode.nodeToRestore = nodeToRestoreRef.current ?? undefined;
631
656
 
632
657
  return () => {
633
- let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
658
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
659
+ if (!treeNode) {
660
+ return;
661
+ }
662
+ let nodeToRestore = treeNode.nodeToRestore;
634
663
 
635
664
  // if we already lost focus to the body and this was the active scope, then we should attempt to restore
636
665
  if (
@@ -661,7 +690,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
661
690
  // ancestor scope that is still in the tree.
662
691
  treeNode = clonedTree.getTreeNode(scopeRef);
663
692
  while (treeNode) {
664
- if (treeNode.scopeRef && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
693
+ if (treeNode.scopeRef && treeNode.scopeRef.current && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
665
694
  focusFirstInScope(treeNode.scopeRef.current, true);
666
695
  return;
667
696
  }
@@ -718,13 +747,13 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
718
747
  focusNext(opts: FocusManagerOptions = {}) {
719
748
  let root = ref.current;
720
749
  if (!root) {
721
- return;
750
+ return null;
722
751
  }
723
752
  let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
724
753
  let node = from || document.activeElement;
725
754
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
726
755
  if (root.contains(node)) {
727
- walker.currentNode = node;
756
+ walker.currentNode = node!;
728
757
  }
729
758
  let nextNode = walker.nextNode() as FocusableElement;
730
759
  if (!nextNode && wrap) {
@@ -739,34 +768,39 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
739
768
  focusPrevious(opts: FocusManagerOptions = defaultOptions) {
740
769
  let root = ref.current;
741
770
  if (!root) {
742
- return;
771
+ return null;
743
772
  }
744
773
  let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
745
774
  let node = from || document.activeElement;
746
775
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
747
776
  if (root.contains(node)) {
748
- walker.currentNode = node;
777
+ walker.currentNode = node!;
749
778
  } else {
750
779
  let next = last(walker);
751
780
  if (next) {
752
781
  focusElement(next, true);
753
782
  }
754
- return next;
783
+ return next ?? null;
755
784
  }
756
785
  let previousNode = walker.previousNode() as FocusableElement;
757
786
  if (!previousNode && wrap) {
758
787
  walker.currentNode = root;
759
- previousNode = last(walker);
788
+ let lastNode = last(walker);
789
+ if (!lastNode) {
790
+ // couldn't wrap
791
+ return null;
792
+ }
793
+ previousNode = lastNode;
760
794
  }
761
795
  if (previousNode) {
762
796
  focusElement(previousNode, true);
763
797
  }
764
- return previousNode;
798
+ return previousNode ?? null;
765
799
  },
766
800
  focusFirst(opts = defaultOptions) {
767
801
  let root = ref.current;
768
802
  if (!root) {
769
- return;
803
+ return null;
770
804
  }
771
805
  let {tabbable = defaultOptions.tabbable, accept = defaultOptions.accept} = opts;
772
806
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
@@ -779,7 +813,7 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
779
813
  focusLast(opts = defaultOptions) {
780
814
  let root = ref.current;
781
815
  if (!root) {
782
- return;
816
+ return null;
783
817
  }
784
818
  let {tabbable = defaultOptions.tabbable, accept = defaultOptions.accept} = opts;
785
819
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
@@ -787,13 +821,13 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
787
821
  if (next) {
788
822
  focusElement(next, true);
789
823
  }
790
- return next;
824
+ return next ?? null;
791
825
  }
792
826
  };
793
827
  }
794
828
 
795
829
  function last(walker: TreeWalker) {
796
- let next: FocusableElement;
830
+ let next: FocusableElement | undefined = undefined;
797
831
  let last: FocusableElement;
798
832
  do {
799
833
  last = walker.lastChild() as FocusableElement;
@@ -824,6 +858,9 @@ class Tree {
824
858
 
825
859
  addTreeNode(scopeRef: ScopeRef, parent: ScopeRef, nodeToRestore?: FocusableElement) {
826
860
  let parentNode = this.fastMap.get(parent ?? null);
861
+ if (!parentNode) {
862
+ return;
863
+ }
827
864
  let node = new TreeNode({scopeRef});
828
865
  parentNode.addChild(node);
829
866
  node.parent = parentNode;
@@ -843,6 +880,9 @@ class Tree {
843
880
  return;
844
881
  }
845
882
  let node = this.fastMap.get(scopeRef);
883
+ if (!node) {
884
+ return;
885
+ }
846
886
  let parentNode = node.parent;
847
887
  // when we remove a scope, check if any sibling scopes are trying to restore focus to something inside the scope we're removing
848
888
  // if we are, then replace the siblings restore with the restore from the scope we're removing
@@ -851,6 +891,7 @@ class Tree {
851
891
  current !== node &&
852
892
  node.nodeToRestore &&
853
893
  current.nodeToRestore &&
894
+ node.scopeRef &&
854
895
  node.scopeRef.current &&
855
896
  isElementInScope(current.nodeToRestore, node.scopeRef.current)
856
897
  ) {
@@ -858,9 +899,11 @@ class Tree {
858
899
  }
859
900
  }
860
901
  let children = node.children;
861
- parentNode.removeChild(node);
862
- if (children.size > 0) {
863
- children.forEach(child => parentNode.addChild(child));
902
+ if (parentNode) {
903
+ parentNode.removeChild(node);
904
+ if (children.size > 0) {
905
+ children.forEach(child => parentNode && parentNode.addChild(child));
906
+ }
864
907
  }
865
908
 
866
909
  this.fastMap.delete(node.scopeRef);
@@ -881,7 +924,7 @@ class Tree {
881
924
  clone(): Tree {
882
925
  let newTree = new Tree();
883
926
  for (let node of this.traverse()) {
884
- newTree.addTreeNode(node.scopeRef, node.parent.scopeRef, node.nodeToRestore);
927
+ newTree.addTreeNode(node.scopeRef, node.parent?.scopeRef ?? null, node.nodeToRestore);
885
928
  }
886
929
  return newTree;
887
930
  }
@@ -889,8 +932,8 @@ class Tree {
889
932
 
890
933
  class TreeNode {
891
934
  public scopeRef: ScopeRef;
892
- public nodeToRestore: FocusableElement;
893
- public parent: TreeNode;
935
+ public nodeToRestore?: FocusableElement;
936
+ public parent?: TreeNode;
894
937
  public children: Set<TreeNode> = new Set();
895
938
  public contain = false;
896
939
 
@@ -24,7 +24,7 @@ function isStyleVisible(element: Element) {
24
24
  );
25
25
 
26
26
  if (isVisible) {
27
- const {getComputedStyle} = element.ownerDocument.defaultView;
27
+ const {getComputedStyle} = element.ownerDocument.defaultView as unknown as Window;
28
28
  let {display: computedDisplay, visibility: computedVisibility} = getComputedStyle(element);
29
29
 
30
30
  isVisible = (
@@ -49,11 +49,11 @@ function isAttributeVisible(element: Element, childElement?: Element) {
49
49
  }
50
50
 
51
51
  /**
52
- * Adapted from https://github.com/testing-library/jest-dom and
52
+ * Adapted from https://github.com/testing-library/jest-dom and
53
53
  * https://github.com/vuejs/vue-test-utils-next/.
54
54
  * Licensed under the MIT License.
55
55
  * @param element - Element to evaluate for display or visibility.
56
- */
56
+ */
57
57
  export function isElementVisible(element: Element, childElement?: Element) {
58
58
  return (
59
59
  element.nodeName !== '#comment' &&