diff --git a/docs/CheckboxGroupInput.md b/docs/CheckboxGroupInput.md index 347ffbca35d..fe9ae8d92a3 100644 --- a/docs/CheckboxGroupInput.md +++ b/docs/CheckboxGroupInput.md @@ -65,6 +65,7 @@ The form value for the source must be an array of the selected values, e.g. | `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | | `row` | Optional | `boolean` | `true` | Display group of elements in a compact row. | | `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| `disableValue` | Optional | `string` | `disabled` | The custom field name used in `choices` to disable some choices | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -92,6 +93,17 @@ You can also use an array of objects with different properties for the label and ]} optionValue="_id" optionText="label" /> ``` +You can render some options as disabled by setting the `disabled` field in some choices: + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', disabled: true }, +]; + +``` + The choices are translated by default, so you can use translation identifiers as choices: ```jsx @@ -255,6 +267,30 @@ However, in some cases (e.g. inside a ``), you may not want ``` +## `disableValue` + +By default, `` renders the choices with the field `disabled: true` as disabled. + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', disabled: true }, +]; + +``` + +If you want to use another field to denote disabled options, set the `disableValue` prop. + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', not_available: true }, +]; + +``` + ## 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/docs/RadioButtonGroupInput.md b/docs/RadioButtonGroupInput.md index bf481c66868..2aa91552c7c 100644 --- a/docs/RadioButtonGroupInput.md +++ b/docs/RadioButtonGroupInput.md @@ -62,6 +62,7 @@ The form value for the source must be the selected value, e.g. | `optionValue` | Optional | `string` | `id` | Field name of record containing the value to use as input value | | `row` | Optional | `boolean` | `true` | Display options in a compact row. | | `translateChoice` | Optional | `boolean` | `true` | Whether the choices should be translated | +| `disableValue` | Optional | `string` | `disabled` | The custom field name used in `choices` to disable some choices | `` also accepts the [common input props](./Inputs.md#common-input-props). @@ -94,6 +95,17 @@ const choices = [ /> ``` +You can render some options as disabled by setting the `disabled` field in some choices: + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', disabled: true }, +]; + +``` + The choices are translated by default, so you can use translation identifiers as choices: ```jsx @@ -282,6 +294,30 @@ However, in some cases, you may not want the choice to be translated. In that ca Note that `translateChoice` is set to `false` when `` is a child of ``. +## `disableValue` + +By default, `` renders the choices with the field `disabled: true` as disabled. + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', disabled: true }, +]; + +``` + +If you want to use another field to denote disabled options, set the `disableValue` prop. + +```jsx +const choices = [ + { id: 'tech', name: 'Tech' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'people', name: 'People', not_available: true }, +]; + +``` + ## Fetching Choices You can use [`useGetList`](./useGetList.md) to fetch choices. For example, to fetch a list of countries for a user profile: @@ -390,4 +426,4 @@ const CompanyInput = () => ( This is the recommended approach for using `` to select a foreign key. This not only signifies that the input is a `` but also highlights its function in fetching choices from another resource, ultimately enhancing the code's readability. -**Tip**: `` is much more powerful than the initial snippet. It optimizes and caches API calls, enables refetching of both API calls with a single command, and stores supplementary data in the ``. `` can provide choices to ``, but also to [``](./AutocompleteInput.md) and [``](./SelectInput.md). For further information, refer to [the `` documentation](./ReferenceInput.md). \ No newline at end of file +**Tip**: `` is much more powerful than the initial snippet. It optimizes and caches API calls, enables refetching of both API calls with a single command, and stores supplementary data in the ``. `` can provide choices to ``, but also to [``](./AutocompleteInput.md) and [``](./SelectInput.md). For further information, refer to [the `` documentation](./ReferenceInput.md). diff --git a/packages/ra-core/src/form/choices/useChoices.tsx b/packages/ra-core/src/form/choices/useChoices.tsx index 6030453259c..c1ce1a0709f 100644 --- a/packages/ra-core/src/form/choices/useChoices.tsx +++ b/packages/ra-core/src/form/choices/useChoices.tsx @@ -20,6 +20,7 @@ export interface ChoicesProps { optionValue?: string; optionText?: OptionText; translateChoice?: boolean; + disableValue?: string; } export interface UseChoicesOptions { diff --git a/packages/ra-ui-materialui/src/field/SelectField.tsx b/packages/ra-ui-materialui/src/field/SelectField.tsx index eadec98b270..f31b2ce255a 100644 --- a/packages/ra-ui-materialui/src/field/SelectField.tsx +++ b/packages/ra-ui-materialui/src/field/SelectField.tsx @@ -138,7 +138,7 @@ export const SelectField = genericMemo(SelectFieldImpl); export interface SelectFieldProps< RecordType extends Record = Record, -> extends ChoicesProps, +> extends Omit, FieldProps, Omit {} diff --git a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx index 425041913f4..553036e0826 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx +++ b/packages/ra-ui-materialui/src/input/AutocompleteInput.tsx @@ -780,7 +780,7 @@ export interface AutocompleteInputProps< DisableClearable extends boolean | undefined = false, SupportCreate extends boolean | undefined = false, > extends Omit, - ChoicesProps, + Omit, UseSuggestionsOptions, Omit, Omit< diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx index c2e76bda554..4da4cb753c7 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.tsx @@ -426,4 +426,65 @@ describe('', () => { await screen.findByText('Option 1 (This is option 1)'); }); }); + + it('should render disabled choices marked as so', () => { + const choices = [ + { id: 'ang', name: 'Angular' }, + { id: 'rct', name: 'React', disabled: true }, + ]; + render( + + + + + + + + ); + fireEvent.mouseDown(screen.getByLabelText('React')); + + const enabledInput = screen.getByLabelText( + 'Angular' + ) as HTMLInputElement; + expect(enabledInput.disabled).toBe(false); + + const disabledInput = screen.getByLabelText( + 'React' + ) as HTMLInputElement; + expect(disabledInput.disabled).toBe(true); + }); + + it('should render disabled choices marked as so by disableValue prop', () => { + const choices = [ + { id: 'ang', name: 'Angular' }, + { id: 'rct', name: 'React', not_available: true }, + ]; + render( + + + + + + + + ); + fireEvent.mouseDown(screen.getByLabelText('React')); + + const enabledInput = screen.getByLabelText( + 'Angular' + ) as HTMLInputElement; + expect(enabledInput.disabled).toBe(false); + + const disabledInput = screen.getByLabelText( + 'React' + ) as HTMLInputElement; + expect(disabledInput.disabled).toBe(true); + }); }); diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx index 166ce64ea55..aea963071da 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.stories.tsx @@ -322,3 +322,17 @@ export const SetFocus = () => ( ); + +export const DisabledChoice = () => ( + + + +); diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx index 35fd11b9167..b1a155c85f7 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.tsx @@ -95,6 +95,23 @@ import { LinearProgress } from '../layout'; * * * The object passed as `options` props is passed to the Material UI components + * + * You can disable some choices by providing a `disableValue` field which name is `disabled` by default + * @example + * const choices = [ + * { id: 'programming', name: 'myroot.category.programming' }, + * { id: 'lifestyle', name: 'myroot.category.lifestyle' }, + * { id: 'photography', name: 'myroot.category.photography', disabled: true }, + * ]; + * + * @example + * const choices = [ + * { id: 'programming', name: 'myroot.category.programming' }, + * { id: 'lifestyle', name: 'myroot.category.lifestyle' }, + * { id: 'photography', name: 'myroot.category.photography', not_available: true }, + * ]; + * + * */ export const CheckboxGroupInput = (inProps: CheckboxGroupInputProps) => { const props = useThemeProps({ @@ -124,6 +141,7 @@ export const CheckboxGroupInput = (inProps: CheckboxGroupInputProps) => { source: sourceProp, translateChoice, validate, + disableValue = 'disabled', disabled, readOnly, ...rest @@ -262,6 +280,7 @@ export const CheckboxGroupInput = (inProps: CheckboxGroupInputProps) => { value={value} labelPlacement={labelPlacement} inputRef={index === 0 ? ref : undefined} + disableValue={disableValue} disabled={disabled || readOnly} readOnly={readOnly} {...sanitizeRestProps(rest)} diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx b/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx index 4e82fee8907..eeeb4173d88 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInputItem.tsx @@ -32,16 +32,19 @@ export const CheckboxGroupInputItem = ( value, labelPlacement, inputRef, + disableValue = 'disabled', ...rest } = props; - const { getChoiceText, getChoiceValue } = useChoices({ + const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ optionText, optionValue, translateChoice, + disableValue, }); const choiceName = getChoiceText(choice); + const disabled = getDisableValue(choice); return ( } @@ -74,7 +78,10 @@ export const CheckboxGroupInputItem = ( export interface CheckboxGroupInputItemProps extends Omit, - Pick { + Pick< + ChoicesProps, + 'optionValue' | 'optionText' | 'translateChoice' | 'disableValue' + > { choice: any; value: any; fullWidth?: boolean; diff --git a/packages/ra-ui-materialui/src/input/DatagridInput.tsx b/packages/ra-ui-materialui/src/input/DatagridInput.tsx index b649fce16f1..6dc405afcc3 100644 --- a/packages/ra-ui-materialui/src/input/DatagridInput.tsx +++ b/packages/ra-ui-materialui/src/input/DatagridInput.tsx @@ -177,7 +177,7 @@ export type DatagridInputProps = Omit< CommonInputProps, 'source' | 'readOnly' | 'disabled' > & - ChoicesProps & + Omit & Omit & DatagridProps & { children?: ReactNode; diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx index 408651685a0..a45e87e712b 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.tsx @@ -541,6 +541,63 @@ describe('', () => { expect(screen.queryByRole('progressbar')).toBeNull(); }); + it('should render disabled choices marked as so', () => { + const choices = [ + { id: 1, name: 'VISA' }, + { id: 2, name: 'Mastercard', disabled: true }, + ]; + render( + + + + + + + + ); + fireEvent.mouseDown(screen.getByLabelText('Mastercard')); + + const enabledInput = screen.getByLabelText('VISA') as HTMLInputElement; + expect(enabledInput.disabled).toBe(false); + + const disabledInput = screen.getByLabelText( + 'Mastercard' + ) as HTMLInputElement; + expect(disabledInput.disabled).toBe(true); + }); + + it('should render disabled choices marked as so by disableValue prop', () => { + const choices = [ + { id: 1, name: 'VISA' }, + { id: 2, name: 'Mastercard', not_available: true }, + ]; + render( + + + + + + + + ); + fireEvent.mouseDown(screen.getByLabelText('Mastercard')); + + const enabledInput = screen.getByLabelText('VISA') as HTMLInputElement; + expect(enabledInput.disabled).toBe(false); + + const disabledInput = screen.getByLabelText( + 'Mastercard' + ) as HTMLInputElement; + expect(disabledInput.disabled).toBe(true); + }); + describe('inside ReferenceArrayInput', () => { it('should use the recordRepresentation as optionText', async () => { render(); diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx index 09c483ce6fe..6e840914eff 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.stories.tsx @@ -278,6 +278,28 @@ export const TranslateChoice = () => { ); }; +export const DisabledChoice = () => ( + + + +); + export const Themed = () => ( * * The object passed as `options` props is passed to the Material UI component + * + * You can disable some choices by providing a `disableValue` field which name is `disabled` by default + * @example + * const choices = [ + * { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + * { id: 456, first_name: 'Jane', last_name: 'Austen' }, + * { id: 976, first_name: 'William', last_name: 'Rinkerd', disabled: true }, + * ]; + * + * @example + * const choices = [ + * { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + * { id: 456, first_name: 'Jane', last_name: 'Austen' }, + * { id: 976, first_name: 'William', last_name: 'Rinkerd', not_available: true }, + * ]; + * + * */ export const RadioButtonGroupInput = (inProps: RadioButtonGroupInputProps) => { const props = useThemeProps({ @@ -115,6 +132,7 @@ export const RadioButtonGroupInput = (inProps: RadioButtonGroupInputProps) => { source: sourceProp, translateChoice, validate, + disableValue = 'disabled', disabled, readOnly, ...rest @@ -222,6 +240,7 @@ export const RadioButtonGroupInput = (inProps: RadioButtonGroupInputProps) => { optionValue={optionValue} source={id} translateChoice={translateChoice ?? !isFromReference} + disableValue={disableValue} /> ))} diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx index 50d304740d9..466cf09a29a 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInputItem.tsx @@ -19,19 +19,22 @@ export const RadioButtonGroupInputItem = ( optionValue, source, translateChoice, + disableValue = 'disabled', ...rest } = useThemeProps({ props: props, name: PREFIX, }); - const { getChoiceText, getChoiceValue } = useChoices({ + const { getChoiceText, getChoiceValue, getDisableValue } = useChoices({ optionText, optionValue, translateChoice, + disableValue, }); const label = getChoiceText(choice); const value = getChoiceValue(choice); + const disabled = getDisableValue(choice); const nodeId = `${source}_${value}`; @@ -40,6 +43,7 @@ export const RadioButtonGroupInputItem = ( label={label} htmlFor={nodeId} value={value} + disabled={disabled} control={} {...sanitizeInputRestProps(rest)} /> @@ -50,7 +54,10 @@ export default RadioButtonGroupInputItem; export interface RadioButtonGroupInputItemProps extends Omit, - Pick { + Pick< + ChoicesProps, + 'optionValue' | 'optionText' | 'translateChoice' | 'disableValue' + > { choice: any; source: any; } diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx index 5d91cee2e3e..1ae1b76aa6c 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.tsx @@ -380,7 +380,6 @@ export type SelectArrayInputProps = ChoicesProps & Omit & Omit & { options?: SelectProps; - disableValue?: string; source?: string; onChange?: (event: ChangeEvent | RaRecord) => void; }; diff --git a/packages/ra-ui-materialui/src/input/SelectInput.tsx b/packages/ra-ui-materialui/src/input/SelectInput.tsx index 6b3af86b6f1..6cf53f2d252 100644 --- a/packages/ra-ui-materialui/src/input/SelectInput.tsx +++ b/packages/ra-ui-materialui/src/input/SelectInput.tsx @@ -415,7 +415,6 @@ export type SelectInputProps = Omit & ChoicesProps & Omit & Omit & { - disableValue?: string; emptyText?: string | ReactElement; emptyValue?: any; resettable?: boolean;