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

feat: Implement RadioTileGroup component #2826

Merged
merged 12 commits into from
May 3, 2024
14 changes: 8 additions & 6 deletions src/ui/Card/Card.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { cva, VariantProps } from 'cva'
import React from 'react'

import { cn } from 'shared/utils/cn'

const card = cva(['border border-ds-gray-secondary'])
interface CardProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof card> {}

const CardRoot = React.forwardRef<HTMLDivElement, CardProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={card({ className })} {...props} />
<div ref={ref} className={cn(card({ className }))} {...props} />
)
)
CardRoot.displayName = 'Card'
Expand All @@ -20,7 +22,7 @@ interface HeaderProps

const Header = React.forwardRef<HTMLDivElement, HeaderProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={header({ className })} {...props} />
<div ref={ref} className={cn(header({ className }))} {...props} />
)
)
Header.displayName = 'Card.Header'
Expand All @@ -42,7 +44,7 @@ interface TitleProps

const Title = React.forwardRef<HTMLParagraphElement, TitleProps>(
({ className, size, children, ...props }, ref) => (
<h3 ref={ref} className={title({ className, size })} {...props}>
<h3 ref={ref} className={cn(title({ className, size }))} {...props}>
{children}
</h3>
)
Expand All @@ -56,7 +58,7 @@ interface DescriptionProps

const Description = React.forwardRef<HTMLParagraphElement, DescriptionProps>(
({ className, ...props }, ref) => (
<p ref={ref} className={description({ className })} {...props} />
<p ref={ref} className={cn(description({ className }))} {...props} />
)
)
Description.displayName = 'Card.Description'
Expand All @@ -68,7 +70,7 @@ interface ContentProps

const Content = React.forwardRef<HTMLDivElement, ContentProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={content({ className })} {...props} />
<div ref={ref} className={cn(content({ className }))} {...props} />
)
)
Content.displayName = 'Card.Content'
Expand All @@ -80,7 +82,7 @@ interface FooterProps

const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
({ className, ...props }, ref) => (
<div ref={ref} className={footer({ className })} {...props} />
<div ref={ref} className={cn(footer({ className }))} {...props} />
)
)
Footer.displayName = 'Card.Footer'
Expand Down
100 changes: 100 additions & 0 deletions src/ui/RadioTileGroup/RadioTileGroup.spec.tsx
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()
})
})
})
101 changes: 101 additions & 0 deletions src/ui/RadioTileGroup/RadioTileGroup.stories.tsx
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>
)
},
}