-
Notifications
You must be signed in to change notification settings - Fork 1
Feat(design-system): toast components 구현 #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3027ef0
0e82001
60307fb
c7126cf
5fcfa60
cd8dd53
42c65fa
3c61c0a
b9c8f99
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import type { Meta, StoryObj } from '@storybook/react-vite'; | ||
| import React from 'react'; | ||
| import { within, userEvent, expect, waitFor } from '@storybook/test'; | ||
| import AutoDismissToast from './hooks/uesFadeOut'; | ||
| import Toast from './Toast'; | ||
|
|
||
| type AutoDismissStoryArgs = { | ||
| text: string; | ||
| duration: number; | ||
| fadeMs: number; | ||
| className?: string; | ||
| }; | ||
|
|
||
| const meta = { | ||
| title: 'Components/Toast', | ||
| tags: ['autodocs'], | ||
| parameters: { | ||
| layout: 'centered', | ||
| docs: { | ||
| description: { | ||
| component: | ||
| '`AutoDismissToast`는 표시/페이드/언마운트만 담당하고, **콘텐츠는 children**으로 받습니다. ' + | ||
| '스토리 컨트롤의 `text`는 children `<Toast>`로 전달합니다.', | ||
| }, | ||
| }, | ||
| }, | ||
| argTypes: { | ||
| text: { control: 'text', description: 'children <Toast>에 전달할 문구' }, | ||
| duration: { control: 'number', description: '표시 시간(ms), 기본 3000' }, | ||
| fadeMs: { control: 'number', description: '페이드아웃(ms), 기본 200' }, | ||
| className: { table: { disable: true } }, | ||
| }, | ||
| args: { | ||
| text: '저장에 실패했어요.\n다시 시도해주세요.', | ||
| duration: 3000, | ||
| fadeMs: 200, | ||
| }, | ||
| } satisfies Meta<AutoDismissStoryArgs>; | ||
|
|
||
| export default meta; | ||
| type Story = StoryObj<AutoDismissStoryArgs>; | ||
|
|
||
| const ManualTriggerExample: React.FC<AutoDismissStoryArgs> = ({ | ||
| text, | ||
| duration, | ||
| fadeMs, | ||
| }) => { | ||
| const [show, setShow] = React.useState(false); | ||
|
|
||
| return ( | ||
| <div className="p-10" style={{ minHeight: '25vh' }}> | ||
| <button | ||
| type="button" | ||
| onClick={() => setShow(true)} | ||
| style={{ | ||
| background: 'gray', | ||
| color: 'white', | ||
| padding: '8px 16px', | ||
| borderRadius: 4, | ||
| }} | ||
| > | ||
| 토스트 띄우기 | ||
| </button> | ||
|
|
||
| {show && ( | ||
| <div className="fixed bottom-6 left-6 z-[9999]"> | ||
| <AutoDismissToast | ||
| duration={duration} | ||
| fadeMs={fadeMs} | ||
| onClose={() => setShow(false)} | ||
| > | ||
| <Toast text={text} /> | ||
| </AutoDismissToast> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const ManualTrigger: Story = { | ||
| name: '버튼으로 띄우기 (AutoDismiss)', | ||
| parameters: { layout: 'fullscreen', docs: { story: { height: 520 } } }, | ||
| render: (args) => <ManualTriggerExample {...args} />, | ||
| play: async ({ canvasElement, args }) => { | ||
| const canvas = within(canvasElement); | ||
|
|
||
| // 버튼 클릭 → 토스트 등장 | ||
| const btn = await canvas.findByRole('button', { name: '토스트 띄우기' }); | ||
| await userEvent.click(btn); | ||
| await expect(await canvas.findByRole('alert')).toBeInTheDocument(); | ||
|
|
||
| // duration + fadeMs 후 사라짐 검증 | ||
| const timeout = (args.duration ?? 3000) + (args.fadeMs ?? 200) + 400; | ||
| await waitFor(() => expect(canvas.queryByRole('alert')).toBeNull(), { | ||
| timeout, | ||
| }); | ||
| }, | ||
| }; | ||
| export const Static_Basic: Story = { | ||
| name: '정적 UI — 기본', | ||
| parameters: { layout: 'centered' }, | ||
| render: (args) => <Toast text={args.text} />, | ||
| }; | ||
|
|
||
| export const Static_LongText: Story = { | ||
| name: '정적 UI — 긴 문구', | ||
| parameters: { layout: 'centered' }, | ||
| render: () => ( | ||
| <Toast | ||
| text={ | ||
| '네트워크 상태가 불안정합니다.\n잠시 후 다시 시도해주세요. 문제가 계속되면 관리자에게 문의해 주세요.' | ||
| } | ||
| /> | ||
| ), | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| import { cn } from '@/lib/utils'; | ||
|
|
||
| export interface ToastProps { | ||
| text: string; | ||
| } | ||
|
|
||
| export default function Toast({ text }: ToastProps) { | ||
| return ( | ||
| <div | ||
| role="alert" | ||
| aria-live="polite" | ||
|
Comment on lines
+10
to
+11
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aria-live 적용까지 꼼꼼하시네요 |
||
| className={cn( | ||
| 'bg-gray800 text-white-bg', | ||
| 'rounded-[0.8rem] px-[1.6rem] py-[1.2rem]', | ||
| 'common-shadow' | ||
| )} | ||
| > | ||
| <p className="caption2-sb whitespace-pre-line">{text}</p> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||||||
| import { useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||
| import { cn } from '@/lib/utils'; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export interface AutoDismissToastProps { | ||||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||||
| duration?: number; | ||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+6
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. React 타입 네임스페이스 직접 참조로 인한 TS 오류 가능성 — ReactNode를 명시 import 권장
적용 diff: -import { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
+import type { ReactNode } from 'react';
@@
export interface AutoDismissToastProps {
- children: React.ReactNode;
+ children: ReactNode;
duration?: number;
fadeMs?: number;
onClose?: () => void;
className?: string;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||
| fadeMs?: number; | ||||||||||||||||||||||||||||
| onClose?: () => void; | ||||||||||||||||||||||||||||
| className?: string; | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| export default function AutoDismissToast({ | ||||||||||||||||||||||||||||
| children, | ||||||||||||||||||||||||||||
| duration = 3000, | ||||||||||||||||||||||||||||
| fadeMs = 200, | ||||||||||||||||||||||||||||
| onClose, | ||||||||||||||||||||||||||||
| className, | ||||||||||||||||||||||||||||
| }: AutoDismissToastProps) { | ||||||||||||||||||||||||||||
| const [visible, setVisible] = useState(true); | ||||||||||||||||||||||||||||
| const [fading, setFading] = useState(false); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||
| const t1 = setTimeout(() => setFading(true), duration); | ||||||||||||||||||||||||||||
| const t2 = setTimeout(() => { | ||||||||||||||||||||||||||||
| setVisible(false); | ||||||||||||||||||||||||||||
| onClose?.(); | ||||||||||||||||||||||||||||
| }, duration + fadeMs); | ||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||
| clearTimeout(t1); | ||||||||||||||||||||||||||||
| clearTimeout(t2); | ||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||
| }, [duration, fadeMs, onClose]); | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if (!visible) return null; | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||
| <div className={cn(className)}> | ||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||
| 'transition-opacity ease-out', | ||||||||||||||||||||||||||||
| fading ? 'opacity-0' : 'opacity-100' | ||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||
| style={{ transitionDuration: `${fadeMs}ms` }} | ||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||
| {children} | ||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Storybook 타입 import 경로 오류 가능성(@storybook/react 권장)
타입은 일반적으로
@storybook/react에서 가져옵니다.@storybook/react-vite는 프레임워크 설정 패키지로 TS 타입을 내보내지 않는 구성이 많아 빌드가 실패할 수 있습니다. 아래처럼 수정해 주세요.📝 Committable suggestion
🤖 Prompt for AI Agents