diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d596b9..7cf50f6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to - ♿(frontend) improve accessibility: - #1354 + - ✨fix tree keyboard toggle when children not yet loaded #1388 ### Fixed diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx index 0b0ee038..46e86ed4 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx @@ -20,6 +20,7 @@ import { useLeftPanelStore } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; import { useKeyboardActivation } from '../hooks/useKeyboardActivation'; +import { useLoadChildrenOnOpen } from '../hooks/useLoadChildrenOnOpen'; import SubPageIcon from './../assets/sub-page-logo.svg'; import { DocTreeItemActions } from './DocTreeItemActions'; @@ -96,6 +97,14 @@ export const DocSubPageItem = (props: TreeViewNodeProps) => { '.c__tree-view', ); + useLoadChildrenOnOpen( + node.data.value.id, + node.isOpen, + treeContext?.treeData.handleLoadChildren, + treeContext?.treeData.setChildren, + (doc.children?.length ?? 0) > 0 || doc.childrenCount === 0, + ); + const docTitle = doc.title || untitledDocument; const hasChildren = (doc.children?.length || 0) > 0; const isExpanded = node.isOpen; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useLoadChildrenOnOpen.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useLoadChildrenOnOpen.ts new file mode 100644 index 00000000..445e74e9 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/hooks/useLoadChildrenOnOpen.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from 'react'; + +/** + * Lazily loads children for a tree node the first time it is expanded. + */ +export const useLoadChildrenOnOpen = ( + nodeId: string, + isOpen: boolean, + handleLoadChildren?: (id: string, signal: AbortSignal) => Promise, + setChildren?: (id: string, children: T[]) => void, + isAlreadyLoaded: boolean = false, +) => { + const hasLoadedRef = useRef(false); + + // Reset only if node changes AND it's not already loaded externally + useEffect(() => { + hasLoadedRef.current = isAlreadyLoaded; + }, [nodeId, isAlreadyLoaded]); + + useEffect(() => { + if (!isOpen) { + return; + } + if (isAlreadyLoaded || hasLoadedRef.current) { + return; + } + if (!handleLoadChildren || !setChildren) { + return; + } + + const abortCtrl = new AbortController(); + hasLoadedRef.current = true; // prevent multiple fetches + + void handleLoadChildren(nodeId, abortCtrl.signal) + .then((children) => { + if (!abortCtrl.signal.aborted) { + setChildren(nodeId, children); + } + }) + .catch(() => { + // allow retry on next open + if (!abortCtrl.signal.aborted) { + hasLoadedRef.current = false; + } + }); + + return () => { + abortCtrl.abort(); + }; + }, [isOpen, nodeId, handleLoadChildren, setChildren, isAlreadyLoaded]); +};