feat: Added hover-to-open with 150ms leave delay and matchMedia desktop…

- "frontend/src/components/AdminDropdown.tsx"

GSD-Task: S05/T01
This commit is contained in:
jlightner 2026-04-03 04:41:04 +00:00
parent 82998c6d8d
commit b4bea10067

View file

@ -1,9 +1,48 @@
import { useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { Link } from "react-router-dom";
const DESKTOP_MQ = "(min-width: 769px)";
const LEAVE_DELAY_MS = 150;
export default function AdminDropdown() {
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const isDesktopRef = useRef(false);
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// Track desktop breakpoint via matchMedia
useEffect(() => {
const mql = window.matchMedia(DESKTOP_MQ);
isDesktopRef.current = mql.matches;
const onChange = (e: MediaQueryListEvent) => {
isDesktopRef.current = e.matches;
};
mql.addEventListener("change", onChange);
return () => mql.removeEventListener("change", onChange);
}, []);
// Clear leave timer on unmount
useEffect(() => {
return () => {
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
};
}, []);
const handleMouseEnter = useCallback(() => {
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
leaveTimerRef.current = null;
}
if (isDesktopRef.current) setOpen(true);
}, []);
const handleMouseLeave = useCallback(() => {
if (!isDesktopRef.current) return;
leaveTimerRef.current = setTimeout(() => {
setOpen(false);
leaveTimerRef.current = null;
}, LEAVE_DELAY_MS);
}, []);
// Close on outside click
useEffect(() => {
@ -31,7 +70,12 @@ export default function AdminDropdown() {
}, [open]);
return (
<div className="admin-dropdown" ref={dropdownRef}>
<div
className="admin-dropdown"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
className="admin-dropdown__trigger"
onClick={() => setOpen((prev) => !prev)}