From 26369567fda0a346afe7ac81b269c6eff4118c93 Mon Sep 17 00:00:00 2001 From: evanpaules Date: Sat, 4 Apr 2026 23:27:49 -0400 Subject: [PATCH] feat: Add emptyText prop to SelectArrayInput --- docs/SelectArrayInput.md | 17 +++++ .../src/input/SelectArrayInput.spec.tsx | 35 ++++++++++ .../src/input/SelectArrayInput.stories.tsx | 24 +++++++ .../src/input/SelectArrayInput.tsx | 64 +++++++++++++------ 4 files changed, 120 insertions(+), 20 deletions(-) diff --git a/docs/SelectArrayInput.md b/docs/SelectArrayInput.md index cc70024e8fb..6519c65a622 100644 --- a/docs/SelectArrayInput.md +++ b/docs/SelectArrayInput.md @@ -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 `` 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 `` element | @@ -283,6 +284,22 @@ const choices = [ ``` +## `emptyText` + +Use the `emptyText` prop to display a custom text when no selection has been made. + +```jsx + +``` + +The `emptyText` prop accepts either a string or a React Element. + +```jsx +All Roles} /> +``` + +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 `` attributes: diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 82d9b948775..6eab951454d 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -19,6 +19,7 @@ import { InsideReferenceArrayInputDefaultValue, CreateLabel, CreateLabelRendered, + EmptyText, } from './SelectArrayInput.stories'; describe('', () => { @@ -85,6 +86,40 @@ describe('', () => { expect(screen.queryByText('Photography')).not.toBeNull(); }); + describe('emptyText', () => { + it('should display the emptyText when the value is empty', () => { + render(); + 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( + + + + + + + + ); + expect(screen.queryByText('No selection')).toBeNull(); + }); + + it('should accept a React element as emptyText', () => { + render(); + expect(screen.queryByText('All Channels')).not.toBeNull(); + }); + }); + it('should use optionValue as value identifier', () => { render( diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index 3be0e5b35e4..c9dd0316fd5 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -58,6 +58,30 @@ export const Basic = () => ( ); +export const EmptyText = () => ( + + + All Channels} + /> + +); + export const StringChoices = () => ( { createLabel, createValue, disableValue = 'disabled', + emptyText, format, helperText, label, @@ -128,6 +130,7 @@ export const SelectArrayInput = (inProps: SelectArrayInputProps) => { ...rest } = props; + const translate = useTranslate(); const inputLabel = useRef(null); const { @@ -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 ( { } multiple error={!!fetchError || invalid} - renderValue={(selected: any[]) => ( -
- {(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 ( +
+ {selectedArray + .map(item => + (allChoices || []).find( + // eslint-disable-next-line eqeqeq + choice => + getChoiceValue(choice) == item + ) ) - ) - .filter(item => !!item) - .map(item => ( - - ))} -
- )} + .filter(item => !!item) + .map(item => ( + + ))} +
+ ); + }} disabled={disabled || readOnly} readOnly={readOnly} data-testid="selectArray" @@ -380,6 +403,7 @@ export type SelectArrayInputProps = ChoicesProps & Omit & Omit & Omit & { + emptyText?: ReactNode; options?: SelectProps; InputLabelProps?: Omit; source?: string;