This commit is contained in:
Bruno Bergher 2026-01-10 09:54:47 +00:00
parent 89a94b885a
commit d34f136590
3 changed files with 49 additions and 13 deletions

View file

@ -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<HTMLInputElement>
inputRef?: RefObject<HTMLInputElement>
}
export function SettingsSearchInput({ value, onChange, onFocus, onBlur, onKeyDown }: SettingsSearchInputProps) {
export function SettingsSearchInput({
value,
onChange,
onFocus,
onBlur,
onKeyDown,
inputRef,
}: SettingsSearchInputProps) {
const { t } = useAppTranslation()
return (
<div className="relative flex justify-end ml-2">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-vscode-descriptionForeground pointer-events-none" />
<Input
ref={inputRef}
data-testid="settings-search-input"
type="text"
value={value}
@ -28,8 +38,8 @@ export function SettingsSearchInput({ value, onChange, onFocus, onBlur, onKeyDow
onKeyDown={onKeyDown}
placeholder={t("settings:search.placeholder")}
className={cn(
"pl-6 w-[0px] border-none focus:border-vscode-input-border focus:pl-8 focus:min-w-[130px] focus:w-full",
value && "pr-4 min-w-[150px]",
"pl-6 w-[0px] border-none focus:border-vscode-input-border focus:pl-8 focus:min-w-[132px] focus:w-full",
value && "pr-4 min-w-[148px]",
)}
/>
{value && (

View file

@ -136,6 +136,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
)
const [searchQuery, setSearchQuery] = useState("")
const [isSearchFocused, setIsSearchFocused] = useState(false)
const searchInputRef = useRef<HTMLInputElement | null>(null)
const [highlightedResultId, setHighlightedResultId] = useState<string | undefined>(undefined)
const scrollPositions = useRef<Record<SectionName, number>>(
@ -590,9 +591,11 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setTimeout(() => setIsSearchFocused(false), 200)}
onKeyDown={handleSearchKeyDown}
inputRef={searchInputRef}
/>
{searchQuery && isSearchFocused && (
<div className="absolute top-full w-full min-w-50 right-0 mt-1 bg-vscode-dropdown-background border border-vscode-dropdown-border rounded shadow-lg z-50">

View file

@ -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", () => ({
</button>
),
StandardTooltip: ({ children }: any) => <>{children}</>,
Input: ({ value, onChange, onFocus, onBlur, onKeyDown, "data-testid": dataTestId }: any) => (
<input
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
data-testid={dataTestId}
/>
Input: forwardRef<HTMLInputElement, any>(
({ value, onChange, onFocus, onBlur, onKeyDown, "data-testid": dataTestId }, ref) => (
<input
ref={ref}
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
onKeyDown={onKeyDown}
data-testid={dataTestId}
/>
),
),
AlertDialog: ({ children }: any) => <div>{children}</div>,
AlertDialogContent: ({ children }: any) => <div>{children}</div>,
@ -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(<SettingsView onDone={vi.fn()} />)
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()
})
})