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`.
  • Loading branch information
adamkudrna committed Dec 3, 2024
1 parent db3b229 commit 47bcb52
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 @@ -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 (
<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';

Check failure on line 14 in src/components/CheckboxField/__tests__/CheckboxField.test.jsx

View workflow job for this annotation

GitHub Actions / Lint

renderAsRequiredPropTest not found in '../../../../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';

Check failure on line 13 in src/components/Radio/__tests__/Radio.test.jsx

View workflow job for this annotation

GitHub Actions / Lint

renderAsRequiredPropTest not found in '../../../../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';

Check failure on line 17 in src/components/SelectField/__tests__/SelectField.test.jsx

View workflow job for this annotation

GitHub Actions / Lint

renderAsRequiredPropTest not found in '../../../../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 47bcb52

Please sign in to comment.