fix: make the reading viewport a script-driven scroll container so sticky and view timelines work
This commit is contained in:
@@ -99,8 +99,19 @@ const browserRuntime = (): NavigationRuntime => ({
|
||||
pushHash: (hash) => window.history.pushState(window.history.state, "", hash),
|
||||
replaceHash: (hash) =>
|
||||
window.history.replaceState(window.history.state, "", hash),
|
||||
scrollTarget: (target, behavior) =>
|
||||
target.scrollIntoView({ behavior, block: "start" }),
|
||||
scrollTarget: (target, behavior) => {
|
||||
// Inside the live reading stage the document is the only real scroller;
|
||||
// scrollIntoView would move the script-driven inner viewport instead.
|
||||
if (target.closest('[data-reading-stage="live"]') === null) {
|
||||
target.scrollIntoView({ behavior, block: "start" });
|
||||
return;
|
||||
}
|
||||
const margin = parseFloat(getComputedStyle(target).scrollMarginTop) || 0;
|
||||
window.scrollTo({
|
||||
top: window.scrollY + target.getBoundingClientRect().top - margin,
|
||||
behavior,
|
||||
});
|
||||
},
|
||||
focusHeading: (target) => {
|
||||
target
|
||||
.querySelector<HTMLElement>('h1[tabindex="-1"], h2[tabindex="-1"]')
|
||||
|
||||
@@ -48,8 +48,10 @@ export function createSignStage(resources: SceneResourceBag) {
|
||||
Math.min(window.scrollY, contentHeight - viewportHeight),
|
||||
);
|
||||
if (offset !== lastOffset) {
|
||||
main.style.transform = `translate3d(0, ${-offset}px, 0)`;
|
||||
// The viewport is a real (overflow: hidden) scroll container driven by
|
||||
// the document scroll, so sticky and view-timeline layout work inside it.
|
||||
lastOffset = offset;
|
||||
viewport.scrollTop = offset;
|
||||
}
|
||||
// Quantize the opening reveal: the mask image is rebuilt at most
|
||||
// REVEAL_STEPS times on the way in instead of on every scroll event.
|
||||
@@ -149,6 +151,21 @@ export function createSignStage(resources: SceneResourceBag) {
|
||||
};
|
||||
|
||||
resources.listen(window, 'scroll', moveContent, { passive: true });
|
||||
// Anything the browser scrolls inside the viewport (find-in-page, focus,
|
||||
// scrollIntoView) is forwarded to the document, which stays the one scroller.
|
||||
if (viewport)
|
||||
resources.listen(
|
||||
viewport,
|
||||
'scroll',
|
||||
() => {
|
||||
if (!ownsStage() || Number.isNaN(lastOffset)) return;
|
||||
const drift = viewport.scrollTop - lastOffset;
|
||||
if (Math.abs(drift) < 1) return;
|
||||
viewport.scrollTop = lastOffset;
|
||||
window.scrollBy({ top: drift, behavior: 'instant' });
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
const layoutChanged = () => {
|
||||
dirty = true;
|
||||
if (ownsStage()) update(root.clientWidth, window.innerHeight);
|
||||
@@ -184,7 +201,7 @@ export function createSignStage(resources: SceneResourceBag) {
|
||||
delete stage.dataset.readingStage;
|
||||
stage.style.removeProperty('height');
|
||||
}
|
||||
main.style.removeProperty('transform');
|
||||
if (viewport) viewport.scrollTop = 0;
|
||||
viewport?.style.removeProperty('mask-image');
|
||||
viewport?.style.removeProperty('height');
|
||||
for (const image of lazyImages) image.loading = 'lazy';
|
||||
|
||||
@@ -524,12 +524,12 @@
|
||||
.reading-stage[data-reading-stage='live'] > .reading-viewport {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
overflow: clip;
|
||||
/* A real scroll container (scrolled from script, never by the user), so
|
||||
sticky children and view timelines resolve against the reading window. */
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
isolation: isolate;
|
||||
}
|
||||
.reading-stage[data-reading-stage='live'] .portfolio-main {
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* The stationary reading edge supplies the live reveal. View timelines use
|
||||
layout positions, which no longer follow the translated content here. */
|
||||
|
||||
Reference in New Issue
Block a user