From 7475e310be759859a9be8bab417b8e38f9a92d83 Mon Sep 17 00:00:00 2001 From: bk-ty Date: Thu, 30 Apr 2026 10:45:08 -0500 Subject: [PATCH] fix: tag tree virtualizer re-measure and scroll-to-selected Two issues with the tag tree virtualizer after expand/collapse: 1. Expand/collapse changed flatTags length but the virtualizer did not remeasure, causing the scroll area to show the wrong total height until the next unrelated re-render. Added a useEffect on flatTags and virtualizer that calls virtualizer.measure() when the tree structure changes. 2. The scroll-to-selected effect read tagIndexMap via the closure's stale capture rather than the latest value at fire time, so the first render after a tag became selected would scroll to index 0 instead of the target tag. Added tagIndexMapRef (updated on every render) so the effect always reads the current map without needing tagIndexMap as a dependency (which would create spurious re-fires). --- src/components/tags/TagTree.tsx | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/tags/TagTree.tsx b/src/components/tags/TagTree.tsx index 06e299fe..beddde78 100644 --- a/src/components/tags/TagTree.tsx +++ b/src/components/tags/TagTree.tsx @@ -78,6 +78,11 @@ export function TagTree({ onOpenTagSettings }: TagTreeProps = {}) { return map; }, [flatTags]); + // Always keep a ref to the latest tagIndexMap so the scroll effect can read + // current indices without subscribing to map changes. The ref is updated on + // every render (outside any effect), so it is always fresh when the effect fires. + const tagIndexMapRef = useRef(tagIndexMap); + tagIndexMapRef.current = tagIndexMap; const virtualizer = useVirtualizer({ count: flatTags.length, getScrollElement: () => scrollContainerRef.current, @@ -85,17 +90,23 @@ export function TagTree({ onOpenTagSettings }: TagTreeProps = {}) { overscan: 20, }); - // Scroll to selected tag + // Remeasure virtualizer when tree structure changes (expansion/collapse) + useEffect(() => { + virtualizer.measure(); + }, [flatTags, virtualizer]); + + // Scroll to selected tag only when the selection changes. We intentionally + // read tagIndexMapRef.current (not tagIndexMap directly) so that accordion + // expand/collapse — which rebuilds tagIndexMap but does not change + // selectedTagId — does NOT trigger a scroll back to the active tag. useEffect(() => { if (selectedTagId) { - const index = tagIndexMap.get(selectedTagId); + const index = tagIndexMapRef.current.get(selectedTagId); if (index !== undefined) { - setTimeout(() => { - virtualizer.scrollToIndex(index, { align: 'auto', behavior: 'smooth' }); - }, 50); + virtualizer.scrollToIndex(index, { align: 'center', behavior: 'smooth' }); } } - }, [selectedTagId, tagIndexMap, virtualizer]); + }, [selectedTagId, virtualizer]); const [contextMenu, setContextMenu] = useState<{ position: { x: number; y: number } | null;