Skip to content

Commit 5a5f607

Browse files
committed
fix(examples): pin opened example to viewport bottom (button visible, no jump)
Restores the original correct end state — the whole example in view with the clicked button still visible just above it — without the old delayed jump. The subtlety: a single scroll on open lands wrong because the layout settles over the first few hundred ms (header/Monaco panels reserving height; async diagram images above the example loading and pushing it down). A smooth scroll here also inherits the page's `scroll-behavior: smooth`, animates ~1s, and gets interrupted mid-animation. So: re-align INSTANTLY to block:"end" each frame for a short (~700ms) window — instant re-aligns are imperceptible, so the example just stays pinned to the bottom as the page settles instead of jumping a second later. Bail immediately on any user wheel/touch/keydown so it never fights the user. Verified: example fully in view + bottom-aligned + clicked button visible, and no delayed scroll after the window.
1 parent e7b4c09 commit 5a5f607

1 file changed

Lines changed: 39 additions & 8 deletions

File tree

src/ExampleEditor.tsx

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,47 @@ export function ExampleEditor(props: IExampleData) {
1515
const [template, updateTemplate] = useState<string | undefined>(JSON.stringify(props.template, null, 2));
1616
const [outputResult, updateOutputResult] = useState<string | undefined>(JSON.stringify(props.result, null, 2));
1717

18-
// Bring the just-opened example into view ONCE, immediately on open (no debounce), and only when
19-
// it opened largely below the fold — e.g. the button was near the bottom, so the whole example
20-
// would otherwise be off-screen. We align its TOP (`block: "start"`): the top is a stable target
21-
// (the three panels reserve fixed height), so there's no delayed re-scroll / jump like the old
22-
// debounced `block: "end"` had. An already-visible example doesn't move.
18+
// Keep the newly-opened example pinned to the BOTTOM of the viewport — the original, correct end
19+
// state: the whole example is in view AND the clicked example button stays visible just above it.
20+
// Two things this has to survive, which the old 500ms-debounced smooth scroll handled clumsily
21+
// (it opened, paused, then "jumped" a second later):
22+
// - a "smooth"/"auto" scroll inherits the page's `scroll-behavior: smooth` and animates over
23+
// ~1s, and gets interrupted mid-animation — so it lands short;
24+
// - async content ABOVE the example (docs diagram images) finishes loading in the first few
25+
// hundred ms and pushes the example down after a single scroll.
26+
// So: re-align INSTANTLY every frame for a short settle window, and bail the moment the user
27+
// scrolls, so we never fight them. Instant re-aligns are imperceptible; the example simply stays
28+
// put as the page settles. ("instant" is a valid runtime ScrollBehavior; TS 4.9's lib predates it.)
2329
useEffect(() => {
2430
const el = ref.current;
25-
if (el && el.getBoundingClientRect().top > window.innerHeight * 0.5) {
26-
el.scrollIntoView({ block: "start", behavior: "smooth" });
27-
}
31+
if (!el) return;
32+
// Pin the example to the BOTTOM of the viewport (the original, correct end state: whole
33+
// example in view, clicked button still visible above it). This has to survive the layout
34+
// settling in the first few hundred ms (the header/Monaco panels reserving their height, and
35+
// async diagram images ABOVE the example loading and pushing it down). So re-align INSTANTLY
36+
// each frame for a short window — instant re-aligns are imperceptible, so the example just
37+
// "stays put" instead of the old smooth scroll that opened, paused, then jumped a second
38+
// later. Bail the instant the user scrolls, so we never fight them.
39+
// ("instant" is a valid runtime ScrollBehavior; TS 4.9's DOM lib predates it — hence the cast.)
40+
let raf = 0;
41+
let userScrolled = false;
42+
const onUserScroll = () => { userScrolled = true; };
43+
window.addEventListener("wheel", onUserScroll, { passive: true });
44+
window.addEventListener("touchstart", onUserScroll, { passive: true });
45+
window.addEventListener("keydown", onUserScroll);
46+
const start = performance.now();
47+
const align = () => {
48+
if (userScrolled) return;
49+
el.scrollIntoView({ block: "end", behavior: "instant" as ScrollBehavior });
50+
if (performance.now() - start < 700) raf = requestAnimationFrame(align);
51+
};
52+
align();
53+
return () => {
54+
cancelAnimationFrame(raf);
55+
window.removeEventListener("wheel", onUserScroll);
56+
window.removeEventListener("touchstart", onUserScroll);
57+
window.removeEventListener("keydown", onUserScroll);
58+
};
2859
// Run once, on open.
2960
// eslint-disable-next-line react-hooks/exhaustive-deps
3061
}, []);

0 commit comments

Comments
 (0)