Skip to content
282 changes: 202 additions & 80 deletions apps/docs/src/stories/Toast.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,149 +1,271 @@
import { Meta, StoryObj, StoryFn } from "@storybook/react";
import { useToast, ToastProvider, type ToastOptionType } from "@sopt-makers/ui";
import { IconCopy } from "@sopt-makers/icons";
import { Meta, StoryObj, StoryFn } from '@storybook/react';
import { useToast, ToastProvider, type ToastOptionType } from '@sopt-makers/ui';
import { IconCopy } from '@sopt-makers/icons';
import { CSSProperties } from 'react';
import { fn } from '@storybook/test';

const COPY_ICON = <IconCopy />;

interface ToastStoryArgs {
icon?: 'success' | 'alert' | 'error' | 'custom' | undefined;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

icon props 자체가 optional이라 undefined는 없어도 될 것 같아요

content: string;

/* NOTE: 스토리북 전용 인터페이스 (action 속성을 개별 속성으로 평면화) */
actionName?: string;
onActionClick?: () => void;

style?: CSSProperties;
}

const meta: Meta<ToastStoryArgs> = {
title: 'Components/Toast',
tags: ['autodocs'],

const meta: Meta = {
title: "Components/Toast",
tags: ["autodocs"],
argTypes: {
icon: {
control: "radio",
options: ["success", "alert", "error", "custom"],
description: "토스트의 아이콘을 지정합니다.",
mapping: { custom: <IconCopy /> },
table: {
type: {
summary: "success | alert | error | ReactElement",
},
},
control: 'select',
options: ['default', 'success', 'alert', 'error', 'custom'],
defaultValue: undefined,
description: '토스트의 아이콘을 지정합니다.<br />기본 값으로 `undefined`값을 가집니다.',
mapping: { default: undefined, custom: COPY_ICON },
table: { type: { summary: 'success | alert | error | ReactElement' } },
},
content: { description: "토스트의 내용을 작성합니다." },
action: {
description: "토스트의 액션을 지정합니다.",
table: { type: { summary: "object" } },

content: { description: '토스트의 내용을 작성합니다.', control: 'text', table: { type: { summary: 'string' } } },

actionName: {
description: '토스트 액션 버튼의 텍스트를 지정합니다.<br />`action.name` 속성에 해당합니다.',
control: 'text',
table: { type: { summary: 'string (action.name)' } },
},

onActionClick: {
description: '토스트 액션 버튼 클릭 시 실행할 함수를 지정합니다.<br />`action.onClick` 속성에 해당합니다.',
action: 'action clicked',
table: { type: { summary: '() => void (action.onClick)' } },
},

style: {
description: "토스트의 스타일을 사용자가 지정합니다.",
table: { type: { summary: "object" } },
description: '토스트의 스타일을 지정합니다.',
control: 'object',
table: { type: { summary: 'CSSProperties' } },
},
},
decorators: [
(Story: StoryFn) => (
<ToastProvider>
<Story />
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'start', height: '80px' }}>
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;

// export const Component = {
// component: Toast,
// args: {
// icon: "success",
// content: "프로젝트가 등록되었어요.",
// action: { name: "보러가기", onClick: () => {} },
// style: {
// root: { position: "static", animation: "none", transform: "none" },
// },
// },
// };

const ToastSample = ({ option }: { option: ToastOptionType }) => {
const { open } = useToast();
return <button onClick={() => open(option)}>Open Toast</button>;
};

export const Default: StoryObj = {
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const Default: StoryObj<ToastStoryArgs> = {
args: {
icon: undefined,
content: '토스트 메시지입니다.',
actionName: '',
onActionClick: fn(),
style: undefined,
},
render: (args) => {
const { open } = useToast();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기 eslint error 떠서 아래 CloseToast처럼 // eslint-disable-next-line react-hooks/rules-of-hooks 이 주석 추가해주거나, 따로 이 부분도 컴포넌트 분리해서 useToast 써야할 것 같아요


const option: ToastOptionType = {
content: "저는 토스트예요.",
icon: args.icon === 'custom' ? COPY_ICON : args.icon,
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;

return <button onClick={() => open(option)}>Open Toast</button>;
},
};

export const SuccessIcon: StoryObj = {
name: "Icon - Success ",
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const SuccessIcon: StoryObj<ToastStoryArgs> = {
name: 'Success',
args: {
content: '프로젝트가 등록되었어요.',
actionName: '',
onActionClick: fn(),
},
parameters: {
controls: { exclude: ['icon'] },
},
render: (args) => {
const option: ToastOptionType = {
icon: "success",
content: "프로젝트가 등록되었어요.",
icon: 'success',
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
Comment on lines 109 to 119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

모든 스토리마다 option 설정하는 로직이 있어서 이를 ToastSample 내부에서 한번만 설정하도록 해서 반복을 줄일 수 있을 것 같아요

return <ToastSample option={option} />;
},
};

export const AlertIcon: StoryObj = {
name: "Icon - Alert",
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const AlertIcon: StoryObj<ToastStoryArgs> = {
name: 'Alert',
args: {
content: '이메일을 입력해주세요.',
actionName: '',
onActionClick: fn(),
},
parameters: {
controls: { exclude: ['icon'] },
},
render: (args) => {
const option: ToastOptionType = {
icon: "alert",
content: "이메일을 입력해주세요.",
icon: 'alert',
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;
},
};

export const ErrorIcon: StoryObj = {
name: "Icon - Error",
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const ErrorIcon: StoryObj<ToastStoryArgs> = {
name: 'Error',
args: {
content: '프로젝트 수정에 실패했어요.',
actionName: '',
onActionClick: fn(),
},
parameters: {
controls: { exclude: ['icon'] },
},
render: (args) => {
const option: ToastOptionType = {
icon: "error",
content: "프로젝트 수정에 실패했어요.",
icon: 'error',
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;
},
};

export const CustomIcon: StoryObj = {
name: "Icon - Custom",
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const CustomIcon: StoryObj<ToastStoryArgs> = {
name: 'Custom',
args: {
content: '링크를 복사했어요.',
actionName: '',
onActionClick: fn(),
},
parameters: {
controls: { exclude: ['icon'] },
},
render: (args) => {
const option: ToastOptionType = {
icon: <IconCopy />,
content: "링크를 복사했어요.",
icon: COPY_ICON,
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;
},
};

export const ActionButton: StoryObj = {
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const ActionButton: StoryObj<ToastStoryArgs> = {
args: {
icon: 'success',
content: '프로젝트가 등록되었어요.',
actionName: '보러가기',
onActionClick: fn(),
},
render: (args) => {
const option: ToastOptionType = {
icon: "success",
content: "프로젝트가 등록되었어요.",
action: { name: "보러가기", onClick: () => { } },
icon: args.icon === 'custom' ? COPY_ICON : args.icon,
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;
},
};

export const TextOver: StoryObj = {
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const TextOver: StoryObj<ToastStoryArgs> = {
args: {
icon: 'alert',
content:
'토스트 내용은 두 줄을 초과할 수 없습니다. 토스트 내용은 두 줄을 초과할 수 없습니다. 토스트 내용은 두 줄을 초과할 수 없습니다. ',
actionName: '',
onActionClick: fn(),
},
render: (args) => {
const option: ToastOptionType = {
icon: "alert",
content:
"토스트 내용은 두 줄을 초과할 수 없습니다. 토스트 내용은 두 줄을 초과할 수 없습니다. 토스트 내용은 두 줄을 초과할 수 없습니다. ",
icon: args.icon === 'custom' ? COPY_ICON : args.icon,
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return <ToastSample option={option} />;
},
};

export const CloseToast: StoryObj = {
argTypes: { icon: { control: { disable: true } } },
render: () => {
export const CloseToast: StoryObj<ToastStoryArgs> = {
args: {
icon: 'success',
content: '토스트를 원하는 타이밍에 닫을 수 있습니다.',
actionName: '',
onActionClick: fn(),
},
render: (args) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
const { open, close } = useToast();
const option: ToastOptionType = {
icon: "success",
content: "토스트를 원하는 타이밍에 닫을 수 있습니다.",
icon: args.icon === 'custom' ? COPY_ICON : args.icon,
content: args.content,
action: args.actionName
? {
name: args.actionName,
onClick: args.onActionClick || fn(),
}
: undefined,
style: args.style ? { root: args.style } : undefined,
};
return (
<>
Expand Down
Loading