From 2173e90484e2da8ee82d8d43466dedcb186eb05a Mon Sep 17 00:00:00 2001 From: Aymeric Giraudet Date: Thu, 27 Nov 2025 15:22:33 +0100 Subject: [PATCH] fix(autocomplete): clear panel query in instantsearch.js --- .../src/widgets/autocomplete/autocomplete.tsx | 9 +- tests/common/widgets/autocomplete/options.tsx | 109 ++++++++++++++---- 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx index eead06d9ab..a90e803dd6 100644 --- a/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx +++ b/packages/instantsearch.js/src/widgets/autocomplete/autocomplete.tsx @@ -190,7 +190,7 @@ function AutocompleteWrapper({ indices, getSearchPageURL, onSelect: userOnSelect, - refine, + refine: refineAutocomplete, cssClasses, renderState, instantSearchInstance, @@ -223,6 +223,7 @@ function AutocompleteWrapper({ ) ?? false; const onRefine = (query: string) => { + refineAutocomplete(query); instantSearchInstance.setUiState((uiState) => ({ ...uiState, [targetIndex!.getIndexId()]: { @@ -388,9 +389,11 @@ function AutocompleteWrapper({ ...getInputProps(), // @ts-ignore - This clashes with some ambient React JSX declarations. onInput: (evt: JSXPreact.TargetedEvent) => - refine(evt.currentTarget.value), + refineAutocomplete(evt.currentTarget.value), + }} + onClear={() => { + onRefine(''); }} - onClear={() => onRefine('')} isSearchStalled={instantSearchInstance.status === 'stalled'} /> diff --git a/tests/common/widgets/autocomplete/options.tsx b/tests/common/widgets/autocomplete/options.tsx index 7514ad7209..bfd785fcbe 100644 --- a/tests/common/widgets/autocomplete/options.tsx +++ b/tests/common/widgets/autocomplete/options.tsx @@ -8,7 +8,7 @@ import { screen } from '@testing-library/dom'; import userEvent from '@testing-library/user-event'; import type { AutocompleteWidgetSetup } from '.'; -import type { SupportedFlavor, TestOptions } from '../../common'; +import type { TestOptions } from '../../common'; function createMockedSearchClient( response: ReturnType @@ -139,25 +139,15 @@ export function createOptionsTests( await wait(0); }); - const callTimes: Record> = { - initial: { javascript: 2, react: 2, vue: 0 }, - refined: { javascript: 1, react: 2, vue: 0 }, - }; - - expect(searchClient.search).toHaveBeenCalledTimes( - callTimes.initial[flavor] - ); - expect(searchClient.search).toHaveBeenNthCalledWith( - callTimes.initial[flavor] - 1, - [ - { - indexName: 'query_suggestions', - params: expect.objectContaining({ - query: '', - }), - }, - ] - ); + expect(searchClient.search).toHaveBeenCalledTimes(2); + expect(searchClient.search).toHaveBeenNthCalledWith(1, [ + { + indexName: 'query_suggestions', + params: expect.objectContaining({ + query: '', + }), + }, + ]); (searchClient.search as jest.Mock).mockClear(); expect(screen.getAllByRole('row', { hidden: true }).length).toBe(2); @@ -174,9 +164,7 @@ export function createOptionsTests( await wait(0); }); - expect(searchClient.search).toHaveBeenCalledTimes( - callTimes.refined[flavor] - ); + expect(searchClient.search).toHaveBeenCalledTimes(2); expect(searchClient.search).toHaveBeenLastCalledWith([ { indexName: 'query_suggestions', @@ -788,6 +776,81 @@ export function createOptionsTests( expect(input).toHaveFocus(); }); + test('clearing the query also clears internal autocomplete query', async () => { + const searchClient = createMockedSearchClient( + createMultiSearchResponse( + createSingleSearchResponse({ + index: 'indexName', + hits: [ + { objectID: '1', name: 'Item 1' }, + { objectID: '2', name: 'Item 2' }, + ], + }) + ) + ); + + await setup({ + instantSearchOptions: { + indexName: 'indexName', + searchClient, + }, + widgetParams: { + javascript: { + indices: [ + { + indexName: 'indexName', + templates: { + item: (props) => props.item.name, + }, + }, + ], + }, + react: { + indices: [ + { + indexName: 'indexName', + itemComponent: (props) => props.item.name, + }, + ], + }, + vue: {}, + }, + }); + + await act(async () => { + await wait(0); + }); + + const input = screen.getByRole('combobox', { name: /submit/i }); + + await act(async () => { + userEvent.click(input); + userEvent.type(input, 'Item 3'); + userEvent.keyboard('{Enter}'); + await wait(0); + userEvent.keyboard('{Enter}'); + await wait(0); + }); + + expect(input).not.toHaveFocus(); + expect(input).toHaveAttribute('aria-expanded', 'false'); + expect(screen.getByRole('button', { name: /clear/i })).toBeVisible(); + + await act(async () => { + userEvent.click(screen.getByRole('button', { name: /clear/i })); + await wait(0); + }); + + expect(searchClient.search).toHaveBeenLastCalledWith([ + { + indexName: 'indexName', + params: expect.objectContaining({ + query: '', + }), + }, + ]); + }); + test('refocuses the input after clearing the query', async () => { const searchClient = createMockedSearchClient( createMultiSearchResponse(