import { useId, useImperativeHandle, useRef, type Ref } from "react"; import { FloatingPortal } from "@floating-ui/react"; import type { SuggestionKeyDownProps } from "@tiptap/suggestion"; import { useMenuNavigation } from "../menu/navigation"; import { preserveEditorSelectionOnMouseDown, useEditorRectPopover } from "../menu/popover"; import type { ResolvedSlashMenuItem } from "./items"; export interface SlashMenuPanelProps { items: ResolvedSlashMenuItem[]; command: (item: ResolvedSlashMenuItem) => void; clientRect: (() => DOMRect | null) | null; contextElement?: HTMLElement | null; onClose?: () => void; ref?: Ref; } export interface SlashMenuPanelHandle { onKeyDown: (props: SuggestionKeyDownProps) => boolean; } export function SlashMenuPanel({ items, command, clientRect, contextElement, onClose, ref }: SlashMenuPanelProps) { const panelRef = useRef(null); const listboxId = useId(); const handleSelect = (item: ResolvedSlashMenuItem) => { if (item.blockedReason) return; command(item); }; const navigation = useMenuNavigation({ items, listRef: panelRef, onSelect: handleSelect, }); const { floatingStyles, setFloating } = useEditorRectPopover({ open: items.length > 0, onClose, getAnchorRect: () => clientRect?.() ?? null, contextElement, maxHeight: true, deferOutsidePress: true, }); useImperativeHandle(ref, () => ({ onKeyDown: ({ event }: SuggestionKeyDownProps) => navigation.onKeyDown(event), })); if (items.length === 0) return null; const groups: { name: string; items: { item: ResolvedSlashMenuItem; index: number }[] }[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; const last = groups[groups.length - 1]; if (!last || last.name !== item.group) { groups.push({ name: item.group, items: [{ item, index: i }] }); } else { last.items.push({ item, index: i }); } } return (
{ setFloating(node); panelRef.current = node; }} className="tiptap-slash-menu" role="listbox" aria-label="Insert block" style={{ ...floatingStyles, zIndex: 80 }} onMouseDownCapture={(e) => preserveEditorSelectionOnMouseDown(e)} > {groups.map((group) => { const labelId = `${listboxId}-group-${group.name}`; return (
{group.name}
{group.items.map(({ item, index }) => { const disabled = Boolean(item.blockedReason); const subtitle = item.blockedReason ?? item.description ?? null; return ( ); })}
); })}
); }