@react-aria/focus 3.14.3 → 3.16.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.
@@ -12,10 +12,9 @@
12
12
 
13
13
  import {FocusableElement} from '@react-types/shared';
14
14
  import {focusSafely} from './focusSafely';
15
+ import {getOwnerDocument, useLayoutEffect} from '@react-aria/utils';
15
16
  import {isElementVisible} from './isElementVisible';
16
17
  import React, {ReactNode, RefObject, useContext, useEffect, useMemo, useRef} from 'react';
17
- import {useLayoutEffect} from '@react-aria/utils';
18
-
19
18
 
20
19
  export interface FocusScopeProps {
21
20
  /** The contents of the focus scope. */
@@ -50,22 +49,22 @@ export interface FocusManagerOptions {
50
49
 
51
50
  export interface FocusManager {
52
51
  /** Moves focus to the next focusable or tabbable element in the focus scope. */
53
- focusNext(opts?: FocusManagerOptions): FocusableElement,
52
+ focusNext(opts?: FocusManagerOptions): FocusableElement | null,
54
53
  /** Moves focus to the previous focusable or tabbable element in the focus scope. */
55
- focusPrevious(opts?: FocusManagerOptions): FocusableElement,
54
+ focusPrevious(opts?: FocusManagerOptions): FocusableElement | null,
56
55
  /** Moves focus to the first focusable or tabbable element in the focus scope. */
57
- focusFirst(opts?: FocusManagerOptions): FocusableElement,
56
+ focusFirst(opts?: FocusManagerOptions): FocusableElement | null,
58
57
  /** Moves focus to the last focusable or tabbable element in the focus scope. */
59
- focusLast(opts?: FocusManagerOptions): FocusableElement
58
+ focusLast(opts?: FocusManagerOptions): FocusableElement | null
60
59
  }
61
60
 
62
- type ScopeRef = RefObject<Element[]>;
61
+ type ScopeRef = RefObject<Element[]> | null;
63
62
  interface IFocusContext {
64
63
  focusManager: FocusManager,
65
64
  parentNode: TreeNode | null
66
65
  }
67
66
 
68
- const FocusContext = React.createContext<IFocusContext>(null);
67
+ const FocusContext = React.createContext<IFocusContext | null>(null);
69
68
 
70
69
  let activeScope: ScopeRef = null;
71
70
 
@@ -81,8 +80,8 @@ let activeScope: ScopeRef = null;
81
80
  */
82
81
  export function FocusScope(props: FocusScopeProps) {
83
82
  let {children, contain, restoreFocus, autoFocus} = props;
84
- let startRef = useRef<HTMLSpanElement>();
85
- let endRef = useRef<HTMLSpanElement>();
83
+ let startRef = useRef<HTMLSpanElement>(null);
84
+ let endRef = useRef<HTMLSpanElement>(null);
86
85
  let scopeRef = useRef<Element[]>([]);
87
86
  let {parentNode} = useContext(FocusContext) || {};
88
87
 
@@ -109,16 +108,18 @@ export function FocusScope(props: FocusScopeProps) {
109
108
 
110
109
  useLayoutEffect(() => {
111
110
  let node = focusScopeTree.getTreeNode(scopeRef);
112
- node.contain = contain;
111
+ if (node) {
112
+ node.contain = !!contain;
113
+ }
113
114
  }, [contain]);
114
115
 
115
116
  useLayoutEffect(() => {
116
117
  // Find all rendered nodes between the sentinels and add them to the scope.
117
- let node = startRef.current.nextSibling;
118
- let nodes = [];
118
+ let node = startRef.current?.nextSibling!;
119
+ let nodes: Element[] = [];
119
120
  while (node && node !== endRef.current) {
120
- nodes.push(node);
121
- node = node.nextSibling;
121
+ nodes.push(node as Element);
122
+ node = node.nextSibling as Element;
122
123
  }
123
124
 
124
125
  scopeRef.current = nodes;
@@ -129,43 +130,42 @@ export function FocusScope(props: FocusScopeProps) {
129
130
  useRestoreFocus(scopeRef, restoreFocus, contain);
130
131
  useAutoFocus(scopeRef, autoFocus);
131
132
 
132
- // this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
133
+ // This needs to be an effect so that activeScope is updated after the FocusScope tree is complete.
134
+ // It cannot be a useLayoutEffect because the parent of this node hasn't been attached in the tree yet.
133
135
  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
- }
136
+ const activeElement = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement;
137
+ let scope: TreeNode | null = null;
138
+
139
+ if (isElementInScope(activeElement, scopeRef.current)) {
140
+ // 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 (node.scopeRef && isElementInScope(activeElement, node.scopeRef.current)) {
144
+ scope = node;
146
145
  }
146
+ }
147
147
 
148
- if (scope === focusScopeTree.getTreeNode(scopeRef)) {
149
- activeScope = scope.scopeRef;
150
- }
148
+ if (scope === focusScopeTree.getTreeNode(scopeRef)) {
149
+ activeScope = scope.scopeRef;
151
150
  }
151
+ }
152
+ }, [scopeRef]);
152
153
 
153
- return () => {
154
- // Scope may have been re-parented.
155
- let parentScope = focusScopeTree.getTreeNode(scopeRef).parent.scopeRef;
154
+ // This layout effect cleanup is so that the tree node is removed synchronously with react before the RAF
155
+ // in useRestoreFocus cleanup runs.
156
+ useLayoutEffect(() => {
157
+ return () => {
158
+ // Scope may have been re-parented.
159
+ let parentScope = focusScopeTree.getTreeNode(scopeRef)?.parent?.scopeRef ?? null;
156
160
 
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
- }
161
+ if (
162
+ (scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)) &&
163
+ (!parentScope || focusScopeTree.getTreeNode(parentScope))
164
+ ) {
165
+ activeScope = parentScope;
166
+ }
167
+ focusScopeTree.removeTreeNode(scopeRef);
168
+ };
169
169
  }, [scopeRef]);
170
170
 
171
171
  let focusManager = useMemo(() => createFocusManagerForScope(scopeRef), []);
@@ -188,18 +188,19 @@ export function FocusScope(props: FocusScopeProps) {
188
188
  * A FocusManager can be used to programmatically move focus within
189
189
  * a FocusScope, e.g. in response to user events like keyboard navigation.
190
190
  */
191
- export function useFocusManager(): FocusManager {
191
+ export function useFocusManager(): FocusManager | undefined {
192
192
  return useContext(FocusContext)?.focusManager;
193
193
  }
194
194
 
195
195
  function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): FocusManager {
196
196
  return {
197
197
  focusNext(opts: FocusManagerOptions = {}) {
198
- let scope = scopeRef.current;
198
+ let scope = scopeRef.current!;
199
199
  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);
200
+ let node = from || getOwnerDocument(scope[0]).activeElement!;
201
+ let sentinel = scope[0].previousElementSibling!;
202
+ let scopeRoot = getScopeRoot(scope);
203
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
203
204
  walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
204
205
  let nextNode = walker.nextNode() as FocusableElement;
205
206
  if (!nextNode && wrap) {
@@ -212,11 +213,12 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
212
213
  return nextNode;
213
214
  },
214
215
  focusPrevious(opts: FocusManagerOptions = {}) {
215
- let scope = scopeRef.current;
216
+ let scope = scopeRef.current!;
216
217
  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);
218
+ let node = from || getOwnerDocument(scope[0]).activeElement!;
219
+ let sentinel = scope[scope.length - 1].nextElementSibling!;
220
+ let scopeRoot = getScopeRoot(scope);
221
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
220
222
  walker.currentNode = isElementInScope(node, scope) ? node : sentinel;
221
223
  let previousNode = walker.previousNode() as FocusableElement;
222
224
  if (!previousNode && wrap) {
@@ -229,10 +231,11 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
229
231
  return previousNode;
230
232
  },
231
233
  focusFirst(opts = {}) {
232
- let scope = scopeRef.current;
234
+ let scope = scopeRef.current!;
233
235
  let {tabbable, accept} = opts;
234
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
235
- walker.currentNode = scope[0].previousElementSibling;
236
+ let scopeRoot = getScopeRoot(scope);
237
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
238
+ walker.currentNode = scope[0].previousElementSibling!;
236
239
  let nextNode = walker.nextNode() as FocusableElement;
237
240
  if (nextNode) {
238
241
  focusElement(nextNode, true);
@@ -240,10 +243,11 @@ function createFocusManagerForScope(scopeRef: React.RefObject<Element[]>): Focus
240
243
  return nextNode;
241
244
  },
242
245
  focusLast(opts = {}) {
243
- let scope = scopeRef.current;
246
+ let scope = scopeRef.current!;
244
247
  let {tabbable, accept} = opts;
245
- let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable, accept}, scope);
246
- walker.currentNode = scope[scope.length - 1].nextElementSibling;
248
+ let scopeRoot = getScopeRoot(scope);
249
+ let walker = getFocusableTreeWalker(scopeRoot, {tabbable, accept}, scope);
250
+ walker.currentNode = scope[scope.length - 1].nextElementSibling!;
247
251
  let previousNode = walker.previousNode() as FocusableElement;
248
252
  if (previousNode) {
249
253
  focusElement(previousNode, true);
@@ -275,7 +279,7 @@ focusableElements.push('[tabindex]:not([tabindex="-1"]):not([disabled])');
275
279
  const TABBABLE_ELEMENT_SELECTOR = focusableElements.join(':not([hidden]):not([tabindex="-1"]),');
276
280
 
277
281
  function getScopeRoot(scope: Element[]) {
278
- return scope[0].parentElement;
282
+ return scope[0].parentElement!;
279
283
  }
280
284
 
281
285
  function shouldContainFocus(scopeRef: ScopeRef) {
@@ -291,39 +295,45 @@ function shouldContainFocus(scopeRef: ScopeRef) {
291
295
  return true;
292
296
  }
293
297
 
294
- function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
298
+ function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean) {
295
299
  let focusedNode = useRef<FocusableElement>();
296
300
 
297
- let raf = useRef(null);
301
+ let raf = useRef<ReturnType<typeof requestAnimationFrame>>();
298
302
  useLayoutEffect(() => {
299
303
  let scope = scopeRef.current;
300
304
  if (!contain) {
301
305
  // if contain was changed, then we should cancel any ongoing waits to pull focus back into containment
302
306
  if (raf.current) {
303
307
  cancelAnimationFrame(raf.current);
304
- raf.current = null;
308
+ raf.current = undefined;
305
309
  }
306
310
  return;
307
311
  }
308
312
 
313
+ const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined);
314
+
309
315
  // Handle the Tab key to contain focus within the scope
310
316
  let onKeyDown = (e) => {
311
317
  if (e.key !== 'Tab' || e.altKey || e.ctrlKey || e.metaKey || !shouldContainFocus(scopeRef)) {
312
318
  return;
313
319
  }
314
320
 
315
- let focusedElement = document.activeElement;
321
+ let focusedElement = ownerDocument.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,27 +368,27 @@ 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 (ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) {
362
372
  activeScope = scopeRef;
363
- if (document.body.contains(e.target)) {
373
+ if (ownerDocument.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
  }
370
380
  });
371
381
  };
372
382
 
373
- document.addEventListener('keydown', onKeyDown, false);
374
- document.addEventListener('focusin', onFocus, false);
375
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
376
- scope.forEach(element => element.addEventListener('focusout', onBlur, false));
383
+ ownerDocument.addEventListener('keydown', onKeyDown, false);
384
+ ownerDocument.addEventListener('focusin', onFocus, false);
385
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
386
+ scope?.forEach(element => element.addEventListener('focusout', onBlur, false));
377
387
  return () => {
378
- document.removeEventListener('keydown', onKeyDown, false);
379
- document.removeEventListener('focusin', onFocus, false);
380
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
381
- scope.forEach(element => element.removeEventListener('focusout', onBlur, false));
388
+ ownerDocument.removeEventListener('keydown', onKeyDown, false);
389
+ ownerDocument.removeEventListener('focusin', onFocus, 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,13 @@ 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
+ const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined);
493
+ if (!isElementInScope(ownerDocument.activeElement, activeScope.current) && scopeRef.current) {
475
494
  focusFirstInScope(scopeRef.current);
476
495
  }
477
496
  }
@@ -479,7 +498,7 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
479
498
  }, [scopeRef]);
480
499
  }
481
500
 
482
- function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean, contain: boolean) {
501
+ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore?: boolean, contain?: boolean) {
483
502
  // tracks the active scope, in case restore and contain are both false.
484
503
  // if either are true, this is tracked in useRestoreFocus or useFocusContainment.
485
504
  useLayoutEffect(() => {
@@ -488,8 +507,9 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean,
488
507
  }
489
508
 
490
509
  let scope = scopeRef.current;
510
+ const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined);
491
511
 
492
- let onFocus = (e: FocusEvent) => {
512
+ let onFocus = (e) => {
493
513
  let target = e.target as Element;
494
514
  if (isElementInScope(target, scopeRef.current)) {
495
515
  activeScope = scopeRef;
@@ -498,11 +518,11 @@ function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean,
498
518
  }
499
519
  };
500
520
 
501
- document.addEventListener('focusin', onFocus, false);
502
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
521
+ ownerDocument.addEventListener('focusin', onFocus, false);
522
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
503
523
  return () => {
504
- document.removeEventListener('focusin', onFocus, false);
505
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
524
+ ownerDocument.removeEventListener('focusin', onFocus, false);
525
+ scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
506
526
  };
507
527
  }, [scopeRef, restore, contain]);
508
528
  }
@@ -520,14 +540,16 @@ function shouldRestoreFocus(scopeRef: ScopeRef) {
520
540
  return scope?.scopeRef === scopeRef;
521
541
  }
522
542
 
523
- function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean, contain: boolean) {
543
+ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus?: boolean, contain?: boolean) {
524
544
  // create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
525
- const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? document.activeElement as FocusableElement : null);
545
+ // eslint-disable-next-line no-restricted-globals
546
+ const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined).activeElement as FocusableElement : null);
526
547
 
