-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
프로필 바텀 시트 UI 구현
- Loading branch information
Showing
44 changed files
with
1,274 additions
and
124 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
30 changes: 30 additions & 0 deletions
30
frontend/src/components/BottomSheet/BottomSheet.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,30 @@ | ||
import type { Meta, StoryObj } from '@storybook/react'; | ||
import BottomSheet from './BottomSheet'; | ||
import Button from '@_components/Button/Button'; | ||
|
||
const meta = { | ||
component: BottomSheet, | ||
title: 'Components/BottomSheet', | ||
} satisfies Meta<typeof BottomSheet>; | ||
|
||
export default meta; | ||
|
||
type Story = StoryObj<typeof meta>; | ||
|
||
export const Default: Story = { | ||
args: { | ||
isOpen: true, | ||
onDimmerClick: () => {}, | ||
header: <BottomSheet.Header>Header</BottomSheet.Header>, | ||
cta: ( | ||
<BottomSheet.CTA> | ||
<Button shape="bar">CTA</Button> | ||
</BottomSheet.CTA> | ||
), | ||
}, | ||
render: (args) => ( | ||
<BottomSheet {...args}> | ||
<BottomSheet.Main>Content</BottomSheet.Main> | ||
</BottomSheet> | ||
), | ||
}; |
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,136 @@ | ||
import Dimmer from '@_components/Dimmer/Dimmer'; | ||
import { PropsWithChildren, useEffect, useState } from 'react'; | ||
import BottomSheetContainer from './BottomSheetContainer/BottomSheetContainer'; | ||
import BottomSheetBody from './BottomSheetBody/BottomSheetBody'; | ||
import BottomSheetHandle from './BottomSheetHandle/BottomSheetHandle'; | ||
|
||
interface BottomSheetProps { | ||
isOpen: boolean; | ||
onDimmerClick: () => void; | ||
|
||
header?: React.ReactNode; | ||
cta?: React.ReactNode; | ||
|
||
size?: 'small' | 'medium' | 'large' | 'full'; | ||
} | ||
|
||
export default function BottomSheet( | ||
props: PropsWithChildren<BottomSheetProps>, | ||
) { | ||
const { isOpen, onDimmerClick, header, cta, children, size } = props; | ||
|
||
const [startY, setStartY] = useState(0); // 터치 시작 Y좌표 | ||
const [currentY, setCurrentY] = useState(window.innerHeight); // 초기에는 화면 아래에 위치 | ||
const [isDragging, setIsDragging] = useState(false); // 드래그 중인지 여부 | ||
const [isClosing, setIsClosing] = useState(false); // 닫히는 애니메이션 여부 | ||
|
||
// Bottom Sheet가 열릴 때 애니메이션으로 위로 올라옴 | ||
useEffect(() => { | ||
if (isOpen) { | ||
document.body.style.overflow = 'hidden'; // 스크롤 비활성화 | ||
|
||
// 열릴 때 100px 아래에서 0으로 부드럽게 올라오도록 애니메이션 시작 | ||
setTimeout(() => { | ||
setCurrentY(0); // Y값을 0으로 변경 -> 밑에서 위로 올라오는 애니메이션 | ||
}, 10); // 딜레이를 줘야 애니메이션이 자연스럽게 동작 | ||
} else { | ||
setCurrentY(window.innerHeight); // 닫힐 때 다시 화면 아래로 | ||
document.body.style.overflow = 'auto'; // 스크롤 활성화 | ||
} | ||
|
||
return () => { | ||
document.body.style.overflow = 'auto'; | ||
}; | ||
}, [isOpen]); | ||
|
||
useEffect(() => { | ||
if (!isOpen) { | ||
setCurrentY(window.innerHeight); // 닫힐 때 높이를 초기화 | ||
setIsClosing(false); // 닫힘 애니메이션 초기화 | ||
} | ||
}, [isOpen]); | ||
|
||
// Dimmer 클릭 시 애니메이션으로 밑으로 내려간 후 닫힘 | ||
const handleDimmerClick = () => { | ||
setIsClosing(true); | ||
setCurrentY(window.innerHeight); // 화면 아래로 내려가는 애니메이션 | ||
setTimeout(() => { | ||
onDimmerClick(); // 300ms 후 실제로 닫기 | ||
}, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음 | ||
}; | ||
|
||
// 드래그 시작 | ||
const handleTouchStart = (event: React.TouchEvent) => { | ||
setStartY(event.touches[0].clientY); | ||
setIsDragging(true); | ||
}; | ||
|
||
// 드래그 중 | ||
const handleTouchMove = (event: React.TouchEvent) => { | ||
if (!isDragging) return; | ||
|
||
const currentTouchY = event.touches[0].clientY; | ||
const deltaY = currentTouchY - startY; | ||
|
||
// Y축으로만 움직임을 감지 | ||
if (deltaY > 0) { | ||
setCurrentY(deltaY); // 드래그된 만큼 값을 저장 | ||
} | ||
}; | ||
|
||
// 드래그 종료 | ||
const handleTouchEnd = () => { | ||
if (!isDragging) return; | ||
setIsDragging(false); | ||
|
||
// 드래그가 일정 값 이상이면 Bottom Sheet를 닫음 | ||
if (currentY > 100) { | ||
// 애니메이션으로 Bottom Sheet를 아래로 내리고 닫기 | ||
setIsClosing(true); | ||
setCurrentY(window.innerHeight); // 화면 하단으로 내리는 애니메이션 | ||
setTimeout(() => { | ||
onDimmerClick(); // 애니메이션 후 실제로 닫기 | ||
}, 300); // 애니메이션 시간이 0.3초이므로 300ms 후에 닫음 | ||
} else { | ||
setCurrentY(0); // 원래 위치로 되돌림 | ||
} | ||
}; | ||
|
||
if (!isOpen && !isClosing) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<BottomSheetContainer> | ||
<Dimmer onClick={handleDimmerClick} /> | ||
<BottomSheetBody currentY={currentY} size={size} isDragging={isDragging}> | ||
<BottomSheetHandle | ||
onTouchStart={handleTouchStart} | ||
onTouchMove={handleTouchMove} | ||
onTouchEnd={handleTouchEnd} | ||
/> | ||
{header} | ||
{children} | ||
{cta} | ||
</BottomSheetBody> | ||
</BottomSheetContainer> | ||
); | ||
} | ||
|
||
BottomSheet.Header = function BottomSheetHeader(props: PropsWithChildren) { | ||
const { children } = props; | ||
|
||
return <div css={{ padding: '0 24px' }}>{children}</div>; | ||
}; | ||
|
||
BottomSheet.Main = function BottomSheetMain(props: PropsWithChildren) { | ||
const { children } = props; | ||
|
||
return <div css={{ padding: '0px 24px' }}>{children}</div>; | ||
}; | ||
|
||
BottomSheet.CTA = function BottomSheetCTA(props: PropsWithChildren) { | ||
const { children } = props; | ||
|
||
return <div css={{ padding: '0px 24px' }}>{children}</div>; | ||
}; |
39 changes: 39 additions & 0 deletions
39
frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.style.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,39 @@ | ||
import { css, Theme } from '@emotion/react'; | ||
|
||
export const body = ({ | ||
theme, | ||
currentY, | ||
size, | ||
isDragging, | ||
}: { | ||
theme: Theme; | ||
currentY: number; | ||
size?: 'small' | 'medium' | 'large' | 'full'; | ||
isDragging: boolean; | ||
}) => css` | ||
z-index: 2; | ||
/* 터치 드래그에 따른 Y축 이동 */ | ||
transform: translateY(${currentY}px); | ||
display: flex; | ||
flex-direction: column; | ||
gap: 24px; | ||
width: 100%; | ||
max-width: 600px; | ||
height: ${size === 'medium' | ||
? '50vh' | ||
: size === 'large' | ||
? '80vh' | ||
: size === 'full' | ||
? '100vh' | ||
: 'auto'}; | ||
padding-bottom: 32px; | ||
background-color: ${theme.colorPalette.white[100]}; | ||
border-radius: 28px 28px 0 0; | ||
/* 터치 드래그에 따른 Y축 이동 */ | ||
transition: ${isDragging ? 'none' : 'transform 0.3s ease'}; | ||
`; |
21 changes: 21 additions & 0 deletions
21
frontend/src/components/BottomSheet/BottomSheetBody/BottomSheetBody.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,21 @@ | ||
import { useTheme } from '@emotion/react'; | ||
import { PropsWithChildren } from 'react'; | ||
import * as S from './BottomSheetBody.style'; | ||
|
||
interface BottomSheetBodyProps { | ||
currentY: number; | ||
size?: 'small' | 'medium' | 'large' | 'full'; | ||
isDragging: boolean; | ||
} | ||
|
||
export default function BottomSheetBody( | ||
props: PropsWithChildren<BottomSheetBodyProps>, | ||
) { | ||
const { currentY, size, isDragging, children } = props; | ||
|
||
const theme = useTheme(); | ||
|
||
return ( | ||
<div css={S.body({ theme, currentY, size, isDragging })}>{children}</div> | ||
); | ||
} |
15 changes: 15 additions & 0 deletions
15
frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.style.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,15 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const container = css` | ||
position: fixed; | ||
z-index: 1; | ||
inset: 0; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: flex-end; | ||
width: 100%; | ||
height: 100%; | ||
`; |
8 changes: 8 additions & 0 deletions
8
frontend/src/components/BottomSheet/BottomSheetContainer/BottomSheetContainer.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,8 @@ | ||
import { PropsWithChildren } from 'react'; | ||
import * as S from './BottomSheetContainer.style'; | ||
|
||
export default function BottomSheetContainer(props: PropsWithChildren) { | ||
const { children } = props; | ||
|
||
return <div css={S.container}>{children}</div>; | ||
} |
15 changes: 15 additions & 0 deletions
15
frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.style.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,15 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const handleWrapper = css` | ||
display: flex; | ||
align-items: center; | ||
justify-content: center; | ||
height: 32px; | ||
`; | ||
|
||
export const handleBar = css` | ||
width: 50px; | ||
height: 6px; | ||
background-color: black; | ||
border-radius: 12px; | ||
`; |
22 changes: 22 additions & 0 deletions
22
frontend/src/components/BottomSheet/BottomSheetHandle/BottomSheetHandle.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,22 @@ | ||
import * as S from './BottomSheetHandle.style'; | ||
|
||
interface BottomSheetHandleProps { | ||
onTouchStart: (event: React.TouchEvent) => void; | ||
onTouchMove: (event: React.TouchEvent) => void; | ||
onTouchEnd: () => void; | ||
} | ||
|
||
export default function BottomSheetHandle(props: BottomSheetHandleProps) { | ||
const { onTouchStart, onTouchMove, onTouchEnd } = props; | ||
|
||
return ( | ||
<div | ||
css={S.handleWrapper} | ||
onTouchStart={onTouchStart} | ||
onTouchMove={onTouchMove} | ||
onTouchEnd={onTouchEnd} | ||
> | ||
<div css={S.handleBar} /> | ||
</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,22 @@ | ||
import CloseIcon from '@_components/Icons/CloseIcon'; | ||
import { css } from '@emotion/react'; | ||
import { ButtonHTMLAttributes } from 'react'; | ||
|
||
interface CloseButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {} | ||
|
||
export default function CloseButton(props: CloseButtonProps) { | ||
const { ...rest } = props; | ||
|
||
return ( | ||
<button | ||
css={css` | ||
padding: 0.4rem; | ||
background: none; | ||
border: none; | ||
`} | ||
{...rest} | ||
> | ||
<CloseIcon /> | ||
</button> | ||
); | ||
} |
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,16 @@ | ||
import { css } from '@emotion/react'; | ||
|
||
export const dimmer = css` | ||
position: fixed; | ||
inset: 0; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: center; | ||
justify-content: flex-end; | ||
width: 100%; | ||
height: 100%; | ||
background-color: rgb(0 0 0 / 20%); | ||
`; |
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,5 @@ | ||
import * as S from './Dimmer.style'; | ||
|
||
export default function Dimmer({ onClick }: { onClick: () => void }) { | ||
return <div onClick={onClick} css={S.dimmer} />; | ||
} |
Oops, something went wrong.