From 4c1ef5019e0067684ff6963ae023c1caee921bd8 Mon Sep 17 00:00:00 2001 From: mARTin Date: Sun, 29 Mar 2026 16:14:53 +0200 Subject: [PATCH] fix: iOS scroll blocked by enforcement interval + missing touch-action on slots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three root causes fixed: 1. Scroll enforcement interval (every 50ms for 2s) was the main blocker: The setInterval forced gridRef.scrollTop on every frame, making iOS think the page was always scrolling programmatically. User touches during those 2 seconds were overridden. The header appeared 'stuck' because the view was frozen. Replaced with a single setTimeout scroll (300ms on first load, 50ms on subsequent). Added history.scrollRestoration='manual' so Safari doesn't fight us with scroll position restoration. 2. touch-action: pan-y was only on .time-grid-wrapper — NOT inherited by CSS. Slot child elements had touch-action: auto (default), so iOS still waited for JS before committing scroll on day column touches. Added touch-action: pan-y directly to .time-slots-container (the actual touched element area). Time column labels have no JS handlers so iOS scrolled them freely — this explains why time column worked but day columns didn't. 3. align-items: start on .weekly-days-grid was wrong (added in last commit). Removed — CSS grid default stretch is correct. v1.77.4 --- package.json | 2 +- src/app/globals.css | 5 +++-- src/components/WeeklyView.tsx | 38 +++++++++++++++-------------------- 3 files changed, 20 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index fd1e623..ea1a727 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.77.3", + "version": "1.77.4", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view", "main": "index.js", "scripts": { diff --git a/src/app/globals.css b/src/app/globals.css index d531c86..73ee5ad 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3167,8 +3167,6 @@ h3 { .time-grid-wrapper .weekly-days-grid { flex: 1; overflow: visible; - /* Align grid items to the top so sticky headers work correctly */ - align-items: start; } .time-grid-wrapper .weekly-day-column { @@ -3190,6 +3188,9 @@ h3 { .time-grid-wrapper .time-slots-container { overflow: visible; flex: 1; + /* touch-action is NOT inherited — must be set on the actual touched element. + pan-y tells iOS: vertical swipe = scroll immediately, don't wait for JS handlers */ + touch-action: pan-y; } /* All-Day Events Section */ diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 8b90d7e..e75d4d4 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -3110,34 +3110,28 @@ export default function WeeklyView() { } }, [currentWeekStart, session, fetchCalendarEvents]); + // Disable browser scroll restoration so Safari doesn't fight our initial scroll position + useEffect(() => { + if (typeof window !== 'undefined' && window.history.scrollRestoration) { + window.history.scrollRestoration = 'manual'; + } + }, []); + // Scroll to preferred start hour (initial load + when user changes startHour) useEffect(() => { if (!isLoading) { - const performScroll = () => { - const slotsPerHour = 60 / cellDuration; - const slotHeight = getSlotHeight(cellDuration); - const scrollOffset = workingHoursStart * slotsPerHour * slotHeight; + const slotsPerHour = 60 / cellDuration; + const slotHeight = getSlotHeight(cellDuration); + const scrollOffset = workingHoursStart * slotsPerHour * slotHeight; - // Single scroll container: gridRef points to time-grid-wrapper + // Single scroll — no interval, no enforcement loop. + // We set scrollRestoration='manual' so the browser won't override this. + const delay = isInitialScrollDone.current ? 50 : 300; + const timer = setTimeout(() => { if (gridRef.current) gridRef.current.scrollTop = scrollOffset; - intendedScrollTop.current = scrollOffset; - - // On initial load, re-enforce for 2s to fight browser auto-scroll restoration - if (!isInitialScrollDone.current) { - const interval = setInterval(() => { - if (gridRef.current) gridRef.current.scrollTop = scrollOffset; - }, 50); - - setTimeout(() => { - clearInterval(interval); - isInitialScrollDone.current = true; - }, 2000); - } - }; - - // Delay slightly to ensure layout is stable - const timer = setTimeout(performScroll, isInitialScrollDone.current ? 50 : 500); + isInitialScrollDone.current = true; + }, delay); return () => clearTimeout(timer); } }, [isLoading, workingHoursStart, cellDuration]);