diff --git a/src/components/CheckboxField/CheckboxField.jsx b/src/components/CheckboxField/CheckboxField.jsx index 2c35432d..e23e521c 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 51594d3e..aa7bdf04 100644 --- a/src/components/CheckboxField/README.md +++ b/src/components/CheckboxField/README.md @@ -186,6 +186,81 @@ React.createElement(() => { }); ``` +### Required State + +The required state indicates that the input is mandatory. Required fields +display an asterisk `*` after the label by default. + +```docoff-react-preview +React.createElement(() => { + const [agree, setAgree] = React.useState(true); + return ( + setAgree(!agree)} + required + /> + ); +}); +``` + +#### Styling the Required State + +All form fields in React UI can be +[styled](/docs/customize/theming/forms/#required-state) +to indicate the required state. + +However, you may find yourself in a situation where a form field is valid in +both checked and unchecked states, for example to turn on or off a feature. +If your project uses the label color as the primary means to indicate the +required state of input fields and the usual asterisk `*` is omitted, you may +want to keep the label color consistent for both states to avoid confusion. + +For this edge case, there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [optional, setOptional] = React.useState(false); + const [renderAsRequired, setRenderAsRequired] = React.useState(false); + return ( + + +
+ setOptional(!optional)} + /> + setRenderAsRequired(!renderAsRequired)} + renderAsRequired + /> +
+
+ ); +}); +``` + +It renders the field as if it was 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..9d8447df 100644 --- a/src/components/Radio/README.md +++ b/src/components/Radio/README.md @@ -237,6 +237,109 @@ 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 + /> + ); +}) +``` + +#### Styling the Required State + +All form fields in React UI can be +[styled](/docs/customize/theming/forms/#required-state) +to indicate the required state. + +However, you may find yourself in a situation where a form field is valid in +both selected and unselected states, for example to turn on or off a feature. +If your project uses the label color as the primary means to indicate the +required state of input fields and the usual asterisk `*` is omitted, you may +want to keep the label color consistent for both states to avoid confusion. + +For this edge case, there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + const options = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]; + return ( + + +
+ setFruit(e.target.value)} + options={options} + value={fruit} + /> + setFruit(e.target.value)} + options={options} + value={fruit} + renderAsRequired + /> +
+
+ ); +}) +``` + +It renders the field as if it was 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 be5248d6..19f080c6 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/Radio.module.scss b/src/components/Radio/Radio.module.scss index 4d7d0372..6cecf6a6 100644 --- a/src/components/Radio/Radio.module.scss +++ b/src/components/Radio/Radio.module.scss @@ -52,6 +52,10 @@ @include foundation.label-required(); } + .isRootRequired .optionLabel { + @include foundation.label-required($show-require-sign: false); + } + // States .isRootStateInvalid { @include variants.validation(invalid); 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..a61ef74a 100644 --- a/src/components/SelectField/README.md +++ b/src/components/SelectField/README.md @@ -592,6 +592,109 @@ 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 + /> + ); +}); +``` + +#### Styling the Required State + +All form fields in React UI can be +[styled](/docs/customize/theming/forms/#required-state) +to indicate the required state. + +However, you may find yourself in a situation where a form field is valid in +both selected and unselected states, for example to turn on or off a feature. +If your project uses the label color as the primary means to indicate the +required state of input fields and the usual asterisk `*` is omitted, you may +want to keep the label color consistent for both states to avoid confusion. + +For this edge case, there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [fruit, setFruit] = React.useState('apple'); + const options = [ + { + label: 'Apple', + value: 'apple', + }, + { + label: 'Banana', + value: 'banana', + }, + { + label: 'Grapefruit', + value: 'grapefruit', + }, + ]; + return ( + + +
+ setFruit(e.target.value)} + options={options} + value={fruit} + /> + setFruit(e.target.value)} + options={options} + value={fruit} + renderAsRequired + /> +
+
+ ); +}); +``` + +It renders the field as if it was 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 613b0eff..e8731e28 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 f1373746..3eac252c 100644 --- a/src/components/Toggle/README.md +++ b/src/components/Toggle/README.md @@ -163,6 +163,80 @@ 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 + /> + ); +}); +``` + +#### Styling the Required State + +All form fields in React UI can be +[styled](/docs/customize/theming/forms/#required-state) +to indicate the required state. + +However, you may find yourself in a situation where a form field is valid in +both checked and unchecked states, for example to turn on or off a feature. +If your project uses the label color as the primary means to indicate the +required state of input fields and the usual asterisk `*` is omitted, you may +want to keep the label color consistent for both states to avoid confusion. + +For this edge case, there is the `renderAsRequired` prop: + +```docoff-react-preview +React.createElement(() => { + const [optional, setOptional] = React.useState(false); + const [renderAsRequired, setRenderAsRequired] = React.useState(false); + return ( + + +
+ setOptional(!optional)} + /> + setRenderAsRequired(!renderAsRequired)} + renderAsRequired + /> +
+
+ ); +}); +``` + +It renders the field as if it was 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 77d1fd0d..44ff357c 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/src/docs/customize/theming/forms.md b/src/docs/customize/theming/forms.md index 0c259956..2bb5f7d4 100644 --- a/src/docs/customize/theming/forms.md +++ b/src/docs/customize/theming/forms.md @@ -23,9 +23,6 @@ The following theme options define basic appearance of all form fields. | `--rui-FormField__help-text__font-size` | Help text font size | | `--rui-FormField__help-text__font-style` | Help text font style, e.g. italic | | `--rui-FormField__help-text__color` | Help text color | -| `--rui-FormField--required__label__color` | Color of required input labels | -| `--rui-FormField--required__sign` | Text appended to required input labels | -| `--rui-FormField--required__sign__color` | Color of text appended to required input labels | ## Horizontal Layout @@ -599,6 +596,26 @@ React.createElement(() => { }); ``` +## Required State + +Theming options for required fields are shared by all form components. + +| Custom Property | Description | +|------------------------------------------------------|--------------------------------------------------------------| +| `--rui-FormField--required__label__color` | Color of required input labels | +| `--rui-FormField--required__sign` | Text appended to required input labels | +| `--rui-FormField--required__sign__color` | Color of text appended to required input labels | + +👉 Please note that selected components can be rendered as required by setting +the `renderAsRequired` prop to `true`. This is useful when +`--rui-FormField--required__label__color` is used to indicate the required state +of input fields, but you want to bypass it for inputs like feature toggles. +This applies to +[CheckboxField](/components/CheckboxField/#styling-the-required-state), +[Radio](/components/Radio/#styling-the-required-state), +[SelectField](/components/SelectField/#styling-the-required-state), +and [Toggle](/components/Toggle/#styling-the-required-state). + ## Disabled State By default, all disabled form fields are semi-transparent and change mouse diff --git a/src/styles/tools/form-fields/_foundation.scss b/src/styles/tools/form-fields/_foundation.scss index f23f127d..7c30f6ff 100644 --- a/src/styles/tools/form-fields/_foundation.scss +++ b/src/styles/tools/form-fields/_foundation.scss @@ -18,12 +18,14 @@ color: var(--rui-local-surrounding-text-color, #{theme.$label-color}); } -@mixin label-required() { +@mixin label-required($show-require-sign: true) { color: var(--rui-local-surrounding-text-color, #{theme.$required-label-color}); - &::after { - content: theme.$required-sign; - color: theme.$required-sign-color; + @if $show-require-sign { + &::after { + content: theme.$required-sign; + color: theme.$required-sign-color; + } } } 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'), + ], +];