fix(web): user-menu links navigate client-side, not via full reload
The username dropdown's container had onclick={(e) => e.stopPropagation()}
to keep the window-level close-on-outside-click handler from firing on
inside clicks. But that also blocked SvelteKit's document-level link-click
interceptor, so clicking Settings/Admin inside the menu fell through to
the browser's native navigation — a full-page reload.
Symptom: clicking Admin redirected to /. On the hard reload, the admin
guard (/admin/+layout.ts) ran before bootstrap() had settled the user
store; user.value was still null, so the guard threw redirect(302, '/').
By the time you saw / render, bootstrap had finished and the dropdown
showed Admin again, primed to repeat the cycle.
Replaces the stopPropagation-based outside-click protection with a
target-containment check in the window handler: close only when the click
landed outside both the menu button and the menu container. Drops the
redundant stopPropagation on the menu button too (no longer needed since
the window handler now ignores in-menu clicks).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,24 @@
|
||||
let { children } = $props<{ children: import('svelte').Snippet }>();
|
||||
|
||||
let menuOpen = $state(false);
|
||||
let menuRef: HTMLElement | undefined = $state();
|
||||
let menuButtonRef: HTMLElement | undefined = $state();
|
||||
|
||||
// Close the dropdown when the click was outside both the button and the menu.
|
||||
// The previous implementation used onclick={(e) => e.stopPropagation()} on the
|
||||
// menu container, which kept the window-level close-on-outside-click handler
|
||||
// from firing — but it also blocked SvelteKit's document-level link-click
|
||||
// interceptor, so clicking Admin/Settings inside the dropdown fell through
|
||||
// to the browser's native navigation (a full-page reload). On a hard reload,
|
||||
// /admin/+layout.ts's load function ran before bootstrap() settled the user
|
||||
// store, so the admin guard saw user.value === null and redirected to /.
|
||||
function handleWindowClick(e: MouseEvent) {
|
||||
if (!menuOpen) return;
|
||||
const target = e.target as Node | null;
|
||||
if (!target) return;
|
||||
if (menuRef?.contains(target) || menuButtonRef?.contains(target)) return;
|
||||
menuOpen = false;
|
||||
}
|
||||
|
||||
async function handleLogout() {
|
||||
menuOpen = false;
|
||||
@@ -34,7 +52,7 @@
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:window onclick={() => (menuOpen = false)} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} />
|
||||
<svelte:window onclick={handleWindowClick} onkeydown={(e) => e.key === 'Escape' && (menuOpen = false)} />
|
||||
|
||||
<div class="grid h-screen grid-cols-[auto_1fr] grid-rows-[auto_1fr_auto] bg-background text-text-primary">
|
||||
<header class="col-span-2 flex items-center gap-4 border-b border-border bg-surface px-4 py-3">
|
||||
@@ -44,17 +62,17 @@
|
||||
</div>
|
||||
<div class="relative">
|
||||
<button
|
||||
bind:this={menuButtonRef}
|
||||
type="button"
|
||||
class="rounded px-3 py-1 hover:bg-surface-hover"
|
||||
onclick={(e) => { e.stopPropagation(); menuOpen = !menuOpen; }}
|
||||
onclick={() => (menuOpen = !menuOpen)}
|
||||
>
|
||||
{user.value?.username ?? ''}
|
||||
</button>
|
||||
{#if menuOpen}
|
||||
<div
|
||||
bind:this={menuRef}
|
||||
class="absolute right-0 z-50 mt-1 w-40 rounded border border-border bg-surface shadow"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user