diff --git a/src/components/shared/ModalBottomSheet/ModalBottomSheet.styles.ts b/src/components/shared/ModalBottomSheet/ModalBottomSheet.styles.ts new file mode 100644 index 00000000..83c9c446 --- /dev/null +++ b/src/components/shared/ModalBottomSheet/ModalBottomSheet.styles.ts @@ -0,0 +1,55 @@ +import styled from '@emotion/styled'; + +export const StyledDeemBackground = styled.div` + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.PALETTE.GRAY_900}; + opacity: 0.5; +`; + +export const StyledBottomSheet = styled.div` + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 75%; + ${({ theme }) => theme.STYLES.FLEX_CENTER}; + flex-direction: column; + background-color: white; + border-top-left-radius: 0.75rem; + border-top-right-radius: 0.75rem; + animation: bottom-sheet-up 0.2s ease-in-out; +`; + +export const StyledModalHeader = styled.div` + position: absolute; + top: 0.69rem; + width: 3.125rem; + height: 0.1875rem; + background-color: ${({ theme }) => theme.PALETTE.GRAY_300}; +`; + +export const StyledModalBottom = styled.div` + @keyframes bottom-sheet-up { + 0% { + transform: translateY(100%); + } + 100% { + transform: translateY(0); + } + } + + @keyframes bottom-sheet-down { + 0% { + transform: translateY(0); + } + 100% { + transform: translateY(100%); + } + } +`; + +export const BottomSheetContainer = styled.div``; diff --git a/src/components/shared/ModalBottomSheet/ModalBottomSheet.tsx b/src/components/shared/ModalBottomSheet/ModalBottomSheet.tsx new file mode 100644 index 00000000..10a7d357 --- /dev/null +++ b/src/components/shared/ModalBottomSheet/ModalBottomSheet.tsx @@ -0,0 +1,92 @@ +import React, { useEffect, useRef, useState } from 'react'; + +import { useBottomSheet } from '@hooks/useBottomSheet'; + +import { + BottomSheetContainer, + StyledBottomSheet, + StyledDeemBackground, + StyledModalBottom, + StyledModalHeader, +} from './ModalBottomSheet.styles'; + +type ModalBottomSheetProps = { + children: React.ReactNode; + setShowModal: React.Dispatch>; +}; + +const BottomSheet = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +export const ModalBottomSheet = (props: ModalBottomSheetProps) => { + const [isModalVisible, setIsModalVisible] = useState(true); + const modalRef = useRef(null); + + useEffect(() => { + document.body.style.overflow = 'hidden'; + + const handleScroll = () => { + if (window.scrollY < 0) { + setIsModalVisible(false); + } + }; + + const handleClickOutside = (event: MouseEvent) => { + if ( + modalRef.current && + !modalRef.current.contains(event.target as Node) + ) { + setIsModalVisible(false); + props.setShowModal(false); + + if (modalRef.current.style) { + modalRef.current.style.animation = + 'bottom-sheet-down 0.2s ease-in-out'; + modalRef.current.style.transform = 'translateY(100%)'; + } + + setTimeout(() => { + setIsModalVisible(false); + }, 200); + } + }; + + window.addEventListener('scroll', handleScroll); + document.addEventListener('mousedown', handleClickOutside); + + return () => { + window.removeEventListener('scroll', handleScroll); + document.removeEventListener('mousedown', handleClickOutside); + document.body.style.overflow = 'unset'; + }; + }, [props]); + + const { handleTouchStart, handleTouchMove, handleTouchEnd } = useBottomSheet( + () => { + setIsModalVisible(false); + props.setShowModal(false); + } + ); + + return ( + + {isModalVisible && ( + <> + + + + + {props.children} + + + + )} + + ); +}; diff --git a/src/components/shared/ModalBottomSheet/index.ts b/src/components/shared/ModalBottomSheet/index.ts new file mode 100644 index 00000000..6ba92c56 --- /dev/null +++ b/src/components/shared/ModalBottomSheet/index.ts @@ -0,0 +1 @@ +export * from './ModalBottomSheet'; diff --git a/src/hooks/useBottomSheet.ts b/src/hooks/useBottomSheet.ts new file mode 100644 index 00000000..476b5647 --- /dev/null +++ b/src/hooks/useBottomSheet.ts @@ -0,0 +1,30 @@ +import { useCallback, useRef } from 'react'; + +export const useBottomSheet = (closeModal: () => void) => { + const record = useRef({ + first: 0, + move: 0, + }); + + const handleTouchStart = (event: React.TouchEvent) => { + record.current.first = event.touches[0].screenY; + }; + + const handleTouchMove = useCallback((event: React.TouchEvent) => { + if (!record.current.move) { + record.current.move = event.touches[0].screenY; + } + }, []); + + const handleTouchEnd = () => { + if (record.current.first !== null && record.current.move !== null) { + if (record.current.first < record.current.move) { + closeModal(); + } + } + record.current.first = 0; + record.current.move = 0; + }; + + return { handleTouchStart, handleTouchMove, handleTouchEnd }; +};