Skip to content

Commit 67745ee

Browse files
committed
Skip 400ms search throttle for in-memory autocomplete filtering
1 parent 7078668 commit 67745ee

4 files changed

Lines changed: 50 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/cli-kit': patch
3+
---
4+
5+
Make the autocomplete prompt feel instant when filtering against in-memory choices. The 400ms throttle on the search callback was designed for remote/paginated backends, but it also gated the default in-memory filter used by callers that don't supply their own `search` (e.g. the theme selector), producing a noticeable lag while typing. The prompt now exposes a `searchDebounceMs` prop, and `renderAutocompletePrompt` sets it to `0` when it injects its own synchronous filter. Custom remote-search consumers keep the existing 400ms throttle unless they opt out.

packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,34 @@ describe('AutocompletePrompt', async () => {
620620
`)
621621
})
622622

623+
test('searchDebounceMs: 0 invokes search on every keystroke without throttling', async () => {
624+
const search = vi.fn(async (term: string) => ({
625+
data: DATABASE.filter((item) => item.label.includes(term)),
626+
}))
627+
628+
const renderInstance = render(
629+
<AutocompletePrompt
630+
message="Associate your project with the org Castile Ventures?"
631+
choices={DATABASE}
632+
onSubmit={() => {}}
633+
search={search}
634+
searchDebounceMs={0}
635+
/>,
636+
)
637+
638+
await waitForInputsToBeReady()
639+
await sendInputAndWaitForChange(renderInstance, 'f')
640+
await sendInputAndWaitForChange(renderInstance, 'i')
641+
await sendInputAndWaitForChange(renderInstance, 'r')
642+
643+
// With the default 400ms throttle, three rapid keystrokes coalesce to ~2 calls
644+
// (leading + trailing edge). With searchDebounceMs=0, each keystroke fires.
645+
expect(search).toHaveBeenCalledTimes(3)
646+
expect(search).toHaveBeenNthCalledWith(1, 'f')
647+
expect(search).toHaveBeenNthCalledWith(2, 'fi')
648+
expect(search).toHaveBeenNthCalledWith(3, 'fir')
649+
})
650+
623651
test('displays an error message if the search fails', async () => {
624652
const search = (_term: string) => {
625653
return Promise.reject(new Error('Something went wrong'))

packages/cli-kit/src/private/node/ui/components/AutocompletePrompt.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,17 @@ export interface AutocompletePromptProps<T> {
2828
abortSignal?: AbortSignal
2929
infoMessage?: InfoMessageProps['message']
3030
groupOrder?: string[]
31+
/**
32+
* Throttle window in milliseconds applied to the search callback. Defaults to 400ms,
33+
* which is appropriate for remote/paginated backends. In-memory consumers (where the
34+
* search callback resolves synchronously) can pass 0 for instant filtering on every
35+
* keystroke.
36+
*/
37+
searchDebounceMs?: number
3138
}
3239

3340
const MIN_NUMBER_OF_ITEMS_FOR_SEARCH = 5
41+
const DEFAULT_SEARCH_DEBOUNCE_MS = 400
3442

3543
function AutocompletePrompt<T>({
3644
message,
@@ -42,6 +50,7 @@ function AutocompletePrompt<T>({
4250
abortSignal,
4351
infoMessage,
4452
groupOrder,
53+
searchDebounceMs = DEFAULT_SEARCH_DEBOUNCE_MS,
4554
}: React.PropsWithChildren<AutocompletePromptProps<T>>): ReactElement | null {
4655
const complete = useComplete()
4756
const [searchTerm, setSearchTerm] = useState('')
@@ -121,10 +130,10 @@ function AutocompletePrompt<T>({
121130
clearTimeout(setLoadingWhenSlow.current)
122131
})
123132
},
124-
400,
133+
searchDebounceMs,
125134
{leading: true, trailing: true},
126135
),
127-
[paginatedSearch, setPromptState],
136+
[paginatedSearch, setPromptState, searchDebounceMs],
128137
)
129138

130139
return (

packages/cli-kit/src/public/node/ui.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,11 @@ export async function renderAutocompletePrompt<T>(
417417
): Promise<T> {
418418
throwInNonTTY({message: props.message, stdin: renderOptions?.stdin}, uiDebugOptions)
419419

420+
// The default search filters in-memory choices synchronously, so it doesn't need
421+
// throttling. Skipping the throttle makes the keystroke-to-result latency feel
422+
// instant. Callers that supply their own (typically remote/paginated) search keep
423+
// the component's default throttle unless they opt out via `searchDebounceMs`.
424+
const usingDefaultSearch = props.search === undefined
420425
const newProps = {
421426
search(term: string) {
422427
const lowerTerm = term.toLowerCase()
@@ -426,6 +431,7 @@ export async function renderAutocompletePrompt<T>(
426431
}),
427432
})
428433
},
434+
...(usingDefaultSearch ? {searchDebounceMs: 0} : {}),
429435
...props,
430436
}
431437

0 commit comments

Comments
 (0)