@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.
- package/dist/import.mjs +141 -100
- package/dist/main.js +143 -100
- package/dist/main.js.map +1 -1
- package/dist/module.js +141 -100
- 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 +6 -6
- package/src/FocusScope.tsx +190 -139
- package/src/focusSafely.ts +4 -3
- package/src/isElementVisible.ts +7 -4
- package/src/useFocusable.tsx +6 -5
package/src/FocusScope.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
148
|
+
if (scope === focusScopeTree.getTreeNode(scopeRef)) {
|
|
149
|
+
activeScope = scope.scopeRef;
|
|
151
150
|
}
|
|
151
|
+
}
|
|
152
|
+
}, [scopeRef]);
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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 ||
|
|
201
|
-
let sentinel = scope[0].previousElementSibling
|
|
202
|
-
let
|
|
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 ||
|
|
218
|
-
let sentinel = scope[scope.length - 1].nextElementSibling
|
|
219
|
-
let
|
|
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
|
|
235
|
-
walker
|
|
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
|
|
246
|
-
walker
|
|
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
|
|
298
|
+
function useFocusContainment(scopeRef: RefObject<Element[]>, contain?: boolean) {
|
|
295
299
|
let focusedNode = useRef<FocusableElement>();
|
|
296
300
|
|
|
297
|
-
let raf = useRef(
|
|
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 =
|
|
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 =
|
|
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
|
|
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())
|
|
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(
|
|
371
|
+
if (ownerDocument.activeElement && shouldContainFocus(scopeRef) && !isElementInChildScope(ownerDocument.activeElement, scopeRef)) {
|
|
362
372
|
activeScope = scopeRef;
|
|
363
|
-
if (
|
|
373
|
+
if (ownerDocument.body.contains(e.target)) {
|
|
364
374
|
focusedNode.current = e.target;
|
|
365
|
-
focusedNode.current
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
scope
|
|
376
|
-
scope
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
scope
|
|
381
|
-
scope
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
502
|
-
scope
|
|
521
|
+
ownerDocument.addEventListener('focusin', onFocus, false);
|
|
522
|
+
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
503
523
|
return () => {
|
|
504
|
-
|
|
505
|
-
scope
|
|
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
|
|
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
|
-
|
|
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(
|
|
561
|
+
isElementInScope(ownerDocument.activeElement, scopeRef.current)
|
|
540
562
|
) {
|
|
541
563
|
activeScope = scopeRef;
|
|
542
564
|
}
|
|
543
565
|
};
|
|
544
566
|
|
|
545
|
-
|
|
546
|
-
scope
|
|
567
|
+
ownerDocument.addEventListener('focusin', onFocus, false);
|
|
568
|
+
scope?.forEach(element => element.addEventListener('focusin', onFocus, false));
|
|
547
569
|
return () => {
|
|
548
|
-
|
|
549
|
-
scope
|
|
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 =
|
|
592
|
+
let focusedElement = ownerDocument.activeElement as FocusableElement;
|
|
569
593
|
if (!isElementInScope(focusedElement, scopeRef.current)) {
|
|
570
594
|
return;
|
|
571
595
|
}
|
|
572
|
-
let
|
|
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(
|
|
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 (!
|
|
582
|
-
nodeToRestore =
|
|
583
|
-
|
|
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
|
-
|
|
642
|
+
ownerDocument.addEventListener('keydown', onKeyDown, true);
|
|
615
643
|
}
|
|
616
644
|
|
|
617
645
|
return () => {
|
|
618
646
|
if (!contain) {
|
|
619
|
-
|
|
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)
|
|
631
|
-
|
|
660
|
+
let treeNode = focusScopeTree.getTreeNode(scopeRef);
|
|
661
|
+
if (!treeNode) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
treeNode.nodeToRestore = nodeToRestoreRef.current ?? undefined;
|
|
632
665
|
return () => {
|
|
633
|
-
let
|
|
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(
|
|
642
|
-
|| (
|
|
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 (
|
|
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 &&
|
|
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 =
|
|
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 ||
|
|
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 ||
|
|
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
|
-
|
|
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
|
|
862
|
-
|
|
863
|
-
children.
|
|
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
|
|
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
|
|
893
|
-
public parent
|
|
943
|
+
public nodeToRestore?: FocusableElement;
|
|
944
|
+
public parent?: TreeNode;
|
|
894
945
|
public children: Set<TreeNode> = new Set();
|
|
895
946
|
public contain = false;
|
|
896
947
|
|