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

View File

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