Files
dashboards/components/CategoryTreeView.tsx
T

130 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import styles from "./CategoryTreeView.module.css";
export type CategoryTreeNode = {
id: string;
name: string;
hint?: string;
children: CategoryTreeNode[];
};
export type CategoryTreeActions = {
onAddChild?: (id: string) => void;
onEdit?: (id: string) => void;
onRemove?: (id: string) => void;
};
export default function CategoryTreeView({
nodes,
emptyLabel = "No categories yet",
actions,
}: {
nodes: CategoryTreeNode[];
emptyLabel?: string;
actions?: CategoryTreeActions;
}) {
if (!nodes.length) {
return <p className={styles.empty}>{emptyLabel}</p>;
}
return (
<ul className={styles.list}>
{nodes.map((node) => (
<TreeBranch key={node.id} node={node} depth={0} actions={actions} />
))}
</ul>
);
}
function TreeBranch({
node,
depth,
actions,
}: {
node: CategoryTreeNode;
depth: number;
actions?: CategoryTreeActions;
}) {
const [open, setOpen] = useState(false);
const childCount = node.children.length;
const previousChildCount = useRef(childCount);
const hasChildren = childCount > 0;
const hasActions = Boolean(
actions?.onAddChild || actions?.onEdit || actions?.onRemove,
);
useEffect(() => {
if (childCount > previousChildCount.current) {
setOpen(true);
}
previousChildCount.current = childCount;
}, [childCount]);
return (
<li className={styles.node}>
<div
className={styles.row}
style={{ paddingInlineStart: `${12 + depth * 56}px` }}
>
{hasChildren ? (
<button
type="button"
className={`${styles.expand} ${open ? styles.expandOpen : ""}`}
aria-expanded={open}
aria-label={open ? "Collapse" : "Expand"}
onClick={() => setOpen((value) => !value)}
>
<span aria-hidden>{open ? "−" : "+"}</span>
</button>
) : (
<span className={styles.expandSpacer} aria-hidden />
)}
<span className={styles.name} dir="auto">
{node.name}
</span>
{node.hint ? <span className={styles.hint}>{node.hint}</span> : null}
{hasActions ? (
<div className={styles.actions}>
{actions?.onAddChild ? (
<button
type="button"
onClick={() => actions.onAddChild?.(node.id)}
>
Add sub
</button>
) : null}
{actions?.onEdit ? (
<button type="button" onClick={() => actions.onEdit?.(node.id)}>
Edit
</button>
) : null}
{actions?.onRemove ? (
<button
type="button"
className={styles.danger}
onClick={() => actions.onRemove?.(node.id)}
>
Remove
</button>
) : null}
</div>
) : null}
</div>
{hasChildren && open ? (
<ul className={styles.list}>
{node.children.map((child) => (
<TreeBranch
key={child.id}
node={child}
depth={depth + 1}
actions={actions}
/>
))}
</ul>
) : null}
</li>
);
}