diff --git a/web/playground/src/sidebar/Sidebar.jsx b/web/playground/src/sidebar/Sidebar.jsx
index 1f86d1011c54..1ba227bf7fb9 100644
--- a/web/playground/src/sidebar/Sidebar.jsx
+++ b/web/playground/src/sidebar/Sidebar.jsx
@@ -1,5 +1,4 @@
import "./Sidebar.css";
-import React from "react";
import { useState } from "react";
function Sidebar({ library, onLoadFile }) {
@@ -8,8 +7,7 @@ function Sidebar({ library, onLoadFile }) {
}
function toggleFolder(id) {
- openFolders[id] = !Boolean(openFolders[id]);
- setOpenFolders(() => ({ ...openFolders }));
+ setOpenFolders((folders) => ({ ...folders, [id]: !folders[id] }));
}
function handleClick(section, file, id) {
@@ -29,28 +27,37 @@ function Sidebar({ library, onLoadFile }) {
for (const [section, files] of Object.entries(library)) {
const fileRows = [];
- for (const [index, filename] of Object.keys(files).entries()) {
- const array = files[filename];
- const depth = array[2];
- const parent = array[3];
- const id = array[4];
- const name = array[5];
+ // Whether each row's children should render: the row is itself visible
+ // *and* open. `generateBook.cjs` emits parents before their children, so
+ // an entry's ancestors are already recorded by the time it's read. Testing
+ // only the immediate parent would leave a subtree dangling at the top of
+ // the sidebar when a grandparent is collapsed while the parent stays open.
+ const childrenVisible = {};
+ for (const filename of Object.keys(files)) {
+ // The `book` section carries tree metadata past the editor and content;
+ // the flat sections (examples, tables, local storage) leave it undefined.
+ const [, , depth, parent, id, name] = files[filename];
+ const visible =
+ parent == null || depth === 0 || Boolean(childrenVisible[parent]);
+ childrenVisible[id] = visible && Boolean(openFolders[id]);
+
+ if (!visible) {
+ continue;
+ }
+
fileRows.push(
-