Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions docs/SelectArrayInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ The form value for the source must be an array of the selected values, e.g.
| `create` | Optional | `Element` | - | A React Element to render when users want to create a new choice |
| `createLabel` | Optional | `string` | `ReactNode` | `ra.action. create` | The label for the menu item allowing users to create a new choice. Used when the filter is empty |
| `disableValue` | Optional | `string` | 'disabled' | The custom field name used in `choices` to disable some choices |
| `emptyText` | Optional | `string` | `ReactNode` | - | The text to display when no selection has been made |
| `InputLabelProps` | Optional | `Object` | - | Props to pass to the underlying `<InputLabel>` element |
| `onCreate` | Optional | `Function` | - | A function called with the current filter value when users choose to create a new choice. |
| `options` | Optional | `Object` | - | Props to pass to the underlying `<SelectInput>` element |
Expand Down Expand Up @@ -283,6 +284,22 @@ const choices = [
<SelectArrayInput source="roles" choices={choices} disableValue="not_available" />
```

## `emptyText`

Use the `emptyText` prop to display a custom text when no selection has been made.

```jsx
<SelectArrayInput source="roles" choices={choices} emptyText="All Roles" />
```

The `emptyText` prop accepts either a string or a React Element.

```jsx
<SelectArrayInput source="roles" choices={choices} emptyText={<em>All Roles</em>} />
```

When `emptyText` is a string, it is passed through the translation function, so you can use a translation key.

## `InputLabelProps`

Use the `options` attribute if you want to override Material UI's `<InputLabel>` attributes:
Expand Down
35 changes: 35 additions & 0 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
InsideReferenceArrayInputDefaultValue,
CreateLabel,
CreateLabelRendered,
EmptyText,
} from './SelectArrayInput.stories';

describe('<SelectArrayInput />', () => {
Expand Down Expand Up @@ -85,6 +86,40 @@ describe('<SelectArrayInput />', () => {
expect(screen.queryByText('Photography')).not.toBeNull();
});

describe('emptyText', () => {
it('should display the emptyText when the value is empty', () => {
render(<EmptyText />);
expect(screen.queryByText('All Roles')).not.toBeNull();
expect(screen.queryByText('All Channels')).not.toBeNull();
});

it('should not display the emptyText when a value is selected', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
<ResourceContextProvider value="posts">
<SimpleForm
onSubmit={jest.fn()}
defaultValues={{
categories: ['programming'],
}}
>
<SelectArrayInput
{...defaultProps}
emptyText="No selection"
/>
</SimpleForm>
</ResourceContextProvider>
</AdminContext>
);
expect(screen.queryByText('No selection')).toBeNull();
});

it('should accept a React element as emptyText', () => {
render(<EmptyText />);
expect(screen.queryByText('All Channels')).not.toBeNull();
});
});

it('should use optionValue as value identifier', () => {
render(
<AdminContext dataProvider={testDataProvider()}>
Expand Down
24 changes: 24 additions & 0 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ export const Basic = () => (
</Wrapper>
);

export const EmptyText = () => (
<Wrapper>
<SelectArrayInput
source="roles"
choices={[
{ id: 'admin', name: 'Admin' },
{ id: 'u001', name: 'Editor' },
{ id: 'u002', name: 'Moderator' },
{ id: 'u003', name: 'Reviewer' },
]}
emptyText="All Roles"
/>
<SelectArrayInput
source="channels"
choices={[
{ id: 'web', name: 'Web' },
{ id: 'mobile', name: 'Mobile' },
{ id: 'email', name: 'Email' },
]}
emptyText={<em>All Channels</em>}
/>
</Wrapper>
);

export const StringChoices = () => (
<Wrapper>
<SelectArrayInput
Expand Down
64 changes: 44 additions & 20 deletions packages/ra-ui-materialui/src/input/SelectArrayInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
styled,
useThemeProps,
} from '@mui/material/styles';
import { useCallback, useRef, type ChangeEvent } from 'react';
import { useCallback, useRef, type ChangeEvent, type ReactNode } from 'react';
import clsx from 'clsx';
import {
Select,
Expand All @@ -28,6 +28,7 @@ import {
useGetRecordRepresentation,
type SupportCreateSuggestionOptions,
useSupportCreateSuggestion,
useTranslate,
} from 'ra-core';
import { InputHelperText } from './InputHelperText';

Expand Down Expand Up @@ -102,6 +103,7 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => {
createLabel,
createValue,
disableValue = 'disabled',
emptyText,
format,
helperText,
label,
Expand All @@ -128,6 +130,7 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => {
...rest
} = props;

const translate = useTranslate();
const inputLabel = useRef(null);

const {
Expand Down Expand Up @@ -254,6 +257,12 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => {
[getChoiceValue, getDisableValue, renderMenuItemOption, createItem]
);

const renderEmptyText = useCallback(() => {
return typeof emptyText === 'string'
? translate(emptyText, { _: emptyText })
: emptyText;
}, [emptyText, translate]);

if (isPending) {
return (
<Labeled
Expand Down Expand Up @@ -330,26 +339,40 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => {
}
multiple
error={!!fetchError || invalid}
renderValue={(selected: any[]) => (
<div className={SelectArrayInputClasses.chips}>
{(Array.isArray(selected) ? selected : [])
.map(item =>
(allChoices || []).find(
// eslint-disable-next-line eqeqeq
choice => getChoiceValue(choice) == item
renderValue={(selected: any[]) => {
const selectedArray = Array.isArray(selected)
? selected
: [];
if (
selectedArray.length === 0 &&
emptyText != null
) {
return renderEmptyText();
}
return (
<div className={SelectArrayInputClasses.chips}>
{selectedArray
.map(item =>
(allChoices || []).find(
// eslint-disable-next-line eqeqeq
choice =>
getChoiceValue(choice) == item
)
)
)
.filter(item => !!item)
.map(item => (
<Chip
key={getChoiceValue(item)}
label={renderMenuItemOption(item)}
className={SelectArrayInputClasses.chip}
size="small"
/>
))}
</div>
)}
.filter(item => !!item)
.map(item => (
<Chip
key={getChoiceValue(item)}
label={renderMenuItemOption(item)}
className={
SelectArrayInputClasses.chip
}
size="small"
/>
))}
</div>
);
}}
disabled={disabled || readOnly}
readOnly={readOnly}
data-testid="selectArray"
Expand Down Expand Up @@ -380,6 +403,7 @@ export type SelectArrayInputProps = ChoicesProps &
Omit<SupportCreateSuggestionOptions, 'handleChange'> &
Omit<CommonInputProps, 'source'> &
Omit<FormControlProps, 'defaultValue' | 'onBlur' | 'onChange'> & {
emptyText?: ReactNode;
options?: SelectProps;
InputLabelProps?: Omit<InputLabelProps, 'htmlFor' | 'id' | 'ref'>;
source?: string;
Expand Down