fix(nav): portal account popover to body (stacking + click-through)
The nav's .glass backdrop-filter makes it a containing block for fixed descendants and a stacking context, so the account popover's fixed inset-0 backdrop only covered the nav (not the viewport) and the popover sat below the main content — controls behind it stayed clickable. Portal the popover + its dismiss backdrop to <body> with fixed coords computed from the trigger rect, so it's truly top-most (z-50) and blocks clicks underneath.
This commit is contained in:
parent
2442e462c4
commit
8b39cea5f3
1 changed files with 29 additions and 9 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BarChart3,
|
||||
|
|
@ -35,6 +36,19 @@ export default function NavSidebar({
|
|||
() => localStorage.getItem("siftlode.navCollapsed") === "1"
|
||||
);
|
||||
const [acctOpen, setAcctOpen] = useState(false);
|
||||
const acctBtnRef = useRef<HTMLButtonElement | null>(null);
|
||||
// Popover position (fixed, viewport coords). It's portaled to <body> so it escapes the
|
||||
// nav's backdrop-filter, which would otherwise trap fixed positioning + stacking and let
|
||||
// clicks fall through to the controls behind it.
|
||||
const [acctPos, setAcctPos] = useState<{ left: number; bottom: number }>({ left: 0, bottom: 0 });
|
||||
|
||||
function toggleAccount() {
|
||||
if (!acctOpen) {
|
||||
const r = acctBtnRef.current?.getBoundingClientRect();
|
||||
if (r) setAcctPos({ left: r.right + 8, bottom: window.innerHeight - r.bottom });
|
||||
}
|
||||
setAcctOpen((o) => !o);
|
||||
}
|
||||
|
||||
function toggleCollapsed() {
|
||||
setCollapsed((c) => {
|
||||
|
|
@ -128,7 +142,8 @@ export default function NavSidebar({
|
|||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setAcctOpen((o) => !o)}
|
||||
ref={acctBtnRef}
|
||||
onClick={toggleAccount}
|
||||
title={collapsed ? name : undefined}
|
||||
className={`${rowBase} ${collapsed ? "justify-center px-0" : ""} text-muted hover:text-fg hover:bg-card`}
|
||||
>
|
||||
|
|
@ -140,10 +155,14 @@ export default function NavSidebar({
|
|||
{!collapsed && <span className="truncate flex-1 text-left">{name}</span>}
|
||||
</button>
|
||||
|
||||
{acctOpen && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-20" onClick={() => setAcctOpen(false)} />
|
||||
<div className="glass-menu absolute left-full bottom-0 ml-2 w-60 rounded-xl p-3 z-30 animate-[popIn_0.16s_ease]">
|
||||
{acctOpen &&
|
||||
createPortal(
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setAcctOpen(false)} />
|
||||
<div
|
||||
style={{ position: "fixed", left: acctPos.left, bottom: acctPos.bottom }}
|
||||
className="glass-menu w-60 rounded-xl p-3 z-50 animate-[popIn_0.16s_ease]"
|
||||
>
|
||||
<div className="flex items-center gap-3 pb-3 border-b border-border">
|
||||
<AvatarImg
|
||||
src={me.avatar_url}
|
||||
|
|
@ -180,9 +199,10 @@ export default function NavSidebar({
|
|||
{t("header.account.signOut")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue