The shop's website editor has a sidebar that shows its files. Build FileTree, a named export of App.jsx, which takes tree: an array of nodes. A node with a children array (even an empty one) is a folder, and its children are more nodes; any other node, { name }, is a file. Names are unique within a folder. The default export, App, is only for the Preview.
Draw each level as a <ul> with one <li> per node. In every level, folders come first, then files, and each group is in alphabetical order by name, ignoring upper and lower case (App.jsx comes before index.js).
A folder's <li> holds a <button> showing its name, with aria-expanded set to whether it's open. Folders start closed, and clicking the button opens or closes the folder. An open folder shows its contents as a <ul> inside its <li>, after the button; an open folder with nothing in it shows the text Empty there instead. A closed folder's contents aren't on the page. A file's <li> holds just its name, as text.
Don't change the tree you're given (it may be frozen). Each folder remembers whether it's open even when the tree changes around it: if a new folder appears before an open one, the open one stays open.
<FileTree tree={[{ name: "readme.md" }, { name: "src", children: [{ name: "index.js" }] }]} />
// draws, with src open:
<ul>
<li>
<button aria-expanded="true">src</button>
<ul>
<li>index.js</li>
</ul>
</li>
<li>readme.md</li>
</ul>
const tree = [
{ name: "readme.md" },
{ name: "src", children: [{ name: "index.js" }] },
{ name: "assets", children: [{ name: "logo.svg" }] },
{ name: "package.json" },
];
render(<FileTree tree={tree} />);
const top = screen.getAllByRole("list")[0];
expect([...top.children].map((item) => item.textContent)).toEqual(["assets", "src", "package.json", "readme.md"]);
const tree = [
{ name: "notes.txt" },
{ name: "README.md" },
{ name: "app.js" },
{ name: "src", children: [] },
{ name: "Public", children: [] },
];
render(<FileTree tree={tree} />);
const top = screen.getAllByRole("list")[0];
expect([...top.children].map((item) => item.textContent)).toEqual(["Public", "src", "app.js", "notes.txt", "README.md"]);
const tree = [
{ name: "readme.md" },
{ name: "src", children: [{ name: "index.js" }] },
];
render(<FileTree tree={tree} />);
expect(screen.getByRole("button", { name: "src" })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText("index.js")).not.toBeInTheDocument();
expect(screen.getByText("readme.md")).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "readme.md" })).not.toBeInTheDocument();
const tree = [
{
name: "src",
children: [
{ name: "index.js" },
{ name: "utils", children: [{ name: "format.js" }] },
{ name: "App.jsx" },
{ name: "components", children: [] },
],
},
];
render(<FileTree tree={tree} />);
const button = screen.getByRole("button", { name: "src" });
await user.click(button);
expect(button).toHaveAttribute("aria-expanded", "true");
const list = within(button.closest("li")).getByRole("list");
expect([...list.children].map((item) => item.textContent)).toEqual(["components", "utils", "App.jsx", "index.js"]);
const tree = [{ name: "src", children: [{ name: "index.js" }] }];
render(<FileTree tree={tree} />);
const button = screen.getByRole("button", { name: "src" });
await user.click(button);
await user.click(button);
expect(button).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText("index.js")).not.toBeInTheDocument();
const tree = [
{ name: "src", children: [{ name: "index.js" }, { name: "utils", children: [{ name: "format.js" }] }] },
];
render(<FileTree tree={tree} />);
await user.click(screen.getByRole("button", { name: "src" }));
await user.click(screen.getByRole("button", { name: "utils" }));
const format = screen.getByText("format.js");
expect(format.closest("li").parentElement.closest("li")).toBe(screen.getByRole("button", { name: "utils" }).closest("li"));
expect(within(screen.getByRole("button", { name: "src" }).closest("li")).getByText("format.js")).toBe(format);
const tree = [{ name: "src", children: [{ name: "components", children: [] }, { name: "index.js" }] }];
render(<FileTree tree={tree} />);
await user.click(screen.getByRole("button", { name: "src" }));
const components = screen.getByRole("button", { name: "components" });
expect(screen.queryByText("Empty")).not.toBeInTheDocument();
await user.click(components);
expect(within(components.closest("li")).getByText("Empty")).toBeInTheDocument();
const freeze = (nodes) =>
Object.freeze(nodes.map((node) => Object.freeze(node.children ? { ...node, children: freeze(node.children) } : { ...node })));
const tree = freeze([
{ name: "readme.md" },
{ name: "src", children: [{ name: "index.js" }, { name: "App.jsx" }] },
]);
render(<FileTree tree={tree} />);
await user.click(screen.getByRole("button", { name: "src" }));
expect(screen.getByText("App.jsx")).toBeInTheDocument();
expect(tree.map((node) => node.name)).toEqual(["readme.md", "src"]);
const tree = [
{ name: "assets", children: [{ name: "logo.svg" }] },
{ name: "src", children: [{ name: "index.js" }] },
];
const { rerender } = render(<FileTree tree={tree} />);
await user.click(screen.getByRole("button", { name: "src" }));
rerender(<FileTree tree={[{ name: "docs", children: [{ name: "guide.md" }] }, ...tree]} />);
expect(screen.getByRole("button", { name: "src" })).toHaveAttribute("aria-expanded", "true");
expect(screen.getByText("index.js")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "docs" })).toHaveAttribute("aria-expanded", "false");
expect(screen.queryByText("guide.md")).not.toBeInTheDocument();
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with File tree view. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.