feat(ui): autofocus login fields; keep verification code out of form history
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m51s

The two-step e-mail login now drops the cursor on each step's primary
field as it mounts — the e-mail field on load, the code field once the
e-mail step advances — via a small `use:` action. Focusing fires each
input's onfocus, which clears the readonly autofill guard, so the field
is editable straight away.

The code input now requests `autocomplete="one-time-code"` instead of
`new-password`. The latter is a password-manager hint and does not stop
Firefox saving the typed code to form history (it was offering the
previous code back in a dropdown). `one-time-code` is the semantic token
for a verification code; Firefox honours it specifically to keep the
value out of form history (Mozilla bug 1547294). The e-mail field keeps
`new-password` to fend off saved-login autofill.

Tests: new Vitest cases assert autofocus on both steps and the code
field's `one-time-code` token; a new Playwright case covers the same in
Chromium and WebKit (Safari engine). Firefox form history is owner
manual-QA — there is no Firefox project in the e2e matrix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-25 23:53:20 +02:00
parent 6f2967024a
commit 3d5b331bd9
4 changed files with 134 additions and 3 deletions
@@ -1,4 +1,5 @@
<script lang="ts">
import { tick } from "svelte";
import { appScreen } from "$lib/app-nav.svelte";
import {
AuthError,
@@ -22,11 +23,30 @@
// fields and pops the Keychain suggester regardless. The classic
// workaround is to render the input as `readonly` initially —
// Safari does not autofill readonly fields — and drop the
// attribute on the first user focus so typing still works. Once
// dropped, the flag stays false for the rest of the page life.
// attribute on the first focus (user-driven or via `focusOnMount`
// below) so typing still works. Once dropped, the flag stays false
// for the rest of the page life.
let emailReadonly = $state(true);
let codeReadonly = $state(true);
// Autofill intent differs per field. The e-mail input asks for
// `new-password` to stop password managers injecting a saved login.
// The code input asks for `one-time-code` (set on the element): it is
// the semantic token for a verification code and the only reliable way
// to keep Firefox from saving the code to form history and offering it
// back in a dropdown — Firefox honours it specifically, while plain
// `autocomplete="off"` is not respected here (Mozilla bug 1547294).
// Drop the cursor on the step's primary field as soon as it mounts so
// the user can start typing immediately: the e-mail field on load, the
// code field once the e-mail step advances. Deferring one tick lets the
// field's own focus handler wire up first; firing it clears the
// readonly autofill guard above, leaving the field editable. Mirrors
// the focus pattern in `designer-science.svelte`.
function focusOnMount(node: HTMLInputElement): void {
void tick().then(() => node.focus());
}
function describe(err: unknown): string {
if (err instanceof AuthError) {
return err.message;
@@ -199,6 +219,7 @@
spellcheck="false"
readonly={emailReadonly}
onfocus={() => (emailReadonly = false)}
use:focusOnMount
bind:value={email}
disabled={pending}
required
@@ -228,12 +249,13 @@
type="text"
name="galaxy-login-code"
inputmode="numeric"
autocomplete="new-password"
autocomplete="one-time-code"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
readonly={codeReadonly}
onfocus={() => (codeReadonly = false)}
use:focusOnMount
bind:value={code}
disabled={pending}
required