diff options
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 { |