114 lines
2.9 KiB
JavaScript
114 lines
2.9 KiB
JavaScript
import { useEffect, useRef, useState } from 'react';
|
|
import './MobileSheet.css';
|
|
|
|
export default function MobileSheet({ open, onClose, title, snap = 'full', children }) {
|
|
const [dragY, setDragY] = useState(0);
|
|
const [isDragging, setIsDragging] = useState(false);
|
|
const startYRef = useRef(null);
|
|
const sheetRef = useRef(null);
|
|
|
|
// Lock body scroll when open
|
|
useEffect(() => {
|
|
if (open) {
|
|
document.body.style.overflow = 'hidden';
|
|
} else {
|
|
document.body.style.overflow = '';
|
|
}
|
|
return () => {
|
|
document.body.style.overflow = '';
|
|
};
|
|
}, [open]);
|
|
|
|
// Reset drag state on close
|
|
useEffect(() => {
|
|
if (!open) {
|
|
setDragY(0);
|
|
setIsDragging(false);
|
|
}
|
|
}, [open]);
|
|
|
|
const handleHandleTouchStart = (e) => {
|
|
startYRef.current = e.touches[0].clientY;
|
|
setIsDragging(true);
|
|
};
|
|
|
|
const handleHandleTouchMove = (e) => {
|
|
if (startYRef.current === null) return;
|
|
const delta = e.touches[0].clientY - startYRef.current;
|
|
if (delta < 0) return; // no drag up
|
|
setDragY(delta);
|
|
};
|
|
|
|
const handleHandleTouchEnd = () => {
|
|
setIsDragging(false);
|
|
if (dragY > 100) {
|
|
setDragY(0);
|
|
onClose?.();
|
|
} else {
|
|
setDragY(0);
|
|
}
|
|
startYRef.current = null;
|
|
};
|
|
|
|
const sheetTransform = open
|
|
? `translateY(${isDragging ? dragY : 0}px)`
|
|
: 'translateY(100%)';
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
className={`mobile-sheet__backdrop ${open ? 'is-open' : ''}`}
|
|
onClick={onClose}
|
|
aria-hidden="true"
|
|
/>
|
|
<div
|
|
ref={sheetRef}
|
|
className={`mobile-sheet snap-${snap} ${open ? 'is-open' : ''}`}
|
|
style={{
|
|
transform: sheetTransform,
|
|
transition: isDragging ? 'none' : undefined,
|
|
}}
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={title}
|
|
>
|
|
{/* Drag handle */}
|
|
<div
|
|
className="mobile-sheet__handle"
|
|
onTouchStart={handleHandleTouchStart}
|
|
onTouchMove={handleHandleTouchMove}
|
|
onTouchEnd={handleHandleTouchEnd}
|
|
aria-hidden="true"
|
|
>
|
|
<span className="mobile-sheet__handle-bar" />
|
|
</div>
|
|
|
|
{/* Header */}
|
|
<div className="mobile-sheet__header">
|
|
<span className="mobile-sheet__title">{title}</span>
|
|
<button
|
|
type="button"
|
|
className="mobile-sheet__close"
|
|
onClick={onClose}
|
|
aria-label="닫기"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
|
<path
|
|
d="M3 3l12 12M15 3L3 15"
|
|
stroke="currentColor"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body */}
|
|
<div className="mobile-sheet__body">
|
|
{children}
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|