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

Add CheckboxGroup and RadioGroup components #830

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/popular-seas-yawn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@primer/react-brand': patch
---

add `CheckboxGroup` and `RadioGroup` components
133 changes: 133 additions & 0 deletions packages/react/src/forms/CheckboxGroup/CheckboxGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React from 'react'
import type {Meta, StoryObj} from '@storybook/react'

import {CheckboxGroup, type CheckboxGroupProps} from '.'
import {Checkbox, FormControl, type FormValidationStatus, Stack} from '../..'

type CheckboxGroupStoryProps = {
labelChildren: string
labelVisuallyHidden: boolean
captionChildren: string
validationChildren: string
validationVariant: FormValidationStatus
}

type MetaProps = CheckboxGroupProps & CheckboxGroupStoryProps

const meta: Meta<MetaProps> = {
title: 'Components/Forms/CheckboxGroup',
component: CheckboxGroup,
args: {
labelChildren: 'Choices',
labelVisuallyHidden: false,
captionChildren: 'Select all that apply',
validationChildren: 'Great job!',
validationVariant: 'success',
},
argTypes: {
labelChildren: {
name: 'Text',
control: 'text',
table: {
category: 'Label',
},
},
labelVisuallyHidden: {
name: 'Visually hidden',
control: 'boolean',
table: {
category: 'Label',
},
},
captionChildren: {
name: 'Text',
control: 'text',
table: {
category: 'Caption',
},
},
validationChildren: {
name: 'Text',
control: 'text',
table: {
category: 'Validation',
},
},
validationVariant: {
name: 'Variant',
options: ['error', 'success'],
control: {type: 'inline-radio'},
table: {
category: 'Validation',
},
},
},
}

export default meta

type Story = StoryObj<MetaProps>

export const Playground: Story = {
render: ({labelChildren, labelVisuallyHidden, captionChildren, validationChildren, validationVariant}) => {
return (
<CheckboxGroup>
<CheckboxGroup.Label visuallyHidden={labelVisuallyHidden}>{labelChildren}</CheckboxGroup.Label>
{captionChildren ? <CheckboxGroup.Caption>{captionChildren}</CheckboxGroup.Caption> : null}

<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>

{validationChildren ? (
<CheckboxGroup.Validation variant={validationVariant}>{validationChildren}</CheckboxGroup.Validation>
) : null}
</CheckboxGroup>
)
},
}

export const Inline: Story = {
args: {
labelChildren: 'Inline example',
labelVisuallyHidden: true,
captionChildren: 'Some inline checkboxes with a visually hidden label',
validationChildren: '',
},
render: ({labelChildren, labelVisuallyHidden, captionChildren, validationChildren, validationVariant}) => {
return (
<CheckboxGroup>
<CheckboxGroup.Label visuallyHidden={labelVisuallyHidden}>{labelChildren}</CheckboxGroup.Label>
{captionChildren ? <CheckboxGroup.Caption>{captionChildren}</CheckboxGroup.Caption> : null}

<Stack direction="horizontal" gap="normal" padding="none">
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>
</Stack>

{validationChildren ? (
<CheckboxGroup.Validation variant={validationVariant}>{validationChildren}</CheckboxGroup.Validation>
) : null}
</CheckboxGroup>
)
},
}
153 changes: 153 additions & 0 deletions packages/react/src/forms/CheckboxGroup/CheckboxGroup.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import React, {render, cleanup} from '@testing-library/react'
import '@testing-library/jest-dom'
import {axe, toHaveNoViolations} from 'jest-axe'

import {CheckboxGroup} from './CheckboxGroup'
import {Checkbox} from '../Checkbox'
import {FormControl} from '../FormControl'

expect.extend(toHaveNoViolations)

