From 8a4d7b82ec815f4d694b9befff7b904a753acddf Mon Sep 17 00:00:00 2001 From: nangkyeonglim Date: Mon, 14 Aug 2023 23:21:33 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20`AnimationDiv`=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../@common/AnimationDiv/AnimationDiv.tsx | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 frontend/src/components/@common/AnimationDiv/AnimationDiv.tsx diff --git a/frontend/src/components/@common/AnimationDiv/AnimationDiv.tsx b/frontend/src/components/@common/AnimationDiv/AnimationDiv.tsx new file mode 100644 index 000000000..3ff7fddca --- /dev/null +++ b/frontend/src/components/@common/AnimationDiv/AnimationDiv.tsx @@ -0,0 +1,120 @@ +import { useState, useLayoutEffect, HTMLAttributes, AnimationEventHandler } from 'react'; +import { css, styled } from 'styled-components'; +import * as animation from 'styles/animation'; + +type DisplayState = 'hidden' | 'appear' | 'disappear' | 'show'; +type Animation = 'slide'; +type DirectionTo = 'up' | 'down' | 'left' | 'right'; + +type Props = { + appearAnimation?: Animation; + disappearAnimation?: Animation; + animationDuration?: number; + directionTo?: DirectionTo; + isVisible: boolean; + children: React.ReactNode; + onAppear?: AnimationEventHandler; + onDisappear?: AnimationEventHandler; +} & Omit, 'onAnimationEnd'>; + +// hidden -> appear(애니메이션) -> show -> disappear(애니메이션) -> hidden 반복. +const AnimationDiv = ({ + appearAnimation = 'slide', + disappearAnimation = 'slide', + animationDuration = 0.5, + directionTo = 'up', + isVisible, + children, + onAppear, + onDisappear, + ...rest +}: Props) => { + const [displayState, setDisplayState] = useState('hidden'); + + // 바깥에서 받아온 isVisible 상태로 appear할 건지 disappear 할 건지 결정. + // isVisible을 true로 받으면 appear, display가 show인 상태에서 isVisible을 false로 받으면 disappear. + useLayoutEffect( + function checkDisplayState() { + const isConditionAppear = isVisible; + const isConditionDisappear = displayState === 'show' && !isVisible; + + if (!isConditionAppear && !isConditionDisappear) return; + + setDisplayState(isVisible ? 'appear' : 'disappear'); + }, + [isVisible], + ); + + // hidden 상태인데 isVisible도 false라면 아무것도 렌더링 되지 않아야함. + const isUnmounted = isVisible === false && displayState === 'hidden'; + + if (isUnmounted) return <>; + + // appear와 disappear 애니메이션이 끝나고(onAnimationEnd) displayState를 업데이트. + const updateDisplayState: AnimationEventHandler = (e) => { + if (displayState === 'appear') { + setDisplayState('show'); + onAppear?.(e); + } + + if (displayState === 'disappear') { + setDisplayState('hidden'); + onDisappear?.(e); + } + }; + + // 현재 실행할 애니메이션 결정. + const currentAnimation = isVisible ? appearAnimation : disappearAnimation; + + return ( + + {children} + + ); +}; + +export default AnimationDiv; + +const genAnimationStyle = ( + displayState: DisplayState, + animationType: Animation, + direction: DirectionTo, +) => { + if (displayState === 'show' || displayState === 'hidden') return 'none'; + if (animationType === 'slide') { + if (direction === 'down') return animation.slideToDown; + if (direction === 'up') return animation.slideToUp; + if (direction === 'right') return animation.slideToRight; + if (direction === 'left') return animation.slideToLeft; + } +}; + +const S = { + Wrapper: styled.div<{ + $displayState: DisplayState; + $directionTo: DirectionTo; + $animation: Animation; + $duration: number; + }>` + animation: ${({ $displayState, $animation, $directionTo, $duration }) => css` + ${genAnimationStyle($displayState, $animation, $directionTo)} ${$duration}s + `}; + ${({ $displayState }) => + $displayState === 'disappear' + ? css` + animation-direction: reverse; + animation-fill-mode: forwards; + ` + : css` + animation-direction: normal; + animation-fill-mode: normal; + `} + `, +};