summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.changeset/three-toes-talk.md5
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/astro.config.mjs2
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro2
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/pages/page-with-persistent-form.astro20
-rw-r--r--packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro3
-rw-r--r--packages/astro/e2e/view-transitions.test.js22
-rw-r--r--packages/astro/src/transitions/router.ts43
7 files changed, 93 insertions, 4 deletions
diff --git a/.changeset/three-toes-talk.md b/.changeset/three-toes-talk.md
new file mode 100644
index 000000000..a6a879ad6
--- /dev/null
+++ b/.changeset/three-toes-talk.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Save and restore focus for persisted input elements during view transitions
diff --git a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
index 68fdc8e2e..2b22ff9cf 100644
--- a/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
+++ b/packages/astro/e2e/fixtures/view-transitions/astro.config.mjs
@@ -4,7 +4,7 @@ import nodejs from '@astrojs/node';
// https://astro.build/config
export default defineConfig({
- output: 'server',
+ output: 'hybrid',
adapter: nodejs({ mode: 'standalone' }),
integrations: [react()],
redirects: {
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro
index ddafb98a9..7ef7b93f8 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/components/Layout.astro
@@ -18,7 +18,7 @@ const { link } = Astro.props as Props;
margin: auto;
}
</style>
- <link rel="stylesheet" href="/style.css">
+ <link rel="stylesheet" href="/styles.css">
<ViewTransitions />
<DarkMode />
<meta name="script-executions" content="0">
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/page-with-persistent-form.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/page-with-persistent-form.astro
new file mode 100644
index 000000000..c150726ed
--- /dev/null
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/page-with-persistent-form.astro
@@ -0,0 +1,20 @@
+---
+import Layout from '../components/Layout.astro';
+---
+<Layout>
+ <h2>Form 1</h2>
+ <form transition:persist>
+ <input id="input" type="text" name="name" autocomplete="false"/>
+ </form>
+
+ <script>
+ import {navigate} from "astro:transitions/client"
+ const form = document.querySelector("form");
+ form.addEventListener("submit", (e) => {
+ console.log("submit");
+ e.preventDefault();
+ navigate(`${location.pathname}?name=${input.value}`,{history: "replace"});
+ return false;
+ });
+ </script>
+</Layout>
diff --git a/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro b/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro
index 44dd03ce0..e9bee1f44 100644
--- a/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro
+++ b/packages/astro/e2e/fixtures/view-transitions/src/pages/query.astro
@@ -1,6 +1,7 @@
---
import Layout from '../components/Layout.astro';
-
+export const prerender = false;
+// this works only with SSR, not with SSG. E2e tests run with output=hybrid or server
const page = Astro.url.searchParams.get('page') || 1;
---
<Layout>
diff --git a/packages/astro/e2e/view-transitions.test.js b/packages/astro/e2e/view-transitions.test.js
index 559592fba..d2c14aabd 100644
--- a/packages/astro/e2e/view-transitions.test.js
+++ b/packages/astro/e2e/view-transitions.test.js
@@ -788,7 +788,7 @@ test.describe('View Transitions', () => {
test('replace history', async ({ page, astro }) => {
await page.goto(astro.resolveUrl('/one'));
- // page six loads the router and automatically uses the router to navigate to page 1
+
let p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');
@@ -833,4 +833,24 @@ test.describe('View Transitions', () => {
p = page.locator('#one');
await expect(p, 'should have content').toHaveText('Page 1');
});
+
+ test('Keep focus on transition', async ({ page, astro }) => {
+ await page.goto(astro.resolveUrl('/page-with-persistent-form'));
+ let locator = page.locator('h2');
+ await expect(locator, 'should have content').toHaveText('Form 1');
+
+ locator = page.locator('#input');
+ await locator.type('Hello');
+ await expect(locator).toBeFocused();
+ await locator.press('Enter');
+
+ await page.waitForURL(/.*name=Hello/);
+ locator = page.locator('h2');
+ await expect(locator, 'should have content').toHaveText('Form 1');
+ locator = page.locator('#input');
+ await expect(locator).toBeFocused();
+
+ await locator.type(' World');
+ await expect(locator).toHaveValue('Hello World');
+ });
});
diff --git a/packages/astro/src/transitions/router.ts b/packages/astro/src/transitions/router.ts
index c21392e3a..869ed87af 100644
--- a/packages/astro/src/transitions/router.ts
+++ b/packages/astro/src/transitions/router.ts
@@ -215,6 +215,45 @@ async function updateDOM(
return null;
};
+ type SavedFocus = {
+ activeElement: HTMLElement | null;
+ start?: number | null;
+ end?: number | null;
+ };
+
+ const saveFocus = (): SavedFocus => {
+ const activeElement = document.activeElement as HTMLElement;
+ // The element that currently has the focus is part of a DOM tree
+ // that will survive the transition to the new document.
+ // Save the element and the cursor position
+ if (activeElement?.closest('[data-astro-transition-persist]')) {
+ if (
+ activeElement instanceof HTMLInputElement ||
+ activeElement instanceof HTMLTextAreaElement
+ ) {
+ const start = activeElement.selectionStart;
+ const end = activeElement.selectionEnd;
+ return { activeElement, start, end };
+ }
+ return { activeElement };
+ } else {
+ return { activeElement: null };
+ }
+ };
+
+ const restoreFocus = ({ activeElement, start, end }: SavedFocus) => {
+ if (activeElement) {
+ activeElement.focus();
+ if (
+ activeElement instanceof HTMLInputElement ||
+ activeElement instanceof HTMLTextAreaElement
+ ) {
+ activeElement.selectionStart = start!;
+ activeElement.selectionEnd = end!;
+ }
+ }
+ };
+
const swap = () => {
// swap attributes of the html element
// - delete all attributes from the current document
@@ -263,6 +302,8 @@ async function updateDOM(
// Persist elements in the existing body
const oldBody = document.body;
+ const savedFocus = saveFocus();
+
// this will reset scroll Position
document.body.replaceWith(newDocument.body);
for (const el of oldBody.querySelectorAll(`[${PERSIST_ATTR}]`)) {
@@ -275,6 +316,8 @@ async function updateDOM(
}
}
+ restoreFocus(savedFocus);
+
if (popState) {
scrollTo(popState.scrollX, popState.scrollY); // usings 'auto' scrollBehavior
} else {