diff --git a/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx b/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx index b148de9f51..e3b3d69151 100644 --- a/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx +++ b/packages/instantsearch.js/src/components/SearchBox/SearchBox.tsx @@ -27,6 +27,7 @@ type SearchBoxProps = { refine?: (value: string) => void; autofocus?: boolean; searchAsYouType?: boolean; + ignoreCompositionEvents?: boolean; isSearchStalled?: boolean; disabled?: boolean; ariaLabel?: string; @@ -42,6 +43,7 @@ const defaultProps = { showLoadingIndicator: true, autofocus: false, searchAsYouType: true, + ignoreCompositionEvents: false, isSearchStalled: false, disabled: false, ariaLabel: 'Search', @@ -88,8 +90,10 @@ class SearchBox extends Component< const query = (event.target as HTMLInputElement).value; if ( - event.type === 'compositionend' || - !(event as KeyboardEvent).isComposing + !( + this.props.ignoreCompositionEvents && + (event as KeyboardEvent).isComposing + ) ) { if (searchAsYouType) { refine(query); diff --git a/packages/instantsearch.js/src/widgets/search-box/search-box.tsx b/packages/instantsearch.js/src/widgets/search-box/search-box.tsx index 5cbfa28a1c..242bc33013 100644 --- a/packages/instantsearch.js/src/widgets/search-box/search-box.tsx +++ b/packages/instantsearch.js/src/widgets/search-box/search-box.tsx @@ -99,6 +99,12 @@ export type SearchBoxWidgetParams = { * once `` is pressed only. */ searchAsYouType?: boolean; + /** + * Whether to update the search state in the middle of a + * composition session. + * @default false + */ + ignoreCompositionEvents?: boolean; /** * Whether to show the reset button */ @@ -137,6 +143,7 @@ const renderer = templates, autofocus, searchAsYouType, + ignoreCompositionEvents, showReset, showSubmit, showLoadingIndicator, @@ -147,6 +154,7 @@ const renderer = templates: SearchBoxComponentTemplates; autofocus: boolean; searchAsYouType: boolean; + ignoreCompositionEvents: boolean; showReset: boolean; showSubmit: boolean; showLoadingIndicator: boolean; @@ -163,6 +171,7 @@ const renderer = autofocus={autofocus} refine={refine} searchAsYouType={searchAsYouType} + ignoreCompositionEvents={ignoreCompositionEvents} templates={templates} showSubmit={showSubmit} showReset={showReset} @@ -195,6 +204,7 @@ const searchBox: SearchBoxWidget = function searchBox(widgetParams) { cssClasses: userCssClasses = {}, autofocus = false, searchAsYouType = true, + ignoreCompositionEvents = false, showReset = true, showSubmit = true, showLoadingIndicator = true, @@ -242,6 +252,7 @@ const searchBox: SearchBoxWidget = function searchBox(widgetParams) { templates, autofocus, searchAsYouType, + ignoreCompositionEvents, showReset, showSubmit, showLoadingIndicator, diff --git a/packages/react-instantsearch/src/widgets/SearchBox.tsx b/packages/react-instantsearch/src/widgets/SearchBox.tsx index 4976bc4eaa..e18b94ff08 100644 --- a/packages/react-instantsearch/src/widgets/SearchBox.tsx +++ b/packages/react-instantsearch/src/widgets/SearchBox.tsx @@ -28,12 +28,19 @@ export type SearchBoxProps = Omit< * @default true */ searchAsYouType?: boolean; + /** + * Whether to update the search state in the middle of a + * composition session. + * @default false + */ + ignoreCompositionEvents?: boolean; translations?: Partial; }; export function SearchBox({ queryHook, searchAsYouType = true, + ignoreCompositionEvents = false, translations, ...props }: SearchBoxProps) { @@ -44,10 +51,10 @@ export function SearchBox({ const [inputValue, setInputValue] = useState(query); const inputRef = useRef(null); - function setQuery(newQuery: string, compositionComplete = true) { + function setQuery(newQuery: string, isComposing = false) { setInputValue(newQuery); - if (searchAsYouType && compositionComplete) { + if (searchAsYouType && !(ignoreCompositionEvents && isComposing)) { refine(newQuery); } } @@ -63,11 +70,10 @@ export function SearchBox({ function onChange( event: Parameters>[0] ) { - const compositionComplete = - event.type === 'compositionend' || - !(event.nativeEvent as KeyboardEvent).isComposing; - - setQuery(event.currentTarget.value, compositionComplete); + setQuery( + event.currentTarget.value, + (event.nativeEvent as KeyboardEvent).isComposing + ); } function onSubmit(event: React.FormEvent) { diff --git a/packages/vue-instantsearch/src/components/SearchBox.vue b/packages/vue-instantsearch/src/components/SearchBox.vue index f8101a12fb..0467af0282 100644 --- a/packages/vue-instantsearch/src/components/SearchBox.vue +++ b/packages/vue-instantsearch/src/components/SearchBox.vue @@ -13,6 +13,7 @@ :autofocus="autofocus" :show-loading-indicator="showLoadingIndicator" :should-show-loading-indicator="state.isSearchStalled" + :ignore-composition-events="ignoreCompositionEvents" :submit-title="submitTitle" :reset-title="resetTitle" :class-names="classNames" @@ -76,6 +77,10 @@ export default { type: Boolean, default: true, }, + ignoreCompositionEvents: { + type: Boolean, + default: false, + }, submitTitle: { type: String, default: 'Submit the search query', diff --git a/packages/vue-instantsearch/src/components/SearchInput.vue b/packages/vue-instantsearch/src/components/SearchInput.vue index 2b16ddaea9..c241b2d58f 100644 --- a/packages/vue-instantsearch/src/components/SearchInput.vue +++ b/packages/vue-instantsearch/src/components/SearchInput.vue @@ -131,6 +131,10 @@ export default { type: Boolean, default: false, }, + ignoreCompositionEvents: { + type: Boolean, + default: false, + }, submitTitle: { type: String, default: 'Search', @@ -161,7 +165,7 @@ export default { return document.activeElement === this.$refs.input; }, onInput(event) { - if (event.type === 'compositionend' || !event.isComposing) { + if (!(this.ignoreCompositionEvents && event.isComposing)) { this.$emit('input', event.target.value); this.$emit('update:modelValue', event.target.value); } diff --git a/tests/common/widgets/search-box/options.ts b/tests/common/widgets/search-box/options.ts index 51c118fe47..eeb5ac575f 100644 --- a/tests/common/widgets/search-box/options.ts +++ b/tests/common/widgets/search-box/options.ts @@ -308,7 +308,7 @@ export function createOptionsTests( expect(screen.getByRole('searchbox')).toHaveValue('iPhone'); }); - test('refines query only when composition is complete', async () => { + test('does not refine within composition session when `ignoreCompositionEvents: true`', async () => { const searchClient = createSearchClient({}); await setup({ @@ -316,7 +316,9 @@ export function createOptionsTests( indexName: 'indexName', searchClient, }, - widgetParams: {}, + widgetParams: { + ignoreCompositionEvents: true, + }, }); // Typing 木 using the Wubihua input method