Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
04f4526
test(CardHorizontal): 💍 Add visual regression tests and expanded stories
punkbit Apr 15, 2026
cda7ad4
fix(CardHorizontal): 🐛 Add GridCenter wrapper decorator for consisten…
punkbit Apr 15, 2026
09cc5cb
chore: 🤖 update snapshots
punkbit Apr 15, 2026
90a3a50
test(CardHorizontal): 💍 Add test coverage for disabled+selected state
punkbit Apr 15, 2026
90eff3e
test(CardHorizontal): 💍 Fix flaky keyboard navigation test
punkbit Apr 15, 2026
e9a09f3
test(CardHorizontal): 💍 Add assertion to click event test
punkbit Apr 15, 2026
f1d1245
test(CardHorizontal): 💍 Use fixed pixel width for deterministic snaps…
punkbit Apr 15, 2026
1d26dca
test(CardHorizontal): 💍 Add visual regression snapshots
punkbit Apr 15, 2026
dad24eb
fix(CardHorizontal): 🐛 Add aria-selected for accessible selection state
punkbit Apr 15, 2026
ae01c03
test(CardHorizontal): 💍 Add unit tests for aria-selected
punkbit Apr 15, 2026
894ae1d
test(CardHorizontal): 💍 Add aria-selected assertions to selected stat…
punkbit Apr 15, 2026
3a247c7
fix(CardHorizontal): 🐛 Add role=button for assistive technology
punkbit Apr 15, 2026
2e11c6a
test(CardHorizontal): 💍 Add unit tests for role attribute
punkbit Apr 15, 2026
ae9d2e2
test(CardHorizontal): 💍 Remove redundant waitForTimeout in hover/focu…
punkbit Apr 15, 2026
5035a50
fix(CardHorizontal): 🐛 Use aria-pressed instead of aria-selected for …
punkbit Apr 15, 2026
7971ee2
fix(CardHorizontal): 🐛 Ensure aria-pressed defaults to false when isS…
punkbit Apr 15, 2026
43df276
test(CardHorizontal): 💍 Add test for isSelected=undefined case
punkbit Apr 15, 2026
863ddce
docs(CardHorizontal): 📝 Add comment about click/keyboard test limitat…
punkbit Apr 15, 2026
5752b0a
chore: 🤖 update snapshots
punkbit Apr 15, 2026
00fd434
chore: 🤖 update snapshots
punkbit Apr 15, 2026
a420d2c
test(CardHorizontal): 💍 Add dark theme coverage and interactive story…
punkbit Apr 15, 2026
e04e0e2
fix(CardHorizontal): 🐛 Add Space key handler for role=button accessib…
punkbit Apr 15, 2026
39d4915
chore: 🤖 update snapshots
punkbit Apr 15, 2026
590f3e1
refactor: 💡 prefer importing event type instead using React
punkbit Apr 15, 2026
647e971
refactor: 💡 remove redundant comments
punkbit Apr 15, 2026
abc4ff4
refactor: 💡 remove odd stories due to unexepcted states
punkbit Apr 15, 2026
4830992
Revert "refactor: 💡 remove odd stories due to unexepcted states"
punkbit Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 201 additions & 3 deletions src/components/CardHorizontal/CardHorizontal.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CardHorizontal } from '@/components/CardHorizontal';
const GridCenter = styled.div`
display: grid;
width: 60%;
max-width: 480px;
`;

