casualroom/apps/fenghuo/web/hooks/use-menu-navigation.ts

162 lines
3.6 KiB
TypeScript
Raw Normal View History

2025-07-28 07:50:50 +08:00
"use client"
import * as React from "react"
import type { Editor } from "@tiptap/react"
type Orientation = "horizontal" | "vertical" | "both"
interface MenuNavigationOptions<T> {
editor?: Editor | null
containerRef?: React.RefObject<HTMLElement | null>
query?: string
items: T[]
onSelect?: (item: T) => void
onClose?: () => void
orientation?: Orientation
autoSelectFirstItem?: boolean
}
export function useMenuNavigation<T>({
editor,
containerRef,
query,
items,
onSelect,
onClose,
orientation = "vertical",
autoSelectFirstItem = true,
}: MenuNavigationOptions<T>) {
const [selectedIndex, setSelectedIndex] = React.useState<number>(
autoSelectFirstItem ? 0 : -1
)
React.useEffect(() => {
const handleKeyboardNavigation = (event: KeyboardEvent) => {
if (!items.length) return false
const moveNext = () =>
setSelectedIndex((currentIndex) => {
if (currentIndex === -1) return 0
return (currentIndex + 1) % items.length
})
const movePrev = () =>
setSelectedIndex((currentIndex) => {
if (currentIndex === -1) return items.length - 1
return (currentIndex - 1 + items.length) % items.length
})
switch (event.key) {
case "ArrowUp": {
if (orientation === "horizontal") return false
event.preventDefault()
movePrev()
return true
}
case "ArrowDown": {
if (orientation === "horizontal") return false
event.preventDefault()
moveNext()
return true
}
case "ArrowLeft": {
if (orientation === "vertical") return false
event.preventDefault()
movePrev()
return true
}
case "ArrowRight": {
if (orientation === "vertical") return false
event.preventDefault()
moveNext()
return true
}
case "Tab": {
event.preventDefault()
if (event.shiftKey) {
movePrev()
} else {
moveNext()
}
return true
}
case "Home": {
event.preventDefault()
setSelectedIndex(0)
return true
}
case "End": {
event.preventDefault()
setSelectedIndex(items.length - 1)
return true
}
case "Enter": {
if (event.isComposing) return false
event.preventDefault()
if (selectedIndex !== -1 && items[selectedIndex]) {
onSelect?.(items[selectedIndex])
}
return true
}
case "Escape": {
event.preventDefault()
onClose?.()
return true
}
default:
return false
}
}
let targetElement: HTMLElement | null = null
if (editor) {
targetElement = editor.view.dom
} else if (containerRef?.current) {
targetElement = containerRef.current
}
if (targetElement) {
targetElement.addEventListener("keydown", handleKeyboardNavigation, true)
return () => {
targetElement?.removeEventListener(
"keydown",
handleKeyboardNavigation,
true
)
}
}
return undefined
}, [
editor,
containerRef,
items,
selectedIndex,
onSelect,
onClose,
orientation,
])
React.useEffect(() => {
if (query) {
setSelectedIndex(autoSelectFirstItem ? 0 : -1)
}
}, [query, autoSelectFirstItem])
return {
selectedIndex: items.length ? selectedIndex : undefined,
setSelectedIndex,
}
}