- 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.
import { useEffect, useRef } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import type { ColumnState } from "../types";
import { focusElement } from "../focus";
import { IconMove, IconTrash } from "../icons";
export interface MenuPosition {
left: number;
top: number;
}
interface CardMenuProps {
cardTitle: string;
currentColumnTitle: string;
others: ColumnState[];
pos: MenuPosition;
button: HTMLButtonElement;
onMove: (colId: string) => void;
onDelete: () => void;
onClose: (refocus: boolean) => void;
}
export function CardMenu(props: CardMenuProps) {
const { cardTitle, currentColumnTitle, others, pos, button, onMove, onDelete, onClose } = props;
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const first = ref.current?.querySelector<HTMLElement>("[role='menuitem']");
focusElement(first);
const onDocPointerDown = (e: PointerEvent) => {
const target = e.target as HTMLElement | null;
if (target && ref.current?.contains(target)) return;
if (target && button.contains(target)) return;
onClose(false);
};
const onScroll = () => onClose(false);
window.addEventListener("pointerdown", onDocPointerDown, true);
window.addEventListener("scroll", onScroll, true);
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("pointerdown", onDocPointerDown, true);
window.removeEventListener("scroll", onScroll, true);
window.removeEventListener("resize", onScroll);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onKeyDown = (e: ReactKeyboardEvent) => {
if (e.key === "Escape") {
e.stopPropagation();
onClose(true);
return;
}
if (e.key === "Tab") {
e.preventDefault();
onClose(true);
return;
}
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const items = Array.from(ref.current?.querySelectorAll<HTMLElement>("[role='menuitem']") ?? []);
if (items.length === 0) return;
const idx = items.indexOf(document.activeElement as HTMLElement);
const next = e.key === "ArrowDown" ? (idx + 1) % items.length : (idx - 1 + items.length) % items.length;
focusElement(items[next]);
}
};
return (
<div
ref={ref}
className="menu"
role="menu"
aria-label={`Actions for “${cardTitle}”`}
style={{ left: pos.left, top: pos.top }}
onKeyDown={onKeyDown}
>
<p className="menu__title">Move from {currentColumnTitle} to</p>
{others.map((c) => (
<button key={c.id} type="button" role="menuitem" className="menu__item" onClick={() => onMove(c.id)}>
<IconMove width={14} height={14} />
<span>{c.title}</span>
{c.wip_limit != null && <span className="menu__meta">limit {c.wip_limit}</span>}
</button>
))}
<hr className="menu__sep" />
<button type="button" role="menuitem" className="menu__item menu__item--danger" onClick={onDelete}>
<IconTrash width={14} height={14} />
<span>Delete card…</span>
</button>
</div>
);
}Source is browser-viewable only. There is no download or archive endpoint, and no additional license is granted beyond inspection and research.