Skip to content

Commit

Permalink
Allow check fields and selectable fields to render as required #487
Browse files Browse the repository at this point in the history
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
  • Loading branch information
adamkudrna committed Dec 6, 2024
1 parent d3264dd commit 7a65aab
Show file tree
Hide file tree
Showing 13 changed files with 266 additions and 8 deletions.
10 changes: 8 additions & 2 deletions src/components/CheckboxField/CheckboxField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const CheckboxField = React.forwardRef((props, ref) => {
isLabelVisible,
label,
labelPosition,
renderAsRequired,
required,
validationState,
validationText,
Expand All @@ -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}
Expand Down Expand Up @@ -82,6 +83,7 @@ CheckboxField.defaultProps = {
id: undefined,
isLabelVisible: true,
labelPosition: 'after',
renderAsRequired: false,
required: false,
validationState: null,
validationText: null,
Expand Down Expand Up @@ -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,
/**
Expand Down
40 changes: 40 additions & 0 deletions src/components/CheckboxField/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,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 (
<CheckboxField
checked={agree}
label="I agree"
onChange={() => 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 (
<CheckboxField
checked={agree}
label="I agree"
onChange={() => 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.
Expand Down
2 changes: 2 additions & 0 deletions src/components/CheckboxField/__tests__/CheckboxField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('rendering', () => {
],
...isLabelVisibleTest(),
...labelPropTest(),
...renderAsRequiredPropTest,
...requiredPropTest,
...validationStatePropTest,
...validationTextPropTest,
Expand Down
68 changes: 68 additions & 0 deletions src/components/Radio/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Radio
label="Your favourite fruit"
onChange={(e) => 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 (
<Radio
label="Your favourite fruit"
onChange={(e) => 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.
Expand Down
10 changes: 8 additions & 2 deletions src/components/Radio/Radio.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const Radio = ({
label,
layout,
options,
renderAsRequired,
required,
validationState,
validationText,
Expand All @@ -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}
Expand Down Expand Up @@ -116,6 +117,7 @@ Radio.defaultProps = {
id: undefined,
isLabelVisible: true,
layout: 'vertical',
renderAsRequired: false,
required: false,
validationState: null,
validationText: null,
Expand Down Expand Up @@ -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,
/**
Expand Down
2 changes: 2 additions & 0 deletions src/components/Radio/__tests__/Radio.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -83,6 +84,7 @@ describe('rendering', () => {
expect(within(rootElement).getByLabelText('option 2')).toBeDisabled();
},
],
...renderAsRequiredPropTest,
...requiredPropTest,
...validationStatePropTest,
...validationTextPropTest,
Expand Down
68 changes: 68 additions & 0 deletions src/components/SelectField/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<SelectField
label="Your favourite fruit"
onChange={(e) => 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 (
<SelectField
label="Your favourite fruit"
onChange={(e) => 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.
Expand Down
10 changes: 8 additions & 2 deletions src/components/SelectField/SelectField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const SelectField = React.forwardRef((props, ref) => {
label,
layout,
options,
renderAsRequired,
required,
size,
validationState,
Expand All @@ -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,
Expand Down Expand Up @@ -136,6 +137,7 @@ SelectField.defaultProps = {
id: undefined,
isLabelVisible: true,
layout: 'vertical',
renderAsRequired: false,
required: false,
size: 'medium',
validationState: null,
Expand Down Expand Up @@ -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,
/**
Expand Down
2 changes: 2 additions & 0 deletions src/components/SelectField/__tests__/SelectField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -107,6 +108,7 @@ describe('rendering', () => {
expect(within(rootElement).getByText('option 4')).toHaveAttribute('id', 'id__item__key');
},
],
...renderAsRequiredPropTest,
...requiredPropTest,
...sizePropTest,
...validationStatePropTest,
Expand Down
Loading

0 comments on commit 7a65aab

Please sign in to comment.