From 5f6ebafd02e75628cb49b94699f569da82e1c13c Mon Sep 17 00:00:00 2001 From: nakaterm <104970808+nakaterm@users.noreply.github.com> Date: Sun, 23 Nov 2025 06:53:58 +0900 Subject: [PATCH] =?UTF-8?q?=E3=83=89=E3=83=A9=E3=83=83=E3=82=B0=E9=81=B8?= =?UTF-8?q?=E6=8A=9E=E6=99=82=E3=81=AB=E3=82=B9=E3=82=AF=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=AB=E3=82=92=E7=84=A1=E5=8A=B9=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/Calendar.tsx | 12 ++- client/src/hooks/useCalendarScrollBlock.ts | 90 ++++++++++++++++++++++ 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 client/src/hooks/useCalendarScrollBlock.ts diff --git a/client/src/components/Calendar.tsx b/client/src/components/Calendar.tsx index 7e40117..d23b004 100644 --- a/client/src/components/Calendar.tsx +++ b/client/src/components/Calendar.tsx @@ -15,6 +15,7 @@ import type { import type React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Tooltip } from "react-tooltip"; +import useCalendarScrollBlock from "../hooks/useCalendarScrollBlock"; import { EditingMatrix, ViewingMatrix } from "../lib/CalendarMatrix"; import type { EditingSlot } from "../pages/eventId/Submission"; @@ -69,6 +70,13 @@ type Props = { const OPACITY = 0.2; const PRIMARY_RGB = [15, 130, 177]; +/** + * 長押しでドラッグ開始とみなすまでの遅延時間 (ms) + * - FullCalendar で選択イベントが点灯し始めるまでの時間。 + * - また、これ以上の時間で押し続けるとドラッグ操作として扱われ、スクロールを無効化する + */ +const LONG_PRESS_DELAY = 150; + const EDITING_EVENT = "ih-editing-event"; const VIEWING_EVENT = "ih-viewing-event"; const SELECT_EVENT = "ih-select-event"; @@ -284,6 +292,8 @@ export const Calendar = ({ }; }, []); + useCalendarScrollBlock(LONG_PRESS_DELAY); + const pageCount = Math.ceil(countDays / 7); const headerToolbar = useMemo( @@ -456,7 +466,7 @@ export const Calendar = ({ ref={calendarRef} plugins={[timeGridPlugin, interactionPlugin]} height={"100%"} - longPressDelay={200} + longPressDelay={LONG_PRESS_DELAY} slotDuration={"00:15:00"} allDaySlot={false} initialDate={startDate} diff --git a/client/src/hooks/useCalendarScrollBlock.ts b/client/src/hooks/useCalendarScrollBlock.ts new file mode 100644 index 0000000..f77ee34 --- /dev/null +++ b/client/src/hooks/useCalendarScrollBlock.ts @@ -0,0 +1,90 @@ +import { useEffect } from "react"; + +/** + * 長押し時にカレンダーのスクロールをブロックする + * @param LONG_PRESS_DELAY 長押しとみなすまでの時間 (ms) + * @param MOVE_TOLERANCE これ以上動いたらスクロールとみなす (px) + */ +export default function useCalendarScrollBlock(LONG_PRESS_DELAY = 150, MOVE_TOLERANCE = 5) { + useEffect(() => { + const wrapper = document.getElementById("ih-cal-wrapper"); + if (!wrapper) return; + const scroller = wrapper.querySelector(".fc-scroller.fc-scroller-liquid-absolute") as HTMLElement | null; + if (!scroller) return; + + let pressTimer: number | null = null; + let isDragMode = false; + let startX = 0; + let startY = 0; + + const clearPressTimer = () => { + if (pressTimer !== null) { + window.clearTimeout(pressTimer); + pressTimer = null; + } + }; + + const resetDragMode = () => { + clearPressTimer(); + if (isDragMode) { + isDragMode = false; + scroller.style.overflowY = ""; + scroller.style.touchAction = ""; + } + }; + + const onTouchStart = (e: TouchEvent) => { + if (e.touches.length !== 1) return; + const t = e.touches[0]; + startX = t.clientX; + startY = t.clientY; + isDragMode = false; + clearPressTimer(); + + // 一定時間動かなければ、スクロールを無効化 + pressTimer = window.setTimeout(() => { + isDragMode = true; + scroller.style.overflowY = "hidden"; + scroller.style.touchAction = "none"; + }, LONG_PRESS_DELAY); + }; + + const onTouchMove = (e: TouchEvent) => { + if (e.touches.length !== 1) return; + const t = e.touches[0]; + const dx = Math.abs(t.clientX - startX); + const dy = Math.abs(t.clientY - startY); + + if (!isDragMode) { + // ロングプレス判定中に大きく動いたらスクロールとみなしてキャンセル + if (dx > MOVE_TOLERANCE || dy > MOVE_TOLERANCE) { + clearPressTimer(); + } + return; + } + + e.preventDefault(); // 今回は overflowY: hidden で十分だが一応 + }; + + const onTouchEnd = () => { + resetDragMode(); + }; + + const onTouchCancel = () => { + resetDragMode(); + }; + + // touchmove で preventDefault するには passive: false が必要 + scroller.addEventListener("touchstart", onTouchStart, { passive: true }); + scroller.addEventListener("touchmove", onTouchMove, { passive: false }); + scroller.addEventListener("touchend", onTouchEnd, { passive: true }); + scroller.addEventListener("touchcancel", onTouchCancel, { passive: true }); + + return () => { + scroller.removeEventListener("touchstart", onTouchStart); + scroller.removeEventListener("touchmove", onTouchMove); + scroller.removeEventListener("touchend", onTouchEnd); + scroller.removeEventListener("touchcancel", onTouchCancel); + }; + }, [LONG_PRESS_DELAY, MOVE_TOLERANCE]); +}