resource-portal / frontend /src /components /projects /AllocationBarMenu.tsx
Gowrisankar
Make allocation bar menu dots always visible on colored pills.
11d3514
import { MoreVertical } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import type { Person } from "../../types";
interface AllocationBarMenuProps {
onEdit: () => void;
onClone?: () => void;
onSelectAllToRight?: () => void;
onToggleMultiSelect?: () => void;
multiSelectActive?: boolean;
onDelete?: () => void;
transferPeople?: Person[];
onTransferPerson?: (personId: number) => void;
}
export function AllocationBarMenuTrigger({
onEdit,
onClone,
onSelectAllToRight,
onToggleMultiSelect,
multiSelectActive = false,
onDelete,
transferPeople = [],
onTransferPerson,
}: AllocationBarMenuProps) {
const triggerRef = useRef<HTMLButtonElement>(null);
const menuRef = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false);
const [transferOpen, setTransferOpen] = useState(false);
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
useEffect(() => {
if (!open) return;
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setOpen(false);
setTransferOpen(false);
}
};
const onPointerDown = (event: PointerEvent) => {
const target = event.target as Node;
if (triggerRef.current?.contains(target)) return;
if (menuRef.current?.contains(target)) return;
setOpen(false);
setTransferOpen(false);
};
window.addEventListener("keydown", onKey);
const timer = window.setTimeout(() => {
window.addEventListener("pointerdown", onPointerDown);
}, 0);
return () => {
window.clearTimeout(timer);
window.removeEventListener("keydown", onKey);
window.removeEventListener("pointerdown", onPointerDown);
};
}, [open]);
const toggleMenu = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
event.preventDefault();
if (open) {
setOpen(false);
setTransferOpen(false);
return;
}
const rect = triggerRef.current?.getBoundingClientRect();
if (!rect) return;
setAnchor({ top: rect.bottom + 4, left: rect.right - 200 });
setOpen(true);
};
const close = () => {
setOpen(false);
setTransferOpen(false);
};
const run = (event: React.MouseEvent, action: () => void) => {
event.preventDefault();
event.stopPropagation();
close();
window.setTimeout(() => action(), 0);
};
return (
<>
<button
aria-expanded={open}
aria-label="Assignment options"
className="allocation-bar-menu-btn"
onClick={toggleMenu}
onPointerDown={(event) => event.stopPropagation()}
onPointerUp={(event) => event.stopPropagation()}
ref={triggerRef}
type="button"
>
<MoreVertical aria-hidden size={16} strokeWidth={2.75} />
</button>
{open && anchor
? createPortal(
<div
className="allocation-bar-menu"
onMouseDown={(event) => event.stopPropagation()}
onPointerDown={(event) => event.stopPropagation()}
ref={menuRef}
style={{ position: "fixed", top: anchor.top, left: Math.max(8, anchor.left), zIndex: 3500 }}
>
<button
className="allocation-bar-menu-item"
onClick={(event) => run(event, onEdit)}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
Edit
</button>
{onTransferPerson && transferPeople.length > 0 ? (
<>
<button
className="allocation-bar-menu-item"
onClick={(event) => {
event.stopPropagation();
setTransferOpen((value) => !value);
}}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
Transfer
</button>
{transferOpen ? (
<div className="allocation-bar-menu-sub">
<select
onChange={(event) => {
const id = Number(event.target.value);
if (id) {
close();
window.setTimeout(() => onTransferPerson(id), 0);
}
}}
onMouseDown={(event) => event.stopPropagation()}
value=""
>
<option value="">Choose person…</option>
{transferPeople.map((person) => (
<option key={person.id} value={person.id}>
{person.name}
</option>
))}
</select>
</div>
) : null}
</>
) : null}
{onClone ? (
<button
className="allocation-bar-menu-item"
onClick={(event) => run(event, onClone)}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
Clone
</button>
) : null}
{onSelectAllToRight ? (
<button
className="allocation-bar-menu-item"
onClick={(event) => run(event, onSelectAllToRight)}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
Select All to Right
</button>
) : null}
{onToggleMultiSelect ? (
<button
className="allocation-bar-menu-item"
onClick={(event) => run(event, onToggleMultiSelect)}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
{multiSelectActive ? "Disable Multi-Select Mode" : "Enable Multi-Select Mode"}
</button>
) : null}
{onDelete ? (
<button
className="allocation-bar-menu-item is-danger"
onClick={(event) => run(event, onDelete)}
onMouseDown={(event) => event.stopPropagation()}
type="button"
>
Delete
</button>
) : null}
</div>,
document.body,
)
: null}
</>
);
}