diff --git a/webview-ui/src/components/settings/SettingsSearchInput.tsx b/webview-ui/src/components/settings/SettingsSearchInput.tsx index c0b7165470..5f27128612 100644 --- a/webview-ui/src/components/settings/SettingsSearchInput.tsx +++ b/webview-ui/src/components/settings/SettingsSearchInput.tsx @@ -1,3 +1,4 @@ +import { type RefObject } from "react" import { Search, X } from "lucide-react" import { cn } from "@/lib/utils" @@ -10,15 +11,24 @@ export interface SettingsSearchInputProps { onFocus?: () => void onBlur?: () => void onKeyDown?: React.KeyboardEventHandler + inputRef?: RefObject } -export function SettingsSearchInput({ value, onChange, onFocus, onBlur, onKeyDown }: SettingsSearchInputProps) { +export function SettingsSearchInput({ + value, + onChange, + onFocus, + onBlur, + onKeyDown, + inputRef, +}: SettingsSearchInputProps) { const { t } = useAppTranslation() return (
{value && ( diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 1f9b7d32b4..ded37de756 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -136,6 +136,7 @@ const SettingsView = forwardRef(({ onDone, t ) const [searchQuery, setSearchQuery] = useState("") const [isSearchFocused, setIsSearchFocused] = useState(false) + const searchInputRef = useRef(null) const [highlightedResultId, setHighlightedResultId] = useState(undefined) const scrollPositions = useRef>( @@ -590,9 +591,11 @@ const SettingsView = forwardRef(({ onDone, t const handleSelectResult = useCallback( (result: SearchResult) => { setSearchQuery("") - setIsSearchFocused(false) setHighlightedResultId(undefined) handleTabChange(result.tab) + // Keep focus in the input so dropdown remains open for follow-up search + setIsSearchFocused(true) + requestAnimationFrame(() => searchInputRef.current?.focus()) // Small delay to allow tab switch and render setTimeout(() => scrollToSetting(result.id), 150) }, @@ -683,6 +686,7 @@ const SettingsView = forwardRef(({ onDone, t onFocus={() => setIsSearchFocused(true)} onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)} onKeyDown={handleSearchKeyDown} + inputRef={searchInputRef} /> {searchQuery && isSearchFocused && (
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.search.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.search.spec.tsx index 70cdac05c6..d2a7156f96 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.search.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.search.spec.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from "react" import { fireEvent, render, screen } from "@testing-library/react" import { vi, describe, it, beforeEach } from "vitest" @@ -140,15 +141,18 @@ vi.mock("@/components/ui", () => ({ ), StandardTooltip: ({ children }: any) => <>{children}, - Input: ({ value, onChange, onFocus, onBlur, onKeyDown, "data-testid": dataTestId }: any) => ( - + Input: forwardRef( + ({ value, onChange, onFocus, onBlur, onKeyDown, "data-testid": dataTestId }, ref) => ( + + ), ), AlertDialog: ({ children }: any) =>
{children}
, AlertDialogContent: ({ children }: any) =>
{children}
, @@ -242,4 +246,22 @@ describe("SettingsView search interactions", () => { expect(screen.queryByRole("listbox")).not.toBeInTheDocument() expect(input.value).toBe("") }) + + it("keeps input focused after selecting a result to allow immediate follow-up search", async () => { + render() + + const input = screen.getByTestId("settings-search-input") as HTMLInputElement + fireEvent.focus(input) + fireEvent.change(input, { target: { value: "browser" } }) + + await screen.findByRole("listbox") + const options = screen.getAllByRole("option") + fireEvent.mouseDown(options[0]) + fireEvent.click(options[0]) + + // Second search still produces results + fireEvent.change(input, { target: { value: "browser" } }) + const secondListbox = await screen.findByRole("listbox") + expect(secondListbox).toBeInTheDocument() + }) })