terry-wallet-login #15
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: "Mobile menu drawer invisible — Quick Fix"
|
||||||
|
type: quick-fix
|
||||||
|
date: 2026-06-03
|
||||||
|
---
|
||||||
|
|
||||||
|
# Mobile menu drawer invisible — Quick Fix
|
||||||
|
|
||||||
|
## Bug
|
||||||
|
After redesigning the mobile menu to the full-screen Figma drawer (`4164-5336` → `ARK V2 - 導航菜單`), tapping the hamburger toggled the icon to `X` but the drawer overlay never appeared on screen. Page content stayed fully visible and the bottom nav stayed on top.
|
||||||
|
|
||||||
|
## Root Cause
|
||||||
|
The drawer was rendered as a child of `<header className="sticky top-0 z-40 …">`. A `position: sticky` element with a `z-index` creates its own stacking context, which traps the drawer's `position: fixed; z-50` inside that context. Globally, the drawer ends up bound to the header's `z-40` layer, while the unrelated bottom navigation (`<nav className="fixed inset-x-0 bottom-0 z-40 …">`) lives in the root stacking context at `z-40`. With equal global `z`, source order wins — the bottom nav paints later and the drawer never reaches the foreground.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
Move the drawer JSX out of `<header>` and render it as a sibling at the layout root, so its `fixed`/`z-50` positioning lives in the root stacking context and stacks above both the header and the bottom nav.
|
||||||
|
|
||||||
|
### Files Modified
|
||||||
|
- `src/layouts/PublicLayout.tsx` — relocated the `{open ? (…) : null}` mobile drawer block from inside `<header>` to immediately after `</header>`. Logic unchanged; the `menuRef`, click-outside handler, body scroll lock, and inner nav/CTA structure all keep working because they reference the element by ref/state, not by DOM position.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
- `npx tsc --noEmit` — clean.
|
||||||
|
- `npm run format` then `npm run format:check` — clean.
|
||||||
|
- `npm test` — 49/49 passing.
|
||||||
|
- Expected on device: tapping the hamburger now reveals the dark full-screen drawer with the 5 nav items, active item in gold, and the bottom `链接钱包` CTA (or the connected-wallet pill).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This is the same class of issue any future fullscreen overlay should avoid: do not nest `position: fixed` overlays inside a `position: sticky + z-index` ancestor. Either render them at the layout root or use a React Portal.
|
||||||
|
- `position: sticky` *without* `z-index` does not create a stacking context, but adding any `z-index` to it does. The header here uses both because it needs to sit above the content while scrolled.
|
||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ChevronDown, Menu, Search as SearchIcon, X } from "lucide-react";
|
||||||
ChevronDown,
|
|
||||||
Heart,
|
|
||||||
Menu,
|
|
||||||
Search as SearchIcon,
|
|
||||||
X,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { AnimatePresence, m } from "framer-motion";
|
import { AnimatePresence, m } from "framer-motion";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom";
|
import { Link, useLocation, useNavigate, useOutlet } from "react-router-dom";
|
||||||
@@ -82,10 +76,6 @@ function navClassName(active: boolean) {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function mobileMenuNavClassName(active: boolean) {
|
|
||||||
return `${navClassName(active)} w-fit justify-self-start`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dropdownAnimationClass = "ark-header-popover-enter";
|
const dropdownAnimationClass = "ark-header-popover-enter";
|
||||||
const headerMenuAnimationClass = "ark-header-menu-enter";
|
const headerMenuAnimationClass = "ark-header-menu-enter";
|
||||||
|
|
||||||
@@ -493,6 +483,33 @@ export function PublicLayout() {
|
|||||||
};
|
};
|
||||||
}, [mobileSearchOpen]);
|
}, [mobileSearchOpen]);
|
||||||
|
|
||||||
|
// Lock background scroll while the full-screen mobile menu drawer is open.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) return;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const body = document.body;
|
||||||
|
const prev = {
|
||||||
|
position: body.style.position,
|
||||||
|
top: body.style.top,
|
||||||
|
left: body.style.left,
|
||||||
|
right: body.style.right,
|
||||||
|
width: body.style.width,
|
||||||
|
};
|
||||||
|
body.style.position = "fixed";
|
||||||
|
body.style.top = `-${scrollY}px`;
|
||||||
|
body.style.left = "0";
|
||||||
|
body.style.right = "0";
|
||||||
|
body.style.width = "100%";
|
||||||
|
return () => {
|
||||||
|
body.style.position = prev.position;
|
||||||
|
body.style.top = prev.top;
|
||||||
|
body.style.left = prev.left;
|
||||||
|
body.style.right = prev.right;
|
||||||
|
body.style.width = prev.width;
|
||||||
|
window.scrollTo(0, scrollY);
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[100dvh] flex-col bg-ark-bg">
|
<div className="flex min-h-[100dvh] flex-col bg-ark-bg">
|
||||||
<DocumentMeta />
|
<DocumentMeta />
|
||||||
@@ -691,67 +708,63 @@ export function PublicLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{open ? (
|
{open ? (
|
||||||
<div
|
<div
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
className={`${headerMenuAnimationClass} fixed inset-x-0 top-[64px] z-50 grid gap-2 bg-[#08070c] px-4 py-3 shadow-2xl shadow-black/50 min-[440px]:px-5 sm:px-6 md:top-[70px] md:px-9 min-[1000px]:hidden`}
|
className={`${headerMenuAnimationClass} fixed inset-x-0 bottom-0 top-[64px] z-50 flex flex-col bg-ark-bg md:top-[70px] min-[1000px]:hidden`}
|
||||||
>
|
>
|
||||||
|
<nav className="flex-1 overflow-y-auto px-5 pt-2">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
to: lp("/browse"),
|
||||||
|
label: t("all"),
|
||||||
|
active: na("browseAll"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: lp("/categories"),
|
||||||
|
label: t("categories"),
|
||||||
|
active: na("categories"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: lp("/official-recommendations"),
|
||||||
|
label: t("official"),
|
||||||
|
active: na("browseRecommended"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: lp("/browse?sort=latest"),
|
||||||
|
label: t("latest"),
|
||||||
|
active: na("browseLatest"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: popularHref,
|
||||||
|
label: t("popular"),
|
||||||
|
active: na("browsePopular"),
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
).map((item) => (
|
||||||
<Link
|
<Link
|
||||||
to={lp("/browse")}
|
key={item.to}
|
||||||
className={mobileMenuNavClassName(na("browseAll"))}
|
to={item.to}
|
||||||
aria-current={na("browseAll") ? "page" : undefined}
|
aria-current={item.active ? "page" : undefined}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
|
className={`flex h-[68px] items-center border-b border-[#2B2B37] text-[15px] font-medium leading-[20px] outline-none transition-colors focus-visible:text-ark-gold ${
|
||||||
|
item.active
|
||||||
|
? "text-ark-gold"
|
||||||
|
: "text-[#A8A9AE] [@media(hover:hover)]:hover:text-ark-gold"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{t("all")}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
))}
|
||||||
to={lp("/categories")}
|
</nav>
|
||||||
className={mobileMenuNavClassName(na("categories"))}
|
<div className="px-5 pb-[max(env(safe-area-inset-bottom),20px)] pt-4">
|
||||||
aria-current={na("categories") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("categories")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={lp("/official-recommendations")}
|
|
||||||
className={mobileMenuNavClassName(na("browseRecommended"))}
|
|
||||||
aria-current={na("browseRecommended") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("official")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={lp("/browse?sort=latest")}
|
|
||||||
className={mobileMenuNavClassName(na("browseLatest"))}
|
|
||||||
aria-current={na("browseLatest") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("latest")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={popularHref}
|
|
||||||
className={mobileMenuNavClassName(na("browsePopular"))}
|
|
||||||
aria-current={na("browsePopular") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("popular")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={lp("/favorites")}
|
|
||||||
className={`${mobileMenuNavClassName(na("favorites"))} flex items-center gap-2`}
|
|
||||||
aria-current={na("favorites") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<Heart size={16} strokeWidth={2} />
|
|
||||||
{t("favorites")}
|
|
||||||
</Link>
|
|
||||||
<div className="mt-2 w-full max-w-xs">
|
|
||||||
<WalletButton compact onOpenLogin={() => setOpen(false)} />
|
<WalletButton compact onOpenLogin={() => setOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
|
||||||
|
|
||||||
{mobileSearchOpen ? (
|
{mobileSearchOpen ? (
|
||||||
<SearchPanel
|
<SearchPanel
|
||||||
|
|||||||
Reference in New Issue
Block a user