527
548
  // restoring scopes should all track if they are active regardless of contain, but contain already tracks it plus logic to contain the focus
528
549
  // restoring-non-containing scopes should only care if they become active so they can perform the restore
529
550
  useLayoutEffect(() => {
530
551
  let scope = scopeRef.current;
552
+ const ownerDocument = getOwnerDocument(scope ? scope[0] : undefined);
531
553
  if (!restoreFocus || contain) {
532
554
  return;
533
555
  }
@@ -536,22 +558,24 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
536
558
  // If focusing an element in a child scope of the currently active scope, the child becomes active.
537
559
  // Moving out of the active scope to an ancestor is not allowed.
538
560
  if ((!activeScope || isAncestorScope(activeScope, scopeRef)) &&
539
- isElementInScope(document.activeElement, scopeRef.current)
561
+ isElementInScope(ownerDocument.activeElement, scopeRef.current)
540
562
  ) {
541
563
  activeScope = scopeRef;
542
564
  }
543
565
  };
544
566
 
545
- document.addEventListener('focusin', onFocus, false);
546
- scope.forEach(element => element.addEventListener('focusin', onFocus, false));
567
+ ownerDocument.addEventListener('focusin', onFocus, false);
568
+ scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
547
569
  return () => {
548
- document.removeEventListener('focusin', onFocus, false);
549
- scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
570
+ ownerDocument.removeEventListener('focusin', onFocus, false);
571
+ scope?.forEach(element => element.removeEventListener('focusin', onFocus, false));
550
572
  };
551
573
  // eslint-disable-next-line react-hooks/exhaustive-deps
552
574
  }, [scopeRef, contain]);
