Skip to content

Commit

Permalink
feat(APP-3630): Update Radio/Checkbox Card components card to accept …
Browse files Browse the repository at this point in the history
…children property rendered when selected (#310)
  • Loading branch information
shan8851 authored Oct 14, 2024
1 parent 0afc311 commit 179cf12
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 40 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## [Unreleased]

### Added

- Add optional `children` property to `RadioCard` and `CheckboxCard` core components displayed only when component is
selected

### Changed

- Update core components `RadioCard` and `CheckboxCard` to have optional description property and correct alignment
when no description provided
- Bump `actions/checkout` from 4.2.0 to 4.2.1
- Update minor and patch NPM dependencies

Expand Down
25 changes: 24 additions & 1 deletion src/core/components/forms/checkboxCard/checkboxCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,35 @@ type Story = StoryObj<typeof CheckboxCard>;
* Default usage of the CheckboxCard component
*/
export const Default: Story = {
render: (props) => <CheckboxCard {...props} />,
args: {
avatar: 'https://assets-global.website-files.com/5e997428d0f2eb13a90aec8c/63f47db62df04b569e4e004e_icon_aragon.svg',
label: 'Checkbox label',
tag: { label: 'Tag', variant: 'info' },
},
};

/**
* Default usage of the CheckboxCard component
*/
export const WithDescription: Story = {
args: {
avatar: 'https://assets-global.website-files.com/5e997428d0f2eb13a90aec8c/63f47db62df04b569e4e004e_icon_aragon.svg',
label: 'Checkbox label',
description: 'Checkbox description',
tag: { label: 'Tag', variant: 'info' },
},
};

/**
* Usage of the CheckboxCard component with children when checked
*/
export const WithChildrenWhenChecked: Story = {
args: {
avatar: 'https://assets-global.website-files.com/5e997428d0f2eb13a90aec8c/63f47db62df04b569e4e004e_icon_aragon.svg',
label: 'Checkbox label',
description: 'Checkbox description',
tag: { label: 'Tag', variant: 'info' },
children: <div>Children</div>,
},
};

Expand Down
6 changes: 6 additions & 0 deletions src/core/components/forms/checkboxCard/checkboxCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,10 @@ describe('<CheckboxCard /> component', () => {
render(createTestComponent({ checked }));
expect(screen.getByTestId(IconType.CHECKBOX_INDETERMINATE)).toBeVisible();
});

it('renders the children when checkbox is checked', () => {
const checked = true;
render(createTestComponent({ checked, children: <div data-testid="children" /> }));
expect(screen.getByTestId('children')).toBeVisible();
});
});
92 changes: 61 additions & 31 deletions src/core/components/forms/checkboxCard/checkboxCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as RadixCheckbox from '@radix-ui/react-checkbox';
import classNames from 'classnames';
import { forwardRef, type ComponentProps } from 'react';
import { forwardRef, type ComponentProps, type ReactNode } from 'react';
import { useRandomId } from '../../../hooks';
import { Avatar } from '../../avatars';
import { Icon, IconType } from '../../icon';
Expand All @@ -19,7 +19,7 @@ export interface ICheckboxCardProps extends ComponentProps<'button'> {
/**
* Description of the checkbox.
*/
description: string;
description?: string;
/**
* Optional tag for the checkbox.
*/
Expand All @@ -40,10 +40,26 @@ export interface ICheckboxCardProps extends ComponentProps<'button'> {
* Id of the checkbox.
*/
id?: string;
/**
* Additional children to render when the checkbox is checked.
*/
children?: ReactNode;
}

export const CheckboxCard = forwardRef<HTMLButtonElement, ICheckboxCardProps>((props, ref) => {
const { id, avatar, label, description, tag, className, checked, onCheckedChange, disabled, ...otherProps } = props;
const {
id,
avatar,
label,
description,
tag,
className,
checked,
onCheckedChange,
disabled,
children,
...otherProps
} = props;

const randomId = useRandomId(id);
const labelId = `${randomId}-label`;
Expand All @@ -57,7 +73,7 @@ export const CheckboxCard = forwardRef<HTMLButtonElement, ICheckboxCardProps>((p
onCheckedChange={onCheckedChange}
disabled={disabled}
className={classNames(
'group flex h-16 min-w-0 flex-row items-center gap-3 outline-none transition-all md:h-20', // Layout
'group flex min-w-0 flex-col gap-3 outline-none transition-all', // Layout
'rounded-xl border bg-neutral-0 px-4 py-3 md:gap-4 md:px-6 md:py-4', // Style
'focus:outline-none focus-visible:ring focus-visible:ring-primary focus-visible:ring-offset', // Focus
'border-primary-400 shadow-primary hover:shadow-primary-md', // Checked/indeterminate & hover
Expand All @@ -69,37 +85,51 @@ export const CheckboxCard = forwardRef<HTMLButtonElement, ICheckboxCardProps>((p
)}
{...otherProps}
>
{avatar && <Avatar size="sm" responsiveSize={{ md: 'md' }} src={avatar} />}
<div className="flex min-w-0 flex-1 flex-col items-start gap-0.5 text-sm font-normal leading-tight md:gap-1 md:text-base">
<p
id={randomId}
className={classNames(
'max-w-full cursor-pointer truncate text-neutral-800 group-data-[state=unchecked]:text-neutral-800',
'group-data-[disabled]:cursor-default group-data-[disabled]:group-data-[state=unchecked]:text-neutral-300',
<div className={classNames('flex w-full min-w-0 flex-row gap-3', { 'items-center': !description })}>
{avatar && <Avatar size="sm" responsiveSize={{ md: 'md' }} src={avatar} />}
<div className="flex min-w-0 flex-1 flex-col items-start gap-0.5 text-sm font-normal leading-tight md:gap-1 md:text-base">
<p
id={randomId}
className={classNames(
'max-w-full cursor-pointer truncate text-neutral-800 group-data-[state=unchecked]:text-neutral-800',
'group-data-[disabled]:cursor-default group-data-[disabled]:group-data-[state=unchecked]:text-neutral-300',
)}
>
{label}
</p>
{description && (
<p className="max-w-full truncate text-neutral-500 group-data-[disabled]:text-neutral-300">
{description}
</p>
)}
>
{label}
</p>
<p className="max-w-full truncate text-neutral-500 group-data-[disabled]:text-neutral-300">
{description}
</p>
</div>
{tag && <Tag {...tag} className={classNames('self-start', tag.className)} />}
<Icon
icon={IconType.CHECKBOX}
size="md"
className={classNames(
'mt-0.5 hidden self-start text-neutral-400 group-data-[state=unchecked]:block group-data-[disabled]:text-neutral-300 md:mt-1',
)}
/>
<RadixCheckbox.Indicator className="mt-0.5 self-start text-primary-400 group-data-[disabled]:text-neutral-500 md:mt-1">
<Icon icon={IconType.CHECKBOX_SELECTED} size="md" className="hidden group-data-[state=checked]:block" />
</div>
{tag && <Tag {...tag} className={classNames(tag.className, { 'self-start': description })} />}
<Icon
icon={IconType.CHECKBOX_INDETERMINATE}
icon={IconType.CHECKBOX}
size="md"
className="hidden group-data-[state=indeterminate]:block"
className={classNames(
'mt-0.5 hidden text-neutral-400 group-data-[state=unchecked]:block group-data-[disabled]:text-neutral-300 md:mt-1',
{ 'self-start': description },
)}
/>
</RadixCheckbox.Indicator>
<RadixCheckbox.Indicator
className={classNames('mt-0.5 text-primary-400 group-data-[disabled]:text-neutral-500 md:mt-1', {
'self-start': description,
})}
>
<Icon
icon={IconType.CHECKBOX_SELECTED}
size="md"
className="hidden group-data-[state=checked]:block"
/>
<Icon
icon={IconType.CHECKBOX_INDETERMINATE}
size="md"
className="hidden group-data-[state=indeterminate]:block"
/>
</RadixCheckbox.Indicator>
</div>
{children && <div className="hidden group-data-[state=checked]:block">{children}</div>}
</RadixCheckbox.Root>
);
});
Expand Down
36 changes: 36 additions & 0 deletions src/core/components/forms/radioCard/radioCard.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,41 @@ 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',
tag: { label: 'Platinum' },
},
};

/**
* Default usage of the `RadioCard` component with description
*/
export const WithDescription: 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' },
},
};

/**
* Usage of the `RadioCard` component with children when selected
*/
export const WithChildrenWhenSelected: Story = {
render: (props) => (
<RadioGroup>
<RadioCard {...props} />
Expand All @@ -33,6 +68,7 @@ export const Default: Story = {
label: 'Option one',
description: 'The best option ever',
tag: { label: 'Platinum' },
children: <div>Children</div>,
},
};

Expand Down
13 changes: 13 additions & 0 deletions src/core/components/forms/radioCard/radioCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,17 @@ describe('<RadioCard/> component', () => {

expect(screen.getByRole('radio')).toHaveValue(value);
});

it('renders children when radio button is checked', async () => {
const user = userEvent.setup();

const children = 'test-children';
render(createTestComponent({ children }));

const radioButton = screen.getByRole('radio');

await user.click(radioButton);
expect(screen.getByText(children)).toBeVisible();
expect(screen.getByRole('radio')).toBeChecked();
});
});
25 changes: 17 additions & 8 deletions src/core/components/forms/radioCard/radioCard.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RadioGroupIndicator, RadioGroupItem } from '@radix-ui/react-radio-group';
import classNames from 'classnames';
import { forwardRef, type ComponentProps } from 'react';
import { forwardRef, type ComponentProps, type ReactNode } from 'react';
import { useRandomId } from '../../../hooks';
import { Avatar } from '../../avatars';
import { Icon, IconType } from '../../icon';
Expand All @@ -14,7 +14,7 @@ export interface IRadioCardProps extends ComponentProps<'button'> {
/**
* Description
*/
description: string;
description?: string;
/**
* Radio label
*/
Expand All @@ -31,16 +31,20 @@ export interface IRadioCardProps extends ComponentProps<'button'> {
* Indicates if the radio is disabled.
*/
disabled?: boolean;
/**
* Additional children to render when the radio is selected.
*/
children?: ReactNode;
}

export const RadioCard = forwardRef<HTMLButtonElement, IRadioCardProps>((props, ref) => {
const { value, id, className, tag, avatar, label, description, disabled, ...rest } = props;
const { value, id, className, tag, avatar, label, description, disabled, children, ...rest } = props;

const randomId = useRandomId(id);
const labelId = `${randomId}-label`;

const containerClasses = classNames(
'group h-16 rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 shadow-neutral-sm outline-none transition-all md:h-20 md:rounded-2xl md:px-6 md:py-4', // default
'group flex w-full flex-col gap-3 rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 shadow-neutral-sm outline-none transition-all 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 hover:data-[state=checked]:shadow-primary-md', // hover
Expand All @@ -67,18 +71,22 @@ export const RadioCard = forwardRef<HTMLButtonElement, IRadioCardProps>((props,
aria-labelledby={labelId}
{...rest}
>
<div className="flex h-full items-center gap-x-3 md:gap-x-4">
<div className="flex size-full items-center gap-x-3 md:gap-x-4">
{avatar && <Avatar size="sm" responsiveSize={{ md: 'md' }} src={avatar} />}
<div className="flex min-w-0 flex-1 gap-x-0.5 md:gap-x-4">
<div
className={classNames('flex min-w-0 flex-1 gap-x-0.5 md:gap-x-4', {
'items-center': !description,
})}
>
<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>
{description && <p className={baseTextClasses}>{description}</p>}
</div>
{tag && <Tag {...tag} />}
</div>
<span className="h-full">
<span className={classNames({ 'h-full': description })}>
<Icon icon={IconType.RADIO} className="text-neutral-300 group-data-[state=checked]:hidden" />
<RadioGroupIndicator>
<Icon
Expand All @@ -88,6 +96,7 @@ export const RadioCard = forwardRef<HTMLButtonElement, IRadioCardProps>((props,
</RadioGroupIndicator>
</span>
</div>
{children && <div className="hidden group-data-[state=checked]:block">{children}</div>}
</RadioGroupItem>
);
});
Expand Down

0 comments on commit 179cf12

Please sign in to comment.