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 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
885dd26
add CheckboxGroup, RadioGroup, and InputGroup components
joshfarrant Nov 21, 2024
09ba756
add changeset
joshfarrant Nov 25, 2024
50b888c
update snapshots
joshfarrant Nov 25, 2024
c3f881d
improve changeset
joshfarrant Nov 25, 2024
b71c4c2
remove unused InputGroup test file
joshfarrant Nov 25, 2024
e43f7fb
fixed broken animation
joshfarrant Nov 25, 2024
aca86dc
github-actions[bot] Regenerated snapshots
joshfarrant Nov 25, 2024
790fa13
rename InputGroup to ControlGroup
joshfarrant Nov 25, 2024
526ae02
reset snapshots
joshfarrant Nov 26, 2024
a9aca70
reduce Caption and Validation font-size to 100
joshfarrant Nov 26, 2024
52cc180
rename CSS declaration file
joshfarrant Nov 26, 2024
810a183
github-actions[bot] Regenerated snapshots
joshfarrant Nov 26, 2024
d65b102
add documentation for CheckboxGroup and RadioGroup components
joshfarrant Nov 27, 2024
4b9b5f4
reduce font-weight on FormControl labels within a ControlGroup
joshfarrant Nov 27, 2024
78366b2
github-actions[bot] Regenerated snapshots
joshfarrant Nov 27, 2024
7bd4fea
update changeset
joshfarrant Nov 27, 2024
0fd5510
update changeset semver bump
joshfarrant Nov 27, 2024
df783b5
various docs updates based on PR feedback
joshfarrant Nov 27, 2024
fa4729d
leverage Text component
joshfarrant Nov 27, 2024
17aefc5
split stories up into multiple fies
joshfarrant Nov 27, 2024
8405890
remove regex
joshfarrant Nov 27, 2024
3bbaec1
updated docs to align with prc
joshfarrant Nov 27, 2024
d3ab439
fix tabs in React docs
joshfarrant Nov 27, 2024
1aa22e8
fix case
joshfarrant Nov 27, 2024
8e91013
stop forwarding unused prop to Radio
joshfarrant Nov 27, 2024
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
joshfarrant marked this conversation as resolved.
Show resolved Hide resolved
---

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>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does Storybook provide a built-in type for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

StoryObj is the built-in, recommended type. See the example in the docs here.


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 = {
Copy link
Collaborator

@rezrah rezrah Nov 27, 2024

Choose a reason for hiding this comment

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

Could we move this story into a separate CheckboxGroup.features.stories.tsx file and organize it inside a CheckboxGroup/Examples or CheckboxGroup/Features subfolder please? Probably former in this case as it's not a feature of the component.

I know the other form control stories don't follow this format. It's because those predate the Primer ADR around story organization and also need to be updated at some point.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let me know what you think about my approach here

17aefc5

I wanted to keep the interactivity of the Inline story as it currently works nicely with the controls, but I didn't want to duplicate the controls set up (args, argTypes) across both files, so I've brought the meta object defined in the main story file into the examples file and reused it there, just with a modified title.

lmk if you're happy with this

args: {
labelChildren: 'Inline example',
labelVisuallyHidden: true,
joshfarrant marked this conversation as resolved.
Show resolved Hide resolved
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()
joshfarrant marked this conversation as resolved.
Show resolved Hide resolved
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