fix: eliminate iOS scroll bounce by using single scroll container for time grid
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
This commit is contained in:
parent
ba17025bb9
commit
bf61733317
@ -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": {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2492,8 +2492,7 @@ export default function WeeklyView() {
|
||||
// Translation helper
|
||||
const t = translations[language] || translations["en"];
|
||||
|
||||
// Refs for scroll synchronization
|
||||
const timeColumnRef = useRef<HTMLDivElement>(null);
|
||||
// Refs for scroll
|
||||
const dayColumnsRef = useRef<HTMLDivElement[]>([]);
|
||||
const isScrollSyncing = useRef(false);
|
||||
const isInitialScrollDone = useRef(false);
|
||||
@ -2538,30 +2537,11 @@ export default function WeeklyView() {
|
||||
const somedaySectionRef = useRef<HTMLElement | null>(null);
|
||||
|
||||
// Unified scroll sync handlers
|
||||
const handleTimeColumnScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
||||
const scrollTop = e.currentTarget.scrollTop;
|
||||
if (isScrollSyncing.current) return;
|
||||
isScrollSyncing.current = true;
|
||||
// handleTimeColumnScroll no longer needed — single scroll container via time-grid-wrapper
|
||||
|
||||
if (gridRef.current) {
|
||||
gridRef.current.scrollTop = scrollTop;
|
||||
}
|
||||
setTimeout(() => {
|
||||
isScrollSyncing.current = false;
|
||||
}, 50);
|
||||
};
|
||||
|
||||
const handleGridScroll = (e: React.UIEvent<HTMLElement>) => {
|
||||
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<HTMLElement>) => {
|
||||
// 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<HTMLElement>(null);
|
||||
const timeGridWrapperRef = useRef<HTMLDivElement>(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" && <div style={{ position: "relative", flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||
<div className="time-grid-wrapper" ref={timeGridWrapperRef} style={showTimeGrid ? {
|
||||
maxHeight: '100%',
|
||||
<div className="time-grid-wrapper" ref={gridRef as React.Ref<HTMLDivElement>} onScroll={handleGridScroll} style={showTimeGrid ? {
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'hidden',
|
||||
WebkitOverflowScrolling: 'touch' as any,
|
||||
} : undefined}>
|
||||
{/* Time Column */}
|
||||
{showTimeGrid && (
|
||||
<div
|
||||
className="time-column"
|
||||
ref={timeColumnRef}
|
||||
onScroll={handleTimeColumnScroll}
|
||||
style={{
|
||||
height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + measuredHeaderHeight}px`,
|
||||
maxHeight: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + measuredHeaderHeight}px`,
|
||||
overflowY: 'auto',
|
||||
flex: 'none',
|
||||
alignSelf: "flex-start",
|
||||
position: 'relative'
|
||||
@ -7761,17 +7732,13 @@ export default function WeeklyView() {
|
||||
|
||||
{/* Day Columns */}
|
||||
<main
|
||||
ref={gridRef}
|
||||
className={`weekly-days-grid cols-${viewDays}`}
|
||||
data-slide-direction={slideDirection}
|
||||
data-nav-type={viewDays > 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,
|
||||
}}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user