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
- frontend
- Spring
- 오블완
- RTK
- typescript
- satisfiles
- component
- css
- backend
- test
- ReactHooks
- javascript
- storybook
- 티스토리챌린지
- Redux
- springboot
- React
- golang
- Chakra
- java
- JPA
- Gin
- 웹애플리케이션서버
- designpatterns
- JavaSpring
- react-hook-form
- go
- hook
- tanstackquery
Archives
- Today
- Total
bkdragon's log
BottomSheet 본문
화면 하단에서 올라오는 BottomSheet 를 React로 구현해보려고 한다. 데스크탑 해상도에서 Modal 창을 사용하는 화면을 대체할 수 있을 것 같다.
주 기능은 아래와 같다.
- 특정 동작(버튼 클릭 등) 이후 화면 아래에서 올라온다.
- 드래그를 통해 화면 아래로 내릴 수 있다.
style은 tailwindcss를 사용했다.
주 기능을 토대로 Props의 interface 부터 만들어보자.
interface IProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
보여져야 하는 상황인지 알 수 있는 상태와 다시 안보이게 할 수 있는 함수를 받는다. UI와 기본 기능만 재활용할 것이기 때문에 내부 요소들은 children 으로 전달 받게 했다.
보이는 화면 밖에 있다가 나타나게 하는것은 position과 bottom 속성을 이용할 것이다.
fixed 에 isOpen 일 때 bottom 0, 아닐 때 (-100%, -높이)
jsx 부분 이다.
<>
<div
className={clsx(
"transition-all duration-300 left-0 right-0 top-0 bottom-0 p-4 bg-black/40 border-t-2 border-gray-200 z-25",
isOpen ? "fixed" : "hidden"
)}
/>
<div
ref={ref}
style={{
transform: `translateY(${-diffY}px)`,
height: `${defaultHeight}px`,
bottom: isOpen ? "0" : `-${height || defaultHeight}px`,
}}
className={`fixed transition-all duration-300 left-0 right-0 p-4 bg-white border-t-2 border-gray-200 rounded-tl-xl rounded-tr-xl z-50 flex flex-col`}
>
<button onTouchStart={handleTouchStart} className="flex items-center justify-center w-full">
<RiDraggable className="text-2xl rotate-90" />
</button>
<div className="flex-1 overflow-y-auto basis-0">{children}</div>
</div>
</>
첫번째 Div는 뒷 배경을 어둡게하는 역할이다. (clsx 는 조건부 스타일을 쉽게 적용하는 라이브러리, tailwind 와 조합이 좋다고 생각한다.)
높이는 화면 높이의 70%를 기본으로 가지게 했다. defaultHeight는 resize 이벤트 핸들러를 등록해서 계산한다.
const [defaultHeight, setDefaultHeight] = useState(0);
useEffect(() => {
const calculateHeight = () => {
setDefaultHeight(window?.innerHeight * 0.7 || 400);
};
calculateHeight();
window.addEventListener("resize", calculateHeight);
return () => {
window.removeEventListener("resize", calculateHeight);
};
}, []);
2번 기능은 TouchStart 이벤트를 사용해서 구현할 수 있다. TouchStart 했을 때 TouchMove 이벤트 핸들러를 등록해서 첫 터치 지점과 움직이는 지점으로 차이로 이동거리를 계산하고 이동 거리가 BottomSheet의 70퍼센트를 넘어가면 close 되게 한다.
const handleTouchStart = (e: React.TouchEvent<HTMLButtonElement>) => {
if (!ref.current) return;
const bottomBarHeight = ref.current.offsetHeight;
let startY = e.touches[0].clientY; // 여러 손가락으로 터치를 할 수 있어서 배열이다.
let diffY = 0;
const handleTouchMove = (e: TouchEvent) => {
const currentY = e.touches[0].clientY;
diffY = startY - currentY;
// 아래로만 이동하는데 70퍼 이상 이동하면 닫고, 아니면 다시 원상복구
if (diffY < 0) {
setDiffY(diffY);
if (Math.abs(diffY) > bottomBarHeight * 0.7) {
onClose();
removeEventListeners();
setDiffY(0);
}
}
};
const handleTouchEnd = () => {
setDiffY(0);
removeEventListeners();
};
const removeEventListeners = () => {
document.removeEventListener("touchmove", handleTouchMove as EventListener);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove as EventListener);
document.addEventListener("touchend", handleTouchEnd);
};
완성 화면!
'React' 카테고리의 다른 글
Storybook 으로 협업하기 (1) | 2024.10.30 |
---|---|
useReducer Action 객체 타입 (1) | 2024.10.08 |
렌더링과 커밋 (2) | 2024.09.21 |
[React Query] 쿼리 키 관리하기 (0) | 2024.07.16 |
드래그를 활용한 그리드 아이템 크기 조절 (0) | 2024.06.29 |