Skip to content

Commit 8c814a4

Browse files
authored
Merge pull request marmelab#10391 from marmelab/improve-array-input-create-support
Fix: Improve AutocompleteInput creation support
2 parents 66dbf99 + 9ec5765 commit 8c814a4

File tree

4 files changed

+141
-2
lines changed

4 files changed

+141
-2
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
InsideReferenceArrayInput,
1919
InsideReferenceArrayInputOnChange,
2020
OnChange,
21+
OnCreate,
2122
} from './AutocompleteArrayInput.stories';
2223

2324
describe('<AutocompleteArrayInput />', () => {
@@ -812,6 +813,26 @@ describe('<AutocompleteArrayInput />', () => {
812813
expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
813814
});
814815

816+
it('should allow the creation of a new choice by pressing enter', async () => {
817+
render(<OnCreate />);
818+
const input = (await screen.findByLabelText(
819+
'Roles'
820+
)) as HTMLInputElement;
821+
// Enter an unknown value and submit it with Enter
822+
await userEvent.type(input, 'New Value{Enter}');
823+
// AutocompleteArrayInput does not have an input with all values.
824+
// Instead it adds buttons for each values.
825+
await screen.findByText('New Value', { selector: '[role=button] *' });
826+
// Clear the input, otherwise the new value won't be shown in the dropdown as it is selected
827+
fireEvent.change(input, {
828+
target: { value: '' },
829+
});
830+
// Open the dropdown
831+
fireEvent.mouseDown(input);
832+
// Check the new value is in the dropdown
833+
await screen.findByText('New Value');
834+
});
835+
815836
it('should support creation of a new choice through the create element', async () => {
816837
const choices = [
817838
{ id: 'ang', name: 'Angular' },

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

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
useCreate,
77
useRecordContext,
88
TestMemoryRouter,
9+
testDataProvider,
910
} from 'ra-core';
1011
import polyglotI18nProvider from 'ra-i18n-polyglot';
1112
import englishMessages from 'ra-language-english';
@@ -182,6 +183,98 @@ const CreateRole = () => {
182183
);
183184
};
184185

186+
const OnCreateInput = () => {
187+
const [choices, setChoices] = React.useState<
188+
{ id: string; name: string }[]
189+
>([
190+
{ id: 'admin', name: 'Admin' },
191+
{ id: 'u001', name: 'Editor' },
192+
{ id: 'u002', name: 'Moderator' },
193+
{ id: 'u003', name: 'Reviewer' },
194+
]);
195+
return (
196+
<AutocompleteArrayInput
197+
source="roles"
198+
choices={choices}
199+
onCreate={async filter => {
200+
if (!filter) return;
201+
202+
const newOption = {
203+
id: filter,
204+
name: filter,
205+
};
206+
setChoices(options => [...options, newOption]);
207+
// Wait until next tick to give some time for React to update the state
208+
await new Promise(resolve => setTimeout(resolve));
209+
return newOption;
210+
}}
211+
TextFieldProps={{
212+
placeholder: 'Start typing to create a new item',
213+
}}
214+
/>
215+
);
216+
};
217+
218+
export const OnCreate = () => (
219+
<Wrapper>
220+
<OnCreateInput />
221+
</Wrapper>
222+
);
223+
224+
const OnCreateInputStringChoices = () => {
225+
const [choices, setChoices] = React.useState<string[]>([
226+
'Admin',
227+
'Editor',
228+
'Moderator',
229+
'Reviewer',
230+
]);
231+
return (
232+
<AutocompleteArrayInput
233+
source="roles"
234+
choices={choices}
235+
onCreate={async filter => {
236+
if (!filter) return;
237+
238+
const newOption = {
239+
id: filter,
240+
name: filter,
241+
};
242+
setChoices(options => [...options, filter]);
243+
// Wait until next tick to give some time for React to update the state
244+
await new Promise(resolve => setTimeout(resolve));
245+
return newOption;
246+
}}
247+
TextFieldProps={{
248+
placeholder: 'Start typing to create a new item',
249+
}}
250+
/>
251+
);
252+
};
253+
254+
export const OnCreateStringChoices = () => (
255+
<AdminContext
256+
dataProvider={testDataProvider({
257+
// @ts-expect-error
258+
create: async (resource, params) => {
259+
console.log(resource, params);
260+
return params;
261+
},
262+
})}
263+
i18nProvider={i18nProvider}
264+
defaultTheme="light"
265+
>
266+
<Create
267+
resource="posts"
268+
record={{ roles: ['Editor', 'Moderator'] }}
269+
sx={{ width: 600 }}
270+
>
271+
<SimpleForm>
272+
<OnCreateInputStringChoices />
273+
</SimpleForm>
274+
</Create>
275+
</AdminContext>
276+
);
277+
185278
export const CreateProp = () => (
186279
<Wrapper>
187280
<AutocompleteArrayInput

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,23 @@ describe('<AutocompleteInput />', () => {
13381338
fireEvent.focus(input);
13391339
expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
13401340
});
1341+
it('should allow the creation of a new choice by pressing enter', async () => {
1342+
render(<OnCreate />);
1343+
const input = (await screen.findByLabelText(
1344+
'Author'
1345+
)) as HTMLInputElement;
1346+
// Enter an unknown value and submit it with Enter
1347+
await userEvent.type(input, 'New Value{Enter}');
1348+
await screen.getByDisplayValue('New Value');
1349+
// Clear the input, otherwise the new value won't be shown in the dropdown as it is selected
1350+
fireEvent.change(input, {
1351+
target: { value: '' },
1352+
});
1353+
// Open the dropdown
1354+
fireEvent.mouseDown(input);
1355+
// Check the new value is in the dropdown
1356+
await screen.findByText('New Value');
1357+
});
13411358
it('should allow the creation of a new choice with a promise', async () => {
13421359
const choices = [
13431360
{ id: 'ang', name: 'Angular' },

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import isEqual from 'lodash/isEqual';
1414
import clsx from 'clsx';
1515
import {
1616
Autocomplete,
17+
AutocompleteChangeReason,
1718
AutocompleteProps,
1819
Chip,
1920
TextField,
@@ -552,12 +553,19 @@ If you provided a React element for the optionText prop, you must also provide t
552553
};
553554

554555
const handleAutocompleteChange = useCallback(
555-
(event: any, newValue: any, _reason: string) => {
556+
(event: any, newValue: any, reason: AutocompleteChangeReason) => {
557+
// This prevents auto-submitting a form inside a dialog passed to the `create` prop
558+
event.preventDefault();
559+
if (reason === 'createOption') {
560+
// When users press the enter key after typing a new value, we can handle it as if they clicked on the create option
561+
handleChangeWithCreateSupport(getCreateItem(newValue));
562+
return;
563+
}
556564
handleChangeWithCreateSupport(
557565
newValue != null ? newValue : emptyValue
558566
);
559567
},
560-
[emptyValue, handleChangeWithCreateSupport]
568+
[emptyValue, getCreateItem, handleChangeWithCreateSupport]
561569
);
562570

563571
const oneSecondHasPassed = useTimeout(1000, filterValue);

0 commit comments

Comments
 (0)