84 lines
2.7 KiB
TypeScript
84 lines
2.7 KiB
TypeScript
"use client"
|
|
|
|
import { useCallback, useEffect, useRef, useState } from "react"
|
|
|
|
import { useHandleKeyPress } from "@/hooks/useHandleKeyPress"
|
|
import findTabbableDescendants from "@/utils/tabbable"
|
|
|
|
const TAB_KEY = "Tab"
|
|
const optionsDefault = { focusOnRender: true, returnFocus: true }
|
|
type OptionsType = {
|
|
focusOnRender?: boolean
|
|
returnFocus?: boolean
|
|
}
|
|
export function useTrapFocus(opts?: OptionsType) {
|
|
const options = opts ? { ...optionsDefault, ...opts } : optionsDefault
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
const previouseFocusedElement = useRef<HTMLElement>(
|
|
document.activeElement as HTMLElement
|
|
)
|
|
const [tabbableElements, setTabbableElements] = useState<HTMLElement[]>([])
|
|
// Handle initial focus of the referenced element, and return focus to previously focused element on cleanup
|
|
// and find all the tabbable elements in the referenced element
|
|
|
|
useEffect(() => {
|
|
const { current } = ref
|
|
if (current) {
|
|
const focusableChildNodes = findTabbableDescendants(current)
|
|
if (options.focusOnRender) {
|
|
current.focus()
|
|
}
|
|
|
|
setTabbableElements(focusableChildNodes)
|
|
}
|
|
return () => {
|
|
const { current } = previouseFocusedElement
|
|
if (current instanceof HTMLElement && options.returnFocus) {
|
|
current.focus()
|
|
}
|
|
}
|
|
}, [options.focusOnRender, options.returnFocus, ref, setTabbableElements])
|
|
|
|
const handleUserKeyPress = useCallback(
|
|
(event: KeyboardEvent) => {
|
|
const { code, shiftKey } = event
|
|
const first = tabbableElements[0]
|
|
const last = tabbableElements[tabbableElements.length - 1]
|
|
|
|
const currentActiveElement = document.activeElement
|
|
// Scope current tabs to current root element
|
|
if (isWithinCurrentElementScope([...tabbableElements, ref.current])) {
|
|
if (code === TAB_KEY) {
|
|
if (
|
|
currentActiveElement === first ||
|
|
currentActiveElement === ref.current
|
|
) {
|
|
// move focus to last element if shift+tab while currently focusing the first tabbable element
|
|
if (shiftKey) {
|
|
event.preventDefault()
|
|
last.focus()
|
|
}
|
|
}
|
|
if (currentActiveElement === last) {
|
|
// move focus back to first if tabbing while currently focusing the last tabbable element
|
|
if (!shiftKey) {
|
|
event.preventDefault()
|
|
first.focus()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[ref, tabbableElements]
|
|
)
|
|
useHandleKeyPress(handleUserKeyPress)
|
|
|
|
return ref
|
|
}
|
|
function isWithinCurrentElementScope(
|
|
elementList: (HTMLInputElement | Element | null)[]
|
|
) {
|
|
const currentActiveElement = document.activeElement
|
|
return elementList.includes(currentActiveElement)
|
|
}
|