95 lines
2.6 KiB
TypeScript
95 lines
2.6 KiB
TypeScript
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface PopoverContextValue {
|
|
open: boolean;
|
|
setOpen: (open: boolean) => void;
|
|
}
|
|
|
|
const PopoverContext = React.createContext<PopoverContextValue | null>(null);
|
|
|
|
export interface PopoverProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
const Popover: React.FC<PopoverProps> = ({ children }) => {
|
|
const [open, setOpen] = React.useState(false);
|
|
const popoverRef = React.useRef<HTMLDivElement>(null);
|
|
|
|
React.useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (popoverRef.current && !popoverRef.current.contains(event.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
if (open) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
}
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<PopoverContext.Provider value={{ open, setOpen }}>
|
|
<div ref={popoverRef} className="relative inline-block text-left">
|
|
{children}
|
|
</div>
|
|
</PopoverContext.Provider>
|
|
);
|
|
};
|
|
|
|
export interface PopoverTriggerProps {
|
|
children: React.ReactElement;
|
|
}
|
|
|
|
const PopoverTrigger: React.FC<PopoverTriggerProps> = ({ children }) => {
|
|
const context = React.useContext(PopoverContext);
|
|
if (!context) throw new Error('PopoverTrigger must be used within Popover');
|
|
|
|
return React.cloneElement(children, {
|
|
onClick: (e: React.MouseEvent) => {
|
|
children.props.onClick?.(e);
|
|
context.setOpen(!context.open);
|
|
},
|
|
'aria-expanded': context.open,
|
|
});
|
|
};
|
|
|
|
export interface PopoverContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
align?: 'left' | 'right' | 'center';
|
|
}
|
|
|
|
const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
|
|
({ className, align = 'center', children, ...props }, ref) => {
|
|
const context = React.useContext(PopoverContext);
|
|
if (!context) throw new Error('PopoverContent must be used within Popover');
|
|
|
|
if (!context.open) return null;
|
|
|
|
const alignClasses = {
|
|
left: 'left-0 origin-top-left',
|
|
right: 'right-0 origin-top-right',
|
|
center: 'left-1/2 -translate-x-1/2 origin-top',
|
|
};
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={cn(
|
|
'absolute z-50 mt-2 w-72 rounded-xl border border-border bg-popover p-4 text-popover-foreground shadow-dropdown outline-none animate-scale-in',
|
|
alignClasses[align],
|
|
className
|
|
)}
|
|
{...props}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
);
|
|
PopoverContent.displayName = 'PopoverContent';
|
|
|
|
export { Popover, PopoverTrigger, PopoverContent };
|