diff --git a/apps/docs/src/stories/Toast.stories.tsx b/apps/docs/src/stories/Toast.stories.tsx index 59690fad..e282e3da 100644 --- a/apps/docs/src/stories/Toast.stories.tsx +++ b/apps/docs/src/stories/Toast.stories.tsx @@ -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 = ; + +interface ToastStoryArgs { + icon?: 'success' | 'alert' | 'error' | 'custom' | undefined; + content: string; + + /* NOTE: 스토리북 전용 인터페이스 (action 속성을 개별 속성으로 평면화) */ + actionName?: string; + onActionClick?: () => void; + + style?: CSSProperties; +} + +const meta: Meta = { + 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: }, - table: { - type: { - summary: "success | alert | error | ReactElement", - }, - }, + control: 'select', + options: ['default', 'success', 'alert', 'error', 'custom'], + defaultValue: undefined, + description: '토스트의 아이콘을 지정합니다.
기본 값으로 `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: '토스트 액션 버튼의 텍스트를 지정합니다.
`action.name` 속성에 해당합니다.', + control: 'text', + table: { type: { summary: 'string (action.name)' } }, }, + + onActionClick: { + description: '토스트 액션 버튼 클릭 시 실행할 함수를 지정합니다.
`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) => ( - +
+ +
), ], }; 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 ; }; -export const Default: StoryObj = { - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const Default: StoryObj = { + args: { + icon: undefined, + content: '토스트 메시지입니다.', + actionName: '', + onActionClick: fn(), + style: undefined, + }, + render: (args) => { + const { open } = 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 ; + + return ; }, }; -export const SuccessIcon: StoryObj = { - name: "Icon - Success ", - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const SuccessIcon: StoryObj = { + 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, }; return ; }, }; -export const AlertIcon: StoryObj = { - name: "Icon - Alert", - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const AlertIcon: StoryObj = { + 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 ; }, }; -export const ErrorIcon: StoryObj = { - name: "Icon - Error", - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const ErrorIcon: StoryObj = { + 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 ; }, }; -export const CustomIcon: StoryObj = { - name: "Icon - Custom", - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const CustomIcon: StoryObj = { + name: 'Custom', + args: { + content: '링크를 복사했어요.', + actionName: '', + onActionClick: fn(), + }, + parameters: { + controls: { exclude: ['icon'] }, + }, + render: (args) => { const option: ToastOptionType = { - icon: , - 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 ; }, }; -export const ActionButton: StoryObj = { - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const ActionButton: StoryObj = { + 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 ; }, }; -export const TextOver: StoryObj = { - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const TextOver: StoryObj = { + 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 ; }, }; -export const CloseToast: StoryObj = { - argTypes: { icon: { control: { disable: true } } }, - render: () => { +export const CloseToast: StoryObj = { + 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 ( <> diff --git a/packages/ui/Toast/parts/index.tsx b/packages/ui/Toast/parts/index.tsx index e2205ac0..42fc3537 100644 --- a/packages/ui/Toast/parts/index.tsx +++ b/packages/ui/Toast/parts/index.tsx @@ -1,7 +1,7 @@ -import { forwardRef } from "react"; -import type { ActionType, DefaultIconType, StrictPropsWithChildren } from "../types"; -import { ToastIconSuccess, ToastIconAlert, ToastIconError } from "../icons"; -import * as styles from "./style.css"; +import { forwardRef } from 'react'; +import type { ActionType, DefaultIconType, StrictPropsWithChildren } from '../types'; +import { ToastIconSuccess, ToastIconAlert, ToastIconError } from '../icons'; +import * as styles from './style.css'; // ============================== ToastRoot =============================== @@ -18,16 +18,12 @@ interface RootProps { function Root(props: StrictPropsWithChildren, ref: React.Ref) { const { children, icon, style } = props; - const isDefaultIcon = typeof icon === "string"; + const isDefaultIcon = typeof icon === 'string'; const DefaultIcon = isDefaultIcon ? convertToIcon[icon] : undefined; return ( -
- {DefaultIcon ? ( - - ) : ( - icon &&
{icon}
- )} +
+ {DefaultIcon ? : icon &&
{icon}
} {children}
); @@ -42,10 +38,14 @@ interface ContentProps { style?: React.CSSProperties; } -function Content(props : ContentProps) { +function Content(props: ContentProps) { const { content, style } = props; - return

{content}

; + return ( +

+ {content} +

+ ); } // ============================== ToastAction =============================== @@ -58,7 +58,7 @@ function Action(props: ActionProps) { const { name, style, ...actionProps } = props; return ( - );