'use client'; import React, { useState, useRef, useEffect, createContext, useContext } from 'react'; interface DropdownContextType { open: boolean; dropup: boolean; toggle: () => void; close: () => void; } const DropdownContext = createContext(null); export interface DropdownProps { children: React.ReactNode; dropup?: boolean; className?: string; } export const Dropdown: React.FC = ({ children, dropup = false, className = '', }) => { const [open, setOpen] = useState(false); const dropdownRef = useRef(null); const toggle = () => setOpen((prev) => !prev); const close = () => setOpen(false); useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { close(); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') close(); }; if (open) { document.addEventListener('mousedown', handleClickOutside); document.addEventListener('keydown', handleKeyDown); } return () => { document.removeEventListener('mousedown', handleClickOutside); document.removeEventListener('keydown', handleKeyDown); }; }, [open]); const classes = [ 'qhr-dropdown', dropup && 'qhr-dropdown--dropup dropup', className, ].filter(Boolean).join(' '); return (
{children}
); }; export interface DropdownTriggerProps { children: React.ReactElement<{ onClick?: (e: React.MouseEvent) => void; 'aria-haspopup'?: string; 'aria-expanded'?: boolean; className?: string; }>; noCaret?: boolean; } export const DropdownTrigger: React.FC = ({ children, noCaret = false }) => { const ctx = useContext(DropdownContext); if (!ctx) throw new Error('DropdownTrigger must be used inside Dropdown'); const existingClass = children.props.className || ''; const triggerClass = [existingClass, noCaret && 'qhr-dropdown-toggle--no-caret no-caret'].filter(Boolean).join(' '); return React.cloneElement(children, { onClick: (e: React.MouseEvent) => { children.props.onClick?.(e); ctx.toggle(); }, className: triggerClass, 'aria-haspopup': 'menu', 'aria-expanded': ctx.open, }); }; export interface DropdownMenuProps { children: React.ReactNode; align?: 'start' | 'end'; className?: string; } export const DropdownMenu: React.FC = ({ children, align = 'start', className = '', }) => { const ctx = useContext(DropdownContext); if (!ctx || !ctx.open) return null; const classes = [ 'qhr-dropdown-menu', align === 'end' && 'qhr-dropdown-menu--end dropdown-menu-end', className, ].filter(Boolean).join(' '); return (
{children}
); }; export interface DropdownItemProps { children: React.ReactNode; onClick?: () => void; destructive?: boolean; disabled?: boolean; active?: boolean; icon?: React.ReactNode; badge?: React.ReactNode; inset?: boolean; className?: string; } export const DropdownItem: React.FC = ({ children, onClick, destructive = false, disabled = false, active = false, icon, badge, inset = false, className = '', }) => { const ctx = useContext(DropdownContext); const classes = [ 'qhr-dropdown-item', destructive && 'qhr-dropdown-item--destructive', disabled && 'is-disabled disabled', active && 'is-active active', inset && 'qhr-dropdown-item--inset', className, ].filter(Boolean).join(' '); return ( ); }; export const DropdownHeader: React.FC<{ children: React.ReactNode; inset?: boolean; className?: string }> = ({ children, inset = false, className = '', }) => (
{children}
); export const DropdownSeparator: React.FC<{ className?: string }> = ({ className = '' }) => (
); export const DropdownDivider: React.FC<{ className?: string }> = ({ className = '' }) => (
); export const DropdownShortcut: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '', }) => ( {children} ); export interface DropdownCheckboxItemProps { children: React.ReactNode; checked?: boolean; onCheckedChange?: (checked: boolean) => void; disabled?: boolean; className?: string; } export const DropdownCheckboxItem: React.FC = ({ children, checked = false, onCheckedChange, disabled = false, className = '', }) => { const classes = [ 'qhr-dropdown-item', 'qhr-dropdown-item--checkbox', disabled && 'is-disabled disabled', className, ].filter(Boolean).join(' '); return ( ); }; interface DropdownRadioContextType { value?: string; onValueChange?: (val: string) => void; } const DropdownRadioContext = createContext(null); export const DropdownRadioGroup: React.FC<{ value?: string; onValueChange?: (val: string) => void; children: React.ReactNode; className?: string; }> = ({ value, onValueChange, children, className = '' }) => (
{children}
); export interface DropdownRadioItemProps { value: string; children: React.ReactNode; disabled?: boolean; className?: string; } export const DropdownRadioItem: React.FC = ({ value, children, disabled = false, className = '', }) => { const groupCtx = useContext(DropdownRadioContext); const checked = groupCtx?.value === value; const classes = [ 'qhr-dropdown-item', 'qhr-dropdown-item--radio', disabled && 'is-disabled disabled', className, ].filter(Boolean).join(' '); return ( ); }; export const DropdownSub: React.FC<{ children: React.ReactNode; className?: string }> = ({ children, className = '', }) => { const [subOpen, setSubOpen] = useState(false); return (
setSubOpen(true)} onMouseLeave={() => setSubOpen(false)} > {React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child as React.ReactElement, { subOpen, setSubOpen }); } return child; })}
); }; export const DropdownSubTrigger: React.FC<{ children: React.ReactNode; icon?: React.ReactNode; inset?: boolean; className?: string; subOpen?: boolean; setSubOpen?: (open: boolean) => void; }> = ({ children, icon, inset = false, className = '', subOpen = false, setSubOpen }) => { const classes = [ 'qhr-dropdown-item', inset && 'qhr-dropdown-item--inset', className, ].filter(Boolean).join(' '); return ( ); }; export const DropdownSubContent: React.FC<{ children: React.ReactNode; className?: string; subOpen?: boolean; }> = ({ children, className = '', subOpen = false }) => { if (!subOpen) return null; return (
{children}
); };