fix(header): kill the inline<->burger oscillation around the threshold width
Some checks failed
Deploy to Frontend Servers / deploy (push) Has been cancelled

User report: after switching to Bahasa Melayu and opening the window to
full width, the header juddered every frame. Root cause was a feedback
loop between the fit measurement and the right-side layout:

* In inline mode the right side renders the favorites label
  ("Kegemaran Saya") AND no burger button => ~80px wider.
* In burger mode it renders the burger but drops the label => ~80px
  narrower.

Available space for the nav was therefore mode-dependent, and the old
60px hysteresis was smaller than that ~80px swing. In a band roughly
1282-1302px (with ms needed=591px) the measurement decided:

  burger: needed + 60 <= available_burger     -> flip to inline
  inline: needed     >  available_inline      -> flip back to burger
  (repeat every frame, ResizeObserver re-fires, header shakes)

Two-part fix:

1. Keep the burger button mounted in both modes. When inline it is
   pointer-events:none + invisible + aria-hidden=true + tabIndex=-1,
   so it stays unclickable but still occupies its 40px box. The
   right-side width no longer changes when we flip modes, so the
   measurement input stops jumping.

2. Widen the burger->inline hysteresis from 60 to 120 to absorb the
   remaining ~80px difference from the favorites label (and per-locale
   variations: "My Favorites" vs "Kegemaran Saya" etc). The header
   now picks a mode at each width and stays there.

Verified by sweeping the row width across 1100-2200px on the ms locale
in the browser: modeFlips=0 at every previously-broken width, single
clean burger->inline transition once there's room.
This commit is contained in:
TerryM
2026-06-08 01:31:26 +08:00
parent 78186486c5
commit 0b8c4fe1f7

View File

@@ -421,9 +421,13 @@ export function PublicLayout() {
setShowInlineNav((current) => { setShowInlineNav((current) => {
if (current) return needed <= available; if (current) return needed <= available;
// Need real breathing room before switching back, to avoid flicker // Hysteresis has to be wider than the right-side width difference
// when the right-side width shrinks after the burger button hides. // between the two modes (“My Favorites / Kegemaran Saya” label
return needed + 60 <= available; // shows in inline mode, ~80px). 60px was not enough and produced a
// dead zone (~12821302px in ms) where the header oscillated each
// frame. 120px keeps switching one-way — once were in burger we
// only flip back when theres real headroom.
return needed + 120 <= available;
}); });
}; };
@@ -813,20 +817,27 @@ export function PublicLayout() {
<div className="hidden md:block"> <div className="hidden md:block">
<WalletButton /> <WalletButton />
</div> </div>
{showInlineNav ? null : ( {/* Burger toggle is always in the DOM so the right-side width
<button doesnt change between inline and burger modes. The visible
ref={desktopMenuButtonRef} / invisible toggle controls click-ability without resizing
type="button" the row, which would otherwise feed back into the fit
className="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg" measurement and cause oscillation at borderline widths. */}
onClick={() => { <button
setDesktopSearchOpen(false); ref={desktopMenuButtonRef}
setOpen((v) => !v); type="button"
}} className={`inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full border border-ark-line bg-[#1a1b20] text-neutral-200 outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-ark-bg ${
aria-label="menu" showInlineNav ? "pointer-events-none invisible" : ""
> }`}
{open ? <X size={18} /> : <Menu size={18} />} tabIndex={showInlineNav ? -1 : 0}
</button> onClick={() => {
)} setDesktopSearchOpen(false);
setOpen((v) => !v);
}}
aria-hidden={showInlineNav}
aria-label="menu"
>
{open ? <X size={18} /> : <Menu size={18} />}
</button>
</div> </div>
</div> </div>
</div> </div>