fix(studio): a timeline at 00:00 stays at 00:00 when the window resizes or the zoom is set (#4411)

The centre-anchor step ran on every scale change, including a window resize and a zoom set by a host, and from scroll 0 it always moved the view right, hiding 00:00 under the track header. A view at the start now stays there; a scrolled view still keeps its centre time.
This commit is contained in:
Miguel Ángel
2026-09-24 08:32:05 -04:00
committed by GitHub
parent 55bd0ceba9
commit aa2c2c68fe
2 changed files with 67 additions and 1 deletions
@@ -0,0 +1,65 @@
// @vitest-environment happy-dom
import { act, createRef } from "react";
import { createRoot } from "react-dom/client";
import { describe, expect, it } from "vitest";
import { useTimelinePlayhead } from "./useTimelinePlayhead";
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
function scrollBox(scrollLeft: number) {
const el = document.createElement("div");
let left = scrollLeft;
Object.defineProperties(el, {
clientWidth: { value: 800 },
scrollWidth: { value: 20_000 },
scrollLeft: { get: () => left, set: (v: number) => (left = v) },
});
return el;
}
function Harness({ pps, scroll }: { pps: number; scroll: HTMLDivElement }) {
useTimelinePlayhead({
playheadRef: createRef(),
scrollRef: { current: scroll },
ppsRef: { current: pps },
durationRef: { current: 60 },
isDragging: { current: false },
currentTime: 0,
zoomMode: "manual",
manualZoomPercent: 100,
zoomModeRef: { current: "manual" },
manualZoomPercentRef: { current: 100 },
fitPps: pps,
fitPpsRef: { current: pps },
effectiveDuration: 60,
pps,
timelineReady: true,
elementsLength: 1,
setZoomMode: () => {},
setManualZoomPercent: () => {},
contentOrigin: 32,
});
return null;
}
function zoom(scrollLeft: number, fromPps: number, toPps: number) {
const scroll = scrollBox(scrollLeft);
const host = document.createElement("div");
const root = createRoot(host);
act(() => root.render(<Harness pps={fromPps} scroll={scroll} />));
act(() => root.render(<Harness pps={toPps} scroll={scroll} />));
act(() => root.unmount());
return scroll.scrollLeft;
}
describe("useTimelinePlayhead centre anchor", () => {
it("keeps a view at the start at the start when the scale changes", () => {
expect(zoom(0, 100, 114)).toBe(0);
});
it("keeps the time at the viewport centre when the view is scrolled", () => {
// Centre time (400 + 400 - 32) / 100 = 7.68s lands at 32 + 7.68 * 200 - 400.
expect(zoom(400, 100, 200)).toBe(1168);
});
});
@@ -75,7 +75,8 @@ export function useTimelinePlayhead({
// it true and the next toolbar zoom would wrongly skip center-anchoring.
const skip = skipCenterAnchorRef.current;
skipCenterAnchorRef.current = false;
if (!scroll || pps === prevPps || skip) return;
// A view at the start stays there: a resize or a host zoom must not hide 00:00.
if (!scroll || pps === prevPps || skip || scroll.scrollLeft < 1) return;
const nextScrollLeft = getTimelineScrollLeftForZoomAnchor({
pointerX: scroll.clientWidth / 2,
currentScrollLeft: scroll.scrollLeft,