@react-aria/focus 3.14.2 → 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.
- package/dist/import.mjs +102 -71
- package/dist/main.js +103 -70
- package/dist/main.js.map +1 -1
- package/dist/module.js +102 -71
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/FocusScope.tsx +156 -113
- package/src/isElementVisible.ts +3 -3
- package/src/useFocusable.tsx +6 -5
package/src/FocusScope.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
149
|
+
if (scope === focusScopeTree.getTreeNode(scopeRef)) {
|
|
150
|
+
activeScope = scope.scopeRef;
|
|
151
151
|
}
|
|
152
|
+
}
|
|
153
|
+
}, [scopeRef]);
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
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
|
|
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
|
|
235
|
-
walker
|
|
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
|
|
246
|
-
walker
|
|
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
|
|
299
|
+
function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean) {
|
|
295
300
|
let focusedNode = useRef<FocusableElement>();
|
|
296
301
|
|
|
297
|
-
let raf = useRef(
|
|
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 =
|
|
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
|
|
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())
|
|
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
|
|
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
|
|
376
|
-
scope
|
|
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
|
|
381
|
-
scope
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
519
|
+
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
503
520
|
return () => {
|
|
504
521
|
document.removeEventListener('focusin', onFocus, false);
|
|
505
|
-
scope
|
|
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
|
|
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
|
|
563
|
+
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
547
564
|
return () => {
|
|
548
565
|
document.removeEventListener('focusin', onFocus, false);
|
|
549
|
-
scope
|
|
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
|
|
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 =
|
|
583
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
862
|
-
|
|
863
|
-
children.
|
|
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
|
|
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
|
|
893
|
-
public parent
|
|
935
|
+
public nodeToRestore?: FocusableElement;
|
|
936
|
+
public parent?: TreeNode;
|
|
894
937
|
public children: Set<TreeNode> = new Set();
|
|
895
938
|
public contain = false;
|
|
896
939
|
|
package/src/isElementVisible.ts
CHANGED
|
@@ -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' &&
|