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}
/>
);
}