The tea shop's filter panel groups teas into kinds, and kinds into families. Build App: it takes nodes, a list of tree nodes { id, label, children? }, and defaultChecked, a list of the leaf ids that start checked (default none). When nodes isn't given, as in the Preview, show a tree of your own. A node with a non-empty children list is a parent; any other node is a leaf. The tree can be any depth.
Render the tree as nested lists: a <ul> of <li>s, each holding a <label> with a checkbox and the node's label, and a parent's <li> also holding a <ul> of its children.
Checking a parent checks every node under it, at every depth; unchecking it unchecks them all. Checking or unchecking a leaf changes only that leaf.
A parent's box follows the leaves under it: checked when all of them are, unchecked when none are, and indeterminate when some are. An indeterminate box is unchecked, with its indeterminate property set to true; at every other time indeterminate is false. Clicking an indeterminate parent checks everything under it.
<App nodes={[
{ id: "green", label: "Green", children: [
{ id: "sencha", label: "Sencha" },
{ id: "matcha", label: "Matcha" },
] },
]} defaultChecked={["sencha"]} />
// Green is indeterminate, Sencha checked, Matcha not.
// Checking Matcha too makes Green checked.
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
expect(screen.getAllByRole("checkbox")).toHaveLength(11);
expect(screen.getAllByRole("checkbox").every((input) => !input.checked)).toBe(true);
const black = box("Black").closest("li");
expect(within(black).getAllByRole("checkbox").map((input) => input.labels[0].textContent.trim())).toEqual(["Black", "Assam", "Darjeeling", "First flush", "Second flush"]);
expect(box("Assam").closest("ul").closest("li")).toBe(black);
expect(box("First flush").closest("ul").closest("li")).toBe(box("Darjeeling").closest("li"));
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("Black"));
for (const name of ["Black", "Assam", "Darjeeling", "First flush", "Second flush"]) {
expect(box(name)).toBeChecked();
}
expect(box("Sencha")).not.toBeChecked();
expect(box("Rooibos")).not.toBeChecked();
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("All teas"));
expect(screen.getAllByRole("checkbox").every((input) => input.checked)).toBe(true);
await user.click(box("Green"));
for (const name of ["Green", "Sencha", "Matcha", "Gyokuro"]) {
expect(box(name)).not.toBeChecked();
}
expect(box("Assam")).toBeChecked();
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("Sencha"));
expect(box("Green").indeterminate).toBe(true);
expect(box("Green")).not.toBeChecked();
expect(box("All teas").indeterminate).toBe(true);
expect(box("Black").indeterminate).toBe(false);
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("Sencha"));
await user.click(box("Matcha"));
await user.click(box("Gyokuro"));
expect(box("Green")).toBeChecked();
expect(box("Green").indeterminate).toBe(false);
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("First flush"));
expect(box("Darjeeling").indeterminate).toBe(true);
expect(box("Black").indeterminate).toBe(true);
expect(box("All teas").indeterminate).toBe(true);
await user.click(box("Second flush"));
expect(box("Darjeeling")).toBeChecked();
expect(box("Black").indeterminate).toBe(true);
await user.click(box("Assam"));
expect(box("Black")).toBeChecked();
expect(box("Black").indeterminate).toBe(false);
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("Assam"));
await user.click(box("Black"));
for (const name of ["Black", "Assam", "Darjeeling", "First flush", "Second flush"]) {
expect(box(name)).toBeChecked();
}
expect(box("Black").indeterminate).toBe(false);
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} />);
await user.click(box("Matcha"));
await user.click(box("Matcha"));
expect(box("Green").indeterminate).toBe(false);
expect(box("Green")).not.toBeChecked();
expect(box("All teas").indeterminate).toBe(false);
const nodes = [
{
id: "all",
label: "All teas",
children: [
{ id: "green", label: "Green", children: [{ id: "sencha", label: "Sencha" }, { id: "matcha", label: "Matcha" }, { id: "gyokuro", label: "Gyokuro" }] },
{
id: "black",
label: "Black",
children: [
{ id: "assam", label: "Assam" },
{ id: "darjeeling", label: "Darjeeling", children: [{ id: "first-flush", label: "First flush" }, { id: "second-flush", label: "Second flush" }] },
],
},
{ id: "rooibos", label: "Rooibos" },
],
},
];
const box = (name) => screen.getByRole("checkbox", { name });
render(<App nodes={nodes} defaultChecked={["sencha", "matcha", "gyokuro", "first-flush"]} />);
expect(box("Green")).toBeChecked();
expect(box("Darjeeling").indeterminate).toBe(true);
expect(box("All teas").indeterminate).toBe(true);
expect(box("Assam")).not.toBeChecked();
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 Nested checkboxes. 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.