mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
123 lines
3.0 KiB
TypeScript
123 lines
3.0 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react';
|
|
import { clsx } from 'clsx';
|
|
|
|
export interface PopoverProps {
|
|
trigger: React.ReactNode;
|
|
content: React.ReactNode;
|
|
open?: boolean;
|
|
onOpenChange?: (open: boolean) => void;
|
|
align?: 'start' | 'center' | 'end';
|
|
side?: 'top' | 'bottom' | 'left' | 'right';
|
|
className?: string;
|
|
triggerClassName?: string;
|
|
modal?: boolean;
|
|
}
|
|
|
|
const alignClasses = {
|
|
start: 'start-0',
|
|
center: 'left-1/2 -translate-x-1/2',
|
|
end: 'end-0',
|
|
};
|
|
|
|
const sideClasses = {
|
|
top: 'bottom-full mb-2',
|
|
bottom: 'top-full mt-2',
|
|
left: 'right-full mr-2',
|
|
right: 'left-full ml-2',
|
|
};
|
|
|
|
/**
|
|
* Popover component - contextual popup (different from Tooltip)
|
|
*/
|
|
export const Popover = ({
|
|
trigger,
|
|
content,
|
|
open: controlledOpen,
|
|
onOpenChange,
|
|
align = 'center',
|
|
side = 'bottom',
|
|
className,
|
|
triggerClassName,
|
|
modal = false,
|
|
}: PopoverProps) => {
|
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
const popoverRef = useRef<HTMLDivElement>(null);
|
|
const triggerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isControlled = controlledOpen !== undefined;
|
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
|
|
const setOpen = (value: boolean) => {
|
|
if (!isControlled) {
|
|
setInternalOpen(value);
|
|
}
|
|
onOpenChange?.(value);
|
|
};
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (
|
|
popoverRef.current &&
|
|
triggerRef.current &&
|
|
!popoverRef.current.contains(event.target as Node) &&
|
|
!triggerRef.current.contains(event.target as Node)
|
|
) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
const handleEscape = (event: KeyboardEvent) => {
|
|
if (event.key === 'Escape' && open) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
|
|
if (open) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
document.addEventListener('keydown', handleEscape);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener('mousedown', handleClickOutside);
|
|
document.removeEventListener('keydown', handleEscape);
|
|
};
|
|
}, [open]);
|
|
|
|
return (
|
|
<div className={clsx('relative inline-block', className)}>
|
|
<div
|
|
ref={triggerRef}
|
|
onClick={() => setOpen(!open)}
|
|
className={clsx('cursor-pointer', triggerClassName)}
|
|
role="button"
|
|
aria-haspopup="true"
|
|
aria-expanded={open}
|
|
>
|
|
{trigger}
|
|
</div>
|
|
|
|
{open && (
|
|
<>
|
|
{modal && (
|
|
<div className="fixed inset-0 z-40" onClick={() => setOpen(false)} aria-hidden="true" />
|
|
)}
|
|
<div
|
|
ref={popoverRef}
|
|
className={clsx(
|
|
'absolute z-50',
|
|
'bg-background border rounded-lg shadow-lg',
|
|
'p-4 min-w-[200px]',
|
|
'animate-in fade-in-0 zoom-in-95 duration-200',
|
|
alignClasses[align],
|
|
sideClasses[side]
|
|
)}
|
|
role="dialog"
|
|
>
|
|
{content}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|