kit
kit copied to clipboard
Create a restore-focus option for progressively-enhanced GET forms
Describe the problem
There's an amazing, brand new feature whereby GET forms can now be progressively enhanced, which means we can use them to navigate to new search results, with less code, that will even work without JavaScript. This is a huge win. Unfortunately there's still one shortcoming: there's no way to have focus restored after the navigation.
In other words, type into the textbox, hit enter, have the new results show up, but also have focus restore into the same textbox
Describe the proposed solution
I guess there's be a data-sveltekit-restore-focus attribute?
Alternatives considered
n/a
Importance
would make my life easier - I think this feature (progressively enhanced GET forms) is severely hampered without this.
Additional Information
No response
Use autofocus:
<form>
<input autofocus name="q" />
<button>click</button>
</form>
If you want the input to only focus after a submission, you can use afterNavigate:
<script>
import { afterNavigate } from '$app/navigation';
/** @type {HTMLInputElement} */
let input;
afterNavigate(({ type }) => {
if (type === 'form') {
input.focus();
}
});
</script>
<form>
<input bind:this={input} name="q" />
<button>click</button>
</form>
A data-sveltekit-keepfocus attribute would keep focus on the current element, which is potentially useful in some cases but doesn't work if you interact with the form via a <button>, say. The approaches above are therefore probably better, I think
@Rich-Harris thanks for replying! I know you can sort of hack this with an afterNavigate event. And I know restore-focus wouldn’t restore focus to the textbox if you clicked a submit button.
But it does seem lacking that you can call goto from a keyDown handler, with restoreFocus, and keep your input focused, but if you replace that with a form, it’s no longer possible.
Also, the afterNavigate handler that checks for type === “form” would be a bit fragile. You’d only want it to restore focus if you submitted via pressing enter, but NOT if you clicked a submit button.
autofocus is a decent middleground, but that causes the input to always focus after the page loads, regardless of what was focused, or what caused the navigation.
The purpose of the progressive enhancement is to emulate the browser native behaviour but without reloading the page. If you want to only focus the input after an enter but not a button click, you could do this:
<script>
import { beforeNavigate, afterNavigate } from '$app/navigation';
/** @type {HTMLElement} */
let focused;
beforeNavigate(({ type }) => {
focused = document.activeElement;
});
afterNavigate(({ type }) => {
if (type === 'form') {
focused?.focus();
}
});
</script>
<form>
<input name="q" />
<button>click</button>
</form>
There's also nothing preventing you from providing a custom on:submit function, if the emulated browser native behaviour isn't what you're looking for.
(To be clear: not ruling out a declarative option. Just exploring whether or not it's truly necessary, and providing a workaround until we reach a decision on that.)
@Rich-Harris thanks! - yeah, the primitives are there. I'm sure some subset of the approaches above could be used to make a nice helper to streamline this.
This certainly isn't any kind of blocking need, but I am curious if you think this would be a useful addition to SvelteKit at some point. goto already has this option; shouldn't it also be available for forms? Most of the other goto / link options work with forms, so I'd be surprised if this was by design the lone exception.
Our replies criss-crosed - you answered my point - thank you!
Coming back to this, I've been making a lot of progressive-enhanced-form demos with <form method="GET"> and often find myself needing to replicate SK's form handling so I can use keepFocus and replaceState from goto. I wish I didn't have to do that, and would prefer a declarative solution if possible.
For example, I find myself doing this pattern for a debounced search input. Because I'm submitting the form as they're typing, I need to keep focus and want to replace state to avoid pushing a bunch of partial searches into the history.
<script lang="ts">
import { goto } from '$app/navigation';
let form: HTMLFormElement;
const debouncedSubmit = debounce(() => {
// not supported in all browsers
if (typeof HTMLFormElement.prototype.requestSubmit == 'function') {
form.requestSubmit();
}
}, 300);
export function submitReplaceState(e: SubmitEvent) {
e.preventDefault();
const form = e.target as HTMLFormElement;
const url = new URL(form.action);
// @ts-expect-error
const params = new URLSearchParams(new FormData(form));
url.search = params.toString();
goto(url, { replaceState: true, keepFocus: true, noScroll: true });
}
</script>
<form bind:this={form} on:submit|preventDefault={submitReplaceState}>
<label for="q">Query</label>
<input
id="q"
type="text"
name="q"
on:input={debouncedSubmit}
/>
</form>
I think it would be cool to be able to teach something like this without having to introduce a custom submit handler. Otherwise SvelteKit's automatic <form method="GET"> handling is a bit lacking with all the scenarios in which you need to eject.
I'm sure this has been considered, but what about introducing new data-sveltekit link attributes for both keepFocus and replaceState? They could be misused on regular ol' <a> tags, but I think it would be quite useful on forms. We already have one goto option available declaratively (noscroll), why not more?
Things like this are why I voted against SvelteKit handling this as a <a> attribute and instead introducing a use:enhanceGet action, which I feel could have handled such use cases easier while better managing expectations ("ok I need to add this action and I can customize this, nice" vs "Oooh so cool I gotta do nothing to progressively enhance these ... but oh, the default behavior is useless for me")
@dummdidumm more basically, is there some sort of structural problem why a keepFocus option can't / shouldn't be added to a form? That option already exists in the goto api, so if nothing else this seems like a question of consistent api.
Things like this are why I voted against SvelteKit handling this as a attribute and instead introducing a use:enhanceGet action, which I feel could have handled such use cases easier while better managing expectations ("ok I need to add this action and I can customize this, nice" vs "Oooh so cool I gotta do nothing to progressively enhance these ... but oh, the default behavior is useless for me")
I don't think having a dedicated enhanceGet for this would have really changed the approach for solving this issue - we're talking about the difference between
<form data-sveltekit-replacestate data-sveltekit-keepfocus>
and
<form use:enhanceGet={{ replaceState: true, keepFocus: true }}>
either way, it's some sort of boolean flag.
And I think it makes sense to have parity between our declarative navigation API (<a> and <form> with data-sveltekit-* attributes) and our imperative navigation API (goto with options).
I'd love a data-sveltekit-keepfocus option, I think. We had a path-based (ie, not querystring-based) app navigation structure, and we realised that it did not produce a good keyboard experience. As a workaround we did a:
on:click|preventDefault
and used gotoUrl (with keepfocus: true)
https://github.com/ONSdigital/dp-census-atlas/blob/3e29b03b8505a704ddcb2530d51228fd502b12e2/src/components/CategoryPage.svelte#L47
It would be nicer to be able to decorate the <a> with an attribute.
This would be really nice to have, as mentioned - especially for those inline-saving fields. And I believe that passing {keepFocus: true} to the update() would be quite elegant. You obviously can go with a custom on:submit but since use:ehnace is doing 99% of the things you usually want, some configuration passed to update() wouldn't hurt, would it?
@dummdidumm thank you!!!