-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Implement RadioTileGroup component (#2826)
* First pass at RadioTileGroup * Add test for selection behaviour * Use correct blue color * Remove unnecessary styles * Remove space * Add Title and Description subcomponents * Fix tests * Await user interactions in tests * Add controlled input story * Fix test * Make item label a proper input label * Use cn util for styles
- Loading branch information
1 parent
4ce2a59
commit 4a402db
Showing
5 changed files
with
353 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { render, screen } from '@testing-library/react' | ||
import { userEvent } from '@testing-library/user-event' | ||
|
||
import { RadioTileGroup } from './RadioTileGroup' | ||
|
||
describe('RadioTileGroup', () => { | ||
function setup() { | ||
return { | ||
user: userEvent.setup(), | ||
} | ||
} | ||
|
||
it('renders', async () => { | ||
render( | ||
<RadioTileGroup> | ||
<RadioTileGroup.Item value="asdf"> | ||
<RadioTileGroup.Label>Asdf</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="jkl;"> | ||
<RadioTileGroup.Label>Jkl;</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
) | ||
const item1 = await screen.findByText('Asdf') | ||
expect(item1).toBeInTheDocument() | ||
const item2 = await screen.findByText('Jkl;') | ||
expect(item2).toBeInTheDocument() | ||
}) | ||
|
||
describe('item title', () => { | ||
it('has htmlFor attribute when used inside Item', async () => { | ||
render( | ||
<RadioTileGroup> | ||
<RadioTileGroup.Item value="test"> | ||
<RadioTileGroup.Label>Label</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
) | ||
const label = await screen.findByText('Label') | ||
expect(label).toBeInTheDocument() | ||
expect(label.hasAttribute('for')).toBeTruthy() | ||
}) | ||
|
||
it('does not have htmlFor attribute when used outside of Item', async () => { | ||
render(<RadioTileGroup.Label>Label</RadioTileGroup.Label>) | ||
const label = await screen.findByText('Label') | ||
expect(label).toBeInTheDocument() | ||
expect(label.hasAttribute('for')).toBeFalsy() | ||
}) | ||
}) | ||
|
||
describe('item description', () => { | ||
it('renders', async () => { | ||
render( | ||
<RadioTileGroup> | ||
<RadioTileGroup.Item value="asdf"> | ||
<RadioTileGroup.Label>Asdf</RadioTileGroup.Label> | ||
<RadioTileGroup.Description> | ||
This is a description. | ||
</RadioTileGroup.Description> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
) | ||
const description = await screen.findByText('This is a description.') | ||
expect(description).toBeInTheDocument() | ||
}) | ||
}) | ||
|
||
describe('when an item is clicked', () => { | ||
it('toggles selected circle', async () => { | ||
const { user } = setup() | ||
render( | ||
<RadioTileGroup> | ||
<RadioTileGroup.Item value="asdf"> | ||
<RadioTileGroup.Label>Asdf</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="jkl;"> | ||
<RadioTileGroup.Label>Jkl;</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
) | ||
const tile = await screen.findByText('Asdf') | ||
const tile2 = await screen.findByText('Jkl;') | ||
|
||
await user.click(tile) | ||
|
||
const selected = await screen.findByTestId('radio-button-circle-selected') | ||
expect(selected).toBeInTheDocument() | ||
|
||
await user.click(tile2) | ||
|
||
expect(selected).not.toBeInTheDocument() | ||
|
||
const otherSelected = await screen.findByTestId( | ||
'radio-button-circle-selected' | ||
) | ||
expect(otherSelected).toBeInTheDocument() | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
import { Meta, StoryObj } from '@storybook/react' | ||
import { useState } from 'react' | ||
|
||
import { RadioTileGroup } from './RadioTileGroup' | ||
|
||
type RadioTileGroupStory = React.ComponentProps<typeof RadioTileGroup> & { | ||
flex: 1 | 'none' | ||
} | ||
|
||
const meta: Meta<RadioTileGroupStory> = { | ||
title: 'Components/RadioTileGroup', | ||
component: RadioTileGroup, | ||
argTypes: { | ||
direction: { | ||
description: 'Controls the flex direction of the RadioTileGroup', | ||
control: 'radio', | ||
options: ['row', 'col'], | ||
}, | ||
flex: { | ||
description: 'Toggles between the item flexing and not', | ||
control: 'radio', | ||
options: [1, 'none'], | ||
}, | ||
}, | ||
} | ||
export default meta | ||
|
||
type Story = StoryObj<RadioTileGroupStory> | ||
|
||
export const Default: Story = { | ||
args: { | ||
direction: 'row', | ||
flex: 1, | ||
}, | ||
render: (args) => ( | ||
<RadioTileGroup className="w-full" direction={args.direction}> | ||
<RadioTileGroup.Item value="radio" flex={args.flex}> | ||
<RadioTileGroup.Label>Radio</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="tile" flex={args.flex}> | ||
<RadioTileGroup.Label>Tile</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="group" flex={args.flex}> | ||
<RadioTileGroup.Label>Group</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
), | ||
} | ||
|
||
export const WithDescription: Story = { | ||
args: { | ||
direction: 'row', | ||
flex: 1, | ||
}, | ||
render: (args) => ( | ||
<RadioTileGroup className="w-full" direction={args.direction}> | ||
<RadioTileGroup.Item value="description" flex={args.flex}> | ||
<RadioTileGroup.Label>Description</RadioTileGroup.Label> | ||
<RadioTileGroup.Description> | ||
A RadioTileGroup Item can optionally have a description | ||
</RadioTileGroup.Description> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="noDescription" flex={args.flex}> | ||
<RadioTileGroup.Label>No Description</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
), | ||
} | ||
|
||
export const WithControlledInput: Story = { | ||
args: { | ||
direction: 'row', | ||
flex: 1, | ||
}, | ||
render: (args) => { | ||
// eslint-disable-next-line react-hooks/rules-of-hooks | ||
const [value, setValue] = useState<string | undefined>(undefined) | ||
|
||
return ( | ||
<RadioTileGroup | ||
className="w-full" | ||
direction={args.direction} | ||
value={value} | ||
onValueChange={(value) => { | ||
setValue(value) | ||
// controlled input state isn't always required, you can also do things here ... like navigation etc. | ||
}} | ||
> | ||
<RadioTileGroup.Item value="radio" flex={args.flex}> | ||
<RadioTileGroup.Label>Radio</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="tile" flex={args.flex}> | ||
<RadioTileGroup.Label>Tile</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
<RadioTileGroup.Item value="group" flex={args.flex}> | ||
<RadioTileGroup.Label>Group</RadioTileGroup.Label> | ||
</RadioTileGroup.Item> | ||
</RadioTileGroup> | ||
) | ||
}, | ||
} |
Oops, something went wrong.