🐛(frontend) fix removed item in the tree

react-arborist's scrollTo calls react-window's scrollToItem, which mutates
the internal scrollOffset state. When navigating to a deep item in a large
tree, this causes all items above the target to be removed from the DOM
(virtualized away), making the tree appear empty above the selected node.
We no-op it to prevent that — the panel's own overflow-y handles scrolling.
This commit is contained in:
Anthony LC
2026-06-22 17:09:41 +02:00
parent a8d67f34c4
commit a63fa17e20
2 changed files with 58 additions and 1 deletions
+5
View File
@@ -6,6 +6,11 @@ and this project adheres to
## [Unreleased]
### Fixed
- 🐛(frontend) fix removed item in the tree #2420
## [v5.3.0] - 2026-06-19
### Added
@@ -7,7 +7,13 @@ import {
useTreeContext,
} from '@gouvfr-lasuite/ui-kit';
import { useRouter } from 'next/navigation';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -258,6 +264,52 @@ export const DocTree = ({ currentDoc }: DocTreeProps) => {
}
}, [currentDoc, treeContext]);
/**
* react-arborist's scrollTo calls react-window's scrollToItem, which mutates
* the internal scrollOffset state. When navigating to a deep item in a large
* tree, this causes all items above the target to be removed from the DOM
* (virtualized away), making the tree appear empty above the selected node.
* We no-op it to prevent that — the panel's own overflow-y handles scrolling.
*/
const treeApiRef = treeContext?.treeApiRef;
useLayoutEffect(() => {
if (!treeRoot || !treeApiRef?.current) {
return;
}
const api = treeApiRef.current as unknown as Record<string, unknown>;
const origScrollTo = api['scrollTo'];
if (typeof origScrollTo !== 'function') {
return;
}
api['scrollTo'] = () => {};
return () => {
api['scrollTo'] = origScrollTo;
};
}, [treeRoot, treeApiRef]);
/**
* On initial tree load, scroll the panel to show the current document.
* This fires once when initialOpenState is first set (tree data just loaded).
* It does not re-fire on user navigation — clicked items are already in view.
*/
useEffect(() => {
if (!treeRoot || !initialOpenState) {
return;
}
const timeoutId = setTimeout(() => {
treeRoot
.querySelector<HTMLElement>(
`[data-testid="doc-sub-page-item-${currentDoc.id}"]`,
)
?.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}, 100);
return () => {
clearTimeout(timeoutId);
};
}, [treeRoot, initialOpenState, currentDoc.id]);
if (!treeContext || !treeContext.root) {
return <TreeSkeleton />;
}