Skip to content

Commit 597faad

Browse files
committed
feat: async checkbox propagation (wip tests)
1 parent b0ee382 commit 597faad

File tree

4 files changed

+122
-14
lines changed

4 files changed

+122
-14
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@headless-tree/core": patch
3+
---
4+
5+
Checkbox propagation is now supported for trees with async data loaders!

packages/core/src/features/checkboxes/checkboxes.spec.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, it, vi } from "vitest";
1+
import { describe, expect, it, vi } from "vitest";
22
import { TestTree } from "../../test-utils/test-tree";
33
import { checkboxesFeature } from "./feature";
44
import { CheckedState } from "./types";
@@ -12,22 +12,22 @@ describe("core-feature/checkboxes", () => {
1212
});
1313

1414
it("should check items", async () => {
15-
tree.item("x111").setChecked();
16-
tree.item("x112").setChecked();
15+
await tree.item("x111").setChecked();
16+
await tree.item("x112").setChecked();
1717
expect(tree.instance.getState().checkedItems).toEqual(["x111", "x112"]);
1818
});
1919

2020
it("should uncheck an item", async () => {
21-
tree.item("x111").setChecked();
22-
tree.item("x111").setUnchecked();
21+
await tree.item("x111").setChecked();
22+
await tree.item("x111").setUnchecked();
2323
expect(tree.instance.getState().checkedItems).not.toContain("x111");
2424
});
2525

2626
it("should toggle checked state", async () => {
2727
const item = tree.item("x111");
28-
item.toggleCheckedState();
28+
await item.toggleCheckedState();
2929
expect(tree.instance.getState().checkedItems).toContain("x111");
30-
item.toggleCheckedState();
30+
await item.toggleCheckedState();
3131
expect(tree.instance.getState().checkedItems).not.toContain("x111");
3232
});
3333

@@ -47,7 +47,7 @@ describe("core-feature/checkboxes", () => {
4747
});
4848

4949
it("should create indeterminate state", async () => {
50-
tree.item("x111").setChecked();
50+
await tree.item("x111").setChecked();
5151
const refObject = { indeterminate: undefined };
5252
tree.item("x11").getCheckboxProps().ref(refObject);
5353
await vi.waitFor(() => expect(refObject.indeterminate).toBe(true));

packages/core/src/features/checkboxes/feature.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
import {
2-
type FeatureImplementation,
3-
FeatureImplementation,
4-
TreeInstance,
5-
} from "../../types/core";
1+
import { type FeatureImplementation, TreeInstance } from "../../types/core";
62
import { makeStateUpdater } from "../../utils";
73
import { CheckedState } from "./types";
84
import { throwError } from "../../utilities/errors";
@@ -16,7 +12,7 @@ const getAllLoadedDescendants = <T>(
1612
return [itemId];
1713
}
1814
const descendants = tree
19-
.retrieveChildrenIds(itemId)
15+
.retrieveChildrenIds(itemId, true)
2016
.map((child) => getAllLoadedDescendants(tree, child, includeFolders))
2117
.flat();
2218
return includeFolders ? [itemId, ...descendants] : descendants;
@@ -27,6 +23,7 @@ const getAllDescendants = async <T>(
2723
itemId: string,
2824
includeFolders = false,
2925
): Promise<string[]> => {
26+
await tree.loadItemData(itemId);
3027
if (!tree.getConfig().isItemFolder(tree.getItemInstance(itemId))) {
3128
return [itemId];
3229
}
@@ -113,6 +110,7 @@ export const checkboxesFeature: FeatureImplementation = {
113110

114111
if (item.isFolder() && propagateCheckedState) {
115112
const descendants = getAllLoadedDescendants(tree, itemId);
113+
if (descendants.length === 0) return CheckedState.Unchecked;
116114
if (descendants.every((d) => checkedItems.includes(d))) {
117115
return CheckedState.Checked;
118116
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/* eslint-disable jsx-a11y/label-has-associated-control */
2+
import type { Meta } from "@storybook/react";
3+
import React, { useState } from "react";
4+
import {
5+
asyncDataLoaderFeature,
6+
checkboxesFeature,
7+
hotkeysCoreFeature,
8+
selectionFeature,
9+
} from "@headless-tree/core";
10+
import { useTree } from "@headless-tree/react";
11+
import cx from "classnames";
12+
import { DemoItem, createDemoData } from "../utils/data";
13+
14+
const meta = {
15+
title: "React/Checkboxes/Async Configurability",
16+
tags: ["feature/checkbox", "checkbox"],
17+
} satisfies Meta;
18+
19+
export default meta;
20+
21+
const { asyncDataLoader } = createDemoData();
22+
23+
// story-start
24+
export const AsyncConfigurability = () => {
25+
const [canCheckFolders, setCanCheckFolders] = useState(false);
26+
const [propagateCheckedState, setPropagateCheckedState] = useState(true);
27+
const tree = useTree<DemoItem>({
28+
rootItemId: "root",
29+
// initialState: { expandedItems: ["fruit", "berries"] },
30+
getItemName: (item) => item.getItemData().name,
31+
isItemFolder: (item) => !!item.getItemData().children,
32+
dataLoader: asyncDataLoader,
33+
createLoadingItemData: () => ({ name: "Loading..." }),
34+
indent: 20,
35+
canCheckFolders,
36+
propagateCheckedState,
37+
features: [
38+
asyncDataLoaderFeature,
39+
selectionFeature,
40+
checkboxesFeature,
41+
hotkeysCoreFeature,
42+
],
43+
});
44+
45+
return (
46+
<>
47+
<div>
48+
<label>
49+
<input
50+
type="checkbox"
51+
checked={canCheckFolders}
52+
onChange={(e) => setCanCheckFolders(e.target.checked)}
53+
/>
54+
Can check folders
55+
</label>
56+
<label>
57+
<input
58+
type="checkbox"
59+
checked={propagateCheckedState}
60+
onChange={(e) => setPropagateCheckedState(e.target.checked)}
61+
/>
62+
Propagate checked state
63+
</label>
64+
</div>
65+
<div {...tree.getContainerProps()} className="tree">
66+
{tree.getItems().map((item) => (
67+
<div className="outeritem" key={item.getId()}>
68+
<button
69+
{...item.getProps()}
70+
style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
71+
>
72+
<div
73+
className={cx("treeitem", {
74+
focused: item.isFocused(),
75+
expanded: item.isExpanded(),
76+
selected: item.isSelected(),
77+
folder: item.isFolder(),
78+
})}
79+
>
80+
{item.getItemName()}
81+
</div>
82+
</button>
83+
{!tree
84+
.getState()
85+
.loadingItemChildrens.some((child) =>
86+
tree.getItemInstance(child).isDescendentOf(item.getId()),
87+
) ? (
88+
<input type="checkbox" {...item.getCheckboxProps()} />
89+
) : (
90+
<>Loading</>
91+
)}
92+
</div>
93+
))}
94+
</div>
95+
<pre>Checked Items: {JSON.stringify(tree.getState().checkedItems)}</pre>
96+
<pre>
97+
Loading Item Children:{" "}
98+
{JSON.stringify(tree.getState().loadingItemChildrens)}
99+
</pre>
100+
<pre>
101+
Loading Item Data: {JSON.stringify(tree.getState().loadingItemData)}
102+
</pre>
103+
</>
104+
);
105+
};

0 commit comments

Comments
 (0)