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",
|
"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",
|
"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": {
|
||||||
|
|||||||
@ -2703,9 +2703,8 @@ h3 {
|
|||||||
flex: none !important;
|
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 {
|
.time-grid-wrapper {
|
||||||
overflow: visible;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.time-grid-wrapper .weekly-days-grid {
|
.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 {
|
.time-grid-wrapper .weekly-days-grid {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto; /* Unified scroll container */
|
overflow: visible;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-grid-wrapper .weekly-day-column {
|
.time-grid-wrapper .weekly-day-column {
|
||||||
@ -4974,17 +4972,10 @@ h3 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
/* Day headers are not sticky on mobile — CSS sticky doesn't work in a CSS grid scroll container.
|
/* time-grid-wrapper is now the single scroll container, so position:sticky works.
|
||||||
The mobile-sticky-day-bar overlay (fixed position) handles this instead. */
|
time-column-header is hidden on mobile to save vertical space. */
|
||||||
.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-on .time-column-header {
|
.time-grid-on .time-column-header {
|
||||||
position: relative !important;
|
display: none;
|
||||||
top: auto !important;
|
|
||||||
z-index: auto !important;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2492,8 +2492,7 @@ export default function WeeklyView() {
|
|||||||
// Translation helper
|
// Translation helper
|
||||||
const t = translations[language] || translations["en"];
|
const t = translations[language] || translations["en"];
|
||||||
|
|
||||||
// Refs for scroll synchronization
|
// Refs for scroll
|
||||||
const timeColumnRef = useRef<HTMLDivElement>(null);
|
|
||||||
const dayColumnsRef = useRef<HTMLDivElement[]>([]);
|
const dayColumnsRef = useRef<HTMLDivElement[]>([]);
|
||||||
const isScrollSyncing = useRef(false);
|
const isScrollSyncing = useRef(false);
|
||||||
const isInitialScrollDone = useRef(false);
|
const isInitialScrollDone = useRef(false);
|
||||||
@ -2538,30 +2537,11 @@ export default function WeeklyView() {
|
|||||||
const somedaySectionRef = useRef<HTMLElement | null>(null);
|
const somedaySectionRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
// Unified scroll sync handlers
|
// Unified scroll sync handlers
|
||||||
const handleTimeColumnScroll = (e: React.UIEvent<HTMLDivElement>) => {
|
// handleTimeColumnScroll no longer needed — single scroll container via time-grid-wrapper
|
||||||
const scrollTop = e.currentTarget.scrollTop;
|
|
||||||
if (isScrollSyncing.current) return;
|
|
||||||
isScrollSyncing.current = true;
|
|
||||||
|
|
||||||
if (gridRef.current) {
|
const handleGridScroll = (_e: React.UIEvent<HTMLElement>) => {
|
||||||
gridRef.current.scrollTop = scrollTop;
|
// Single scroll container — no sync needed
|
||||||
}
|
// Mobile sticky day is handled by the native scroll listener in the useEffect
|
||||||
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 jumpToHour = (hour: number) => {
|
const jumpToHour = (hour: number) => {
|
||||||
@ -2577,9 +2557,7 @@ export default function WeeklyView() {
|
|||||||
intendedScrollTop.current = scrollOffset;
|
intendedScrollTop.current = scrollOffset;
|
||||||
|
|
||||||
const perform = () => {
|
const perform = () => {
|
||||||
if (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset;
|
|
||||||
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
||||||
if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Repeated enforcement
|
// Repeated enforcement
|
||||||
@ -3140,20 +3118,15 @@ export default function WeeklyView() {
|
|||||||
const slotHeight = getSlotHeight(cellDuration);
|
const slotHeight = getSlotHeight(cellDuration);
|
||||||
const scrollOffset = workingHoursStart * slotsPerHour * slotHeight;
|
const scrollOffset = workingHoursStart * slotsPerHour * slotHeight;
|
||||||
|
|
||||||
// Scroll the time-grid-wrapper (the scrollable viewport)
|
// Single scroll container: gridRef points to time-grid-wrapper
|
||||||
if (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset;
|
|
||||||
// Also scroll inner refs as fallback
|
|
||||||
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
||||||
if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset;
|
|
||||||
|
|
||||||
intendedScrollTop.current = scrollOffset;
|
intendedScrollTop.current = scrollOffset;
|
||||||
|
|
||||||
// On initial load, re-enforce for 2s to fight browser auto-scroll restoration
|
// On initial load, re-enforce for 2s to fight browser auto-scroll restoration
|
||||||
if (!isInitialScrollDone.current) {
|
if (!isInitialScrollDone.current) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
if (timeGridWrapperRef.current) timeGridWrapperRef.current.scrollTop = scrollOffset;
|
|
||||||
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
if (gridRef.current) gridRef.current.scrollTop = scrollOffset;
|
||||||
if (timeColumnRef.current) timeColumnRef.current.scrollTop = scrollOffset;
|
|
||||||
}, 50);
|
}, 50);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -4523,7 +4496,6 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
// Navigation handlers with CSS class-based slide animation (works in all browsers)
|
// Navigation handlers with CSS class-based slide animation (works in all browsers)
|
||||||
const gridRef = useRef<HTMLElement>(null);
|
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 allSlideClasses = ["slide-animate-next", "slide-animate-prev", "slide-animate-week-next", "slide-animate-week-prev"];
|
||||||
const navigate = (
|
const navigate = (
|
||||||
newDate: Date,
|
newDate: Date,
|
||||||
@ -7641,19 +7613,18 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
{/* Main Grid with Time Column */}
|
{/* Main Grid with Time Column */}
|
||||||
{viewStyle !== "kanban" && <div style={{ position: "relative", flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
{viewStyle !== "kanban" && <div style={{ position: "relative", flex: 1, minHeight: 0, overflow: 'hidden' }}>
|
||||||
<div className="time-grid-wrapper" ref={timeGridWrapperRef} style={showTimeGrid ? {
|
<div className="time-grid-wrapper" ref={gridRef as React.Ref<HTMLDivElement>} onScroll={handleGridScroll} style={showTimeGrid ? {
|
||||||
maxHeight: '100%',
|
height: '100%',
|
||||||
|
overflowY: 'auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
WebkitOverflowScrolling: 'touch' as any,
|
||||||
} : undefined}>
|
} : undefined}>
|
||||||
{/* Time Column */}
|
{/* Time Column */}
|
||||||
{showTimeGrid && (
|
{showTimeGrid && (
|
||||||
<div
|
<div
|
||||||
className="time-column"
|
className="time-column"
|
||||||
ref={timeColumnRef}
|
|
||||||
onScroll={handleTimeColumnScroll}
|
|
||||||
style={{
|
style={{
|
||||||
height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + measuredHeaderHeight}px`,
|
height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + measuredHeaderHeight}px`,
|
||||||
maxHeight: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + measuredHeaderHeight}px`,
|
|
||||||
overflowY: 'auto',
|
|
||||||
flex: 'none',
|
flex: 'none',
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
position: 'relative'
|
position: 'relative'
|
||||||
@ -7761,17 +7732,13 @@ export default function WeeklyView() {
|
|||||||
|
|
||||||
{/* Day Columns */}
|
{/* Day Columns */}
|
||||||
<main
|
<main
|
||||||
ref={gridRef}
|
|
||||||
className={`weekly-days-grid cols-${viewDays}`}
|
className={`weekly-days-grid cols-${viewDays}`}
|
||||||
data-slide-direction={slideDirection}
|
data-slide-direction={slideDirection}
|
||||||
data-nav-type={viewDays > 1 ? "week" : "day"}
|
data-nav-type={viewDays > 1 ? "week" : "day"}
|
||||||
onScroll={handleGridScroll}
|
|
||||||
style={showTimeGrid ? {
|
style={showTimeGrid ? {
|
||||||
height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`,
|
height: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`,
|
||||||
maxHeight: `${24 * (60 / effectiveCellDuration) * getSlotHeight(effectiveCellDuration) + getHeaderHeight(effectiveCellDuration)}px`,
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignSelf: "flex-start",
|
alignSelf: "flex-start",
|
||||||
overflowY: 'auto'
|
|
||||||
} : {
|
} : {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user