fix: make auth dropdown controlled in React to prevent password manager focus-loss from closing it
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m41s

This commit is contained in:
Kroonk
2026-05-21 11:35:21 +02:00
parent a21e2df6c4
commit 85f50c5530
2 changed files with 42 additions and 13 deletions

View File

@@ -2,6 +2,7 @@
import { IconUser, IconShield, IconUserCircle, IconSettings2, IconLogout } from "@tabler/icons-react";
import Link from "next/link";
import { useState, useEffect, useRef } from "react";
import NotificationPanel from "./NotificationPanel";
import ThemeToggle from "./ThemeToggle";
import { useSession } from "@/context/session";
@@ -11,10 +12,29 @@ import AuthDropdownContent from "./auth/AuthDropdownContent";
export default function Header() {
const { user, setUser } = useSession();
const [isUserMenuOpen, setIsUserMenuOpen] = useState(false);
const [isAuthMenuOpen, setIsAuthMenuOpen] = useState(false);
const userMenuRef = useRef<HTMLDivElement>(null);
const authMenuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) {
setIsUserMenuOpen(false);
}
if (authMenuRef.current && !authMenuRef.current.contains(event.target as Node)) {
setIsAuthMenuOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
async function handleLogout() {
await logout();
setUser(null);
setIsUserMenuOpen(false);
}
return (
@@ -47,28 +67,31 @@ export default function Header() {
<ThemeToggle />
{user?.id && typeof user.id === "number" ? (
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div ref={userMenuRef} className={`dropdown dropdown-end ${isUserMenuOpen ? "dropdown-open" : ""}`}>
<button
onClick={() => setIsUserMenuOpen(!isUserMenuOpen)}
className="btn btn-ghost btn-circle avatar"
>
<div className="w-10 rounded-full">
<IconUser className="m-auto block h-full" />
</div>
</label>
<div tabIndex={0} className="dropdown-content z-50 mt-3 w-80 rounded-box bg-base-100 p-3 shadow-lg">
</button>
<div className="dropdown-content z-50 mt-3 w-80 rounded-box bg-base-100 p-3 shadow-lg border border-base-300 dark:border-neutral">
<div className="px-2 pb-2 font-semibold">{user.name}</div>
<ul className="menu menu-sm p-0">
<li>
<Link href={`/user/${user.name}`}>
<Link href={`/user/${user.name}`} onClick={() => setIsUserMenuOpen(false)}>
<IconUserCircle size={16} /> Profil
</Link>
</li>
<li>
<Link href="/settings">
<Link href="/settings" onClick={() => setIsUserMenuOpen(false)}>
<IconSettings2 size={16} /> Einstellungen
</Link>
</li>
{user.role === "admin" && (
<li>
<Link href="/admin">
<Link href="/admin" onClick={() => setIsUserMenuOpen(false)}>
<IconShield size={16} /> Admin-Panel
</Link>
</li>
@@ -90,14 +113,17 @@ export default function Header() {
</div>
</div>
) : (
<div className="dropdown dropdown-end">
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
<div ref={authMenuRef} className={`dropdown dropdown-end ${isAuthMenuOpen ? "dropdown-open" : ""}`}>
<button
onClick={() => setIsAuthMenuOpen(!isAuthMenuOpen)}
className="btn btn-ghost btn-circle avatar"
>
<div className="w-10 rounded-full">
<IconUser className="m-auto block h-full" />
</div>
</label>
<div tabIndex={0} className="dropdown-content z-50 mt-3 w-80 rounded-box bg-base-100 p-4 shadow-lg border border-base-300 dark:border-neutral">
<AuthDropdownContent />
</button>
<div className="dropdown-content z-50 mt-3 w-80 rounded-box bg-base-100 p-4 shadow-lg border border-base-300 dark:border-neutral">
<AuthDropdownContent onClose={() => setIsAuthMenuOpen(false)} />
</div>
</div>
)}

View File

@@ -10,7 +10,7 @@ import Guest from "./Guest";
import Login from "./Login";
import Register from "./Register";
export default function AuthDropdownContent() {
export default function AuthDropdownContent({ onClose }: { onClose?: () => void }) {
const session = useContext(SessionContext);
const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest");
const [serverMessage, setServerMessage] = useState<string | null>(null);
@@ -29,6 +29,9 @@ export default function AuthDropdownContent() {
}, []);
function closeDropdown() {
if (onClose) {
onClose();
}
if (typeof document !== "undefined" && document.activeElement) {
(document.activeElement as HTMLElement).blur();
}