Files
Psychotoxical-psysonic/src/hooks/useSidebarLibraryDropdown.ts
T
Frank Stellmacher b8a9fe860e refactor(sidebar): H2 — extract 5 hooks + 5 sub-components (#664)
* refactor(sidebar): H2.1 — extract helpers + constants

* refactor(sidebar): H2.2 — extract useSidebarNewReleasesUnread hook

* refactor(sidebar): H2.3 — extract useSidebarNavDnd hook

* refactor(sidebar): H2.4 — extract 3 hooks (LibraryDropdown + ScrollVisible + PerfProbe)

* refactor(sidebar): H2.5 — extract SidebarPerfProbeModal component

* refactor(sidebar): H2.5–H2.8 — split JSX into per-block components

Sidebar.tsx 938 → 271 LOC. All resulting files under the 400-LOC guideline:
  components/sidebar/SidebarLibraryPicker.tsx       96
  components/sidebar/SidebarActiveJobs.tsx          58
  components/sidebar/SidebarNavBody.tsx            293
  components/sidebar/SidebarPerfProbeModal.tsx     259
  components/sidebar/SidebarPerfProbePhase2.tsx    176
2026-05-13 21:42:18 +02:00

66 lines
2.1 KiB
TypeScript

import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
interface DropdownRect {
top: number;
left: number;
width: number;
}
interface Result {
libraryDropdownOpen: boolean;
setLibraryDropdownOpen: (open: boolean) => void;
dropdownRect: DropdownRect;
libraryTriggerRef: React.RefObject<HTMLButtonElement | null>;
}
export function useSidebarLibraryDropdown(): Result {
const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false);
const [dropdownRect, setDropdownRect] = useState<DropdownRect>({ top: 0, left: 0, width: 0 });
const libraryTriggerRef = useRef<HTMLButtonElement>(null);
const updateDropdownPosition = useCallback(() => {
const el = libraryTriggerRef.current;
if (!el) return;
const r = el.getBoundingClientRect();
setDropdownRect({
top: r.bottom + 4,
left: r.left,
width: r.width,
});
}, []);
useLayoutEffect(() => {
if (!libraryDropdownOpen) return;
updateDropdownPosition();
const onWin = () => updateDropdownPosition();
window.addEventListener('resize', onWin);
window.addEventListener('scroll', onWin, true);
return () => {
window.removeEventListener('resize', onWin);
window.removeEventListener('scroll', onWin, true);
};
}, [libraryDropdownOpen, updateDropdownPosition]);
useEffect(() => {
if (!libraryDropdownOpen) return;
const onDown = (e: MouseEvent) => {
const t = e.target as Node;
if (libraryTriggerRef.current?.contains(t)) return;
const panel = document.querySelector('.nav-library-dropdown-panel');
if (panel?.contains(t)) return;
setLibraryDropdownOpen(false);
};
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') setLibraryDropdownOpen(false);
};
document.addEventListener('mousedown', onDown);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('mousedown', onDown);
document.removeEventListener('keydown', onKey);
};
}, [libraryDropdownOpen]);
return { libraryDropdownOpen, setLibraryDropdownOpen, dropdownRect, libraryTriggerRef };
}