{"version":3,"file":"TreeView.cjs","names":[],"sources":["../../../src/components/TreeView/TreeView.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — expansion and selection\n * are two independent controlled states, each with its uncontrolled twin\n * (expandedIds/defaultExpandedIds/onExpandedChange, selectedId/\n * defaultSelectedId/onSelect), plus toggleOnSelect for the case where they should\n * move together. The body is the flattened visible list and the roving focus over\n * it.\n */\nimport { ChevronRight } from \"lucide-react\";\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./TreeView.module.css\";\n\n/** One node of the tree. Children make it a branch; no children makes it a leaf. */\nexport interface TreeNode {\n    /** Stable identifier, unique across the whole tree. */\n    id: string;\n    /** Rendered label. */\n    label: ReactNode;\n    /** Child nodes. An empty array still renders as a branch (an empty folder). */\n    children?: TreeNode[];\n    /** Icon rendered before the label. */\n    icon?: ReactNode;\n    /** Blocks selection and expansion, and skips the node in keyboard navigation. */\n    disabled?: boolean;\n}\n\nexport interface TreeViewProps {\n    /** Root nodes. */\n    nodes: TreeNode[];\n    /** Controlled expanded ids. */\n    expandedIds?: string[];\n    /** Uncontrolled initial expanded ids. */\n    defaultExpandedIds?: string[];\n    onExpandedChange?: (expandedIds: string[]) => void;\n    /** Controlled selected id. `null` means nothing selected. */\n    selectedId?: string | null;\n    /** Uncontrolled initial selection. */\n    defaultSelectedId?: string | null;\n    onSelect?: (node: TreeNode) => void;\n    /**\n     * Selecting a branch also toggles it. Default `true` — matches how a file\n     * explorer behaves; set `false` when a branch is itself a meaningful choice\n     * (a category that owns items, for instance).\n     */\n    toggleOnSelect?: boolean;\n    /** Accessible name for the tree. */\n    label?: string;\n    className?: string;\n}\n\ninterface FlatNode {\n    node: TreeNode;\n    depth: number;\n    parentId: string | null;\n    hasChildren: boolean;\n    expanded: boolean;\n}\n\n/**\n * Walk the tree into the list of *currently visible* rows.\n *\n * Keyboard navigation works on this flattened view, which is what makes\n * ArrowDown from the last child of a collapsed-sibling branch land on the right\n * row: rows that are not rendered simply are not in the list.\n */\nfunction flatten(\n    nodes: TreeNode[],\n    expandedIds: Set<string>,\n    depth = 0,\n    parentId: string | null = null,\n    out: FlatNode[] = [],\n): FlatNode[] {\n    for (const node of nodes) {\n        const hasChildren = Array.isArray(node.children);\n        const expanded = hasChildren && expandedIds.has(node.id);\n        out.push({ node, depth, parentId, hasChildren, expanded });\n        if (expanded && node.children) {\n            flatten(node.children, expandedIds, depth + 1, node.id, out);\n        }\n    }\n    return out;\n}\n\n/**\n * Accessible tree for hierarchical data — categories, permissions, folders, an\n * org chart.\n *\n * Implements the `tree` role with **roving tabindex**: exactly one row is\n * tabbable, and the arrow keys move focus within the widget. That is what keeps a\n * 500-node tree from adding 500 stops to the page's tab order.\n *\n * Keyboard map: `↓`/`↑` move, `→` expands (or descends), `←` collapses (or goes to\n * the parent), `Home`/`End` jump to the first/last visible row, `Enter`/`Space`\n * select.\n *\n * The chevron is decoration (`aria-hidden`), not a button: the row itself carries\n * `aria-expanded`, so a second focusable control there would only add noise for a\n * screen reader while duplicating an action the keyboard map already has. It still\n * accepts a click, with the event stopped so it toggles without also selecting.\n *\n * @example\n * ```tsx\n * const nodes: TreeNode[] = [\n *   {\n *     id: \"vendas\",\n *     label: \"Vendas\",\n *     children: [\n *       { id: \"vendas.ler\", label: \"Ler\" },\n *       { id: \"vendas.editar\", label: \"Editar\" },\n *     ],\n *   },\n *   { id: \"config\", label: \"Configurações\", children: [] },\n * ];\n *\n * <TreeView nodes={nodes} defaultExpandedIds={[\"vendas\"]} onSelect={(node) => console.log(node.id)} />\n * ```\n */\nexport function TreeView({\n    nodes,\n    expandedIds,\n    defaultExpandedIds = [],\n    onExpandedChange,\n    selectedId,\n    defaultSelectedId = null,\n    onSelect,\n    toggleOnSelect = true,\n    label,\n    className,\n}: TreeViewProps) {\n    const expandedControlled = expandedIds !== undefined;\n    const [internalExpanded, setInternalExpanded] = useState<string[]>(defaultExpandedIds);\n    const expanded = expandedControlled ? expandedIds : internalExpanded;\n\n    const selectionControlled = selectedId !== undefined;\n    const [internalSelected, setInternalSelected] = useState<string | null>(defaultSelectedId);\n    const selected = selectionControlled ? selectedId : internalSelected;\n\n    const expandedSet = useMemo(() => new Set(expanded), [expanded]);\n    const rows = useMemo(() => flatten(nodes, expandedSet), [nodes, expandedSet]);\n\n    const [focusedId, setFocusedId] = useState<string | null>(null);\n    const containerRef = useRef<HTMLUListElement>(null);\n\n    const activeId =\n        focusedId ?? selected ?? rows.find((row) => !row.node.disabled)?.node.id ?? null;\n\n    const setExpanded = useCallback(\n        (next: string[]): void => {\n            if (!expandedControlled) setInternalExpanded(next);\n            onExpandedChange?.(next);\n        },\n        [expandedControlled, onExpandedChange],\n    );\n\n    const toggle = useCallback(\n        (id: string): void => {\n            setExpanded(\n                expanded.includes(id) ? expanded.filter((x) => x !== id) : [...expanded, id],\n            );\n        },\n        [expanded, setExpanded],\n    );\n\n    const select = useCallback(\n        (row: FlatNode): void => {\n            if (row.node.disabled) return;\n            if (!selectionControlled) setInternalSelected(row.node.id);\n            setFocusedId(row.node.id);\n            onSelect?.(row.node);\n            if (row.hasChildren && toggleOnSelect) toggle(row.node.id);\n        },\n        [onSelect, selectionControlled, toggle, toggleOnSelect],\n    );\n\n    /** Move DOM focus to a row by id, so the roving tabindex actually roves. */\n    const focusRow = useCallback((id: string): void => {\n        setFocusedId(id);\n        const element = containerRef.current?.querySelector<HTMLElement>(`[data-tree-id=\"${id}\"]`);\n        element?.focus();\n    }, []);\n\n    const moveFocus = useCallback(\n        (from: number, delta: number): void => {\n            for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {\n                if (!rows[index].node.disabled) {\n                    focusRow(rows[index].node.id);\n                    return;\n                }\n            }\n        },\n        [rows, focusRow],\n    );\n\n    const handleKeyDown = useCallback(\n        (event: KeyboardEvent<HTMLDivElement>, row: FlatNode, index: number): void => {\n            switch (event.key) {\n                case \"ArrowDown\":\n                    event.preventDefault();\n                    moveFocus(index, 1);\n                    break;\n                case \"ArrowUp\":\n                    event.preventDefault();\n                    moveFocus(index, -1);\n                    break;\n                case \"ArrowRight\":\n                    event.preventDefault();\n                    if (row.hasChildren && !row.expanded) toggle(row.node.id);\n                    else if (row.expanded) moveFocus(index, 1);\n                    break;\n                case \"ArrowLeft\":\n                    event.preventDefault();\n                    if (row.expanded) {\n                        toggle(row.node.id);\n                    } else if (row.parentId) {\n                        focusRow(row.parentId);\n                    }\n                    break;\n                case \"Home\":\n                    event.preventDefault();\n                    moveFocus(-1, 1);\n                    break;\n                case \"End\":\n                    event.preventDefault();\n                    moveFocus(rows.length, -1);\n                    break;\n                case \"Enter\":\n                case \" \":\n                    event.preventDefault();\n                    select(row);\n                    break;\n                default:\n                    break;\n            }\n        },\n        [moveFocus, rows.length, select, toggle, focusRow],\n    );\n\n    return (\n        <ul\n            ref={containerRef}\n            role=\"tree\"\n            aria-label={label}\n            className={cn(styles.tree, className)}\n        >\n            {rows.map((row, index) => {\n                const isSelected = selected === row.node.id;\n                return (\n                    <li\n                        key={row.node.id}\n                        role=\"treeitem\"\n                        aria-expanded={row.hasChildren ? row.expanded : undefined}\n                        aria-selected={isSelected}\n                        aria-level={row.depth + 1}\n                        aria-disabled={row.node.disabled || undefined}\n                        className={styles.item}\n                    >\n                        <div\n                            data-tree-id={row.node.id}\n                            className={cn(\n                                styles.row,\n                                isSelected && styles.selected,\n                                row.node.disabled && styles.disabled,\n                            )}\n                            style={{\n                                paddingInlineStart: `calc(${row.depth} * var(--tempest-space-5))`,\n                            }}\n                            tabIndex={activeId === row.node.id && !row.node.disabled ? 0 : -1}\n                            onClick={() => select(row)}\n                            onFocus={() => setFocusedId(row.node.id)}\n                            onKeyDown={(event) => handleKeyDown(event, row, index)}\n                        >\n                            {row.hasChildren ? (\n                                <span\n                                    className={cn(\n                                        styles.chevron,\n                                        row.expanded && styles.chevronOpen,\n                                    )}\n                                    aria-hidden=\"true\"\n                                    onClick={(event) => {\n                                        event.stopPropagation();\n                                        if (!row.node.disabled) toggle(row.node.id);\n                                    }}\n                                >\n                                    <ChevronRight size={14} />\n                                </span>\n                            ) : (\n                                <span className={styles.chevronPlaceholder} aria-hidden=\"true\" />\n                            )}\n                            {row.node.icon ? (\n                                <span className={styles.icon} aria-hidden=\"true\">\n                                    {row.node.icon}\n                                </span>\n                            ) : null}\n                            <span className={styles.label}>{row.node.label}</span>\n                        </div>\n                    </li>\n                );\n            })}\n        </ul>\n    );\n}\n"],"mappings":"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"}