({
data,
renderNode,
selectedIds,
expandedIds,
onSelect,
onToggle,
searchTerm,
searchMatch,
includeChildrenOnSearchMatch = false,
height = 300,
rowHeight = 28,
indent = 16,
onMove,
disableDrag = true,
disableDrop = true,
className,
}: TreeProps<T>)
| 49 | } |
| 50 | |
| 51 | export function Tree<T>({ |
| 52 | data, |
| 53 | renderNode, |
| 54 | selectedIds, |
| 55 | expandedIds, |
| 56 | onSelect, |
| 57 | onToggle, |
| 58 | searchTerm, |
| 59 | searchMatch, |
| 60 | includeChildrenOnSearchMatch = false, |
| 61 | height = 300, |
| 62 | rowHeight = 28, |
| 63 | indent = 16, |
| 64 | onMove, |
| 65 | disableDrag = true, |
| 66 | disableDrop = true, |
| 67 | className, |
| 68 | }: TreeProps<T>) { |
| 69 | const { t } = useTranslation(); |
| 70 | const treeRef = useRef<TreeApi<TreeDataNode<T>> | null>(null); |
| 71 | const prevExpandedRef = useRef<readonly string[] | undefined>(undefined); |
| 72 | // react-arborist fires `onToggle` synchronously inside `tree.open()` / |
| 73 | // `tree.close()` calls. If we forward those programmatic notifications to |
| 74 | // the consumer's onToggle (which usually toggles the id in/out of the |
| 75 | // expanded set), we get a feedback loop: setting expandedIds to N keys |
| 76 | // → primitive opens N keys → Arborist fires onToggle for each → consumer |
| 77 | // toggles keys back out → primitive closes them → onToggle fires again. |
| 78 | // The flag below suppresses consumer notifications during the |
| 79 | // programmatic batch so the consumer's state stays stable. |
| 80 | const programmaticToggleRef = useRef(false); |
| 81 | // When the user clicks the chevron, react-arborist toggles its INTERNAL |
| 82 | // open state synchronously AND fires onToggle. The consumer then mirrors |
| 83 | // that change in `expandedIds`, which fires the sync effect below — but |
| 84 | // a re-applied `tree.open(id)` / `tree.close(id)` on the same id forces |
| 85 | // react-arborist to recompute its visible-rows list a second time. For |
| 86 | // a "Columns" node with hundreds of descendants that doubled the cost |
| 87 | // of every collapse. Track the id the user just toggled and skip |
| 88 | // syncing it on the next effect pass — react-arborist's internal state |
| 89 | // is already in sync for that node. |
| 90 | const userToggledIdRef = useRef<string | null>(null); |
| 91 | |
| 92 | // Sync expandedIds to the arborist tree programmatically. |
| 93 | // |
| 94 | // We use `useLayoutEffect` (not `useEffect`) deliberately: when the |
| 95 | // consumer flips `expandedIds`, the FIRST commit shows the tree with |
| 96 | // the new `expandedIds` prop but arborist's internal open-state is |
| 97 | // still the OLD value, so the rendered rows haven't changed yet. The |
| 98 | // sync below calls `tree.open(id)` / `tree.close(id)` which triggers |
| 99 | // a SECOND commit inside arborist. With `useEffect` (passive), the |
| 100 | // browser can paint the first (stale) commit before the second one |
| 101 | // runs, and on some interactions only the next input event seemed to |
| 102 | // give the browser a chance to flush the second paint — users saw |
| 103 | // "click did nothing until I moved the mouse." `useLayoutEffect` |
| 104 | // runs after commit but BEFORE paint, so the sync happens between |
| 105 | // the two commits and the browser paints the final state in one go. |
| 106 | useLayoutEffect(() => { |
| 107 | const tree = treeRef.current; |
| 108 | if (!tree) return; |
nothing calls this directly
no test coverage detected