From 45529bca740d0612e32ebb0d1f0130d7cdf6e290 Mon Sep 17 00:00:00 2001 From: Dotta Date: Mon, 27 Apr 2026 15:27:27 -0500 Subject: [PATCH] Fix assignee selector arrow navigation --- .../components/InlineEntitySelector.test.tsx | 80 +++++++++++++++++++ ui/src/components/InlineEntitySelector.tsx | 28 +++++-- 2 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 ui/src/components/InlineEntitySelector.test.tsx diff --git a/ui/src/components/InlineEntitySelector.test.tsx b/ui/src/components/InlineEntitySelector.test.tsx new file mode 100644 index 00000000000..f127f41c330 --- /dev/null +++ b/ui/src/components/InlineEntitySelector.test.tsx @@ -0,0 +1,80 @@ +// @vitest-environment jsdom + +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { InlineEntitySelector } from "./InlineEntitySelector"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; + +describe("InlineEntitySelector", () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + document.body.innerHTML = ""; + }); + + it("keeps handled search navigation keys inside the popover", async () => { + const root = createRoot(container); + const onChange = vi.fn(); + const documentKeyDown = vi.fn(); + document.addEventListener("keydown", documentKeyDown); + + act(() => { + root.render( + , + ); + }); + + const trigger = container.querySelector("button") as HTMLButtonElement | null; + expect(trigger).not.toBeNull(); + + await act(async () => { + trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + + const searchInput = document.querySelector('input[placeholder="Search assignees..."]') as HTMLInputElement | null; + expect(searchInput).not.toBeNull(); + searchInput?.focus(); + + await act(async () => { + await Promise.resolve(); + }); + + await act(async () => { + searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "ArrowDown" })); + }); + + expect(documentKeyDown).not.toHaveBeenCalled(); + + await act(async () => { + searchInput?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })); + }); + + expect(documentKeyDown).not.toHaveBeenCalled(); + expect(onChange).toHaveBeenCalledWith("agent:agent-1"); + + document.removeEventListener("keydown", documentKeyDown); + act(() => { + root.unmount(); + }); + }); +}); diff --git a/ui/src/components/InlineEntitySelector.tsx b/ui/src/components/InlineEntitySelector.tsx index 6bbcf64940f..cae1897712e 100644 --- a/ui/src/components/InlineEntitySelector.tsx +++ b/ui/src/components/InlineEntitySelector.tsx @@ -29,6 +29,8 @@ interface InlineEntitySelectorProps { openOnFocus?: boolean; } +const EMPTY_RECENT_OPTION_IDS: string[] = []; + export const InlineEntitySelector = forwardRef( function InlineEntitySelector( { @@ -43,7 +45,7 @@ export const InlineEntitySelector = forwardRef(null); const shouldPreventCloseAutoFocusRef = useRef(false); const isPointerDownRef = useRef(false); @@ -72,10 +75,16 @@ export const InlineEntitySelector = forwardRef option.id === value) ?? null; + function setHighlightedIndexValue(next: number | ((current: number) => number)) { + const resolved = typeof next === "function" ? next(highlightedIndexRef.current) : next; + highlightedIndexRef.current = resolved; + setHighlightedIndex(resolved); + } + useEffect(() => { if (!open) return; const selectedIndex = filteredOptions.findIndex((option) => option.id === value); - setHighlightedIndex(selectedIndex >= 0 ? selectedIndex : 0); + setHighlightedIndexValue(selectedIndex >= 0 ? selectedIndex : 0); }, [filteredOptions, open, value]); const commitSelection = (index: number, moveNext: boolean) => { @@ -153,14 +162,16 @@ export const InlineEntitySelector = forwardRef { if (event.key === "ArrowDown") { event.preventDefault(); - setHighlightedIndex((current) => + event.stopPropagation(); + setHighlightedIndexValue((current) => filteredOptions.length === 0 ? 0 : (current + 1) % filteredOptions.length, ); return; } if (event.key === "ArrowUp") { event.preventDefault(); - setHighlightedIndex((current) => { + event.stopPropagation(); + setHighlightedIndexValue((current) => { if (filteredOptions.length === 0) return 0; return current <= 0 ? filteredOptions.length - 1 : current - 1; }); @@ -168,16 +179,19 @@ export const InlineEntitySelector = forwardRef setHighlightedIndex(index)} + onMouseEnter={() => setHighlightedIndexValue(index)} onClick={() => commitSelection(index, true)} > {renderOption ? renderOption(option, isSelected) : {option.label}}