fix: iOS scroll blocked by enforcement interval + missing touch-action on slots

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
This commit is contained in:
mARTin 2026-03-29 16:14:53 +02:00
parent aef7c291b4
commit 4c1ef5019e
3 changed files with 20 additions and 25 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "my-weekly-todo-list", "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", "description": "A web-based weekly task management application that organizes to-dos and calendar events in a single, intuitive weekly view",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {

View File

@ -3167,8 +3167,6 @@ h3 {
.time-grid-wrapper .weekly-days-grid { .time-grid-wrapper .weekly-days-grid {
flex: 1; flex: 1;
overflow: visible; overflow: visible;
/* Align grid items to the top so sticky headers work correctly */
align-items: start;
} }
.time-grid-wrapper .weekly-day-column { .time-grid-wrapper .weekly-day-column {
@ -3190,6 +3188,9 @@ h3 {
.time-grid-wrapper .time-slots-container { .time-grid-wrapper .time-slots-container {
overflow: visible; overflow: visible;
flex: 1; 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 */ /* All-Day Events Section */

View File

@ -3110,34 +3110,28 @@ export default function WeeklyView() {
} }
}, [currentWeekStart, session, fetchCalendarEvents]); }, [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) // Scroll to preferred start hour (initial load + when user changes startHour)
useEffect(() => { useEffect(() => {
if (!isLoading) { if (!isLoading) {
const performScroll = () => { const slotsPerHour = 60 / cellDuration;
const slotsPerHour = 60 / cellDuration; const slotHeight = getSlotHeight(cellDuration);
const slotHeight = getSlotHeight(cellDuration); const scrollOffset = workingHoursStart * slotsPerHour * slotHeight;
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; if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
intendedScrollTop.current = scrollOffset; intendedScrollTop.current = scrollOffset;
isInitialScrollDone.current = true;
// On initial load, re-enforce for 2s to fight browser auto-scroll restoration }, delay);
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);
return () => clearTimeout(timer); return () => clearTimeout(timer);
} }
}, [isLoading, workingHoursStart, cellDuration]); }, [isLoading, workingHoursStart, cellDuration]);