mirror of
https://github.com/Noooste/garage-ui.git
synced 2026-09-04 03:05:41 +00:00
add initial project setup
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DropdownMenuContextValue {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
triggerRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const DropdownMenuContext = React.createContext<DropdownMenuContextValue | undefined>(undefined);
|
||||
|
||||
function useDropdownMenu() {
|
||||
const context = React.useContext(DropdownMenuContext);
|
||||
if (!context) {
|
||||
throw new Error('useDropdownMenu must be used within a DropdownMenu');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
interface DropdownMenuProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const DropdownMenu: React.FC<DropdownMenuProps> = ({ children }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const triggerRef = React.useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<DropdownMenuContext.Provider value={{ open, setOpen, triggerRef }}>
|
||||
<div className="relative inline-block text-left">{children}</div>
|
||||
</DropdownMenuContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const DropdownMenuTrigger = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
>(({ onClick, ...props }, ref) => {
|
||||
const { open, setOpen, triggerRef } = useDropdownMenu();
|
||||
|
||||
// Merge the forwarded ref with the context triggerRef
|
||||
React.useImperativeHandle(ref, () => triggerRef.current as HTMLButtonElement);
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={(e) => {
|
||||
setOpen(!open);
|
||||
onClick?.(e);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
DropdownMenuTrigger.displayName = 'DropdownMenuTrigger';
|
||||
|
||||
interface DropdownMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
align?: 'start' | 'end' | 'center';
|
||||
}
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
||||
({ className, children, align = 'start', ...props }, _ref) => {
|
||||
const { open, setOpen, triggerRef } = useDropdownMenu();
|
||||
const contentRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const isClickOnTrigger = triggerRef.current?.contains(event.target as Node);
|
||||
const isClickOnContent = contentRef.current?.contains(event.target as Node);
|
||||
|
||||
if (!isClickOnContent && !isClickOnTrigger) {
|
||||
setOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [open, setOpen, triggerRef]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const alignmentClasses = {
|
||||
start: 'left-0',
|
||||
end: 'right-0',
|
||||
center: 'left-1/2 -translate-x-1/2',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={contentRef}
|
||||
style={{ backgroundColor: 'hsl(var(--popover))' }}
|
||||
className={cn(
|
||||
'absolute z-50 mt-2 w-56 origin-top-right rounded-md text-popover-foreground shadow-lg ring-1 ring-border border border-border focus:outline-none',
|
||||
alignmentClasses[align],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="py-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
DropdownMenuContent.displayName = 'DropdownMenuContent';
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, onClick, ...props }, ref) => {
|
||||
const { setOpen } = useDropdownMenu();
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
onClick?.(e);
|
||||
setOpen(false);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
DropdownMenuItem.displayName = 'DropdownMenuItem';
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
|
||||
)
|
||||
);
|
||||
DropdownMenuSeparator.displayName = 'DropdownMenuSeparator';
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
};
|
||||
Reference in New Issue
Block a user