framer-motionを利用したスワイプに基づいてスクロールするタブも追いかけるようにする
Next.js
framer-motion
これで横スワイプを実装したが、react-indiana-drag-scrollで実装したスクロールできるタブが、一緒に動いてくれない問題を修正した。
タブ変更ロジック内に以下を仕込み、react-indiana-drag-scrollで実装したタブ要素にrefを渡す
// タブの状態が更新された後にスクロールを実行 requestAnimationFrame(() => { tabsRef.current?.scrollToActiveTab(); });
framer-motionで実装したSwipe Containerに以下の様なpassiveの設定をeventListenerを使って設定する
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const touchStartHandler = (e: TouchEvent): void => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
touchStartYRef.current = e.targetTouches[0].clientY;
setIsScrolling(false);
};
const touchMoveHandler = (e: TouchEvent): void => {
if (!touchStart) return;
const currentX = e.targetTouches[0].clientX;
const currentY = e.targetTouches[0].clientY;
const diffX = Math.abs(touchStart - currentX);
const diffY = Math.abs((touchStartYRef.current ?? 0) - currentY);
// 横方向の移動が縦方向より大きい場合、スクロールフラグを立てない
if (!isScrolling && diffY < diffX) {
e.preventDefault();
}
// 縦方向のスクロールを検出
if (!isScrolling && diffY > SCROLL_TOLERANCE && diffY > diffX) {
setIsScrolling(true);
return;
}
// コードブロック内での横スクロールを許可
if (
e.target instanceof HTMLElement &&
(e.target.closest('.mockup-code') ?? e.target.closest('pre'))
) {
return;
}
setTouchEnd(currentX);
};
const touchEndHandler = (): void => {
if (!touchStart || !touchEnd || isScrolling) {
setTouchStart(null);
setTouchEnd(null);
return;
}
const distance = touchStart - touchEnd;
const isLeftSwipe = distance > SWIPE_MIN_DISTANCE;
const isRightSwipe = distance < -SWIPE_MIN_DISTANCE;
if (isLeftSwipe && currentIndex < totalItems - 1) {
onSwipe(currentIndex + 1);
} else if (isRightSwipe && currentIndex > 0) {
onSwipe(currentIndex - 1);
}
setTouchStart(null);
setTouchEnd(null);
};
// passive: falseを指定する理由
// 1. デフォルトではpassive: trueとなっており、preventDefault()が無視される
// 2. スワイプ時の画面スクロールを防ぐためにpreventDefault()が必要
// 3. そのため、addEventListener時にpassive: falseの指定が必須
container.addEventListener('touchstart', touchStartHandler, {
passive: true,
});
container.addEventListener('touchmove', touchMoveHandler, {
passive: false,
});
container.addEventListener('touchend', touchEndHandler, { passive: true });
return () => {
container.removeEventListener('touchstart', touchStartHandler);
container.removeEventListener('touchmove', touchMoveHandler);
container.removeEventListener('touchend', touchEndHandler);
};
}, [currentIndex, isScrolling, onSwipe, touchStart, touchEnd, totalItems]);