fix(header): kill the inline<->burger oscillation around the threshold width
Some checks failed
Deploy to Frontend Servers / deploy (push) Has been cancelled
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:
@@ -421,9 +421,13 @@ export function PublicLayout() {
|
||||
|
||||
setShowInlineNav((current) => {
|
||||
if (current) return needed <= available;
|
||||
// Need real breathing room before switching back, to avoid flicker
|
||||
// when the right-side width shrinks after the burger button hides.
|
||||
return needed + 60 <= available;
|
||||
// Hysteresis has to be wider than the right-side width difference
|
||||
// between the two modes (“My Favorites / Kegemaran Saya” label
|
||||
// shows in inline mode, ~80px). 60px was not enough and produced a
|
||||
// dead zone (~1282–1302px in ms) where the header oscillated each
|
||||
// frame. 120px keeps switching one-way — once we’re in burger we
|
||||
// only flip back when there’s real headroom.
|
||||
return needed + 120 <= available;
|
||||
});
|
||||
};
|
||||
|
||||
@@ -813,20 +817,27 @@ export function PublicLayout() {
|
||||
<div className="hidden md:block">
|
||||
<WalletButton />
|
||||
</div>
|
||||
{showInlineNav ? null : (
|
||||
<button
|
||||
ref={desktopMenuButtonRef}
|
||||
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"
|
||||
onClick={() => {
|
||||
setDesktopSearchOpen(false);
|
||||
setOpen((v) => !v);
|
||||
}}
|
||||
aria-label="menu"
|
||||
>
|
||||
{open ? <X size={18} /> : <Menu size={18} />}
|
||||
</button>
|
||||
)}
|
||||
{/* Burger toggle is always in the DOM so the right-side width
|
||||
doesn’t change between inline and burger modes. The visible
|
||||
/ invisible toggle controls click-ability without resizing
|
||||
the row, which would otherwise feed back into the fit
|
||||
measurement and cause oscillation at borderline widths. */}
|
||||
<button
|
||||
ref={desktopMenuButtonRef}
|
||||
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 ${
|
||||
showInlineNav ? "pointer-events-none invisible" : ""
|
||||
}`}
|
||||
tabIndex={showInlineNav ? -1 : 0}
|
||||
onClick={() => {
|
||||
setDesktopSearchOpen(false);
|
||||
setOpen((v) => !v);
|
||||
}}
|
||||
aria-hidden={showInlineNav}
|
||||
aria-label="menu"
|
||||
>
|
||||
{open ? <X size={18} /> : <Menu size={18} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user