Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- designpatterns
- 티스토리챌린지
- golang
- React
- hook
- springboot
- backend
- frontend
- Gin
- satisfiles
- RTK
- Redux
- java
- css
- javascript
- Spring
- ReactHooks
- 오블완
- tanstackquery
- react-hook-form
- test
- 웹애플리케이션서버
- component
- JPA
- go
- typescript
- Chakra
- storybook
- JavaSpring
Archives
- Today
- Total
bkdragon's log
드래그 가능한 모달창 만들기 본문
브라우저 처럼 드래그로 이동이 가능한 모달창을 만들어보자.
기본 아이디어는 아래와 같다:
- 마우스로 모달창을 클릭한 순간 현재 마우스 위치와 모달의 좌측 상단 모서리와 차이를 저장하고 드래그 중임을 알려주는 flag 상태를 true로 만든다.
- flag 가 true가 되면, movemove 이벤트 리스너를 등록하고, 움직이는 위치로 위치 상태값을 업데이트 시킨다.
현재 마우스 위치와 모달의 좌측 상단 모서리와의 차이가 왜 필요한지는 예를 들면 쉽게 이해된다.
만약 모달의 현재 위치가 (100, 100) 이고, 마우스 클릭 위치가 (120, 130) 이라면
차이(dragOffset)는 (20, 30) 이 된다.
그리고 마우스를 (150, 160) 으로 이동 하면
새로운 모달 위치를 dragOffset 을 빼면 얻을 수 있다. (150-20, 160-30) = (130, 130)
interface ModalProps {
isOpen: boolean;
onClose: () => void;
children?: React.ReactNode;
}
interface Position {
x: number;
y: number;
}
const DraggableModal: React.FC<ModalProps> = ({ isOpen, onClose, children }) => {
const modalRef = useRef<HTMLDivElement>(null);
const [isDragging, setIsDragging] = useState(false);
const [position, setPosition] = useState<Position>({ x: 0, y: 0 });
const [dragOffset, setDragOffset] = useState<Position>({ x: 0, y: 0 });
const handleMouseDown = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
setDragOffset({
x: e.clientX - position.x,
y: e.clientY - position.y,
});
},
[position]
);
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (isDragging) {
setPosition({
x: e.clientX - dragOffset.x,
y: e.clientY - dragOffset.y,
});
}
},
[isDragging, dragOffset]
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [isDragging, handleMouseMove, handleMouseUp]);
// 초기 위치를 중앙으로 설정
useEffect(() => {
if (isOpen && modalRef.current) {
const x = window.innerWidth / 2 - modalRef.current.offsetWidth / 2;
const y = window.innerHeight / 2 - modalRef.current.offsetHeight / 2;
setPosition({ x, y });
}
}, [isOpen]);
if (!isOpen) return null;
return createPortal(
<div
ref={modalRef}
style={{
position: "fixed",
left: position.x,
top: position.y,
backgroundColor: "white",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)",
borderRadius: "4px",
zIndex: 1000,
}}
>
<div
onMouseDown={handleMouseDown}
style={{
padding: "1rem",
backgroundColor: "#f1f1f1",
cursor: isDragging ? "grabbing" : "grab",
borderBottom: "1px solid #ddd",
}}
>
드래그 가능한 헤더
<button
onClick={onClose}
style={{ float: "right", cursor: "pointer" }}
>
✕
</button>
</div>
<div style={{ padding: "1rem" }}>{children}</div>
</div>,
document.body
);
};
export default DraggableModal;
보통 모달은 뒤에 보이지 않는 배경을 둬서 인터렉션을 막는데 이 모달창은 그렇지 않다. 여러개를 열 수도 있다. 추후에 최소화 기능을 추가해봐도 좋을 것 같다.
'React' 카테고리의 다른 글
Storybook Interactions (0) | 2024.11.16 |
---|---|
children 을 함수해서 동적으로 스타일을 지정하기 (0) | 2024.11.14 |
Storybook Decorator 로 전역 상태 사용하기 (0) | 2024.11.07 |
합성 컴포넌트 패턴과 React Children API 를 활용한 컴포넌트 설계 (0) | 2024.10.31 |
Storybook 으로 협업하기 (1) | 2024.10.30 |