import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Box, ClickAwayListener, List, ListItemButton, Paper, Popper, TextField, } from "@mui/material"; import { filterMentionCandidates, findActiveMentionQuery, insertMentionToken, type MentionLookup, } from "@/domain/work-orders/utils/comment-mentions"; type CommentMentionComposerProps = { value: string; onChange: (value: string) => void; candidates: MentionLookup[]; placeholder?: string; disabled?: boolean; rows?: number; onSubmit?: () => void; }; type MentionCandidateMenuProps = { open: boolean; anchorEl: HTMLElement | null; candidates: MentionLookup[]; highlightIndex: number; onSelect: (user: MentionLookup) => void; }; function MentionCandidateMenu({ open, anchorEl, candidates, highlightIndex, onSelect, }: MentionCandidateMenuProps) { return ( {candidates.map((candidate, index) => ( event.preventDefault()} onClick={() => onSelect(candidate)} > @{candidate.name} ))} ); } export function CommentMentionComposer({ value, onChange, candidates, placeholder = "Type a comment...", disabled = false, rows = 2, onSubmit, }: CommentMentionComposerProps) { const inputRef = useRef(null); const [anchorEl, setAnchorEl] = useState(null); const [cursor, setCursor] = useState(0); const [menuOpen, setMenuOpen] = useState(false); const [highlightIndex, setHighlightIndex] = useState(0); const activeQuery = useMemo(() => findActiveMentionQuery(value, cursor), [value, cursor]); const filteredCandidates = useMemo( () => filterMentionCandidates(candidates, activeQuery?.query ?? ""), [activeQuery?.query, candidates], ); const showMenu = Boolean(menuOpen && activeQuery && filteredCandidates.length > 0); useEffect(() => { setHighlightIndex(0); }, [activeQuery?.query, activeQuery?.start]); const syncCursor = useCallback((textOverride?: string) => { const element = inputRef.current; if (!element) { return; } const text = textOverride ?? element.value; const nextCursor = element.selectionStart ?? text.length; setCursor(nextCursor); setMenuOpen(Boolean(findActiveMentionQuery(text, nextCursor))); }, []); const handleChange = (nextValue: string) => { onChange(nextValue); requestAnimationFrame(() => { syncCursor(nextValue); }); }; const applyMention = useCallback( (user: MentionLookup) => { if (!activeQuery) { return; } const { nextText, nextCursor } = insertMentionToken(value, activeQuery.start, cursor, user); onChange(nextText); setMenuOpen(false); setHighlightIndex(0); requestAnimationFrame(() => { const element = inputRef.current; if (!element) { return; } element.focus(); element.setSelectionRange(nextCursor, nextCursor); setCursor(nextCursor); }); }, [activeQuery, cursor, onChange, value], ); const handleKeyDown = (event: React.KeyboardEvent) => { if (showMenu) { if (event.key === "ArrowDown") { event.preventDefault(); setHighlightIndex((current) => (current + 1) % filteredCandidates.length); return; } if (event.key === "ArrowUp") { event.preventDefault(); setHighlightIndex( (current) => (current - 1 + filteredCandidates.length) % filteredCandidates.length, ); return; } if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); const selected = filteredCandidates[highlightIndex]; if (selected) { applyMention(selected); } return; } if (event.key === "Escape") { event.preventDefault(); setMenuOpen(false); return; } } if (event.key === "Enter" && (event.metaKey || event.ctrlKey) && onSubmit) { event.preventDefault(); onSubmit(); } }; return ( setMenuOpen(false)}> { setAnchorEl(node); }} > handleChange(event.target.value)} onClick={() => syncCursor()} onKeyUp={() => syncCursor()} onSelect={() => syncCursor()} onKeyDown={handleKeyDown} placeholder={placeholder} disabled={disabled} /> ); }