Skip to content

Commit 205c427

Browse files
committed
feat(Tree): add component * 2
1 parent 9c60559 commit 205c427

3 files changed

Lines changed: 69 additions & 3 deletions

File tree

src/components/content/Tree/Tree.test.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,40 @@ describe('<Tree />', () => {
6161
expect(info.node.key).toBe('fruits');
6262
});
6363

64+
it('reports the correct toggled node across multiple uncontrolled toggles', async () => {
65+
const onExpand = vi.fn();
66+
const { getAllByRole } = renderWithRoot(
67+
<Tree
68+
treeData={SAMPLE}
69+
defaultExpandedKeys={['fruits']}
70+
onExpand={onExpand}
71+
/>,
72+
);
73+
74+
const rows = getAllByRole('row');
75+
const fruitsToggle = rows[0].querySelector(
76+
'button[data-element="Toggle"]',
77+
) as HTMLButtonElement;
78+
const vegetablesToggle = rows[3].querySelector(
79+
'button[data-element="Toggle"]',
80+
) as HTMLButtonElement;
81+
82+
await act(async () => await userEvent.click(vegetablesToggle));
83+
let [, info] = onExpand.mock.calls[onExpand.mock.calls.length - 1];
84+
expect(info.expanded).toBe(true);
85+
expect(info.node.key).toBe('vegetables');
86+
87+
await act(async () => await userEvent.click(fruitsToggle));
88+
[, info] = onExpand.mock.calls[onExpand.mock.calls.length - 1];
89+
expect(info.expanded).toBe(false);
90+
expect(info.node.key).toBe('fruits');
91+
92+
await act(async () => await userEvent.click(vegetablesToggle));
93+
[, info] = onExpand.mock.calls[onExpand.mock.calls.length - 1];
94+
expect(info.expanded).toBe(false);
95+
expect(info.node.key).toBe('vegetables');
96+
});
97+
6498
describe('checkable mode', () => {
6599
it('renders checkboxes when isCheckable is true', () => {
66100
const { getAllByRole } = renderWithRoot(

src/components/content/Tree/Tree.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,16 +198,33 @@ function TreeBase(props: CubeTreeProps, ref: ForwardedRef<HTMLDivElement>) {
198198

199199
const loadDataController = useLoadData({ nodesByKey, loadData });
200200

201+
/**
202+
* Tracks the previously-expanded set so we can diff against the next set
203+
* inside `handleExpandedChange`. In controlled mode the parent owns the
204+
* truth (read from `effectiveExpandedKeys`); in uncontrolled mode this
205+
* ref is the only source for the previous state — `effectiveExpandedKeys`
206+
* is `undefined` and would otherwise produce an empty `previous` set,
207+
* mis-identifying every toggle.
208+
*/
209+
const previousExpandedRef = useRef<Set<Key>>(
210+
new Set<Key>(expandedKeys ?? defaultExpandedKeys ?? []),
211+
);
212+
201213
/**
202214
* Translate Stately's `Set<Key>` callbacks into the public AntD-flavoured
203215
* `Key[]` shape and dispatch the per-key load.
204216
*/
205217
const handleExpandedChange = useEvent((nextSet: Set<Key>) => {
206218
loadDataController.onExpandedChanged(nextSet);
207219

220+
const previous =
221+
expandedKeys !== undefined
222+
? new Set<Key>(effectiveExpandedKeys ?? [])
223+
: previousExpandedRef.current;
224+
previousExpandedRef.current = new Set<Key>(nextSet);
225+
208226
if (!onExpand) return;
209227
const nextArr: Key[] = Array.from(nextSet);
210-
const previous = new Set<Key>(effectiveExpandedKeys ?? []);
211228

212229
/**
213230
* Find the single key that toggled relative to the previous set.

src/components/content/Tree/use-checkbox-tree.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,14 @@ export function useCheckboxTree(opts: UseCheckboxTreeOptions): CheckboxTree {
145145
};
146146
}
147147

148-
if (isControlled) {
148+
/**
149+
* If the consumer passed the object shape (`{ checked, halfChecked }`),
150+
* they own the half-checked set and we use it as-is. With the array
151+
* shape we still need to derive `halfChecked` ourselves — the consumer
152+
* only provides `checked`, so parents should still light up
153+
* indeterminate when only some descendants are checked.
154+
*/
155+
if (isControlled && wantsObjectShape) {
149156
return {
150157
checkedSet: new Set(sourceChecked),
151158
halfCheckedSet: new Set(controlled!.halfChecked),
@@ -209,7 +216,15 @@ export function useCheckboxTree(opts: UseCheckboxTreeOptions): CheckboxTree {
209216
}
210217

211218
return { checkedSet: checked, halfCheckedSet: half };
212-
}, [isCheckable, isControlled, sourceChecked, controlled, treeData, index]);
219+
}, [
220+
isCheckable,
221+
isControlled,
222+
wantsObjectShape,
223+
sourceChecked,
224+
controlled,
225+
treeData,
226+
index,
227+
]);
213228

214229
const toggle = useEvent((key: string) => {
215230
const node = index.byKey.get(key);

0 commit comments

Comments
 (0)