-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[NDD-312] Toast 개발 완료 (10h / 8h) (#171)
* feat: 토스트 상태 관리를 위한 EventManger 추가 * feat: 토스트 틀 구현 완료 * design: 토스트 타입별 아이콘 추가 * fix: autoClose 옵션 꺼져있을 때 호버 이벤트시 토스트 바로 종료되는 문제 수정 * feat: 토스트 위치 지정 기능 추가 * fix: autoClose 타입 변경 autoClose 속성이 true이면 토스트가 바로 사라지는 문제 발생 * remove: 토스트 업데이트 관련 로직 제거 업데이트는 토스트를 사용해서 promise 등의 상태를 표시해줄 때 필요함. 추후 이 부분이 기획으로 추가된다면 그 때 개발하기 * feat: collapseToast을 적용해서 토스트가 자연스럽게 사라지도록 구현 * refactor: 애니메이션 끝나는 이벤트 훅으로 분리 * feat: 토스트 프로그래스바 추가 * feat: 마우스 호버시 토스트 타이머 멈춤 * refactor: useToastContainer 훅 분리 * rename: EventManger -> eventMangerˆ * rename: EventManger -> eventManger 변경 후 import 수정 * rename: Toast -> ToastItem * refactor: toast.type(text) 형식으로 출력할 수 있게 함수 변경 * design: 토스트 타입에 따라서 다른 프로그래스바 색상 추가 * chore: 토스트 옵션의 타입을 옵셔널로 변경 * refactor: useAnimationEnd 훅 제거하고 컴포넌트에 직접 리스너 달기 react는 바닐라가 아니야!!! * rename: 핸들러 네이밍 수정 * design: 토스트 애니메이션 조정
- Loading branch information
Showing
11 changed files
with
467 additions
and
0 deletions.
There are no files selected for viewing
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
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
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,80 @@ | ||
import { css, keyframes } from '@emotion/react'; | ||
import { theme } from '@styles/theme'; | ||
|
||
const TOAST_GAP = '0.75rem'; | ||
export const ToastProgressBarStyle = { | ||
info: css` | ||
background-color: ${theme.colors.toast.info}; | ||
`, | ||
success: css` | ||
background-color: ${theme.colors.toast.success}; | ||
`, | ||
warning: css` | ||
background-color: ${theme.colors.toast.warning}; | ||
`, | ||
error: css` | ||
background-color: ${theme.colors.toast.error}; | ||
`, | ||
default: css` | ||
background-color: ${theme.colors.toast.default}; | ||
`, | ||
}; | ||
|
||
export const ToastTypeIconName = { | ||
info: 'info-round', | ||
success: 'check-round', | ||
warning: 'warning', | ||
error: 'bomb', | ||
default: null, | ||
}; | ||
|
||
export const ToastPositionStyle = { | ||
topLeft: css` | ||
top: ${TOAST_GAP}; | ||
left: ${TOAST_GAP}; | ||
`, | ||
topRight: css` | ||
top: ${TOAST_GAP}; | ||
right: ${TOAST_GAP}; | ||
`, | ||
topCenter: css` | ||
top: ${TOAST_GAP}; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
z-index: 10000; | ||
`, | ||
bottomLeft: css` | ||
bottom: ${TOAST_GAP}; | ||
left: ${TOAST_GAP}; | ||
`, | ||
bottomRight: css` | ||
bottom: ${TOAST_GAP}; | ||
right: ${TOAST_GAP}; | ||
`, | ||
bottomCenter: css` | ||
bottom: ${TOAST_GAP}; | ||
left: 50%; | ||
transform: translateX(-50%); | ||
z-index: 10000; | ||
`, | ||
}; | ||
|
||
export const ToastFadeOutUpAnimation = keyframes` | ||
from { | ||
opacity: 1; | ||
transform: translateY(0); | ||
} | ||
to { | ||
opacity: 0; | ||
transform: translateY(-1.5rem); | ||
} | ||
`; | ||
|
||
export const ToastProgressBarAnimation = keyframes` | ||
from { | ||
transform: scaleX(1); | ||
} | ||
to { | ||
transform: scaleX(0); | ||
} | ||
`; |
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,29 @@ | ||
import { css } from '@emotion/react'; | ||
import { ToastPositionStyle } from '@foundation/Toast/Toast.styles'; | ||
import useToastContainer from '@foundation/Toast/useToastContainer'; | ||
import ToastItem from '@foundation/Toast/ToastItem'; | ||
|
||
export const ToastContainer = () => { | ||
const { getToastPositionGroupToRender } = useToastContainer(); | ||
const positionGroup = getToastPositionGroupToRender(); | ||
|
||
return Array.from(positionGroup).map(([position, toasts]) => ( | ||
<div | ||
key={position} | ||
css={[ | ||
css` | ||
position: fixed; | ||
display: flex; | ||
flex-direction: column; | ||
row-gap: 0.5rem; | ||
z-index: 9999; | ||
`, | ||
ToastPositionStyle[position], | ||
]} | ||
> | ||
{toasts.map((toastProps) => ( | ||
<ToastItem key={toastProps.toastId} {...toastProps} /> | ||
))} | ||
</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,117 @@ | ||
import { useRef, useState } from 'react'; | ||
import { ToastEvent, ToastProps } from '@foundation/Toast/type'; | ||
import { css } from '@emotion/react'; | ||
import { theme } from '@styles/theme'; | ||
import { Box, Icon } from '@foundation/index'; | ||
import { collapseToast } from '@foundation/Toast/collapseToast'; | ||
import { eventManager } from '@foundation/Toast/eventManger'; | ||
import { | ||
ToastFadeOutUpAnimation, | ||
ToastProgressBarAnimation, | ||
ToastProgressBarStyle, | ||
ToastTypeIconName, | ||
} from '@foundation/Toast/Toast.styles'; | ||
|
||
const ToastItem: React.FC<ToastProps> = ({ | ||
toastId, | ||
text, | ||
autoClose = 3000, | ||
closeOnClick = true, | ||
type = 'default', | ||
pauseOnHover = true, | ||
}) => { | ||
const toastRef = useRef<HTMLDivElement>(null); | ||
const [isExiting, setIsExiting] = useState(false); | ||
const [isPaused, setIsPaused] = useState(false); | ||
|
||
const handleExitingAnimationEnd = () => { | ||
collapseToast(toastRef.current!, () => { | ||
eventManager.emit(ToastEvent.Delete, toastId); | ||
}); | ||
}; | ||
|
||
const handleProgressAnimationEnd = () => { | ||
autoClose && setIsExiting(true); | ||
}; | ||
|
||
const handleClick = () => { | ||
closeOnClick && handleProgressAnimationEnd(); | ||
}; | ||
|
||
const handleMouseEnter = () => { | ||
pauseOnHover && autoClose && setIsPaused(true); | ||
}; | ||
|
||
const handleMouseLeave = () => { | ||
if (pauseOnHover && autoClose) { | ||
setIsPaused(false); | ||
} | ||
}; | ||
|
||
const IconType = () => | ||
type === 'default' ? null : ( | ||
<Icon id={ToastTypeIconName[type]} width="20" height="20" /> | ||
); | ||
|
||
return ( | ||
<div ref={toastRef}> | ||
<Box | ||
onClick={handleClick} | ||
onMouseEnter={handleMouseEnter} | ||
onMouseLeave={handleMouseLeave} | ||
onAnimationEnd={handleExitingAnimationEnd} | ||
css={[ | ||
css` | ||
position: relative; | ||
display: flex; | ||
flex-direction: column; | ||
row-gap: 0.5rem; | ||
min-width: 20rem; | ||
border-radius: 0.5rem; | ||
overflow: hidden; | ||
background-color: ${theme.colors.surface.default}; | ||
animation: ${isExiting | ||
? css` | ||
${ToastFadeOutUpAnimation} 0.8s forwards | ||
` | ||
: 'none'}; | ||
`, | ||
]} | ||
> | ||
<div | ||
css={css` | ||
display: flex; | ||
align-items: center; | ||
column-gap: 1rem; | ||
padding: 1rem; | ||
`} | ||
> | ||
<IconType /> | ||
{text} | ||
</div> | ||
<div | ||
onAnimationEnd={handleProgressAnimationEnd} | ||
css={[ | ||
css` | ||
position: absolute; | ||
bottom: 0; | ||
height: 0.3125rem; | ||
width: 100%; | ||
transform-origin: left; | ||
animation: ${autoClose | ||
? css` | ||
${ToastProgressBarAnimation} ${autoClose}ms linear | ||
forwards | ||
` | ||
: 'none'}; | ||
animation-play-state: ${isPaused ? 'paused' : 'running'}; | ||
`, | ||
ToastProgressBarStyle[type], | ||
]} | ||
/> | ||
</Box> | ||
</div> | ||
); | ||
}; | ||
|
||
export default ToastItem; |
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,17 @@ | ||
export function collapseToast( | ||
node: HTMLDivElement, | ||
done: () => void, | ||
duration = 1000 | ||
) { | ||
const { scrollHeight, style } = node; | ||
|
||
requestAnimationFrame(() => { | ||
style.height = scrollHeight + 'px'; | ||
style.transition = `all ${duration}ms`; | ||
|
||
requestAnimationFrame(() => { | ||
style.height = '0'; | ||
setTimeout(done, duration); | ||
}); | ||
}); | ||
} |
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,55 @@ | ||
import { EventManager } from '@foundation/Toast/type'; | ||
|
||
export const eventManager: EventManager = { | ||
list: new Map(), // 이벤트 리스너 저장 | ||
emitQueue: new Map(), // 이벤트 지연을 위한 큐 | ||
|
||
// 새로운 이벤트 리스너 등록 | ||
on(event, callback) { | ||
this.list.has(event) | ||
? this.list.get(event)!.push(callback) | ||
: this.list.set(event, [callback]); | ||
|
||
return this; | ||
}, | ||
|
||
// 콜백이 있는경우 해당하는 리스너 제거, 없는 경우 이벤트에 대한 모든 리스너 제거 | ||
off(event, callback) { | ||
if (callback) { | ||
const cb = this.list.get(event)?.filter((cb) => cb !== callback); | ||
cb && this.list.set(event, cb); | ||
return this; | ||
} | ||
this.list.delete(event); | ||
|
||
return this; | ||
}, | ||
|
||
//대기중인 이벤트를 취소시킬 때 필요함 | ||
cancelEmit(event) { | ||
const timers = this.emitQueue.get(event); | ||
if (timers) { | ||
timers.forEach(clearTimeout); | ||
this.emitQueue.delete(event); | ||
} | ||
|
||
return this; | ||
}, | ||
|
||
// 이벤트 발생시키기 | ||
// 타입문제는 일단 보류중! (라이브러리 내부도 해결 안되어있음) | ||
emit(event, ...args: never[]) { | ||
this.list.has(event) && | ||
this.list.get(event)!.forEach((callback) => { | ||
const timer = setTimeout(() => { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-expect-error | ||
return callback(...args); | ||
}, 0); | ||
|
||
this.emitQueue.has(event) | ||
? this.emitQueue.get(event)!.push(timer) | ||
: this.emitQueue.set(event, [timer]); | ||
}); | ||
}, | ||
}; |
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,30 @@ | ||
import { ToastEvent, ToastProps, ToastType } from '@foundation/Toast/type'; | ||
import { eventManager } from '@foundation/Toast/eventManger'; | ||
|
||
type ToastFunctionProps = Omit<ToastProps, 'toastId' | 'type'>; | ||
type ToastOptions = Omit<ToastProps, 'toastId' | 'type' | 'text'>; | ||
const generateUniqueId = () => { | ||
return Date.now().toString(36) + Math.random().toString(36).substring(4); | ||
}; | ||
|
||
const emitToast = (type: ToastType, toastProps: ToastFunctionProps) => { | ||
const id = generateUniqueId(); | ||
eventManager.emit(ToastEvent.Add, { | ||
...toastProps, | ||
toastId: id, | ||
type, | ||
}); | ||
}; | ||
|
||
export const toast = { | ||
default: (text: string, toastOptions?: ToastOptions) => | ||
emitToast('default', { text: text, ...toastOptions }), | ||
info: (text: string, toastOptions?: ToastOptions) => | ||
emitToast('info', { text: text, ...toastOptions }), | ||
success: (text: string, toastOptions?: ToastOptions) => | ||
emitToast('success', { text: text, ...toastOptions }), | ||
warning: (text: string, toastOptions?: ToastOptions) => | ||
emitToast('warning', { text: text, ...toastOptions }), | ||
error: (text: string, toastOptions?: ToastOptions) => | ||
emitToast('error', { text: text, ...toastOptions }), | ||
}; |
Oops, something went wrong.