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:
npeter83 2026-06-16 01:46:32 +02:00
parent 74491991d7
commit 5bb85b039a

View file

@ -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 && (
{acctOpen &&
createPortal(
<>
<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]">
<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}
@ -181,7 +200,8 @@ export default function NavSidebar({
</button>
</div>
</div>
</>
</>,
document.body
)}
</div>
</div>