Skip to content

Commit 0f2698d

Browse files
committed
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`. Closes #487
1 parent ca1c36a commit 0f2698d

File tree

16 files changed

+435
-15
lines changed

16 files changed

+435
-15
lines changed

src/components/CheckboxField/CheckboxField.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const CheckboxField = React.forwardRef((props, ref) => {
1515
isLabelVisible,
1616
label,
1717
labelPosition,
18+
renderAsRequired,
1819
required,
1920
validationState,
2021
validationText,
@@ -30,7 +31,7 @@ export const CheckboxField = React.forwardRef((props, ref) => {
3031
context && context.layout === 'horizontal' ? styles.isRootLayoutHorizontal : styles.isRootLayoutVertical,
3132
labelPosition === 'before' && styles.hasRootLabelBefore,
3233
disabled && styles.isRootDisabled,
33-
required && styles.isRootRequired,
34+
(renderAsRequired || required) && styles.isRootRequired,
3435
getRootValidationStateClassName(validationState, styles),
3536
)}
3637
htmlFor={id}
@@ -82,6 +83,7 @@ CheckboxField.defaultProps = {
8283
id: undefined,
8384
isLabelVisible: true,
8485
labelPosition: 'after',
86+
renderAsRequired: false,
8587
required: false,
8688
validationState: null,
8789
validationText: null,
@@ -120,7 +122,11 @@ CheckboxField.propTypes = {
120122
*/
121123
labelPosition: PropTypes.oneOf(['before', 'after']),
122124
/**
123-
* If `true`, the input will be required.
125+
* If `true`, the input will be rendered as if it was required.
126+
*/
127+
renderAsRequired: PropTypes.bool,
128+
/**
129+
* If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
124130
*/
125131
required: PropTypes.bool,
126132
/**

src/components/CheckboxField/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,81 @@ React.createElement(() => {
186186
});
187187
```
188188

