diff --git a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx index aa1bee9..e480963 100644 --- a/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx +++ b/src/components/FormGenerator/FormGeneratorTree/FormGeneratorTree.tsx @@ -452,6 +452,7 @@ export function FormGeneratorTree({ const _handleToggleExpand = useCallback( async (id: string) => { + const wasExpanded = expandedIds.has(id); setExpandedIds((prev) => { const next = new Set(prev); if (next.has(id)) { @@ -463,7 +464,7 @@ export function FormGeneratorTree({ }); const node = nodes.find((n) => n.id === id); - if (node && !expandedIds.has(id)) { + if (node && !wasExpanded) { const childMap = _buildChildMap(nodes); const existingChildren = childMap.get(id); if (!existingChildren || existingChildren.length === 0) { @@ -472,11 +473,28 @@ export function FormGeneratorTree({ setNodes((prev) => [...prev, ...childNodes]); } } + setTimeout(() => { + _scrollExpandedNodeToCenter(id); + }, 50); } }, [nodes, expandedIds, provider, ownership], ); + const _scrollExpandedNodeToCenter = useCallback((nodeId: string) => { + const container = treeContentRef.current; + if (!container) return; + const el = container.querySelector(`[data-node-id="${nodeId}"]`) as HTMLElement | null; + if (!el) return; + const containerRect = container.getBoundingClientRect(); + const elRect = el.getBoundingClientRect(); + const midpoint = containerRect.top + containerRect.height / 2; + if (elRect.top > midpoint) { + const scrollTarget = container.scrollTop + (elRect.top - midpoint); + container.scrollTo({ top: scrollTarget, behavior: 'smooth' }); + } + }, []); + const _handleToggleSelect = useCallback( (id: string, e: React.MouseEvent) => { const newSelection = new Set(selectedIds); diff --git a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx index 4b9beee..2f81d1b 100644 --- a/src/components/Navigation/TreeNavigation/TreeNavigation.tsx +++ b/src/components/Navigation/TreeNavigation/TreeNavigation.tsx @@ -10,7 +10,7 @@ * - NavLink integration with React Router */ -import React, { useState, useEffect, ReactNode } from 'react'; +import React, { useState, useEffect, useRef, useCallback, ReactNode } from 'react'; import { NavLink, useLocation } from 'react-router-dom'; import styles from './TreeNavigation.module.css'; @@ -151,6 +151,7 @@ const TreeNode: React.FC = ({ const [isExpanded, setIsExpanded] = useState( node.defaultExpanded ?? shouldAutoExpand ?? false ); + const containerRef = useRef(null); // Auto-expand when path becomes active useEffect(() => { @@ -159,6 +160,16 @@ const TreeNode: React.FC = ({ } }, [currentPath, autoExpandActive, node]); + const _scrollAfterExpand = useCallback(() => { + const el = containerRef.current; + if (!el) return; + const rect = el.getBoundingClientRect(); + const viewportMid = window.innerHeight / 2; + if (rect.top > viewportMid) { + el.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + }, []); + // Check if this node is active (exact match or ancestor of active path) const isActive = node.path ? currentPath === node.path || currentPath.startsWith(node.path + '/') : false; // Differentiate: leaf active (strong highlight) vs group active (subtle text only) @@ -179,12 +190,13 @@ const TreeNode: React.FC = ({ } if (isExpandable && !node.path) { - // If only expandable (no path), toggle expand - setIsExpanded(!isExpanded); + const willExpand = !isExpanded; + setIsExpanded(willExpand); + if (willExpand) setTimeout(_scrollAfterExpand, 50); } else if (isExpandable && node.path) { - // If both expandable and has path, expand on click but allow navigation if (!isExpanded) { setIsExpanded(true); + setTimeout(_scrollAfterExpand, 50); } } @@ -197,7 +209,9 @@ const TreeNode: React.FC = ({ const handleToggleClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - setIsExpanded(!isExpanded); + const willExpand = !isExpanded; + setIsExpanded(willExpand); + if (willExpand) setTimeout(_scrollAfterExpand, 50); }; // Render the node content (actions are rendered outside to avoid button-in-button nesting) @@ -255,7 +269,7 @@ const TreeNode: React.FC = ({ const canRenderChildren = maxDepth === 0 || level < maxDepth; return ( -
+
{nodeElement} {node.actions && ( e.stopPropagation()}>