From dca587a2df5230576c07bf50ab467d01f0d1190f Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 3 May 2026 22:19:20 +0200
Subject: [PATCH] fixed ux for expand object scrolling
---
.../FormGeneratorTree/FormGeneratorTree.tsx | 20 +++++++++++++-
.../TreeNavigation/TreeNavigation.tsx | 26 ++++++++++++++-----
2 files changed, 39 insertions(+), 7 deletions(-)
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()}>