describe('CheckboxGroup', () => {
afterEach(cleanup)

it('renders a checkbox group correctly into the document', () => {
const {getByLabelText, getByRole, getByText} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<CheckboxGroup.Caption>You can only pick one</CheckboxGroup.Caption>
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>

<CheckboxGroup.Validation variant="success">Great job!</CheckboxGroup.Validation>
</CheckboxGroup>,
)

expect(getByRole('group', {name: /choices/i})).toBeInTheDocument()
expect(getByText('You can only pick one')).toBeInTheDocument()
expect(getByLabelText('Choice one')).toBeInTheDocument()
expect(getByLabelText('Choice two')).toBeInTheDocument()
expect(getByLabelText('Choice three')).toBeInTheDocument()
expect(getByText('Great job!')).toBeInTheDocument()
})

it('has no a11y violations', async () => {
const {container} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<CheckboxGroup.Caption>You can only pick one</CheckboxGroup.Caption>
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>

<CheckboxGroup.Validation variant="success">Great job!</CheckboxGroup.Validation>
</CheckboxGroup>,
)
const results = await axe(container)

expect(results).toHaveNoViolations()
})

describe('aria-describedby', () => {
it('associates the hint with the input', () => {
const {getByRole, getByText} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<CheckboxGroup.Caption>You can only pick one</CheckboxGroup.Caption>
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>
</CheckboxGroup>,
)

const fieldset = getByRole('group', {name: /choices/i})
const caption = getByText('You can only pick one')

expect(fieldset).toHaveAttribute('aria-describedby', caption.id)
})

it('associates the validation with the input', () => {
const {getByRole, getByText} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>

<CheckboxGroup.Validation variant="error">Uh oh!</CheckboxGroup.Validation>
</CheckboxGroup>,
)

const fieldset = getByRole('group', {name: /choices/i})
const validation = getByText('Uh oh!')

expect(fieldset).toHaveAttribute('aria-describedby', validation.id)
})

it('associates both a hint and validation with the input', () => {
const {getByRole, getByText} = render(
<CheckboxGroup>
<CheckboxGroup.Label>Choices</CheckboxGroup.Label>
<CheckboxGroup.Caption>You can only pick one</CheckboxGroup.Caption>
<FormControl>
<FormControl.Label>Choice one</FormControl.Label>
<Checkbox value="one" defaultChecked />
</FormControl>
<FormControl>
<FormControl.Label>Choice two</FormControl.Label>
<Checkbox value="two" />
</FormControl>
<FormControl>
<FormControl.Label>Choice three</FormControl.Label>
<Checkbox value="three" />
</FormControl>

<CheckboxGroup.Validation variant="success">Great job!</CheckboxGroup.Validation>
</CheckboxGroup>,
)

const fieldset = getByRole('group', {name: /choices/i})
const hint = getByText('You can only pick one')
const validation = getByText('Great job!')

expect(fieldset).toHaveAttribute('aria-describedby', `${hint.id} ${validation.id}`)
})
})
})
14 changes: 14 additions & 0 deletions packages/react/src/forms/CheckboxGroup/CheckboxGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {
InputGroup,
type InputGroupCaptionProps,
type InputGroupLabelProps,
type InputGroupProps,
type InputGroupValidationProps,
} from '../InputGroup'

export type CheckboxGroupProps = InputGroupProps
export type CheckboxGroupLabelProps = InputGroupLabelProps
export type CheckboxGroupCaptionProps = InputGroupCaptionProps
export type CheckboxGroupValidationProps = InputGroupValidationProps

export const CheckboxGroup = InputGroup
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Do not modify this file directly.
* This file was generated by: playwright.generate-tests.ts.
* Regenerate using: npm run test:visual:generate
*/
import {test, expect} from '@playwright/test'

// eslint-disable-next-line i18n-text/no-en
test.describe('Visual Comparison: CheckboxGroup', () => {
test('CheckboxGroup / Playground', async ({page}) => {
await page.goto(
'http://localhost:6006/iframe.html?args=&id=components-forms-checkboxgroup--playground&viewMode=story',
)

await page.waitForTimeout(500)
expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
})

test('CheckboxGroup / Inline', async ({page}) => {
await page.goto('http://localhost:6006/iframe.html?args=&id=components-forms-checkboxgroup--inline&viewMode=story')

await page.waitForTimeout(500)
expect(await page.screenshot({fullPage: true})).toMatchSnapshot()
})
})
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.
1 change: 1 addition & 0 deletions packages/react/src/forms/CheckboxGroup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './CheckboxGroup'
Loading
Loading