fix(timeline): use empty-deps useLayoutEffect for mount scroll to today
CI / Architecture Guardrails (pull_request) Successful in 4m53s
CI / Typecheck (pull_request) Successful in 4m55s
CI / Assistant Split Regression (pull_request) Successful in 5m38s
CI / Build (pull_request) Has been cancelled
CI / Lint (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Fresh-Linux Docker Deploy (pull_request) Has been cancelled
CI / Release Images (pull_request) Has been cancelled
CI / Unit Tests (pull_request) Has been cancelled

The guard-ref approach broke in React Strict Mode (dev): the ref
persisted as `true` across the simulated remount, so the second
invocation skipped the scroll — leaving scrollLeft=0 (today-90
at the left edge, not today). An empty-deps useLayoutEffect runs
twice in Strict Mode but both executions fire against the same
initial `toLeft` and produce the correct result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 08:38:08 +02:00
parent 944d36bdb2
commit 7285668c52
@@ -689,17 +689,15 @@ function TimelineViewContent({
const pendingLeftCompensationPx = useRef(0);
// Flag: scroll viewport to today after the next viewStart-driven re-layout.
const pendingScrollToTodayRef = useRef(false);
// Guard: only auto-scroll to today once on initial mount.
const scrolledToTodayOnMount = useRef(false);
// Scroll to today on first mount so the viewport opens with today at the left edge.
// Scroll to today on mount so the viewport opens with today at the left edge.
// Empty deps: intentionally runs once (and twice in React Strict Mode dev, both correct).
useLayoutEffect(() => {
if (scrolledToTodayOnMount.current) return;
const el = scrollContainerRef.current;
if (!el) return;
el.scrollLeft = toLeft(today);
scrolledToTodayOnMount.current = true;
}, [toLeft, today]);
}, []);
// Apply scroll compensation synchronously after the canvas grows (left-extend or Today button).
useLayoutEffect(() => {