-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: APP-2736 - Implement RadioCard Component (#88)
- Loading branch information
1 parent
9fdd8a2
commit 96fd0ca
Showing
8 changed files
with
251 additions
and
3 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 |
---|---|---|
@@ -1,2 +1,2 @@ | ||
export { Link } from './link'; | ||
export { ILinkProps, LinkVariant } from './link.api'; | ||
export { type ILinkProps, type LinkVariant } from './link.api'; |
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 |
---|---|---|
@@ -1,2 +1,3 @@ | ||
export * from './radio'; | ||
export * from './radioCard'; | ||
export * from './radioGroup'; |
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 @@ | ||
export { RadioCard, type IRadioCardProps } from './radioCard'; |
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,37 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { RadioGroup } from '../radioGroup'; | ||
import { RadioCard } from './radioCard'; | ||
|
||
const meta: Meta<typeof RadioCard> = { | ||
title: 'components/RadioGroup/RadioCard', | ||
component: RadioCard, | ||
tags: ['autodocs'], | ||
parameters: { | ||
design: { | ||
type: 'figma', | ||
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/jfKRr1V9evJUp1uBeyP3Zz/Aragon-ODS?type=design&node-id=10095-19157&mode=design&t=FsK7MCOZgi86zSuS-0', | ||
}, | ||
}, | ||
}; | ||
|
||
type Story = StoryObj<typeof RadioCard>; | ||
|
||
/** | ||
* Default usage of the `RadioCard` component | ||
*/ | ||
export const Default: Story = { | ||
render: (props) => ( | ||
<RadioGroup> | ||
<RadioCard {...props} /> | ||
</RadioGroup> | ||
), | ||
args: { | ||
avatar: 'https://assets-global.website-files.com/5e997428d0f2eb13a90aec8c/63f47db62df04b569e4e004e_icon_aragon.svg', | ||
value: '1', | ||
label: 'Option one', | ||
description: 'The best option ever', | ||
tag: { label: 'Platinum' }, | ||
}, | ||
}; | ||
|
||
export default meta; |
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,73 @@ | ||
import { fireEvent, render, screen } from '@testing-library/react'; | ||
import { RadioGroup } from '..'; | ||
import { IconType } from '../../icon'; | ||
import { RadioCard, type IRadioCardProps } from './radioCard'; | ||
|
||
jest.mock('../../avatars', () => ({ | ||
Avatar: () => <div data-testid="avatar" />, | ||
})); | ||
|
||
describe('<RadioCard/> component', () => { | ||
const createTestComponent = (props?: Partial<IRadioCardProps>) => { | ||
const completeProps = { label: 'test label', value: 'test value', description: 'test description', ...props }; | ||
|
||
return ( | ||
<RadioGroup name="Test Group"> | ||
<RadioCard {...completeProps} />; | ||
</RadioGroup> | ||
); | ||
}; | ||
|
||
it('renders with avatar, label, description, tag, and unchecked radio button', async () => { | ||
const avatar = 'avatar'; | ||
const description = 'Test Description'; | ||
const label = 'Test Label'; | ||
const tag = { label: 'Tag Label' }; | ||
|
||
render(createTestComponent({ avatar, description, label, tag })); | ||
|
||
const radioButton = screen.getByRole('radio'); | ||
|
||
expect(radioButton).toBeInTheDocument(); | ||
expect(radioButton).not.toBeChecked(); | ||
expect(screen.getByText(label)).toBeInTheDocument(); | ||
expect(screen.getByText(description)).toBeInTheDocument(); | ||
expect(screen.getByText(tag.label)).toBeInTheDocument(); | ||
expect(screen.getByTestId('avatar')).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the RADIO_DEFAULT icon when unchecked', () => { | ||
render(createTestComponent()); | ||
|
||
const uncheckedIcon = screen.getByTestId(IconType.RADIO_DEFAULT); | ||
|
||
expect(uncheckedIcon).toBeVisible(); | ||
expect(screen.getByRole('radio')).not.toBeChecked(); | ||
}); | ||
|
||
it('renders the RADIO_CHECK icon when checked', () => { | ||
render(createTestComponent()); | ||
|
||
const radioButton = screen.getByRole('radio'); | ||
|
||
fireEvent.click(radioButton); | ||
const checkedIcon = screen.getByTestId(IconType.RADIO_CHECK); | ||
|
||
expect(checkedIcon).toBeVisible(); | ||
expect(screen.getByRole('radio')).toBeChecked(); | ||
}); | ||
|
||
it('disables the radio button when disabled prop is true', () => { | ||
render(createTestComponent({ disabled: true })); | ||
|
||
expect(screen.getByRole('radio')).toBeDisabled(); | ||
}); | ||
|
||
it('sets the radio button value correctly', () => { | ||
const value = 'Test value'; | ||
|
||
render(createTestComponent({ value })); | ||
|
||
expect(screen.getByRole('radio')).toHaveValue(value); | ||
}); | ||
}); |
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,98 @@ | ||
import { RadioGroupIndicator, RadioGroupItem, type RadioGroupItemProps } from '@radix-ui/react-radio-group'; | ||
import classNames from 'classnames'; | ||
import { forwardRef, useId } from 'react'; | ||
import { Avatar } from '../../avatars'; | ||
import { Icon, IconType } from '../../icon'; | ||
import { Tag, type ITagProps } from '../../tag'; | ||
|
||
export interface IRadioCardProps extends RadioGroupItemProps { | ||
/** | ||
* Radio card avatar image source | ||
*/ | ||
avatar?: string; | ||
/** | ||
* Description | ||
*/ | ||
description: string; | ||
/** | ||
* Radio label | ||
*/ | ||
label: string; | ||
/** | ||
* Radio card tag | ||
*/ | ||
tag?: ITagProps; | ||
} | ||
|
||
/** | ||
* `RadioCard` component | ||
* | ||
* This component is based on the Radix-UI radio implementation. | ||
* An exhaustive list of its properties can be found in the corresponding Radix primitive | ||
* [documentation](https://www.radix-ui.com/primitives/docs/components/radio-group#item). | ||
* | ||
* **NOTE**: The component must be used inside a `<RadioGroup />` component in order to work properly. | ||
*/ | ||
export const RadioCard = forwardRef<HTMLButtonElement, IRadioCardProps>((props, ref) => { | ||
const { value, id, className, tag, avatar, label, description, ...rest } = props; | ||
|
||
const randomId = useId(); | ||
const processedId = id ?? randomId; | ||
const labelId = `${processedId}-label`; | ||
|
||
const containerClasses = classNames( | ||
'group h-16 rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 md:h-20 md:rounded-2xl md:px-6 md:py-4', // default | ||
'data-[state=checked]:border-primary-400 data-[state=checked]:shadow-primary', // checked | ||
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // focus | ||
'hover:border-neutral-200 hover:shadow-neutral-md hover:data-[state=checked]:shadow-primary-md', // hover | ||
'disabled:border-neutral-300 disabled:bg-neutral-100 disabled:shadow-none', // disabled | ||
'disabled:data-[state=checked]:border-neutral-300 disabled:data-[state=checked]:shadow-none', // disabled & checked | ||
className, | ||
); | ||
|
||
const baseTextClasses = | ||
'text-sm leading-tight text-left text-neutral-500 md:text-base w-full group-disabled:text-neutral-300 truncate'; | ||
|
||
const labelClasses = classNames( | ||
baseTextClasses, | ||
'group-data-[state=checked]:text-neutral-800 group-disabled:group-data-[state=checked]:text-neutral-800', | ||
); | ||
|
||
return ( | ||
<RadioGroupItem | ||
id={processedId} | ||
ref={ref} | ||
value={value} | ||
className={containerClasses} | ||
aria-labelledby={labelId} | ||
{...rest} | ||
> | ||
<div className="flex h-full items-center gap-x-3 md:gap-x-4"> | ||
{avatar && <Avatar responsiveSize={{ sm: 'sm', md: 'md' }} src={avatar} className="" />} | ||
<div className="flex min-w-0 flex-1 gap-x-0.5 md:gap-x-4"> | ||
<div className="flex min-w-0 flex-1 flex-col gap-y-0.5 md:gap-y-1"> | ||
<p className={labelClasses} id={labelId}> | ||
{label} | ||
</p> | ||
<p className={baseTextClasses}>{description}</p> | ||
</div> | ||
{tag?.label && <Tag {...tag} />} | ||
</div> | ||
<span className="h-full"> | ||
<Icon | ||
icon={IconType.RADIO_DEFAULT} | ||
className="text-neutral-300 group-data-[state=checked]:hidden" | ||
/> | ||
<RadioGroupIndicator> | ||
<Icon | ||
icon={IconType.RADIO_CHECK} | ||
className="text-primary-400 group-disabled:text-neutral-500" | ||
/> | ||
</RadioGroupIndicator> | ||
</span> | ||
</div> | ||
</RadioGroupItem> | ||
); | ||
}); | ||
|
||
RadioCard.displayName = 'RadioCard'; |
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