-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathuseFocusTrap.ts
77 lines (68 loc) · 2.71 KB
/
useFocusTrap.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import React from 'react'
import {focusTrap} from '@primer/behaviors'
export interface FocusTrapHookSettings {
/**
* Ref that will be used for the trapping container. If not provided, one will
* be created by this hook and returned.
*/
containerRef: React.RefObject<HTMLElement>
/**
* Ref for the element that should receive focus when the focus trap is first enabled.
* If not provided, one will be created by this hook and returned. Its use is optional.
*/
initialFocusRef?: React.RefObject<HTMLElement>
/**
* Set to true to disable the focus trap and clean up listeners. Can be re-enabled at any time.
*/
disabled?: boolean
/**
* If true, when this focus trap is cleaned up, restore focus to the element that had focus immediately before the focus trap was enabled. (Default: false)
*/
restoreFocusOnCleanUp?: boolean
}
/**
* Hook used to trap focus inside a container. Returns a ref that can be added to the container
* that should trap focus.
* @param settings {FocusTrapHookSettings}
*/
export function useFocusTrap(
settings?: FocusTrapHookSettings,
dependencies: React.DependencyList = [],
): {containerRef: React.RefObject<HTMLElement> | undefined; initialFocusRef: React.RefObject<HTMLElement> | undefined} {
const containerRef = settings?.containerRef
const initialFocusRef = settings?.initialFocusRef
const disabled = settings?.disabled
const abortController = React.useRef<AbortController>()
const previousFocusedElement = React.useRef<Element | null>(null)
// If we are enabling a focus trap and haven't already stored the previously focused element
// go ahead an do that so we can restore later when the trap is disabled.
if (!previousFocusedElement.current && !settings?.disabled) {
previousFocusedElement.current = document.activeElement
}
// This function removes the event listeners that enable the focus trap and restores focus
// to the previously-focused element (if necessary).
function disableTrap() {
abortController.current?.abort()
if (settings?.restoreFocusOnCleanUp && previousFocusedElement.current instanceof HTMLElement) {
previousFocusedElement.current.focus()
previousFocusedElement.current = null
}
}
React.useEffect(
() => {
if (containerRef?.current instanceof HTMLElement) {
if (!disabled) {
abortController.current = focusTrap(containerRef.current, initialFocusRef?.current ?? undefined)
return () => {
disableTrap()
}
} else {
disableTrap()
}
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[containerRef, initialFocusRef, disabled, ...dependencies],
)
return {containerRef, initialFocusRef}
}