mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-07 16:19:01 +00:00
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:
parent
8783aa6e18
commit
5a29bf6c33
2 changed files with 80 additions and 27 deletions
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
28
src/test/components/common/comment-mention-composer.test.tsx
Normal file
28
src/test/components/common/comment-mention-composer.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue