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