diff --git a/packages/dashboard/app/components/CreateRoomModal.tsx b/packages/dashboard/app/components/CreateRoomModal.tsx index db8e8b95ea..e4a82e41f4 100644 --- a/packages/dashboard/app/components/CreateRoomModal.tsx +++ b/packages/dashboard/app/components/CreateRoomModal.tsx @@ -49,6 +49,7 @@ export function CreateRoomModal({ isOpen, onClose, onCreate, projectId, existing const [submitError, setSubmitError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const nameInputRef = useRef(null); + const modalRef = useRef(null); const previousFocusRef = useRef(null); const agentLoadEpochRef = useRef(0); /* @@ -104,7 +105,16 @@ export function CreateRoomModal({ isOpen, onClose, onCreate, projectId, existing setIsSubmitting(false); return; } - const frame = window.requestAnimationFrame(() => nameInputRef.current?.focus()); + /* + FNXC:CreateRoomModal 2026-08-16-09:20: + A late open-time animation frame must not yank focus from a field the user already selected + inside this dialog. Otherwise member-search keystrokes can be swallowed by the room-name field. + */ + const frame = window.requestAnimationFrame(() => { + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement && modalRef.current?.contains(activeElement)) return; + nameInputRef.current?.focus(); + }); return () => window.cancelAnimationFrame(frame); }, [isOpen]); @@ -198,7 +208,7 @@ export function CreateRoomModal({ isOpen, onClose, onCreate, projectId, existing closeOnOutsidePointerDown layer="utility" > -
+

{t("createRoom.title", "Create room")}

diff --git a/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx b/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx index bc7aa7d88e..5bebf67bab 100644 --- a/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/CreateRoomModal.test.tsx @@ -197,6 +197,32 @@ describe("CreateRoomModal", () => { await waitFor(() => expect(screen.getByRole("button", { name: "Room launcher" })).toHaveFocus()); }); + it("does not let deferred autofocus steal member-search keystrokes", async () => { + let capturedFrameCallback: FrameRequestCallback | undefined; + const requestAnimationFrame = vi.spyOn(window, "requestAnimationFrame").mockImplementation((callback) => { + capturedFrameCallback = callback; + return 1; + }); + + try { + render(); + await screen.findByRole("button", { name: /Alpha/i }); + + await userEvent.click(screen.getByLabelText("Members")); + const searchInput = screen.getByLabelText("Members"); + expect(searchInput).toHaveFocus(); + expect(capturedFrameCallback).toBeDefined(); + act(() => { capturedFrameCallback?.(0); }); + await userEvent.keyboard("zzz"); + + expect(searchInput).toHaveValue("zzz"); + expect(screen.getByLabelText("Room name")).toHaveValue(""); + expect(screen.getByText("No agents match your search.")).toBeInTheDocument(); + } finally { + requestAnimationFrame.mockRestore(); + } + }); + it.each(["desktop", "mobile"])("shows loading, empty, no-match, populated, and selected-member picker states on %s", async (viewport) => { Object.defineProperty(window, "innerWidth", { configurable: true, value: viewport === "mobile" ? 375 : 1280 }); const loading = createDeferred(); @@ -225,10 +251,11 @@ describe("CreateRoomModal", () => { await waitFor(() => expect(mockFetchAgents).toHaveBeenCalledTimes(3)); await act(async () => { populated.resolve(agents); }); expect(await screen.findByRole("button", { name: /Alpha/i })).toBeInTheDocument(); - fireEvent.change(screen.getByLabelText("Members"), { target: { value: "zzz" } }); + const memberSearch = screen.getByLabelText("Members"); + await userEvent.type(memberSearch, "zzz"); expect(screen.getByText("No agents match your search.")).toBeInTheDocument(); expect(screen.queryByText("No agents in this project yet.")).not.toBeInTheDocument(); - fireEvent.change(screen.getByLabelText("Members"), { target: { value: "" } }); + await userEvent.clear(memberSearch); await userEvent.click(screen.getByRole("button", { name: /Alpha/i })); expect(screen.getByTestId("create-room-selected-chips")).toHaveTextContent("Alpha"); expect(mockFetchAgents).toHaveBeenCalledTimes(3); @@ -289,7 +316,7 @@ describe("CreateRoomModal", () => { render(); await screen.findByRole("button", { name: /Alpha/i }); - fireEvent.change(screen.getByLabelText("Members"), { target: { value: "zzz" } }); + await userEvent.type(screen.getByLabelText("Members"), "zzz"); expect(screen.getByText("No agents match your search.")).toBeInTheDocument(); });