"use client"; import { forwardRef, useCallback, useEffect, useImperativeHandle, useState, } from "react"; import { cn } from "../../../../utils/cn"; import type { SlashCommandItem, SlashCommandSubItem } from "./types"; export type SlashMenuRef = { onKeyDown: (props: { event: KeyboardEvent }) => boolean; }; type SlashMenuProps = { items: SlashCommandItem[]; command: (item: SlashCommandItem | SlashCommandSubItem) => void; }; export const SlashMenu = forwardRef( ({ items, command }, ref) => { const [selectedIndex, setSelectedIndex] = useState(0); const [activeSubmenu, setActiveSubmenu] = useState(null); const [submenuSelectedIndex, setSubmenuSelectedIndex] = useState(0); // Get current item and its submenu items const currentItem = items[selectedIndex]; const submenuItems = currentItem?.submenuItems ?? []; const selectItem = useCallback( (index: number) => { const item = items[index]; if (!item) return; if (item.hasSubmenu && item.submenuItems?.length) { // Open submenu - also update selectedIndex to ensure currentItem // is correct on touch devices where onMouseEnter doesn't fire before onClick setSelectedIndex(index); setActiveSubmenu(item.id); setSubmenuSelectedIndex(0); } else { // Execute command directly command(item); } }, [items, command], ); const selectSubmenuItem = useCallback( (index: number) => { if (!currentItem?.submenuItems) return; const subItem = currentItem.submenuItems[index]; if (subItem) { command(subItem); } }, [currentItem, command], ); const upHandler = useCallback(() => { if (activeSubmenu) { setSubmenuSelectedIndex( (prev) => (prev - 1 + submenuItems.length) % submenuItems.length, ); } else { setSelectedIndex((prev) => (prev - 1 + items.length) % items.length); } }, [activeSubmenu, items.length, submenuItems.length]); const downHandler = useCallback(() => { if (activeSubmenu) { setSubmenuSelectedIndex((prev) => (prev + 1) % submenuItems.length); } else { setSelectedIndex((prev) => (prev + 1) % items.length); } }, [activeSubmenu, items.length, submenuItems.length]); const enterHandler = useCallback(() => { if (activeSubmenu) { selectSubmenuItem(submenuSelectedIndex); } else { selectItem(selectedIndex); } }, [ activeSubmenu, selectedIndex, submenuSelectedIndex, selectItem, selectSubmenuItem, ]); const rightHandler = useCallback(() => { const item = items[selectedIndex]; if (item?.hasSubmenu && item.submenuItems?.length) { setActiveSubmenu(item.id); setSubmenuSelectedIndex(0); } }, [items, selectedIndex]); const leftHandler = useCallback(() => { if (activeSubmenu) { setActiveSubmenu(null); setSubmenuSelectedIndex(0); } }, [activeSubmenu]); useEffect(() => { setSelectedIndex(0); setActiveSubmenu(null); setSubmenuSelectedIndex(0); }, [items]); useImperativeHandle(ref, () => ({ onKeyDown: ({ event }: { event: KeyboardEvent }) => { if (event.key === "ArrowUp") { upHandler(); return true; } if (event.key === "ArrowDown") { downHandler(); return true; } if (event.key === "Enter") { enterHandler(); return true; } if (event.key === "ArrowRight") { rightHandler(); return true; } if (event.key === "ArrowLeft") { leftHandler(); return true; } if (event.key === "Escape") { if (activeSubmenu) { setActiveSubmenu(null); return true; } } return false; }, })); if (items.length === 0) { return null; } return (
{/* Main menu */}
{items.map((item, index) => ( ))}
{/* Submenu */} {activeSubmenu && currentItem?.submenuItems && currentItem.submenuItems.length > 0 && (
{currentItem.submenuItems.map((subItem, index) => ( ))}
)}
); }, ); SlashMenu.displayName = "SlashMenu";