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:
parent
0abff97fed
commit
3bef32f81c
7 changed files with 93 additions and 4 deletions
5
.changeset/three-toes-talk.md
Normal file
5
.changeset/three-toes-talk.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Save and restore focus for persisted input elements during view transitions
|
|
@ -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: {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue