"use client"; import { AnimatePresence, motion } from "framer-motion"; import { ChevronRightIcon } from "lucide-react"; import { KeyboardEvent } from "react"; import { Checkbox } from "@/components/shadcn/checkbox"; import { cn } from "@/lib/utils"; import { TreeNodeProps } from "@/types/tree"; import { TreeItemLabel } from "./tree-item-label"; import { TreeLeaf } from "./tree-leaf"; import { TreeSpinner } from "./tree-spinner"; import { TreeStatusIndicator } from "./tree-status-indicator"; import { getAllDescendantIds, getTreeNodePadding } from "./utils"; /** * TreeNode component for rendering expandable nodes with children. * * Features: * - Collapsible content using Radix UI * - Indeterminate checkbox state when partially selected * - Recursive selection of all descendants * - Loading spinner state * - Custom rendering via renderItem prop * - Keyboard navigation support (Enter/Space to select, Arrow keys to expand) */ export function TreeNode({ item, level, selectedIds, expandedIds, onSelectionChange, onExpandedChange, showCheckboxes, renderItem, enableSelectChildren, }: TreeNodeProps) { const isExpanded = expandedIds.includes(item.id); const isSelected = selectedIds.includes(item.id); const statusIcon = !item.isLoading && item.status ? ( ) : null; // Calculate indeterminate state based on descendant selection const descendantIds = getAllDescendantIds(item); const selectedDescendantCount = descendantIds.filter((id) => selectedIds.includes(id), ).length; const isIndeterminate = selectedDescendantCount > 0 && selectedDescendantCount < descendantIds.length; const handleToggleExpand = () => { const newExpandedIds = isExpanded ? expandedIds.filter((id) => id !== item.id) : [...expandedIds, item.id]; onExpandedChange(newExpandedIds); }; const handleSelect = () => { onSelectionChange(item.id, item); }; const handleContentClick = showCheckboxes ? handleSelect : handleToggleExpand; const handleKeyDown = (event: KeyboardEvent) => { switch (event.key) { case "Enter": case " ": event.preventDefault(); handleContentClick(); break; case "ArrowRight": if (!isExpanded) { event.preventDefault(); handleToggleExpand(); } break; case "ArrowLeft": if (isExpanded) { event.preventDefault(); handleToggleExpand(); } break; } }; return ( { e.stopPropagation(); handleToggleExpand(); }} tabIndex={-1} > {item.isLoading ? ( ) : ( )} {statusIcon} {showCheckboxes && ( e.stopPropagation()} /> )} {renderItem ? ( renderItem({ item, level, isLeaf: false, isSelected, isExpanded, isIndeterminate, hasChildren: true, }) ) : ( )} {isExpanded && ( {item.children?.map((child) => ( {child.children && child.children.length > 0 ? ( ) : ( )} ))} )} ); }