-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
75 changes: 75 additions & 0 deletions
75
packages/ui-react/src/lib/components/card/card.stories.tsx
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,75 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import { | ||
NatuCard, | ||
NatuCardBody, | ||
NatuCardFooter, | ||
NatuCardFooterProps, | ||
NatuCardHeader, | ||
NatuCardHeaderProps, | ||
NatuCardProps, | ||
} from './card'; | ||
import RocketIcon from '@natu/assets/svg/rocket.svg?react'; | ||
|
||
interface StoryProps extends NatuCardProps { | ||
hideHeader?: boolean; | ||
hideFooter?: boolean; | ||
headerArgs?: Partial<NatuCardHeaderProps>; | ||
footerArgs?: Partial<NatuCardFooterProps>; | ||
} | ||
|
||
const meta = { | ||
title: 'Components/Card', | ||
component: NatuCard, | ||
tags: ['autodocs'], | ||
render: (args) => { | ||
const { headerArgs, footerArgs, hideHeader, hideFooter, ...cardArgs } = args; | ||
|
||
return ( | ||
<NatuCard {...cardArgs}> | ||
{!hideHeader && ( | ||
<NatuCardHeader {...headerArgs} icon={<RocketIcon className="natu-svg-icon" />}> | ||
Example header | ||
</NatuCardHeader> | ||
)} | ||
|
||
<NatuCardBody>Example actions</NatuCardBody> | ||
|
||
{!hideFooter && <NatuCardFooter {...footerArgs}>Example secondary actions</NatuCardFooter>} | ||
</NatuCard> | ||
); | ||
}, | ||
} satisfies Meta<StoryProps>; | ||
|
||
export default meta; | ||
type Story = StoryObj<StoryProps>; | ||
|
||
export const Default: Story = {}; | ||
|
||
export const Dismissable: Story = { | ||
args: { isDismissable: true }, | ||
}; | ||
|
||
export const Embedded: Story = { | ||
args: { isEmbedded: true }, | ||
}; | ||
|
||
export const Small: Story = { | ||
args: { | ||
isDismissable: true, | ||
headerArgs: { size: 'small' }, | ||
footerArgs: { size: 'small' }, | ||
}, | ||
}; | ||
|
||
export const WithFooterDivider: Story = { | ||
args: { | ||
footerArgs: { hasDivider: true }, | ||
}, | ||
}; | ||
|
||
export const NoHeaderOrFooter: Story = { | ||
args: { | ||
hideHeader: true, | ||
hideFooter: true, | ||
}, | ||
}; |
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,34 @@ | ||
import { composeStories } from '@storybook/react'; | ||
import * as stories from './card.stories'; | ||
import { axe, render, renderStory } from '../../test'; | ||
import { NatuCard, NatuCardBody, NatuCardFooter, NatuCardHeader } from './card'; | ||
import { screen } from '@testing-library/react'; | ||
|
||
const storyTestCases = Object.entries(composeStories(stories)); | ||
|
||
test.each(storyTestCases)('renders %s story', (_, Story) => { | ||
const { container } = renderStory(<Story />); | ||
expect(container).toBeInTheDocument(); | ||
}); | ||
|
||
test.each(storyTestCases)('%s has no accessibility violations', async (_, Story) => { | ||
const { baseElement } = renderStory(<Story />); | ||
expect(await axe(baseElement)).toHaveNoViolations(); | ||
}); | ||
|
||
test('trigger onDimiss callback when clicked', async () => { | ||
const onDismissSpy = vi.fn(); | ||
const { userEvent } = render( | ||
<NatuCard isDismissable={true} onDismiss={onDismissSpy}> | ||
<NatuCardHeader>Example header</NatuCardHeader> | ||
|
||
<NatuCardBody>Example actions</NatuCardBody> | ||
|
||
<NatuCardFooter>Example secondary actions</NatuCardFooter> | ||
</NatuCard>, | ||
); | ||
|
||
await userEvent.click(screen.getByRole('button', { name: 'Dismiss' })); | ||
|
||
expect(onDismissSpy).toHaveBeenCalledTimes(1); | ||
}); |
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,102 @@ | ||
import clsx from 'clsx'; | ||
import { ComponentPropsWithoutRef, ReactNode } from 'react'; | ||
import { NatuButton } from '../button/button'; | ||
import XIcon from '@natu/assets/svg/x.svg?react'; | ||
import { Slot } from '@radix-ui/react-slot'; | ||
|
||
export interface NatuCardProps extends ComponentPropsWithoutRef<'div'> { | ||
children?: ReactNode; | ||
/** | ||
* Whether the card is part of another component or not. | ||
* Will hide borders, box-shadows, etc if true. | ||
*/ | ||
isEmbedded?: boolean; | ||
|
||
/** Whether to show the dismissable button or not. */ | ||
isDismissable?: boolean; | ||
|
||
onDismiss?: () => void; | ||
} | ||
|
||
export interface NatuCardHeaderProps extends ComponentPropsWithoutRef<'div'> { | ||
children?: ReactNode; | ||
icon?: ReactNode; | ||
size?: 'small' | 'medium'; | ||
} | ||
|
||
export interface NatuCardBodyProps extends ComponentPropsWithoutRef<'div'> { | ||
children?: ReactNode; | ||
} | ||
|
||
export interface NatuCardFooterProps extends ComponentPropsWithoutRef<'div'> { | ||
children?: ReactNode; | ||
hasDivider?: boolean; | ||
size?: 'small' | 'medium'; | ||
} | ||
|
||
export function NatuCard(props: NatuCardProps) { | ||
const { className, isEmbedded, isDismissable, onDismiss, ...cardProps } = props; | ||
|
||
return ( | ||
<div | ||
{...cardProps} | ||
className={clsx('natu-card', { 'natu-card--embedded': isEmbedded }, className)} | ||
> | ||
{props.children} | ||
|
||
{isDismissable && ( | ||
<NatuButton | ||
type="button" | ||
className="natu-card__dismiss" | ||
variant="ghost" | ||
isIconButton={true} | ||
onPress={onDismiss} | ||
> | ||
<span className="natu-visually-hidden">Dismiss</span> | ||
<XIcon className="natu-svg-icon" aria-hidden="true" /> | ||
</NatuButton> | ||
)} | ||
</div> | ||
); | ||
} | ||
|
||
export function NatuCardHeader(props: NatuCardHeaderProps) { | ||
const { size = 'medium', className, icon, ...headerProps } = props; | ||
|
||
return ( | ||
<div | ||
{...headerProps} | ||
className={clsx('natu-card__header', `natu-card__header--${size}`, className)} | ||
> | ||
<Slot className="natu-card__header-icon">{icon}</Slot> | ||
<div>{props.children}</div> | ||
</div> | ||
); | ||
} | ||
|
||
export function NatuCardBody(props: NatuCardBodyProps) { | ||
const { className, ...bodyProps } = props; | ||
return ( | ||
<div {...bodyProps} className={clsx('natu-card__body', className)}> | ||
{props.children} | ||
</div> | ||
); | ||
} | ||
|
||
export function NatuCardFooter(props: NatuCardFooterProps) { | ||
const { hasDivider, size = 'medium', className, ...footerProps } = props; | ||
|
||
return ( | ||
<div | ||
{...footerProps} | ||
className={clsx( | ||
'natu-card__footer', | ||
`natu-card__footer--${size}`, | ||
{ 'natu-card__footer--with-divider': hasDivider }, | ||
className, | ||
)} | ||
> | ||
{props.children} | ||
</div> | ||
); | ||
} |
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,25 @@ | ||
import { VrtScenario, createVrtStorybookScenarios } from '@natu/vrt'; | ||
import { test } from '@playwright/test'; | ||
import { defaultVrtVariants } from '../../vrt/variants'; | ||
|
||
const scenarios: VrtScenario[] = [ | ||
{ story: 'default' }, | ||
{ story: 'dismissable' }, | ||
{ story: 'embedded' }, | ||
{ story: 'small' }, | ||
{ story: 'with-footer-divider' }, | ||
{ story: 'no-header-or-footer' }, | ||
]; | ||
|
||
const testScenarios = createVrtStorybookScenarios({ | ||
scenarios, | ||
page: 'components-card', | ||
viewports: [{ name: 'custom', width: 500, height: 500 }], | ||
variants: defaultVrtVariants, | ||
}); | ||
|
||
testScenarios.forEach((scenario) => { | ||
test(scenario.id, async ({ page }, testInfo) => { | ||
await scenario.test(page, testInfo); | ||
}); | ||
}); |