Support view transitions for client:only components
This commit is contained in:
parent
aa265d7302
commit
daad1def81
5 changed files with 95 additions and 15 deletions
5
.changeset/lazy-keys-shout.md
Normal file
5
.changeset/lazy-keys-shout.md
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
'astro': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Support view transitions for client:only components
|
|
@ -3,6 +3,7 @@ import Layout from '../components/Layout.astro';
|
||||||
import Island from '../components/Island';
|
import Island from '../components/Island';
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout>
|
||||||
|
<p id="page-one">Page 1</p>
|
||||||
<a id="click-two" href="/client-only-two">go to page 2</a>
|
<a id="click-two" href="/client-only-two">go to page 2</a>
|
||||||
<div transition:persist="island">
|
<div transition:persist="island">
|
||||||
<Island client:only count={5}>message here</Island>
|
<Island client:only count={5}>message here</Island>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Island from '../components/Island';
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout>
|
||||||
<p id="page-two">Page 2</p>
|
<p id="page-two">Page 2</p>
|
||||||
|
<a id="click-one" href="/client-only-one">go to page 1</a>
|
||||||
<div transition:persist="island">
|
<div transition:persist="island">
|
||||||
<Island client:only count={5}>message here</Island>
|
<Island client:only count={5}>message here</Island>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -649,9 +649,14 @@ test.describe('View Transitions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('client:only styles are retained on transition', async ({ page, astro }) => {
|
test('client:only styles are retained on transition', async ({ page, astro }) => {
|
||||||
|
const loads = [];
|
||||||
|
page.addListener('load', async (p) => {
|
||||||
|
loads.push(p);
|
||||||
|
});
|
||||||
|
|
||||||
const totalExpectedStyles = 7;
|
const totalExpectedStyles = 7;
|
||||||
|
|
||||||
// Go to page 1
|
// Go to page 1 (normal load)
|
||||||
await page.goto(astro.resolveUrl('/client-only-one'));
|
await page.goto(astro.resolveUrl('/client-only-one'));
|
||||||
let msg = page.locator('.counter-message');
|
let msg = page.locator('.counter-message');
|
||||||
await expect(msg).toHaveText('message here');
|
await expect(msg).toHaveText('message here');
|
||||||
|
@ -659,13 +664,24 @@ test.describe('View Transitions', () => {
|
||||||
let styles = await page.locator('style').all();
|
let styles = await page.locator('style').all();
|
||||||
expect(styles.length).toEqual(totalExpectedStyles);
|
expect(styles.length).toEqual(totalExpectedStyles);
|
||||||
|
|
||||||
|
// Transition to page 2 (will do a full load)
|
||||||
await page.click('#click-two');
|
await page.click('#click-two');
|
||||||
|
|
||||||
let pageTwo = page.locator('#page-two');
|
let pageTwo = page.locator('#page-two');
|
||||||
await expect(pageTwo, 'should have content').toHaveText('Page 2');
|
await expect(pageTwo, 'should have content').toHaveText('Page 2');
|
||||||
|
|
||||||
|
// Transition to page 1 (will do a full load)
|
||||||
|
await page.click('#click-one');
|
||||||
|
|
||||||
|
let pageOne = page.locator('#page-one');
|
||||||
|
await expect(pageOne, 'should have content').toHaveText('Page 1');
|
||||||
|
|
||||||
|
// Transition to page 1 (real transition, no full load)
|
||||||
|
await page.click('#click-two');
|
||||||
|
|
||||||
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');
|
||||||
|
expect(loads.length, 'There should only be 1 page load').toEqual(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Horizontal scroll position restored on back button', async ({ page, astro }) => {
|
test('Horizontal scroll position restored on back button', async ({ page, astro }) => {
|
||||||
|
|
|
@ -10,6 +10,15 @@ type State = {
|
||||||
};
|
};
|
||||||
type Events = 'astro:page-load' | 'astro:after-swap';
|
type Events = 'astro:page-load' | 'astro:after-swap';
|
||||||
|
|
||||||
|
let viteDevIds: { static: Record<string, string[]>; dynamic: Record<string, string[]> };
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// viteDevIds on a page
|
||||||
|
viteDevIds = JSON.parse(
|
||||||
|
sessionStorage.getItem('astro:viteDevIds') || '{"static":{},"dynamic":{}}'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const page = (url: { origin: string; pathname: string }) => url.origin + url.pathname;
|
||||||
|
|
||||||
// only update history entries that are managed by us
|
// only update history entries that are managed by us
|
||||||
// leave other entries alone and do not accidently add state.
|
// leave other entries alone and do not accidently add state.
|
||||||
const persistState = (state: State) => history.state && history.replaceState(state, '');
|
const persistState = (state: State) => history.state && history.replaceState(state, '');
|
||||||
|
@ -44,8 +53,10 @@ const PERSIST_ATTR = 'data-astro-transition-persist';
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
// explained at its usage
|
// explained at its usage
|
||||||
let noopEl: HTMLDivElement;
|
let noopEl: HTMLDivElement;
|
||||||
|
let reloadEl: HTMLDivElement;
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
noopEl = document.createElement('div');
|
noopEl = document.createElement('div');
|
||||||
|
reloadEl = document.createElement('div');
|
||||||
}
|
}
|
||||||
|
|
||||||
// The History API does not tell you if navigation is forward or back, so
|
// The History API does not tell you if navigation is forward or back, so
|
||||||
|
@ -198,20 +209,40 @@ async function updateDOM(
|
||||||
const href = el.getAttribute('href');
|
const href = el.getAttribute('href');
|
||||||
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
|
return newDocument.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const viteDevId = el.getAttribute('data-vite-dev-id');
|
||||||
|
if (!viteDevId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const newDevEl = newDocument.head.querySelector(`[data-vite-dev-id="${viteDevId}"]`);
|
||||||
|
if (newDevEl) {
|
||||||
|
return newDevEl;
|
||||||
|
}
|
||||||
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
|
// What follows is a fix for an issue (#8472) with missing client:only styles after transition.
|
||||||
// That problem exists only in dev mode where styles are injected into the page by Vite.
|
// That problem exists only in dev mode where styles are injected into the page by Vite.
|
||||||
// Returning a noop element ensures that the styles are not removed from the old document.
|
// Returning a noop element ensures that the styles are not removed from the old document.
|
||||||
// Guarding the code below with the dev mode check
|
// Guarding the code below with the dev mode check
|
||||||
// allows tree shaking to remove this code in production.
|
// allows tree shaking to remove this code in production.
|
||||||
if (import.meta.env.DEV) {
|
if (
|
||||||
if (el.tagName === 'STYLE' && el.dataset.viteDevId) {
|
document.querySelector(
|
||||||
const devId = el.dataset.viteDevId;
|
`[${PERSIST_ATTR}] astro-island[client="only"], astro-island[client="only"][${PERSIST_ATTR}]`
|
||||||
// If this same style tag exists, remove it from the new page
|
)
|
||||||
return (
|
) {
|
||||||
newDocument.querySelector(`style[data-vite-dev-id="${devId}"]`) ||
|
const here = page(toLocation);
|
||||||
// Otherwise, keep it anyways. This is client:only styles.
|
const dynamicViteDevIds = viteDevIds.dynamic[here];
|
||||||
noopEl
|
if (!dynamicViteDevIds) {
|
||||||
);
|
console.info(`
|
||||||
|
${toLocation.pathname}
|
||||||
|
Development mode only: This page uses view transitions with persisted client:only Astro islands.
|
||||||
|
On the first transition to this page, Astro did a full page reload to capture the dynamic effects of the client only code.
|
||||||
|
`);
|
||||||
|
location.href = toLocation.href;
|
||||||
|
return reloadEl;
|
||||||
|
}
|
||||||
|
if (dynamicViteDevIds?.includes(viteDevId)) {
|
||||||
|
return noopEl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -249,12 +280,16 @@ async function updateDOM(
|
||||||
// Swap head
|
// Swap head
|
||||||
for (const el of Array.from(document.head.children)) {
|
for (const el of Array.from(document.head.children)) {
|
||||||
const newEl = persistedHeadElement(el as HTMLElement);
|
const newEl = persistedHeadElement(el as HTMLElement);
|
||||||
|
if (newEl === reloadEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// If the element exists in the document already, remove it
|
// If the element exists in the document already, remove it
|
||||||
// from the new document and leave the current node alone
|
// from the new document and leave the current node alone
|
||||||
if (newEl) {
|
if (newEl) {
|
||||||
newEl.remove();
|
newEl.remove();
|
||||||
} else {
|
} else {
|
||||||
// Otherwise remove the element in the head. It doesn't exist in the new page.
|
// Otherwise remove the element from the head.
|
||||||
|
// It doesn't exist in the new page or will be re-inserted after this loop
|
||||||
el.remove();
|
el.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,6 +371,20 @@ async function transition(
|
||||||
options: Options,
|
options: Options,
|
||||||
popState?: State
|
popState?: State
|
||||||
) {
|
) {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const thisPageStaticViteDevIds = viteDevIds.static[page(location)];
|
||||||
|
if (thisPageStaticViteDevIds) {
|
||||||
|
const allViteDevIds = new Set<string>();
|
||||||
|
document.head
|
||||||
|
.querySelectorAll('[data-vite-dev-id]')
|
||||||
|
.forEach((el) => allViteDevIds.add(el.getAttribute('data-vite-dev-id')!));
|
||||||
|
viteDevIds.dynamic[page(location)] = [...allViteDevIds].filter(
|
||||||
|
(x) => !thisPageStaticViteDevIds.includes(x)
|
||||||
|
);
|
||||||
|
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let finished: Promise<void>;
|
let finished: Promise<void>;
|
||||||
const href = toLocation.href;
|
const href = toLocation.href;
|
||||||
const response = await fetchHTML(href);
|
const response = await fetchHTML(href);
|
||||||
|
@ -360,6 +409,14 @@ async function transition(
|
||||||
location.href = href;
|
location.href = href;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const staticViteDevIds = new Set<string>();
|
||||||
|
newDocument.querySelectorAll('head > [data-vite-dev-id]').forEach((el) => {
|
||||||
|
staticViteDevIds.add(el.getAttribute('data-vite-dev-id')!);
|
||||||
|
});
|
||||||
|
viteDevIds.static[page(toLocation)] = [...staticViteDevIds];
|
||||||
|
sessionStorage.setItem('astro:viteDevIds', JSON.stringify(viteDevIds, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (!popState) {
|
if (!popState) {
|
||||||
// save the current scroll position before we change the DOM and transition to the new page
|
// save the current scroll position before we change the DOM and transition to the new page
|
||||||
|
|
Loading…
Reference in a new issue