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