Skip to content

Commit

Permalink
feat(astro): Add support for View Transitions (#4354)
Browse files Browse the repository at this point in the history
  • Loading branch information
wobsoriano authored Oct 18, 2024
1 parent 1be6dac commit 9d34d4f
Show file tree
Hide file tree
Showing 11 changed files with 178 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/late-pigs-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@clerk/astro": minor
---

Add support for Astro View Transitions
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
import { ViewTransitions } from 'astro:transitions';
interface Props {
title: string;
}
const { title } = Astro.props;
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<ViewTransitions />
</head>
<body>
<main>
<slot />
</main>
</body>
</html>
15 changes: 15 additions & 0 deletions integration/templates/astro-node/src/pages/transitions/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
import { SignedIn, SignedOut, UserButton } from "@clerk/astro/components";
import Layout from "../../layouts/ViewTransitionsLayout.astro";
---

<Layout title="Sign in">
<div class="w-full flex justify-center">
<SignedOut>
<a href="/transitions/sign-in">Sign in</a>
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</div>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import { SignIn } from "@clerk/astro/components";
import Layout from "../../layouts/ViewTransitionsLayout.astro";
---

<Layout title="Sign in">
<div class="w-full flex justify-center">
<SignIn forceRedirectUrl="/transitions" />
</div>
</Layout>
30 changes: 30 additions & 0 deletions integration/tests/astro/components.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,34 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await u.po.expect.toBeSignedIn();
await expect(u.page.getByText('Not a member')).toBeVisible();
});

test('renders components and keep internal routing behavior when view transitions is enabled', async ({
page,
context,
}) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/transitions');
// Navigate to sign-in page using link to simulate transition
await u.page.getByRole('link', { name: /Sign in/i }).click();

// Components should be mounted on the new document
// when navigating through links
await u.page.waitForURL(`${app.serverUrl}/transitions/sign-in`);
await u.po.signIn.waitForMounted();

await u.po.signIn.setIdentifier(fakeAdmin.email);
await u.po.signIn.continue();
await u.page.waitForURL(`${app.serverUrl}/transitions/sign-in#/factor-one`);

await u.po.signIn.setPassword(fakeAdmin.password);
await u.po.signIn.continue();

await u.po.expect.toBeSignedIn();

// Internal Clerk routing should still work
await u.page.waitForURL(`${app.serverUrl}/transitions`);

// Components should be rendered on hard reload
await u.po.userButton.waitForMounted();
});
});
27 changes: 25 additions & 2 deletions packages/astro/src/integration/create-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,31 @@ function createIntegration<Params extends HotloadAstroClerkIntegrationParams>()
'page',
`
${command === 'dev' ? `console.log("${packageName}","Initialize Clerk: page")` : ''}
import { runInjectionScript } from "${buildImportPath}";
await runInjectionScript(${JSON.stringify(internalParams)});`,
import { runInjectionScript, swapDocument } from "${buildImportPath}";
import { navigate, transitionEnabledOnThisPage } from "astro:transitions/client";
if (transitionEnabledOnThisPage()) {
document.addEventListener('astro:before-swap', (e) => {
const clerkComponents = document.querySelector('#clerk-components');
// Keep the div element added by Clerk
if (clerkComponents) {
const clonedEl = clerkComponents.cloneNode(true);
e.newDocument.body.appendChild(clonedEl);
}
e.swap = () => swapDocument(e.newDocument);
});
document.addEventListener('astro:page-load', async (e) => {
await runInjectionScript({
...${JSON.stringify(internalParams)},
routerPush: navigate,
routerReplace: (url) => navigate(url, { history: 'replace' }),
});
})
} else {
await runInjectionScript(${JSON.stringify(internalParams)});
}`,
);
},
},
Expand Down
2 changes: 2 additions & 0 deletions packages/astro/src/integration/vite-plugin-astro-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function vitePluginAstroConfig(astroConfig: AstroConfig): VitePlugin {
// This ensures @clerk/astro/client is properly processed and bundled,
// resolving runtime import issues in these components.
config.optimizeDeps?.include?.push('@clerk/astro/client');
// Let astro vite plugin handle this.
config.optimizeDeps?.exclude?.push('astro:transitions/client');
},
load(id) {
if (id === resolvedVirtualModuleId) {
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/internal/create-clerk-instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ async function createClerkInstanceInternal(options?: AstroClerkCreateInstancePar
}

initOptions = {
...options,
routerPush: createNavigationHandler(window.history.pushState.bind(window.history)),
routerReplace: createNavigationHandler(window.history.replaceState.bind(window.history)),
...options,
};

return clerkJSInstance
Expand Down
1 change: 1 addition & 0 deletions packages/astro/src/internal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ const runInjectionScript = createInjectionScriptRunner(createClerkInstance);
export { runInjectionScript };

export { generateSafeId } from './utils/generateSafeId';
export { swapDocument } from './swap-document';
61 changes: 61 additions & 0 deletions packages/astro/src/internal/swap-document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// eslint-disable-next-line import/no-unresolved
import { swapFunctions } from 'astro:transitions/client';

const PERSIST_ATTR = 'data-astro-transition-persist';
const EMOTION_ATTR = 'data-emotion';

/**
* @internal
* Custom swap function to make mounting and styling
* of Clerk components work with View Transitions in Astro.
*
* See https://docs.astro.build/en/guides/view-transitions/#building-a-custom-swap-function
*/
export function swapDocument(doc: Document) {
swapFunctions.deselectScripts(doc);
swapFunctions.swapRootAttributes(doc);

// Keep the elements created by `@emotion/cache`
const emotionElements = document.querySelectorAll(`style[${EMOTION_ATTR}]`);
swapHeadElements(doc, Array.from(emotionElements));

const restoreFocusFunction = swapFunctions.saveFocus();
swapFunctions.swapBodyElement(doc.body, document.body);
restoreFocusFunction();
}

/**
* This function is a copy of the original `swapHeadElements` function from `astro:transitions/client`.
* The difference is that you can pass a list of elements that should not be removed
* in the new document.
*
* See https://github.com/withastro/astro/blob/d6f17044d3873df77cfbc73230cb3194b5a7d82a/packages/astro/src/transitions/swap-functions.ts#L51
*/
function swapHeadElements(doc: Document, ignoredElements: Element[]) {
for (const el of Array.from(document.head.children)) {
const newEl = persistedHeadElement(el, doc);

if (newEl) {
newEl.remove();
} else {
if (!ignoredElements.includes(el)) {
el.remove();
}
}
}

document.head.append(...doc.head.children);
}

function persistedHeadElement(el: Element, newDoc: Document) {
const id = el.getAttribute(PERSIST_ATTR);
const newEl = id && newDoc.head.querySelector(`[${PERSIST_ATTR}="${id}"]`);
if (newEl) {
return newEl;
}
if (el.matches('link[rel=stylesheet]')) {
const href = el.getAttribute('href');
return newDoc.head.querySelector(`link[rel=stylesheet][href="${href}"]`);
}
return null;
}
2 changes: 1 addition & 1 deletion packages/astro/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,6 @@ export default defineConfig(() => {
bundle: true,
sourcemap: true,
format: ['esm'],
external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage'],
external: ['astro', 'react', 'react-dom', 'node:async_hooks', '#async-local-storage', 'astro:transitions/client'],
};
});

0 comments on commit 9d34d4f

Please sign in to comment.