// src/components/DraggableBottomSheet.tsx
import { Box } from '@mui/material';
import { useState, useRef, useEffect } from 'react';
const BOTTOM_NAV_HEIGHT = 56;
const HANDLE_HEIGHT = 40;
const MIN_SHEET_HEIGHT = BOTTOM_NAV_HEIGHT + HANDLE_HEIGHT;
export const DraggableBottomSheet = () => {
// 상태 및 ref 선언
const [height, setHeight] = useState<number>(MIN_SHEET_HEIGHT);
const startYRef = useRef<number | null>(null);
const heightRef = useRef<number>(MIN_SHEET_HEIGHT);
const maxHeightRef = useRef<number>(window.innerHeight * 0.5);
// 화면 리사이즈 대응
useEffect(() => {
const handleResize = () => {
maxHeightRef.current = window.innerHeight * 0.5;
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// 터치 이벤트 핸들러
const handleTouchStart = (e: React.TouchEvent) => {
startYRef.current = e.touches[0].clientY;
heightRef.current = height;
};
const handleTouchMove = (e: React.TouchEvent) => {
if (startYRef.current !== null) {
const deltaY = e.touches[0].clientY - startYRef.current;
const newHeight = Math.min(
Math.max(MIN_SHEET_HEIGHT, heightRef.current - deltaY),
maxHeightRef.current
);
setHeight(newHeight);
}
};
const handleTouchEnd = () => {
startYRef.current = null;
};
// handle 클릭 시 toggle
const handleClickHandle = () => {
const threshold = 20;
if (Math.abs(height - maxHeightRef.current) < threshold) {
setHeight(MIN_SHEET_HEIGHT);
} else {
setHeight(maxHeightRef.current);
}
};
// 렌더링
return (
<Box
sx={{
position: 'fixed',
bottom: `${BOTTOM_NAV_HEIGHT}px`,
left: 2,
right: 2,
height: `${height}px`,
backgroundColor: 'white',
border: '1px solid #e0e0e0',
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
boxShadow: 3,
overflow: 'hidden',
zIndex: 1400,
touchAction: 'none',
transition: 'height 0.1s ease',
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
{/* handle */}
<Box
sx={{
position: 'absolute', // (1)
top: 0, // (2)
left: '50%',
transform: 'translate(-50%, 25%)',
width: 50,
height: 6,
borderRadius: 3,
backgroundColor: 'grey.400', // (3)
cursor: 'pointer',
userSelect: 'none',
zIndex: 1,
}}
onClick={handleClickHandle}
/>
{/* 본문 */}
<Box
sx={{
position: 'relative', // (4)
width: '100%',
height: '100%',
pt: 6,
}}
>
<Box sx={{ p: 2, mt: 1 }}>
Draggable BottomSheet Content
</Box>
</Box>
</Box>
);
};
const handleTouchStart = (e: React.TouchEvent) => {
startYRef.current = e.touches[0].clientY; // (1)
heightRef.current = height; // (2)
};
const handleTouchMove = (e: React.TouchEvent) => {
if (startYRef.current !== null) {
const deltaY = e.touches[0].clientY - startYRef.current; // (1)
const newHeight = Math.min(
Math.max(MIN_SHEET_HEIGHT, heightRef.current - deltaY), // (2)
maxHeightRef.current
);
setHeight(newHeight); // (3)
}
};
const handleTouchEnd = () => {
startYRef.current = null; // (1)
};
const handleClickHandle = () => {
const threshold = 20;
if (Math.abs(height - maxHeightRef.current) < threshold) { // (1)
setHeight(MIN_SHEET_HEIGHT); // (2)
} else {
setHeight(maxHeightRef.current); // (3)
}
};
<Box
sx={{ position: 'fixed', ... }}
>
<Box
sx={{
position: 'absolute', // (1)
top: 0, // (2)
backgroundColor: 'grey.400', // (3)
}}
/>
<Box
sx={{
position: 'relative', // (4)
}}
>
{/* Content */}
</Box>
</Box>
✅ absolute는 가장 가까운 relative, fixed, absolute, sticky 부모를 기준으로 위치를 잡는다.
✅ 여기서는 fixed가 기준이다.