fix(work-orders): keep mention menu open after typing @

Pass live text into syncCursor instead of a render-time valueRef.
Add a composer interaction regression for @ autocomplete visibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Arthur Bassi 2026-08-11 11:56:19 -03:00
parent 8783aa6e18
commit 5a29bf6c33
2 changed files with 80 additions and 27 deletions

View file

@ -25,6 +25,41 @@ type CommentMentionComposerProps = {
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 (
<Popper open={open} anchorEl={anchorEl} placement="bottom-start" className="z-50">
<Paper elevation={4} className="mt-1 max-h-48 overflow-y-auto">
<List dense disablePadding>
{candidates.map((candidate, index) => (
<ListItemButton
key={String(candidate.id)}
selected={index === highlightIndex}
onMouseDown={(event) => event.preventDefault()}
onClick={() => onSelect(candidate)}
>
@{candidate.name}
</ListItemButton>
))}
</List>
</Paper>
</Popper>
);
}
export function CommentMentionComposer({
value,
onChange,
@ -35,8 +70,6 @@ export function CommentMentionComposer({
onSubmit,
}: CommentMentionComposerProps) {
const inputRef = useRef<HTMLTextAreaElement | HTMLInputElement>(null);
const valueRef = useRef(value);
valueRef.current = value;
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
const [cursor, setCursor] = useState(0);
const [menuOpen, setMenuOpen] = useState(false);
@ -55,22 +88,23 @@ export function CommentMentionComposer({
setHighlightIndex(0);
}, [activeQuery?.query, activeQuery?.start]);
const syncCursor = useCallback(() => {
const syncCursor = useCallback((textOverride?: string) => {
const element = inputRef.current;
if (!element) {
return;
}
const currentValue = valueRef.current;
const nextCursor = element.selectionStart ?? currentValue.length;
const text = textOverride ?? element.value;
const nextCursor = element.selectionStart ?? text.length;
setCursor(nextCursor);
setMenuOpen(Boolean(findActiveMentionQuery(currentValue, nextCursor)));
setMenuOpen(Boolean(findActiveMentionQuery(text, nextCursor)));
}, []);
const handleChange = (nextValue: string) => {
valueRef.current = nextValue;
onChange(nextValue);
requestAnimationFrame(syncCursor);
requestAnimationFrame(() => {
syncCursor(nextValue);
});
};
const applyMention = useCallback(
@ -150,30 +184,21 @@ export function CommentMentionComposer({
fullWidth
value={value}
onChange={(event) => handleChange(event.target.value)}
onClick={syncCursor}
onKeyUp={syncCursor}
onSelect={syncCursor}
onClick={() => syncCursor()}
onKeyUp={() => syncCursor()}
onSelect={() => syncCursor()}
onKeyDown={handleKeyDown}
placeholder={placeholder}
disabled={disabled}
/>
<Popper open={showMenu} anchorEl={anchorEl} placement="bottom-start" className="z-50">
<Paper elevation={4} className="mt-1 max-h-48 overflow-y-auto">
<List dense disablePadding>
{filteredCandidates.map((candidate, index) => (
<ListItemButton
key={String(candidate.id)}
selected={index === highlightIndex}
onMouseDown={(event) => event.preventDefault()}
onClick={() => applyMention(candidate)}
>
@{candidate.name}
</ListItemButton>
))}
</List>
</Paper>
</Popper>
<MentionCandidateMenu
open={showMenu}
anchorEl={anchorEl}
candidates={filteredCandidates}
highlightIndex={highlightIndex}
onSelect={applyMention}
/>
</Box>
</ClickAwayListener>
);

View file

@ -0,0 +1,28 @@
import { screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { describe, expect, it } from "vitest";
import { CommentMentionComposer } from "@/components/common/comment-mention-composer";
import { renderWithProviders } from "@/test/test-utils";
const candidates = [{ id: "1", name: "Alice Dispatcher" }];
function MentionComposerHarness() {
const [value, setValue] = useState("");
return <CommentMentionComposer value={value} onChange={setValue} candidates={candidates} />;
}
describe("CommentMentionComposer", () => {
it("keeps the suggestion menu visible after typing @", async () => {
const user = userEvent.setup();
renderWithProviders(<MentionComposerHarness />, { withAuth: false });
await user.type(screen.getByRole("textbox"), "@");
await waitFor(() => {
expect(screen.getByRole("button", { name: "@Alice Dispatcher" })).toBeVisible();
});
});
});