- Updated icon imports in dashboard, policy engine, and custom components to use phosphor-icons. - Replaced icons such as Check, Plus, Trash2, and others with their phosphor equivalents. - Ensured consistent icon usage across the application for better visual coherence.
148 lines
4.8 KiB
TypeScript
148 lines
4.8 KiB
TypeScript
import React, { useCallback, useEffect, useRef, useState, useLayoutEffect } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { DotsThreeVerticalIcon } from "@phosphor-icons/react";
|
|
|
|
interface CustomActionMenuProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
export const CustomActionMenu: React.FC<CustomActionMenuProps> = ({
|
|
children,
|
|
}) => {
|
|
const [isOpen, setIsOpen] = useState(false);
|
|
const [position, setPosition] = useState({ top: 0, left: 0 });
|
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
|
|
const updatePosition = useCallback(() => {
|
|
if (buttonRef.current && isOpen) {
|
|
const buttonRect = buttonRef.current.getBoundingClientRect();
|
|
const menuRect = menuRef.current?.getBoundingClientRect();
|
|
|
|
let top = buttonRect.bottom + 4;
|
|
let left = buttonRect.right - (menuRect?.width || 160); // Default items width
|
|
|
|
// Check if it fits vertically
|
|
if (menuRect && top + menuRect.height > window.innerHeight) {
|
|
top = buttonRect.top - menuRect.height - 4; // Flip up
|
|
}
|
|
|
|
// Check if it fits horizontally
|
|
if (left < 0) {
|
|
left = buttonRect.left; // Align left if no space on right
|
|
}
|
|
|
|
setPosition({ top, left });
|
|
}
|
|
}, [isOpen]);
|
|
|
|
useLayoutEffect(() => {
|
|
updatePosition();
|
|
}, [updatePosition]);
|
|
|
|
useEffect(() => {
|
|
const handleScrollOrResize = () => {
|
|
if (isOpen) updatePosition();
|
|
};
|
|
|
|
window.addEventListener("scroll", handleScrollOrResize, true);
|
|
window.addEventListener("resize", handleScrollOrResize);
|
|
|
|
return () => {
|
|
window.removeEventListener("scroll", handleScrollOrResize, true);
|
|
window.removeEventListener("resize", handleScrollOrResize);
|
|
};
|
|
}, [isOpen, updatePosition]);
|
|
|
|
// Handle click outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
const target = event.target as HTMLElement;
|
|
if (
|
|
isOpen &&
|
|
buttonRef.current &&
|
|
!buttonRef.current.contains(target) &&
|
|
!target.closest(".custom-action-menu-dropdown")
|
|
) {
|
|
setIsOpen(false);
|
|
}
|
|
};
|
|
|
|
if (isOpen) {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
}
|
|
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, [isOpen]);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
ref={buttonRef}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsOpen(!isOpen);
|
|
}}
|
|
className={`flex h-8 w-8 items-center justify-center rounded-full transition-colors focus:outline-none ${isOpen ? 'bg-gray-100 text-black' : 'text-black hover:bg-gray-100'}`}
|
|
aria-label="Actions"
|
|
>
|
|
<DotsThreeVerticalIcon size={18} />
|
|
</button>
|
|
|
|
{isOpen &&
|
|
createPortal(
|
|
<div
|
|
ref={menuRef}
|
|
className="custom-action-menu-dropdown fixed z-[9999] min-w-[160px] rounded-xl bg-white shadow-[0_4px_20px_-4px_rgba(0,0,0,0.1)] border border-gray-100 focus:outline-none p-1.5"
|
|
style={{
|
|
top: position.top,
|
|
left: position.left,
|
|
}}
|
|
onClick={() => setIsOpen(false)}
|
|
>
|
|
<div className="flex flex-col gap-0.5">
|
|
{children}
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export interface CustomActionItemProps {
|
|
children: React.ReactNode;
|
|
onClick?: () => void;
|
|
variant?: "default" | "success" | "danger";
|
|
icon?: React.ReactNode;
|
|
}
|
|
|
|
export const CustomActionItem: React.FC<CustomActionItemProps> = ({
|
|
children,
|
|
onClick,
|
|
variant = "default",
|
|
icon
|
|
}) => {
|
|
|
|
const variantClasses = {
|
|
default: "text-gray-700 hover:bg-gray-50 hover:text-gray-900",
|
|
success: "text-primary bg-[#EBF7F2] hover:brightness-95",
|
|
danger: "text-red-600 bg-red-50 hover:brightness-95",
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className={`flex items-center gap-2.5 w-full cursor-pointer px-3 py-2 text-[13px] font-medium transition-all rounded-md ${variantClasses[variant]}`}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
onClick?.();
|
|
}}
|
|
>
|
|
{icon && <span className="flex items-center justify-center w-4 h-4">{icon}</span>}
|
|
{children}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CustomActionMenu;
|