From 98765175238938ade1f9d50473bd8b44cd8078dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=90=E1=85=A2=E1=84=92=E1=85=AE?= =?UTF-8?q?=E1=86=AB?= Date: Sun, 4 Aug 2024 22:48:49 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20BottomSheet=20handler=20drag=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomSheet/BottomSheet.style.ts | 15 +++-- .../components/BottomSheet/BottomSheet.tsx | 52 ++++++++++------ .../components/BottomSheet/useBottomSheet.ts | 59 +++++++++++++++---- 3 files changed, 90 insertions(+), 36 deletions(-) diff --git a/HDesign/src/components/BottomSheet/BottomSheet.style.ts b/HDesign/src/components/BottomSheet/BottomSheet.style.ts index 8b96a65f3..2a6506d7e 100644 --- a/HDesign/src/components/BottomSheet/BottomSheet.style.ts +++ b/HDesign/src/components/BottomSheet/BottomSheet.style.ts @@ -5,7 +5,6 @@ import {Theme} from '@theme/theme.type'; export const display = (visible: boolean) => css({ visibility: visible ? 'visible' : 'hidden', - transition: 'visibility 0.2s ease-in-out', }); export const dimmedLayerStyle = (theme: Theme, isOpened: boolean) => @@ -23,7 +22,7 @@ export const dimmedLayerStyle = (theme: Theme, isOpened: boolean) => transitionTimingFunction: 'cubic-bezier(0.7, 0.62, 0.62, 1.16)', }); -export const bottomSheetContainerStyle = (theme: Theme, isOpened: boolean) => +export const bottomSheetContainerStyle = (theme: Theme, isOpened: boolean, isDragging: boolean, translateY: number) => css({ position: 'fixed', display: 'flex', @@ -37,14 +36,20 @@ export const bottomSheetContainerStyle = (theme: Theme, isOpened: boolean) => borderRadius: '1.5rem 1.5rem 0 0', backgroundColor: theme.colors.white, - transform: isOpened ? 'translateY(0)' : 'translateY(100%)', - transition: 'transform 0.2s ease-in-out', + transform: isOpened ? `translateY(${translateY}px)` : 'translateY(100%)', + transition: isDragging ? 'none' : 'transform 0.2s ease-in-out', }); +export const indicatorContainerStyle = css({ + display: 'flex', + justifyContent: 'center', + padding: '0.5rem 0', + width: '100%', +}); + export const indicatorStyle = (theme: Theme) => css({ display: 'flex', - margin: '0.5rem 0', width: '5rem', height: '0.25rem', borderRadius: '0.125rem', diff --git a/HDesign/src/components/BottomSheet/BottomSheet.tsx b/HDesign/src/components/BottomSheet/BottomSheet.tsx index 4e25137c0..f920a0fdb 100644 --- a/HDesign/src/components/BottomSheet/BottomSheet.tsx +++ b/HDesign/src/components/BottomSheet/BottomSheet.tsx @@ -1,6 +1,6 @@ /** @jsxImportSource @emotion/react */ import {createPortal} from 'react-dom'; -import {useEffect, useState} from 'react'; +import {useEffect, useRef, useState} from 'react'; import {BottomSheetProps} from '@components/BottomSheet/BottomSheet.type'; import FixedButton from '@components/FixedButton/FixedButton'; @@ -8,29 +8,45 @@ import FixedButton from '@components/FixedButton/FixedButton'; import {useTheme} from '@theme/HDesignProvider'; import {useBottomSheet} from './useBottomSheet'; -import {bottomSheetContainerStyle, dimmedLayerStyle, display, indicatorStyle} from './BottomSheet.style'; - -const BottomSheet: React.FC = ({isOpened = false, children, ...props}: BottomSheetProps) => { +import { + bottomSheetContainerStyle, + dimmedLayerStyle, + display, + indicatorContainerStyle, + indicatorStyle, +} from './BottomSheet.style'; + +const BottomSheet: React.FC = ({ + isOpened = false, + children, + onChangeClose, + onChangeOpen, + ...props +}: BottomSheetProps) => { const {theme} = useTheme(); - const {opened, handleClose} = useBottomSheet({isOpened, ...props}); - const [visible, setVisible] = useState(isOpened); - - useEffect(() => { - if (opened) { - setVisible(true); - } else { - const timer = setTimeout(() => setVisible(false), 200); - - return () => clearTimeout(timer); - } - }, [opened]); + const {opened, visible, handleClose, handleDragStart, handleDrag, handleDragEnd, isDragging, translateY} = + useBottomSheet({ + isOpened, + onChangeClose, + onChangeOpen, + }); // TODO: (@todari) : children 길이 길 때 overflow button에 안가리는 영역 처리 return createPortal(
-
-
+
+
+
+
{children}
, diff --git a/HDesign/src/components/BottomSheet/useBottomSheet.ts b/HDesign/src/components/BottomSheet/useBottomSheet.ts index ca2274e19..2c877041f 100644 --- a/HDesign/src/components/BottomSheet/useBottomSheet.ts +++ b/HDesign/src/components/BottomSheet/useBottomSheet.ts @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useRef, useState} from 'react'; interface UseBottomSheetProps { isOpened: boolean; @@ -7,7 +7,22 @@ interface UseBottomSheetProps { } export const useBottomSheet = ({isOpened, onChangeClose, onChangeOpen}: UseBottomSheetProps) => { - const [opened, setOpened] = useState(false); + const [opened, setOpened] = useState(isOpened); + const [visible, setVisible] = useState(isOpened); + + const [isDragging, setIsDragging] = useState(false); + const [translateY, setTranslateY] = useState(0); + const startY = useRef(0); + + useEffect(() => { + if (opened) { + setVisible(true); + } else { + const timer = setTimeout(() => setVisible(false), 200); + + return () => clearTimeout(timer); + } + }, [opened]); useEffect(() => { setOpened(isOpened); @@ -16,6 +31,14 @@ export const useBottomSheet = ({isOpened, onChangeClose, onChangeOpen}: UseBotto } else { handleOpen(); } + + document.body.style.overflow = 'hidden'; + document.body.addEventListener('keydown', handleKeyDownEsc); + + return () => { + document.body.style.overflow = 'scroll'; + document.body.removeEventListener('keydown', handleKeyDownEsc); + }; }, [isOpened]); const handleClose = useCallback(() => { @@ -23,24 +46,34 @@ export const useBottomSheet = ({isOpened, onChangeClose, onChangeOpen}: UseBotto if (onChangeClose) { onChangeClose(); } - }, []); + }, [onChangeClose]); const handleOpen = useCallback(() => { setOpened(true); if (onChangeOpen) { onChangeOpen(); } - }, []); + }, [onChangeOpen]); - useEffect(() => { - document.body.style.overflow = 'hidden'; - document.body.addEventListener('keydown', handleKeyDownEsc); + const handleDragStart = (e: React.TouchEvent | React.MouseEvent) => { + setIsDragging(true); + startY.current = 'touches' in e ? e.touches[0].clientY : e.clientY; + }; - return () => { - document.body.style.overflow = 'scroll'; - document.body.removeEventListener('keydown', handleKeyDownEsc); - }; - }, [isOpened]); + const handleDrag = (e: React.TouchEvent | React.MouseEvent) => { + if (!isDragging) return; + const currentY = 'touches' in e ? e.touches[0].clientY : e.clientY; + const deltaY = currentY - startY.current; + setTranslateY(Math.max(deltaY, 0)); + }; + + const handleDragEnd = () => { + setIsDragging(false); + if (translateY > window.screen.height / 10) { + handleClose(); + } + setTranslateY(0); + }; const handleKeyDownEsc = (e: KeyboardEvent) => { if (e.key === 'Escape' && isOpened) { @@ -48,5 +81,5 @@ export const useBottomSheet = ({isOpened, onChangeClose, onChangeOpen}: UseBotto } }; - return {opened, handleClose}; + return {opened, visible, handleClose, handleDragStart, handleDrag, handleDragEnd, isDragging, translateY}; };