-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* chore: 체크 svg를 재사용할 수 있도록 current 속성으로 변경 * chore: 경고 토스트를 보여줄 때, 사용할 svg 추가 * chore: 필요없는 공백 제거 * feat: 토스트 컴포넌트 구현 - 토스트의 타입에 따라서 아이콘, 아이콘의 배경색을 동적으로 결정할 수 있도록 구현 * design: 토스트 컴포넌트를 스타일링하기 위한 css 추가 * test: 토스트 컴포넌트 UI 테스트 * feat: 토스트를 감싸는 컴포넌트 구현 - 토스트 컴포넌트 UI 테스트를 쉽게할 수 있도록, 토스트가 사라질 때 애니메이션을 적용하기 위한 상태를 관리하는 Wrapper 컴포넌트 구현 * feat: 여러개의 토스트를 보여주기 위한 컴포넌트 구현 * design: 토스트 목록 컴포넌트 스타일링을 위한 css 추가 - 사용자의 디바이스 너비에 따라서 동적으로 토스트 UI의 너비를 조절하기 위해 padding, width 속성 사용 * feat: 전역으로 토스트의 렌더링 상태를 관리하기 위한 컨텍스트, 프로바이더 컴포넌트 구현 - 동일한 피드백을 전달하는 토스트가 이미 렌더링중이라면 추가적으로 렌더링되지 않도록 처리 * test: 토스트 컴포넌트 폴더 구조 이동, 공통 스타일 추출, 버튼을 클릭했을 때 토스트 UI가 렌더링되는 스토리 추가 * chore: 토스트 UI 기본 시간 추가
- Loading branch information
Showing
13 changed files
with
416 additions
and
3 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
158 changes: 158 additions & 0 deletions
158
frontend/src/components/_common/Toast/Toast.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,158 @@ | ||
import { css } from '@emotion/react'; | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
|
||
import ToastProvider from '@contexts/ToastProvider'; | ||
|
||
import useToast from '@hooks/useToast/useToast'; | ||
|
||
import Toast from '.'; | ||
import { Button } from '../Buttons/Button'; | ||
|
||
const meta = { | ||
title: 'Components/Toast', | ||
component: Toast, | ||
parameters: { | ||
layout: 'centered', | ||
}, | ||
argTypes: { | ||
message: { | ||
control: { | ||
type: 'text', | ||
}, | ||
description: '토스트 UI를 통해 사용자에게 전달할 메시지입니다.', | ||
}, | ||
type: { | ||
control: { | ||
type: 'select', | ||
}, | ||
options: ['default', 'warning', 'success'], | ||
}, | ||
isOpen: { | ||
control: { | ||
type: 'boolean', | ||
}, | ||
description: '토스트 UI에 애니메이션을 적용하기 위한 추가 상태입니다.', | ||
}, | ||
}, | ||
|
||
decorators: [ | ||
(Story) => { | ||
return ( | ||
<ToastProvider> | ||
<div | ||
css={css` | ||
width: 43rem; | ||
`} | ||
> | ||
<Story /> | ||
</div> | ||
</ToastProvider> | ||
); | ||
}, | ||
], | ||
} satisfies Meta<typeof Toast>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof Toast>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'default', | ||
message: '안녕하세요, 내 이름은 기본 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const Warning: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'warning', | ||
message: '안녕하세요, 내 이름은 경고 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const Success: Story = { | ||
args: { | ||
isOpen: true, | ||
type: 'success', | ||
message: '안녕하세요, 내 이름은 성공 토스트입니다.', | ||
}, | ||
|
||
render: (args) => { | ||
return <Toast {...args} />; | ||
}, | ||
}; | ||
|
||
export const ToastPlayground: Story = { | ||
render: () => { | ||
const { addToast } = useToast(); | ||
|
||
const renderDefaultToast = () => { | ||
addToast({ | ||
type: 'default', | ||
message: '안녕하세요, 내 이름은 기본 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
const renderSuccessToast = () => { | ||
addToast({ | ||
type: 'success', | ||
message: '안녕하세요, 내 이름은 성공 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
const renderWarningToast = () => { | ||
addToast({ | ||
type: 'warning', | ||
message: '안녕하세요, 내 이름은 경고 토스트입니다', | ||
duration: 3000, | ||
}); | ||
}; | ||
|
||
return ( | ||
<div | ||
css={css` | ||
position: relative; | ||
display: flex; | ||
justify-content: center; | ||
width: 100%; | ||
height: 100vh; | ||
`} | ||
> | ||
<div | ||
css={css` | ||
position: absolute; | ||
bottom: 2.4rem; | ||
display: flex; | ||
flex-direction: column; | ||
row-gap: 1.2rem; | ||
`} | ||
> | ||
<Button variant="primary" size="s" onClick={renderDefaultToast}> | ||
기본 토스트 렌더링하기 | ||
</Button> | ||
<Button variant="primary" size="s" onClick={renderSuccessToast}> | ||
성공 토스트 렌더링하기 | ||
</Button> | ||
<Button variant="primary" size="s" onClick={renderWarningToast}> | ||
경고 토스트 렌더링하기 | ||
</Button> | ||
</div> | ||
</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,67 @@ | ||
import type { SerializedStyles } from '@emotion/react'; | ||
import { css, keyframes } from '@emotion/react'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
import type { ToastType } from './Toast.type'; | ||
|
||
const toastSlideIn = keyframes` | ||
from{ | ||
opacity: 0; | ||
}to{ | ||
opacity: 1; | ||
} | ||
`; | ||
|
||
const toastSlideOut = keyframes` | ||
from{ | ||
opacity: 1; | ||
}to{ | ||
opacity: 0; | ||
} | ||
`; | ||
|
||
export const s_toastContainer = (isOpen: boolean) => css` | ||
display: flex; | ||
gap: 1.2rem; | ||
align-items: center; | ||
width: 100%; | ||
height: 4.8rem; | ||
padding: 1.2rem; | ||
background-color: #a1a1aa; | ||
border-radius: 1.6rem; | ||
box-shadow: 0 0.4rem 0.4rem rgb(0 0 0 / 20%); | ||
animation: ${isOpen ? toastSlideIn : toastSlideOut} 0.5s ease-in-out forwards; | ||
`; | ||
|
||
export const s_toastText = css` | ||
${theme.typography.captionBold} | ||
color: ${theme.colors.white}; | ||
`; | ||
|
||
const ICON_BACKGROUND_COLORS: Record<Exclude<ToastType, 'default'>, SerializedStyles> = { | ||
warning: css` | ||
background-color: #ef4545; | ||
`, | ||
success: css` | ||
background-color: ${theme.colors.green.mediumDark}; | ||
`, | ||
}; | ||
|
||
export const s_iconBackgroundColor = (type: Exclude<ToastType, 'default'>) => { | ||
return ICON_BACKGROUND_COLORS[type]; | ||
}; | ||
|
||
export const s_iconContainer = css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
width: 2.4rem; | ||
height: 2.4rem; | ||
border-radius: 50%; | ||
`; |
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 @@ | ||
export type ToastType = 'default' | 'warning' | 'success'; |
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,28 @@ | ||
import { useEffect, useState } from 'react'; | ||
|
||
import Toast from '.'; | ||
import type { ToastType } from './Toast.type'; | ||
|
||
interface ToastContainerProps { | ||
duration?: number; | ||
type: ToastType; | ||
message: string; | ||
} | ||
|
||
const TOAST_ANIMATION_DURATION_TIME = 500; | ||
|
||
export default function ToastContainer({ type, message, duration = 3000 }: ToastContainerProps) { | ||
const [isOpen, setIsOpen] = useState(true); | ||
|
||
useEffect(() => { | ||
const animationTimer = setTimeout(() => { | ||
setIsOpen(false); | ||
}, duration - TOAST_ANIMATION_DURATION_TIME); | ||
|
||
return () => { | ||
clearTimeout(animationTimer); | ||
}; | ||
}, [duration]); | ||
|
||
return <Toast isOpen={isOpen} type={type} message={message} />; | ||
} |
19 changes: 19 additions & 0 deletions
19
frontend/src/components/_common/Toast/ToastList/ToastList.styles.ts
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,19 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const s_toastListContainer = css` | ||
position: fixed; | ||
z-index: 3; | ||
top: 9rem; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
display: flex; | ||
flex-direction: column; | ||
gap: 1.2rem; | ||
align-items: center; | ||
justify-content: center; | ||
width: 100%; | ||
max-width: 43rem; | ||
padding: 1.6rem; | ||
`; |
20 changes: 20 additions & 0 deletions
20
frontend/src/components/_common/Toast/ToastList/ToastList.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,20 @@ | ||
import { createPortal } from 'react-dom'; | ||
|
||
import useToast from '@hooks/useToast/useToast'; | ||
|
||
import ToastContainer from '../ToastContainer'; | ||
import { s_toastListContainer } from './ToastList.styles'; | ||
|
||
export default function ToastList() { | ||
const { toasts } = useToast(); | ||
|
||
return createPortal( | ||
<div css={s_toastListContainer}> | ||
{toasts && | ||
toasts.map(({ id, type, message, duration }) => ( | ||
<ToastContainer key={id} type={type} message={message} duration={duration} /> | ||
))} | ||
</div>, | ||
document.body, | ||
); | ||
} |
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,40 @@ | ||
import Check from '@assets/images/attendeeCheck.svg'; | ||
import Exclamation from '@assets/images/exclamation.svg'; | ||
|
||
import theme from '@styles/theme'; | ||
|
||
import { | ||
s_iconBackgroundColor, | ||
s_iconContainer, | ||
s_toastContainer, | ||
s_toastText, | ||
} from './Toast.styles'; | ||
import type { ToastType } from './Toast.type'; | ||
|
||
interface ToastProps { | ||
isOpen: boolean; | ||
type: ToastType; | ||
message: string; | ||
} | ||
|
||
const iconMap: Record<ToastType, React.FC<React.SVGProps<SVGSVGElement>> | null> = { | ||
default: null, | ||
success: Check, | ||
warning: Exclamation, | ||
}; | ||
|
||
// 토스트 컴포넌트는 UI를 보여주는 책임만 가질 수 있도록 최대한 책임을 분리하고 스토리북을 활용한 UI 테스트를 쉽게할 수 있도록 한다.(@해리) | ||
export default function Toast({ isOpen, type = 'default', message }: ToastProps) { | ||
const ToastIcon = iconMap[type]; | ||
|
||
return ( | ||
<div css={s_toastContainer(isOpen)}> | ||
{type !== 'default' && ( | ||
<div css={[s_iconContainer, s_iconBackgroundColor(type)]}> | ||
{ToastIcon && <ToastIcon width={16} height={16} stroke={theme.colors.white} />} | ||
</div> | ||
)} | ||
<p css={s_toastText}>{message}</p> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.