Files
aeroresolve_frontend/src/components/custom/CustomActionMenu.tsx
T
Syed Waseem 48dbbdfcbb refactor: replace lucide-react icons with phosphor-icons in various components
- 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.
2026-07-18 16:15:06 +05:30

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;