- Run record
- loading
Loading this Run…
Loading the Revisions of this Run…
A published source file of Revision 0 of Boardly — Website Relaunch 1.1.0, shown as text in the browser.
Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import type { CardData, ColumnData } from "../types";
import { IconAlert, IconMove } from "../icons";
interface MoveMenuProps {
card: CardData;
columns: ColumnData[];
cardColumnId: string;
isColumnFull: (columnId: string) => boolean;
onChoose: (columnId: string) => void;
onClose: () => void;
anchor: HTMLElement;
}
export function MoveMenu({
card,
columns,
cardColumnId,
isColumnFull,
onChoose,
onClose,
anchor,
}: MoveMenuProps) {
const menuRef = useRef<HTMLDivElement | null>(null);
const [position, setPosition] = useState<{ left: number; top: number } | null>(null);
const options = columns.filter((column) => column.id !== cardColumnId);
useLayoutEffect(() => {
const place = () => {
const anchorRect = anchor.getBoundingClientRect();
const menuNode = menuRef.current;
const width = 236;
const height = menuNode ? menuNode.offsetHeight : options.length * 40 + 62;
let left = anchorRect.left;
if (left + width > window.innerWidth - 8) left = window.innerWidth - width - 8;
if (left < 8) left = 8;
let top = anchorRect.bottom + 6;
if (top + height > window.innerHeight - 8) {
top = Math.max(8, anchorRect.top - height - 6);
}
setPosition({ left, top });
};
place();
window.addEventListener("resize", place);
window.addEventListener("scroll", place, true);
return () => {
window.removeEventListener("resize", place);
window.removeEventListener("scroll", place, true);
};
}, [anchor, options.length]);
useEffect(() => {
if (!position) return;
const node = menuRef.current;
node?.querySelector<HTMLButtonElement>("[data-menu-item]")?.focus();
}, [position]);
useEffect(() => {
const onPointerDown = (event: PointerEvent) => {
const target = event.target as Node;
if (menuRef.current?.contains(target)) return;
if (anchor.contains(target)) return;
onClose();
};
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
event.preventDefault();
event.stopPropagation();
onClose();
}
};
document.addEventListener("pointerdown", onPointerDown, true);
document.addEventListener("keydown", onKeyDown, true);
return () => {
document.removeEventListener("pointerdown", onPointerDown, true);
document.removeEventListener("keydown", onKeyDown, true);
};
}, [anchor, onClose]);
const focusSibling = (direction: 1 | -1) => {
const items = Array.from(
menuRef.current?.querySelectorAll<HTMLButtonElement>("[data-menu-item]") ?? [],
);
if (items.length === 0) return;
const active = document.activeElement as HTMLButtonElement | null;
const index = active ? items.indexOf(active) : -1;
const next = (index + direction + items.length) % items.length;
items[next].focus();
};
return (
<div
ref={menuRef}
className="popover move-menu"
role="menu"
aria-label={`Move card “${card.title}”`}
style={{
left: position?.left ?? -9999,
top: position?.top ?? -9999,
visibility: position ? "visible" : "hidden",
}}
onKeyDown={(event) => {
if (event.key === "ArrowDown") {
event.preventDefault();
focusSibling(1);
} else if (event.key === "ArrowUp") {
event.preventDefault();
focusSibling(-1);
}
}}
>
<p className="popover-heading">
<IconMove size={13} />
Move to column
</p>
<ul className="popover-list">
{options.map((column) => {
const full = isColumnFull(column.id);
return (
<li key={column.id}>
<button
type="button"
role="menuitem"
data-menu-item
className={`popover-item ${full ? "is-blocked" : ""}`}
aria-label={full
? `Move “${card.title}” to ${column.title} — blocked, WIP limit of ${column.wip_limit} reached`
: `Move “${card.title}” to ${column.title}`}
onClick={() => onChoose(column.id)}
>
<span className="popover-item-name">{column.title}</span>
<span className="popover-item-meta">
{full ? (
<>
<IconAlert size={13} />
Limit {column.wip_limit} reached
</>
) : column.wip_limit !== null ? (
`Limit ${column.wip_limit}`
) : (
""
)}
</span>
</button>
</li>
);
})}
</ul>
<p className="popover-note">
The card moves to the bottom of the chosen column. You can also drag the card with a
pointer.
</p>
</div>
);
}