'use client'; import * as React from 'react'; import { Menu, MenuButton, MenuItem, MenuItems, Portal, } from '@headlessui/react'; import { ChevronDownIcon, EllipsisVerticalIcon, } from '@heroicons/react/20/solid'; import clsx from 'clsx'; type DropdownTone = 'default' | 'danger'; export type DropdownItem = { id?: string; label: string; href?: string; onClick?: () => void; icon?: React.ReactNode; tone?: DropdownTone; disabled?: boolean; }; export type DropdownSection = { id?: string; label?: string; items: DropdownItem[]; }; // 🔹 NEU: 'input' export type DropdownTriggerVariant = 'button' | 'icon' | 'input'; export interface DropdownProps { label?: string; ariaLabel?: string; align?: 'left' | 'right'; triggerVariant?: DropdownTriggerVariant; header?: React.ReactNode; sections: DropdownSection[]; triggerClassName?: string; menuClassName?: string; /** Dropdown komplett deaktivieren (Trigger klickt nicht) */ disabled?: boolean; /** Nur für triggerVariant="input": gesteuerter Eingabewert */ inputValue?: string; inputPlaceholder?: string; onInputChange?: (value: string) => void; } /* ───────── interne Helfer ───────── */ const itemBaseClasses = 'block px-4 py-2 text-sm ' + 'text-gray-700 dark:text-gray-300 ' + 'hover:bg-gray-100 hover:text-gray-900 ' + 'dark:hover:bg-white/5 dark:hover:text-white ' + 'focus:outline-none'; const itemWithIconClasses = 'group flex items-center gap-x-3 px-4 py-2 text-sm ' + 'text-gray-700 dark:text-gray-300 ' + 'hover:bg-gray-100 hover:text-gray-900 ' + 'dark:hover:bg-white/5 dark:hover:text-white ' + 'focus:outline-none'; const iconClasses = 'flex size-5 shrink-0 items-center justify-center text-gray-400 ' + 'group-hover:text-gray-500 ' + 'dark:text-gray-500 dark:group-hover:text-white'; const toneClasses: Record = { default: '', danger: 'text-rose-600 data-focus:text-rose-700 dark:text-rose-400 dark:data-focus:text-rose-300', }; function renderItemContent(item: DropdownItem) { const hasIcon = !!item.icon; if (hasIcon) { return ( {item.label} ); } return ( {item.label} ); } /* ───────── Spezielle Variante: Textfeld + Dropdown (OHNE Menu) ───────── */ function InputDropdown({ label = 'Options', ariaLabel = 'Open options', align = 'left', header, sections, triggerClassName, menuClassName, disabled = false, inputValue, inputPlaceholder, onInputChange, }: DropdownProps) { const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); const menuRef = React.useRef(null); // 🔹 NEU const [position, setPosition] = React.useState<{ top: number; left: number; width: number; } | null>(null); const hasDividers = sections.length > 1; const effectivePlaceholder = inputPlaceholder ?? label; // Position neu berechnen, wenn geöffnet React.useEffect(() => { if (!open || !anchorRef.current) return; const rect = anchorRef.current.getBoundingClientRect(); const scrollX = window.pageXOffset || document.documentElement.scrollLeft; const scrollY = window.pageYOffset || document.documentElement.scrollTop; const leftBase = rect.left + scrollX; setPosition({ top: rect.bottom + scrollY + 4, left: leftBase, width: rect.width, }); }, [open, align]); // Klick außerhalb / ESC schließt das Dropdown React.useEffect(() => { if (!open) return; function handleClickOutside(ev: MouseEvent) { const target = ev.target as Node; // 🔹 Wenn Klick im Input-Bereich oder im Menü: NICHT schließen if ( (anchorRef.current && anchorRef.current.contains(target)) || (menuRef.current && menuRef.current.contains(target)) ) { return; } setOpen(false); } function handleKey(ev: KeyboardEvent) { if (ev.key === 'Escape') { setOpen(false); } } window.addEventListener('mousedown', handleClickOutside); window.addEventListener('keydown', handleKey); return () => { window.removeEventListener('mousedown', handleClickOutside); window.removeEventListener('keydown', handleKey); }; }, [open]); return (
{ onInputChange?.(e.target.value); if (!disabled && !open) setOpen(true); }} onFocus={() => { if (!disabled) setOpen(true); }} className="block w-full rounded-md border-0 bg-gray-900/40 px-2.5 py-1.5 pr-8 text-sm text-gray-100 shadow-xs ring-1 ring-inset ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-indigo-500 focus:outline-none dark:bg-gray-900/60 dark:ring-gray-600" />
{open && position && !disabled && (
{header &&
{header}
} {sections.map((section, sectionIndex) => (
{section.label && (
{section.label}
)} {section.items.map((item, itemIndex) => { const key = item.id ?? `${sectionIndex}-${itemIndex}`; const handleClick = () => { if (item.disabled) return; item.onClick?.(); // 🔹 Auswahl nach außen melden setOpen(false); // 🔹 Danach Dropdown schließen }; if (item.href) { return ( {renderItemContent(item)} ); } return ( ); })}
))}
)}
); } /* ───────── Standard-Dropdown (Menu) für Button/Icon ───────── */ export function Dropdown(props: DropdownProps) { const { label = 'Options', ariaLabel = 'Open options', align = 'right', triggerVariant = 'button', header, sections, triggerClassName, menuClassName, disabled = false, } = props; const triggerIsInput = triggerVariant === 'input'; const triggerIsIcon = triggerVariant === 'icon'; const triggerIsButton = triggerVariant === 'button'; // 🔹 Spezialfall: Textfeld + Dropdown (ohne Menu) if (triggerIsInput) { return ( ); } const hasDividers = sections.length > 1; return ( {({ open }) => { const buttonRef = React.useRef(null); const [position, setPosition] = React.useState<{ top: number; left: number; } | null>(null); React.useEffect(() => { if (!open || !buttonRef.current) return; const rect = buttonRef.current.getBoundingClientRect(); const scrollX = window.pageXOffset || document.documentElement.scrollLeft; const scrollY = window.pageYOffset || document.documentElement.scrollTop; const left = align === 'left' ? rect.left + scrollX : rect.right + scrollX; setPosition({ top: rect.bottom + scrollY + 4, left, }); }, [open, align]); return ( <> {triggerIsButton ? ( <> {label} {open && position && !disabled && ( {header && (
{header}
)} {sections.map((section, sectionIndex) => (
{section.label && (
{section.label}
)} {section.items.map((item, itemIndex) => { const key = item.id ?? `${sectionIndex}-${itemIndex}`; return ( {item.href ? ( {renderItemContent(item)} ) : ( )} ); })}
))}
)} ); }}
); } export default Dropdown;