Restore horizontal scroll position on history navigation (view transitions) (#8505)
This commit is contained in:
parent
4105491732
commit
2db9762eb0
4 changed files with 74 additions and 11 deletions
5
.changeset/polite-ravens-serve.md
Normal file
5
.changeset/polite-ravens-serve.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Restore horizontal scroll position on history navigation (view transitions)
|
|
@ -15,6 +15,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
type Direction = 'forward' | 'back';
|
type Direction = 'forward' | 'back';
|
||||||
type State = {
|
type State = {
|
||||||
index: number;
|
index: number;
|
||||||
|
scrollX: number;
|
||||||
scrollY: number;
|
scrollY: number;
|
||||||
};
|
};
|
||||||
type Events = 'astro:page-load' | 'astro:after-swap';
|
type Events = 'astro:page-load' | 'astro:after-swap';
|
||||||
|
@ -37,9 +38,9 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
// we reloaded a page with history state
|
// we reloaded a page with history state
|
||||||
// (e.g. history navigation from non-transition page or browser reload)
|
// (e.g. history navigation from non-transition page or browser reload)
|
||||||
currentHistoryIndex = history.state.index;
|
currentHistoryIndex = history.state.index;
|
||||||
scrollTo({ left: 0, top: history.state.scrollY });
|
scrollTo({ left: history.state.scrollX, top: history.state.scrollY });
|
||||||
} else if (transitionEnabledOnThisPage()) {
|
} else if (transitionEnabledOnThisPage()) {
|
||||||
history.replaceState({ index: currentHistoryIndex, scrollY }, '');
|
history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
|
||||||
}
|
}
|
||||||
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
const throttle = (cb: (...args: any[]) => any, delay: number) => {
|
||||||
let wait = false;
|
let wait = false;
|
||||||
|
@ -208,17 +209,29 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
// Chromium based browsers (Chrome, Edge, Opera, ...)
|
// Chromium based browsers (Chrome, Edge, Opera, ...)
|
||||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||||
|
|
||||||
|
let initialScrollX = 0;
|
||||||
let initialScrollY = 0;
|
let initialScrollY = 0;
|
||||||
if (!state && loc.hash) {
|
if (!state && loc.hash) {
|
||||||
const id = decodeURIComponent(loc.hash.slice(1));
|
const id = decodeURIComponent(loc.hash.slice(1));
|
||||||
const elem = document.getElementById(id);
|
const elem = document.getElementById(id);
|
||||||
// prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
|
// prefer scrollIntoView() over scrollTo() because it takes scroll-padding into account
|
||||||
elem && (initialScrollY = elem.offsetTop) && elem.scrollIntoView();
|
if (elem) {
|
||||||
} else if (state && state.scrollY !== 0) {
|
elem.scrollIntoView();
|
||||||
scrollTo(0, state.scrollY); // usings default scrollBehavior
|
initialScrollX = Math.max(
|
||||||
|
0,
|
||||||
|
elem.offsetLeft + elem.offsetWidth - document.documentElement.clientWidth
|
||||||
|
);
|
||||||
|
initialScrollY = elem.offsetTop;
|
||||||
|
}
|
||||||
|
} else if (state) {
|
||||||
|
scrollTo(state.scrollX, state.scrollY); // usings default scrollBehavior
|
||||||
}
|
}
|
||||||
!state &&
|
!state &&
|
||||||
history.pushState({ index: ++currentHistoryIndex, scrollY: initialScrollY }, '', loc.href);
|
history.pushState(
|
||||||
|
{ index: ++currentHistoryIndex, scrollX: initialScrollX, scrollY: initialScrollY },
|
||||||
|
'',
|
||||||
|
loc.href
|
||||||
|
);
|
||||||
triggerEvent('astro:after-swap');
|
triggerEvent('astro:after-swap');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -280,7 +293,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we are sure that we will push state, and it is time to create a state if it is still missing.
|
// Now we are sure that we will push state, and it is time to create a state if it is still missing.
|
||||||
!state && history.replaceState({ index: currentHistoryIndex, scrollY }, '');
|
!state && history.replaceState({ index: currentHistoryIndex, scrollX, scrollY }, '');
|
||||||
|
|
||||||
document.documentElement.dataset.astroTransition = dir;
|
document.documentElement.dataset.astroTransition = dir;
|
||||||
if (supportsViewTransitions) {
|
if (supportsViewTransitions) {
|
||||||
|
@ -357,8 +370,11 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
// push state on the first navigation but not if we were here already
|
// push state on the first navigation but not if we were here already
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
history.replaceState({ index: currentHistoryIndex, scrollY: -(scrollY + 1) }, '');
|
history.replaceState(
|
||||||
const newState: State = { index: ++currentHistoryIndex, scrollY: 0 };
|
{ index: currentHistoryIndex, scrollX, scrollY: -(scrollY + 1) },
|
||||||
|
''
|
||||||
|
);
|
||||||
|
const newState: State = { index: ++currentHistoryIndex, scrollX: 0, scrollY: 0 };
|
||||||
history.pushState(newState, '', link.href);
|
history.pushState(newState, '', link.href);
|
||||||
}
|
}
|
||||||
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
scrollTo({ left: 0, top: 0, behavior: 'instant' });
|
||||||
|
@ -404,7 +420,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
const direction: Direction = nextIndex > currentHistoryIndex ? 'forward' : 'back';
|
||||||
currentHistoryIndex = nextIndex;
|
currentHistoryIndex = nextIndex;
|
||||||
if (state.scrollY < 0) {
|
if (state.scrollY < 0) {
|
||||||
scrollTo(0, -(state.scrollY + 1));
|
scrollTo(state.scrollX, -(state.scrollY + 1));
|
||||||
} else {
|
} else {
|
||||||
navigate(direction, new URL(location.href), state);
|
navigate(direction, new URL(location.href), state);
|
||||||
}
|
}
|
||||||
|
@ -432,7 +448,7 @@ const { fallback = 'animate' } = Astro.props as Props;
|
||||||
// There's not a good way to record scroll position before a back button.
|
// There's not a good way to record scroll position before a back button.
|
||||||
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
// So the way we do it is by listening to scrollend if supported, and if not continuously record the scroll position.
|
||||||
const updateState = () => {
|
const updateState = () => {
|
||||||
persistState({ ...history.state, scrollY });
|
persistState({ ...history.state, scrollX, scrollY });
|
||||||
};
|
};
|
||||||
|
|
||||||
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
if ('onscrollend' in window) addEventListener('scrollend', updateState);
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import Layout from '../components/Layout.astro';
|
||||||
|
---
|
||||||
|
<Layout>
|
||||||
|
<a id="click-right" href="#click-one">go right</a>
|
||||||
|
<article id="widepage">
|
||||||
|
<div style="width:300vw; text-align: center">
|
||||||
|
<a id="click-top" href="#click-right">go to top</a> |
|
||||||
|
<a id="click-one" href="/one">go to 1</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</Layout>
|
|
@ -585,4 +585,34 @@ test.describe('View Transitions', () => {
|
||||||
styles = await page.locator('style').all();
|
styles = await page.locator('style').all();
|
||||||
expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed');
|
expect(styles.length).toEqual(totalExpectedStyles, 'style count has not changed');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Horizontal scroll position restored on back button', async ({ page, astro }) => {
|
||||||
|
await page.goto(astro.resolveUrl('/wide-page'));
|
||||||
|
let article = page.locator('#widepage');
|
||||||
|
await expect(article, 'should have script content').toBeVisible('exists');
|
||||||
|
|
||||||
|
let locator = page.locator('#click-one');
|
||||||
|
await expect(locator).not.toBeInViewport();
|
||||||
|
|
||||||
|
await page.click('#click-right');
|
||||||
|
locator = page.locator('#click-one');
|
||||||
|
await expect(locator).toBeInViewport();
|
||||||
|
locator = page.locator('#click-top');
|
||||||
|
await expect(locator).toBeInViewport();
|
||||||
|
|
||||||
|
await page.click('#click-one');
|
||||||
|
let p = page.locator('#one');
|
||||||
|
await expect(p, 'should have content').toHaveText('Page 1');
|
||||||
|
|
||||||
|
await page.goBack();
|
||||||
|
locator = page.locator('#click-one');
|
||||||
|
await expect(locator).toBeInViewport();
|
||||||
|
|
||||||
|
locator = page.locator('#click-top');
|
||||||
|
await expect(locator).toBeInViewport();
|
||||||
|
|
||||||
|
await page.click('#click-top');
|
||||||
|
locator = page.locator('#click-one');
|
||||||
|
await expect(locator).not.toBeInViewport();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue