story-research-zapwall/hooks/useArrowNavigation.ts
2026-01-15 11:31:09 +01:00

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