From bf617333174dd489b395665c718a2239ce43c4ab Mon Sep 17 00:00:00 2001 From: mARTin Date: Sun, 29 Mar 2026 11:32:47 +0200 Subject: [PATCH] fix: eliminate iOS scroll bounce by using single scroll container for time grid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The time grid previously had two separate overflow-y:auto scroll containers (the time-column labels + the day columns grid), synced via JS scrollTop assignment. On iOS Safari, this caused severe desync and bounce effects: momentum scroll continued after touch-end, and the programmatic scrollTop correction fought against native momentum, making the grid stutter/bounce. Changes: - time-grid-wrapper is now the SINGLE overflow-y:auto scroll container (ref=gridRef, onScroll handler moved here from weekly-days-grid) - time-column and weekly-days-grid have overflow:visible — they scroll with their parent wrapper without fighting each other - Removed handleTimeColumnScroll, timeColumnRef, timeGridWrapperRef (all sync logic now redundant) - jumpToHour and initial scroll position now set only gridRef.scrollTop - CSS: weekly-days-grid overflow changed to visible - CSS: re-enabled position:sticky on .weekly-day-header for mobile — sticky now works correctly since scroll container is an overflow-y parent - CSS: hid time-column-header on mobile (saves vertical space) v1.76.2 --- package.json | 2 +- src/app/globals.css | 21 ++++--------- src/components/WeeklyView.tsx | 59 ++++++++--------------------------- 3 files changed, 20 insertions(+), 62 deletions(-) diff --git a/package.json b/package.json index f490201..a350147 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "my-weekly-todo-list", - "version": "1.76.1", + "version": "1.76.2", "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 aa070de..52f9430 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2703,9 +2703,8 @@ h3 { flex: none !important; } - /* Ensure grid respects viewDays columns on mobile landscape */ + /* time-grid-wrapper is the single scroll container — keep overflow-y from inline style */ .time-grid-wrapper { - overflow: visible; min-width: 0; } .time-grid-wrapper .weekly-days-grid { @@ -3161,11 +3160,10 @@ h3 { } -/* Make day columns scrollable when using time grid */ +/* Day columns: no overflow — scroll is handled by time-grid-wrapper (single scroll container) */ .time-grid-wrapper .weekly-days-grid { flex: 1; - overflow-y: auto; /* Unified scroll container */ - overflow-x: hidden; + overflow: visible; } .time-grid-wrapper .weekly-day-column { @@ -4974,17 +4972,10 @@ h3 { } @media (max-width: 768px) { - /* Day headers are not sticky on mobile — CSS sticky doesn't work in a CSS grid scroll container. - The mobile-sticky-day-bar overlay (fixed position) handles this instead. */ - .time-grid-on .weekly-day-header { - position: relative !important; - top: auto !important; - } - /* On mobile, time-column-header should NOT be sticky — it wastes vertical space */ + /* time-grid-wrapper is now the single scroll container, so position:sticky works. + time-column-header is hidden on mobile to save vertical space. */ .time-grid-on .time-column-header { - position: relative !important; - top: auto !important; - z-index: auto !important; + display: none; } } diff --git a/src/components/WeeklyView.tsx b/src/components/WeeklyView.tsx index 6f9b241..acd1bb1 100644 --- a/src/components/WeeklyView.tsx +++ b/src/components/WeeklyView.tsx @@ -2492,8 +2492,7 @@ export default function WeeklyView() { // Translation helper const t = translations[language] || translations["en"]; - // Refs for scroll synchronization - const timeColumnRef = useRef(null); + // Refs for scroll const dayColumnsRef = useRef([]); const isScrollSyncing = useRef(false); const isInitialScrollDone = useRef(false); @@ -2538,30 +2537,11 @@ export default function WeeklyView() { const somedaySectionRef = useRef(null); // Unified scroll sync handlers - const handleTimeColumnScroll = (e: React.UIEvent) => { - const scrollTop = e.currentTarget.scrollTop; - if (isScrollSyncing.current) return; - isScrollSyncing.current = true; - - if (gridRef.current) { - gridRef.current.scrollTop = scrollTop; - } - setTimeout(() => { - isScrollSyncing.current = false; - }, 50); - }; + // handleTimeColumnScroll no longer needed — single scroll container via time-grid-wrapper - const handleGridScroll = (e: React.UIEvent) => { - const scrollTop = e.currentTarget.scrollTop; - if (isScrollSyncing.current) return; - isScrollSyncing.current = true; - - if (timeColumnRef.current) { - timeColumnRef.current.scrollTop = scrollTop; - } - setTimeout(() => { - isScrollSyncing.current = false; - }, 50); + const handleGridScroll = (_e: React.UIEvent) => { + // Single scroll container — no sync needed + // Mobile sticky day is handled by the native scroll listener in the useEffect }; const jumpToHour = (hour: number) => { @@ -2577,9 +2557,7 @@ export default function WeeklyView() { intendedScrollTop.current = scrollOffset; const perform = () => { - if (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset; if (gridRef.current) gridRef.current.scrollTop = scrollOffset; - if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset; }; // Repeated enforcement @@ -3140,20 +3118,15 @@ export default function WeeklyView() { const slotHeight = getSlotHeight(cellDuration); const scrollOffset = workingHoursStart * slotsPerHour * slotHeight; - // Scroll the time-grid-wrapper (the scrollable viewport) - if (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset; - // Also scroll inner refs as fallback + // Single scroll container: gridRef points to time-grid-wrapper if (gridRef.current) gridRef.current.scrollTop = scrollOffset; - if (timeColumnRef.current) timeColumnRef.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 (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset; if (gridRef.current) gridRef.current.scrollTop = scrollOffset; - if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset; }, 50); setTimeout(() => { @@ -4523,7 +4496,6 @@ export default function WeeklyView() { // Navigation handlers with CSS class-based slide animation (works in all browsers) const gridRef = useRef(null); - const timeGridWrapperRef = useRef(null); const allSlideClasses = ["slide-animate-next", "slide-animate-prev", "slide-animate-week-next", "slide-animate-week-prev"]; const navigate = ( newDate: Date, @@ -7641,19 +7613,18 @@ export default function WeeklyView() { {/* Main Grid with Time Column */} {viewStyle !== "kanban" &&
-
} onScroll={handleGridScroll} style={showTimeGrid ? { + height: '100%', + overflowY: 'auto', + overflowX: 'hidden', + WebkitOverflowScrolling: 'touch' as any, } : undefined}> {/* Time Column */} {showTimeGrid && ( -
1 ? "week" : "day"} - onScroll={handleGridScroll} style={showTimeGrid ? { height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`, - maxHeight: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`, flex: 1, alignSelf: "flex-start", - overflowY: 'auto' } : { flex: 1, }}