} | [] |
61 | | checkStrictly | check node precisely, parent and children nodes are not associated | bool | false |
62 | | className | additional css class of root dom node | String | '' |
63 | | defaultCheckedKeys | default checked treeNodes | String[] | [] |
64 | | defaultExpandedKeys | expand specific treeNodes | String[] | [] |
65 | | defaultExpandAll | expand all treeNodes | bool | false |
66 | | defaultExpandParent | auto expand parent treeNodes when init | bool | true |
67 | | defaultSelectedKeys | default selected treeNodes | String[] | [] |
68 | | disabled | whether disabled the tree | bool | false |
69 | | draggable | whether can drag treeNode. (drag events are not supported in Internet Explorer 8 and earlier versions or Safari 5.1 and earlier versions.) | bool \| ({ node }) => boolean | false |
70 | | expandedKeys | Controlled expand specific treeNodes | String[] | - |
71 | | filterTreeNode | filter some treeNodes as you need. it should return true | function(node) | - |
72 | | icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - |
73 | | loadedKeys | Mark node is loaded when `loadData` is true | String[] | - |
74 | | loadData | load data asynchronously and the return value should be a promise | function(node) | - |
75 | | multiple | whether multiple select | bool | false |
76 | | prefixCls | prefix class | String | 'rc-tree' |
77 | | selectable | whether can be selected | bool | true |
78 | | selectedKeys | Controlled selected treeNodes(After setting, defaultSelectedKeys will not work) | String[] | [] |
79 | | showIcon | whether show icon | bool | true |
80 | | showLine | whether show line | bool | false |
81 | | treeData | treeNodes data Array, if set it then you need not to construct children TreeNode. (value should be unique across the whole array) | array<{key,title,children, [disabled, selectable]}> | - |
82 | | onCheck | click the treeNode/checkbox to fire | function(checkedKeys, e:{checked: bool, checkedNodes, node, event, nativeEvent}) | - |
83 | | onExpand | fire on treeNode expand or not | function(expandedKeys, {expanded: bool, node, nativeEvent}) | - |
84 | | onDragEnd | it execs when fire the tree's dragend event | function({event,node}) | - |
85 | | onDragEnter | it execs when fire the tree's dragenter event | function({event,node,expandedKeys}) | - |
86 | | onDragLeave | it execs when fire the tree's dragleave event | function({event,node}) | - |
87 | | onDragOver | it execs when fire the tree's dragover event | function({event,node}) | - |
88 | | onDragStart | it execs when fire the tree's dragstart event | function({event,node}) | - |
89 | | onDrop | it execs when fire the tree's drop event | function({event, node, dragNode, dragNodesKeys}) | - |
90 | | onLoad | Trigger when a node is loaded. If you set the `loadedKeys`, you must handle `onLoad` to avoid infinity loop | function(loadedKeys, {event, node}) | - |
91 | | onMouseEnter | call when mouse enter a treeNode | function({event,node}) | - |
92 | | onMouseLeave | call when mouse leave a treeNode | function({event,node}) | - |
93 | | onRightClick | select current treeNode and show customized contextmenu | function({event,node}) | - |
94 | | onSelect | click the treeNode to fire | function(selectedKeys, e:{selected: bool, selectedNodes, node, event, nativeEvent}) | - |
95 | | switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - |
96 | | virtual | Disable virtual scroll when `false` | boolean | - |
97 | | allowDrop | Whether to allow drop on node | ({ dragNode, dropNode, dropPosition }) => boolean | - |
98 | | dropIndicatorRender | The indicator to render when dragging | ({ dropPosition, dropLevelOffset, indent: number, prefixCls }) => ReactNode| - |
99 | | direction | Display direction of the tree, it may affect dragging behavior | `ltr` \| `rtl` | - |
100 | | expandAction | Tree open logic, optional: false \| `click` \| `doubleClick` | string \| boolean | `click` |
101 |
102 | ### TreeNode props
103 |
104 | > note: if you have a lot of TreeNode, like more than 1000,
105 | > make the parent node is collapsed by default, will obvious effect, very fast.
106 | > Because the children hide TreeNode will not insert into dom.
107 |
108 | | name | description | type | default |
109 | | --- | --- | --- | --- |
110 | | className | additional class to treeNode | String | '' |
111 | | checkable | control node checkable if Tree is checkable | bool | false |
112 | | style | set style to treeNode | Object | '' |
113 | | disabled | whether disabled the treeNode | bool | false |
114 | | disableCheckbox | whether disable the treeNode' checkbox | bool | false |
115 | | title | tree/subTree's title | String/element/((data: DataNode) => React.ReactNode) | '---' |
116 | | key | it's used with tree props's (default)ExpandedKeys / (default)CheckedKeys / (default)SelectedKeys. you'd better to set it, and it must be unique in the tree's all treeNodes | String | treeNode's position |
117 | | isLeaf | whether it's leaf node | bool | false |
118 | | icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - |
119 | | switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - |
120 |
121 | ## Note
122 |
123 | The number of treeNodes can be very large, but when enable `checkable`, it will spend more computing time, so we cached some calculations(e.g. `this.treeNodesStates`), to avoid double computing. But, this bring some restrictions, **when you async load treeNodes, you should render tree like this** `{this.state.treeData.length ? {this.state.treeData.map(t => )} : 'loading tree'}`
124 |
125 | ## Development
126 |
127 | ```bash
128 | npm install
129 | npm start
130 | ```
131 |
132 | ## Test Case
133 |
134 | http://localhost:8018/tests/runner.html?coverage
135 |
136 | ## Coverage
137 |
138 | http://localhost:8018/node_modules/rc-server/node_modules/node-jscover/lib/front-end/jscoverage.html?w=http://localhost:8018/tests/runner.html?coverage
139 |
140 | ## License
141 |
142 | rc-tree is released under the MIT license.
143 |
144 | ## Other tree views
145 |
146 | - [zTree](http://www.treejs.cn/)
147 | - [jqTree](https://mbraak.github.io/jqTree/)
148 | - [jquery.treeselect](https://travistidwell.com/jquery.treeselect.js/)
149 | - [Angular Multi Select Tree](https://a5hik.github.io/angular-multi-select-tree/)
150 |
--------------------------------------------------------------------------------
/assets/icons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/tree/a45cf7bc879a5b928a2e21313553f6cc8739aa7d/assets/icons.png
--------------------------------------------------------------------------------
/assets/line.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/tree/a45cf7bc879a5b928a2e21313553f6cc8739aa7d/assets/line.gif
--------------------------------------------------------------------------------
/assets/loading.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/react-component/tree/a45cf7bc879a5b928a2e21313553f6cc8739aa7d/assets/loading.gif
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/demo/animation-draggable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Animation Draggable
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/animation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Animation
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/basic-controlled.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Basic Controlled
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/basic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Basic
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/big-data.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Big Data
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/contextmenu.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Context Menu
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/custom-switch-icon.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Custom Switch Icon
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/draggable-allow-drop.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Draggable Allow Drop
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/draggable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Draggable
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/dropdown.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dropdown
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/dynamic.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Dynamic
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/expandAction.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Expand Action
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/fieldNames.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Field Names
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/funtionTitle.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Function Title
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/icon.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Icon
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/demo/selectable.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Selectable
3 | nav:
4 | title: Demo
5 | path: /demo
6 | ---
7 |
8 |
9 |
--------------------------------------------------------------------------------
/docs/examples/animation-draggable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate,
2 | react/no-danger, no-param-reassign */
3 | import React from 'react';
4 | import { gData } from './utils/dataUtil';
5 | import '../../assets/index.less';
6 | import Tree from '@rc-component/tree';
7 |
8 | const STYLE = `
9 | .rc-tree-child-tree {
10 | display: block;
11 | }
12 |
13 | .node-motion {
14 | transition: all .3s;
15 | overflow-y: hidden;
16 | }
17 | `;
18 |
19 | const motion = {
20 | motionName: 'node-motion',
21 | motionAppear: false,
22 | onAppearStart: node => {
23 | console.log('Start Motion:', node);
24 | return { height: 0 };
25 | },
26 | onAppearActive: node => ({ height: node.scrollHeight }),
27 | onLeaveStart: node => ({ height: node.offsetHeight }),
28 | onLeaveActive: () => ({ height: 0 }),
29 | };
30 |
31 | // const gData = [
32 | // { title: '0-0', key: '0-0' },
33 | // { title: '0-1', key: '0-1' },
34 | // { title: '0-2', key: '0-2', children: [{ title: '0-2-0', key: '0-2-0' }] },
35 | // ];
36 |
37 | class Demo extends React.Component {
38 | state = {
39 | gData,
40 | autoExpandParent: true,
41 | expandedKeys: ['0-0-key', '0-0-0-key', '0-0-0-0-key'],
42 | };
43 |
44 | onDragEnter = ({ expandedKeys }) => {
45 | console.log('enter', expandedKeys);
46 | this.setState({
47 | expandedKeys,
48 | });
49 | };
50 |
51 | onDrop = info => {
52 | console.log('drop', info);
53 | const dropKey = info.node.props.eventKey;
54 | const dragKey = info.dragNode.props.eventKey;
55 | const dropPos = info.node.props.pos.split('-');
56 | const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
57 |
58 | const loop = (data, key, callback) => {
59 | data.forEach((item, index, arr) => {
60 | if (item.key === key) {
61 | callback(item, index, arr);
62 | return;
63 | }
64 | if (item.children) {
65 | loop(item.children, key, callback);
66 | }
67 | });
68 | };
69 | const data = [...this.state.gData];
70 |
71 | // Find dragObject
72 | let dragObj;
73 | loop(data, dragKey, (item, index, arr) => {
74 | arr.splice(index, 1);
75 | dragObj = item;
76 | });
77 |
78 | if (!info.dropToGap) {
79 | // Drop on the content
80 | loop(data, dropKey, item => {
81 | item.children = item.children || [];
82 | // where to insert 示例添加到尾部,可以是随意位置
83 | item.children.push(dragObj);
84 | });
85 | } else if (
86 | (info.node.props.children || []).length > 0 && // Has children
87 | info.node.props.expanded && // Is expanded
88 | dropPosition === 1 // On the bottom gap
89 | ) {
90 | loop(data, dropKey, item => {
91 | item.children = item.children || [];
92 | // where to insert 示例添加到尾部,可以是随意位置
93 | item.children.unshift(dragObj);
94 | });
95 | } else {
96 | // Drop on the gap
97 | let ar;
98 | let i;
99 | loop(data, dropKey, (item, index, arr) => {
100 | ar = arr;
101 | i = index;
102 | });
103 | if (dropPosition === -1) {
104 | ar.splice(i, 0, dragObj);
105 | } else {
106 | ar.splice(i + 1, 0, dragObj);
107 | }
108 | }
109 |
110 | this.setState({
111 | gData: data,
112 | });
113 | };
114 |
115 | onExpand = expandedKeys => {
116 | console.log('onExpand', expandedKeys);
117 | this.setState({
118 | expandedKeys,
119 | autoExpandParent: false,
120 | });
121 | };
122 |
123 | render() {
124 | const { expandedKeys } = this.state;
125 |
126 | return (
127 |
128 |
129 |
130 |
draggable
131 |
drag a node into another node
132 |
143 |
144 | );
145 | }
146 | }
147 |
148 | export default Demo;
149 |
--------------------------------------------------------------------------------
/docs/examples/animation.jsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0, react/no-danger: 0 */
2 | import { Provider } from '@rc-component/motion';
3 | import Tree from '@rc-component/tree';
4 | import React from 'react';
5 | import '../../assets/index.less';
6 | import './animation.less';
7 |
8 | const STYLE = `
9 | .rc-tree-child-tree {
10 | display: block;
11 | }
12 |
13 | .node-motion {
14 | transition: all .3s;
15 | overflow-y: hidden;
16 | }
17 | `;
18 |
19 | const defaultExpandedKeys = ['0', '0-2', '0-9-2'];
20 |
21 | const motion = {
22 | motionName: 'node-motion',
23 | motionAppear: false,
24 | onAppearStart: () => ({ height: 0 }),
25 | onAppearActive: node => ({ height: node.scrollHeight }),
26 | onLeaveStart: node => ({ height: node.offsetHeight }),
27 | onLeaveActive: () => ({ height: 0 }),
28 | };
29 |
30 | function getTreeData() {
31 | // big-data: generateData(1000, 3, 2)
32 | return [
33 | {
34 | key: '0',
35 | title: 'node 0',
36 | children: [
37 | { key: '0-0', title: 'node 0-0' },
38 | { key: '0-1', title: 'node 0-1' },
39 | {
40 | key: '0-2',
41 | title: 'node 0-2',
42 | children: [
43 | { key: '0-2-0', title: 'node 0-2-0' },
44 | { key: '0-2-1', title: 'node 0-2-1' },
45 | { key: '0-2-2', title: 'node 0-2-2' },
46 | ],
47 | },
48 | { key: '0-3', title: 'node 0-3' },
49 | { key: '0-4', title: 'node 0-4' },
50 | { key: '0-5', title: 'node 0-5' },
51 | { key: '0-6', title: 'node 0-6' },
52 | { key: '0-7', title: 'node 0-7' },
53 | { key: '0-8', title: 'node 0-8' },
54 | {
55 | key: '0-9',
56 | title: 'node 0-9',
57 | children: [
58 | { key: '0-9-0', title: 'node 0-9-0' },
59 | {
60 | key: '0-9-1',
61 | title: 'node 0-9-1',
62 | children: [
63 | { key: '0-9-1-0', title: 'node 0-9-1-0' },
64 | { key: '0-9-1-1', title: 'node 0-9-1-1' },
65 | { key: '0-9-1-2', title: 'node 0-9-1-2' },
66 | { key: '0-9-1-3', title: 'node 0-9-1-3' },
67 | { key: '0-9-1-4', title: 'node 0-9-1-4' },
68 | ],
69 | },
70 | {
71 | key: '0-9-2',
72 | title: 'node 0-9-2',
73 | children: [
74 | { key: '0-9-2-0', title: 'node 0-9-2-0' },
75 | { key: '0-9-2-1', title: 'node 0-9-2-1' },
76 | ],
77 | },
78 | ],
79 | },
80 | ],
81 | },
82 | {
83 | key: '1',
84 | title: 'node 1',
85 | // children: new Array(1000)
86 | // .fill(null)
87 | // .map((_, index) => ({ title: `auto ${index}`, key: `auto-${index}` })),
88 | children: [
89 | {
90 | key: '1-0',
91 | title: 'node 1-0',
92 | children: [
93 | { key: '1-0-0', title: 'node 1-0-0' },
94 | {
95 | key: '1-0-1',
96 | title: 'node 1-0-1',
97 | children: [
98 | { key: '1-0-1-0', title: 'node 1-0-1-0' },
99 | { key: '1-0-1-1', title: 'node 1-0-1-1' },
100 | ],
101 | },
102 | { key: '1-0-2', title: 'node 1-0-2' },
103 | ],
104 | },
105 | ],
106 | },
107 | ];
108 | }
109 |
110 | const Demo = () => {
111 | const treeRef = React.useRef();
112 | const [enableMotion, setEnableMotion] = React.useState(true);
113 |
114 | setTimeout(() => {
115 | treeRef.current.scrollTo({ key: '0-9-2' });
116 | }, 100);
117 |
118 | return (
119 |
120 |
127 |
128 |
129 |
130 |
expanded
131 |
132 |
133 |
134 |
135 |
With Virtual
136 |
147 |
148 |
149 |
Without Virtual
150 |
157 |
158 |
159 |
160 |
161 |
162 | );
163 | };
164 |
165 | export default Demo;
166 |
--------------------------------------------------------------------------------
/docs/examples/animation.less:
--------------------------------------------------------------------------------
1 | .animation {
2 | .rc-tree-treenode {
3 | display: flex;
4 |
5 | .rc-tree-indent {
6 | position: relative;
7 | align-self: stretch;
8 | display: flex;
9 | flex-wrap: nowrap;
10 | align-items: stretch;
11 |
12 | .rc-tree-indent-unit {
13 | position: relative;
14 | height: 100%;
15 |
16 | &::before {
17 | position: absolute;
18 | top: 0;
19 | bottom: 0;
20 | border-right: 1px solid blue;
21 | left: 50%;
22 | content: '';
23 | }
24 |
25 | &-end {
26 | &::before {
27 | display: none;
28 | }
29 | }
30 |
31 | // End should ignore
32 | // &-end {
33 | // &::before {
34 | // display: none;
35 | // }
36 | // }
37 | }
38 | }
39 |
40 | .rc-tree-switcher-noop {
41 | &::before {
42 | content: '';
43 | display: inline-block;
44 | width: 16px;
45 | height: 16px;
46 | background: red;
47 | border-radius: 100%;
48 | }
49 | }
50 |
51 | // Motion
52 | &-motion:not(.node-motion-appear-active) {
53 | // .rc-tree-indent-unit {
54 | // &::before {
55 | // display: none;
56 | // }
57 | // }
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/docs/examples/basic-controlled.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-unescaped-entities */
2 | import '../../assets/index.less';
3 | import React from 'react';
4 | import '@rc-component/dialog/assets/index.css';
5 | import Modal from '@rc-component/dialog';
6 | import Tree, { TreeNode } from '@rc-component/tree';
7 | import { gData, getRadioSelectKeys } from './utils/dataUtil';
8 |
9 | class Demo extends React.Component {
10 | static defaultProps = {
11 | multiple: true,
12 | };
13 |
14 | state = {
15 | // expandedKeys: getFilterExpandedKeys(gData, ['0-0-0-key']),
16 | expandedKeys: ['0-0-0-key'],
17 | autoExpandParent: true,
18 | // checkedKeys: ['0-0-0-0-key', '0-0-1-0-key', '0-1-0-0-key'],
19 | checkedKeys: ['0-0-0-key'],
20 | checkStrictlyKeys: { checked: ['0-0-1-key'], halfChecked: [] },
21 | selectedKeys: [],
22 | treeData: [],
23 | };
24 |
25 | onExpand = expandedKeys => {
26 | console.log('onExpand', expandedKeys);
27 | // if not set autoExpandParent to false, if children expanded, parent can not collapse.
28 | // or, you can remove all expanded chilren keys.
29 | this.setState({
30 | expandedKeys,
31 | autoExpandParent: false,
32 | });
33 | };
34 |
35 | onCheck = checkedKeys => {
36 | this.setState({
37 | checkedKeys,
38 | });
39 | };
40 |
41 | onCheckStrictly = checkedKeys => {
42 | console.log(checkedKeys);
43 | // const { checkedNodesPositions } = extra;
44 | // const pps = filterParentPosition(checkedNodesPositions.map(i => i.pos));
45 | // console.log(checkedNodesPositions.filter(i => pps.indexOf(i.pos) > -1).map(i => i.node.key));
46 | const cks = {
47 | checked: checkedKeys.checked || checkedKeys,
48 | halfChecked: [`0-0-${parseInt(Math.random() * 3, 10)}-key`],
49 | };
50 | this.setState({
51 | // checkedKeys,
52 | checkStrictlyKeys: cks,
53 | // checkStrictlyKeys: checkedKeys,
54 | });
55 | };
56 |
57 | onSelect = (selectedKeys, info) => {
58 | console.log('onSelect', selectedKeys, info);
59 | this.setState({
60 | selectedKeys,
61 | });
62 | };
63 |
64 | onRbSelect = (selectedKeys, info) => {
65 | let newSelectedKeys = selectedKeys;
66 | if (info.selected) {
67 | newSelectedKeys = getRadioSelectKeys(gData, selectedKeys, info.node.props.eventKey);
68 | }
69 | this.setState({
70 | selectedKeys: newSelectedKeys,
71 | });
72 | };
73 |
74 | onClose = () => {
75 | this.setState({
76 | visible: false,
77 | });
78 | };
79 |
80 | handleOk = () => {
81 | this.setState({
82 | visible: false,
83 | });
84 | };
85 |
86 | showModal = () => {
87 | this.setState({
88 | expandedKeys: ['0-0-0-key', '0-0-1-key'],
89 | checkedKeys: ['0-0-0-key'],
90 | visible: true,
91 | });
92 | // simulate Ajax
93 | setTimeout(() => {
94 | this.setState({
95 | treeData: [...gData],
96 | });
97 | }, 2000);
98 | };
99 |
100 | triggerChecked = () => {
101 | this.setState({
102 | checkedKeys: [`0-0-${parseInt(Math.random() * 3, 10)}-key`],
103 | });
104 | };
105 |
106 | render() {
107 | const loop = data =>
108 | data.map(item => {
109 | if (item.children) {
110 | return (
111 |
112 | {loop(item.children)}
113 |
114 | );
115 | }
116 | return ;
117 | });
118 | // console.log(getRadioSelectKeys(gData, this.state.selectedKeys));
119 | return (
120 |
121 |
dialog
122 |
125 |
131 | {this.state.treeData.length ? (
132 |
141 | {loop(this.state.treeData)}
142 |
143 | ) : (
144 | 'loading...'
145 | )}
146 |
147 |
148 | controlled
149 |
159 | {loop(gData)}
160 |
161 |
164 |
165 | checkStrictly
166 |
176 | {loop(gData)}
177 |
178 |
179 | radio's behavior select (in the same level)
180 |
186 | {loop(gData)}
187 |
188 |
189 | );
190 | }
191 | }
192 |
193 | export default Demo;
194 |
--------------------------------------------------------------------------------
/docs/examples/basic.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-alert, no-console, react/no-find-dom-node */
2 | import React from 'react';
3 | import '../../assets/index.less';
4 | import './basic.less';
5 | import Tree, { TreeNode } from '@rc-component/tree';
6 |
7 | const treeData = [
8 | {
9 | key: '0-0',
10 | title: 'parent 1',
11 | children: [
12 | { key: '0-0-0', title: 'parent 1-1', children: [{ key: '0-0-0-0', title: 'parent 1-1-0' }] },
13 | {
14 | key: '0-0-1',
15 | title: 'parent 1-2',
16 | children: [
17 | { key: '0-0-1-0', title: 'parent 1-2-0', disableCheckbox: true },
18 | { key: '0-0-1-1', title: 'parent 1-2-1' },
19 | { key: '0-0-1-2', title: 'parent 1-2-2' },
20 | { key: '0-0-1-3', title: 'parent 1-2-3' },
21 | { key: '0-0-1-4', title: 'parent 1-2-4' },
22 | { key: '0-0-1-5', title: 'parent 1-2-5' },
23 | { key: '0-0-1-6', title: 'parent 1-2-6' },
24 | { key: '0-0-1-7', title: 'parent 1-2-7' },
25 | { key: '0-0-1-8', title: 'parent 1-2-8' },
26 | { key: '0-0-1-9', title: 'parent 1-2-9' },
27 | { key: 1128, title: 1128 },
28 | ],
29 | },
30 | ],
31 | },
32 | ];
33 |
34 | class Demo extends React.Component {
35 | static defaultProps = {
36 | keys: ['0-0-0-0'],
37 | };
38 |
39 | constructor(props) {
40 | super(props);
41 | const { keys } = props;
42 | this.state = {
43 | defaultExpandedKeys: keys,
44 | defaultSelectedKeys: keys,
45 | defaultCheckedKeys: keys,
46 | };
47 |
48 | this.treeRef = React.createRef();
49 | }
50 |
51 | onExpand = expandedKeys => {
52 | console.log('onExpand', expandedKeys);
53 | };
54 |
55 | onSelect = (selectedKeys, info) => {
56 | console.log('selected', selectedKeys, info);
57 | this.selKey = info.node.props.eventKey;
58 | };
59 |
60 | onCheck = (checkedKeys, info) => {
61 | console.log('onCheck', checkedKeys, info);
62 | };
63 |
64 | onEdit = () => {
65 | setTimeout(() => {
66 | console.log('current key: ', this.selKey);
67 | }, 0);
68 | };
69 |
70 | onDel = e => {
71 | if (!window.confirm('sure to delete?')) {
72 | return;
73 | }
74 | e.stopPropagation();
75 | };
76 |
77 | setTreeRef = tree => {
78 | this.tree = tree;
79 | };
80 |
81 | render() {
82 | const customLabel = (
83 |
84 | operations:
85 |
86 | Edit
87 |
88 |
89 |
92 |
93 |
94 | Delete
95 |
96 |
97 | );
98 |
99 | return (
100 |
101 |
simple
102 |
103 | console.log('Active:', key)}
116 | >
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | Check on Click TreeNode
134 |
147 |
148 | Select
149 |
157 |
158 |
169 |
170 | );
171 | }
172 | }
173 |
174 | export default Demo;
175 |
--------------------------------------------------------------------------------
/docs/examples/basic.less:
--------------------------------------------------------------------------------
1 | .rc-tree li a.rc-tree-node-selected{
2 | .cus-label {
3 | background-color: white;
4 | border: none;
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/examples/big-data-generator.js:
--------------------------------------------------------------------------------
1 | /* eslint react/no-string-refs:0 */
2 |
3 | import React from 'react';
4 | import { generateData, calcTotal } from './utils/dataUtil';
5 |
6 | class Gen extends React.Component {
7 | static defaultProps = {
8 | onGen: () => {},
9 | x: 20,
10 | y: 18,
11 | z: 1,
12 | };
13 |
14 | state = {
15 | nums: '',
16 | };
17 |
18 | componentDidMount() {
19 | const vals = this.getVals();
20 | this.props.onGen(generateData(vals.x, vals.y, vals.z));
21 | }
22 |
23 | onGen = e => {
24 | e.preventDefault();
25 | const vals = this.getVals();
26 | this.props.onGen(generateData(vals.x, vals.y, vals.z));
27 | this.setState({
28 | nums: calcTotal(vals.x, vals.y, vals.z),
29 | });
30 | };
31 |
32 | getVals() {
33 | return {
34 | x: parseInt(this.refs.x.value, 10),
35 | y: parseInt(this.refs.y.value, 10),
36 | z: parseInt(this.refs.z.value, 10),
37 | };
38 | }
39 |
40 | render() {
41 | const { x, y, z } = this.props;
42 | return (
43 |
44 |
big data generator
45 |
61 |
62 | x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级)
63 |
64 |
65 | );
66 | }
67 | }
68 | export default Gen;
69 |
--------------------------------------------------------------------------------
/docs/examples/big-data.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, prefer-destructuring */
2 | import React from 'react';
3 | import Gen from './big-data-generator';
4 | import '../../assets/index.less';
5 | import Tree, { TreeNode } from '@rc-component/tree';
6 |
7 | class Demo extends React.Component {
8 | state = {
9 | gData: [],
10 | expandedKeys: [],
11 | checkedKeys: [],
12 | checkedKeys1: [],
13 | selectedKeys: [],
14 | };
15 |
16 | componentDidUpdate(nextProps, nextState) {
17 | // invoked immediately before rendering with new props or state, not for initial 'render'
18 | // see componentWillReceiveProps if you need to call setState
19 | // console.log(nextState.gData === this.state.gData);
20 | if (nextState.gData === this.state.gData) {
21 | this.notReRender = true;
22 | } else {
23 | this.notReRender = false;
24 | }
25 | }
26 |
27 | onCheck = checkedKeys => {
28 | this.setState({
29 | checkedKeys,
30 | });
31 | };
32 |
33 | onCheckStrictly = checkedKeys1 => {
34 | console.log(checkedKeys1);
35 | this.setState({
36 | checkedKeys1,
37 | });
38 | };
39 |
40 | onSelect = (selectedKeys, info) => {
41 | console.log('onSelect', selectedKeys, info);
42 | this.setState({
43 | selectedKeys,
44 | });
45 | };
46 |
47 | onGen = data => {
48 | this.setState({
49 | gData: data,
50 | expandedKeys: ['0-0-0-key'],
51 | // checkedKeys: ['0-0-0-0-key', '0-0-1-0-key', '0-1-0-0-key'],
52 | checkedKeys: ['0-0-0-key'],
53 | checkedKeys1: ['0-0-0-key'],
54 | selectedKeys: [],
55 | });
56 | };
57 |
58 | render() {
59 | const loop = data =>
60 | data.map(item => {
61 | if (item.children) {
62 | return (
63 |
64 | {loop(item.children)}
65 |
66 | );
67 | }
68 | return ;
69 | });
70 | // const s = Date.now();
71 | // const treeNodes = loop(this.state.gData);
72 | let treeNodes;
73 | if (this.treeNodes && this.notReRender) {
74 | treeNodes = this.treeNodes;
75 | } else {
76 | treeNodes = loop(this.state.gData);
77 | this.treeNodes = treeNodes;
78 | }
79 | // console.log(Date.now()-s);
80 | return (
81 |
82 |
83 |
84 |
大数据量下优化建议:
85 | 初始展开的节点少,向dom中插入节点就会少,速度更快。
86 | treeNodes 总数据量尽量少变化,缓存并复用计算出的 treeNodes,可在 componentWillUpdate
87 | 等时机做判断。
88 |
89 | {this.state.gData.length ? (
90 |
91 |
92 |
normal check
93 |
102 | {treeNodes}
103 |
104 |
105 |
106 |
checkStrictly
107 |
117 | {treeNodes}
118 |
119 |
120 |
121 | ) : null}
122 |
123 | );
124 | }
125 | }
126 |
127 | export default Demo;
128 |
--------------------------------------------------------------------------------
/docs/examples/contextmenu.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-find-dom-node */
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import Tooltip from '@rc-component/tooltip';
5 | import './contextmenu.less';
6 | import '../../assets/index.less';
7 | import Tree, { TreeNode } from '@rc-component/tree';
8 |
9 | function contains(root, n) {
10 | let node = n;
11 | while (node) {
12 | if (node === root) {
13 | return true;
14 | }
15 | node = node.parentNode;
16 | }
17 | return false;
18 | }
19 |
20 | class Demo extends React.Component {
21 | state = {
22 | selectedKeys: ['0-1', '0-1-1'],
23 | };
24 |
25 | componentDidMount() {
26 | this.getContainer();
27 | // console.log(ReactDOM.findDOMNode(this), this.cmContainer);
28 | console.log(contains(ReactDOM.findDOMNode(this), this.cmContainer));
29 | }
30 |
31 | componentWillUnmount() {
32 | if (this.cmContainer) {
33 | ReactDOM.unmountComponentAtNode(this.cmContainer);
34 | document.body.removeChild(this.cmContainer);
35 | this.cmContainer = null;
36 | }
37 | }
38 |
39 | onSelect = selectedKeys => {
40 | this.setState({ selectedKeys });
41 | };
42 |
43 | onRightClick = info => {
44 | console.log('right click', info);
45 | this.setState({ selectedKeys: [info.node.props.eventKey] });
46 | this.renderCm(info);
47 | };
48 |
49 | onMouseEnter = info => {
50 | console.log('enter', info);
51 | this.renderCm(info);
52 | };
53 |
54 | onMouseLeave = info => {
55 | console.log('leave', info);
56 | };
57 |
58 | getContainer() {
59 | if (!this.cmContainer) {
60 | this.cmContainer = document.createElement('div');
61 | document.body.appendChild(this.cmContainer);
62 | }
63 | return this.cmContainer;
64 | }
65 |
66 | renderCm(info) {
67 | if (this.toolTip) {
68 | ReactDOM.unmountComponentAtNode(this.cmContainer);
69 | this.toolTip = null;
70 | }
71 | this.toolTip = (
72 | {info.node.props.title}}
78 | >
79 |
80 |
81 | );
82 |
83 | const container = this.getContainer();
84 | Object.assign(this.cmContainer.style, {
85 | position: 'absolute',
86 | left: `${info.event.pageX}px`,
87 | top: `${info.event.pageY}px`,
88 | });
89 |
90 | ReactDOM.render(this.toolTip, container);
91 | }
92 |
93 | render() {
94 | return (
95 |
96 |
right click contextmenu
97 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | hover popup contextmenu
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | );
138 | }
139 | }
140 |
141 | export default Demo;
142 |
--------------------------------------------------------------------------------
/docs/examples/contextmenu.less:
--------------------------------------------------------------------------------
1 | @contextmenuPrefixCls: rc-tree-contextmenu;
2 | .@{contextmenuPrefixCls} {
3 | position: absolute;
4 | left: -9999px;
5 | top: -9999px;
6 | z-index: 1070;
7 | display: block;
8 | background-color: #fff;
9 |
10 | &-hidden {
11 | display: none;
12 | }
13 |
14 | &-inner {
15 | border: 1px solid #ddd;
16 | padding: 10px 20px;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/docs/examples/custom-switch-icon.jsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | /* eslint no-alert:0 */
3 | import React from 'react';
4 | import '../../assets/index.less';
5 | import Tree, { TreeNode } from '@rc-component/tree';
6 |
7 | const arrowPath =
8 | 'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88' +
9 | '.5c-7.4 0-10.8 9.2-5.2 14l350.2 304H152c-4.4 0-8 3.6-8 8v60c0 4.4 3.' +
10 | '6 8 8 8h585.1L386.9 854c-5.6 4.9-2.2 14 5.2 14h91.5c1.9 0 3.8-0.7 5.' +
11 | '2-2L869 536.2c14.7-12.8 14.7-35.6 0-48.4z';
12 |
13 | const getSvgIcon = (path, iStyle = {}, style = {}) => (
14 |
15 |
24 |
25 | );
26 |
27 | class Demo extends React.Component {
28 | static defaultProps = {
29 | keys: ['0-0-0-0'],
30 | };
31 |
32 | constructor(props) {
33 | super(props);
34 | const { keys } = props;
35 | this.state = {
36 | defaultExpandedKeys: keys,
37 | defaultSelectedKeys: keys,
38 | defaultCheckedKeys: keys,
39 | };
40 | }
41 |
42 | render() {
43 | const switcherIcon = obj => {
44 | if (obj.data.key?.startsWith('0-0-3')) {
45 | return false;
46 | }
47 | if (obj.isLeaf) {
48 | return getSvgIcon(
49 | arrowPath,
50 | { cursor: 'pointer', backgroundColor: 'white' },
51 | { transform: 'rotate(270deg)' },
52 | );
53 | }
54 | return getSvgIcon(
55 | arrowPath,
56 | { cursor: 'pointer', backgroundColor: 'white' },
57 | { transform: `rotate(${obj.expanded ? 90 : 0}deg)` },
58 | );
59 | };
60 | const treeCls = `myCls${(this.state.useIcon && ' customIcon') || ''}`;
61 |
62 | return (
63 |
64 |
custom switch icon
65 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | );
96 | }
97 | }
98 |
99 | export default Demo;
100 |
--------------------------------------------------------------------------------
/docs/examples/draggable-allow-drop.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate */
2 | import React from 'react';
3 | import { gData } from './utils/dataUtil';
4 | import './draggable.less';
5 | import '../../assets/index.less';
6 | import Tree from '@rc-component/tree';
7 |
8 | function allowDrop({ dropNode, dropPosition }) {
9 | if (!dropNode.children) {
10 | if (dropPosition === 0) return false;
11 | }
12 | return true;
13 | }
14 |
15 | class Demo extends React.Component {
16 | state = {
17 | gData,
18 | autoExpandParent: true,
19 | expandedKeys: ['0-0-key', '0-0-0-key', '0-0-0-0-key'],
20 | };
21 |
22 | onDragStart = info => {
23 | console.log('start', info);
24 | };
25 |
26 | onDragEnter = () => {
27 | console.log('enter');
28 | };
29 |
30 | onDrop = info => {
31 | console.log('drop', info);
32 | const dropKey = info.node.key;
33 | const dragKey = info.dragNode.key;
34 | const dropPos = info.node.pos.split('-');
35 | const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
36 |
37 | const loop = (data, key, callback) => {
38 | data.forEach((item, index, arr) => {
39 | if (item.key === key) {
40 | callback(item, index, arr);
41 | return;
42 | }
43 | if (item.children) {
44 | loop(item.children, key, callback);
45 | }
46 | });
47 | };
48 | const data = [...this.state.gData];
49 |
50 | // Find dragObject
51 | let dragObj;
52 | loop(data, dragKey, (item, index, arr) => {
53 | arr.splice(index, 1);
54 | dragObj = item;
55 | });
56 |
57 | if (dropPosition === 0) {
58 | // Drop on the content
59 | loop(data, dropKey, item => {
60 | // eslint-disable-next-line no-param-reassign
61 | item.children = item.children || [];
62 | // where to insert 示例添加到尾部,可以是随意位置
63 | item.children.unshift(dragObj);
64 | });
65 | } else {
66 | // Drop on the gap (insert before or insert after)
67 | let ar;
68 | let i;
69 | loop(data, dropKey, (item, index, arr) => {
70 | ar = arr;
71 | i = index;
72 | });
73 | if (dropPosition === -1) {
74 | ar.splice(i, 0, dragObj);
75 | } else {
76 | ar.splice(i + 1, 0, dragObj);
77 | }
78 | }
79 |
80 | this.setState({
81 | gData: data,
82 | });
83 | };
84 |
85 | onExpand = expandedKeys => {
86 | console.log('onExpand', expandedKeys);
87 | this.setState({
88 | expandedKeys,
89 | autoExpandParent: false,
90 | });
91 | };
92 |
93 | render() {
94 | return (
95 |
96 |
draggable with allow drop
97 |
node can not be dropped inside a leaf node
98 |
99 |
109 |
110 |
111 | );
112 | }
113 | }
114 |
115 | export default Demo;
116 |
--------------------------------------------------------------------------------
/docs/examples/draggable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate */
2 | import React from 'react';
3 | import '../../assets/index.less';
4 | import Tree from '../../src';
5 | import './draggable.less';
6 | import { generateData } from './utils/dataUtil';
7 |
8 | const gData = generateData(2, 2, 2);
9 |
10 | class Demo extends React.Component {
11 | state = {
12 | gData,
13 | autoExpandParent: true,
14 | expandedKeys: [
15 | '0-0-key',
16 | '0-0-0-key',
17 | '0-0-0-0-key',
18 | '0-0-0-1-key',
19 | '0-0-1-key',
20 | '0-0-1-0-key',
21 | '0-0-1-1-key',
22 | '0-1-key',
23 | '0-1-0-key',
24 | '0-1-0-0-key',
25 | '0-1-0-1-key',
26 | '0-1-1-key',
27 | '0-1-1-0-key',
28 | '0-1-1-1-key',
29 | ],
30 | };
31 |
32 | onDragStart = info => {
33 | console.log('start', info);
34 | };
35 |
36 | onDragEnter = () => {
37 | console.log('enter');
38 | };
39 |
40 | onDrop = info => {
41 | console.log('drop', info);
42 | const dropKey = info.node.key;
43 | const dragKey = info.dragNode.key;
44 | const dropPos = info.node.pos.split('-');
45 | const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
46 |
47 | const loop = (data, key, callback) => {
48 | data.forEach((item, index, arr) => {
49 | if (item.key === key) {
50 | callback(item, index, arr);
51 | return;
52 | }
53 | if (item.children) {
54 | loop(item.children, key, callback);
55 | }
56 | });
57 | };
58 | const data = [...this.state.gData];
59 |
60 | // Find dragObject
61 | let dragObj;
62 | loop(data, dragKey, (item, index, arr) => {
63 | arr.splice(index, 1);
64 | dragObj = item;
65 | });
66 |
67 | if (dropPosition === 0) {
68 | // Drop on the content
69 | loop(data, dropKey, item => {
70 | // eslint-disable-next-line no-param-reassign
71 | item.children = item.children || [];
72 | // where to insert 示例添加到尾部,可以是随意位置
73 | item.children.unshift(dragObj);
74 | });
75 | } else {
76 | // Drop on the gap (insert before or insert after)
77 | let ar;
78 | let i;
79 | loop(data, dropKey, (item, index, arr) => {
80 | ar = arr;
81 | i = index;
82 | });
83 | if (dropPosition === -1) {
84 | ar.splice(i, 0, dragObj);
85 | } else {
86 | ar.splice(i + 1, 0, dragObj);
87 | }
88 | }
89 |
90 | this.setState({
91 | gData: data,
92 | });
93 | };
94 |
95 | onExpand = expandedKeys => {
96 | console.log('onExpand', expandedKeys);
97 | this.setState({
98 | expandedKeys,
99 | autoExpandParent: false,
100 | });
101 | };
102 |
103 | render() {
104 | return (
105 |
106 |
draggable
107 |
drag a node into another node
108 |
109 |
125 |
This element is draggable, but it cannot be dragged into tree.
126 |
127 |
128 | );
129 | }
130 | }
131 |
132 | export default Demo;
133 |
--------------------------------------------------------------------------------
/docs/examples/draggable.less:
--------------------------------------------------------------------------------
1 |
2 | .draggable-demo{
3 | padding: 0 20px;
4 | .draggable-container {
5 | margin: 10px 30px;
6 | width: 300px;
7 | height: 200px;
8 | overflow: auto;
9 | border: 1px solid #ccc;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/examples/dropdown.jsx:
--------------------------------------------------------------------------------
1 | /* eslint react/no-multi-comp:0 */
2 | /* eslint no-console:0 */
3 | /* eslint react/no-string-refs:0 */
4 | import React from 'react';
5 | import Trigger from '@rc-component/trigger';
6 | import { gData } from './utils/dataUtil';
7 | import './dropdown.less';
8 | import '../../assets/index.less';
9 | import Tree, { TreeNode } from '@rc-component/tree';
10 |
11 | const placements = {
12 | topLeft: {
13 | points: ['bl', 'tl'],
14 | overflow: {
15 | adjustX: 1,
16 | adjustY: 1,
17 | },
18 | offset: [0, -3],
19 | targetOffset: [0, 0],
20 | },
21 | bottomLeft: {
22 | points: ['tl', 'bl'],
23 | overflow: {
24 | adjustX: 1,
25 | adjustY: 1,
26 | },
27 | offset: [0, 3],
28 | targetOffset: [0, 0],
29 | },
30 | };
31 | class DropdownTree extends React.Component {
32 | static defaultProps = {
33 | prefixCls: 'demo-dropdown-tree',
34 | trigger: ['hover'],
35 | overlayClassName: '',
36 | overlayStyle: {},
37 | defaultVisible: false,
38 | onVisibleChange() {},
39 | placement: 'bottomLeft',
40 | };
41 |
42 | constructor(props) {
43 | super(props);
44 | if ('visible' in props) {
45 | this.state = {
46 | visible: props.visible,
47 | };
48 | return;
49 | }
50 | this.state = {
51 | visible: props.defaultVisible,
52 | };
53 | }
54 |
55 | static getDerivedStateFromProps(props) {
56 | if ('visible' in props) {
57 | return {
58 | visible: props.visible,
59 | };
60 | }
61 | return null;
62 | }
63 |
64 | onChange = value => {
65 | console.log('change', value);
66 | };
67 |
68 | onSelect = value => {
69 | console.log('select ', value);
70 | };
71 |
72 | onClick = e => {
73 | const { props } = this;
74 | const overlayProps = props.overlay.props;
75 | if (!('visible' in props)) {
76 | this.setState({
77 | visible: false,
78 | });
79 | }
80 | if (overlayProps.onClick) {
81 | overlayProps.onClick(e);
82 | }
83 | };
84 |
85 | onVisibleChange = v => {
86 | const { props } = this;
87 | if (!('visible' in props)) {
88 | this.setState({
89 | visible: v,
90 | });
91 | }
92 | props.onVisibleChange(v);
93 | };
94 |
95 | getPopupElement = () => {
96 | const { props } = this;
97 | return React.cloneElement(props.overlay, {
98 | // prefixCls: `${props.prefixCls}-menu`,
99 | onClick: this.onClick,
100 | });
101 | };
102 |
103 | render() {
104 | const {
105 | prefixCls,
106 | children,
107 | transitionName,
108 | animation,
109 | align,
110 | placement,
111 | overlayClassName,
112 | overlayStyle,
113 | trigger,
114 | } = this.props;
115 | return (
116 |
131 | {children}
132 |
133 | );
134 | }
135 | }
136 |
137 | class Demo extends React.Component {
138 | state = {
139 | visible: false,
140 | inputValue: '',
141 | sel: '',
142 | expandedKeys: [],
143 | autoExpandParent: true,
144 | };
145 |
146 | onChange = event => {
147 | this.filterKeys = [];
148 | this.setState({
149 | inputValue: event.target.value,
150 | });
151 | };
152 |
153 | onVisibleChange = visible => {
154 | this.setState({
155 | visible,
156 | });
157 | };
158 |
159 | onSelect = (selectedKeys, info) => {
160 | console.log('selected: ', info);
161 | this.setState({
162 | visible: false,
163 | sel: info.node.props.title,
164 | });
165 | };
166 |
167 | onExpand = expandedKeys => {
168 | this.filterKeys = undefined;
169 | console.log('onExpand', expandedKeys);
170 | // if not set autoExpandParent to false, if children expanded, parent can not collapse.
171 | // or, you can remove all expanded chilren keys.
172 | this.setState({
173 | expandedKeys,
174 | autoExpandParent: false,
175 | });
176 | };
177 |
178 | filterTreeNode = treeNode => {
179 | console.log(treeNode);
180 | // 根据 key 进行搜索,可以根据其他数据,如 value
181 | return this.filterFn(treeNode.props.eventKey);
182 | };
183 |
184 | filterFn = key => {
185 | if (this.state.inputValue && key.indexOf(this.state.inputValue) > -1) {
186 | return true;
187 | }
188 | return false;
189 | };
190 |
191 | render() {
192 | const loop = data =>
193 | data.map(item => {
194 | if (this.filterKeys && this.filterFn(item.key)) {
195 | this.filterKeys.push(item.key);
196 | }
197 | if (item.children) {
198 | return (
199 |
200 | {loop(item.children)}
201 |
202 | );
203 | }
204 | return ;
205 | });
206 | let { expandedKeys } = this.state;
207 | let { autoExpandParent } = this.state;
208 | if (this.filterKeys) {
209 | expandedKeys = this.filterKeys;
210 | autoExpandParent = true;
211 | }
212 |
213 | const overlay = (
214 |
215 |
216 |
223 | {loop(gData)}
224 |
225 |
226 | );
227 |
228 | return (
229 |
230 |
tree in dropdown
231 |
239 | {this.state.sel}
240 |
241 |
242 | );
243 | }
244 | }
245 |
246 | export default Demo;
247 |
--------------------------------------------------------------------------------
/docs/examples/dropdown.less:
--------------------------------------------------------------------------------
1 |
2 | .demo-dropdown-trigger {
3 | width: 200px;
4 | height: 30px;
5 | overflow-y: auto;
6 | border: 1px solid #ddd;
7 | }
8 | .demo-dropdown-tree {
9 | position: absolute;
10 | left: -9999px;
11 | top: -9999px;
12 | z-index: 1070;
13 | display: block;
14 | background-color: #fff;
15 |
16 | &-hidden {
17 | display: none;
18 | }
19 |
20 | .effect() {
21 | animation-duration: 0.3s;
22 | animation-fill-mode: both;
23 | transform-origin: 0 0;
24 | display: block !important;
25 | }
26 |
27 | &-slide-up-enter,&-slide-up-appear {
28 | .effect();
29 | opacity: 0;
30 | animation-timing-function: cubic-bezier(0.08, 0.82, 0.17, 1);
31 | animation-play-state: paused;
32 | }
33 |
34 | &-slide-up-leave {
35 | .effect();
36 | opacity: 1;
37 | animation-timing-function: cubic-bezier(0.6, 0.04, 0.98, 0.34);
38 | animation-play-state: paused;
39 | }
40 |
41 | &-slide-up-enter&-slide-up-enter-active&-placement-bottomLeft,
42 | &-slide-up-appear&-slide-up-appear-active&-placement-bottomLeft {
43 | animation-name: slideUpIn;
44 | animation-play-state: running;
45 | }
46 |
47 | &-slide-up-enter&-slide-up-enter-active&-placement-topLeft,
48 | &-slide-up-appear&-slide-up-appear-active&-placement-topLeft {
49 | animation-name: slideDownIn;
50 | animation-play-state: running;
51 | }
52 |
53 | &-slide-up-leave&-slide-up-leave-active&-placement-bottomLeft {
54 | animation-name: slideUpOut;
55 | animation-play-state: running;
56 | }
57 |
58 | &-slide-up-leave&-slide-up-leave-active&-placement-topLeft {
59 | animation-name: slideDownOut;
60 | animation-play-state: running;
61 | }
62 |
63 | @keyframes slideUpIn {
64 | 0% {
65 | opacity: 0;
66 | transform-origin: 0% 0%;
67 | transform: scaleY(0);
68 | }
69 | 100% {
70 | opacity: 1;
71 | transform-origin: 0% 0%;
72 | transform: scaleY(1);
73 | }
74 | }
75 | @keyframes slideUpOut {
76 | 0% {
77 | opacity: 1;
78 | transform-origin: 0% 0%;
79 | transform: scaleY(1);
80 | }
81 | 100% {
82 | opacity: 0;
83 | transform-origin: 0% 0%;
84 | transform: scaleY(0);
85 | }
86 | }
87 |
88 | @keyframes slideDownIn {
89 | 0% {
90 | opacity: 0;
91 | transform-origin: 0% 100%;
92 | transform: scaleY(0);
93 | }
94 | 100% {
95 | opacity: 1;
96 | transform-origin: 0% 100%;
97 | transform: scaleY(1);
98 | }
99 | }
100 | @keyframes slideDownOut {
101 | 0% {
102 | opacity: 1;
103 | transform-origin: 0% 100%;
104 | transform: scaleY(1);
105 | }
106 | 100% {
107 | opacity: 0;
108 | transform-origin: 0% 100%;
109 | transform: scaleY(0);
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/docs/examples/dynamic.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate */
2 | import '../../assets/index.less';
3 | import React from 'react';
4 | import Tree from '@rc-component/tree';
5 |
6 | function generateTreeNodes(treeNode) {
7 | const arr = [];
8 | const key = treeNode.props.eventKey;
9 | for (let i = 0; i < 3; i += 1) {
10 | arr.push({ title: `leaf ${key}-${i}`, key: `${key}-${i}` });
11 | }
12 | return arr;
13 | }
14 |
15 | function setLeaf(treeData, curKey, level) {
16 | const loopLeaf = (data, lev) => {
17 | const l = lev - 1;
18 | data.forEach(item => {
19 | if (
20 | item.key.length > curKey.length
21 | ? item.key.indexOf(curKey) !== 0
22 | : curKey.indexOf(item.key) !== 0
23 | ) {
24 | return;
25 | }
26 | if (item.children) {
27 | loopLeaf(item.children, l);
28 | } else if (l < 1) {
29 | // eslint-disable-next-line no-param-reassign
30 | item.isLeaf = true;
31 | }
32 | });
33 | };
34 | loopLeaf(treeData, level + 1);
35 | }
36 |
37 | function getNewTreeData(treeData, curKey, child, level) {
38 | const loop = data => {
39 | if (level < 1 || curKey.length - 3 > level * 2) return;
40 | data.forEach(item => {
41 | if (curKey.indexOf(item.key) === 0) {
42 | if (item.children) {
43 | loop(item.children);
44 | } else {
45 | // eslint-disable-next-line no-param-reassign
46 | item.children = child;
47 | }
48 | }
49 | });
50 | };
51 | loop(treeData);
52 | setLeaf(treeData, curKey, level);
53 | }
54 |
55 | class Demo extends React.Component {
56 | state = {
57 | treeData: [],
58 | checkedKeys: [],
59 | };
60 |
61 | componentDidMount() {
62 | setTimeout(() => {
63 | this.setState({
64 | treeData: [
65 | { title: 'pNode 01', key: '0-0' },
66 | { title: 'pNode 02', key: '0-1' },
67 | { title: 'pNode 03', key: '0-2', isLeaf: true },
68 | ],
69 | checkedKeys: ['0-0'],
70 | });
71 | }, 100);
72 | }
73 |
74 | onSelect = info => {
75 | console.log('selected', info);
76 | };
77 |
78 | onCheck = checkedKeys => {
79 | console.log(checkedKeys);
80 | this.setState({
81 | checkedKeys,
82 | });
83 | };
84 |
85 | onLoadData = treeNode => {
86 | console.log('load data...');
87 | return new Promise(resolve => {
88 | setTimeout(() => {
89 | const treeData = [...this.state.treeData];
90 | getNewTreeData(treeData, treeNode.props.eventKey, generateTreeNodes(treeNode), 2);
91 | this.setState({ treeData });
92 | resolve();
93 | }, 500);
94 | });
95 | };
96 |
97 | render() {
98 | const { treeData } = this.state;
99 |
100 | return (
101 |
102 |
dynamic render
103 |
111 |
112 | );
113 | }
114 | }
115 |
116 | export default Demo;
117 |
--------------------------------------------------------------------------------
/docs/examples/expandAction.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate */
2 | import React from 'react';
3 | import '../../assets/index.less';
4 | import Tree, { TreeNode } from '@rc-component/tree';
5 |
6 | const Demo = () => (
7 |
8 |
expandAction
9 |
normal
10 |
11 |
12 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | export default Demo;
24 |
--------------------------------------------------------------------------------
/docs/examples/expandAction.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/index.less';
2 | .expandAction-demo {
3 | padding: 0 20px;
4 | }
5 |
--------------------------------------------------------------------------------
/docs/examples/fieldNames.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-alert, no-console, react/no-find-dom-node */
2 | import React from 'react';
3 | import '../../assets/index.less';
4 | import './basic.less';
5 | import Tree from '@rc-component/tree';
6 |
7 | const treeData = [
8 | {
9 | name: 'parent 1',
10 | test: '0-0',
11 | child: [
12 | {
13 | name: '张晨成',
14 | test: '0-0-0',
15 | disabled: true,
16 | child: [
17 | {
18 | name: 'leaf',
19 | test: '0-0-0-0',
20 | disableCheckbox: true,
21 | },
22 | {
23 | name: 'leaf',
24 | test: '0-0-0-1',
25 | },
26 | ],
27 | },
28 | {
29 | name: 'parent 1-1',
30 | test: '0-0-1',
31 | child: [
32 | {
33 | test: '0-0-1-0',
34 | name: 'zcvc',
35 | },
36 | ],
37 | },
38 | ],
39 | },
40 | ];
41 |
42 | const Demo = () => {
43 | const onSelect = (selectedKeys, info) => {
44 | console.log('selected', selectedKeys, info);
45 | };
46 |
47 | const onCheck = (checkedKeys, info) => {
48 | console.log('onCheck', checkedKeys, info);
49 | };
50 | const fieldNames = {
51 | children: 'child',
52 | title: 'name',
53 | key: 'test',
54 | };
55 |
56 | return (
57 |
67 | );
68 | };
69 |
70 | export default Demo;
71 |
--------------------------------------------------------------------------------
/docs/examples/funtionTitle.jsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0, react/no-danger: 0 */
2 | import '../../assets/index.less';
3 | import './animation.less';
4 | import React, { useState } from 'react';
5 | import Tree from '@rc-component/tree';
6 | import data from './longData.json';
7 |
8 | const STYLE = `
9 | .rc-tree-child-tree {
10 | display: block;
11 | }
12 |
13 | .node-motion {
14 | transition: all .3s;
15 | overflow-y: hidden;
16 | }
17 | `;
18 |
19 | const motion = {
20 | motionName: 'node-motion',
21 | motionAppear: false,
22 | onAppearStart: () => ({ height: 0 }),
23 | onAppearActive: node => ({ height: node.scrollHeight }),
24 | onLeaveStart: node => ({ height: node.offsetHeight }),
25 | onLeaveActive: () => ({ height: 0 }),
26 | };
27 |
28 | const renderTitle = title =>
29 | // console.log('run');
30 | title;
31 | const groupList = (list, targetVar) => {
32 | const obj = {};
33 | list.forEach(item => {
34 | if (!obj[item.fieldType]) {
35 | obj[item.fieldType] = [];
36 | }
37 | const disabled = item.is_key === 1 || item.ti === targetVar;
38 |
39 | obj[item.fieldType].push({
40 | ...item,
41 | disabled,
42 | });
43 | });
44 | // console.log(obj);
45 | return (
46 | Object.keys(obj)
47 | .map(key => ({
48 | title: key,
49 | key,
50 | children: obj[key],
51 | }))
52 | .filter(({ children }) => children.length) || []
53 | );
54 | };
55 |
56 | function getTreeData() {
57 | // return [
58 | // { key: '00', children: [{ key: '000' }, { key: '001' }] },
59 | // { key: '01', children: [{ key: '010' }, { key: '011' }] },
60 | // ];
61 |
62 | return groupList(
63 | data.map(item => ({
64 | title: () => renderTitle(item.fieldName),
65 | key: item.fieldName,
66 | checkable: true,
67 | ...item,
68 | })),
69 | 'id',
70 | [],
71 | );
72 | }
73 |
74 | const Demo = () => {
75 | const [keys, setKeys] = useState(data.map(item => item.fieldName));
76 | // const [keys, setKeys] = useState(['00', '01']);
77 |
78 | return (
79 |
80 |
expanded
81 |
82 |
83 |
84 |
85 |
With Virtual
86 | {
94 | console.log('onCheck:', checkedKeys);
95 | setKeys(checkedKeys);
96 | }}
97 | style={{ border: '1px solid #000' }}
98 | treeData={getTreeData()}
99 | />
100 |
101 |
102 |
103 | );
104 | };
105 |
106 | export default Demo;
107 |
--------------------------------------------------------------------------------
/docs/examples/icon.jsx:
--------------------------------------------------------------------------------
1 | /* eslint no-console:0 */
2 | /* eslint no-alert:0 */
3 | import React from 'react';
4 | import classNames from 'classnames';
5 | import Tree, { TreeNode } from '@rc-component/tree';
6 | import '../../assets/index.less';
7 | import './icon.less';
8 |
9 | const Icon = ({ selected }) => (
10 |
11 | );
12 |
13 | const Demo = () => (
14 |
15 |
Customize icon with element
16 |
17 | } title="Parent">
18 | } title="Child" />
19 |
20 |
21 |
22 | Customize icon with component
23 |
24 |
25 |
26 |
27 |
28 |
29 | Customize icon with Tree prop
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 |
38 | export default Demo;
39 |
--------------------------------------------------------------------------------
/docs/examples/icon.less:
--------------------------------------------------------------------------------
1 | @des: 2px;
2 |
3 | .customize-icon {
4 | display: inline-block;
5 | position: relative;
6 | background: #2F54EB;
7 | border-radius: 3px;
8 | box-sizing: border-box;
9 | width: 12px;
10 | height: 12px;
11 | vertical-align: top;
12 |
13 | &:before {
14 | content: '';
15 | position: absolute;
16 | left: @des;
17 | right: @des;
18 | top: @des;
19 | bottom: @des;
20 | background: #FFF;
21 | border-radius: 100%;
22 | }
23 |
24 | &.sub-icon {
25 | background: #FF4D4F;
26 | }
27 |
28 | &.selected-icon {
29 | background: green;
30 | }
31 | }
--------------------------------------------------------------------------------
/docs/examples/selectable.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console, react/no-access-state-in-setstate */
2 | import React from 'react';
3 | import './selectable.less';
4 | import '../../assets/index.less';
5 | import Tree, { TreeNode } from '@rc-component/tree';
6 |
7 | class Demo extends React.Component {
8 | render() {
9 | return (
10 |
11 |
selectable
12 |
normal
13 |
14 |
15 |
16 |
17 |
21 |
22 |
23 |
24 |
29 |
34 |
35 |
36 |
41 |
46 |
47 |
48 |
49 |
50 |
customized tree node style if unselectable
51 |
52 |
53 |
54 |
55 |
59 |
60 |
61 |
62 |
67 |
72 |
73 |
74 |
79 |
84 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 | }
92 |
93 | export default Demo;
94 |
--------------------------------------------------------------------------------
/docs/examples/selectable.less:
--------------------------------------------------------------------------------
1 | @import '../../assets/index.less';
2 | .selectable-demo {
3 | padding: 0 20px;
4 | .selectable-container {
5 | margin: 10px 30px;
6 | overflow: auto;
7 | border: 1px solid #ccc;
8 | .@{treeNodePrefixCls}[aria-selected="false"] {
9 | .@{treePrefixCls}-node-content-wrapper span:last-child {
10 | text-decoration: line-through;
11 | }
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/docs/examples/utils/dataUtil.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-loop-func, no-mixed-operators, no-console, no-plusplus, no-underscore-dangle */
2 |
3 | export function generateData(x = 3, y = 2, z = 1, gData = []) {
4 | // x:每一级下的节点总数。y:每级节点里有y个节点、存在子节点。z:树的level层级数(0表示一级)
5 | function _loop(_level, _preKey, _tns) {
6 | const preKey = _preKey || '0';
7 | const tns = _tns || gData;
8 |
9 | const children = [];
10 | for (let i = 0; i < x; i++) {
11 | const key = `${preKey}-${i}`;
12 | tns.push({ title: `${key}-label`, key: `${key}-key` });
13 | if (i < y) {
14 | children.push(key);
15 | }
16 | }
17 | if (_level < 0) {
18 | return tns;
19 | }
20 | const __level = _level - 1;
21 | children.forEach((key, index) => {
22 | tns[index].children = [];
23 | return _loop(__level, key, tns[index].children);
24 | });
25 |
26 | return null;
27 | }
28 | _loop(z);
29 | return gData;
30 | }
31 | export function calcTotal(x = 3, y = 2, z = 1) {
32 | /* eslint no-param-reassign:0 */
33 | const rec = n => (n >= 0 ? x * y ** n-- + rec(n) : 0);
34 | return rec(z + 1);
35 | }
36 | console.log('总节点数(单个tree):', calcTotal());
37 | // 性能测试:总节点数超过 2000(z要小)明显感觉慢。z 变大时,递归多,会卡死。
38 |
39 | export const gData = generateData();
40 |
41 | function isPositionPrefix(smallPos, bigPos) {
42 | if (bigPos.length < smallPos.length) {
43 | return false;
44 | }
45 | // attention: "0-0-1" "0-0-10"
46 | if (bigPos.length > smallPos.length && bigPos.charAt(smallPos.length) !== '-') {
47 | return false;
48 | }
49 | return bigPos.substr(0, smallPos.length) === smallPos;
50 | }
51 | // console.log(isPositionPrefix("0-1", "0-10-1"));
52 |
53 | // arr.length === 628, use time: ~20ms
54 | export function filterParentPosition(arr) {
55 | const levelObj = {};
56 | arr.forEach(item => {
57 | const posLen = item.split('-').length;
58 | if (!levelObj[posLen]) {
59 | levelObj[posLen] = [];
60 | }
61 | levelObj[posLen].push(item);
62 | });
63 | const levelArr = Object.keys(levelObj).sort();
64 | for (let i = 0; i < levelArr.length; i += 1) {
65 | if (levelArr[i + 1]) {
66 | levelObj[levelArr[i]].forEach(ii => {
67 | for (let j = i + 1; j < levelArr.length; j += 1) {
68 | levelObj[levelArr[j]].forEach((_i, index) => {
69 | if (isPositionPrefix(ii, _i)) {
70 | levelObj[levelArr[j]][index] = null;
71 | }
72 | });
73 | levelObj[levelArr[j]] = levelObj[levelArr[j]].filter(p => p);
74 | }
75 | });
76 | }
77 | }
78 | let nArr = [];
79 | levelArr.forEach(i => {
80 | nArr = nArr.concat(levelObj[i]);
81 | });
82 | return nArr;
83 | }
84 | // console.log(filterParentPosition(
85 | // ['0-2', '0-3-3', '0-10', '0-10-0', '0-0-1', '0-0', '0-1-1', '0-1']
86 | // ));
87 |
88 | function loopData(data, callback) {
89 | const loop = (d, level = 0) => {
90 | d.forEach((item, index) => {
91 | const pos = `${level}-${index}`;
92 | if (item.children) {
93 | loop(item.children, pos);
94 | }
95 | callback(item, index, pos);
96 | });
97 | };
98 | loop(data);
99 | }
100 |
101 | function spl(str) {
102 | return str.split('-');
103 | }
104 | function splitLen(str) {
105 | return str.split('-').length;
106 | }
107 |
108 | export function getFilterExpandedKeys(data, expandedKeys) {
109 | const expandedPosArr = [];
110 | loopData(data, (item, index, pos) => {
111 | if (expandedKeys.indexOf(item.key) > -1) {
112 | expandedPosArr.push(pos);
113 | }
114 | });
115 | const filterExpandedKeys = [];
116 | loopData(data, (item, index, pos) => {
117 | expandedPosArr.forEach(p => {
118 | if (
119 | ((splitLen(pos) < splitLen(p) && p.indexOf(pos) === 0) || pos === p) &&
120 | filterExpandedKeys.indexOf(item.key) === -1
121 | ) {
122 | filterExpandedKeys.push(item.key);
123 | }
124 | });
125 | });
126 | return filterExpandedKeys;
127 | }
128 |
129 | function isSibling(pos, pos1) {
130 | pos.pop();
131 | pos1.pop();
132 | return pos.join(',') === pos1.join(',');
133 | }
134 |
135 | export function getRadioSelectKeys(data, selectedKeys, key) {
136 | const res = [];
137 | const pkObjArr = [];
138 | const selPkObjArr = [];
139 | loopData(data, (item, index, pos) => {
140 | if (selectedKeys.indexOf(item.key) > -1) {
141 | pkObjArr.push([pos, item.key]);
142 | }
143 | if (key && key === item.key) {
144 | selPkObjArr.push(pos, item.key);
145 | }
146 | });
147 | const lenObj = {};
148 | const getPosKey = (pos, k) => {
149 | const posLen = splitLen(pos);
150 | if (!lenObj[posLen]) {
151 | lenObj[posLen] = [[pos, k]];
152 | } else {
153 | lenObj[posLen].forEach((pkArr, i) => {
154 | if (isSibling(spl(pkArr[0]), spl(pos))) {
155 | // 后来覆盖前者
156 | lenObj[posLen][i] = [pos, k];
157 | } else if (spl(pkArr[0]) !== spl(pos)) {
158 | lenObj[posLen].push([pos, k]);
159 | }
160 | });
161 | }
162 | };
163 | pkObjArr.forEach(pk => {
164 | getPosKey(pk[0], pk[1]);
165 | });
166 | if (key) {
167 | getPosKey(selPkObjArr[0], selPkObjArr[1]);
168 | }
169 |
170 | Object.keys(lenObj).forEach(item => {
171 | lenObj[item].forEach(i => {
172 | if (res.indexOf(i[1]) === -1) {
173 | res.push(i[1]);
174 | }
175 | });
176 | });
177 | return res;
178 | }
179 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: rc-tree
3 | ---
4 |
5 |
6 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = require('./src/');
4 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | setupFilesAfterEnv: ['/tests/setupFilesAfterEnv.js'],
3 | };
4 |
--------------------------------------------------------------------------------
/now.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "name": "rc-tree",
4 | "builds": [
5 | {
6 | "src": "package.json",
7 | "use": "@now/static-build",
8 | "config": { "distDir": "dist" }
9 | }
10 | ],
11 | "routes": [
12 | { "src": "/(.*)", "dest": "/dist/$1" }
13 | ]
14 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@rc-component/tree",
3 | "version": "1.0.1",
4 | "description": "tree ui component for react",
5 | "engines": {
6 | "node": ">=10.x"
7 | },
8 | "keywords": [
9 | "react",
10 | "react-component",
11 | "react-tree",
12 | "tree"
13 | ],
14 | "files": [
15 | "assets",
16 | "es",
17 | "lib"
18 | ],
19 | "homepage": "http://github.com/react-component/tree",
20 | "author": "smith3816@gmail.com",
21 | "repository": {
22 | "type": "git",
23 | "url": "git@github.com:react-component/tree.git"
24 | },
25 | "bugs": {
26 | "url": "http://github.com/react-component/tree/issues"
27 | },
28 | "license": "MIT",
29 | "main": "./lib/index",
30 | "module": "./es/index",
31 | "scripts": {
32 | "start": "dumi dev",
33 | "docs:build": "dumi build",
34 | "docs:deploy": "gh-pages -d dist",
35 | "compile": "father build && lessc assets/index.less assets/index.css",
36 | "prepare": "husky",
37 | "prepublishOnly": "npm run compile && rc-np",
38 | "postpublish": "npm run gh-pages",
39 | "lint": "eslint src/ --ext .tsx,.ts,.jsx,.js",
40 | "test": "rc-test",
41 | "coverage": "rc-test --coverage",
42 | "gh-pages": "npm run docs:build && npm run docs:deploy",
43 | "now-build": "npm run docs:build"
44 | },
45 | "lint-staged": {
46 | "*": "prettier --write --ignore-unknown"
47 | },
48 | "peerDependencies": {
49 | "react": "*",
50 | "react-dom": "*"
51 | },
52 | "devDependencies": {
53 | "@rc-component/father-plugin": "^2.0.3",
54 | "@testing-library/jest-dom": "^6.1.5",
55 | "@testing-library/react": "^16.1.0",
56 | "@types/jest": "^29.5.10",
57 | "@types/node": "^22.7.3",
58 | "@types/react": "^19.0.1",
59 | "@types/react-dom": "^19.0.1",
60 | "@types/warning": "^3.0.0",
61 | "@umijs/fabric": "^4.0.1",
62 | "dumi": "^2.1.0",
63 | "eslint": "^8.55.0",
64 | "eslint-plugin-jest": "^28.8.3",
65 | "eslint-plugin-unicorn": "^56.0.1",
66 | "father": "^4.4.0",
67 | "gh-pages": "^6.1.1",
68 | "glob": "^11.0.0",
69 | "husky": "^9.1.6",
70 | "less": "^4.2.1",
71 | "lint-staged": "^15.2.10",
72 | "@rc-component/np": "^1.0.0",
73 | "prettier": "^3.3.3",
74 | "@rc-component/dialog": "^1.0.0",
75 | "rc-test": "^7.0.15",
76 | "@rc-component/tooltip": "^1.0.0",
77 | "@rc-component/trigger": "^3.0.0",
78 | "react": "^18.2.0",
79 | "react-dom": "^18.2.0",
80 | "typescript": "^5.3.3"
81 | },
82 | "dependencies": {
83 | "classnames": "2.x",
84 | "@rc-component/motion": "^1.0.0",
85 | "@rc-component/util": "^1.2.1",
86 | "rc-virtual-list": "^3.5.1"
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/DropIndicator.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export interface DropIndicatorProps {
4 | dropPosition: -1 | 0 | 1;
5 | dropLevelOffset: number;
6 | indent: number;
7 | }
8 |
9 | const DropIndicator: React.FC> = props => {
10 | const { dropPosition, dropLevelOffset, indent } = props;
11 | const style: React.CSSProperties = {
12 | pointerEvents: 'none',
13 | position: 'absolute',
14 | right: 0,
15 | backgroundColor: 'red',
16 | height: 2,
17 | };
18 | switch (dropPosition) {
19 | case -1:
20 | style.top = 0;
21 | style.left = -dropLevelOffset * indent;
22 | break;
23 | case 1:
24 | style.bottom = 0;
25 | style.left = -dropLevelOffset * indent;
26 | break;
27 | case 0:
28 | style.bottom = 0;
29 | style.left = indent;
30 | break;
31 | }
32 | return ;
33 | };
34 |
35 | if (process.env.NODE_ENV !== 'production') {
36 | DropIndicator.displayName = 'DropIndicator';
37 | }
38 |
39 | export default DropIndicator;
40 |
--------------------------------------------------------------------------------
/src/Indent.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import * as React from 'react';
3 |
4 | interface IndentProps {
5 | prefixCls: string;
6 | level: number;
7 | isStart: boolean[];
8 | isEnd: boolean[];
9 | }
10 |
11 | const Indent: React.FC = ({ prefixCls, level, isStart, isEnd }) => {
12 | const baseClassName = `${prefixCls}-indent-unit`;
13 | const list: React.ReactElement[] = [];
14 | for (let i = 0; i < level; i += 1) {
15 | list.push(
16 | ,
23 | );
24 | }
25 |
26 | return (
27 |
28 | {list}
29 |
30 | );
31 | };
32 |
33 | export default React.memo(Indent);
34 |
--------------------------------------------------------------------------------
/src/MotionTreeNode.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames';
2 | import CSSMotion from '@rc-component/motion';
3 | import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
4 | import * as React from 'react';
5 | import { TreeContext } from './contextTypes';
6 | import type { FlattenNode, TreeNodeProps } from './interface';
7 | import TreeNode from './TreeNode';
8 | import useUnmount from './useUnmount';
9 | import { getTreeNodeProps, type TreeNodeRequiredProps } from './utils/treeUtil';
10 |
11 | interface MotionTreeNodeProps extends Omit {
12 | active: boolean;
13 | motion?: any;
14 | motionNodes?: FlattenNode[];
15 | onMotionStart: () => void;
16 | onMotionEnd: () => void;
17 | motionType?: 'show' | 'hide';
18 |
19 | treeNodeRequiredProps: TreeNodeRequiredProps;
20 | }
21 |
22 | const MotionTreeNode = React.forwardRef((oriProps, ref) => {
23 | const {
24 | className,
25 | style,
26 | motion,
27 | motionNodes,
28 | motionType,
29 | onMotionStart: onOriginMotionStart,
30 | onMotionEnd: onOriginMotionEnd,
31 | active,
32 | treeNodeRequiredProps,
33 | ...props
34 | } = oriProps;
35 | const [visible, setVisible] = React.useState(true);
36 | const { prefixCls } = React.useContext(TreeContext);
37 |
38 | // Calculate target visible here.
39 | // And apply in effect to make `leave` motion work.
40 | const targetVisible = motionNodes && motionType !== 'hide';
41 |
42 | useLayoutEffect(() => {
43 | if (motionNodes) {
44 | if (targetVisible !== visible) {
45 | setVisible(targetVisible);
46 | }
47 | }
48 | }, [motionNodes]);
49 |
50 | const triggerMotionStart = () => {
51 | if (motionNodes) {
52 | onOriginMotionStart();
53 | }
54 | };
55 |
56 | // Should only trigger once
57 | const triggerMotionEndRef = React.useRef(false);
58 | const triggerMotionEnd = () => {
59 | if (motionNodes && !triggerMotionEndRef.current) {
60 | triggerMotionEndRef.current = true;
61 | onOriginMotionEnd();
62 | }
63 | };
64 |
65 | // Effect if unmount
66 | useUnmount(triggerMotionStart, triggerMotionEnd);
67 |
68 | // Motion end event
69 | const onVisibleChanged = (nextVisible: boolean) => {
70 | if (targetVisible === nextVisible) {
71 | triggerMotionEnd();
72 | }
73 | };
74 |
75 | if (motionNodes) {
76 | return (
77 |
84 | {({ className: motionClassName, style: motionStyle }, motionRef) => (
85 |
90 | {motionNodes.map(treeNode => {
91 | const {
92 | data: { ...restProps },
93 | title,
94 | key,
95 | isStart,
96 | isEnd,
97 | } = treeNode;
98 | delete restProps.children;
99 |
100 | const treeNodeProps = getTreeNodeProps(key, treeNodeRequiredProps);
101 |
102 | return (
103 | )}
105 | {...treeNodeProps}
106 | title={title}
107 | active={active}
108 | data={treeNode.data}
109 | key={key}
110 | isStart={isStart}
111 | isEnd={isEnd}
112 | />
113 | );
114 | })}
115 |
116 | )}
117 |
118 | );
119 | }
120 | return ;
121 | });
122 |
123 | if (process.env.NODE_ENV !== 'production') {
124 | MotionTreeNode.displayName = 'MotionTreeNode';
125 | }
126 |
127 | export default MotionTreeNode;
128 |
--------------------------------------------------------------------------------
/src/NodeList.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Handle virtual list of the TreeNodes.
3 | */
4 |
5 | import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
6 | import VirtualList, { type ListRef } from 'rc-virtual-list';
7 | import * as React from 'react';
8 | import MotionTreeNode from './MotionTreeNode';
9 | import type {
10 | BasicDataNode,
11 | DataEntity,
12 | DataNode,
13 | FlattenNode,
14 | Key,
15 | KeyEntities,
16 | ScrollTo,
17 | } from './interface';
18 | import { findExpandedKeys, getExpandRange } from './utils/diffUtil';
19 | import { getKey, getTreeNodeProps } from './utils/treeUtil';
20 |
21 | const HIDDEN_STYLE = {
22 | width: 0,
23 | height: 0,
24 | display: 'flex',
25 | overflow: 'hidden',
26 | opacity: 0,
27 | border: 0,
28 | padding: 0,
29 | margin: 0,
30 | };
31 |
32 | const noop = () => {};
33 |
34 | export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
35 |
36 | const MotionNode: DataNode = {
37 | key: MOTION_KEY,
38 | };
39 |
40 | export const MotionEntity: DataEntity = {
41 | key: MOTION_KEY,
42 | level: 0,
43 | index: 0,
44 | pos: '0',
45 | node: MotionNode,
46 | nodes: [MotionNode],
47 | };
48 |
49 | const MotionFlattenData: FlattenNode = {
50 | parent: null,
51 | children: [],
52 | pos: MotionEntity.pos,
53 | data: MotionNode,
54 | title: null,
55 | key: MOTION_KEY,
56 | /** Hold empty list here since we do not use it */
57 | isStart: [],
58 | isEnd: [],
59 | };
60 |
61 | export interface NodeListRef {
62 | scrollTo: ScrollTo;
63 | getIndentWidth: () => number;
64 | }
65 |
66 | interface NodeListProps {
67 | prefixCls: string;
68 | style: React.CSSProperties;
69 | data: FlattenNode[];
70 | motion: any;
71 | focusable?: boolean;
72 | activeItem: FlattenNode;
73 | focused?: boolean;
74 | tabIndex: number;
75 | checkable?: boolean;
76 | selectable?: boolean;
77 | disabled?: boolean;
78 |
79 | expandedKeys: Key[];
80 | selectedKeys: Key[];
81 | checkedKeys: Key[];
82 | loadedKeys: Key[];
83 | loadingKeys: Key[];
84 | halfCheckedKeys: Key[];
85 | keyEntities: KeyEntities;
86 |
87 | dragging: boolean;
88 | dragOverNodeKey: Key;
89 | dropPosition: number;
90 |
91 | // Virtual list
92 | height: number;
93 | itemHeight: number;
94 | virtual?: boolean;
95 | scrollWidth?: number;
96 |
97 | onKeyDown?: React.KeyboardEventHandler;
98 | onFocus?: React.FocusEventHandler;
99 | onBlur?: React.FocusEventHandler;
100 | onActiveChange: (key: Key) => void;
101 |
102 | onListChangeStart: () => void;
103 | onListChangeEnd: () => void;
104 | }
105 |
106 | /**
107 | * We only need get visible content items to play the animation.
108 | */
109 | export function getMinimumRangeTransitionRange(
110 | list: FlattenNode[],
111 | virtual: boolean,
112 | height: number,
113 | itemHeight: number,
114 | ) {
115 | if (virtual === false || !height) {
116 | return list;
117 | }
118 |
119 | return list.slice(0, Math.ceil(height / itemHeight) + 1);
120 | }
121 |
122 | function itemKey(item: FlattenNode) {
123 | const { key, pos } = item;
124 | return getKey(key, pos);
125 | }
126 |
127 | function getAccessibilityPath(item: FlattenNode): string {
128 | let path = String(item.data.key);
129 | let current = item;
130 |
131 | while (current.parent) {
132 | current = current.parent;
133 | path = `${current.data.key} > ${path}`;
134 | }
135 |
136 | return path;
137 | }
138 |
139 | const NodeList = React.forwardRef>((props, ref) => {
140 | const {
141 | prefixCls,
142 | data,
143 | selectable,
144 | checkable,
145 | expandedKeys,
146 | selectedKeys,
147 | checkedKeys,
148 | loadedKeys,
149 | loadingKeys,
150 | halfCheckedKeys,
151 | keyEntities,
152 | disabled,
153 |
154 | dragging,
155 | dragOverNodeKey,
156 | dropPosition,
157 | motion,
158 |
159 | height,
160 | itemHeight,
161 | virtual,
162 | scrollWidth,
163 |
164 | focusable,
165 | activeItem,
166 | focused,
167 | tabIndex,
168 |
169 | onKeyDown,
170 | onFocus,
171 | onBlur,
172 | onActiveChange,
173 |
174 | onListChangeStart,
175 | onListChangeEnd,
176 |
177 | ...domProps
178 | } = props;
179 |
180 | // =============================== Ref ================================
181 | const listRef = React.useRef(null);
182 | const indentMeasurerRef = React.useRef(null);
183 | React.useImperativeHandle(ref, () => ({
184 | scrollTo: scroll => {
185 | listRef.current.scrollTo(scroll);
186 | },
187 | getIndentWidth: () => indentMeasurerRef.current.offsetWidth,
188 | }));
189 |
190 | // ============================== Motion ==============================
191 | const [prevExpandedKeys, setPrevExpandedKeys] = React.useState(expandedKeys);
192 | const [prevData, setPrevData] = React.useState(data);
193 | const [transitionData, setTransitionData] = React.useState(data);
194 | const [transitionRange, setTransitionRange] = React.useState([]);
195 | const [motionType, setMotionType] = React.useState<'show' | 'hide' | null>(null);
196 |
197 | // When motion end but data change, this will makes data back to previous one
198 | const dataRef = React.useRef(data);
199 | dataRef.current = data;
200 |
201 | function onMotionEnd() {
202 | const latestData = dataRef.current;
203 |
204 | setPrevData(latestData);
205 | setTransitionData(latestData);
206 | setTransitionRange([]);
207 | setMotionType(null);
208 |
209 | onListChangeEnd();
210 | }
211 |
212 | // Do animation if expanded keys changed
213 | // layoutEffect here to avoid blink of node removing
214 | useLayoutEffect(() => {
215 | setPrevExpandedKeys(expandedKeys);
216 |
217 | const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
218 |
219 | if (diffExpanded.key !== null) {
220 | if (diffExpanded.add) {
221 | const keyIndex = prevData.findIndex(({ key }) => key === diffExpanded.key);
222 |
223 | const rangeNodes = getMinimumRangeTransitionRange(
224 | getExpandRange(prevData, data, diffExpanded.key),
225 | virtual,
226 | height,
227 | itemHeight,
228 | );
229 |
230 | const newTransitionData: FlattenNode[] = prevData.slice();
231 | newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
232 |
233 | setTransitionData(newTransitionData);
234 | setTransitionRange(rangeNodes);
235 | setMotionType('show');
236 | } else {
237 | const keyIndex = data.findIndex(({ key }) => key === diffExpanded.key);
238 |
239 | const rangeNodes = getMinimumRangeTransitionRange(
240 | getExpandRange(data, prevData, diffExpanded.key),
241 | virtual,
242 | height,
243 | itemHeight,
244 | );
245 |
246 | const newTransitionData: FlattenNode[] = data.slice();
247 | newTransitionData.splice(keyIndex + 1, 0, MotionFlattenData);
248 |
249 | setTransitionData(newTransitionData);
250 | setTransitionRange(rangeNodes);
251 | setMotionType('hide');
252 | }
253 | } else if (prevData !== data) {
254 | // If whole data changed, we just refresh the list
255 | setPrevData(data);
256 | setTransitionData(data);
257 | }
258 | }, [expandedKeys, data]);
259 |
260 | // We should clean up motion if is changed by dragging
261 | React.useEffect(() => {
262 | if (!dragging) {
263 | onMotionEnd();
264 | }
265 | }, [dragging]);
266 |
267 | const mergedData = motion ? transitionData : data;
268 |
269 | const treeNodeRequiredProps = {
270 | expandedKeys,
271 | selectedKeys,
272 | loadedKeys,
273 | loadingKeys,
274 | checkedKeys,
275 | halfCheckedKeys,
276 | dragOverNodeKey,
277 | dropPosition,
278 | keyEntities,
279 | };
280 |
281 | return (
282 | <>
283 | {focused && activeItem && (
284 |
285 | {getAccessibilityPath(activeItem)}
286 |
287 | )}
288 |
289 |
290 |
301 |
302 |
303 |
320 |
321 |
322 | {...domProps}
323 | data={mergedData}
324 | itemKey={itemKey}
325 | height={height}
326 | fullHeight={false}
327 | virtual={virtual}
328 | itemHeight={itemHeight}
329 | scrollWidth={scrollWidth}
330 | prefixCls={`${prefixCls}-list`}
331 | ref={listRef}
332 | role="tree"
333 | onVisibleChange={originList => {
334 | // The best match is using `fullList` - `originList` = `restList`
335 | // and check the `restList` to see if has the MOTION_KEY node
336 | // but this will cause performance issue for long list compare
337 | // we just check `originList` and repeat trigger `onMotionEnd`
338 | if (originList.every(item => itemKey(item) !== MOTION_KEY)) {
339 | onMotionEnd();
340 | }
341 | }}
342 | >
343 | {treeNode => {
344 | const {
345 | pos,
346 | data: { ...restProps },
347 | title,
348 | key,
349 | isStart,
350 | isEnd,
351 | } = treeNode;
352 | const mergedKey = getKey(key, pos);
353 | delete restProps.key;
354 | delete restProps.children;
355 |
356 | const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
357 |
358 | return (
359 | )}
361 | {...treeNodeProps}
362 | title={title}
363 | active={!!activeItem && key === activeItem.key}
364 | pos={pos}
365 | data={treeNode.data}
366 | isStart={isStart}
367 | isEnd={isEnd}
368 | motion={motion}
369 | motionNodes={key === MOTION_KEY ? transitionRange : null}
370 | motionType={motionType}
371 | onMotionStart={onListChangeStart}
372 | onMotionEnd={onMotionEnd}
373 | treeNodeRequiredProps={treeNodeRequiredProps}
374 | onMouseMove={() => {
375 | onActiveChange(null);
376 | }}
377 | />
378 | );
379 | }}
380 |
381 | >
382 | );
383 | });
384 |
385 | if (process.env.NODE_ENV !== 'production') {
386 | NodeList.displayName = 'NodeList';
387 | }
388 |
389 | export default NodeList;
390 |
--------------------------------------------------------------------------------
/src/contextTypes.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Webpack has bug for import loop, which is not the same behavior as ES module.
3 | * When util.js imports the TreeNode for tree generate will cause treeContextTypes be empty.
4 | */
5 | import * as React from 'react';
6 | import type {
7 | BasicDataNode,
8 | DataNode,
9 | Direction,
10 | EventDataNode,
11 | IconType,
12 | Key,
13 | KeyEntities,
14 | TreeNodeProps,
15 | } from './interface';
16 | import type { DraggableConfig, SemanticName } from './Tree';
17 |
18 | export type NodeMouseEventParams<
19 | TreeDataType extends BasicDataNode = DataNode,
20 | T = HTMLSpanElement,
21 | > = {
22 | event: React.MouseEvent;
23 | node: EventDataNode;
24 | };
25 | export type NodeDragEventParams<
26 | TreeDataType extends BasicDataNode = DataNode,
27 | T = HTMLDivElement,
28 | > = {
29 | event: React.DragEvent;
30 | node: EventDataNode;
31 | };
32 |
33 | export type NodeMouseEventHandler<
34 | TreeDataType extends BasicDataNode = DataNode,
35 | T = HTMLSpanElement,
36 | > = (e: React.MouseEvent, node: EventDataNode) => void;
37 | export type NodeDragEventHandler<
38 | TreeDataType extends BasicDataNode = DataNode,
39 | T = HTMLDivElement,
40 | > = (e: React.DragEvent, nodeProps: TreeNodeProps, outsideTree?: boolean) => void;
41 |
42 | export interface TreeContextProps {
43 | styles?: Partial>;
44 | classNames?: Partial>;
45 | prefixCls: string;
46 | selectable: boolean;
47 | showIcon: boolean;
48 | icon: IconType;
49 | switcherIcon: IconType;
50 | draggable?: DraggableConfig;
51 | draggingNodeKey?: Key;
52 | checkable: boolean | React.ReactNode;
53 | checkStrictly: boolean;
54 | disabled: boolean;
55 | keyEntities: KeyEntities;
56 | // for details see comment in Tree.state (Tree.tsx)
57 | dropLevelOffset?: number;
58 | dropContainerKey: Key | null;
59 | dropTargetKey: Key | null;
60 | dropPosition: -1 | 0 | 1 | null;
61 | indent: number | null;
62 | dropIndicatorRender: (props: {
63 | dropPosition: -1 | 0 | 1;
64 | dropLevelOffset: number;
65 | indent: number;
66 | prefixCls: string;
67 | direction: Direction;
68 | }) => React.ReactNode;
69 | dragOverNodeKey: Key | null;
70 | direction: Direction;
71 |
72 | loadData: (treeNode: EventDataNode) => Promise;
73 | filterTreeNode: (treeNode: EventDataNode) => boolean;
74 | titleRender?: (node: any) => React.ReactNode;
75 |
76 | onNodeClick: NodeMouseEventHandler;
77 | onNodeDoubleClick: NodeMouseEventHandler;
78 | onNodeExpand: NodeMouseEventHandler;
79 | onNodeSelect: NodeMouseEventHandler;
80 | onNodeCheck: (
81 | e: React.MouseEvent,
82 | treeNode: EventDataNode,
83 | checked: boolean,
84 | ) => void;
85 | onNodeLoad: (treeNode: EventDataNode) => void;
86 | onNodeMouseEnter: NodeMouseEventHandler;
87 | onNodeMouseLeave: NodeMouseEventHandler;
88 | onNodeContextMenu: NodeMouseEventHandler;
89 | onNodeDragStart: NodeDragEventHandler;
90 | onNodeDragEnter: NodeDragEventHandler;
91 | onNodeDragOver: NodeDragEventHandler;
92 | onNodeDragLeave: NodeDragEventHandler;
93 | onNodeDragEnd: NodeDragEventHandler;
94 | onNodeDrop: NodeDragEventHandler;
95 | }
96 |
97 | export const TreeContext = React.createContext>(null);
98 |
99 | /** Internal usage, safe to remove. Do not use in prod */
100 | export const UnstableContext = React.createContext<{ nodeDisabled?: (n: DataNode) => boolean }>({});
101 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import Tree from './Tree';
2 | import TreeNode from './TreeNode';
3 | import type { TreeProps } from './Tree';
4 | import type { TreeNodeProps, BasicDataNode, FieldDataNode } from './interface';
5 | import { UnstableContext } from './contextTypes';
6 |
7 | export { TreeNode, UnstableContext };
8 | export type { TreeProps, TreeNodeProps, BasicDataNode, FieldDataNode };
9 | export default Tree;
10 |
--------------------------------------------------------------------------------
/src/interface.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | export type { ScrollTo } from 'rc-virtual-list/lib/List';
4 | export interface TreeNodeProps {
5 | eventKey?: Key; // Pass by parent `cloneElement`
6 | prefixCls?: string;
7 | className?: string;
8 | style?: React.CSSProperties;
9 | id?: string;
10 |
11 | // By parent
12 | expanded?: boolean;
13 | selected?: boolean;
14 | checked?: boolean;
15 | loaded?: boolean;
16 | loading?: boolean;
17 | halfChecked?: boolean;
18 | title?: React.ReactNode | ((data: TreeDataType) => React.ReactNode);
19 | dragOver?: boolean;
20 | dragOverGapTop?: boolean;
21 | dragOverGapBottom?: boolean;
22 | pos?: string;
23 | domRef?: React.Ref;
24 | /** New added in Tree for easy data access */
25 | data?: TreeDataType;
26 | isStart?: boolean[];
27 | isEnd?: boolean[];
28 | active?: boolean;
29 | onMouseMove?: React.MouseEventHandler;
30 |
31 | // By user
32 | isLeaf?: boolean;
33 | checkable?: boolean;
34 | selectable?: boolean;
35 | disabled?: boolean;
36 | disableCheckbox?: boolean;
37 | icon?: IconType;
38 | switcherIcon?: IconType;
39 | children?: React.ReactNode;
40 | }
41 |
42 | /** For fieldNames, we provides a abstract interface */
43 | export interface BasicDataNode {
44 | checkable?: boolean;
45 | disabled?: boolean;
46 | disableCheckbox?: boolean;
47 | icon?: IconType;
48 | isLeaf?: boolean;
49 | selectable?: boolean;
50 | switcherIcon?: IconType;
51 |
52 | /** Set style of TreeNode. This is not recommend if you don't have any force requirement */
53 | className?: string;
54 | style?: React.CSSProperties;
55 | }
56 |
57 | /** Provide a wrap type define for developer to wrap with customize fieldNames data type */
58 | export type FieldDataNode = BasicDataNode &
59 | T &
60 | Partial[]>>;
61 |
62 | export type Key = React.Key;
63 |
64 | /**
65 | * Typescript not support `bigint` as index type yet.
66 | * We use this to mark the `bigint` type is for `Key` usage.
67 | * It's safe to remove this when typescript fix:
68 | * https://github.com/microsoft/TypeScript/issues/50217
69 | */
70 | export type SafeKey = Exclude;
71 |
72 | export type KeyEntities = Record<
73 | SafeKey,
74 | DataEntity
75 | >;
76 |
77 | export type DataNode = FieldDataNode<{
78 | key: Key;
79 | title?: React.ReactNode | ((data: DataNode) => React.ReactNode);
80 | }>;
81 |
82 | export type EventDataNode = {
83 | key: Key;
84 | expanded: boolean;
85 | selected: boolean;
86 | checked: boolean;
87 | loaded: boolean;
88 | loading: boolean;
89 | halfChecked: boolean;
90 | dragOver: boolean;
91 | dragOverGapTop: boolean;
92 | dragOverGapBottom: boolean;
93 | pos: string;
94 | active: boolean;
95 | } & TreeDataType &
96 | BasicDataNode;
97 |
98 | export type IconType = React.ReactNode | ((props: TreeNodeProps) => React.ReactNode);
99 |
100 | export type NodeElement = React.ReactElement & {
101 | selectHandle?: HTMLSpanElement;
102 | type: {
103 | isTreeNode: boolean;
104 | };
105 | };
106 |
107 | export type NodeInstance = React.Component<
108 | TreeNodeProps
109 | > & {
110 | selectHandle?: HTMLSpanElement;
111 | };
112 |
113 | export interface Entity {
114 | node: NodeElement;
115 | index: number;
116 | key: Key;
117 | pos: string;
118 | parent?: Entity;
119 | children?: Entity[];
120 | }
121 |
122 | export interface DataEntity
123 | extends Omit {
124 | node: TreeDataType;
125 | nodes: TreeDataType[];
126 | parent?: DataEntity;
127 | children?: DataEntity[];
128 | level: number;
129 | }
130 |
131 | export interface FlattenNode {
132 | parent: FlattenNode | null;
133 | children: FlattenNode[];
134 | pos: string;
135 | data: TreeDataType;
136 | title: React.ReactNode;
137 | key: Key;
138 | isStart: boolean[];
139 | isEnd: boolean[];
140 | }
141 |
142 | export type GetKey = (record: RecordType, index?: number) => Key;
143 |
144 | export type GetCheckDisabled = (record: RecordType) => boolean;
145 |
146 | export type Direction = 'ltr' | 'rtl' | undefined;
147 |
148 | export interface FieldNames {
149 | title?: string;
150 | /** @private Internal usage for `rc-tree-select`, safe to remove if no need */
151 | _title?: string[];
152 | key?: string;
153 | children?: string;
154 | }
155 |
--------------------------------------------------------------------------------
/src/useUnmount.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
3 |
4 | /**
5 | * Trigger only when component unmount
6 | */
7 | function useUnmount(triggerStart: VoidFunction, triggerEnd: VoidFunction) {
8 | const [firstMount, setFirstMount] = React.useState(false);
9 |
10 | useLayoutEffect(() => {
11 | if (firstMount) {
12 | triggerStart();
13 |
14 | return () => {
15 | triggerEnd();
16 | };
17 | }
18 | }, [firstMount]);
19 |
20 | useLayoutEffect(() => {
21 | setFirstMount(true);
22 |
23 | return () => {
24 | setFirstMount(false);
25 | };
26 | }, []);
27 | }
28 |
29 | export default useUnmount;
30 |
--------------------------------------------------------------------------------
/src/util.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-lonely-if */
2 | /**
3 | * Legacy code. Should avoid to use if you are new to import these code.
4 | */
5 |
6 | import warning from '@rc-component/util/lib/warning';
7 | import React from 'react';
8 | import type {
9 | BasicDataNode,
10 | DataEntity,
11 | DataNode,
12 | Direction,
13 | FlattenNode,
14 | Key,
15 | KeyEntities,
16 | NodeElement,
17 | TreeNodeProps,
18 | } from './interface';
19 | import type { AllowDrop, TreeProps } from './Tree';
20 | import TreeNode from './TreeNode';
21 | import getEntity from './utils/keyUtil';
22 |
23 | export { getPosition, isTreeNode } from './utils/treeUtil';
24 |
25 | export function arrDel(list: Key[], value: Key) {
26 | if (!list) return [];
27 | const clone = list.slice();
28 | const index = clone.indexOf(value);
29 | if (index >= 0) {
30 | clone.splice(index, 1);
31 | }
32 | return clone;
33 | }
34 |
35 | export function arrAdd(list: Key[], value: Key) {
36 | const clone = (list || []).slice();
37 | if (clone.indexOf(value) === -1) {
38 | clone.push(value);
39 | }
40 | return clone;
41 | }
42 |
43 | export function posToArr(pos: string) {
44 | return pos.split('-');
45 | }
46 |
47 | export function getDragChildrenKeys(
48 | dragNodeKey: Key,
49 | keyEntities: KeyEntities,
50 | ): Key[] {
51 | // not contains self
52 | // self for left or right drag
53 | const dragChildrenKeys = [];
54 |
55 | const entity = getEntity(keyEntities, dragNodeKey);
56 | function dig(list: DataEntity[] = []) {
57 | list.forEach(({ key, children }) => {
58 | dragChildrenKeys.push(key);
59 | dig(children);
60 | });
61 | }
62 |
63 | dig(entity.children);
64 |
65 | return dragChildrenKeys;
66 | }
67 |
68 | export function isLastChild(
69 | treeNodeEntity: DataEntity,
70 | ) {
71 | if (treeNodeEntity.parent) {
72 | const posArr = posToArr(treeNodeEntity.pos);
73 | return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1;
74 | }
75 | return false;
76 | }
77 |
78 | export function isFirstChild(
79 | treeNodeEntity: DataEntity,
80 | ) {
81 | const posArr = posToArr(treeNodeEntity.pos);
82 | return Number(posArr[posArr.length - 1]) === 0;
83 | }
84 |
85 | // Only used when drag, not affect SSR.
86 | export function calcDropPosition(
87 | event: React.MouseEvent,
88 | dragNodeProps: TreeNodeProps,
89 | targetNodeProps: TreeNodeProps,
90 | indent: number,
91 | startMousePosition: {
92 | x: number;
93 | y: number;
94 | },
95 | allowDrop: AllowDrop,
96 | flattenedNodes: FlattenNode[],
97 | keyEntities: KeyEntities,
98 | expandKeys: Key[],
99 | direction: Direction,
100 | ): {
101 | dropPosition: -1 | 0 | 1;
102 | dropLevelOffset: number;
103 | dropTargetKey: Key;
104 | dropTargetPos: string;
105 | dropContainerKey: Key;
106 | dragOverNodeKey: Key;
107 | dropAllowed: boolean;
108 | } {
109 | const { clientX, clientY } = event;
110 | const { top, height } = (event.target as HTMLElement).getBoundingClientRect();
111 | // optional chain for testing
112 | const horizontalMouseOffset =
113 | (direction === 'rtl' ? -1 : 1) * ((startMousePosition?.x || 0) - clientX);
114 | const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent;
115 |
116 | // Filter the expanded keys to exclude the node that not has children currently (like async nodes).
117 | const filteredExpandKeys = expandKeys.filter((key: string) => keyEntities[key]?.children?.length);
118 |
119 | // find abstract drop node by horizontal offset
120 | let abstractDropNodeEntity: DataEntity = getEntity(
121 | keyEntities,
122 | targetNodeProps.eventKey,
123 | );
124 |
125 | if (clientY < top + height / 2) {
126 | // first half, set abstract drop node to previous node
127 | const nodeIndex = flattenedNodes.findIndex(
128 | flattenedNode => flattenedNode.key === abstractDropNodeEntity.key,
129 | );
130 | const prevNodeIndex = nodeIndex <= 0 ? 0 : nodeIndex - 1;
131 | const prevNodeKey = flattenedNodes[prevNodeIndex].key;
132 | abstractDropNodeEntity = getEntity(keyEntities, prevNodeKey);
133 | }
134 |
135 | const initialAbstractDropNodeKey = abstractDropNodeEntity.key;
136 |
137 | const abstractDragOverEntity = abstractDropNodeEntity;
138 | const dragOverNodeKey = abstractDropNodeEntity.key;
139 |
140 | let dropPosition: -1 | 0 | 1 = 0;
141 | let dropLevelOffset = 0;
142 |
143 | // Only allow cross level drop when dragging on a non-expanded node
144 | if (!filteredExpandKeys.includes(initialAbstractDropNodeKey)) {
145 | for (let i = 0; i < rawDropLevelOffset; i += 1) {
146 | if (isLastChild(abstractDropNodeEntity)) {
147 | abstractDropNodeEntity = abstractDropNodeEntity.parent;
148 | dropLevelOffset += 1;
149 | } else {
150 | break;
151 | }
152 | }
153 | }
154 |
155 | const abstractDragDataNode = dragNodeProps.data;
156 | const abstractDropDataNode = abstractDropNodeEntity.node;
157 | let dropAllowed = true;
158 | if (
159 | isFirstChild(abstractDropNodeEntity) &&
160 | abstractDropNodeEntity.level === 0 &&
161 | clientY < top + height / 2 &&
162 | allowDrop({
163 | dragNode: abstractDragDataNode,
164 | dropNode: abstractDropDataNode,
165 | dropPosition: -1,
166 | }) &&
167 | abstractDropNodeEntity.key === targetNodeProps.eventKey
168 | ) {
169 | // first half of first node in first level
170 | dropPosition = -1;
171 | } else if (
172 | (abstractDragOverEntity.children || []).length &&
173 | filteredExpandKeys.includes(dragOverNodeKey)
174 | ) {
175 | // drop on expanded node
176 | // only allow drop inside
177 | if (
178 | allowDrop({
179 | dragNode: abstractDragDataNode,
180 | dropNode: abstractDropDataNode,
181 | dropPosition: 0,
182 | })
183 | ) {
184 | dropPosition = 0;
185 | } else {
186 | dropAllowed = false;
187 | }
188 | } else if (dropLevelOffset === 0) {
189 | if (rawDropLevelOffset > -1.5) {
190 | // | Node | <- abstractDropNode
191 | // | -^-===== | <- mousePosition
192 | // 1. try drop after
193 | // 2. do not allow drop
194 | if (
195 | allowDrop({
196 | dragNode: abstractDragDataNode,
197 | dropNode: abstractDropDataNode,
198 | dropPosition: 1,
199 | })
200 | ) {
201 | dropPosition = 1;
202 | } else {
203 | dropAllowed = false;
204 | }
205 | } else {
206 | // | Node | <- abstractDropNode
207 | // | ---==^== | <- mousePosition
208 | // whether it has children or doesn't has children
209 | // always
210 | // 1. try drop inside
211 | // 2. try drop after
212 | // 3. do not allow drop
213 | if (
214 | allowDrop({
215 | dragNode: abstractDragDataNode,
216 | dropNode: abstractDropDataNode,
217 | dropPosition: 0,
218 | })
219 | ) {
220 | dropPosition = 0;
221 | } else if (
222 | allowDrop({
223 | dragNode: abstractDragDataNode,
224 | dropNode: abstractDropDataNode,
225 | dropPosition: 1,
226 | })
227 | ) {
228 | dropPosition = 1;
229 | } else {
230 | dropAllowed = false;
231 | }
232 | }
233 | } else {
234 | // | Node1 | <- abstractDropNode
235 | // | Node2 |
236 | // --^--|----=====| <- mousePosition
237 | // 1. try insert after Node1
238 | // 2. do not allow drop
239 | if (
240 | allowDrop({
241 | dragNode: abstractDragDataNode,
242 | dropNode: abstractDropDataNode,
243 | dropPosition: 1,
244 | })
245 | ) {
246 | dropPosition = 1;
247 | } else {
248 | dropAllowed = false;
249 | }
250 | }
251 |
252 | return {
253 | dropPosition,
254 | dropLevelOffset,
255 | dropTargetKey: abstractDropNodeEntity.key,
256 | dropTargetPos: abstractDropNodeEntity.pos,
257 | dragOverNodeKey,
258 | dropContainerKey: dropPosition === 0 ? null : abstractDropNodeEntity.parent?.key || null,
259 | dropAllowed,
260 | };
261 | }
262 |
263 | /**
264 | * Return selectedKeys according with multiple prop
265 | * @param selectedKeys
266 | * @param props
267 | * @returns [string]
268 | */
269 | export function calcSelectedKeys(selectedKeys: Key[], props: TreeProps) {
270 | if (!selectedKeys) return undefined;
271 |
272 | const { multiple } = props;
273 | if (multiple) {
274 | return selectedKeys.slice();
275 | }
276 |
277 | if (selectedKeys.length) {
278 | return [selectedKeys[0]];
279 | }
280 | return selectedKeys;
281 | }
282 |
283 | const internalProcessProps = (props: DataNode): any => props;
284 | export function convertDataToTree(
285 | treeData: DataNode[],
286 | processor?: { processProps: (prop: DataNode) => any },
287 | ): NodeElement[] {
288 | if (!treeData) return [];
289 |
290 | const { processProps = internalProcessProps } = processor || {};
291 | const list = Array.isArray(treeData) ? treeData : [treeData];
292 | return list.map(({ children, ...props }): NodeElement => {
293 | const childrenNodes = convertDataToTree(children, processor);
294 |
295 | return (
296 |
297 | {childrenNodes}
298 |
299 | );
300 | });
301 | }
302 |
303 | /**
304 | * Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
305 | */
306 | export function parseCheckedKeys(keys: Key[] | { checked: Key[]; halfChecked: Key[] }) {
307 | if (!keys) {
308 | return null;
309 | }
310 |
311 | // Convert keys to object format
312 | let keyProps: { checkedKeys?: Key[]; halfCheckedKeys?: Key[] };
313 | if (Array.isArray(keys)) {
314 | // [Legacy] Follow the api doc
315 | keyProps = {
316 | checkedKeys: keys,
317 | halfCheckedKeys: undefined,
318 | };
319 | } else if (typeof keys === 'object') {
320 | keyProps = {
321 | checkedKeys: keys.checked || undefined,
322 | halfCheckedKeys: keys.halfChecked || undefined,
323 | };
324 | } else {
325 | warning(false, '`checkedKeys` is not an array or an object');
326 | return null;
327 | }
328 |
329 | return keyProps;
330 | }
331 |
332 | /**
333 | * If user use `autoExpandParent` we should get the list of parent node
334 | * @param keyList
335 | * @param keyEntities
336 | */
337 | export function conductExpandParent(keyList: Key[], keyEntities: KeyEntities): Key[] {
338 | const expandedKeys = new Set();
339 |
340 | function conductUp(key: Key) {
341 | if (expandedKeys.has(key)) return;
342 |
343 | const entity = getEntity(keyEntities, key);
344 | if (!entity) return;
345 |
346 | expandedKeys.add(key);
347 |
348 | const { parent, node } = entity;
349 |
350 | if (node.disabled) return;
351 |
352 | if (parent) {
353 | conductUp(parent.key);
354 | }
355 | }
356 |
357 | (keyList || []).forEach(key => {
358 | conductUp(key);
359 | });
360 |
361 | return [...expandedKeys];
362 | }
363 |
--------------------------------------------------------------------------------
/src/utils/conductUtil.ts:
--------------------------------------------------------------------------------
1 | import warning from '@rc-component/util/lib/warning';
2 | import type {
3 | BasicDataNode,
4 | DataEntity,
5 | DataNode,
6 | GetCheckDisabled,
7 | Key,
8 | KeyEntities,
9 | } from '../interface';
10 | import getEntity from './keyUtil';
11 |
12 | interface ConductReturnType {
13 | checkedKeys: Key[];
14 | halfCheckedKeys: Key[];
15 | }
16 |
17 | function removeFromCheckedKeys(halfCheckedKeys: Set, checkedKeys: Set) {
18 | const filteredKeys = new Set();
19 | halfCheckedKeys.forEach(key => {
20 | if (!checkedKeys.has(key)) {
21 | filteredKeys.add(key);
22 | }
23 | });
24 | return filteredKeys;
25 | }
26 |
27 | export function isCheckDisabled(node: TreeDataType) {
28 | const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
29 | return !!(disabled || disableCheckbox) || checkable === false;
30 | }
31 |
32 | // Fill miss keys
33 | function fillConductCheck(
34 | keys: Set,
35 | levelEntities: Map>>,
36 | maxLevel: number,
37 | syntheticGetCheckDisabled: GetCheckDisabled,
38 | ): ConductReturnType {
39 | const checkedKeys = new Set(keys);
40 | const halfCheckedKeys = new Set();
41 |
42 | // Add checked keys top to bottom
43 | for (let level = 0; level <= maxLevel; level += 1) {
44 | const entities = levelEntities.get(level) || new Set();
45 | entities.forEach(entity => {
46 | const { key, node, children = [] } = entity;
47 |
48 | if (checkedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
49 | children
50 | .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
51 | .forEach(childEntity => {
52 | checkedKeys.add(childEntity.key);
53 | });
54 | }
55 | });
56 | }
57 |
58 | // Add checked keys from bottom to top
59 | const visitedKeys = new Set();
60 | for (let level = maxLevel; level >= 0; level -= 1) {
61 | const entities = levelEntities.get(level) || new Set();
62 | entities.forEach(entity => {
63 | const { parent, node } = entity;
64 |
65 | // Skip if no need to check
66 | if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
67 | return;
68 | }
69 |
70 | // Skip if parent is disabled
71 | if (syntheticGetCheckDisabled(entity.parent.node)) {
72 | visitedKeys.add(parent.key);
73 | return;
74 | }
75 |
76 | let allChecked = true;
77 | let partialChecked = false;
78 |
79 | (parent.children || [])
80 | .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
81 | .forEach(({ key }) => {
82 | const checked = checkedKeys.has(key);
83 | if (allChecked && !checked) {
84 | allChecked = false;
85 | }
86 | if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
87 | partialChecked = true;
88 | }
89 | });
90 |
91 | if (allChecked) {
92 | checkedKeys.add(parent.key);
93 | }
94 | if (partialChecked) {
95 | halfCheckedKeys.add(parent.key);
96 | }
97 |
98 | visitedKeys.add(parent.key);
99 | });
100 | }
101 |
102 | return {
103 | checkedKeys: Array.from(checkedKeys),
104 | halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
105 | };
106 | }
107 |
108 | // Remove useless key
109 | function cleanConductCheck(
110 | keys: Set,
111 | halfKeys: Key[],
112 | levelEntities: Map>>,
113 | maxLevel: number,
114 | syntheticGetCheckDisabled: GetCheckDisabled,
115 | ): ConductReturnType {
116 | const checkedKeys = new Set(keys);
117 | let halfCheckedKeys = new Set(halfKeys);
118 |
119 | // Remove checked keys from top to bottom
120 | for (let level = 0; level <= maxLevel; level += 1) {
121 | const entities = levelEntities.get(level) || new Set();
122 | entities.forEach(entity => {
123 | const { key, node, children = [] } = entity;
124 |
125 | if (!checkedKeys.has(key) && !halfCheckedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
126 | children
127 | .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
128 | .forEach(childEntity => {
129 | checkedKeys.delete(childEntity.key);
130 | });
131 | }
132 | });
133 | }
134 |
135 | // Remove checked keys form bottom to top
136 | halfCheckedKeys = new Set();
137 | const visitedKeys = new Set();
138 | for (let level = maxLevel; level >= 0; level -= 1) {
139 | const entities = levelEntities.get(level) || new Set();
140 |
141 | entities.forEach(entity => {
142 | const { parent, node } = entity;
143 |
144 | // Skip if no need to check
145 | if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
146 | return;
147 | }
148 |
149 | // Skip if parent is disabled
150 | if (syntheticGetCheckDisabled(entity.parent.node)) {
151 | visitedKeys.add(parent.key);
152 | return;
153 | }
154 |
155 | let allChecked = true;
156 | let partialChecked = false;
157 |
158 | (parent.children || [])
159 | .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
160 | .forEach(({ key }) => {
161 | const checked = checkedKeys.has(key);
162 | if (allChecked && !checked) {
163 | allChecked = false;
164 | }
165 | if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
166 | partialChecked = true;
167 | }
168 | });
169 |
170 | if (!allChecked) {
171 | checkedKeys.delete(parent.key);
172 | }
173 | if (partialChecked) {
174 | halfCheckedKeys.add(parent.key);
175 | }
176 |
177 | visitedKeys.add(parent.key);
178 | });
179 | }
180 |
181 | return {
182 | checkedKeys: Array.from(checkedKeys),
183 | halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
184 | };
185 | }
186 |
187 | /**
188 | * Conduct with keys.
189 | * @param keyList current key list
190 | * @param keyEntities key - dataEntity map
191 | * @param mode `fill` to fill missing key, `clean` to remove useless key
192 | */
193 | export function conductCheck(
194 | keyList: Key[],
195 | checked: true | { checked: false; halfCheckedKeys: Key[] },
196 | keyEntities: KeyEntities,
197 | getCheckDisabled?: GetCheckDisabled,
198 | ): ConductReturnType {
199 | const warningMissKeys: Key[] = [];
200 |
201 | let syntheticGetCheckDisabled: GetCheckDisabled;
202 | if (getCheckDisabled) {
203 | syntheticGetCheckDisabled = getCheckDisabled;
204 | } else {
205 | syntheticGetCheckDisabled = isCheckDisabled;
206 | }
207 |
208 | // We only handle exist keys
209 | const keys = new Set(
210 | keyList.filter(key => {
211 | const hasEntity = !!getEntity(keyEntities, key);
212 | if (!hasEntity) {
213 | warningMissKeys.push(key);
214 | }
215 |
216 | return hasEntity;
217 | }),
218 | );
219 | const levelEntities = new Map>>();
220 | let maxLevel = 0;
221 |
222 | // Convert entities by level for calculation
223 | Object.keys(keyEntities).forEach(key => {
224 | const entity = keyEntities[key];
225 | const { level } = entity;
226 |
227 | let levelSet: Set> = levelEntities.get(level);
228 | if (!levelSet) {
229 | levelSet = new Set();
230 | levelEntities.set(level, levelSet);
231 | }
232 |
233 | levelSet.add(entity);
234 |
235 | maxLevel = Math.max(maxLevel, level);
236 | });
237 |
238 | warning(
239 | !warningMissKeys.length,
240 | `Tree missing follow keys: ${warningMissKeys
241 | .slice(0, 100)
242 | .map(key => `'${key}'`)
243 | .join(', ')}`,
244 | );
245 |
246 | let result: ConductReturnType;
247 | if (checked === true) {
248 | result = fillConductCheck(
249 | keys,
250 | levelEntities,
251 | maxLevel,
252 | syntheticGetCheckDisabled,
253 | );
254 | } else {
255 | result = cleanConductCheck(
256 | keys,
257 | checked.halfCheckedKeys,
258 | levelEntities,
259 | maxLevel,
260 | syntheticGetCheckDisabled,
261 | );
262 | }
263 |
264 | return result;
265 | }
266 |
--------------------------------------------------------------------------------
/src/utils/diffUtil.ts:
--------------------------------------------------------------------------------
1 | import type { Key, FlattenNode } from '../interface';
2 |
3 | export function findExpandedKeys(prev: Key[] = [], next: Key[] = []) {
4 | const prevLen = prev.length;
5 | const nextLen = next.length;
6 |
7 | if (Math.abs(prevLen - nextLen) !== 1) {
8 | return { add: false, key: null };
9 | }
10 |
11 | function find(shorter: Key[], longer: Key[]) {
12 | const cache: Map = new Map();
13 | shorter.forEach(key => {
14 | cache.set(key, true);
15 | });
16 |
17 | const keys = longer.filter(key => !cache.has(key));
18 |
19 | return keys.length === 1 ? keys[0] : null;
20 | }
21 |
22 | if (prevLen < nextLen) {
23 | return {
24 | add: true,
25 | key: find(prev, next),
26 | };
27 | }
28 |
29 | return {
30 | add: false,
31 | key: find(next, prev),
32 | };
33 | }
34 |
35 | export function getExpandRange(shorter: FlattenNode[], longer: FlattenNode[], key: Key) {
36 | const shorterStartIndex = shorter.findIndex(data => data.key === key);
37 | const shorterEndNode = shorter[shorterStartIndex + 1];
38 | const longerStartIndex = longer.findIndex(data => data.key === key);
39 |
40 | if (shorterEndNode) {
41 | const longerEndIndex = longer.findIndex(data => data.key === shorterEndNode.key);
42 | return longer.slice(longerStartIndex + 1, longerEndIndex);
43 | }
44 | return longer.slice(longerStartIndex + 1);
45 | }
46 |
--------------------------------------------------------------------------------
/src/utils/keyUtil.ts:
--------------------------------------------------------------------------------
1 | import type { Key, KeyEntities, SafeKey } from '../interface';
2 |
3 | export default function getEntity(keyEntities: KeyEntities, key: Key) {
4 | return keyEntities[key as SafeKey];
5 | }
6 |
--------------------------------------------------------------------------------
/src/utils/treeUtil.ts:
--------------------------------------------------------------------------------
1 | import toArray from '@rc-component/util/lib/Children/toArray';
2 | import omit from '@rc-component/util/lib/omit';
3 | import warning from '@rc-component/util/lib/warning';
4 | import * as React from 'react';
5 | import type {
6 | BasicDataNode,
7 | DataEntity,
8 | DataNode,
9 | EventDataNode,
10 | FieldNames,
11 | FlattenNode,
12 | GetKey,
13 | Key,
14 | KeyEntities,
15 | NodeElement,
16 | SafeKey,
17 | TreeNodeProps,
18 | } from '../interface';
19 | import getEntity from './keyUtil';
20 |
21 | export function getPosition(level: string | number, index: number) {
22 | return `${level}-${index}`;
23 | }
24 |
25 | export function isTreeNode(node: NodeElement) {
26 | return node && node.type && node.type.isTreeNode;
27 | }
28 |
29 | export function getKey(key: Key, pos: string) {
30 | if (key !== null && key !== undefined) {
31 | return key;
32 | }
33 | return pos;
34 | }
35 |
36 | export function fillFieldNames(fieldNames?: FieldNames): Required {
37 | const { title, _title, key, children } = fieldNames || {};
38 | const mergedTitle = title || 'title';
39 |
40 | return {
41 | title: mergedTitle,
42 | _title: _title || [mergedTitle],
43 | key: key || 'key',
44 | children: children || 'children',
45 | };
46 | }
47 |
48 | /**
49 | * Warning if TreeNode do not provides key
50 | */
51 | export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames) {
52 | const keys: Map = new Map();
53 |
54 | function dig(list: DataNode[], path: string = '') {
55 | (list || []).forEach(treeNode => {
56 | const key = treeNode[fieldNames.key];
57 | const children = treeNode[fieldNames.children];
58 | warning(
59 | key !== null && key !== undefined,
60 | `Tree node must have a certain key: [${path}${key}]`,
61 | );
62 |
63 | const recordKey = String(key);
64 | warning(
65 | !keys.has(recordKey) || key === null || key === undefined,
66 | `Same 'key' exist in the Tree: ${recordKey}`,
67 | );
68 | keys.set(recordKey, true);
69 |
70 | dig(children, `${path}${recordKey} > `);
71 | });
72 | }
73 |
74 | dig(treeData);
75 | }
76 |
77 | /**
78 | * Convert `children` of Tree into `treeData` structure.
79 | */
80 | export function convertTreeToData(rootNodes: React.ReactNode): DataNode[] {
81 | function dig(node: React.ReactNode): DataNode[] {
82 | const treeNodes = toArray(node) as NodeElement[];
83 | return treeNodes
84 | .map(treeNode => {
85 | // Filter invalidate node
86 | if (!isTreeNode(treeNode)) {
87 | warning(!treeNode, 'Tree/TreeNode can only accept TreeNode as children.');
88 | return null;
89 | }
90 |
91 | const { key } = treeNode;
92 | const { children, ...rest } = treeNode.props;
93 |
94 | const dataNode: DataNode = {
95 | key: key as Key,
96 | ...rest,
97 | };
98 |
99 | const parsedChildren = dig(children);
100 | if (parsedChildren.length) {
101 | dataNode.children = parsedChildren;
102 | }
103 |
104 | return dataNode;
105 | })
106 | .filter((dataNode: DataNode) => dataNode);
107 | }
108 |
109 | return dig(rootNodes);
110 | }
111 |
112 | /**
113 | * Flat nest tree data into flatten list. This is used for virtual list render.
114 | * @param treeNodeList Origin data node list
115 | * @param expandedKeys
116 | * need expanded keys, provides `true` means all expanded (used in `rc-tree-select`).
117 | */
118 | export function flattenTreeData(
119 | treeNodeList: TreeDataType[],
120 | expandedKeys: Key[] | true,
121 | fieldNames: FieldNames,
122 | ): FlattenNode[] {
123 | const {
124 | _title: fieldTitles,
125 | key: fieldKey,
126 | children: fieldChildren,
127 | } = fillFieldNames(fieldNames);
128 |
129 | const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
130 | const flattenList: FlattenNode[] = [];
131 |
132 | function dig(
133 | list: TreeDataType[],
134 | parent: FlattenNode = null,
135 | ): FlattenNode[] {
136 | return list.map((treeNode, index) => {
137 | const pos: string = getPosition(parent ? parent.pos : '0', index);
138 | const mergedKey = getKey(treeNode[fieldKey], pos);
139 |
140 | // Pick matched title in field title list
141 | let mergedTitle: React.ReactNode;
142 | for (let i = 0; i < fieldTitles.length; i += 1) {
143 | const fieldTitle = fieldTitles[i];
144 | if (treeNode[fieldTitle] !== undefined) {
145 | mergedTitle = treeNode[fieldTitle];
146 | break;
147 | }
148 | }
149 |
150 | // Add FlattenDataNode into list
151 | // We use `Object.assign` here to save perf since babel's `objectSpread` has perf issue
152 | const flattenNode: FlattenNode = Object.assign(
153 | omit(treeNode, [...fieldTitles, fieldKey, fieldChildren] as any),
154 | {
155 | title: mergedTitle,
156 | key: mergedKey,
157 | parent,
158 | pos,
159 | children: null,
160 | data: treeNode,
161 | isStart: [...(parent ? parent.isStart : []), index === 0],
162 | isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
163 | },
164 | );
165 | flattenList.push(flattenNode);
166 |
167 | // Loop treeNode children
168 | if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
169 | flattenNode.children = dig(treeNode[fieldChildren] || [], flattenNode);
170 | } else {
171 | flattenNode.children = [];
172 | }
173 |
174 | return flattenNode;
175 | });
176 | }
177 |
178 | dig(treeNodeList);
179 |
180 | return flattenList;
181 | }
182 |
183 | type ExternalGetKey = GetKey | string;
184 |
185 | interface TraverseDataNodesConfig {
186 | childrenPropName?: string;
187 | externalGetKey?: ExternalGetKey;
188 | fieldNames?: FieldNames;
189 | }
190 |
191 | /**
192 | * Traverse all the data by `treeData`.
193 | * Please not use it out of the `rc-tree` since we may refactor this code.
194 | */
195 | export function traverseDataNodes(
196 | dataNodes: DataNode[],
197 | callback: (data: {
198 | node: DataNode;
199 | index: number;
200 | pos: string;
201 | key: Key;
202 | parentPos: string | number;
203 | level: number;
204 | nodes: DataNode[];
205 | }) => void,
206 | // To avoid too many params, let use config instead of origin param
207 | config?: TraverseDataNodesConfig | string,
208 | ) {
209 | let mergedConfig: TraverseDataNodesConfig = {};
210 | if (typeof config === 'object') {
211 | mergedConfig = config;
212 | } else {
213 | mergedConfig = { externalGetKey: config };
214 | }
215 | mergedConfig = mergedConfig || {};
216 |
217 | // Init config
218 | const { childrenPropName, externalGetKey, fieldNames } = mergedConfig;
219 |
220 | const { key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
221 |
222 | const mergeChildrenPropName = childrenPropName || fieldChildren;
223 |
224 | // Get keys
225 | let syntheticGetKey: (node: DataNode, pos?: string) => Key;
226 | if (externalGetKey) {
227 | if (typeof externalGetKey === 'string') {
228 | syntheticGetKey = (node: DataNode) => (node as any)[externalGetKey as string];
229 | } else if (typeof externalGetKey === 'function') {
230 | syntheticGetKey = (node: DataNode) => (externalGetKey as GetKey)(node);
231 | }
232 | } else {
233 | syntheticGetKey = (node, pos) => getKey(node[fieldKey], pos);
234 | }
235 |
236 | // Process
237 | function processNode(
238 | node: DataNode,
239 | index?: number,
240 | parent?: { node: DataNode; pos: string; level: number },
241 | pathNodes?: DataNode[],
242 | ) {
243 | const children = node ? node[mergeChildrenPropName] : dataNodes;
244 | const pos = node ? getPosition(parent.pos, index) : '0';
245 | const connectNodes = node ? [...pathNodes, node] : [];
246 |
247 | // Process node if is not root
248 | if (node) {
249 | const key: Key = syntheticGetKey(node, pos);
250 | const data = {
251 | node,
252 | index,
253 | pos,
254 | key,
255 | parentPos: parent.node ? parent.pos : null,
256 | level: parent.level + 1,
257 | nodes: connectNodes,
258 | };
259 |
260 | callback(data);
261 | }
262 |
263 | // Process children node
264 | if (children) {
265 | children.forEach((subNode, subIndex) => {
266 | processNode(
267 | subNode,
268 | subIndex,
269 | {
270 | node,
271 | pos,
272 | level: parent ? parent.level + 1 : -1,
273 | },
274 | connectNodes,
275 | );
276 | });
277 | }
278 | }
279 |
280 | processNode(null);
281 | }
282 |
283 | interface Wrapper {
284 | posEntities: Record;
285 | keyEntities: KeyEntities;
286 | }
287 |
288 | /**
289 | * Convert `treeData` into entity records.
290 | */
291 | export function convertDataToEntities(
292 | dataNodes: DataNode[],
293 | {
294 | initWrapper,
295 | processEntity,
296 | onProcessFinished,
297 | externalGetKey,
298 | childrenPropName,
299 | fieldNames,
300 | }: {
301 | initWrapper?: (wrapper: Wrapper) => Wrapper;
302 | processEntity?: (entity: DataEntity, wrapper: Wrapper) => void;
303 | onProcessFinished?: (wrapper: Wrapper) => void;
304 | externalGetKey?: ExternalGetKey;
305 | childrenPropName?: string;
306 | fieldNames?: FieldNames;
307 | } = {},
308 | /** @deprecated Use `config.externalGetKey` instead */
309 | legacyExternalGetKey?: ExternalGetKey,
310 | ) {
311 | // Init config
312 | const mergedExternalGetKey = externalGetKey || legacyExternalGetKey;
313 |
314 | const posEntities = {};
315 | const keyEntities = {};
316 | let wrapper: Wrapper = {
317 | posEntities,
318 | keyEntities,
319 | };
320 |
321 | if (initWrapper) {
322 | wrapper = initWrapper(wrapper) || wrapper;
323 | }
324 |
325 | traverseDataNodes(
326 | dataNodes,
327 | item => {
328 | const { node, index, pos, key, parentPos, level, nodes } = item;
329 | const entity: DataEntity = { node, nodes, index, key, pos, level };
330 |
331 | const mergedKey = getKey(key, pos);
332 |
333 | posEntities[pos] = entity;
334 | keyEntities[mergedKey as SafeKey] = entity;
335 |
336 | // Fill children
337 | entity.parent = posEntities[parentPos];
338 | if (entity.parent) {
339 | entity.parent.children = entity.parent.children || [];
340 | entity.parent.children.push(entity);
341 | }
342 |
343 | if (processEntity) {
344 | processEntity(entity, wrapper);
345 | }
346 | },
347 | { externalGetKey: mergedExternalGetKey, childrenPropName, fieldNames },
348 | );
349 |
350 | if (onProcessFinished) {
351 | onProcessFinished(wrapper);
352 | }
353 |
354 | return wrapper;
355 | }
356 |
357 | export interface TreeNodeRequiredProps {
358 | expandedKeys: Key[];
359 | selectedKeys: Key[];
360 | loadedKeys: Key[];
361 | loadingKeys: Key[];
362 | checkedKeys: Key[];
363 | halfCheckedKeys: Key[];
364 | dragOverNodeKey: Key;
365 | dropPosition: number;
366 | keyEntities: KeyEntities;
367 | }
368 |
369 | /**
370 | * Get TreeNode props with Tree props.
371 | */
372 | export function getTreeNodeProps(
373 | key: Key,
374 | {
375 | expandedKeys,
376 | selectedKeys,
377 | loadedKeys,
378 | loadingKeys,
379 | checkedKeys,
380 | halfCheckedKeys,
381 | dragOverNodeKey,
382 | dropPosition,
383 | keyEntities,
384 | }: TreeNodeRequiredProps,
385 | ) {
386 | const entity = getEntity(keyEntities, key);
387 |
388 | const treeNodeProps = {
389 | eventKey: key,
390 | expanded: expandedKeys.indexOf(key) !== -1,
391 | selected: selectedKeys.indexOf(key) !== -1,
392 | loaded: loadedKeys.indexOf(key) !== -1,
393 | loading: loadingKeys.indexOf(key) !== -1,
394 | checked: checkedKeys.indexOf(key) !== -1,
395 | halfChecked: halfCheckedKeys.indexOf(key) !== -1,
396 | pos: String(entity ? entity.pos : ''),
397 |
398 | // [Legacy] Drag props
399 | // Since the interaction of drag is changed, the semantic of the props are
400 | // not accuracy, I think it should be finally removed
401 | dragOver: dragOverNodeKey === key && dropPosition === 0,
402 | dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
403 | dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
404 | };
405 |
406 | return treeNodeProps;
407 | }
408 |
409 | export function convertNodePropsToEventData(
410 | props: TreeNodeProps,
411 | ): EventDataNode {
412 | const {
413 | data,
414 | expanded,
415 | selected,
416 | checked,
417 | loaded,
418 | loading,
419 | halfChecked,
420 | dragOver,
421 | dragOverGapTop,
422 | dragOverGapBottom,
423 | pos,
424 | active,
425 | eventKey,
426 | } = props;
427 |
428 | const eventData = {
429 | ...data,
430 | expanded,
431 | selected,
432 | checked,
433 | loaded,
434 | loading,
435 | halfChecked,
436 | dragOver,
437 | dragOverGapTop,
438 | dragOverGapBottom,
439 | pos,
440 | active,
441 | key: eventKey,
442 | };
443 |
444 | if (!('props' in eventData)) {
445 | Object.defineProperty(eventData, 'props', {
446 | get() {
447 | warning(
448 | false,
449 | 'Second param return from event is node data instead of TreeNode instance. Please read value directly instead of reading from `props`.',
450 | );
451 | return props;
452 | },
453 | });
454 | }
455 |
456 | return eventData;
457 | }
458 |
--------------------------------------------------------------------------------
/tests/Accessibility.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, react/no-multi-comp */
2 | import { fireEvent, render } from '@testing-library/react';
3 | import KeyCode from '@rc-component/util/lib/KeyCode';
4 | import React from 'react';
5 | import Tree, { FieldDataNode } from '../src';
6 | import { spyConsole } from './util';
7 |
8 | describe('Tree Accessibility', () => {
9 | spyConsole();
10 |
11 | describe('key operation', () => {
12 | function typeTest(props, spaceCallback, enterCallback) {
13 | const onExpand = jest.fn();
14 | const onFocus = jest.fn();
15 | const onBlur = jest.fn();
16 | const onKeyDown = jest.fn();
17 | const onActiveChange = jest.fn();
18 |
19 | function checkKeyDownTrigger() {
20 | expect(onKeyDown).toHaveBeenCalled();
21 | onKeyDown.mockReset();
22 | }
23 |
24 | function checkActiveTrigger(key) {
25 | expect(onActiveChange).toHaveBeenCalledWith(key);
26 | onActiveChange.mockReset();
27 | }
28 |
29 | const { container } = render(
30 | ,
40 | );
41 |
42 | function keyDown(keyCode: number) {
43 | fireEvent.keyDown(container.querySelector('input'), {
44 | keyCode,
45 | });
46 | }
47 |
48 | function getTreeNode(index: number) {
49 | const treeNodes = container
50 | .querySelector('.rc-tree-list-holder')
51 | .querySelectorAll('.rc-tree-treenode');
52 |
53 | return treeNodes[(index + treeNodes.length) % treeNodes.length];
54 | }
55 |
56 | // Focus
57 | fireEvent.focus(container.querySelector('input'));
58 | expect(onFocus).toHaveBeenCalled();
59 |
60 | // Arrow up: last one
61 | keyDown(KeyCode.UP);
62 | expect(getTreeNode(-1)).toHaveClass('rc-tree-treenode-active');
63 | checkKeyDownTrigger();
64 | checkActiveTrigger('child 2');
65 |
66 | // Arrow down: first one
67 | keyDown(KeyCode.DOWN);
68 | expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
69 | checkKeyDownTrigger();
70 | checkActiveTrigger('parent');
71 |
72 | // Arrow up: last one again
73 | keyDown(KeyCode.UP);
74 | expect(getTreeNode(-1)).toHaveClass('rc-tree-treenode-active');
75 | checkKeyDownTrigger();
76 | checkActiveTrigger('child 2');
77 |
78 | // Arrow left: parent
79 | keyDown(KeyCode.LEFT);
80 | expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
81 | checkKeyDownTrigger();
82 | checkActiveTrigger('parent');
83 |
84 | // Arrow left: collapse
85 | keyDown(KeyCode.LEFT);
86 | expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
87 | expect(onExpand).toHaveBeenCalledWith(['child 1', 'child 2'], expect.anything());
88 | checkKeyDownTrigger();
89 |
90 | // Arrow right: expand
91 | onExpand.mockReset();
92 | keyDown(KeyCode.RIGHT);
93 | expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
94 | expect(onExpand).toHaveBeenCalledWith(['child 1', 'child 2', 'parent'], expect.anything());
95 | checkKeyDownTrigger();
96 |
97 | // Arrow right: first child
98 | onExpand.mockReset();
99 | keyDown(KeyCode.RIGHT);
100 | expect(getTreeNode(1)).toHaveClass('rc-tree-treenode-active');
101 | checkKeyDownTrigger();
102 | checkActiveTrigger('child 1');
103 |
104 | // SPACE: confirm
105 | keyDown(KeyCode.SPACE);
106 | spaceCallback();
107 | checkKeyDownTrigger();
108 |
109 | // ENTER: confirm again
110 | keyDown(KeyCode.ENTER);
111 | enterCallback();
112 | checkKeyDownTrigger();
113 |
114 | // Blur
115 | fireEvent.blur(container.querySelector('input'));
116 | expect(onBlur).toHaveBeenCalled();
117 |
118 | // null activeKey
119 | fireEvent.mouseMove(getTreeNode(0));
120 | checkActiveTrigger(null);
121 |
122 | for (let i = 0; i < 10; i += 1) {
123 | fireEvent.mouseMove(getTreeNode(0));
124 | expect(onActiveChange).not.toHaveBeenCalled();
125 | }
126 | }
127 |
128 | it('onSelect', () => {
129 | const onSelect = jest.fn();
130 | typeTest(
131 | { onSelect },
132 | () => {
133 | expect(onSelect).toHaveBeenCalledWith(['child 1'], expect.anything());
134 | onSelect.mockReset();
135 | },
136 | () => {
137 | expect(onSelect).toHaveBeenCalledWith([], expect.anything());
138 | },
139 | );
140 | });
141 |
142 | it('onCheck', () => {
143 | const onCheck = jest.fn();
144 | typeTest(
145 | { onCheck, checkable: true, selectable: false },
146 | () => {
147 | expect(onCheck).toHaveBeenCalledWith(['child 1'], expect.anything());
148 | onCheck.mockReset();
149 | },
150 | () => {
151 | expect(onCheck).toHaveBeenCalledWith([], expect.anything());
152 | },
153 | );
154 | });
155 |
156 | it('not crash if not exist', () => {
157 | const { container } = render();
158 |
159 | fireEvent.focus(container.querySelector('input'));
160 |
161 | // Arrow should not work
162 | fireEvent.keyDown(container.querySelector('input'), {
163 | keyCode: KeyCode.UP,
164 | });
165 | expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
166 | });
167 |
168 | it('remove active if mouse hover', () => {
169 | const { container } = render();
170 |
171 | fireEvent.focus(container.querySelector('input'));
172 |
173 | fireEvent.keyDown(container.querySelector('input'), {
174 | keyCode: KeyCode.UP,
175 | });
176 | expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
177 |
178 | // Mouse move
179 | fireEvent.mouseMove(container.querySelectorAll('.rc-tree-treenode')[1]);
180 | expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
181 | });
182 |
183 | it('fieldNames should also work', () => {
184 | const onActiveChange = jest.fn();
185 | const onSelect = jest.fn();
186 |
187 | const { container } = render(
188 | >
189 | defaultExpandAll
190 | treeData={[{ value: 'first' }, { value: 'second' }]}
191 | fieldNames={{ key: 'value' }}
192 | onActiveChange={onActiveChange}
193 | onSelect={onSelect}
194 | />,
195 | );
196 |
197 | fireEvent.focus(container.querySelector('input'));
198 |
199 | fireEvent.keyDown(container.querySelector('input'), {
200 | keyCode: KeyCode.DOWN,
201 | });
202 | expect(onActiveChange).toHaveBeenCalledWith('first');
203 |
204 | fireEvent.keyDown(container.querySelector('input'), {
205 | keyCode: KeyCode.DOWN,
206 | });
207 | expect(onActiveChange).toHaveBeenCalledWith('second');
208 |
209 | fireEvent.keyDown(container.querySelector('input'), {
210 | keyCode: KeyCode.ENTER,
211 | });
212 | expect(onSelect).toHaveBeenCalledWith(['second'], expect.anything());
213 | });
214 | });
215 |
216 | it('disabled should prevent keyboard', () => {
217 | const { container } = render();
218 | expect(container.querySelector('input')).toHaveAttribute('disabled');
219 | });
220 |
221 | describe('activeKey in control', () => {
222 | it('basic', () => {
223 | const { container, rerender } = render(
224 | ,
232 | );
233 |
234 | expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
235 |
236 | rerender(
237 | ,
246 | );
247 | expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
248 | });
249 |
250 | it('with fieldNames', () => {
251 | const { container, rerender } = render(
252 | >
253 | fieldNames={{ key: 'value' }}
254 | treeData={[
255 | {
256 | title: 'Parent',
257 | value: 'parent',
258 | },
259 | ]}
260 | />,
261 | );
262 |
263 | expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
264 |
265 | rerender(
266 | >
267 | fieldNames={{ key: 'value' }}
268 | treeData={[
269 | {
270 | title: 'Parent',
271 | value: 'parent',
272 | },
273 | ]}
274 | activeKey="parent"
275 | />,
276 | );
277 | expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
278 | });
279 | });
280 | });
281 |
--------------------------------------------------------------------------------
/tests/FieldNames.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, react/no-multi-comp */
2 | import React from 'react';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import Tree from '../src';
5 | import { spyConsole } from './util';
6 |
7 | describe('FieldNames', () => {
8 | spyConsole();
9 |
10 | it('customize fieldNames', () => {
11 | const onSelect = jest.fn();
12 | const { container } = render(
13 | ,
35 | );
36 |
37 | // Title
38 | const titleList = Array.from(
39 | container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode'),
40 | ).map(node => node.textContent);
41 |
42 | expect(titleList).toEqual(['Title', 'Sub1', 'Sub2']);
43 |
44 | // Key
45 | container.querySelectorAll('.rc-tree-title').forEach(ele => {
46 | fireEvent.click(ele);
47 | });
48 |
49 | expect(onSelect).toHaveBeenCalledWith(['title', 'sub_1', 'sub_2'], expect.anything());
50 | });
51 |
52 | it('dynamic change fieldNames', () => {
53 | const renderTree = (props?: any) => (
54 |
74 | );
75 |
76 | const { container, rerender } = render(renderTree());
77 |
78 | // Title
79 | expect(
80 | Array.from(container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode')).map(
81 | node => node.textContent,
82 | ),
83 | ).toEqual(['Origin Title', 'Origin Sub 1', 'Origin Sub 2']);
84 |
85 | // Change it
86 | rerender(
87 | renderTree({
88 | treeData: [
89 | {
90 | myTitle: 'New Title',
91 | myKey: 'parent',
92 | myChildren: [
93 | {
94 | myTitle: 'New Sub 1',
95 | myKey: 'sub_1',
96 | },
97 | {
98 | myTitle: 'New Sub 2',
99 | myKey: 'sub_2',
100 | },
101 | ],
102 | },
103 | {
104 | myTitle: 'New Title 2',
105 | myKey: 'parent2',
106 | myChildren: [
107 | {
108 | myTitle: 'New Sub 3',
109 | myKey: 'sub_3',
110 | },
111 | ],
112 | },
113 | ],
114 | fieldNames: {
115 | title: 'myTitle',
116 | key: 'myKey',
117 | children: 'myChildren',
118 | },
119 | }),
120 | );
121 |
122 | expect(
123 | Array.from(container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode')).map(
124 | node => node.textContent,
125 | ),
126 | ).toEqual(['New Title', 'New Sub 1', 'New Sub 2', 'New Title 2']);
127 | });
128 |
129 | it('checkable should work', () => {
130 | const onCheck = jest.fn();
131 |
132 | const { container } = render(
133 | ,
150 | );
151 |
152 | fireEvent.click(container.querySelector('.rc-tree-checkbox'));
153 | expect(onCheck).toHaveBeenCalledWith(['parent', 'child'], expect.anything());
154 | });
155 |
156 | // Internal usage. Safe to remove
157 | it('fieldNames using _title', () => {
158 | const onSelect = jest.fn();
159 |
160 | const { container } = render(
161 | ,
183 | );
184 |
185 | // Title
186 | const titleList = Array.from(
187 | container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode'),
188 | ).map(node => node.textContent);
189 |
190 | expect(titleList).toEqual(['Title', 'Sub1', 'Sub2']);
191 |
192 | // Key
193 | container.querySelectorAll('.rc-tree-title').forEach(ele => {
194 | fireEvent.click(ele);
195 | });
196 |
197 | expect(onSelect).toHaveBeenCalledWith(['title', 'sub_1', 'sub_2'], expect.anything());
198 | });
199 | });
200 |
--------------------------------------------------------------------------------
/tests/MyTree.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Tree from '../src';
3 |
4 | const MyTree = props => {
5 | const { treeData, checkable, checkedKeys, onCheck } = props;
6 | return (
7 |
15 | );
16 | };
17 |
18 | export default MyTree;
19 |
--------------------------------------------------------------------------------
/tests/React18.spec.tsx:
--------------------------------------------------------------------------------
1 | import { fireEvent, render } from '@testing-library/react';
2 | import React from 'react';
3 | import Tree from '../src';
4 |
5 | describe('React 18', () => {
6 | // This does not real work since waring is in 18.3.0 but current still is next version.
7 | it('no warning', () => {
8 | const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
9 | render(
10 |
11 |
20 | ,
21 | );
22 |
23 | expect(errSpy).not.toHaveBeenCalled();
24 | });
25 |
26 | it('expand work', () => {
27 | const onExpand = jest.fn();
28 | const { container } = render(
29 |
30 |
46 | ,
47 | );
48 |
49 | // All opened
50 | expect(onExpand).not.toHaveBeenCalled();
51 | expect(
52 | container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
53 | ).toHaveLength(2);
54 |
55 | // Collapse one
56 | fireEvent.click(container.querySelector('.rc-tree-switcher_open'));
57 | expect(onExpand).toHaveBeenCalled();
58 | expect(
59 | container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
60 | ).toHaveLength(1);
61 | });
62 |
63 | it('checkable work', () => {
64 | const onCheck = jest.fn();
65 | const { container } = render(
66 |
67 |
78 | ,
79 | );
80 |
81 | expect(onCheck).not.toHaveBeenCalled();
82 | expect(container.querySelector('.rc-tree-checkbox-checked')).toBeFalsy();
83 |
84 | fireEvent.click(container.querySelector('.rc-tree-checkbox'));
85 | expect(onCheck).toHaveBeenCalled();
86 | expect(container.querySelector('.rc-tree-checkbox-checked')).toBeTruthy();
87 | });
88 | });
89 |
--------------------------------------------------------------------------------
/tests/TreeMotion.spec.tsx:
--------------------------------------------------------------------------------
1 | import { act, fireEvent, render } from '@testing-library/react';
2 | import React from 'react';
3 | import Tree, { FieldDataNode, TreeNode } from '../src';
4 | import { TreeContext } from '../src/contextTypes';
5 | import MotionTreeNode from '../src/MotionTreeNode';
6 | import { getMinimumRangeTransitionRange } from '../src/NodeList';
7 |
8 | jest.mock('@rc-component/motion/lib/util/motion', () => {
9 | const origin = jest.requireActual('@rc-component/motion/lib/util/motion');
10 |
11 | return {
12 | ...origin,
13 | supportTransition: () => true,
14 | };
15 | });
16 |
17 | describe('Tree Motion', () => {
18 | beforeEach(() => {
19 | jest.useFakeTimers();
20 | });
21 |
22 | afterEach(() => {
23 | jest.clearAllTimers();
24 | jest.useRealTimers();
25 | });
26 |
27 | it('basic', () => {
28 | const motion = {
29 | motionName: 'bamboo',
30 | };
31 | const { container } = render(
32 |
33 |
34 |
35 |
36 | ,
37 | );
38 |
39 | fireEvent.click(container.querySelector('.rc-tree-switcher'));
40 |
41 | expect(container.querySelector('.bamboo-appear')).toBeTruthy();
42 | });
43 |
44 | it('hide item', () => {
45 | const renderTree = (props?: any) => (
46 |
52 | );
53 |
54 | const { container, rerender } = render(renderTree());
55 |
56 | rerender(renderTree({ expandedKeys: [] }));
57 |
58 | expect(container.querySelector('.bamboo-appear')).toBeFalsy();
59 | });
60 |
61 | it('getMinimumRangeTransitionRange', () => {
62 | const visibleList = getMinimumRangeTransitionRange(
63 | new Array(100).fill(null).map((_, index) => index) as any,
64 | true,
65 | 100,
66 | 20,
67 | );
68 |
69 | expect(visibleList.length < 10).toBeTruthy();
70 | });
71 |
72 | it('not crash', () => {
73 | const renderTree = (props?: any) => (
74 |
80 | );
81 | const { rerender } = render(renderTree());
82 |
83 | rerender(renderTree({ treeData: [] }));
84 | });
85 |
86 | it('should not expanded when in motion', () => {
87 | // const raf = jest
88 | // .spyOn(window, 'requestAnimationFrame')
89 | // .mockImplementation(fn => window.setTimeout(fn, 16));
90 |
91 | const onExpand = jest.fn();
92 | const { container } = render(
93 |
101 |
102 |
103 |
104 | ,
105 | );
106 |
107 | function doExpand() {
108 | fireEvent.click(container.querySelector('.rc-tree-switcher'));
109 | }
110 |
111 | // First click should work
112 | doExpand();
113 | expect(onExpand).toHaveBeenCalled();
114 | onExpand.mockReset();
115 |
116 | // Not trigger when in motion
117 | doExpand();
118 | expect(onExpand).not.toHaveBeenCalled();
119 | });
120 |
121 | describe('MotionTreeNode should always trigger motion end', () => {
122 | it('with motionNodes', () => {
123 | const onMotionStart = jest.fn();
124 | const onMotionEnd = jest.fn();
125 | const { unmount } = render(
126 |
127 |
128 |
138 |
139 | ,
140 | );
141 |
142 | expect(onMotionStart).toHaveBeenCalled();
143 | expect(onMotionEnd).not.toHaveBeenCalled();
144 |
145 | unmount();
146 | act(() => {
147 | jest.runAllTimers();
148 | });
149 | expect(onMotionEnd).toHaveBeenCalled();
150 | });
151 |
152 | it('without motionNodes', () => {
153 | const onMotionStart = jest.fn();
154 | const onMotionEnd = jest.fn();
155 | const { unmount } = render(
156 | null,
162 | } as any
163 | }
164 | >
165 |
171 | ,
172 | );
173 |
174 | expect(onMotionStart).not.toHaveBeenCalled();
175 | expect(onMotionEnd).not.toHaveBeenCalled();
176 |
177 | unmount();
178 | act(() => {
179 | jest.runAllTimers();
180 | });
181 | expect(onMotionStart).not.toHaveBeenCalled();
182 | expect(onMotionEnd).not.toHaveBeenCalled();
183 | });
184 | });
185 |
186 | it('motion should work well with fieldNames', () => {
187 | const Demo = () => (
188 | >
189 | defaultExpandAll
190 | fieldNames={{
191 | title: 'name',
192 | key: 'id',
193 | children: 'sub',
194 | }}
195 | motion={{
196 | motionName: 'bamboo',
197 | }}
198 | treeData={[
199 | {
200 | id: '1',
201 | name: 'A',
202 | sub: [
203 | {
204 | id: '2',
205 | name: 'B',
206 | sub: [],
207 | },
208 | ],
209 | },
210 | ]}
211 | />
212 | );
213 |
214 | const { container } = render();
215 | expect(container.querySelector('[title="B"]')).toBeTruthy();
216 |
217 | fireEvent.click(container.querySelector('.rc-tree-switcher'));
218 | act(() => {
219 | jest.runAllTimers();
220 | });
221 | fireEvent.animationEnd(container.querySelector('.bamboo-leave-active'));
222 | expect(container.querySelector('[title="B"]')).toBeFalsy();
223 | });
224 |
225 | it('motion should not revert flatten list', () => {
226 | const renderTree = (props?: any) => (
227 |
247 | );
248 |
249 | const { container, rerender } = render(renderTree());
250 |
251 | rerender(
252 | renderTree({
253 | expandedKeys: ['parent'],
254 | }),
255 | );
256 |
257 | for (let i = 0; i < 10; i += 1) {
258 | act(() => {
259 | jest.runAllTimers();
260 | });
261 | }
262 |
263 | rerender(
264 | renderTree({
265 | treeData: [
266 | {
267 | key: 'parent',
268 | title: 'parent2',
269 | children: [
270 | {
271 | key: 'child',
272 | title: 'child2',
273 | },
274 | ],
275 | },
276 | ],
277 | }),
278 | );
279 |
280 | expect(container.querySelectorAll('span.rc-tree-title')[0].textContent).toEqual('parent2');
281 | expect(container.querySelectorAll('span.rc-tree-title')[1].textContent).toEqual('child2');
282 | });
283 |
284 | // https://github.com/ant-design/ant-design/issues/43282
285 | it('dynamic modify data should stop motion', () => {
286 | const motion = {
287 | motionName: 'bamboo',
288 | };
289 |
290 | const getTreeData = () => [
291 | {
292 | title: 'parent',
293 | key: 'parent',
294 | children: [
295 | {
296 | title: 'child',
297 | key: 'child',
298 | },
299 | ],
300 | },
301 | ];
302 |
303 | const oriData = getTreeData();
304 |
305 | const { container, rerender } = render();
306 | rerender();
307 |
308 | // Replace `treeData` before motion end
309 | const onExpand = jest.fn();
310 | rerender(
311 | ,
317 | );
318 |
319 | // Delay for clean up
320 | act(() => {
321 | jest.advanceTimersByTime(100);
322 | });
323 |
324 | // Click should trigger event
325 | fireEvent.click(container.querySelector('.rc-tree-switcher_open'));
326 | expect(onExpand).toHaveBeenCalled();
327 | });
328 | });
329 |
--------------------------------------------------------------------------------
/tests/TreeNodeProps.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import React from 'react';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import Tree, { TreeNode } from '../src';
5 | import { spyConsole } from './util';
6 |
7 | /**
8 | * For refactor purpose. All the props should be passed by test
9 | */
10 |
11 | describe('TreeNode Props', () => {
12 | spyConsole();
13 |
14 | // prefixCls - is defined by Tree, TreeNode can not change it
15 | // expanded - is defined by Tree, TreeNode can not change it
16 |
17 | it('className', () => {
18 | const { container } = render(
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | ,
28 | );
29 | expect(container.firstChild).toMatchSnapshot();
30 | });
31 |
32 | // disabled - is already full test in Tree.spec.js
33 | // disableCheckbox - is already full test in Tree.spec.js
34 | // title - is already full test in Tree.spec.js
35 | // key - is already full test in Tree.spec.js
36 | it('isLeaf', () => {
37 | const withoutLoadData = render(
38 |
39 |
40 |
41 |
42 | ,
43 | );
44 | expect(withoutLoadData.container.firstChild).toMatchSnapshot();
45 |
46 | const withLoadData = render(
47 |
48 |
49 |
50 |
51 | ,
52 | );
53 | expect(withLoadData.container.firstChild).toMatchSnapshot();
54 |
55 | const forceNoLeaf = render(
56 |
57 |
58 |
59 |
60 | ,
61 | );
62 | expect(forceNoLeaf.container.firstChild).toMatchSnapshot();
63 | });
64 |
65 | it('title function', () => {
66 | const { container } = render(
67 |
68 | title} />
69 | ,
70 | );
71 | expect(container.querySelector('#test-title').textContent).toEqual('title');
72 | });
73 |
74 | describe('customize icon', () => {
75 | it('element', () => {
76 | const withoutLoadData = render(
77 |
78 | } />
79 | ,
80 | );
81 | expect(withoutLoadData.container.firstChild).toMatchSnapshot();
82 | });
83 |
84 | it('component', () => {
85 | const Icon = () => ;
86 |
87 | const withoutLoadData = render(
88 |
89 |
90 | ,
91 | );
92 | expect(withoutLoadData.container.firstChild).toMatchSnapshot();
93 | });
94 |
95 | it('hide icon', () => {
96 | const withoutLoadData = render(
97 |
98 | } />
99 | ,
100 | );
101 | expect(withoutLoadData.container.firstChild).toMatchSnapshot();
102 | });
103 |
104 | it('get props when loading', () => {
105 | const then = jest.fn(() => Promise.resolve());
106 | const loadData: any = jest.fn(() => ({ then }));
107 | const iconFn: any = jest.fn(() => null);
108 | const { container } = render(
109 |
110 |
111 | ,
112 | );
113 |
114 | fireEvent.click(container.querySelector('.rc-tree-switcher'));
115 |
116 | expect(iconFn.mock.calls[0][0].loading).toBe(false);
117 | expect(iconFn.mock.calls[iconFn.mock.calls.length - 1][0].loading).toBeTruthy();
118 | });
119 | });
120 |
121 | describe('data and aria props', () => {
122 | it('renders data attributes on li', () => {
123 | const wrapper = render(
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | ,
133 | );
134 | expect(wrapper.container.firstChild).toMatchSnapshot();
135 | });
136 |
137 | it('renders aria attributes on li', () => {
138 | const wrapper = render(
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 | ,
148 | );
149 | expect(wrapper.container.firstChild).toMatchSnapshot();
150 | });
151 | });
152 |
153 | it('selectable', () => {
154 | const onClick = jest.fn();
155 | const onSelect = jest.fn();
156 |
157 | const { container } = render(
158 |
159 |
160 | ,
161 | );
162 |
163 | fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
164 |
165 | expect(onClick).toHaveBeenCalled();
166 | expect(onSelect).not.toHaveBeenCalled();
167 | });
168 |
169 | it('unselectable', () => {
170 | const onClick = jest.fn();
171 | const onSelect = jest.fn();
172 |
173 | class Demo extends React.Component {
174 | state = {
175 | treeSelectable: false,
176 | treeNodeSelectable: false,
177 | };
178 |
179 | render() {
180 | return (
181 |
182 |
183 |
184 |
185 |
186 |
196 | );
197 | }
198 | }
199 |
200 | const { container } = render();
201 | // tree selectable is false ,then children should be selectable = false if not set selectable alone.
202 | expect(container.querySelectorAll('[aria-selected=false]')).toHaveLength(1);
203 |
204 | fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
205 | expect(onClick).toHaveBeenCalled();
206 | expect(onSelect).not.toHaveBeenCalled();
207 |
208 | // only set tree node use state.
209 | fireEvent.click(container.querySelector('.test-button'));
210 | onClick.mockRestore();
211 | onSelect.mockRestore();
212 | expect(container.querySelectorAll('[aria-selected=false]')).toHaveLength(1);
213 | fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
214 | expect(onClick).toHaveBeenCalled();
215 | expect(onSelect).not.toHaveBeenCalled();
216 | });
217 |
218 | it('elements within the title should be able to respond to clicks', () => {
219 | const onClick = jest.fn();
220 |
221 | const Demo: React.FC<{ filed: string; checkable: boolean }> = ({ filed, checkable }) => (
222 |
223 |
227 |
228 |
229 | }
230 | />
231 |
232 | );
233 |
234 | render();
235 | render();
236 |
237 | fireEvent.click(document.querySelector('.test-check'));
238 | fireEvent.click(document.querySelector('.test-click'));
239 |
240 | expect(onClick).toHaveBeenCalledTimes(2);
241 | });
242 | });
243 |
--------------------------------------------------------------------------------
/tests/TreeVirtual.spec.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {render} from '@testing-library/react';
3 | import Tree from '../src';
4 |
5 | jest.mock('rc-virtual-list', () => jest.requireActual('rc-virtual-list'));
6 |
7 | describe('Tree Virtual', () => {
8 | it('should display all nodes when for nonvirtual tree', () => {
9 | const data = [];
10 | for (let i = 0; i < 99; i += 1) {
11 | data.push({
12 | key: i,
13 | title: i,
14 | });
15 | }
16 |
17 | const { container } = render(
18 | ,
19 | );
20 |
21 | expect(
22 | container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
23 | ).toHaveLength(99);
24 | });
25 |
26 | it('should support scrollWidth parameter and show horizontal scroll', () => {
27 | const data = [{key: 1, title: 'title'}];
28 | const { container } = render(
29 | ,
30 | );
31 |
32 | expect(container.querySelector('.rc-tree-list-scrollbar-horizontal')).toBeInTheDocument();
33 |
34 | jest.useRealTimers();
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/tests/__mocks__/rc-util/lib/raf.ts:
--------------------------------------------------------------------------------
1 | function raf(callback: Function) {
2 | return setTimeout(callback);
3 | }
4 |
5 | raf.cancel = (id: number) => {
6 | clearTimeout(id);
7 | };
8 |
9 | export default raf;
10 |
--------------------------------------------------------------------------------
/tests/__mocks__/rc-virtual-list.tsx:
--------------------------------------------------------------------------------
1 | import List from 'rc-virtual-list/lib/mock';
2 |
3 | export default List;
4 |
--------------------------------------------------------------------------------
/tests/expandAction.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, react/no-multi-comp */
2 | import React from 'react';
3 | import { render, fireEvent } from '@testing-library/react';
4 | import Tree, { TreeNode } from '../src';
5 | import { spyConsole } from './util';
6 |
7 | describe('ExpandAction', () => {
8 | spyConsole();
9 |
10 | it('title expandable when selectable is false and expandAction is "click"', () => {
11 | const onClick = jest.fn();
12 | const onSelect = jest.fn();
13 | const onExpand = jest.fn();
14 |
15 | const { container } = render(
16 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | ,
34 | );
35 |
36 | // test trigger expand when click title
37 | fireEvent.click(container.querySelector('[title="leaf 1"]'));
38 |
39 | expect(onClick).toHaveBeenCalled();
40 | expect(onSelect).not.toHaveBeenCalled();
41 | expect(onExpand).toHaveBeenCalledWith(['0-0', '0-0-0'], {
42 | expanded: true,
43 | node: expect.anything(),
44 | nativeEvent: expect.anything(),
45 | });
46 |
47 | onClick.mockReset();
48 | onSelect.mockReset();
49 | onExpand.mockReset();
50 |
51 | // test trigger un-expand when click title again
52 | fireEvent.click(container.querySelector('[title="leaf 1"]'));
53 |
54 | expect(onClick).toHaveBeenCalled();
55 | expect(onSelect).not.toHaveBeenCalled();
56 | expect(onExpand).toHaveBeenCalledWith(['0-0'], {
57 | expanded: false,
58 | node: expect.anything(),
59 | nativeEvent: expect.anything(),
60 | });
61 | });
62 |
63 | it('title expandable when selectable is false and expandAction is "doubleClick"', () => {
64 | const onDoubleClick = jest.fn();
65 | const onSelect = jest.fn();
66 | const onExpand = jest.fn();
67 |
68 | const { container } = render(
69 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | ,
87 | );
88 |
89 | // test trigger expand when double click title
90 | fireEvent.doubleClick(container.querySelector('[title="leaf 1"]'));
91 |
92 | expect(onDoubleClick).toHaveBeenCalled();
93 | expect(onSelect).not.toHaveBeenCalled();
94 | expect(onExpand).toHaveBeenCalledWith(['0-0', '0-0-0'], {
95 | expanded: true,
96 | node: expect.anything(),
97 | nativeEvent: expect.anything(),
98 | });
99 |
100 | onDoubleClick.mockReset();
101 | onSelect.mockReset();
102 | onExpand.mockReset();
103 |
104 | // test trigger un-expand when double click title again
105 | fireEvent.doubleClick(container.querySelector('[title="leaf 1"]'));
106 |
107 | expect(onDoubleClick).toHaveBeenCalled();
108 | expect(onSelect).not.toHaveBeenCalled();
109 | expect(onExpand).toHaveBeenCalledWith(['0-0'], {
110 | expanded: false,
111 | node: expect.anything(),
112 | nativeEvent: expect.anything(),
113 | });
114 | });
115 |
116 | it('title un-expandable when selectable is false and expandAction is false', () => {
117 | const onClick = jest.fn();
118 | const onSelect = jest.fn();
119 | const onExpand = jest.fn();
120 |
121 | const { container } = render(
122 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | ,
140 | );
141 |
142 | // test won't trigger expand when click title if expandAction is false
143 | fireEvent.click(container.querySelector('[title="leaf 2"]'));
144 |
145 | expect(onClick).toHaveBeenCalled();
146 | expect(onSelect).not.toHaveBeenCalled();
147 | expect(onExpand).not.toHaveBeenCalled();
148 | });
149 |
150 | it('not trigger expand when ctrl pressed', () => {
151 | const onClick = jest.fn();
152 | const onSelect = jest.fn();
153 | const onExpand = jest.fn();
154 |
155 | const { container } = render(
156 |
163 |
164 |
165 |
166 | ,
167 | );
168 |
169 | fireEvent.click(container.querySelector('[title="parent 1"]'), {
170 | ctrlKey: true,
171 | });
172 |
173 | expect(onClick).toHaveBeenCalled();
174 | expect(onSelect).toHaveBeenCalled();
175 | expect(onExpand).not.toHaveBeenCalled();
176 | });
177 | });
178 |
--------------------------------------------------------------------------------
/tests/setupFilesAfterEnv.js:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom';
2 |
--------------------------------------------------------------------------------
/tests/type.spec.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef, react/no-multi-comp */
2 | import React from 'react';
3 | import { render } from '@testing-library/react';
4 | import Tree, { BasicDataNode } from '../src';
5 |
6 | describe('Tree.TypeScript', () => {
7 | it('fieldNames', () => {
8 | interface DataType extends BasicDataNode {
9 | label: string;
10 | value: string;
11 | list?: DataType[];
12 | }
13 |
14 | render(
15 |
16 | treeData={[
17 | {
18 | label: 'parent',
19 | value: 'parent',
20 | list: [],
21 | },
22 | ]}
23 | onSelect={(selectedKeys, info) => {
24 | console.log('info', info.node.isLeaf);
25 | }}
26 | />,
27 | );
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/tests/util.ts:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | export function objectMatcher(item: object) {
4 | const result = Array.isArray(item) ? [] : {};
5 |
6 | Object.keys(item).forEach(key => {
7 | const value = item[key];
8 | if (value && typeof value === 'object' && !(value instanceof Component)) {
9 | result[key] = objectMatcher(value);
10 | } else {
11 | result[key] = value;
12 | }
13 | });
14 |
15 | return expect.objectContaining(result);
16 | }
17 |
18 | export function spyConsole() {
19 | const errorList = [
20 | 'Warning: Tree node must have a certain key:',
21 | 'Warning: `children` of Tree is deprecated. Please use `treeData` instead.',
22 | ];
23 |
24 | // eslint-disable-next-line no-console
25 | const originConsoleErr = console.error;
26 |
27 | beforeAll(() => {
28 | // eslint-disable-next-line no-console
29 | console.error = jest.fn().mockImplementation((...args) => {
30 | if (errorList.some(tmpl => args[0].includes(tmpl))) {
31 | return;
32 | }
33 |
34 | originConsoleErr(...args);
35 | });
36 | });
37 |
38 | afterAll(() => {
39 | // eslint-disable-next-line no-console
40 | console.error = originConsoleErr;
41 | });
42 | }
43 |
44 | export function spyError() {
45 | let errorSpy;
46 |
47 | beforeAll(() => {
48 | errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
49 | });
50 |
51 | beforeEach(() => {
52 | errorSpy.mockReset();
53 | });
54 |
55 | afterAll(() => {
56 | errorSpy.mockRestore();
57 | });
58 |
59 | return () => errorSpy;
60 | }
61 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "moduleResolution": "node",
5 | "baseUrl": "./",
6 | "jsx": "react",
7 | "declaration": true,
8 | "skipLibCheck": true,
9 | "esModuleInterop": true,
10 | "paths": { "@/*": ["src/*"], "@@/*": ["src/.umi/*"], "@rc-component/tree": ["src/index.ts"] }
11 | },
12 | "include": [
13 | ".dumirc.ts",
14 | "./src/**/*.ts",
15 | "./src/**/*.tsx",
16 | "./docs/**/*.tsx",
17 | "./tests/**/*.tsx"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.css';
2 | declare module '*.less';
3 |
--------------------------------------------------------------------------------
/update-example.js:
--------------------------------------------------------------------------------
1 | /*
2 | 用于 dumi 改造使用,
3 | 可用于将 examples 的文件批量修改为 demo 引入形式,
4 | 其他项目根据具体情况使用。
5 | */
6 |
7 | const fs = require('fs');
8 | const glob = require('glob');
9 |
10 | const paths = glob.sync('./docs/examples/*.jsx');
11 |
12 | paths.forEach(path => {
13 | const name = path.split('/').pop().split('.')[0];
14 | fs.writeFile(
15 | `./docs/demo/${name}.md`,
16 | `## ${name}
17 |
18 |
19 | `,
20 | 'utf8',
21 | function(error) {
22 | if(error){
23 | console.log(error);
24 | return false;
25 | }
26 | console.log(`${name} 更新成功~`);
27 | }
28 | )
29 | });
30 |
--------------------------------------------------------------------------------