Skip to content

Commit

Permalink
Feature: APP-2736 - Implement RadioCard Component (#88)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabricevladimir authored Feb 7, 2024
1 parent 9fdd8a2 commit 96fd0ca
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 3 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Implement `RadioGroup` & `Radio` components
- Implement `RadioGroup`, `Radio`, and `RadioCard` components

## [1.0.11] - 2024-02-06

Expand All @@ -21,7 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Implement `Link`, `InputNumber` `InputTime`, `TextArea` and `TextAreaRichText` components
- Implement `Link`, `InputNumber`, `InputTime`, `TextArea` and `TextAreaRichText` components
- Implement Addon element for `InputText` component
- Handle size property on `Progress` component
- `border-none` Tailwind CSS utility class
Expand Down
2 changes: 1 addition & 1 deletion src/components/link/index.ts
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';
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';
1 change: 1 addition & 0 deletions src/components/radioGroup/radioCard/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { RadioCard, type IRadioCardProps } from './radioCard';
37 changes: 37 additions & 0 deletions src/components/radioGroup/radioCard/radioCard.stories.tsx
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;
73 changes: 73 additions & 0 deletions src/components/radioGroup/radioCard/radioCard.test.tsx
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);
});
});
98 changes: 98 additions & 0 deletions src/components/radioGroup/radioCard/radioCard.tsx
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';
38 changes: 38 additions & 0 deletions src/components/radioGroup/radioGroup/radioGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Radio } from '../radio';
import { RadioCard } from '../radioCard';
import { RadioGroup, type IRadioGroupProps } from './radioGroup';

const meta: Meta<typeof RadioGroup> = {
Expand Down Expand Up @@ -36,6 +37,43 @@ export const Default: Story = {
},
};

/**
* Default usage of the `RadioGroup` component with the RadioCard
*/
export const RadioCardVariant: Story = {
render: (props) => (
<RadioGroup {...props}>
<RadioCard
label="Option one"
description="The best option"
value="1"
avatar="gold"
tag={{ label: 'Gold', variant: 'success' }}
/>
<RadioCard
label="Option two"
description="The 2nd best option"
value="2"
avatar="silver"
tag={{ label: 'Silver' }}
/>
<RadioCard
label="Option three"
description="The 3rd best option"
value="3"
avatar="bronze"
tag={{ label: 'Bronze', variant: 'warning' }}
/>
</RadioGroup>
),
args: {
defaultValue: '2',
name: 'Options',
disabled: false,
onValueChange: undefined,
},
};

const ControlledComponent = (props: IRadioGroupProps) => {
const [value, setValue] = useState('1');
return (
Expand Down

0 comments on commit 96fd0ca

Please sign in to comment.