Draggable Tab 개발

박종대·2022년 10월 16일
0

Convi

목록 보기
6/9

Draggable Tab

크롬의 탭을 드래그해서 이동 시켜 보면 header의 위치가 자유롭게 이동되는 것을 확인할 수 있습니다. 이러한 Drag 기능을 Tab에 넣으려고 합니다. 쉽지는 않아 보입니다. 단계별로 차근차근 개발 해보겠습니다.

Tab Elements의 Ref값을 저장하자

Tab을 이동 시키는 것은 런타임에 사용자가 동작 시키는 것이기 때문에 DOM에 직접 접근해서 위치를 파악하고 이동시켜야 하는 위치라면 이동시키는 방식으로 접근해야 합니다.

접근해야 할 DOM은 tab header element'들' 입니다. 여러 개의 tab header에 대한 ref 값을 저장할 것이기 때문에 배열 형태가 되어야 겠습니다.

const refs = useRef<any>([]); // useRef

// ref 속성 설정 방법
ref={el => {
		refs.current[tabIndex] = el;
	}
}

다음과 같이 설정하면 refs.current 배열에는 header element 순서대로 ref 값이 저장되어 있게 됩니다.

draggable, onDrag, onDragEnd

  • draggable : 요소가 drag 가능하도록 변경해줍니다. 이 속성이 true이면 컴포넌트가 드래그 하는 곳으로 반투명 상태에서 따라오는 것처럼 보이게 됩니다.
  • onDrag : 드래그 중일 때 발생하는 이벤트
  • onDragEnd : 드래그가 종료 될 때 발생하는 이벤트
  1. draggable을 true로 설정해 줌으로써 tab header를 drag 가능한 상태로 만듭니다.

  2. onDrag 이벤트 중에는 tab header가 drag된 위치 정보를 통해 header 요소 위치를 변경해야 하는지에 대해 판단합니다.

  3. onDragEnd 이벤트 발생 시에는 swap되어 재정렬된 tab element 결과를 실제 tab elements에 반영합니다.

onDrag

  • Drag 시켜 이동한 위치의 X 좌표를 알아냅니다.
  • 해당 X 좌표가 다른 tab header의 사각형 안쪽으로 들어오면 위치가 변경되어야 한다고 판단합니다.

실제 구현은 다음과 같습니다.

const handleDrag = (index: number, e: React.DragEvent<HTMLSpanElement>) => {
		const delta = e.pageX || e.clientX;

		positions.forEach(pos => {
			const prevMoved = pos.moved || 0;
			const swap = index !== pos.index && pos.rec.left + prevMoved < delta && delta < pos.rec.right + prevMoved;
			if (swap) {
				const idx1 = index;
				const idx2 = pos.index;

				const minus = idx1 > idx2 ? 1 : -1;
				const movePx = minus * (pos.rec.right - pos.rec.left) - prevMoved;

				refs.current[pos.index].style.transform = `translate(${movePx}px, 0px)`;
				positions[idx2].moved = movePx;
			}
		});
	};

delta : 로직 1번에 해당되는 값입니다. 탭의 맨 왼쪽을 0으로 해서 떨어진 X 좌표를 의미합니다. Tab header 사각형의 오른쪽 끝이 기준입니다.
이 때 pageX || clientX인 이유는 나중에 scroll 상황을 대비해서 입니다. pageX는 scroll을 포함한 X 좌표를 알려주고 clientX는 scroll을 포함하지 않은 X 좌표를 알려줍니다.

positions : tab header elements의 위치 값이 저장되어 있습니다. 해당 변수의 타입은 ElementPosition[] 타입이고 ElementPosition 타입은 다음과 같습니다.

interface ElementPosition {
	index: number;
	rec: DOMRect;
	moved?: number;
}

index는 몇 번째 index의 tab header element이냐에 대한 정보이고 rec는 tab header element의 사각형 DOM에 대한 정보이고 moved는 이전에 이동한 거리 값입니다.
moved를 저장하는 이유는 Drag 중에 여러 탭의 위치가 변경될 수 있기 때문입니다.

prevMoved: Drag중 이동되었던 거리를 의미합니다.

swap: swap을 해야 할 지에 대한 상태를 의미합니다. 2번 로직에 대한 구현입니다.

이후 swap을 해야 한다고 판단되면 translate를 통해 탭을 이동시켜 줍니다.

onDragEnd

이제 이동된 Tab들의 상태를 실제 tab elements state에 반영 시켜야 합니다.
반영 시키기 위한 메소드가 onTabPositionChange이고 props로 전달받게 됩니다.

const handleDragEnd = (index: number, e: React.DragEvent<HTMLSpanElement>) => {
		const delta = e.pageX || e.clientX;
		let swapedTabs = null;

		positions.forEach(pos => {
			const swap = index !== pos.index && pos.rec.left < delta && delta < pos.rec.right;
			if (swap) swapedTabs = swapArrayElement(props.children, index, pos.index);
			refs.current[pos.index].style.transform = `translate(0px, 0px)`;
		});

		const newTabs = swapedTabs || props.children;
		if (swapedTabs) {
			props.onSelected(index);
			props.onTabPositionChange(newTabs);
		}
};

handleDrag 핸들러에서와 비슷하게 swap해야 하는지 판단하고 swap 해야 한다면 스왑된 새로운 배열을 리턴해 줍니다.

이렇게 재정렬된 new Tabs를 onTabPositionChange의 인자로 넘겨주어 실제 부모 tab elements에 반영 시키게 됩니다.

몰랐던 부분들이 많아서 시간이 많이 소요된 작업이었습니다. 기억해야 할 부분들을 요약해보면 다음과 같습니다.

  1. useRef에 배열 형태 저장하기
  2. drag event
  3. 위치 정보 가져오기, getBoundingClientRect()
  4. 위치 이동하기
refs.current[pos.index].style.transform = `translate(${movePx}px, 0px)`;
profile
Frontend Developer

0개의 댓글