diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx index 0df689c7e50..b9850e87719 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx @@ -58,6 +58,43 @@ describe('', () => { expect(screen.queryByDisplayValue('foo')).not.toBeNull(); }); + it('should allow filter to match the selected choice while removing characters in the input', async () => { + render( + + + + + + ); + + const input = screen.getByLabelText( + 'resources.users.fields.role' + ) as HTMLInputElement; + + fireEvent.mouseDown(input); + await waitFor(() => { + expect(screen.getByText('foo')).not.toBe(null); + }); + fireEvent.click(screen.getByText('foo')); + await waitFor(() => { + expect(input.value).toEqual('foo'); + }); + fireEvent.focus(input); + userEvent.type(input, '{end}'); + userEvent.type(input, '2'); + expect(input.value).toEqual('foo2'); + userEvent.type(input, '{backspace}'); + await waitFor(() => { + expect(input.value).toEqual('foo'); + }); + }); + describe('emptyText', () => { it('should allow to have an empty menu option text by passing a string', () => { const emptyText = 'Default'; @@ -449,16 +486,15 @@ describe('', () => { }); }); - it('should allow to clear the first character', async () => { + it('should not match selection when selected choice id equals the emptyValue while changing the input', async () => { render( - + diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index 34a6ce242e0..2c1968d7bc0 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -195,19 +195,31 @@ export const AutocompleteInput = < const finalEmptyText = emptyText ?? ''; - const finalChoices = - isRequiredOverride || multiple - ? allChoices - : [ - { - [optionValue || 'id']: emptyValue, - [typeof optionText === 'string' - ? optionText - : 'name']: translate(finalEmptyText, { - _: finalEmptyText, - }), - }, - ].concat(allChoices); + const finalChoices = useMemo( + () => + isRequiredOverride || multiple + ? allChoices + : [ + { + [optionValue || 'id']: emptyValue, + [typeof optionText === 'string' + ? optionText + : 'name']: translate(finalEmptyText, { + _: finalEmptyText, + }), + }, + ].concat(allChoices), + [ + allChoices, + emptyValue, + finalEmptyText, + isRequiredOverride, + multiple, + optionText, + optionValue, + translate, + ] + ); const { id, @@ -422,15 +434,18 @@ If you provided a React element for the optionText prop, you must also provide t newInputValue: string, reason: string ) => { - if (!doesQueryMatchSelection(newInputValue, event?.type)) { + if ( + event?.type === 'change' || + !doesQueryMatchSelection(newInputValue) + ) { setFilterValue(newInputValue); debouncedSetFilter(newInputValue); } }; const doesQueryMatchSelection = useCallback( - (filter: string, eventType?: string) => { - let selectedItemTexts = []; + (filter: string) => { + let selectedItemTexts; if (multiple) { selectedItemTexts = selectedChoice.map(item => @@ -440,9 +455,7 @@ If you provided a React element for the optionText prop, you must also provide t selectedItemTexts = [getOptionLabel(selectedChoice)]; } - return eventType && eventType === 'change' - ? selectedItemTexts.includes(filter) && selectedChoice - : selectedItemTexts.includes(filter); + return selectedItemTexts.includes(filter); }, [getOptionLabel, multiple, selectedChoice] );