Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix autocomplete sets previous value using onInputChange #9243

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/AutocompleteInput.md
Expand Up @@ -652,7 +652,7 @@ const CompanyInput = () => {
choices={choicesWithCurrentCompany}
optionText="name"
disabled={isLoading}
onInputChange={e => setFilter({ q: e.target.value })}
onInputChange={(_, newInputValue) => setFilter({ q: newInputValue })}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add this prop to the list of props in the 'Props' section, and create a new dedicated section for this prop with an example (similar to the other props)?

/>
);
}
Expand Down
Expand Up @@ -995,7 +995,7 @@ describe('<AutocompleteInput />', () => {
</AdminContext>
);
const input = screen.getByLabelText('resources.users.fields.role');
fireEvent.change(input, { target: { value: 'a' } });
userEvent.type(input, 'a');
await waitFor(() => {
expect(screen.queryAllByRole('option').length).toEqual(2);
});
Expand All @@ -1022,7 +1022,7 @@ describe('<AutocompleteInput />', () => {
</AdminContext>
);
const input = screen.getByLabelText('resources.users.fields.role');
fireEvent.change(input, { target: { value: 'ab' } });
userEvent.type(input, 'ab');
await waitFor(() =>
expect(screen.queryAllByRole('option').length).toEqual(2)
);
Expand Down Expand Up @@ -1376,8 +1376,7 @@ describe('<AutocompleteInput />', () => {
'Author'
)) as HTMLInputElement;
expect(input.value).toBe('Leo Tolstoy');
fireEvent.mouseDown(input);
fireEvent.change(input, { target: { value: 'Leo Tolstoy test' } });
userEvent.type(input, 'Leo Tolstoy test');
// Make sure that 'Leo Tolstoy' did not reappear
let testFailed = false;
try {
Expand Down
25 changes: 25 additions & 0 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx
Expand Up @@ -1240,3 +1240,28 @@ export const WithInputProps = () => {
</Admin>
);
};

export const WithInputOnChange = () => {
const [searchText, setSearchText] = React.useState('');

return (
<AdminContext dataProvider={dataProviderWithAuthors}>
<div>Search text: {searchText}</div>
<SimpleForm onSubmit={() => {}} defaultValues={{ role: 2 }}>
<AutocompleteInput
fullWidth
source="role"
resource="users"
choices={[
{ id: 1, name: 'ab' },
{ id: 2, name: 'abc' },
{ id: 3, name: '123' },
]}
onInputChange={(_, value) => {
setSearchText(value);
}}
/>
</SimpleForm>
</AdminContext>
);
};
68 changes: 41 additions & 27 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Expand Up @@ -111,6 +111,13 @@ const defaultFilterOptions = createFilterOptions();
*
* @example
* <AutocompleteInput source="author_id" options={{ color: 'secondary', InputLabelProps: { shrink: true } }} />
*
* Retrieve the value displayed in the textbox using the `onInputChange` prop:
*
* @example
* const [state, setState] = useState('')
*
* <AutocompleteInput source="gender" choices={choices} onInputChange={(_, newInputValue) => setState(newInputValue)} />
*/
export const AutocompleteInput = <
OptionType extends RaRecord = RaRecord,
Expand Down Expand Up @@ -440,33 +447,14 @@ If you provided a React element for the optionText prop, you must also provide t
useEffect(() => {
if (!multiple) {
const optionLabel = getOptionLabel(selectedChoice);
if (typeof optionLabel === 'string') {
setFilterValue(optionLabel);
Copy link
Contributor Author

@tdnl tdnl Sep 5, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the issue was happening because of this effect.

Removing the call to setFilterValue solves the problem as long as handleInputChange (called by MUI's onInputChange) does it.

I'm keeping the useEffect to throw an error if the user does not provide inputText along with optionText, but it could probably be moved in handleInputChange or finalOnBlur.

} else {
if (typeof optionLabel !== 'string') {
throw new Error(
'When optionText returns a React element, you must also provide the inputText prop'
);
}
}
}, [getOptionLabel, multiple, selectedChoice]);

const handleInputChange: AutocompleteProps<
OptionType,
Multiple,
DisableClearable,
SupportCreate
>['onInputChange'] = (event, newInputValue, reason) => {
if (
event?.type === 'change' ||
!doesQueryMatchSelection(newInputValue)
) {
setFilterValue(newInputValue);
debouncedSetFilter(newInputValue);
}

onInputChange?.(event, newInputValue, reason);
};

const doesQueryMatchSelection = useCallback(
(filter: string) => {
let selectedItemTexts;
Expand Down Expand Up @@ -515,13 +503,39 @@ If you provided a React element for the optionText prop, you must also provide t
return filteredOptions;
};

const handleAutocompleteChange = (
event: any,
newValue: any,
_reason: string
) => {
handleChangeWithCreateSupport(newValue != null ? newValue : emptyValue);
};
const handleAutocompleteChange = useCallback<
AutocompleteProps<
OptionType,
Multiple,
DisableClearable,
SupportCreate
>['onChange']
>(
(_event, newValue, _reason) => {
handleChangeWithCreateSupport(
newValue != null ? newValue : emptyValue
);
},
[emptyValue, handleChangeWithCreateSupport]
);

const handleInputChange = useCallback<
AutocompleteProps<
OptionType,
Multiple,
DisableClearable,
SupportCreate
>['onInputChange']
>(
(event, newInputValue, reason) => {
setFilterValue(newInputValue);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MUI fires onInputChange when the component is mounted. In the previous iteration, event could be null resulting in setFilterValue not being called properly.

if (!doesQueryMatchSelection(newInputValue)) {
debouncedSetFilter(newInputValue);
}
onInputChange?.(event, newInputValue, reason);
},
[debouncedSetFilter, doesQueryMatchSelection, onInputChange]
);

const oneSecondHasPassed = useTimeout(1000, filterValue);

Expand Down