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..2942ddc4 --- /dev/null +++ b/tests/propTests/renderAsRequiredPropTest.js @@ -0,0 +1,10 @@ +export const renderAsRequiredPropTest = [ + [ + { renderAsRequired: true }, + (rootElement) => expect(rootElement).toHaveClass('isRootRequired'), + ], + [ + { renderAsRequired: false }, + (rootElement) => expect(rootElement).not.toHaveClass('isRootRequired'), + ], +];