From 5a29bf6c3344e22331d49095276c44217d65a97f Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Tue, 11 Aug 2026 11:56:19 -0300 Subject: [PATCH] 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 --- .../common/comment-mention-composer.tsx | 79 ++++++++++++------- .../common/comment-mention-composer.test.tsx | 28 +++++++ 2 files changed, 80 insertions(+), 27 deletions(-) create mode 100644 src/test/components/common/comment-mention-composer.test.tsx diff --git a/src/components/common/comment-mention-composer.tsx b/src/components/common/comment-mention-composer.tsx index 63c84f22..140753b0 100644 --- a/src/components/common/comment-mention-composer.tsx +++ b/src/components/common/comment-mention-composer.tsx @@ -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 ( + + + + {candidates.map((candidate, index) => ( + event.preventDefault()} + onClick={() => onSelect(candidate)} + > + @{candidate.name} + + ))} + + + + ); +} + export function CommentMentionComposer({ value, onChange, @@ -35,8 +70,6 @@ export function CommentMentionComposer({ onSubmit, }: CommentMentionComposerProps) { const inputRef = useRef(null); - const valueRef = useRef(value); - valueRef.current = value; const [anchorEl, setAnchorEl] = useState(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} /> - - - - {filteredCandidates.map((candidate, index) => ( - event.preventDefault()} - onClick={() => applyMention(candidate)} - > - @{candidate.name} - - ))} - - - + ); diff --git a/src/test/components/common/comment-mention-composer.test.tsx b/src/test/components/common/comment-mention-composer.test.tsx new file mode 100644 index 00000000..a732af50 --- /dev/null +++ b/src/test/components/common/comment-mention-composer.test.tsx @@ -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 ; +} + +describe("CommentMentionComposer", () => { + it("keeps the suggestion menu visible after typing @", async () => { + const user = userEvent.setup(); + renderWithProviders(, { withAuth: false }); + + await user.type(screen.getByRole("textbox"), "@"); + + await waitFor(() => { + expect(screen.getByRole("button", { name: "@Alice Dispatcher" })).toBeVisible(); + }); + }); +});