104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { useEffect, useRef, type RefObject } from 'react'
|
|
|
|
interface UseArrowNavigationParams {
|
|
itemCount: number
|
|
containerRef: RefObject<HTMLElement | null>
|
|
enabled?: boolean
|
|
}
|
|
|
|
function handleArrowDown(
|
|
e: KeyboardEvent,
|
|
focusableElements: NodeListOf<HTMLElement>,
|
|
getCurrentIndex: () => number,
|
|
setCurrentIndex: (index: number) => void
|
|
): void {
|
|
e.preventDefault()
|
|
const currentIndex = getCurrentIndex()
|
|
const nextIndex = currentIndex < focusableElements.length - 1 ? currentIndex + 1 : 0
|
|
setCurrentIndex(nextIndex)
|
|
focusableElements[nextIndex]?.focus()
|
|
}
|
|
|
|
function handleArrowUp(
|
|
e: KeyboardEvent,
|
|
focusableElements: NodeListOf<HTMLElement>,
|
|
getCurrentIndex: () => number,
|
|
setCurrentIndex: (index: number) => void
|
|
): void {
|
|
e.preventDefault()
|
|
const currentIndex = getCurrentIndex()
|
|
const prevIndex = currentIndex > 0 ? currentIndex - 1 : focusableElements.length - 1
|
|
setCurrentIndex(prevIndex)
|
|
focusableElements[prevIndex]?.focus()
|
|
}
|
|
|
|
function handleHome(e: KeyboardEvent, focusableElements: NodeListOf<HTMLElement>, setCurrentIndex: (index: number) => void): void {
|
|
e.preventDefault()
|
|
setCurrentIndex(0)
|
|
focusableElements[0]?.focus()
|
|
}
|
|
|
|
function handleEnd(e: KeyboardEvent, focusableElements: NodeListOf<HTMLElement>, setCurrentIndex: (index: number) => void): void {
|
|
e.preventDefault()
|
|
setCurrentIndex(focusableElements.length - 1)
|
|
focusableElements[focusableElements.length - 1]?.focus()
|
|
}
|
|
|
|
function createKeyDownHandler(
|
|
containerRef: RefObject<HTMLElement | null>,
|
|
getCurrentIndex: () => number,
|
|
setCurrentIndex: (index: number) => void
|
|
): (e: KeyboardEvent) => void {
|
|
return (e: KeyboardEvent): void => {
|
|
if (!containerRef.current) {
|
|
return
|
|
}
|
|
|
|
const focusableElements = containerRef.current.querySelectorAll<HTMLElement>(
|
|
'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
)
|
|
|
|
if (focusableElements.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (e.key === 'ArrowDown') {
|
|
handleArrowDown(e, focusableElements, getCurrentIndex, setCurrentIndex)
|
|
} else if (e.key === 'ArrowUp') {
|
|
handleArrowUp(e, focusableElements, getCurrentIndex, setCurrentIndex)
|
|
} else if (e.key === 'Home') {
|
|
handleHome(e, focusableElements, setCurrentIndex)
|
|
} else if (e.key === 'End') {
|
|
handleEnd(e, focusableElements, setCurrentIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useArrowNavigation({ itemCount, containerRef, enabled = true }: UseArrowNavigationParams): void {
|
|
const currentIndexRef = useRef<number>(-1)
|
|
|
|
useEffect(() => {
|
|
if (!enabled || itemCount === 0) {
|
|
return
|
|
}
|
|
|
|
const getCurrentIndex = (): number => currentIndexRef.current
|
|
const setCurrentIndex = (index: number): void => {
|
|
currentIndexRef.current = index
|
|
}
|
|
|
|
const handleKeyDown = createKeyDownHandler(containerRef, getCurrentIndex, setCurrentIndex)
|
|
|
|
const container = containerRef.current
|
|
if (!container) {
|
|
return
|
|
}
|
|
|
|
container.addEventListener('keydown', handleKeyDown)
|
|
|
|
return () => {
|
|
container.removeEventListener('keydown', handleKeyDown)
|
|
}
|
|
}, [itemCount, containerRef, enabled])
|
|
}
|