const meta: Meta<typeof CardHorizontal> = {
Expand All @@ -20,12 +21,12 @@ const meta: Meta<typeof CardHorizontal> = {
badgeState: {
type: {
name: 'enum',
// FIXME should refer to the Badge constants
value: ['default', 'success', 'neutral', 'danger', 'disabled', 'warning', 'info'],
},
},
// FIXME should refer to a constant
badgeIconDir: { type: { name: 'enum', value: ['start', 'end'] } },
color: { type: { name: 'enum', value: ['default', 'muted'] } },
size: { type: { name: 'enum', value: ['sm', 'md'] } },
},
decorators: Story => (
<GridCenter>
Expand All @@ -36,7 +37,9 @@ const meta: Meta<typeof CardHorizontal> = {

export default meta;

export const Playground: StoryObj<typeof CardHorizontal> = {
type Story = StoryObj<typeof CardHorizontal>;

export const Playground: Story = {
args: {
icon: 'building',
title: 'Card title',
Expand All @@ -50,5 +53,200 @@ export const Playground: StoryObj<typeof CardHorizontal> = {
infoText: '',
infoUrl: '',
size: 'md',
color: 'default',
},
};

export const DefaultColor: Story = {
args: {
icon: 'building',
title: 'Default Color Card',
description: 'This is the default color variant.',
color: 'default',
},
};

export const MutedColor: Story = {
args: {
icon: 'building',
title: 'Muted Color Card',
description: 'This is the muted color variant.',
color: 'muted',
},
};

export const SmallSize: Story = {
args: {
icon: 'building',
title: 'Small Card',
description: 'This is the small size variant.',
size: 'sm',
},
};

export const MediumSize: Story = {
args: {
icon: 'building',
title: 'Medium Card',
description: 'This is the medium size variant.',
size: 'md',
},
};

export const DefaultDisabled: Story = {
args: {
icon: 'building',
title: 'Disabled Default Card',
description: 'This card is disabled.',
disabled: true,
color: 'default',
},
};

export const MutedDisabled: Story = {
args: {
icon: 'building',
title: 'Disabled Muted Card',
description: 'This card is disabled.',
disabled: true,
color: 'muted',
},
};

export const DefaultSelected: Story = {
args: {
icon: 'building',
title: 'Selected Default Card',
description: 'This card is selected.',
isSelected: true,
color: 'default',
},
};

export const MutedSelected: Story = {
args: {
icon: 'building',
title: 'Selected Muted Card',
description: 'This card is selected.',
isSelected: true,
color: 'muted',
},
};

export const DefaultDisabledSelected: Story = {
args: {
icon: 'building',
title: 'Disabled & Selected Card',
description: 'This card is both disabled and selected.',
disabled: true,
isSelected: true,
color: 'default',
},
};

export const WithBadge: Story = {
args: {
icon: 'building',
title: 'Card with Badge',
description: 'This card has a badge.',
badgeText: 'New',
badgeState: 'success',
},
};

export const WithBadgeAndIcon: Story = {
args: {
icon: 'building',
title: 'Card with Badge and Icon',
description: 'This card has a badge with an icon.',
badgeText: 'Info',
badgeState: 'info',
badgeIcon: 'check',
badgeIconDir: 'start',
},
};

export const WithInfoButton: Story = {
args: {
icon: 'building',
title: 'Card with Info Button',
description: 'This card has an info button.',
infoText: 'Learn more',
},
};

export const WithInfoButtonMuted: Story = {
args: {
icon: 'building',
title: 'Muted Card with Info Button',
description: 'This is a muted card with an info button.',
infoText: 'Learn more',
color: 'muted',
},
};

export const WithInfoButtonDisabled: Story = {
args: {
icon: 'building',
title: 'Disabled Card with Info Button',
description: 'This is a disabled card with an info button.',
infoText: 'Learn more',
disabled: true,
},
};

export const NonSelectable: Story = {
args: {
icon: 'building',
title: 'Non-Selectable Card',
description: 'This card is not selectable (has infoText).',
infoText: 'Click me',
},
};

export const WithoutIcon: Story = {
args: {
title: 'Card Without Icon',
description: 'This card does not have an icon.',
},
};

export const TitleOnly: Story = {
args: {
icon: 'building',
title: 'Title Only Card',
},
};

export const DescriptionOnly: Story = {
args: {
icon: 'building',
description: 'This card only has a description, no title.',
},
};

export const WithChildren: Story = {
args: {
icon: 'building',
title: 'Card with Children',
description: 'This card has children content.',
children: 'Additional child content goes here.',
},
};

export const Interactive: Story = {
args: {
icon: 'check',
title: 'Interactive Card',
description: 'Click this card to test interactions.',
},
};

export const InteractiveWithHandler: Story = {
args: {
icon: 'check',
title: 'Interactive Card with Handler',
description: 'This card has an onButtonClick handler for E2E testing.',
onButtonClick: () => console.log('Card clicked!'),
},
};
106 changes: 105 additions & 1 deletion src/components/CardHorizontal/CardHorizontal.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { screen } from '@testing-library/react';
import { screen, fireEvent } from '@testing-library/react';
import { CardHorizontal, CardHorizontalProps } from '@/components/CardHorizontal';
import { renderCUI } from '@/utils/test-utils';

Expand Down Expand Up @@ -187,4 +187,108 @@ describe('CardHorizontal Component', () => {
expect(windowOpenSpy).not.toHaveBeenCalled();
windowOpenSpy.mockRestore();
});

it('should have aria-pressed="true" when selected and selectable', () => {
const { container } = renderCard({
title: 'Test Card',
isSelected: true,
isSelectable: true,
});

const wrapper = container.firstChild;
expect(wrapper).toHaveAttribute('aria-pressed', 'true');
});

it('should have aria-pressed="false" when not selected but selectable', () => {
const { container } = renderCard({
title: 'Test Card',
isSelected: false,
isSelectable: true,
});

const wrapper = container.firstChild;
expect(wrapper).toHaveAttribute('aria-pressed', 'false');
});

it('should have aria-pressed="false" when selectable but isSelected is not provided', () => {
const { container } = renderCard({
title: 'Test Card',
isSelectable: true,
});

const wrapper = container.firstChild;
expect(wrapper).toHaveAttribute('aria-pressed', 'false');
});

it('should not have aria-pressed when not selectable', () => {
const { container } = renderCard({
title: 'Test Card',
infoText: 'Click me',
isSelectable: false,
});

const wrapper = container.firstChild;
expect(wrapper).not.toHaveAttribute('aria-pressed');
});

it('should have role=button when selectable', () => {
const { container } = renderCard({
title: 'Test Card',
isSelectable: true,
});

const wrapper = container.firstChild;
expect(wrapper).toHaveAttribute('role', 'button');
});

it('should not have role when not selectable', () => {
const { container } = renderCard({
title: 'Test Card',
infoText: 'Click me',
isSelectable: false,
});

const wrapper = container.firstChild;
expect(wrapper).not.toHaveAttribute('role');
});

it('should call onClick on Space key when selectable', () => {
const onClickMock = vitest.fn();
const { container } = renderCard({
title: 'Test Card',
isSelectable: true,
onButtonClick: onClickMock,
});

const wrapper = container.firstChild as HTMLElement;
fireEvent.keyDown(wrapper, { key: ' ' });

expect(onClickMock).toHaveBeenCalled();
});

it('should not call onClick on Space key when disabled', () => {
const onClickMock = vitest.fn();
const { container } = renderCard({
title: 'Test Card',
disabled: true,
isSelectable: true,
onButtonClick: onClickMock,
});

const wrapper = container.firstChild as HTMLElement;
fireEvent.keyDown(wrapper, { key: ' ' });

expect(onClickMock).not.toHaveBeenCalled();
});

it('should not have onKeyDown when not selectable', () => {
const { container } = renderCard({
title: 'Test Card',
infoText: 'Click me',
isSelectable: false,
});

const wrapper = container.firstChild;
expect(wrapper).not.toHaveAttribute('onkeydown');
});
});
15 changes: 14 additions & 1 deletion src/components/CardHorizontal/CardHorizontal.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type KeyboardEvent, type MouseEvent } from 'react';
import { styled } from 'styled-components';
import { Badge } from '@/components/Badge';
import { Button } from '@/components/Button';
Expand Down Expand Up @@ -195,7 +196,7 @@ export const CardHorizontal = ({
onButtonClick,
...props
}: CardHorizontalProps) => {
const handleClick = (e: React.MouseEvent<HTMLElement>) => {
const handleClick = (e: MouseEvent<HTMLElement>) => {
if (disabled) {
e.preventDefault();
return;
Expand All @@ -208,6 +209,14 @@ export const CardHorizontal = ({
window.open(infoUrl, '_blank');
}
};

const handleKeyDown = (e: KeyboardEvent<HTMLElement>) => {
if (isSelectable && !disabled && e.key === ' ') {
e.preventDefault();
handleClick(e as unknown as MouseEvent<HTMLElement>);
}
};

return (
<Wrapper
$disabled={disabled}
Expand All @@ -216,8 +225,12 @@ export const CardHorizontal = ({
$color={color}
$size={size}
tabIndex={disabled ? -1 : 0}
role={isSelectable ? 'button' : undefined}
aria-disabled={disabled}
aria-pressed={isSelectable ? (isSelected ?? false) : undefined}
onClick={handleClick}
onKeyDown={isSelectable ? handleKeyDown : undefined}
data-testid="card-horizontal"
{...props}
>
<ContentWrapper $size={size}>
Expand Down
Loading
Loading