0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-03-24 23:21:57 -05:00

Fix: Retain focus for persisted input elements during view transitions (#8813)

* add new e2e test: persist focus on transition

* save and restore focus during swap

---------

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>
This commit is contained in:
Martin Trapp 2023-10-12 00:08:44 +02:00 committed by GitHub
parent 0abff97fed
commit 3bef32f81c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 4 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Save and restore focus for persisted input elements during view transitions

View file

@ -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: {

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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');
});
});

View file

@ -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 {