Skip to content

Commit 47bcb52

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`.
1 parent db3b229 commit 47bcb52

File tree

13 files changed

+266
-8
lines changed

13 files changed

+266
-8
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: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,46 @@ React.createElement(() => {
156156
});
157157
```
158158

159+
### Required State
160+
161+
The required state indicates that the input is mandatory.
162+
163+
```docoff-react-preview
164+
React.createElement(() => {
165+
const [agree, setAgree] = React.useState(true);
166+
return (
167+
<CheckboxField
168+
checked={agree}
169+
label="I agree"
170+
onChange={() => setAgree(!agree)}
171+
required
172+
/>
173+
);
174+
});
175+
```
176+
177+
However, you may find yourself in a situation where the input is not required
178+
(i.e. making the input checked), but you also don't want to render the field as
179+
optional because the unchecked state can be perfectly valid. For this case,
180+
there is the `renderAsRequired` prop:
181+
182+
```docoff-react-preview
183+
React.createElement(() => {
184+
const [agree, setAgree] = React.useState(true);
185+
return (
186+
<CheckboxField
187+
checked={agree}
188+
label="I agree"
189+
onChange={() => setAgree(!agree)}
190+
renderAsRequired
191+
/>
192+
);
193+
});
194+
```
195+
196+
It renders the field as required, but doesn't add the `required` attribute to
197+
the actual input.
198+
159199
### Disabled State
160200

161201
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: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,74 @@ 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+
However, you may find yourself in a situation where the input is not required
273+
(i.e. making the input checked), but you also don't want to render the field as
274+
optional because not choosing an option can be perfectly valid. For this case,
275+
there is the `renderAsRequired` prop:
276+
277+
```docoff-react-preview
278+
React.createElement(() => {
279+
const [fruit, setFruit] = React.useState('apple');
280+
return (
281+
<Radio
282+
label="Your favourite fruit"
283+
onChange={(e) => setFruit(e.target.value)}
284+
options={[
285+
{
286+
label: 'Apple',
287+
value: 'apple',
288+
},
289+
{
290+
label: 'Banana',
291+
value: 'banana',
292+
},
293+
{
294+
label: 'Grapefruit',
295+
value: 'grapefruit',
296+
},
297+
]}
298+
value={fruit}
299+
renderAsRequired
300+
/>
301+
);
302+
})
303+
```
304+
305+
It renders the field as required, but doesn't add the `required` attribute to
306+
the actual input.
307+
240308
### Disabled State
241309

242310
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/__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,

src/components/SelectField/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,74 @@ React.createElement(() => {
592592
})
593593
```
594594

595+
### Required State
596+
597+
The required state indicates that the input is mandatory.
598+
599+
```docoff-react-preview
600+
React.createElement(() => {
601+
const [fruit, setFruit] = React.useState('apple');
602+
return (
603+
<SelectField
604+
label="Your favourite fruit"
605+
onChange={(e) => setFruit(e.target.value)}
606+
options={[
607+
{
608+
label: 'Apple',
609+
value: 'apple',
610+
},
611+
{
612+
label: 'Banana',
613+
value: 'banana',
614+
},
615+
{
616+
label: 'Grapefruit',
617+
value: 'grapefruit',
618+
},
619+
]}
620+
value={fruit}
621+
required
622+
/>
623+
);
624+
});
625+
```
626+
627+
However, you may find yourself in a situation where the input is not required
628+
(i.e. selecting an option), but you also don't want to render the field as
629+
optional because the unselected state can be perfectly valid. For this case,
630+
there is the `renderAsRequired` prop:
631+
632+
```docoff-react-preview
633+
React.createElement(() => {
634+
const [fruit, setFruit] = React.useState('apple');
635+
return (
636+
<SelectField
637+
label="Your favourite fruit"
638+
onChange={(e) => setFruit(e.target.value)}
639+
options={[
640+
{
641+
label: 'Apple',
642+
value: 'apple',
643+
},
644+
{
645+
label: 'Banana',
646+
value: 'banana',
647+
},
648+
{
649+
label: 'Grapefruit',
650+
value: 'grapefruit',
651+
},
652+
]}
653+
value={fruit}
654+
renderAsRequired
655+
/>
656+
);
657+
});
658+
```
659+
660+
It renders the field as required, but doesn't add the `required` attribute to
661+
the actual input.
662+
595663
### Disabled State
596664

597665
It's possible to disable just some options or the whole input.

src/components/SelectField/SelectField.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const SelectField = React.forwardRef((props, ref) => {
2121
label,
2222
layout,
2323
options,
24+
renderAsRequired,
2425
required,
2526
size,
2627
validationState,
@@ -43,7 +44,7 @@ export const SelectField = React.forwardRef((props, ref) => {
4344
? styles.isRootLayoutHorizontal
4445
: styles.isRootLayoutVertical,
4546
inputGroupContext && styles.isRootGrouped,
46-
required && styles.isRootRequired,
47+
(renderAsRequired || required) && styles.isRootRequired,
4748
getRootSizeClassName(
4849
resolveContextOrProp(inputGroupContext && inputGroupContext.size, size),
4950
styles,
@@ -136,6 +137,7 @@ SelectField.defaultProps = {
136137
id: undefined,
137138
isLabelVisible: true,
138139
layout: 'vertical',
140+
renderAsRequired: false,
139141
required: false,
140142
size: 'medium',
141143
validationState: null,
@@ -227,7 +229,11 @@ SelectField.propTypes = {
227229
})),
228230
]).isRequired,
229231
/**
230-
* If `true`, the input will be required.
232+
* If `true`, the input will be rendered as if it was required.
233+
*/
234+
renderAsRequired: PropTypes.bool,
235+
/**
236+
* If `true`, the input will be made and rendered as required, regardless of the `renderAsRequired` prop.
231237
*/
232238
required: PropTypes.bool,
233239
/**

src/components/SelectField/__tests__/SelectField.test.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { formLayoutProviderTest } from '../../../../tests/providerTests/formLayo
1414
import { isLabelVisibleTest } from '../../../../tests/propTests/isLabelVisibleTest';
1515
import { labelPropTest } from '../../../../tests/propTests/labelPropTest';
1616
import { layoutPropTest } from '../../../../tests/propTests/layoutPropTest';
17+
import { renderAsRequiredPropTest } from '../../../../tests/propTests/renderAsRequiredPropTest';
1718
import { requiredPropTest } from '../../../../tests/propTests/requiredPropTest';
1819
import { sizePropTest } from '../../../../tests/propTests/sizePropTest';
1920
import { validationStatePropTest } from '../../../../tests/propTests/validationStatePropTest';
@@ -107,6 +108,7 @@ describe('rendering', () => {
107108
expect(within(rootElement).getByText('option 4')).toHaveAttribute('id', 'id__item__key');
108109
},
109110
],
111+
...renderAsRequiredPropTest,
110112
...requiredPropTest,
111113
...sizePropTest,
112114
...validationStatePropTest,

0 commit comments

Comments
 (0)