Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
InsideReferenceArrayInput,
InsideReferenceArrayInputOnChange,
OnChange,
OnCreate,
} from './AutocompleteArrayInput.stories';

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

it('should allow the creation of a new choice by pressing enter', async () => {
render(<OnCreate />);
const input = (await screen.findByLabelText(
'Roles'
)) as HTMLInputElement;
// Enter an unknown value and submit it with Enter
await userEvent.type(input, 'New Value{Enter}');
// AutocompleteArrayInput does not have an input with all values.
// Instead it adds buttons for each values.
await screen.findByText('New Value', { selector: '[role=button] *' });
// Clear the input, otherwise the new value won't be shown in the dropdown as it is selected
fireEvent.change(input, {
target: { value: '' },
});
// Open the dropdown
fireEvent.mouseDown(input);
// Check the new value is in the dropdown
await screen.findByText('New Value');
});

it('should support creation of a new choice through the create element', async () => {
const choices = [
{ id: 'ang', name: 'Angular' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
useCreate,
useRecordContext,
TestMemoryRouter,
testDataProvider,
} from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
Expand Down Expand Up @@ -182,6 +183,98 @@ const CreateRole = () => {
);
};

const OnCreateInput = () => {
const [choices, setChoices] = React.useState<
{ id: string; name: string }[]
>([
{ id: 'admin', name: 'Admin' },
{ id: 'u001', name: 'Editor' },
{ id: 'u002', name: 'Moderator' },
{ id: 'u003', name: 'Reviewer' },
]);
return (
<AutocompleteArrayInput
source="roles"
choices={choices}
onCreate={async filter => {
if (!filter) return;

const newOption = {
id: filter,
name: filter,
};
setChoices(options => [...options, newOption]);
// Wait until next tick to give some time for React to update the state
await new Promise(resolve => setTimeout(resolve));
return newOption;
}}
TextFieldProps={{
placeholder: 'Start typing to create a new item',
}}
/>
);
};

export const OnCreate = () => (
<Wrapper>
<OnCreateInput />
</Wrapper>
);

const OnCreateInputStringChoices = () => {
const [choices, setChoices] = React.useState<string[]>([
'Admin',
'Editor',
'Moderator',
'Reviewer',
]);
return (
<AutocompleteArrayInput
source="roles"
choices={choices}
onCreate={async filter => {
if (!filter) return;

const newOption = {
id: filter,
name: filter,
};
setChoices(options => [...options, filter]);
// Wait until next tick to give some time for React to update the state
await new Promise(resolve => setTimeout(resolve));
return newOption;
}}
TextFieldProps={{
placeholder: 'Start typing to create a new item',
}}
/>
);
};

export const OnCreateStringChoices = () => (
<AdminContext
dataProvider={testDataProvider({
// @ts-expect-error
create: async (resource, params) => {
console.log(resource, params);
return params;
},
})}
i18nProvider={i18nProvider}
defaultTheme="light"
>
<Create
resource="posts"
record={{ roles: ['Editor', 'Moderator'] }}
sx={{ width: 600 }}
>
<SimpleForm>
<OnCreateInputStringChoices />
</SimpleForm>
</Create>
</AdminContext>
);

export const CreateProp = () => (
<Wrapper>
<AutocompleteArrayInput
Expand Down
17 changes: 17 additions & 0 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1338,6 +1338,23 @@ describe('<AutocompleteInput />', () => {
fireEvent.focus(input);
expect(screen.queryByText('New Kid On The Block')).not.toBeNull();
});
it('should allow the creation of a new choice by pressing enter', async () => {
render(<OnCreate />);
const input = (await screen.findByLabelText(
'Author'
)) as HTMLInputElement;
// Enter an unknown value and submit it with Enter
await userEvent.type(input, 'New Value{Enter}');
await screen.getByDisplayValue('New Value');
// Clear the input, otherwise the new value won't be shown in the dropdown as it is selected
fireEvent.change(input, {
target: { value: '' },
});
// Open the dropdown
fireEvent.mouseDown(input);
// Check the new value is in the dropdown
await screen.findByText('New Value');
});
it('should allow the creation of a new choice with a promise', async () => {
const choices = [
{ id: 'ang', name: 'Angular' },
Expand Down
12 changes: 10 additions & 2 deletions packages/ra-ui-materialui/src/input/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import isEqual from 'lodash/isEqual';
import clsx from 'clsx';
import {
Autocomplete,
AutocompleteChangeReason,
AutocompleteProps,
Chip,
TextField,
Expand Down Expand Up @@ -552,12 +553,19 @@ If you provided a React element for the optionText prop, you must also provide t
};

const handleAutocompleteChange = useCallback(
(event: any, newValue: any, _reason: string) => {
(event: any, newValue: any, reason: AutocompleteChangeReason) => {
// This prevents auto-submitting a form inside a dialog passed to the `create` prop
event.preventDefault();
if (reason === 'createOption') {
// When users press the enter key after typing a new value, we can handle it as if they clicked on the create option
handleChangeWithCreateSupport(getCreateItem(newValue));
return;
}
handleChangeWithCreateSupport(
newValue != null ? newValue : emptyValue
);
},
[emptyValue, handleChangeWithCreateSupport]
[emptyValue, getCreateItem, handleChangeWithCreateSupport]
);

const oneSecondHasPassed = useTimeout(1000, filterValue);
Expand Down
Loading