Skip to content

Commit

Permalink
radio card documentation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabricevladimir committed Feb 7, 2024
1 parent a319439 commit 59e66bb
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 19 deletions.
1 change: 1 addition & 0 deletions src/components/radioGroup/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './radio';
export * from './radioCard';
export * from './radioGroup';
12 changes: 8 additions & 4 deletions src/components/radioGroup/radioCard/radioCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ const meta: Meta<typeof RadioCard> = {

type Story = StoryObj<typeof RadioCard>;

/**
* Default usage of the `RadioCard` component
*/
export const Default: Story = {
render: (props) => (
<RadioGroup>
<RadioCard {...props} />
</RadioGroup>
),
args: {
value: 'value',
label: 'This is a label',
description: 'This is the description',
tag: { label: 'Tag', variant: 'info' },
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' },
},
};

Expand Down
87 changes: 87 additions & 0 deletions src/components/radioGroup/radioCard/radioCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { RadioGroup } from '..';
import { IconType } from '../../icon';
import { RadioCard, type IRadioCardProps } from './radioCard';

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>
);
};

const originalGlobalImage = global.Image;

beforeAll(() => {
(window.Image as unknown) = class MockImage {
onload: () => void = () => {};
src: string = '';
constructor() {
setTimeout(() => {
this.onload();
}, 100);
}
};
});

afterAll(() => {
global.Image = originalGlobalImage;
});

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(await screen.findByRole('img')).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);
});
});
53 changes: 38 additions & 15 deletions src/components/radioGroup/radioCard/radioCard.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,46 @@
import * as RadioGroup from '@radix-ui/react-radio-group';
import { RadioGroupIndicator, RadioGroupItem, type RadioGroupItemProps } from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import { useId, type ComponentPropsWithoutRef } from 'react';
import { forwardRef, useId } from 'react';
import { Avatar } from '../../avatars';
import { Icon, IconType } from '../../icon';
import { Tag, type ITagProps } from '../../tag';

export interface IRadioCardProps extends ComponentPropsWithoutRef<'button'> {
disabled?: boolean;
label: string;
export interface IRadioCardProps extends RadioGroupItemProps {
/**
* Radio card avatar image source
*/
avatar?: string;
/**
* Description
*/
description: string;
value: string;
/**
* Radio label
*/
label: string;
/**
* Radio card tag
*/
tag?: ITagProps;
avatar?: string;
}

export const RadioCard: React.FC<IRadioCardProps> = (props) => {
/**
* `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 containerClasses = classNames(
'group h-16 truncate 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
'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
Expand All @@ -39,12 +58,14 @@ export const RadioCard: React.FC<IRadioCardProps> = (props) => {
);

return (
<RadioGroup.Item value={value} {...rest} id={processedId} className={containerClasses}>
<RadioGroupItem className={containerClasses} id={processedId} ref={ref} value={value} {...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}>{label}</p>
<label className={labelClasses} htmlFor={processedId}>
{label}
</label>
<p className={baseTextClasses}>{description}</p>
</div>
{tag?.label && <Tag {...tag} />}
Expand All @@ -54,14 +75,16 @@ export const RadioCard: React.FC<IRadioCardProps> = (props) => {
icon={IconType.RADIO_DEFAULT}
className="text-neutral-300 group-data-[state=checked]:hidden"
/>
<RadioGroup.Indicator asChild>
<RadioGroupIndicator>
<Icon
icon={IconType.RADIO_CHECK}
className="text-primary-400 group-disabled:text-neutral-500"
/>
</RadioGroup.Indicator>
</RadioGroupIndicator>
</span>
</div>
</RadioGroup.Item>
</RadioGroupItem>
);
};
});

RadioCard.displayName = 'RadioCard';

0 comments on commit 59e66bb

Please sign in to comment.