mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-21 18:13:26 +00:00
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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user