From 5a065c42ad73f30e1eb8c6b463d345e1afdc6992 Mon Sep 17 00:00:00 2001 From: Tomas025 Date: Sat, 8 Feb 2025 15:55:34 -0300 Subject: [PATCH 1/6] Added emptyText to SelectArrayInput component --- docs/SelectArrayInput.md | 20 +++++ .../src/input/SelectArrayInput.spec.tsx | 75 +++++++++++++++++++ .../src/input/SelectArrayInput.stories.tsx | 16 ++++ .../src/input/SelectArrayInput.tsx | 64 ++++++++++++---- 4 files changed, 160 insertions(+), 15 deletions(-) diff --git a/docs/SelectArrayInput.md b/docs/SelectArrayInput.md index e8451d116bf..fbff42e5759 100644 --- a/docs/SelectArrayInput.md +++ b/docs/SelectArrayInput.md @@ -409,6 +409,26 @@ In that case, set the `translateChoice` prop to `false`. ``` +## `emptyText` + +If the input isn't required (using `validate={required()}`), users can select an empty choice with an empty text `''` as label, when clicking on the empty option, all selected values ​​will be deselected. + +You can override that label with the `emptyText` prop. + + +```jsx + + + +``` + +The `emptyText` prop accepts either a string or a React Element. +And if you want to hide that empty choice, make the input required. + +```jsx + +``` + ## Fetching Choices If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](./ReferenceArrayInput.md), and leave the `choices` empty: diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index c0e939dc9ae..7c42c62ea7d 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -84,6 +84,18 @@ describe('', () => { expect(screen.queryByText('Photography')).not.toBeNull(); }); + it('should clear the options when clicking in emptyOption', () => { + render( + + + + + + + + ); + }); + it('should use optionValue as value identifier', () => { render( @@ -764,4 +776,67 @@ describe('', () => { }); }); }); + + it('should render the emptyValue option correctly', () => { + const choices = [ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, + ]; + + render( + + + + + + + + ); + + fireEvent.mouseDown( + screen.getByLabelText('resources.posts.fields.categories') + ); + expect(screen.getByText('Test Option')).toBeInTheDocument(); + }); + + it('should clear the options when clicking on emptyOption', async () => { + const choices = [ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, + ]; + + render( + + + + + + + + ); + + fireEvent.mouseDown( + screen.getByLabelText('resources.posts.fields.categories') + ); + fireEvent.click(screen.getByText('Programming')); + fireEvent.click(screen.getByText('Lifestyle')); + expect( + screen.getByDisplayValue('programming,lifestyle') + ).toBeInTheDocument(); + fireEvent.mouseDown( + screen.getByLabelText('resources.posts.fields.categories') + ); + fireEvent.click(screen.getByText('Clear selections')); + expect(screen.queryByDisplayValue('programming,lifestyle')).toBeNull(); + }); }); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index 915f35b0564..37d2e67b82c 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -251,6 +251,22 @@ export const DefaultValue = () => ( ); +export const EmptyText = () => ( + + + +); + export const InsideArrayInput = () => ( { createLabel, createValue, disableValue = 'disabled', + emptyText = '', format, helperText, label, @@ -120,6 +123,8 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { ...rest } = props; + const translate = useTranslate(); + const inputLabel = useRef(null); const { @@ -172,21 +177,29 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { // We might receive an event from the mui component // In this case, it will be the choice id if (eventOrChoice?.target) { - // when used with different IDs types, unselection leads to double selection with both types - // instead of the value being removed from the array - // e.g. we receive eventOrChoice.target.value = [1, '2', 2] instead of [1] after removing 2 - // this snippet removes a value if it is present twice - eventOrChoice.target.value = eventOrChoice.target.value.reduce( - (acc, value) => { - // eslint-disable-next-line eqeqeq - const index = acc.findIndex(v => v == value); - return index < 0 - ? [...acc, value] - : [...acc.slice(0, index), ...acc.slice(index + 1)]; - }, - [] - ); - field.onChange(eventOrChoice); + // If the selectedValue is emptyValue, clears the selections + const selectedValue = eventOrChoice.target.value; + + if (selectedValue.includes('')) { + field.onChange([]); + } else { + // when used with different IDs types, unselection leads to double selection with both types + // instead of the value being removed from the array + // e.g. we receive eventOrChoice.target.value = [1, '2', 2] instead of [1] after removing 2 + // this snippet removes a value if it is present twice + eventOrChoice.target.value = + eventOrChoice.target.value.reduce((acc, value) => { + // eslint-disable-next-line eqeqeq + const index = acc.findIndex(v => v == value); + return index < 0 + ? [...acc, value] + : [ + ...acc.slice(0, index), + ...acc.slice(index + 1), + ]; + }, []); + field.onChange(eventOrChoice); + } } else { // Or we might receive a choice directly, for instance a newly created one field.onChange([ @@ -217,6 +230,14 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { ? [...(allChoices || []), createItem] : allChoices || []; + const renderEmptyItemOption = useCallback(() => { + return typeof emptyText === 'string' + ? emptyText === '' + ? ' ' // em space, forces the display of an empty line of normal height + : translate(emptyText, { _: emptyText }) + : emptyText; + }, [emptyText, translate]); + const renderMenuItemOption = useCallback( choice => !!createItem && @@ -351,6 +372,18 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { value={finalValue} {...outlinedInputProps} > + {!isRequired && ( + + {renderEmptyItemOption()} + + )} {finalChoices.map(renderMenuItem)} {renderHelperText ? ( @@ -373,6 +406,7 @@ export type SelectArrayInputProps = ChoicesProps & Omit & { options?: SelectProps; disableValue?: string; + emptyText?: string | ReactElement; source?: string; onChange?: (event: ChangeEvent | RaRecord) => void; }; From e34f21471dc212e4c15688e75e4f0326d1f1f65b Mon Sep 17 00:00:00 2001 From: Tomas Braz <69928207+Tomas025@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:53:37 -0300 Subject: [PATCH 2/6] Update packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx Co-authored-by: Michael Tamm --- packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx index 7c42c62ea7d..8a62d2ca999 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.tsx @@ -777,7 +777,7 @@ describe('', () => { }); }); - it('should render the emptyValue option correctly', () => { + it('should render the emptyText option correctly', () => { const choices = [ { id: 'programming', name: 'Programming' }, { id: 'lifestyle', name: 'Lifestyle' }, From a11bdc064a0b3c9ec0323d583b45ce0fa19a9ede Mon Sep 17 00:00:00 2001 From: Tomas Braz <69928207+Tomas025@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:53:46 -0300 Subject: [PATCH 3/6] Update packages/ra-ui-materialui/src/input/SelectArrayInput.tsx Co-authored-by: Gohar Attiq --- packages/ra-ui-materialui/src/input/SelectArrayInput.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index 8fe6c64e34e..08f45478056 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -374,7 +374,7 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { > {!isRequired && ( Date: Mon, 10 Feb 2025 14:53:54 -0300 Subject: [PATCH 4/6] Update packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx Co-authored-by: Gohar Attiq --- .../ra-ui-materialui/src/input/SelectArrayInput.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx index 37d2e67b82c..81c208edd64 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.stories.tsx @@ -261,7 +261,7 @@ export const EmptyText = () => ( { id: 'u002', name: 'Moderator' }, { id: 'u003', name: 'Reviewer' }, ]} - emptyText={'Clear'} + emptyText='Clear' sx={{ width: 300 }} /> From 7a3e4314b954815f10fee9d92bf8fd83fc8b8297 Mon Sep 17 00:00:00 2001 From: Tomas Braz <69928207+Tomas025@users.noreply.github.com> Date: Mon, 10 Feb 2025 15:45:47 -0300 Subject: [PATCH 5/6] Update SelectArrayInput.md --- docs/SelectArrayInput.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/SelectArrayInput.md b/docs/SelectArrayInput.md index fbff42e5759..9ef6f5163a3 100644 --- a/docs/SelectArrayInput.md +++ b/docs/SelectArrayInput.md @@ -418,7 +418,7 @@ You can override that label with the `emptyText` prop. ```jsx - + ``` @@ -426,7 +426,7 @@ The `emptyText` prop accepts either a string or a React Element. And if you want to hide that empty choice, make the input required. ```jsx - + ``` ## Fetching Choices From 0a8b0d341100cba298abc604cc58e5b01cf39e38 Mon Sep 17 00:00:00 2001 From: Tomas025 Date: Wed, 12 Feb 2025 07:33:08 -0300 Subject: [PATCH 6/6] Removed callback function renderEmptyItemOption --- .../src/input/SelectArrayInput.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index 08f45478056..31b25cae34e 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -230,14 +230,6 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { ? [...(allChoices || []), createItem] : allChoices || []; - const renderEmptyItemOption = useCallback(() => { - return typeof emptyText === 'string' - ? emptyText === '' - ? ' ' // em space, forces the display of an empty line of normal height - : translate(emptyText, { _: emptyText }) - : emptyText; - }, [emptyText, translate]); - const renderMenuItemOption = useCallback( choice => !!createItem && @@ -374,14 +366,18 @@ export const SelectArrayInput = (props: SelectArrayInputProps) => { > {!isRequired && ( - {renderEmptyItemOption()} + {typeof emptyText === 'string' + ? emptyText === '' + ? ' ' // em space, forces the display of an empty line of normal height + : translate(emptyText, { _: emptyText }) + : emptyText} )} {finalChoices.map(renderMenuItem)}