Skip to content

Commit 3d8a8b1

Browse files
fix(treeview): revert breaking API change in controlled mode (#19978)
Co-authored-by: Gururaj J <89023023+Gururajj77@users.noreply.github.com>
1 parent 0897c80 commit 3d8a8b1

File tree

3 files changed

+80
-59
lines changed

3 files changed

+80
-59
lines changed

packages/react/src/components/TreeView/TreeContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import { createContext, type SyntheticEvent, type MouseEvent } from 'react';
8+
import { createContext } from 'react';
99
import type { TreeNodeProps } from './TreeNode';
1010

1111
interface TreeContextProps {
1212
active?: string | number;
1313
multiselect?: boolean;
1414
onActivate?: (nodeId?: string | number) => void;
15-
onTreeSelect?: (event: MouseEvent, node?: Partial<TreeNodeProps>) => void;
15+
onTreeSelect?: TreeNodeProps['onTreeSelect'];
1616
selected?: Array<string | number>;
1717
size?: 'xs' | 'sm';
1818
}

packages/react/src/components/TreeView/TreeNode.tsx

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ import { useFeatureFlag } from '../FeatureFlags';
2828
import { IconButton } from '../IconButton';
2929
import { TreeContext, DepthContext } from './TreeContext';
3030

31+
type UncontrolledOnToggle = (
32+
event: React.MouseEvent | React.KeyboardEvent,
33+
node: Pick<TreeNodeProps, 'id' | 'label' | 'value' | 'isExpanded'>
34+
) => void;
35+
36+
type ControlledOnToggle = (isExpanded: TreeNodeProps['isExpanded']) => void;
37+
3138
export type TreeNodeProps = {
3239
/**
3340
* **Note:** this is controlled by the parent TreeView component, do not set manually.
@@ -76,20 +83,20 @@ export type TreeNodeProps = {
7683
* Callback function for when the node is selected
7784
*/
7885
onSelect?: (
79-
event: React.MouseEvent,
80-
node?: Omit<TreeNodeProps, 'children'>
86+
event: React.MouseEvent | React.KeyboardEvent,
87+
node: Pick<TreeNodeProps, 'id' | 'label' | 'value'>
8188
) => void;
8289
/**
8390
* Callback function for when a parent node is expanded or collapsed
8491
*/
85-
onToggle?: (
86-
event: React.MouseEvent | React.KeyboardEvent,
87-
node?: Omit<TreeNodeProps, 'children'>
88-
) => void;
92+
onToggle?: UncontrolledOnToggle | ControlledOnToggle;
8993
/**
9094
* Callback function for when any node in the tree is selected
9195
*/
92-
onTreeSelect?: (event: React.MouseEvent, node?: TreeNodeProps) => void;
96+
onTreeSelect?: (
97+
event: React.MouseEvent | React.KeyboardEvent,
98+
node: Pick<TreeNodeProps, 'id' | 'label' | 'value'>
99+
) => void;
93100
/**
94101
* A component used to render an icon.
95102
*/
@@ -274,14 +281,7 @@ const TreeNode = React.forwardRef<HTMLElement, TreeNodeProps>(
274281

275282
const controllableExpandedState = useControllableState({
276283
value: isExpanded,
277-
onChange: (newValue: boolean) => {
278-
onToggle?.(undefined as unknown as MouseEvent, {
279-
id,
280-
isExpanded: newValue,
281-
label,
282-
value,
283-
});
284-
},
284+
onChange: onToggle as ControlledOnToggle,
285285
defaultValue: defaultIsExpanded ?? false,
286286
});
287287
const uncontrollableExpandedState = useState(isExpanded ?? false);
@@ -360,7 +360,12 @@ const TreeNode = React.forwardRef<HTMLElement, TreeNodeProps>(
360360
}
361361

362362
if (!enableTreeviewControllable) {
363-
onToggle?.(event, { id, isExpanded: !expanded, label, value });
363+
(onToggle as UncontrolledOnToggle)?.(event, {
364+
id,
365+
isExpanded: !expanded,
366+
label,
367+
value,
368+
});
364369
}
365370
setExpanded(!expanded);
366371
}
@@ -406,7 +411,12 @@ const TreeNode = React.forwardRef<HTMLElement, TreeNodeProps>(
406411

407412
if (children && expanded) {
408413
if (!enableTreeviewControllable) {
409-
onToggle?.(event, { id, isExpanded: false, label, value });
414+
(onToggle as UncontrolledOnToggle)?.(event, {
415+
id,
416+
isExpanded: false,
417+
label,
418+
value,
419+
});
410420
}
411421
setExpanded(false);
412422
} else {
@@ -438,7 +448,12 @@ const TreeNode = React.forwardRef<HTMLElement, TreeNodeProps>(
438448
)?.focus();
439449
} else {
440450
if (!enableTreeviewControllable) {
441-
onToggle?.(event, { id, isExpanded: true, label, value });
451+
(onToggle as UncontrolledOnToggle)?.(event, {
452+
id,
453+
isExpanded: true,
454+
label,
455+
value,
456+
});
442457
}
443458
setExpanded(true);
444459
}
@@ -449,7 +464,12 @@ const TreeNode = React.forwardRef<HTMLElement, TreeNodeProps>(
449464
if (match(event, keys.Enter) && children) {
450465
// Toggle expansion state for parent nodes
451466
if (!enableTreeviewControllable) {
452-
onToggle?.(event, { id, isExpanded: !expanded, label, value });
467+
(onToggle as UncontrolledOnToggle)?.(event, {
468+
id,
469+
isExpanded: !expanded,
470+
label,
471+
value,
472+
});
453473
}
454474
setExpanded(!expanded);
455475
}

packages/react/src/components/TreeView/TreeView.tsx

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,24 @@
77

88
import classNames from 'classnames';
99
import PropTypes from 'prop-types';
10-
import React, {
11-
useEffect,
12-
useRef,
13-
useState,
14-
useMemo,
15-
type JSX,
16-
type SyntheticEvent,
17-
} from 'react';
10+
import React, { useEffect, useRef, useState, useMemo, type JSX } from 'react';
1811
import { keys, match, matches } from '../../internal/keyboard';
1912
import { useControllableState } from '../../internal/useControllableState';
2013
import { usePrefix } from '../../internal/usePrefix';
2114
import { useId } from '../../internal/useId';
2215
import { useFeatureFlag } from '../FeatureFlags';
23-
import TreeNode, { TreeNodeProps } from './TreeNode';
16+
import TreeNode, { type TreeNodeProps } from './TreeNode';
2417
import { TreeContext, DepthContext } from './TreeContext';
2518

19+
type UncontrolledOnSelect = (
20+
event: React.MouseEvent | React.KeyboardEvent,
21+
payload: Parameters<NonNullable<TreeNodeProps['onSelect']>>[1] & {
22+
activeNodeId?: TreeViewProps['active'];
23+
}
24+
) => void;
25+
26+
type ControlledOnSelect = (selected: TreeViewProps['selected']) => void;
27+
2628
export type TreeViewProps = {
2729
/**
2830
* Mark the active node in the tree, represented by its ID
@@ -53,16 +55,11 @@ export type TreeViewProps = {
5355
* **[Experimental]** Callback function that is called when any node is activated.
5456
* *This is only supported with the `enable-treeview-controllable` feature flag!*
5557
*/
56-
onActivate?: (activated?: string | number) => void;
58+
onActivate?: (active: TreeViewProps['active']) => void;
5759
/**
5860
* Callback function that is called when any node is selected
5961
*/
60-
onSelect?: (
61-
event: React.SyntheticEvent<HTMLUListElement>,
62-
payload?: Partial<TreeNodeProps> & {
63-
activeNodeId?: string | number;
64-
}
65-
) => void;
62+
onSelect?: UncontrolledOnSelect | ControlledOnSelect;
6663
/**
6764
* Array representing all selected node IDs in the tree
6865
*/
@@ -108,11 +105,7 @@ const TreeView: TreeViewComponent = ({
108105

109106
const controllableSelectionState = useControllableState({
110107
value: preselected,
111-
onChange: (newSelected) => {
112-
onSelect?.(undefined as unknown as SyntheticEvent<HTMLUListElement>, {
113-
activeNodeId: newSelected[0],
114-
});
115-
},
108+
onChange: onSelect as ControlledOnSelect,
116109
defaultValue: [],
117110
});
118111
const uncontrollableSelectionState = useState(preselected ?? []);
@@ -139,24 +132,32 @@ const TreeView: TreeViewComponent = ({
139132
);
140133
}
141134

142-
function handleTreeSelect(event, node = {}) {
143-
const nodeId = (node as any).id;
144-
if (multiselect && (event.metaKey || event.ctrlKey)) {
145-
if (!selected.includes(nodeId)) {
146-
setSelected(selected.concat(nodeId));
147-
} else {
148-
setSelected(selected.filter((selectedId) => selectedId !== nodeId));
149-
}
150-
151-
if (!enableTreeviewControllable) {
152-
onSelect?.(event, node);
153-
}
154-
} else {
155-
setSelected([nodeId]);
156-
setActive(nodeId);
135+
function handleTreeSelect(
136+
event,
137+
node: Parameters<NonNullable<TreeNodeProps['onTreeSelect']>>[1]
138+
) {
139+
const nodeId = node.id;
140+
if (nodeId) {
141+
if (multiselect && (event.metaKey || event.ctrlKey)) {
142+
if (!selected.includes(nodeId)) {
143+
setSelected(selected.concat(nodeId));
144+
} else {
145+
setSelected(selected.filter((selectedId) => selectedId !== nodeId));
146+
}
157147

158-
if (!enableTreeviewControllable) {
159-
onSelect?.(event, { activeNodeId: nodeId, ...node });
148+
if (!enableTreeviewControllable) {
149+
(onSelect as UncontrolledOnSelect)?.(event, node);
150+
}
151+
} else {
152+
setSelected([nodeId]);
153+
setActive(nodeId);
154+
155+
if (!enableTreeviewControllable) {
156+
(onSelect as UncontrolledOnSelect)?.(event, {
157+
activeNodeId: nodeId,
158+
...node,
159+
});
160+
}
160161
}
161162
}
162163
}

0 commit comments

Comments
 (0)