553
575
 
554
576
  useLayoutEffect(() => {
577
+ const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined);
578
+
555
579
  if (!restoreFocus) {
556
580
  return;
557
581
  }
@@ -565,22 +589,26 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
565
589
  return;
566
590
  }
567
591
 
568
- let focusedElement = document.activeElement as FocusableElement;
592
+ let focusedElement = ownerDocument.activeElement as FocusableElement;
569
593
  if (!isElementInScope(focusedElement, scopeRef.current)) {
570
594
  return;
571
595
  }
572
- let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
596
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
597
+ if (!treeNode) {
598
+ return;
599
+ }
600
+ let nodeToRestore = treeNode.nodeToRestore;
573
601
 
574
602
  // Create a DOM tree walker that matches all tabbable elements
575
- let walker = getFocusableTreeWalker(document.body, {tabbable: true});
603
+ let walker = getFocusableTreeWalker(ownerDocument.body, {tabbable: true});
576
604
 
577
605
  // Find the next tabbable element after the currently focused element
578
606
  walker.currentNode = focusedElement;
579
607
  let nextElement = (e.shiftKey ? walker.previousNode() : walker.nextNode()) as FocusableElement;
580
608
 
581
- if (!document.body.contains(nodeToRestore) || nodeToRestore === document.body) {
582
- nodeToRestore = null;
583
- focusScopeTree.getTreeNode(scopeRef).nodeToRestore = null;
609
+ if (!nodeToRestore || !ownerDocument.body.contains(nodeToRestore) || nodeToRestore === ownerDocument.body) {
610
+ nodeToRestore = undefined;
611
+ treeNode.nodeToRestore = undefined;
584
612
  }
585
613
 
586
614
  // If there is no next element, or it is outside the current scope, move focus to the
@@ -611,26 +639,35 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
611
639
  };
