From 01c28c890a796ba0dd7e76b0a7641c78ee9cc919 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 03:28:07 +0400 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B5=D0=B1:=20=D0=B2=D0=B8=D0=BA=D0=B8?= =?UTF-8?q?=20=E2=80=94=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BA=D0=B8=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D1=80=D0=B0=D0=BD=D0=BD=D0=BE=D0=B9=20=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=8B=20=D1=80=D0=B0=D1=81=D0=BA?= =?UTF-8?q?=D1=80=D1=8B=D0=B2=D0=B0=D1=8E=D1=82=D1=81=D1=8F,=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=B3=D0=B4=D0=B0=20=D0=B5=D1=91=20=D1=83=D0=B7=D0=B5?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D1=8F=D0=B2=D0=B8=D0=BB=D1=81=D1=8F=20(#9?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/modules/wiki/Tree.test.tsx | 8 ++++++++ web/src/modules/wiki/Tree.tsx | 11 +++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/web/src/modules/wiki/Tree.test.tsx b/web/src/modules/wiki/Tree.test.tsx index c0b2d06..bfd307f 100644 --- a/web/src/modules/wiki/Tree.test.tsx +++ b/web/src/modules/wiki/Tree.test.tsx @@ -59,4 +59,12 @@ describe("Tree", () => { expect(item("B")).toHaveAttribute("aria-expanded", "true"); expect(item("E")).toBeInTheDocument(); }); + + it("выбранная страница появилась в nodes после монтирования — предки раскрываются", () => { + const { rerender } = render( {}} />); + expect(screen.queryByRole("treeitem", { name: "Стр C" })).toBeNull(); + rerender( {}} />); + expect(item("B")).toHaveAttribute("aria-expanded", "true"); + expect(item("C")).toBeInTheDocument(); + }); }); diff --git a/web/src/modules/wiki/Tree.tsx b/web/src/modules/wiki/Tree.tsx index a381e56..8b24edb 100644 --- a/web/src/modules/wiki/Tree.tsx +++ b/web/src/modules/wiki/Tree.tsx @@ -12,17 +12,20 @@ export function Tree({ nodes, selected, onOpen }: { nodes: WikiNode[]; selected: const [focus, setFocus] = useState(null); const refs = useRef(new Map()); - // предки выбранной страницы раскрываются только при смене selected: новый массив nodes (перечитанное дерево) и ручное - // сворачивание родителя этого не повторяют; актуальные узлы читаются через ref и в зависимости не входят + // предки выбранной страницы раскрываются при смене selected и когда её узел впервые появился в nodes (дерево из кэша + // дополнилось свежим набором). Новый массив nodes, добавление других узлов и ручное сворачивание родителя этого не + // повторяют: selectedKnown меняется только с false на true. Актуальные узлы читаются через byCodeRef; его эффект + // объявлен ВЫШЕ эффекта раскрытия, поэтому в одном проходе ref уже свежий + const selectedKnown = !!selected && byCode.has(selected); const byCodeRef = useRef(byCode); useEffect(() => { byCodeRef.current = byCode; }); useEffect(() => { - if (!selected) return; + if (!selected || !selectedKnown) return; const map = byCodeRef.current; const up: string[] = []; for (let n = map.get(selected)?.parent_code; n && !up.includes(n); n = map.get(n)?.parent_code ?? null) up.push(n); if (up.length) setOpen((o) => (up.every((c) => o.has(c)) ? o : new Set([...o, ...up]))); - }, [selected]); + }, [selected, selectedKnown]); const visible = useMemo(() => nodes.filter((n) => { for (let p = n.parent_code, guard = 0; p && guard < nodes.length; p = byCode.get(p)?.parent_code ?? null, guard++) {