From 47bcb5277b99aca93a535cb6d2cb8c1ecefcd498 Mon Sep 17 00:00:00 2001 From: Adam Kudrna Date: Tue, 3 Dec 2024 13:47:14 +0100 Subject: [PATCH] Allow check fields and selectable fields to render as required #487 Users may find themselves in a situation where the input is not required (i.e. making the input checked), but they also don't want to render the field as optional because not choosing an option can be perfectly valid. For this case, there is the `renderAsRequired` prop. This affects `CheckboxField`, `Radio`, `SelectField`, and `Toggle`. --- .../CheckboxField/CheckboxField.jsx | 10 ++- src/components/CheckboxField/README.md | 40 +++++++++++ .../__tests__/CheckboxField.test.jsx | 2 + src/components/Radio/README.md | 68 +++++++++++++++++++ src/components/Radio/Radio.jsx | 10 ++- src/components/Radio/__tests__/Radio.test.jsx | 2 + src/components/SelectField/README.md | 68 +++++++++++++++++++ src/components/SelectField/SelectField.jsx | 10 ++- .../__tests__/SelectField.test.jsx | 2 + src/components/Toggle/README.md | 40 +++++++++++ src/components/Toggle/Toggle.jsx | 10 ++- .../Toggle/__tests__/Toggle.test.jsx | 2 + tests/propTests/renderAsRequiredPropTest.js | 10 +++ 13 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 tests/propTests/renderAsRequiredPropTest.js diff --git a/src/components/CheckboxField/CheckboxField.jsx b/src/components/CheckboxField/CheckboxField.jsx index e3eed379..2d34e134 100644 --- a/src/components/CheckboxField/CheckboxField.jsx +++ b/src/components/CheckboxField/CheckboxField.jsx @@ -15,6 +15,7 @@ export const CheckboxField = React.forwardRef((props, ref) => { isLabelVisible, label, labelPosition, + renderAsRequired, required, validationState, validationText, @@ -30,7 +31,7 @@ export const CheckboxField = React.forwardRef((props, ref) => { context && context.layout === 'horizontal' ? styles.isRootLayoutHorizontal : styles.isRootLayoutVertical, labelPosition === 'before' && styles.hasRootLabelBefore, disabled && styles.isRootDisabled, - required && styles.isRootRequired, + (renderAsRequired || required) && styles.isRootRequired, getRootValidationStateClassName(validationState, styles), )} htmlFor={id} @@ -82,6 +83,7 @@ CheckboxField.defaultProps = { id: undefined, isLabelVisible: true, labelPosition: 'after', + renderAsRequired: false, required: false, validationState: null, validationText: null, @@ -120,7 +122,11 @@ CheckboxField.propTypes = { */ labelPosition: PropTypes.oneOf(['before', 'after']), /** - * If `true`, the input will be required. + * If `true`, the input will be rendered as if it was required. + */ + renderAsRequired: PropTypes.bool, + /** + * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop. */ required: PropTypes.bool, /** diff --git a/src/components/CheckboxField/README.md b/src/components/CheckboxField/README.md index a280694a..7b0e6b00 100644 --- a/src/components/CheckboxField/README.md +++ b/src/components/CheckboxField/README.md @@ -156,6 +156,46 @@ React.createElement(() => { }); ``` +### Required State + +The required state indicates that the input is mandatory. + +```docoff-react-preview +React.createElement(() => { + const [agree, setAgree] = React.useState(true); + return ( + setAgree(!agree)} + required + /> + ); +}); +``` + +However, you may find yourself in a situation where the input is not required +(i.e. making the input checked), but you also don't want to render the field as +optional because the unchecked state can be perfectly valid. For this case, +there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [agree, setAgree] = React.useState(true); + return ( + setAgree(!agree)} + renderAsRequired + /> + ); +}); +``` + +It renders the field as required, but doesn't add the `required` attribute to +the actual input. + ### Disabled State Disabled state makes the input unavailable. diff --git a/src/components/CheckboxField/__tests__/CheckboxField.test.jsx b/src/components/CheckboxField/__tests__/CheckboxField.test.jsx index 69da448f..a7ced2b8 100644 --- a/src/components/CheckboxField/__tests__/CheckboxField.test.jsx +++ b/src/components/CheckboxField/__tests__/CheckboxField.test.jsx @@ -11,6 +11,7 @@ import { helpTextPropTest } from '../../../../tests/propTests/helpTextPropTest'; import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayoutProviderTest'; import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest'; import { labelPropTest } from '../../../../tests/propTests/labelPropTest'; +import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest'; import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest'; import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest'; import { validationTextPropTest } from '../../../../tests/propTests/validationTextPropTest'; @@ -43,6 +44,7 @@ describe('rendering', () => { ], ...isLabelVisibleTest(), ...labelPropTest(), + ...renderAsRequiredPropTest, ...requiredPropTest, ...validationStatePropTest, ...validationTextPropTest, diff --git a/src/components/Radio/README.md b/src/components/Radio/README.md index a5b317e8..1caeec04 100644 --- a/src/components/Radio/README.md +++ b/src/components/Radio/README.md @@ -237,6 +237,74 @@ have. }) ``` +### Required State + +The required state indicates that the input is mandatory. + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + return ( + setFruit(e.target.value)} + options={[ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]} + value={fruit} + required + /> + ); +}) +``` + +However, you may find yourself in a situation where the input is not required +(i.e. making the input checked), but you also don't want to render the field as +optional because not choosing an option can be perfectly valid. For this case, +there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + return ( + setFruit(e.target.value)} + options={[ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]} + value={fruit} + renderAsRequired + /> + ); +}) +``` + +It renders the field as required, but doesn't add the `required` attribute to +the actual input. + ### Disabled State It's possible to disable just some options or the whole set. diff --git a/src/components/Radio/Radio.jsx b/src/components/Radio/Radio.jsx index 56c57c6b..a5e07196 100644 --- a/src/components/Radio/Radio.jsx +++ b/src/components/Radio/Radio.jsx @@ -16,6 +16,7 @@ export const Radio = ({ label, layout, options, + renderAsRequired, required, validationState, validationText, @@ -33,7 +34,7 @@ export const Radio = ({ ? styles.isRootLayoutHorizontal : styles.isRootLayoutVertical, disabled && styles.isRootDisabled, - required && styles.isRootRequired, + (renderAsRequired || required) && styles.isRootRequired, getRootValidationStateClassName(validationState, styles), )} disabled={disabled} @@ -116,6 +117,7 @@ Radio.defaultProps = { id: undefined, isLabelVisible: true, layout: 'vertical', + renderAsRequired: false, required: false, validationState: null, validationText: null, @@ -181,7 +183,11 @@ Radio.propTypes = { ]), })).isRequired, /** - * If `true`, the input will be required. + * If `true`, the input will be rendered as if it was required. + */ + renderAsRequired: PropTypes.bool, + /** + * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop. */ required: PropTypes.bool, /** diff --git a/src/components/Radio/__tests__/Radio.test.jsx b/src/components/Radio/__tests__/Radio.test.jsx index dc96f372..2f586c42 100644 --- a/src/components/Radio/__tests__/Radio.test.jsx +++ b/src/components/Radio/__tests__/Radio.test.jsx @@ -10,6 +10,7 @@ import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayo import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest'; import { labelPropTest } from '../../../../tests/propTests/labelPropTest'; import { layoutPropTest } from '../../../../tests/propTests/layoutPropTest'; +import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest'; import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest'; import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest'; import { validationTextPropTest } from '../../../../tests/propTests/validationTextPropTest'; @@ -83,6 +84,7 @@ describe('rendering', () => { expect(within(rootElement).getByLabelText('option 2')).toBeDisabled(); }, ], + ...renderAsRequiredPropTest, ...requiredPropTest, ...validationStatePropTest, ...validationTextPropTest, diff --git a/src/components/SelectField/README.md b/src/components/SelectField/README.md index 1200e741..f7fe9e8e 100644 --- a/src/components/SelectField/README.md +++ b/src/components/SelectField/README.md @@ -592,6 +592,74 @@ React.createElement(() => { }) ``` +### Required State + +The required state indicates that the input is mandatory. + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + return ( + setFruit(e.target.value)} + options={[ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]} + value={fruit} + required + /> + ); +}); +``` + +However, you may find yourself in a situation where the input is not required +(i.e. selecting an option), but you also don't want to render the field as +optional because the unselected state can be perfectly valid. For this case, +there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + return ( + setFruit(e.target.value)} + options={[ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]} + value={fruit} + renderAsRequired + /> + ); +}); +``` + +It renders the field as required, but doesn't add the `required` attribute to +the actual input. + ### Disabled State It's possible to disable just some options or the whole input. diff --git a/src/components/SelectField/SelectField.jsx b/src/components/SelectField/SelectField.jsx index 13eb6108..72431243 100644 --- a/src/components/SelectField/SelectField.jsx +++ b/src/components/SelectField/SelectField.jsx @@ -21,6 +21,7 @@ export const SelectField = React.forwardRef((props, ref) => { label, layout, options, + renderAsRequired, required, size, validationState, @@ -43,7 +44,7 @@ export const SelectField = React.forwardRef((props, ref) => { ? styles.isRootLayoutHorizontal : styles.isRootLayoutVertical, inputGroupContext && styles.isRootGrouped, - required && styles.isRootRequired, + (renderAsRequired || required) && styles.isRootRequired, getRootSizeClassName( resolveContextOrProp(inputGroupContext && inputGroupContext.size, size), styles, @@ -136,6 +137,7 @@ SelectField.defaultProps = { id: undefined, isLabelVisible: true, layout: 'vertical', + renderAsRequired: false, required: false, size: 'medium', validationState: null, @@ -227,7 +229,11 @@ SelectField.propTypes = { })), ]).isRequired, /** - * If `true`, the input will be required. + * If `true`, the input will be rendered as if it was required. + */ + renderAsRequired: PropTypes.bool, + /** + * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop. */ required: PropTypes.bool, /** diff --git a/src/components/SelectField/__tests__/SelectField.test.jsx b/src/components/SelectField/__tests__/SelectField.test.jsx index 96a9e596..8b2d7dda 100644 --- a/src/components/SelectField/__tests__/SelectField.test.jsx +++ b/src/components/SelectField/__tests__/SelectField.test.jsx @@ -14,6 +14,7 @@ import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayo import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest'; import { labelPropTest } from '../../../../tests/propTests/labelPropTest'; import { layoutPropTest } from '../../../../tests/propTests/layoutPropTest'; +import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest'; import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest'; import { sizePropTest } from '../../../../tests/propTests/sizePropTest'; import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest'; @@ -107,6 +108,7 @@ describe('rendering', () => { expect(within(rootElement).getByText('option 4')).toHaveAttribute('id', 'id__item__key'); }, ], + ...renderAsRequiredPropTest, ...requiredPropTest, ...sizePropTest, ...validationStatePropTest, diff --git a/src/components/Toggle/README.md b/src/components/Toggle/README.md index d84c3806..91c5fff6 100644 --- a/src/components/Toggle/README.md +++ b/src/components/Toggle/README.md @@ -155,6 +155,46 @@ React.createElement(() => { }); ``` +### Required State + +The required state indicates that the input is mandatory. + +```docoff-react-preview +React.createElement(() => { + const [studioQuality, setStudioQuality] = React.useState(true); + return ( + setStudioQuality(!studioQuality)} + required + /> + ); +}); +``` + +However, you may find yourself in a situation where the input is not required +(i.e. turning the toggle on), but you also don't want to render the field as +optional because the unchecked state can be perfectly valid. For this case, +there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [studioQuality, setStudioQuality] = React.useState(true); + return ( + setStudioQuality(!studioQuality)} + renderAsRequired + /> + ); +}); +``` + +It renders the field as required, but doesn't add the `required` attribute to +the actual input. + ### Disabled State Disabled state makes the input unavailable. diff --git a/src/components/Toggle/Toggle.jsx b/src/components/Toggle/Toggle.jsx index 068b4c06..7f460aad 100644 --- a/src/components/Toggle/Toggle.jsx +++ b/src/components/Toggle/Toggle.jsx @@ -15,6 +15,7 @@ export const Toggle = React.forwardRef((props, ref) => { isLabelVisible, label, labelPosition, + renderAsRequired, required, validationState, validationText, @@ -31,7 +32,7 @@ export const Toggle = React.forwardRef((props, ref) => { context && context.layout === 'horizontal' ? styles.isRootLayoutHorizontal : styles.isRootLayoutVertical, labelPosition === 'before' && styles.hasRootLabelBefore, disabled && styles.isRootDisabled, - required && styles.isRootRequired, + (required || renderAsRequired) && styles.isRootRequired, getRootValidationStateClassName(validationState, styles), )} htmlFor={id} @@ -84,6 +85,7 @@ Toggle.defaultProps = { id: undefined, isLabelVisible: true, labelPosition: 'after', + renderAsRequired: false, required: false, validationState: null, validationText: null, @@ -120,7 +122,11 @@ Toggle.propTypes = { */ labelPosition: PropTypes.oneOf(['before', 'after']), /** - * If `true`, the input will be required. + * If `true`, the input will be rendered as if it was required. + */ + renderAsRequired: PropTypes.bool, + /** + * If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop. */ required: PropTypes.bool, /** diff --git a/src/components/Toggle/__tests__/Toggle.test.jsx b/src/components/Toggle/__tests__/Toggle.test.jsx index f5bfe13c..2feff337 100644 --- a/src/components/Toggle/__tests__/Toggle.test.jsx +++ b/src/components/Toggle/__tests__/Toggle.test.jsx @@ -12,6 +12,7 @@ import { helpTextPropTest } from '../../../../tests/propTests/helpTextPropTest'; import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayoutProviderTest'; import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest'; import { labelPropTest } from '../../../../tests/propTests/labelPropTest'; +import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest'; import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest'; import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest'; import { validationTextPropTest } from '../../../../tests/propTests/validationTextPropTest'; @@ -51,6 +52,7 @@ describe('rendering', () => { { labelPosition: 'after' }, (rootElement) => expect(rootElement).not.toHaveClass('hasRootLabelBefore'), ], + ...renderAsRequiredPropTest, ...requiredPropTest, ...validationStatePropTest, ...validationTextPropTest, diff --git a/tests/propTests/renderAsRequiredPropTest.js b/tests/propTests/renderAsRequiredPropTest.js new file mode 100644 index 00000000..a6a694d0 --- /dev/null +++ b/tests/propTests/renderAsRequiredPropTest.js @@ -0,0 +1,10 @@ +export const renderAsRequired = [ + [ + { renderAsRequired: true }, + (rootElement) => expect(rootElement).toHaveClass('isRootRequired'), + ], + [ + { renderAsRequired: false }, + (rootElement) => expect(rootElement).not.toHaveClass('isRootRequired'), + ], +];