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) => {
|
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 (~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">
|
<div className="hidden md:block">
|
||||||
<WalletButton />
|
<WalletButton />
|
||||||
</div>
|
</div>
|
||||||
{showInlineNav ? null : (
|
{/* 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
|
<button
|
||||||
ref={desktopMenuButtonRef}
|
ref={desktopMenuButtonRef}
|
||||||
type="button"
|
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"
|
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={() => {
|
onClick={() => {
|
||||||
setDesktopSearchOpen(false);
|
setDesktopSearchOpen(false);
|
||||||
setOpen((v) => !v);
|
setOpen((v) => !v);
|
||||||
}}
|
}}
|
||||||
|
aria-hidden={showInlineNav}
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
>
|
>
|
||||||
{open ? <X size={18} /> : <Menu size={18} />}
|
{open ? <X size={18} /> : <Menu size={18} />}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user