docs: allow custom props in catalog decorator (#916)

Relates to #702
This commit is contained in:
Thaïs
2023-07-25 20:02:13 +02:00
committed by GitHub
parent a2ccb643ff
commit a5ca913158
6 changed files with 296 additions and 340 deletions

View File

@ -4,17 +4,25 @@ import styled from '@emotion/styled';
import { SoonPill } from '@/ui/pill/components/SoonPill';
import { rgba } from '@/ui/themes/colors';
export type ButtonVariant =
| 'primary'
| 'secondary'
| 'tertiary'
| 'tertiaryBold'
| 'tertiaryLight'
| 'danger';
export enum ButtonSize {
Medium = 'medium',
Small = 'small',
}
export type ButtonSize = 'medium' | 'small';
export enum ButtonPosition {
Left = 'left',
Middle = 'middle',
Right = 'right',
}
export type ButtonPosition = 'left' | 'middle' | 'right' | undefined;
export enum ButtonVariant {
Primary = 'primary',
Secondary = 'secondary',
Tertiary = 'tertiary',
TertiaryBold = 'tertiaryBold',
TertiaryLight = 'tertiaryLight',
Danger = 'danger',
}
export type ButtonProps = {
icon?: React.ReactNode;
@ -24,6 +32,7 @@ export type ButtonProps = {
size?: ButtonSize;
position?: ButtonPosition;
soon?: boolean;
disabled?: boolean;
} & React.ComponentProps<'button'>;
const StyledButton = styled.button<
@ -172,8 +181,8 @@ export function Button({
icon,
title,
fullWidth = false,
variant = 'primary',
size = 'medium',
variant = ButtonVariant.Primary,
size = ButtonSize.Medium,
position,
soon = false,
disabled = false,

View File

@ -1,145 +1,30 @@
import styled from '@emotion/styled';
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { IconSearch } from '@/ui/icon';
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Button } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
type ButtonProps = React.ComponentProps<typeof Button>;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
padding: 20px;
width: 800px;
> * + * {
margin-top: ${({ theme }) => theme.spacing(4)};
}
`;
const StyledTitle = styled.h1`
font-size: ${({ theme }) => theme.font.size.lg};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(3)};
`;
const StyledDescription = styled.span`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xs};
font-weight: ${({ theme }) => theme.font.weight.semiBold};
margin-bottom: ${({ theme }) => theme.spacing(1)};
text-align: center;
text-transform: uppercase;
`;
const StyledLine = styled.div`
display: flex;
flex: 1;
flex-direction: row;
justify-content: space-between;
width: 100%;
`;
const StyledButtonContainer = styled.div`
border: 1px solid ${({ theme }) => theme.color.gray20};
border-radius: ${({ theme }) => theme.border.radius.sm};
display: flex;
flex-direction: column;
padding: ${({ theme }) => theme.spacing(2)};
`;
const variants: ButtonProps['variant'][] = [
'primary',
'secondary',
'tertiary',
'tertiaryBold',
'tertiaryLight',
'danger',
];
const states = {
'with-icon': {
description: 'With icon',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-with-icon`,
icon: <IconSearch size={14} />,
}),
},
default: {
description: 'Default',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-default`,
onClick: clickJestFn,
}),
},
hover: {
description: 'Hover',
extraProps: (variant: string) => ({
id: `${variant}-button-hover`,
'data-testid': `${variant}-button-hover`,
}),
},
pressed: {
description: 'Pressed',
extraProps: (variant: string) => ({
id: `${variant}-button-pressed`,
'data-testid': `${variant}-button-pressed`,
}),
},
disabled: {
description: 'Disabled',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-disabled`,
disabled: true,
}),
},
soon: {
description: 'Soon',
extraProps: (variant: string) => ({
'data-testid': `${variant}-button-soon`,
soon: true,
}),
},
focus: {
description: 'Focus',
extraProps: (variant: string) => ({
id: `${variant}-button-focus`,
'data-testid': `${variant}-button-focus`,
}),
},
};
import { Button, ButtonPosition, ButtonSize, ButtonVariant } from '../Button';
const meta: Meta<typeof Button> = {
title: 'UI/Button/Button',
component: Button,
decorators: [
(Story) => (
<StyledContainer>
<Story />
</StyledContainer>
),
],
parameters: {
pseudo: Object.keys(states).reduce(
(acc, state) => ({
...acc,
[state]: variants.map(
(variant) =>
variant &&
['#left', '#center', '#right'].map(
(pos) => `${pos}-${variant}-button-${state}`,
),
),
}),
{},
),
argTypes: {
icon: {
type: 'boolean',
mapping: {
true: <IconSearch size={14} />,
false: undefined,
},
},
position: {
control: 'radio',
options: [undefined, ...Object.values(ButtonPosition)],
},
},
argTypes: { icon: { control: false }, variant: { control: false } },
args: { title: 'A button title' },
args: { title: 'Lorem ipsum' },
};
export default meta;
@ -147,34 +32,12 @@ type Story = StoryObj<typeof Button>;
const clickJestFn = jest.fn();
export const MediumSize: Story = {
args: { size: 'medium' },
render: (args) => (
<>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
{Object.entries(states).map(
([state, { description, extraProps }]) => (
<StyledButtonContainer key={`${variant}-container-${state}`}>
<StyledDescription>{description}</StyledDescription>
<Button
{...args}
{...extraProps(variant ?? '')}
variant={variant}
/>
</StyledButtonContainer>
),
)}
</StyledLine>
</div>
))}
</>
),
export const Default: Story = {
args: { onClick: clickJestFn },
decorators: [ComponentDecorator],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByTestId('primary-button-default');
const button = canvas.getByRole('button');
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
@ -182,54 +45,78 @@ export const MediumSize: Story = {
},
};
export const SmallSize: Story = {
...MediumSize,
args: { size: 'small' },
};
export const MediumSizeGroup: Story = {
args: { size: 'medium' },
render: (args) => (
<>
{variants.map((variant) => (
<div key={variant}>
<StyledTitle>{variant}</StyledTitle>
<StyledLine>
{Object.entries(states).map(
([state, { description, extraProps }]) => (
<StyledButtonContainer
key={`${variant}-group-container-${state}`}
>
<StyledDescription>{description}</StyledDescription>
<ButtonGroup>
{['Left', 'Center', 'Right'].map((position) => (
<Button
{...args}
{...extraProps(`${variant}-${position.toLowerCase()}`)}
variant={variant}
title={position}
/>
))}
</ButtonGroup>
</StyledButtonContainer>
),
)}
</StyledLine>
</div>
))}
</>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByTestId('primary-left-button-default');
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(button);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
export const Sizes: Story = {
argTypes: {
size: { control: false },
},
parameters: {
catalog: [
{
name: 'sizes',
values: Object.values(ButtonSize),
props: (size: ButtonSize) => ({ size }),
},
],
},
decorators: [CatalogDecorator],
};
export const SmallSizeGroup: Story = {
...MediumSizeGroup,
args: { size: 'small' },
export const Variants: Story = {
argTypes: {
disabled: { control: false },
variant: { control: false },
},
parameters: {
pseudo: { hover: ['.hover'], active: ['.active'], focus: ['.focus'] },
catalog: [
{
name: 'state',
values: ['default', 'disabled', 'hover', 'active', 'focus'],
props: (state: string) => {
if (state === 'disabled') return { disabled: true };
if (state === 'default') return {};
return { className: state };
},
},
{
name: 'variants',
values: Object.values(ButtonVariant),
props: (variant: ButtonVariant) => ({ variant }),
},
],
},
decorators: [CatalogDecorator],
};
export const Positions: Story = {
argTypes: {
position: { control: false },
},
parameters: {
catalog: [
{
name: 'positions',
values: ['none', ...Object.values(ButtonPosition)],
props: (position: ButtonPosition | 'none') =>
position === 'none' ? {} : { position },
},
],
},
decorators: [CatalogDecorator],
};
export const WithAdornments: Story = {
parameters: {
catalog: [
{
name: 'adornments',
values: ['with icon', 'with soon pill'],
props: (value: string) =>
value === 'with icon'
? { icon: <IconSearch size={14} /> }
: { soon: true },
},
],
},
decorators: [CatalogDecorator],
};

View File

@ -0,0 +1,36 @@
import { expect, jest } from '@storybook/jest';
import type { Meta, StoryObj } from '@storybook/react';
import { userEvent, within } from '@storybook/testing-library';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Button, ButtonPosition } from '../Button';
import { ButtonGroup } from '../ButtonGroup';
const clickJestFn = jest.fn();
const meta: Meta<typeof ButtonGroup> = {
title: 'UI/Button/ButtonGroup',
component: ButtonGroup,
decorators: [ComponentDecorator],
argTypes: { children: { control: false } },
args: {
children: Object.values(ButtonPosition).map((position) => (
<Button title={position} onClick={clickJestFn} />
)),
},
};
export default meta;
type Story = StoryObj<typeof ButtonGroup>;
export const Default: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const leftButton = canvas.getByRole('button', { name: 'left' });
const numberOfClicks = clickJestFn.mock.calls.length;
await userEvent.click(leftButton);
expect(clickJestFn).toHaveBeenCalledTimes(numberOfClicks + 1);
},
};