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

Add ability to remove empty option in SelectInput for required fields #8039

Merged
merged 1 commit into from
Aug 5, 2022
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
73 changes: 50 additions & 23 deletions packages/ra-ui-materialui/src/input/SelectInput.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
import * as React from 'react';
import {
findByText,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import {
required,
testDataProvider,
Expand Down Expand Up @@ -73,7 +67,7 @@ describe('<SelectInput />', () => {
).toEqual('rea');
});

it('should render disable choices marked so', () => {
it('should render disabled choices marked so', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
Expand All @@ -98,6 +92,34 @@ describe('<SelectInput />', () => {
screen.getByText('React').getAttribute('aria-disabled')
).toEqual('true');
});

it('should include an empty option by default', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<SelectInput {...defaultProps} />
</SimpleForm>
</AdminContext>
);
fireEvent.mouseDown(
screen.getByLabelText('resources.posts.fields.language')
);
expect(screen.queryAllByRole('option')).toHaveLength(3);
});

it('should not include an empty option if the field is required', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<SimpleForm onSubmit={jest.fn()}>
<SelectInput {...defaultProps} validate={required()} />
</SimpleForm>
</AdminContext>
);
fireEvent.mouseDown(
screen.getByLabelText('resources.posts.fields.language *')
);
expect(screen.queryAllByRole('option')).toHaveLength(2);
});
});

describe('emptyText', () => {
Expand Down Expand Up @@ -375,12 +397,16 @@ describe('<SelectInput />', () => {
defaultValues={{ language: 'ang' }}
onSubmit={jest.fn()}
>
<SelectInput {...defaultProps} validate={required()} />
<SelectInput
{...defaultProps}
helperText="helperText"
validate={() => 'error'}
/>
</SimpleForm>
</AdminContext>
);
const error = screen.queryAllByText('ra.validation.required');
expect(error.length).toEqual(0);
screen.getByText('helperText');
expect(screen.queryAllByText('error')).toHaveLength(0);
});

it('should not be displayed if field has been touched but is valid', () => {
Expand All @@ -391,18 +417,21 @@ describe('<SelectInput />', () => {
mode="onBlur"
onSubmit={jest.fn()}
>
<SelectInput {...defaultProps} validate={required()} />
<SelectInput
{...defaultProps}
helperText="helperText"
validate={() => undefined}
/>
</SimpleForm>
</AdminContext>
);
const input = screen.getByLabelText(
'resources.posts.fields.language *'
'resources.posts.fields.language'
);
input.focus();
input.blur();

const error = screen.queryAllByText('ra.validation.required');
expect(error.length).toEqual(0);
screen.getByText('helperText');
});

it('should be displayed if field has been touched and is invalid', async () => {
Expand All @@ -411,27 +440,25 @@ describe('<SelectInput />', () => {
<SimpleForm mode="onChange" onSubmit={jest.fn()}>
<SelectInput
{...defaultProps}
helperText="helperText"
emptyText="Empty"
validate={required()}
validate={() => 'error'}
/>
</SimpleForm>
</AdminContext>
);

const select = screen.getByLabelText(
'resources.posts.fields.language *'
'resources.posts.fields.language'
);
fireEvent.mouseDown(select);

const optionAngular = screen.getByText('Angular');
fireEvent.click(optionAngular);
select.blur();

const optionEmpty = screen.getByText('Empty');
fireEvent.click(optionEmpty);

await waitFor(() => {
expect(screen.queryByText('ra.validation.required'));
});
await screen.findByText('error');
expect(screen.queryAllByText('helperText')).toHaveLength(0);
});
});

Expand Down
77 changes: 76 additions & 1 deletion packages/ra-ui-materialui/src/input/SelectInput.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as React from 'react';
import { createMemoryHistory } from 'history';
import { Admin, AdminContext } from 'react-admin';
import { Resource } from 'ra-core';
import { Resource, required } from 'ra-core';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';

Expand All @@ -24,6 +24,42 @@ export const Basic = () => (
</Wrapper>
);

export const DefaultValue = () => (
<Wrapper>
<SelectInput
source="gender"
choices={[
{ id: 'M', name: 'Male ' },
{ id: 'F', name: 'Female' },
]}
defaultValue="F"
/>
</Wrapper>
);

export const InitialValue = () => (
<AdminContext
i18nProvider={i18nProvider}
dataProvider={
{
getOne: () => Promise.resolve({ data: { id: 1, gender: 'F' } }),
} as any
}
>
<Edit resource="posts" id="1">
<SimpleForm>
<SelectInput
source="gender"
choices={[
{ id: 'M', name: 'Male ' },
{ id: 'F', name: 'Female' },
]}
/>
</SimpleForm>
</Edit>
</AdminContext>
);

export const Disabled = () => (
<Wrapper>
<SelectInput
Expand All @@ -37,6 +73,45 @@ export const Disabled = () => (
</Wrapper>
);

export const Validate = () => (
<Wrapper>
<SelectInput
source="gender"
choices={[
{ id: 'M', name: 'Male ' },
{ id: 'F', name: 'Female' },
]}
validate={() => 'error'}
/>
</Wrapper>
);

export const Required = () => (
<Wrapper>
<SelectInput
source="gender"
choices={[
{ id: 'M', name: 'Male ' },
{ id: 'F', name: 'Female' },
]}
validate={required()}
/>
</Wrapper>
);

export const EmptyText = () => (
<Wrapper>
<SelectInput
source="gender"
choices={[
{ id: 'M', name: 'Male ' },
{ id: 'F', name: 'Female' },
]}
emptyText="None"
/>
</Wrapper>
);

const i18nProvider = polyglotI18nProvider(() => englishMessages);

const Wrapper = ({ children }) => (
Expand Down
24 changes: 14 additions & 10 deletions packages/ra-ui-materialui/src/input/SelectInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,10 @@ export const SelectInput = (props: SelectInputProps) => {
});

const createItem = create || onCreate ? getCreateItem() : null;
const finalChoices =
create || onCreate ? [...allChoices, createItem] : allChoices;
let finalChoices = allChoices;
if (create || onCreate) {
finalChoices = [...finalChoices, createItem];
}

const renderMenuItem = useCallback(
choice => {
Expand Down Expand Up @@ -302,14 +304,16 @@ export const SelectInput = (props: SelectInputProps) => {
margin={margin}
{...sanitizeRestProps(rest)}
>
<MenuItem
value={emptyValue}
key="null"
aria-label={translate('ra.action.clear_input_value')}
title={translate('ra.action.clear_input_value')}
>
{renderEmptyItemOption()}
</MenuItem>
{!isRequired && (
<MenuItem
value={emptyValue}
key="null"
aria-label={translate('ra.action.clear_input_value')}
title={translate('ra.action.clear_input_value')}
>
{renderEmptyItemOption()}
</MenuItem>
)}
{finalChoices.map(renderMenuItem)}
</StyledResettableTextField>
{createElement}
Expand Down