fix(console): mobile hamburger toggles correctly on tap (BUG-1330) (#480)

The /console mobile hamburger button never opened its dropdown on tap.
A tap on the closed-state SVG inside the button triggered the toggle's
onclick (`mobileMenuOpen = true`), but Svelte 5 then synced the
{#if mobileMenuOpen}{:else}{/if} swap *before* the bubbled
`<svelte:window onclick={handleWindowClick}>` listener ran. By that
point the original `<rect>` click target was detached from the DOM
(`event.target.isConnected === false`); `target.closest('.console-nav')`
walked an orphaned subtree and returned null, the outside-click branch
fired, and the menu was reset to closed in the same tick — visually
"never opened."

Verified the timing in a Svelte 5 playground that mirrors the pattern;
the window handler logged `target=rect, isConnected=false,
closest(.nav)=NULL` for every tap.

Two-layer fix in web/src/routes/console/+layout.svelte:

1. Add `pointer-events: none` to `.mobile-hamburger svg` and its
   children so the click target is always the button itself, which is
   never re-rendered/detached when `mobileMenuOpen` flips. This is the
   primary fix and matches the standard "icons inside buttons should
   not capture pointer events" pattern.

2. Stop propagation on the toggle button's onclick so the click cannot
   reach `handleWindowClick` even if a future change adds an inner
   element without the same guard. Belt-and-braces.

Inline comments record the BUG-1330 root cause so the next person to
touch this nav doesn't reintroduce the SVG-swap.

TopBar.svelte's mobile hamburger is unaffected — its SVG content
doesn't swap on toggle (same icon regardless of sidebar state), so the
detach race never fires there.
This commit is contained in:
xarmian
2026-05-10 16:15:01 -04:00
committed by GitHub
parent 4b887c77db
commit 671ecabc41
+31 -1
View File
@@ -76,9 +76,19 @@
<nav class="console-nav">
<div class="nav-left">
<a href="/console" class="nav-logo">Pad</a>
<!--
stopPropagation: this toggle click MUST NOT reach
handleWindowClick. See the BUG-1330 note on the SVG below
for the full explanation. The CSS `pointer-events: none`
already moves the click target onto the button, but
stopping propagation is belt-and-braces — if a future
change adds another element inside the button without the
same pointer-events guard, the outside-click handler still
won't fire on the toggle itself.
-->
<button
class="mobile-hamburger"
onclick={() => (mobileMenuOpen = !mobileMenuOpen)}
onclick={(e) => { e.stopPropagation(); mobileMenuOpen = !mobileMenuOpen; }}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
aria-controls="console-nav-links"
@@ -233,6 +243,26 @@
background: var(--bg-hover);
}
/*
BUG-1330: clicks on the hamburger MUST always land on the button,
never on the SVG/rect/path inside it. The SVG content swaps between
closed-state (three rects) and open-state (X paths) on toggle, and
Svelte 5 syncs the DOM update synchronously between the delegated
button onclick and the bubbled `<svelte:window onclick>`. If the
click target is one of the inner SVG primitives, by the time
handleWindowClick runs the original target is already detached
(`isConnected === false`) — `target.closest('.console-nav')` walks
up an orphaned subtree and returns null, the "outside-click" branch
fires, and the menu slams shut as fast as it opened. Forcing the
hamburger SVG and its children to be pointer-event-transparent
makes the button the click target, which is never re-rendered.
Verified with a Svelte 5 playground repro.
*/
.mobile-hamburger svg,
.mobile-hamburger svg * {
pointer-events: none;
}
.nav-links {
display: flex;
align-items: center;