@react-aria/focus 3.0.0-nightly.2519 → 3.0.0-nightly.2531

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