mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(site/src/pages/AgentsPage): dismiss skills trigger on outside click (#25613)
When the personal skills menu is open and the user clicks outside (e.g. the send button), the Popover closes via `onOpenChange` but the `SkillsTriggerPlugin`'s `dismissedTriggerRef` is not set. The next Lexical update listener call detects the trigger again and briefly reopens the menu, causing a visible flash. Addresses this symptom: https://github.com/user-attachments/assets/0c1442a2-df75-442b-bcf8-4b028dc647b0 Fix by recording the current trigger position in `dismissedTriggerRef` when the `open` prop transitions from `true` to `false`. This mirrors what the Escape key handler already does and prevents `refreshTrigger` from immediately re-opening the menu at the same position. <details><summary>Implementation details</summary> - Added a `useLayoutEffect` in `SkillsTriggerPlugin` that tracks `open` prop transitions via a `prevOpenRef`. When `open` goes from `true` to `false`, it snapshots the current trigger position into `dismissedTriggerRef`, matching the pattern the Escape handler uses (line 225-227). - Added `OutsideClickDismissesTriggerOnRefocus` Storybook regression story that verifies the menu stays closed when clicking back into the editor after an outside-click dismissal. </details> --- *PR generated with Coder Agents*
This commit is contained in:
@@ -219,3 +219,18 @@ export const OutsideClickClosesWithoutReplacing: Story = {
|
||||
expect(editor.textContent).toBe("/");
|
||||
},
|
||||
};
|
||||
|
||||
export const OutsideClickDismissesTriggerOnRefocus: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const editor = await typeInEditor(canvasElement, "/");
|
||||
await findVisibleText("/reviewer");
|
||||
const canvas = within(canvasElement);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Outside target" }),
|
||||
);
|
||||
await expectNoVisibleText("/reviewer");
|
||||
await userEvent.click(editor);
|
||||
await expectNoVisibleText("/reviewer");
|
||||
expect(editor.textContent).toBe("/");
|
||||
},
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
KEY_TAB_COMMAND,
|
||||
type NodeKey,
|
||||
} from "lexical";
|
||||
import { useEffect, useEffectEvent, useRef } from "react";
|
||||
import { useEffect, useEffectEvent, useLayoutEffect, useRef } from "react";
|
||||
import type * as TypesGen from "#/api/typesGenerated";
|
||||
import { parsePersonalSkillTrigger } from "../../utils/personalSkills";
|
||||
import type { CaretAnchorRect } from "./PersonalSkillsTriggerMenu";
|
||||
@@ -121,6 +121,16 @@ export const SkillsTriggerPlugin = ({
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const dismissedTriggerRef = useRef<DismissedSkillsTrigger | null>(null);
|
||||
|
||||
const prevOpenRef = useRef(open);
|
||||
useLayoutEffect(() => {
|
||||
if (prevOpenRef.current && !open) {
|
||||
dismissedTriggerRef.current = editor
|
||||
.getEditorState()
|
||||
.read(() => activeTriggerFromSelection());
|
||||
}
|
||||
prevOpenRef.current = open;
|
||||
}, [open, editor]);
|
||||
|
||||
const refreshTrigger = useEffectEvent(() => {
|
||||
const trigger = editor.getEditorState().read(() => {
|
||||
return editor.isEditable() ? activeTriggerFromSelection() : null;
|
||||
|
||||
Reference in New Issue
Block a user