189+
### Required State
190+
191+
The required state indicates that the input is mandatory. Required fields
192+
display an asterisk `*` after the label by default.
193+
194+
```docoff-react-preview
195+
React.createElement(() => {
196+
const [agree, setAgree] = React.useState(true);
197+
return (
198+
<CheckboxField
199+
checked={agree}
200+
label="I agree"
201+
onChange={() => setAgree(!agree)}
202+
required
203+
/>
204+
);
205+
});
206+
```
207+
208+
#### Styling the Required State
209+
210+
All form fields in React UI can be
211+
[styled](/docs/customize/theming/forms/#required-state)
212+
to indicate the required state.
213+
214+
However, you may find yourself in a misleading situation where a form field
215+
is valid in both checked and unchecked states, for example to turn on or off a
216+
feature. If your project uses the label color as the primary means to indicate
217+
the required state of input fields and the usual asterisk `*` is omitted,
218+
you may want to keep the label color consistent for both states.
219+
220+
For this edge case, there is the `renderAsRequired` prop:
221+
222+
```docoff-react-preview
223+
React.createElement(() => {
224+
const [optional, setOptional] = React.useState(false);
225+
const [renderAsRequired, setRenderAsRequired] = React.useState(false);
226+
return (
227+
<React.Fragment>
228+
<style>
229+
{`
230+
.example {
231+
display: flex;
232+
flex-wrap: wrap;
233+
gap: 1rem 0.5rem;
234+
}
235+
236+
.example--themed-form-fields {
237+
--rui-FormField__label__color: var(--rui-color-text-secondary);
238+
--rui-FormField--required__label__color: var(--rui-color-text-primary);
239+
--rui-FormField--required__sign: '';
240+
}
241+
`}
242+
</style>
243+
<div class="example example--themed-form-fields">
244+
<CheckboxField
245+
checked={optional}
246+
label="This field is optional"
247+
onChange={() => setOptional(!optional)}
248+
/>
249+
<CheckboxField
250+
checked={renderAsRequired}
251+
label="This field is optional but looks like required"
252+
onChange={() => setRenderAsRequired(!renderAsRequired)}
253+
renderAsRequired
254+
/>
255+
</div>
256+
</React.Fragment>
257+
);
258+
});
259+
```
260+
261+
It renders the field as if it was required, but doesn't add the `required`
262+
attribute to the actual input.
263+
189264
### Disabled State
190265

191266
Disabled state makes the input unavailable.

src/components/CheckboxField/__tests__/CheckboxField.test.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { helpTextPropTest } from '../../../../tests/propTests/helpTextPropTest';
1111
import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayoutProviderTest';
1212
import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest';
1313
import { labelPropTest } from '../../../../tests/propTests/labelPropTest';
14+
import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest';
1415
import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest';
1516
import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest';
1617
import { validationTextPropTest } from '../../../../tests/propTests/validationTextPropTest';
@@ -43,6 +44,7 @@ describe('rendering', () => {
4344
],
4445
...isLabelVisibleTest(),
4546
...labelPropTest(),
47+
...renderAsRequiredPropTest,
4648
...requiredPropTest,
4749
...validationStatePropTest,
4850
...validationTextPropTest,

src/components/Radio/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,109 @@ have.
237237
})
238238
```
239239

240+
### Required State
241+
242+
The required state indicates that the input is mandatory.
243+
244+
```docoff-react-preview
245+
React.createElement(() => {
246+
const [fruit, setFruit] = React.useState('apple');
247+
return (
248+
<Radio
249+
label="Your favourite fruit"
250+
onChange={(e) => setFruit(e.target.value)}
251+
options={[
252+
{
253+
label: 'Apple',
254+
value: 'apple',
255+
},
256+
{
257+
label: 'Banana',
258+
value: 'banana',
259+
},
260+
{
261+
label: 'Grapefruit',
262+
value: 'grapefruit',
263+
},
264+
]}
265+
value={fruit}
266+
required
267+
/>
268+
);
269+
})
270+
```
271+
272+
#### Styling the Required State
273+
274+
All form fields in React UI can be
275+
[styled](/docs/customize/theming/forms/#required-state)
276+
to indicate the required state.
277+
278+
However, you may find yourself in a misleading situation where a form field
279+
is valid in both selected and unselected states, for example to turn on or off a
280+
feature. If your project uses the label color as the primary means to indicate
281+
the required state of input fields and the usual asterisk `*` is omitted,
282+
you may want to keep the label color consistent for both states.
283+
284+
For this edge case, there is the `renderAsRequired` prop:
285+
286+
```docoff-react-preview
287+
React.createElement(() => {
288+
const [fruit, setFruit] = React.useState('apple');
289+
const options = [
290+
{
291+
label: 'Apple',
292+
value: 'apple',
293+
},
294+
{
295+
label: 'Banana',
296+
value: 'banana',
297+
},
298+
{
299+
label: 'Grapefruit',
300+
value: 'grapefruit',
301+
},
302+
];
303+
return (
304+
<React.Fragment>
305+
<style>
306+
{`
307+
.example {
308+
display: flex;
309+
flex-wrap: wrap;
310+
gap: 1rem 0.5rem;
311+
}
312+
313+
.example--themed-form-fields {
314+
--rui-FormField__label__color: var(--rui-color-text-secondary);
315+
--rui-FormField--required__label__color: var(--rui-color-text-primary);
316+
--rui-FormField--required__sign: '';
317+
}
318+
`}
319+
</style>
320+
<div class="example example--themed-form-fields">
321+
<Radio
322+
label="This field is optional"
323+
onChange={(e) => setFruit(e.target.value)}
324+
options={options}
325+
value={fruit}
326+
/>
327+
<Radio
328+
label="This field is optional but looks like required"
329+
onChange={(e) => setFruit(e.target.value)}
330+
options={options}
331+
value={fruit}
332+
renderAsRequired
333+
/>
334+
</div>
335+
</React.Fragment>
336+
);
337+
})
338+
```
339+
340+
It renders the field as if it was required, but doesn't add the `required`
341+
attribute to the actual input.
342+
240343
### Disabled State
241344

242345
It's possible to disable just some options or the whole set.

src/components/Radio/Radio.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export const Radio = ({
1616
label,
1717
layout,
1818
options,
19+
renderAsRequired,
1920
required,
2021
validationState,
2122
validationText,
@@ -33,7 +34,7 @@ export const Radio = ({
3334
? styles.isRootLayoutHorizontal
3435
: styles.isRootLayoutVertical,
3536
disabled && styles.isRootDisabled,
36-
required && styles.isRootRequired,
37+
(renderAsRequired || required) && styles.isRootRequired,
3738
getRootValidationStateClassName(validationState, styles),
3839
)}
3940
disabled={disabled}
@@ -116,6 +117,7 @@ Radio.defaultProps = {
116117
id: undefined,
117118
isLabelVisible: true,
118119
layout: 'vertical',
120+
renderAsRequired: false,
119121
required: false,
120122
validationState: null,
121123
validationText: null,
@@ -181,7 +183,11 @@ Radio.propTypes = {
181183
]),
182184
})).isRequired,
183185
/**
184-
* If `true`, the input will be required.
186+
* If `true`, the input will be rendered as if it was required.
187+
*/
188+
renderAsRequired: PropTypes.bool,
189+
/**
190+
* If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
185191
*/
186192
required: PropTypes.bool,
187193
/**

src/components/Radio/Radio.module.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@
5252
@include foundation.label-required();
5353
}
5454

55+
.isRootRequired .optionLabel {
56+
@include foundation.label-required($show-require-sign: false);
57+
}
58+
5559
// States
5660
.isRootStateInvalid {
5761
@include variants.validation(invalid);

src/components/Radio/__tests__/Radio.test.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayo
1010
import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest';
1111
import { labelPropTest } from '../../../../tests/propTests/labelPropTest';
1212
import { layoutPropTest } from '../../../../tests/propTests/layoutPropTest';
13+
import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest';
1314
import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest';
1415
import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest';
1516
import { validationTextPropTest } from '../../../../tests/propTests/validationTextPropTest';
@@ -83,6 +84,7 @@ describe('rendering', () => {
8384
expect(within(rootElement).getByLabelText('option 2')).toBeDisabled();
8485
},
8586
],
87+
...renderAsRequiredPropTest,
8688
...requiredPropTest,
8789
...validationStatePropTest,
8890
...validationTextPropTest,

0 commit comments

Comments
 (0)