Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(FormControl): allow required check boxes #5027

5 changes: 5 additions & 0 deletions .changeset/wild-maps-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

fix(FormControl): allow required check boxes in CheckboxGroup
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Playground.argTypes = {
export const Default = () => (
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<FormControl>
<FormControl required>
<Checkbox value="one" defaultChecked />
<FormControl.Label>Choice one</FormControl.Label>
</FormControl>
Expand Down
8 changes: 6 additions & 2 deletions packages/react/src/FormControl/FormControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
const inputProps = React.isValidElement(InputComponent) && InputComponent.props
const isChoiceInput =
React.isValidElement(InputComponent) && (InputComponent.type === Checkbox || InputComponent.type === Radio)
const isRadioInput = React.isValidElement(InputComponent) && InputComponent.type === Radio

if (InputComponent) {
if (inputProps?.id) {
Expand Down Expand Up @@ -99,9 +100,9 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
)
}

if (childrenWithoutSlots.find(child => React.isValidElement(child) && child.props?.required)) {
if (isRadioInput && childrenWithoutSlots.find(child => React.isValidElement(child) && child.props?.required)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so that'd be handled at the RadioGroup level. The FormControl will be contained within the RadioGroup in this case (RadioGroup -> FormControl -> Radio) and doesn't need to be concerned about required being on the parent. Radios will not receive required from the FromControl regardless

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! Thanks for clarifying 🙌🏻

// eslint-disable-next-line no-console
console.warn('An individual checkbox or radio cannot be a required field.')
console.warn('An individual radio cannot be a required field.')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know console.warn was already here so no need to change - I just wanted to mention that there is a utility function for these cases. And it only runs in the dev environment 🤗

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored to use warning function, thanks for pointing that out 🙏

}
} else {
if (slots.leadingVisual) {
Expand Down Expand Up @@ -138,11 +139,14 @@ const FormControl = React.forwardRef<HTMLDivElement, FormControlProps>(
InputComponent as React.ReactElement<{
id: string
disabled: boolean
required: boolean
['aria-describedby']: string
}>,
{
id,
disabled,
// allow checkboxes to be required
required: required && !isRadioInput,
['aria-describedby']: captionId as string,
},
)}
Expand Down
53 changes: 51 additions & 2 deletions packages/react/src/__tests__/FormControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import axe from 'axe-core'
import {
Autocomplete,
Checkbox,
CheckboxGroup,
FormControl,
Radio,
Select,
Textarea,
TextInput,
Expand Down Expand Up @@ -376,20 +378,67 @@ describe('FormControl', () => {
consoleSpy.mockRestore()
})

it('should warn users if they pass `required` to a checkbox or radio', async () => {
it('should warn users if they pass `required` to a radio', async () => {
const consoleSpy = jest.spyOn(global.console, 'warn').mockImplementation()

render(
<FormControl required>
<FormControl.Label>{LABEL_TEXT}</FormControl.Label>
<Checkbox required />
<Radio value="radio" name="radio" required />
<FormControl.Caption>{CAPTION_TEXT}</FormControl.Caption>
</FormControl>,
)

expect(consoleSpy).toHaveBeenCalled()
consoleSpy.mockRestore()
})

it('should allow required prop to individual checkbox', async () => {
const {getByRole} = render(
<FormControl required>
broccolinisoup marked this conversation as resolved.
Show resolved Hide resolved
<FormControl.Label>{LABEL_TEXT}</FormControl.Label>
<Checkbox />
<FormControl.Caption>{CAPTION_TEXT}</FormControl.Caption>
</FormControl>,
)

expect(getByRole('checkbox')).toBeRequired()
})

it('should not add required prop to individual radio', async () => {
const {getByRole} = render(
<FormControl required>
<FormControl.Label>{LABEL_TEXT}</FormControl.Label>
<Radio value="radio" name="radio" />
<FormControl.Caption>{CAPTION_TEXT}</FormControl.Caption>
</FormControl>,
)

expect(getByRole('radio')).not.toBeRequired()
})

it('should allow required prop on checkbox if part of CheckboxGroup', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test for "should allow required prop on radio button if part of RadioButtonGroup"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's my understanding from this that a radio cannot be required, not even when within a RadioButtonGroup, just the group itself can be required (at the top level). I can add a test for "does not allow required prop on radio button when part of RadioButtonGroup" if you think that'd be worth it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, all good!

const {getByTestId} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Checkboxes</CheckboxGroup.Label>
<FormControl required>
<Checkbox value="checkOne" data-testid="checkbox-1" />
<FormControl.Label>Checkbox one</FormControl.Label>
</FormControl>
<FormControl>
<Checkbox value="checkTwo" data-testid="checkbox-2" />
<FormControl.Label>Checkbox two</FormControl.Label>
</FormControl>
<FormControl>
<Checkbox value="checkThree" />
<FormControl.Label>Checkbox three</FormControl.Label>
</FormControl>
</CheckboxGroup>,
)

expect(getByTestId('checkbox-1')).toBeRequired()
expect(getByTestId('checkbox-2')).not.toBeRequired()
})
})

it('should have no axe violations', async () => {
Expand Down
Loading