'use client' import React, { useState, useCallback, useMemo } from 'react' import clsx from 'clsx' import styles from '../../../scss/TreeView.module.scss' export interface TreeNode { id: string label: string icon?: React.ReactNode children?: TreeNode[] disabled?: boolean } export interface TreeViewProps { /** Tree data nodes */ data: TreeNode[] /** Initially expanded node IDs */ defaultExpanded?: string[] /** Controlled expanded node IDs */ expanded?: string[] /** Initially selected node IDs */ defaultSelected?: string[] /** Controlled selected node IDs */ selected?: string[] /** Enable multi-select */ multiSelect?: boolean /** Callback when node is selected */ onSelect?: (nodeId: string, node: TreeNode) => void /** Callback when selection changes (multi-select) */ onSelectionChange?: (nodeIds: string[]) => void /** Callback when node is expanded/collapsed */ onToggle?: (nodeId: string, isExpanded: boolean) => void /** Custom expand icon */ expandIcon?: React.ReactNode /** Custom collapse icon */ collapseIcon?: React.ReactNode /** Custom end icon (leaf nodes) */ endIcon?: React.ReactNode /** Additional CSS class */ className?: string /** Dense layout */ dense?: boolean /** Test ID for automated testing */ testId?: string } interface TreeItemProps { node: TreeNode level: number expanded: Set selected: Set multiSelect: boolean onToggleExpand: (nodeId: string) => void onSelect: (nodeId: string, node: TreeNode) => void expandIcon?: React.ReactNode collapseIcon?: React.ReactNode endIcon?: React.ReactNode dense?: boolean } const DefaultExpandIcon = () => ( ) const DefaultCollapseIcon = () => ( ) function TreeItem({ node, level, expanded, selected, multiSelect, onToggleExpand, onSelect, expandIcon, collapseIcon, endIcon, dense, }: TreeItemProps) { const hasChildren = node.children && node.children.length > 0 const isExpanded = expanded.has(node.id) const isSelected = selected.has(node.id) const handleToggle = useCallback( (e: React.MouseEvent) => { e.stopPropagation() if (hasChildren) { onToggleExpand(node.id) } }, [hasChildren, node.id, onToggleExpand] ) const handleSelect = useCallback( (e: React.MouseEvent) => { if (!node.disabled) { onSelect(node.id, node) } }, [node, onSelect] ) const renderIcon = () => { if (!hasChildren) { return endIcon ? ( {endIcon} ) : ( ) } return ( {isExpanded ? collapseIcon || : expandIcon || } ) } return (
  • {renderIcon()} {node.icon && {node.icon}} {node.label}
    {hasChildren && isExpanded && (
      {node.children!.map((child) => ( ))}
    )}
  • ) } /** * TreeView component - Hierarchical tree navigation * * @example * ```tsx * const data = [ * { id: '1', label: 'Parent', children: [ * { id: '1-1', label: 'Child 1' }, * { id: '1-2', label: 'Child 2' } * ]} * ] * console.log(id)} /> * ``` */ export function TreeView({ data, defaultExpanded = [], expanded: controlledExpanded, defaultSelected = [], selected: controlledSelected, multiSelect = false, onSelect, onSelectionChange, onToggle, expandIcon, collapseIcon, endIcon, className, dense = false, testId, }: TreeViewProps) { const [internalExpanded, setInternalExpanded] = useState>( new Set(defaultExpanded) ) const [internalSelected, setInternalSelected] = useState>( new Set(defaultSelected) ) const expanded = useMemo( () => (controlledExpanded !== undefined ? new Set(controlledExpanded) : internalExpanded), [controlledExpanded, internalExpanded] ) const selected = useMemo( () => (controlledSelected !== undefined ? new Set(controlledSelected) : internalSelected), [controlledSelected, internalSelected] ) const handleToggleExpand = useCallback( (nodeId: string) => { const newExpanded = new Set(expanded) const isExpanding = !newExpanded.has(nodeId) if (isExpanding) { newExpanded.add(nodeId) } else { newExpanded.delete(nodeId) } if (controlledExpanded === undefined) { setInternalExpanded(newExpanded) } onToggle?.(nodeId, isExpanding) }, [expanded, controlledExpanded, onToggle] ) const handleSelect = useCallback( (nodeId: string, node: TreeNode) => { let newSelected: Set if (multiSelect) { newSelected = new Set(selected) if (newSelected.has(nodeId)) { newSelected.delete(nodeId) } else { newSelected.add(nodeId) } } else { newSelected = new Set([nodeId]) } if (controlledSelected === undefined) { setInternalSelected(newSelected) } onSelect?.(nodeId, node) onSelectionChange?.(Array.from(newSelected)) }, [selected, multiSelect, controlledSelected, onSelect, onSelectionChange] ) return (
      {data.map((node) => ( ))}
    ) } export default TreeView