Skip to content

Commit 8b03763

Browse files
authored
ドラッグ選択時にスクロールを無効化 (#55)
1 parent 6cebef1 commit 8b03763

File tree

2 files changed

+101
-1
lines changed

2 files changed

+101
-1
lines changed

client/src/components/Calendar.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
import type React from "react";
1616
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1717
import { Tooltip } from "react-tooltip";
18+
import useCalendarScrollBlock from "../hooks/useCalendarScrollBlock";
1819
import { EditingMatrix, ViewingMatrix } from "../lib/CalendarMatrix";
1920
import type { EditingSlot } from "../pages/eventId/Submission";
2021

@@ -69,6 +70,13 @@ type Props = {
6970
const OPACITY = 0.2;
7071
const PRIMARY_RGB = [15, 130, 177];
7172

73+
/**
74+
* 長押しでドラッグ開始とみなすまでの遅延時間 (ms)
75+
* - FullCalendar で選択イベントが点灯し始めるまでの時間。
76+
* - また、これ以上の時間で押し続けるとドラッグ操作として扱われ、スクロールを無効化する
77+
*/
78+
const LONG_PRESS_DELAY = 150;
79+
7280
const EDITING_EVENT = "ih-editing-event";
7381
const VIEWING_EVENT = "ih-viewing-event";
7482
const SELECT_EVENT = "ih-select-event";
@@ -284,6 +292,8 @@ export const Calendar = ({
284292
};
285293
}, []);
286294

295+
useCalendarScrollBlock(LONG_PRESS_DELAY);
296+
287297
const pageCount = Math.ceil(countDays / 7);
288298

289299
const headerToolbar = useMemo(
@@ -456,7 +466,7 @@ export const Calendar = ({
456466
ref={calendarRef}
457467
plugins={[timeGridPlugin, interactionPlugin]}
458468
height={"100%"}
459-
longPressDelay={200}
469+
longPressDelay={LONG_PRESS_DELAY}
460470
slotDuration={"00:15:00"}
461471
allDaySlot={false}
462472
initialDate={startDate}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useEffect } from "react";
2+
3+
/**
4+
* 長押し時にカレンダーのスクロールをブロックする
5+
* @param LONG_PRESS_DELAY 長押しとみなすまでの時間 (ms)
6+
* @param MOVE_TOLERANCE これ以上動いたらスクロールとみなす (px)
7+
*/
8+
export default function useCalendarScrollBlock(LONG_PRESS_DELAY = 150, MOVE_TOLERANCE = 5) {
9+
useEffect(() => {
10+
const wrapper = document.getElementById("ih-cal-wrapper");
11+
if (!wrapper) return;
12+
const scroller = wrapper.querySelector(".fc-scroller.fc-scroller-liquid-absolute") as HTMLElement | null;
13+
if (!scroller) return;
14+
15+
let pressTimer: number | null = null;
16+
let isDragMode = false;
17+
let startX = 0;
18+
let startY = 0;
19+
20+
const clearPressTimer = () => {
21+
if (pressTimer !== null) {
22+
window.clearTimeout(pressTimer);
23+
pressTimer = null;
24+
}
25+
};
26+
27+
const resetDragMode = () => {
28+
clearPressTimer();
29+
if (isDragMode) {
30+
isDragMode = false;
31+
scroller.style.overflowY = "";
32+
scroller.style.touchAction = "";
33+
}
34+
};
35+
36+
const onTouchStart = (e: TouchEvent) => {
37+
if (e.touches.length !== 1) return;
38+
const t = e.touches[0];
39+
startX = t.clientX;
40+
startY = t.clientY;
41+
isDragMode = false;
42+
clearPressTimer();
43+
44+
// 一定時間動かなければ、スクロールを無効化
45+
pressTimer = window.setTimeout(() => {
46+
isDragMode = true;
47+
scroller.style.overflowY = "hidden";
48+
scroller.style.touchAction = "none";
49+
}, LONG_PRESS_DELAY);
50+
};
51+
52+
const onTouchMove = (e: TouchEvent) => {
53+
if (e.touches.length !== 1) return;
54+
const t = e.touches[0];
55+
const dx = Math.abs(t.clientX - startX);
56+
const dy = Math.abs(t.clientY - startY);
57+
58+
if (!isDragMode) {
59+
// ロングプレス判定中に大きく動いたらスクロールとみなしてキャンセル
60+
if (dx > MOVE_TOLERANCE || dy > MOVE_TOLERANCE) {
61+
clearPressTimer();
62+
}
63+
return;
64+
}
65+
66+
e.preventDefault(); // 今回は overflowY: hidden で十分だが一応
67+
};
68+
69+
const onTouchEnd = () => {
70+
resetDragMode();
71+
};
72+
73+
const onTouchCancel = () => {
74+
resetDragMode();
75+
};
76+
77+
// touchmove で preventDefault するには passive: false が必要
78+
scroller.addEventListener("touchstart", onTouchStart, { passive: true });
79+
scroller.addEventListener("touchmove", onTouchMove, { passive: false });
80+
scroller.addEventListener("touchend", onTouchEnd, { passive: true });
81+
scroller.addEventListener("touchcancel", onTouchCancel, { passive: true });
82+
83+
return () => {
84+
scroller.removeEventListener("touchstart", onTouchStart);
85+
scroller.removeEventListener("touchmove", onTouchMove);
86+
scroller.removeEventListener("touchend", onTouchEnd);
87+
scroller.removeEventListener("touchcancel", onTouchCancel);
88+
};
89+
}, [LONG_PRESS_DELAY, MOVE_TOLERANCE]);
90+
}

0 commit comments

Comments
 (0)