@react-aria/focus 3.7.0 → 3.9.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.
- package/dist/main.js +225 -39
- package/dist/main.js.map +1 -1
- package/dist/module.js +226 -40
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/FocusScope.tsx +287 -55
package/src/FocusScope.tsx
CHANGED
|
@@ -13,11 +13,9 @@
|
|
|
13
13
|
import {FocusableElement} from '@react-types/shared';
|
|
14
14
|
import {focusSafely} from './focusSafely';
|
|
15
15
|
import {isElementVisible} from './isElementVisible';
|
|
16
|
-
import React, {ReactNode, RefObject, useContext, useEffect, useRef} from 'react';
|
|
16
|
+
import React, {ReactNode, RefObject, useContext, useEffect, useMemo, useRef} from 'react';
|
|
17
17
|
import {useLayoutEffect} from '@react-aria/utils';
|
|
18
18
|
|
|
19
|
-
// import {FocusScope, useFocusScope} from 'react-events/focus-scope';
|
|
20
|
-
// export {FocusScope};
|
|
21
19
|
|
|
22
20
|
export interface FocusScopeProps {
|
|
23
21
|
/** The contents of the focus scope. */
|
|
@@ -70,12 +68,9 @@ interface IFocusContext {
|
|
|
70
68
|
const FocusContext = React.createContext<IFocusContext>(null);
|
|
71
69
|
|
|
72
70
|
let activeScope: ScopeRef = null;
|
|
73
|
-
let scopes: Map<ScopeRef, ScopeRef | null> = new Map();
|
|
74
71
|
|
|
75
72
|
// This is a hacky DOM-based implementation of a FocusScope until this RFC lands in React:
|
|
76
73
|
// https://github.com/reactjs/rfcs/pull/109
|
|
77
|
-
// For now, it relies on the DOM tree order rather than the React tree order, and is probably
|
|
78
|
-
// less optimized for performance.
|
|
79
74
|
|
|
80
75
|
/**
|
|
81
76
|
* A FocusScope manages focus for its descendants. It supports containing focus inside
|
|
@@ -90,7 +85,12 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
90
85
|
let endRef = useRef<HTMLSpanElement>();
|
|
91
86
|
let scopeRef = useRef<Element[]>([]);
|
|
92
87
|
let ctx = useContext(FocusContext);
|
|
93
|
-
|
|
88
|
+
|
|
89
|
+
// The parent scope is based on the JSX tree, using context.
|
|
90
|
+
// However, if a new scope mounts outside the active scope (e.g. DialogContainer launched from a menu),
|
|
91
|
+
// we want the parent scope to be the active scope instead.
|
|
92
|
+
let ctxParent = ctx?.scopeRef ?? null;
|
|
93
|
+
let parentScope = useMemo(() => activeScope && focusScopeTree.getTreeNode(activeScope) && !isAncestorScope(activeScope, ctxParent) ? activeScope : ctxParent, [ctxParent]);
|
|
94
94
|
|
|
95
95
|
useLayoutEffect(() => {
|
|
96
96
|
// Find all rendered nodes between the sentinels and add them to the scope.
|
|
@@ -104,26 +104,41 @@ export function FocusScope(props: FocusScopeProps) {
|
|
|
104
104
|
scopeRef.current = nodes;
|
|
105
105
|
}, [children, parentScope]);
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
(!parentScope || scopes.has(parentScope))
|
|
116
|
-
) {
|
|
117
|
-
activeScope = parentScope;
|
|
118
|
-
}
|
|
119
|
-
scopes.delete(scopeRef);
|
|
120
|
-
};
|
|
121
|
-
}, [scopeRef, parentScope]);
|
|
107
|
+
// add to the focus scope tree in render order because useEffects/useLayoutEffects run children first whereas render runs parent first
|
|
108
|
+
// which matters when constructing a tree
|
|
109
|
+
if (focusScopeTree.getTreeNode(parentScope) && !focusScopeTree.getTreeNode(scopeRef)) {
|
|
110
|
+
focusScopeTree.addTreeNode(scopeRef, parentScope);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let node = focusScopeTree.getTreeNode(scopeRef);
|
|
114
|
+
node.contain = contain;
|
|
122
115
|
|
|
116
|
+
useActiveScopeTracker(scopeRef, restoreFocus, contain);
|
|
123
117
|
useFocusContainment(scopeRef, contain);
|
|
124
118
|
useRestoreFocus(scopeRef, restoreFocus, contain);
|
|
125
119
|
useAutoFocus(scopeRef, autoFocus);
|
|
126
120
|
|
|
121
|
+
// this layout effect needs to run last so that focusScopeTree cleanup happens at the last moment possible
|
|
122
|
+
useLayoutEffect(() => {
|
|
123
|
+
if (scopeRef) {
|
|
124
|
+
return () => {
|
|
125
|
+
// Scope may have been re-parented.
|
|
126
|
+
let parentScope = focusScopeTree.getTreeNode(scopeRef).parent.scopeRef;
|
|
127
|
+
|
|
128
|
+
// Restore the active scope on unmount if this scope or a descendant scope is active.
|
|
129
|
+
// Parent effect cleanups run before children, so we need to check if the
|
|
130
|
+
// parent scope actually still exists before restoring the active scope to it.
|
|
131
|
+
if (
|
|
132
|
+
(scopeRef === activeScope || isAncestorScope(scopeRef, activeScope)) &&
|
|
133
|
+
(!parentScope || focusScopeTree.getTreeNode(parentScope))
|
|
134
|
+
) {
|
|
135
|
+
activeScope = parentScope;
|
|
136
|
+
}
|
|
137
|
+
focusScopeTree.removeTreeNode(scopeRef);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}, [scopeRef, parentScope]);
|
|
141
|
+
|
|
127
142
|
let focusManager = createFocusManagerForScope(scopeRef);
|
|
128
143
|
|
|
129
144
|
return (
|
|
@@ -230,6 +245,19 @@ function getScopeRoot(scope: Element[]) {
|
|
|
230
245
|
return scope[0].parentElement;
|
|
231
246
|
}
|
|
232
247
|
|
|
248
|
+
function shouldContainFocus(scopeRef: ScopeRef) {
|
|
249
|
+
let scope = focusScopeTree.getTreeNode(activeScope);
|
|
250
|
+
while (scope && scope.scopeRef !== scopeRef) {
|
|
251
|
+
if (scope.contain) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
scope = scope.parent;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
|
|
233
261
|
function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
234
262
|
let focusedNode = useRef<FocusableElement>();
|
|
235
263
|
|
|
@@ -247,7 +275,7 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
247
275
|
|
|
248
276
|
// Handle the Tab key to contain focus within the scope
|
|
249
277
|
let onKeyDown = (e) => {
|
|
250
|
-
if (e.key !== 'Tab' || e.altKey || e.ctrlKey || e.metaKey || scopeRef
|
|
278
|
+
if (e.key !== 'Tab' || e.altKey || e.ctrlKey || e.metaKey || !shouldContainFocus(scopeRef)) {
|
|
251
279
|
return;
|
|
252
280
|
}
|
|
253
281
|
|
|
@@ -274,10 +302,10 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
274
302
|
let onFocus = (e) => {
|
|
275
303
|
// If focusing an element in a child scope of the currently active scope, the child becomes active.
|
|
276
304
|
// Moving out of the active scope to an ancestor is not allowed.
|
|
277
|
-
if (!activeScope || isAncestorScope(activeScope, scopeRef)) {
|
|
305
|
+
if ((!activeScope || isAncestorScope(activeScope, scopeRef)) && isElementInScope(e.target, scopeRef.current)) {
|
|
278
306
|
activeScope = scopeRef;
|
|
279
307
|
focusedNode.current = e.target;
|
|
280
|
-
} else if (scopeRef
|
|
308
|
+
} else if (shouldContainFocus(scopeRef) && !isElementInChildScope(e.target, scopeRef)) {
|
|
281
309
|
// If a focus event occurs outside the active scope (e.g. user tabs from browser location bar),
|
|
282
310
|
// restore focus to the previously focused node or the first tabbable element in the active scope.
|
|
283
311
|
if (focusedNode.current) {
|
|
@@ -285,7 +313,7 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
285
313
|
} else if (activeScope) {
|
|
286
314
|
focusFirstInScope(activeScope.current);
|
|
287
315
|
}
|
|
288
|
-
} else if (scopeRef
|
|
316
|
+
} else if (shouldContainFocus(scopeRef)) {
|
|
289
317
|
focusedNode.current = e.target;
|
|
290
318
|
}
|
|
291
319
|
};
|
|
@@ -294,10 +322,14 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
294
322
|
// Firefox doesn't shift focus back to the Dialog properly without this
|
|
295
323
|
raf.current = requestAnimationFrame(() => {
|
|
296
324
|
// Use document.activeElement instead of e.relatedTarget so we can tell if user clicked into iframe
|
|
297
|
-
if (scopeRef
|
|
325
|
+
if (shouldContainFocus(scopeRef) && !isElementInChildScope(document.activeElement, scopeRef)) {
|
|
298
326
|
activeScope = scopeRef;
|
|
299
|
-
|
|
300
|
-
|
|
327
|
+
if (document.body.contains(e.target)) {
|
|
328
|
+
focusedNode.current = e.target;
|
|
329
|
+
focusedNode.current.focus();
|
|
330
|
+
} else if (activeScope) {
|
|
331
|
+
focusFirstInScope(activeScope.current);
|
|
332
|
+
}
|
|
301
333
|
}
|
|
302
334
|
});
|
|
303
335
|
};
|
|
@@ -325,23 +357,18 @@ function useFocusContainment(scopeRef: RefObject<Element[]>, contain: boolean) {
|
|
|
325
357
|
}
|
|
326
358
|
|
|
327
359
|
function isElementInAnyScope(element: Element) {
|
|
328
|
-
|
|
329
|
-
if (isElementInScope(element, scope.current)) {
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
return false;
|
|
360
|
+
return isElementInChildScope(element);
|
|
334
361
|
}
|
|
335
362
|
|
|
336
363
|
function isElementInScope(element: Element, scope: Element[]) {
|
|
337
364
|
return scope.some(node => node.contains(element));
|
|
338
365
|
}
|
|
339
366
|
|
|
340
|
-
function isElementInChildScope(element: Element, scope: ScopeRef) {
|
|
367
|
+
function isElementInChildScope(element: Element, scope: ScopeRef = null) {
|
|
341
368
|
// node.contains in isElementInScope covers child scopes that are also DOM children,
|
|
342
369
|
// but does not cover child scopes in portals.
|
|
343
|
-
for (let s of
|
|
344
|
-
if (
|
|
370
|
+
for (let {scopeRef: s} of focusScopeTree.traverse(focusScopeTree.getTreeNode(scope))) {
|
|
371
|
+
if (isElementInScope(element, s.current)) {
|
|
345
372
|
return true;
|
|
346
373
|
}
|
|
347
374
|
}
|
|
@@ -350,16 +377,14 @@ function isElementInChildScope(element: Element, scope: ScopeRef) {
|
|
|
350
377
|
}
|
|
351
378
|
|
|
352
379
|
function isAncestorScope(ancestor: ScopeRef, scope: ScopeRef) {
|
|
353
|
-
let parent =
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return true;
|
|
380
|
+
let parent = focusScopeTree.getTreeNode(scope)?.parent;
|
|
381
|
+
while (parent) {
|
|
382
|
+
if (parent.scopeRef === ancestor) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
parent = parent.parent;
|
|
360
386
|
}
|
|
361
|
-
|
|
362
|
-
return isAncestorScope(ancestor, parent);
|
|
387
|
+
return false;
|
|
363
388
|
}
|
|
364
389
|
|
|
365
390
|
function focusElement(element: FocusableElement | null, scroll = false) {
|
|
@@ -378,11 +403,20 @@ function focusElement(element: FocusableElement | null, scroll = false) {
|
|
|
378
403
|
}
|
|
379
404
|
}
|
|
380
405
|
|
|
381
|
-
function focusFirstInScope(scope: Element[]) {
|
|
406
|
+
function focusFirstInScope(scope: Element[], tabbable:boolean = true) {
|
|
382
407
|
let sentinel = scope[0].previousElementSibling;
|
|
383
|
-
let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable
|
|
408
|
+
let walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable}, scope);
|
|
384
409
|
walker.currentNode = sentinel;
|
|
385
|
-
|
|
410
|
+
let nextNode = walker.nextNode();
|
|
411
|
+
|
|
412
|
+
// If the scope does not contain a tabbable element, use the first focusable element.
|
|
413
|
+
if (tabbable && !nextNode) {
|
|
414
|
+
walker = getFocusableTreeWalker(getScopeRoot(scope), {tabbable: false}, scope);
|
|
415
|
+
walker.currentNode = sentinel;
|
|
416
|
+
nextNode = walker.nextNode();
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
focusElement(nextNode as FocusableElement);
|
|
386
420
|
}
|
|
387
421
|
|
|
388
422
|
function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
|
|
@@ -395,20 +429,86 @@ function useAutoFocus(scopeRef: RefObject<Element[]>, autoFocus: boolean) {
|
|
|
395
429
|
}
|
|
396
430
|
}
|
|
397
431
|
autoFocusRef.current = false;
|
|
398
|
-
}, []);
|
|
432
|
+
}, [scopeRef]);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function useActiveScopeTracker(scopeRef: RefObject<Element[]>, restore: boolean, contain: boolean) {
|
|
436
|
+
// tracks the active scope, in case restore and contain are both false.
|
|
437
|
+
// if either are true, this is tracked in useRestoreFocus or useFocusContainment.
|
|
438
|
+
useLayoutEffect(() => {
|
|
439
|
+
if (restore || contain) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let scope = scopeRef.current;
|
|
444
|
+
|
|
445
|
+
let onFocus = (e: FocusEvent) => {
|
|
446
|
+
let target = e.target as Element;
|
|
447
|
+
if (isElementInScope(target, scopeRef.current)) {
|
|
448
|
+
activeScope = scopeRef;
|
|
449
|
+
} else if (!isElementInAnyScope(target)) {
|
|
450
|
+
activeScope = null;
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
document.addEventListener('focusin', onFocus, false);
|
|
455
|
+
scope.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
456
|
+
return () => {
|
|
457
|
+
document.removeEventListener('focusin', onFocus, false);
|
|
458
|
+
scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
|
|
459
|
+
};
|
|
460
|
+
}, [scopeRef, restore, contain]);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function shouldRestoreFocus(scopeRef: ScopeRef) {
|
|
464
|
+
let scope = focusScopeTree.getTreeNode(activeScope);
|
|
465
|
+
while (scope && scope.scopeRef !== scopeRef) {
|
|
466
|
+
if (scope.nodeToRestore) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
scope = scope.parent;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return true;
|
|
399
474
|
}
|
|
400
475
|
|
|
401
476
|
function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean, contain: boolean) {
|
|
402
477
|
// create a ref during render instead of useLayoutEffect so the active element is saved before a child with autoFocus=true mounts.
|
|
403
478
|
const nodeToRestoreRef = useRef(typeof document !== 'undefined' ? document.activeElement as FocusableElement : null);
|
|
404
479
|
|
|
480
|
+
// restoring scopes should all track if they are active regardless of contain, but contain already tracks it plus logic to contain the focus
|
|
481
|
+
// restoring-non-containing scopes should only care if they become active so they can perform the restore
|
|
482
|
+
useLayoutEffect(() => {
|
|
483
|
+
let scope = scopeRef.current;
|
|
484
|
+
if (!restoreFocus || contain) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
let onFocus = () => {
|
|
489
|
+
// If focusing an element in a child scope of the currently active scope, the child becomes active.
|
|
490
|
+
// Moving out of the active scope to an ancestor is not allowed.
|
|
491
|
+
if (!activeScope || isAncestorScope(activeScope, scopeRef)) {
|
|
492
|
+
activeScope = scopeRef;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
document.addEventListener('focusin', onFocus, false);
|
|
497
|
+
scope.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
498
|
+
return () => {
|
|
499
|
+
document.removeEventListener('focusin', onFocus, false);
|
|
500
|
+
scope.forEach(element => element.removeEventListener('focusin', onFocus, false));
|
|
501
|
+
};
|
|
502
|
+
}, [scopeRef, contain]);
|
|
503
|
+
|
|
405
504
|
// useLayoutEffect instead of useEffect so the active element is saved synchronously instead of asynchronously.
|
|
406
505
|
useLayoutEffect(() => {
|
|
407
|
-
let nodeToRestore = nodeToRestoreRef.current;
|
|
408
506
|
if (!restoreFocus) {
|
|
409
507
|
return;
|
|
410
508
|
}
|
|
411
509
|
|
|
510
|
+
focusScopeTree.getTreeNode(scopeRef).nodeToRestore = nodeToRestoreRef.current;
|
|
511
|
+
|
|
412
512
|
// Handle the Tab key so that tabbing out of the scope goes to the next element
|
|
413
513
|
// after the node that had focus when the scope mounted. This is important when
|
|
414
514
|
// using portals for overlays, so that focus goes to the expected element when
|
|
@@ -422,6 +522,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
|
|
|
422
522
|
if (!isElementInScope(focusedElement, scopeRef.current)) {
|
|
423
523
|
return;
|
|
424
524
|
}
|
|
525
|
+
let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
|
|
425
526
|
|
|
426
527
|
// Create a DOM tree walker that matches all tabbable elements
|
|
427
528
|
let walker = getFocusableTreeWalker(document.body, {tabbable: true});
|
|
@@ -432,6 +533,7 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
|
|
|
432
533
|
|
|
433
534
|
if (!document.body.contains(nodeToRestore) || nodeToRestore === document.body) {
|
|
434
535
|
nodeToRestore = null;
|
|
536
|
+
focusScopeTree.getTreeNode(scopeRef).nodeToRestore = null;
|
|
435
537
|
}
|
|
436
538
|
|
|
437
539
|
// If there is no next element, or it is outside the current scope, move focus to the
|
|
@@ -469,12 +571,42 @@ function useRestoreFocus(scopeRef: RefObject<Element[]>, restoreFocus: boolean,
|
|
|
469
571
|
if (!contain) {
|
|
470
572
|
document.removeEventListener('keydown', onKeyDown, true);
|
|
471
573
|
}
|
|
574
|
+
let nodeToRestore = focusScopeTree.getTreeNode(scopeRef).nodeToRestore;
|
|
472
575
|
|
|
473
|
-
if
|
|
576
|
+
// if we already lost focus to the body and this was the active scope, then we should attempt to restore
|
|
577
|
+
if (
|
|
578
|
+
restoreFocus
|
|
579
|
+
&& nodeToRestore
|
|
580
|
+
&& (
|
|
581
|
+
isElementInScope(document.activeElement, scopeRef.current)
|
|
582
|
+
|| (document.activeElement === document.body && shouldRestoreFocus(scopeRef))
|
|
583
|
+
)
|
|
584
|
+
) {
|
|
585
|
+
// freeze the focusScopeTree so it persists after the raf, otherwise during unmount nodes are removed from it
|
|
586
|
+
let clonedTree = focusScopeTree.clone();
|
|
474
587
|
requestAnimationFrame(() => {
|
|
475
588
|
// Only restore focus if we've lost focus to the body, the alternative is that focus has been purposefully moved elsewhere
|
|
476
|
-
if (document.
|
|
477
|
-
|
|
589
|
+
if (document.activeElement === document.body) {
|
|
590
|
+
// look up the tree starting with our scope to find a nodeToRestore still in the DOM
|
|
591
|
+
let treeNode = clonedTree.getTreeNode(scopeRef);
|
|
592
|
+
while (treeNode) {
|
|
593
|
+
if (treeNode.nodeToRestore && document.body.contains(treeNode.nodeToRestore)) {
|
|
594
|
+
focusElement(treeNode.nodeToRestore);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
treeNode = treeNode.parent;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// If no nodeToRestore was found, focus the first element in the nearest
|
|
601
|
+
// ancestor scope that is still in the tree.
|
|
602
|
+
treeNode = clonedTree.getTreeNode(scopeRef);
|
|
603
|
+
while (treeNode) {
|
|
604
|
+
if (treeNode.scopeRef && focusScopeTree.getTreeNode(treeNode.scopeRef)) {
|
|
605
|
+
focusFirstInScope(treeNode.scopeRef.current, true);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
treeNode = treeNode.parent;
|
|
609
|
+
}
|
|
478
610
|
}
|
|
479
611
|
});
|
|
480
612
|
}
|
|
@@ -611,3 +743,103 @@ function last(walker: TreeWalker) {
|
|
|
611
743
|
} while (last);
|
|
612
744
|
return next;
|
|
613
745
|
}
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
class Tree {
|
|
749
|
+
private root: TreeNode;
|
|
750
|
+
private fastMap = new Map<ScopeRef, TreeNode>();
|
|
751
|
+
|
|
752
|
+
constructor() {
|
|
753
|
+
this.root = new TreeNode({scopeRef: null});
|
|
754
|
+
this.fastMap.set(null, this.root);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
get size() {
|
|
758
|
+
return this.fastMap.size;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
getTreeNode(data: ScopeRef) {
|
|
762
|
+
return this.fastMap.get(data);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
addTreeNode(scopeRef: ScopeRef, parent: ScopeRef, nodeToRestore?: FocusableElement) {
|
|
766
|
+
let parentNode = this.fastMap.get(parent ?? null);
|
|
767
|
+
let node = new TreeNode({scopeRef});
|
|
768
|
+
parentNode.addChild(node);
|
|
769
|
+
node.parent = parentNode;
|
|
770
|
+
this.fastMap.set(scopeRef, node);
|
|
771
|
+
if (nodeToRestore) {
|
|
772
|
+
node.nodeToRestore = nodeToRestore;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
removeTreeNode(scopeRef: ScopeRef) {
|
|
777
|
+
// never remove the root
|
|
778
|
+
if (scopeRef === null) {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
let node = this.fastMap.get(scopeRef);
|
|
782
|
+
let parentNode = node.parent;
|
|
783
|
+
// when we remove a scope, check if any sibling scopes are trying to restore focus to something inside the scope we're removing
|
|
784
|
+
// if we are, then replace the siblings restore with the restore from the scope we're removing
|
|
785
|
+
for (let current of this.traverse()) {
|
|
786
|
+
if (
|
|
787
|
+
current !== node &&
|
|
788
|
+
node.nodeToRestore &&
|
|
789
|
+
current.nodeToRestore &&
|
|
790
|
+
node.scopeRef.current &&
|
|
791
|
+
isElementInScope(current.nodeToRestore, node.scopeRef.current)
|
|
792
|
+
) {
|
|
793
|
+
current.nodeToRestore = node.nodeToRestore;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
let children = node.children;
|
|
797
|
+
parentNode.removeChild(node);
|
|
798
|
+
if (children.length > 0) {
|
|
799
|
+
children.forEach(child => parentNode.addChild(child));
|
|
800
|
+
}
|
|
801
|
+
this.fastMap.delete(node.scopeRef);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Pre Order Depth First
|
|
805
|
+
*traverse(node: TreeNode = this.root): Generator<TreeNode> {
|
|
806
|
+
if (node.scopeRef != null) {
|
|
807
|
+
yield node;
|
|
808
|
+
}
|
|
809
|
+
if (node.children.length > 0) {
|
|
810
|
+
for (let child of node.children) {
|
|
811
|
+
yield* this.traverse(child);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
clone(): Tree {
|
|
817
|
+
let newTree = new Tree();
|
|
818
|
+
for (let node of this.traverse()) {
|
|
819
|
+
newTree.addTreeNode(node.scopeRef, node.parent.scopeRef, node.nodeToRestore);
|
|
820
|
+
}
|
|
821
|
+
return newTree;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
class TreeNode {
|
|
826
|
+
public scopeRef: ScopeRef;
|
|
827
|
+
public nodeToRestore: FocusableElement;
|
|
828
|
+
public parent: TreeNode;
|
|
829
|
+
public children: TreeNode[] = [];
|
|
830
|
+
public contain = false;
|
|
831
|
+
|
|
832
|
+
constructor(props: {scopeRef: ScopeRef}) {
|
|
833
|
+
this.scopeRef = props.scopeRef;
|
|
834
|
+
}
|
|
835
|
+
addChild(node: TreeNode) {
|
|
836
|
+
this.children.push(node);
|
|
837
|
+
node.parent = this;
|
|
838
|
+
}
|
|
839
|
+
removeChild(node: TreeNode) {
|
|
840
|
+
this.children.splice(this.children.indexOf(node), 1);
|
|
841
|
+
node.parent = undefined;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export let focusScopeTree = new Tree();
|