Skip to content

Commit 9be13cb

Browse files
Fix(AutocompleteInput): do not open autocomplete options when shouldRenderSuggestions returns false
1 parent ba76aad commit 9be13cb

3 files changed

Lines changed: 93 additions & 27 deletions

File tree

packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -708,17 +708,19 @@ describe('<AutocompleteInput />', () => {
708708
});
709709
});
710710

711-
it('should respect shouldRenderSuggestions over default if passed in', async () => {
711+
it('should not open the suggestions popper when shouldRenderSuggestions returns false', async () => {
712712
render(
713713
<AdminContext dataProvider={testDataProvider()}>
714714
<ResourceContextProvider value="posts">
715715
<SimpleForm
716716
onSubmit={jest.fn()}
717-
defaultValues={{ role: 2 }}
717+
defaultValues={{ role: null }}
718718
>
719719
<AutocompleteInput
720720
{...defaultProps}
721-
shouldRenderSuggestions={() => false}
721+
shouldRenderSuggestions={(v: string) =>
722+
v.length >= 2
723+
}
722724
noOptionsText="No options"
723725
choices={[
724726
{ id: 1, name: 'bar' },
@@ -730,11 +732,21 @@ describe('<AutocompleteInput />', () => {
730732
</AdminContext>
731733
);
732734

733-
const input = screen.getByLabelText('resources.users.fields.role');
735+
const input = screen.getByLabelText(
736+
'resources.users.fields.role'
737+
) as HTMLInputElement;
734738
fireEvent.focus(input);
739+
740+
// popper must NOT open while shouldRenderSuggestions returns false
741+
expect(screen.queryByRole('listbox')).toBeNull();
742+
expect(screen.queryByText('No options')).toBeNull();
743+
744+
// typing 2+ chars satisfies the predicate -> popper opens
745+
fireEvent.change(input, { target: { value: 'fo' } });
735746
await waitFor(() => {
736-
expect(screen.queryByText('foo')).toBeNull();
747+
expect(screen.queryByRole('listbox')).not.toBeNull();
737748
});
749+
expect(screen.queryByText('foo')).not.toBeNull();
738750
});
739751

740752
it('should not fail when value is null and new choices are applied', () => {

packages/ra-ui-materialui/src/input/AutocompleteInput.stories.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,39 @@ export const InsideReferenceInputWithDisableChoice = () => (
878878
</TestMemoryRouter>
879879
);
880880

881+
const enableGetChoicesMinTwoChars = (filters: any) => filters?.q?.length >= 2;
882+
const shouldRenderSuggestionsMinTwoChars = (v: string) => v.length >= 2;
883+
884+
export const InsideReferenceInputWithShouldRenderSuggestions = () => (
885+
<TestMemoryRouter initialEntries={['/books/1']}>
886+
<Admin dataProvider={dataProviderWithAuthors}>
887+
<Resource name="authors" />
888+
<Resource
889+
name="books"
890+
edit={() => (
891+
<Edit mutationMode="pessimistic">
892+
<SimpleForm>
893+
<ReferenceInput
894+
reference="authors"
895+
source="author"
896+
enableGetChoices={enableGetChoicesMinTwoChars}
897+
>
898+
<AutocompleteInput
899+
optionText="name"
900+
noOptionsText="Type 2+ chars to search"
901+
shouldRenderSuggestions={
902+
shouldRenderSuggestionsMinTwoChars
903+
}
904+
/>
905+
</ReferenceInput>
906+
</SimpleForm>
907+
</Edit>
908+
)}
909+
/>
910+
</Admin>
911+
</TestMemoryRouter>
912+
);
913+
881914
const LanguageChangingAuthorInput = ({ onChange }) => {
882915
const { setValue } = useFormContext();
883916
const handleChange = (value, record) => {

packages/ra-ui-materialui/src/input/AutocompleteInput.tsx

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,53 @@
1-
import * as React from 'react';
2-
import {
3-
isValidElement,
4-
useCallback,
5-
useEffect,
6-
useMemo,
7-
useRef,
8-
useState,
9-
type ReactNode,
10-
} from 'react';
11-
import debounce from 'lodash/debounce.js';
12-
import get from 'lodash/get.js';
13-
import isEqual from 'lodash/isEqual.js';
14-
import clsx from 'clsx';
151
import {
162
Autocomplete,
173
type AutocompleteChangeReason,
4+
type AutocompleteCloseReason,
185
type AutocompleteProps,
196
Chip,
7+
createFilterOptions,
208
TextField,
219
type TextFieldProps,
22-
createFilterOptions,
2310
useForkRef,
2411
} from '@mui/material';
2512
import {
2613
type ComponentsOverrides,
2714
styled,
2815
useThemeProps,
2916
} from '@mui/material/styles';
17+
import clsx from 'clsx';
18+
import debounce from 'lodash/debounce.js';
19+
import get from 'lodash/get.js';
20+
import isEqual from 'lodash/isEqual.js';
3021
import {
3122
type ChoicesProps,
3223
FieldTitle,
3324
type RaRecord,
25+
type SupportCreateSuggestionOptions,
3426
useChoicesContext,
27+
useEvent,
28+
useGetRecordRepresentation,
3529
useInput,
3630
useSuggestions,
3731
type UseSuggestionsOptions,
32+
useSupportCreateSuggestion,
3833
useTimeout,
3934
useTranslate,
4035
warning,
41-
useGetRecordRepresentation,
42-
useEvent,
43-
type SupportCreateSuggestionOptions,
44-
useSupportCreateSuggestion,
4536
} from 'ra-core';
37+
import * as React from 'react';
38+
import {
39+
isValidElement,
40+
type ReactNode,
41+
useCallback,
42+
useEffect,
43+
useMemo,
44+
useRef,
45+
useState,
46+
} from 'react';
47+
import { Offline } from '../Offline';
4648
import type { CommonInputProps } from './CommonInputProps';
4749
import { InputHelperText } from './InputHelperText';
4850
import { sanitizeInputRestProps } from './sanitizeInputRestProps';
49-
import { Offline } from '../Offline';
5051

5152
const defaultFilterOptions = createFilterOptions();
5253

@@ -171,7 +172,9 @@ export const AutocompleteInput = <
171172
offline = defaultOffline,
172173
onBlur,
173174
onChange,
175+
onClose: onCloseProp,
174176
onCreate,
177+
onOpen: onOpenProp,
175178
openText = 'ra.action.open',
176179
optionText,
177180
optionValue,
@@ -338,6 +341,22 @@ If you provided a React element for the optionText prop, you must also provide t
338341

339342
const [filterValue, setFilterValue] = useState('');
340343

344+
const [isOpen, setIsOpen] = useState(false);
345+
const canRenderSuggestions =
346+
!shouldRenderSuggestions || shouldRenderSuggestions(filterValue);
347+
348+
const handleOpen = useEvent((event: React.SyntheticEvent) => {
349+
setIsOpen(true);
350+
onOpenProp?.(event);
351+
});
352+
353+
const handleClose = useEvent(
354+
(event: React.SyntheticEvent, reason: AutocompleteCloseReason) => {
355+
setIsOpen(false);
356+
onCloseProp?.(event, reason);
357+
}
358+
);
359+
341360
const handleChange = useEvent((newValue: any) => {
342361
if (multiple) {
343362
if (Array.isArray(newValue)) {
@@ -755,13 +774,15 @@ If you provided a React element for the optionText prop, you must also provide t
755774
clearOnBlur={clearOnBlur}
756775
{...sanitizeInputRestProps(rest)}
757776
freeSolo={!!create || !!onCreate}
777+
open={isOpen && canRenderSuggestions}
778+
onOpen={handleOpen}
779+
onClose={handleClose}
758780
handleHomeEndKeys={!!create || !!onCreate}
759781
filterOptions={filterOptions}
760782
options={
761783
isPaused && isPlaceholderData
762784
? []
763-
: shouldRenderSuggestions == undefined || // eslint-disable-line eqeqeq
764-
shouldRenderSuggestions(filterValue)
785+
: canRenderSuggestions
765786
? suggestions
766787
: []
767788
}

0 commit comments

Comments
 (0)