Files
web/hooks/useTrapFocus.ts
Erik Tiekstra d1a2175804 chore: cleanup
2024-09-03 13:26:15 +02:00

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)
}