612
640
 
613
641
  if (!contain) {
614
- document.addEventListener('keydown', onKeyDown, true);
642
+ ownerDocument.addEventListener('keydown', onKeyDown, true);
615
643
  }
616
644
 
617
645
  return () => {
618
646
  if (!contain) {
619
- document.removeEventListener('keydown', onKeyDown, true);
647
+ ownerDocument.removeEventListener('keydown', onKeyDown, true);
620
648
  }
621
649
  };
622
650
  }, [scopeRef, restoreFocus, contain]);
623
651
 
624
652
  // useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously.
625
653
  useLayoutEffect(() => {
654
+ const ownerDocument = getOwnerDocument(scopeRef.current ? scopeRef.current[0] : undefined);
655
+
626
656
  if (!restoreFocus) {
627
657
  return;
628
658
  }
629
659
 
630
- focusScopeTree.getTreeNode(scopeRef).nodeToRestore = nodeToRestoreRef.current;
631
-
660
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
661
+ if (!treeNode) {
662
+ return;
663
+ }
664
+ treeNode.nodeToRestore = nodeToRestoreRef.current ?? undefined;
632
665
  return () => {
633
- let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
666
+ let treeNode = focusScopeTree.getTreeNode(scopeRef);
667
+ if (!treeNode) {
668
+ return;
669
+ }
670
+ let nodeToRestore = treeNode.nodeToRestore;
634
671
 
635
672
  // if we already lost focus to the body and this was the active scope, then we should attempt to restore
636
673
  if (
@@ -638,19 +675,19 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
638
675
  && nodeToRestore
639
676
  && (
640
677
  // eslint-disable-next-line react-hooks/exhaustive-deps
641
- isElementInScope(document.activeElement, scopeRef.current)
642
- || (document.activeElement === document.body && shouldRestoreFocus(scopeRef))
678
+ isElementInScope(ownerDocument.activeElement, scopeRef.current)
679
+ || (ownerDocument.activeElement === ownerDocument.body && shouldRestoreFocus(scopeRef))
643
680
  )
644
681
  ) {
645
682
  // freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
646
683
  let clonedTree = focusScopeTree.clone();
647
684
  requestAnimationFrame(() => {
648
685
  // Only restore focus if we've lost focus to the body, the alternative is that focus has been purposefully moved elsewhere
649
- if (document.activeElement === document.body) {
686
+ if (ownerDocument.activeElement === ownerDocument.body) {
650
687
  // look up the tree starting with our scope to find a nodeToRestore still in the DOM
651
688
  let treeNode = clonedTree.getTreeNode(scopeRef);
652
689
  while (treeNode) {
653
- if (treeNode.nodeToRestore && document.body.contains(treeNode.nodeToRestore)) {
690
+ if (treeNode.nodeToRestore && treeNode.nodeToRestore.isConnected) {
654
691
  focusElement(treeNode.nodeToRestore);
655
692
  return;
656
693
  }
@@ -661,7 +698,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
661
698
  // ancestor scope that is still in the tree.
662
699
  treeNode = clonedTree.getTreeNode(scopeRef);
663
700
  while (treeNode) {
664
- if (treeNode.scopeRef && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
701
+ if (treeNode.scopeRef && treeNode.scopeRef.current && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
665
702
  focusFirstInScope(treeNode.scopeRef.current, true);
666
703
  return;
667
704
  }
@@ -680,7 +717,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
680
717
  */
681
718
  export function getFocusableTreeWalker(root: Element, opts?: FocusManagerOptions, scope?: Element[]) {
682
719
  let selector = opts?.tabbable ? TABBABLE_ELEMENT_SELECTOR : FOCUSABLE_ELEMENT_SELECTOR;
683
- let walker = document.createTreeWalker(
720
+ let walker = getOwnerDocument(root).createTreeWalker(
684
721
  root,
685
722
  NodeFilter.SHOW_ELEMENT,
686
723
  {
@@ -718,13 +755,13 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
718
755
  focusNext(opts: FocusManagerOptions = {}) {
719
756
  let root = ref.current;
720
757
  if (!root) {
721
- return;
758
+ return null;
722
759
  }
723
760
  let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
724
- let node = from || document.activeElement;
761
+ let node = from || getOwnerDocument(root).activeElement;
725
762
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
726
763
  if (root.contains(node)) {
727
- walker.currentNode = node;
764
+ walker.currentNode = node!;
728
765
  }
729
766
  let nextNode = walker.nextNode() as FocusableElement;
730
767
  if (!nextNode && wrap) {
@@ -739,34 +776,39 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
739
776
  focusPrevious(opts: FocusManagerOptions = defaultOptions) {
740
777
  let root = ref.current;
741
778
  if (!root) {
742
- return;
779
+ return null;
743
780
  }
744
781
  let {from, tabbable = defaultOptions.tabbable, wrap = defaultOptions.wrap, accept = defaultOptions.accept} = opts;
745
- let node = from || document.activeElement;
782
+ let node = from || getOwnerDocument(root).activeElement;
746
783
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
747
784
  if (root.contains(node)) {
748
- walker.currentNode = node;
785
+ walker.currentNode = node!;
749
786
  } else {
750
787
  let next = last(walker);
751
788
  if (next) {
752
789
  focusElement(next, true);
753
790
  }
754
- return next;
791
+ return next ?? null;
755
792
  }
756
793
  let previousNode = walker.previousNode() as FocusableElement;
757
794
  if (!previousNode && wrap) {
758
795
  walker.currentNode = root;
759
- previousNode = last(walker);
796
+ let lastNode = last(walker);
797
+ if (!lastNode) {
798
+ // couldn't wrap
799
+ return null;
800
+ }
801
+ previousNode = lastNode;
760
802
  }
761
803
  if (previousNode) {
762
804
  focusElement(previousNode, true);
763
805
  }
764
- return previousNode;
806
+ return previousNode ?? null;
765
807
  },
766
808
  focusFirst(opts = defaultOptions) {
767
809
  let root = ref.current;
768
810
  if (!root) {
769
- return;
811
+ return null;
770
812
  }
771
813
  let {tabbable = defaultOptions.tabbable, accept = defaultOptions.accept} = opts;
772
814
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
@@ -779,7 +821,7 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
779
821
  focusLast(opts = defaultOptions) {
780
822
  let root = ref.current;
781
823
  if (!root) {
782
- return;
824
+ return null;
783
825
  }
784
826
  let {tabbable = defaultOptions.tabbable, accept = defaultOptions.accept} = opts;
785
827
  let walker = getFocusableTreeWalker(root, {tabbable, accept});
@@ -787,13 +829,13 @@ export function createFocusManager(ref: RefObject<Element>, defaultOptions: Focu
787
829
  if (next) {
788
830
  focusElement(next, true);
789
831
  }
790
- return next;
832
+ return next ?? null;
791
833
  }
792
834
  };
793
835
  }
794
836
 
795
837
  function last(walker: TreeWalker) {
796
- let next: FocusableElement;
838
+ let next: FocusableElement | undefined = undefined;
797
839
  let last: FocusableElement;
798
840
  do {
799
841
  last = walker.lastChild() as FocusableElement;
@@ -824,6 +866,9 @@ class Tree {
824
866
 
825
867
  addTreeNode(scopeRef: ScopeRef, parent: ScopeRef, nodeToRestore?: FocusableElement) {
826
868
  let parentNode = this.fastMap.get(parent ?? null);
869
+ if (!parentNode) {
870
+ return;
871
+ }
827
872
  let node = new TreeNode({scopeRef});
828
873
  parentNode.addChild(node);
829
874
  node.parent = parentNode;
@@ -843,6 +888,9 @@ class Tree {
843
888
  return;
844
889
  }
845
890
  let node = this.fastMap.get(scopeRef);
891
+ if (!node) {
892
+ return;
893
+ }
846
894
  let parentNode = node.parent;
847
895
  // when we remove a scope, check if any sibling scopes are trying to restore focus to something inside the scope we're removing
848
896
  // if we are, then replace the siblings restore with the restore from the scope we're removing
@@ -851,6 +899,7 @@ class Tree {
851
899
  current !== node &&
852
900
  node.nodeToRestore &&
853
901
  current.nodeToRestore &&
902
+ node.scopeRef &&
854
903
  node.scopeRef.current &&
855
904
  isElementInScope(current.nodeToRestore, node.scopeRef.current)
856
905
  ) {
@@ -858,9 +907,11 @@ class Tree {
858
907
  }
859
908
  }
860
909
  let children = node.children;
861
- parentNode.removeChild(node);
862
- if (children.size > 0) {
863
- children.forEach(child => parentNode.addChild(child));
910
+ if (parentNode) {
911
+ parentNode.removeChild(node);
912
+ if (children.size > 0) {
913
+ children.forEach(child => parentNode && parentNode.addChild(child));
914
+ }
864
915
  }
865
916
 
866
917
  this.fastMap.delete(node.scopeRef);
@@ -881,7 +932,7 @@ class Tree {
881
932
  clone(): Tree {
882
933
  let newTree = new Tree();
883
934
  for (let node of this.traverse()) {
884
- newTree.addTreeNode(node.scopeRef, node.parent.scopeRef, node.nodeToRestore);
935
+ newTree.addTreeNode(node.scopeRef, node.parent?.scopeRef ?? null, node.nodeToRestore);
885
936
  }
886
937
  return newTree;
887
938
  }
@@ -889,8 +940,8 @@ class Tree {
889
940
 
890
941
  class TreeNode {
891
942
  public scopeRef: ScopeRef;
892
- public nodeToRestore: FocusableElement;
893
- public parent: TreeNode;
943
+ public nodeToRestore?: FocusableElement;
944
+ public parent?: TreeNode;
894
945
  public children: Set<TreeNode> = new Set();
895
946
  public contain = false;
896
947