fix(frontend): robust Escape handling + deep-link back navigation

focus-trap: focus the trapped container synchronously on mount instead of
only after a deferred rAF. The keydown listener is node-scoped, so an Escape
pressed before the rAF moved focus into the sheet landed on an element outside
the node and was silently dropped — a real keyboard-a11y gap (open a sheet,
immediately press Escape → nothing happened). The rAF still refines focus to
the first control once laid out.

recover: replace the `window.history.length > 1` back-chevron heuristic with
SvelteKit's afterNavigate `from` signal. history.length is 2 on a fresh-tab
deep link (about:blank + page), so history.back() landed on the blank entry.
`from` is null only on a full-page load, so deep-linked users now correctly
fall back to /join (or /feed when authed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
fabi
2026-06-30 22:12:05 +02:00
parent b32231d4f4
commit 792a4f0e4b
2 changed files with 18 additions and 4 deletions

View File

@@ -64,11 +64,16 @@ export function focusTrap(
node.addEventListener('keydown', onKeyDown);
if (opts.autoFocus !== false) {
// Defer one frame so the element is fully laid out (sheets animate in).
// Focus the container synchronously on mount so the trap owns the keyboard
// immediately — otherwise an Escape pressed before the deferred focus below
// lands on an element *outside* the node, where this node-scoped listener
// never sees it (the keystroke is silently dropped). Then defer one frame to
// move focus onto the first control once the sheet has laid out / animated in.
if (!node.hasAttribute('tabindex')) node.setAttribute('tabindex', '-1');
node.focus({ preventScroll: true });
requestAnimationFrame(() => {
const list = focusables(node);
const target = list[0] ?? node;
if (!node.hasAttribute('tabindex')) node.setAttribute('tabindex', '-1');
target.focus({ preventScroll: true });
});
}

View File

@@ -1,14 +1,23 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { goto, afterNavigate } from '$app/navigation';
import { api, ApiError } from '$lib/api';
import { setAuth, getPin, getToken } from '$lib/auth';
import { browser } from '$app/environment';
import IconButton from '$lib/components/IconButton.svelte';
// `from` is non-null only when we arrived here via in-app (client-side)
// navigation; on a full-page load (deep link, new tab) it's null. Using
// history.length is unreliable — a fresh tab keeps `about:blank` as the prior
// entry, so history.back() would land there instead of inside the app.
let cameFromApp = $state(false);
afterNavigate(({ from }) => {
cameFromApp = from !== null;
});
function goBack() {
// Prefer the actual previous page (most users land here from /join or /account).
// Fall back to a sensible default based on auth state for deep-linked users.
if (browser && window.history.length > 1) {
if (cameFromApp) {
window.history.back();
return;
}