diff --git a/.eslintrc.js b/.eslintrc.js
index b885a86dd..a491cd868 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -7,10 +7,13 @@ module.exports = {
'eslint-comments/disable-enable-pair': 0,
'react/require-default-props': 0,
'react/no-unused-prop-types': 1,
+ 'react-hooks/exhaustive-deps': 0,
'jsx-a11y/label-has-for': 0,
'jsx-a11y/label-has-associated-control': 0,
'no-loop-func': 0,
'@typescript-eslint/no-loop-func': 0,
+ '@typescript-eslint/consistent-type-imports': 0,
+ '@typescript-eslint/consistent-type-definitions': 0,
'max-classes-per-file': 0,
},
};
diff --git a/.fatherrc.js b/.fatherrc.js
index 912aa0aae..96268ae1e 100644
--- a/.fatherrc.js
+++ b/.fatherrc.js
@@ -1,9 +1,5 @@
-export default {
- cjs: 'babel',
- esm: { type: 'babel', importLibToEs: true },
- preCommit: {
- eslint: true,
- prettier: true,
- },
- runtimeHelpers: true,
-};
+import { defineConfig } from 'father';
+
+export default defineConfig({
+ plugins: ['@rc-component/father-plugin'],
+});
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..437d0bd3b
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,29 @@
+version: 2
+updates:
+- package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "21:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "@types/react"
+ versions:
+ - 17.0.0
+ - 17.0.1
+ - 17.0.2
+ - 17.0.3
+ - dependency-name: "@types/react-dom"
+ versions:
+ - 17.0.0
+ - 17.0.1
+ - 17.0.2
+ - dependency-name: '@rc-component/np'
+ versions:
+ - 1.0.0
+ - dependency-name: '@rc-component/tooltip'
+ versions:
+ - 1.0.0
+ - dependency-name: less
+ versions:
+ - 4.1.0
\ No newline at end of file
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 000000000..f860ff107
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,6 @@
+name: ✅ test
+on: [push, pull_request]
+jobs:
+ test:
+ uses: react-component/rc-test/.github/workflows/test.yml@main
+ secrets: inherit
diff --git a/.gitignore b/.gitignore
index 0e6334955..8aae36ae1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.storybook
+.vscode
*.iml
*.log
.doc/
@@ -31,4 +32,11 @@ lib
/coverage
yarn.lock
es/
-!tests/__mocks__/rc-util/lib
\ No newline at end of file
+!tests/__mocks__/rc-util/lib
+# umi
+.umi
+.umi-production
+.umi-test
+.env.local
+
+.dumi/
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 000000000..af5adff9d
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1 @@
+lint-staged
\ No newline at end of file
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 000000000..c2e0187bd
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+node-options="--openssl-legacy-provider"
\ No newline at end of file
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..214377229
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,11 @@
+.doc
+.storybook
+es
+lib
+**/*.svg
+**/*.ejs
+**/*.html
+package.json
+.umi
+.umi-production
+.umi-test
diff --git a/.prettierrc b/.prettierrc
index f307fb192..b04278194 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -4,5 +4,7 @@
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
- "printWidth": 100
+ "proseWrap": "never",
+ "printWidth": 100,
+ "arrowParens": "avoid"
}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index a04c90edb..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-language: node_js
-
-sudo: false
-
-notifications:
- email:
- - hualei5280@gmail.com
-
-node_js:
-- 14
-
-before_install:
-- |
- if ! git diff --name-only $TRAVIS_COMMIT_RANGE | grep -qvE '(\.md$)|(^(docs|examples))/'
- then
- echo "Only docs were updated, stopping build process."
- exit
- fi
-script:
-- |
- if [ "$TEST_TYPE" = test ]; then
- npm test -- --coverage && \
- bash <(curl -s https://codecov.io/bash)
- else
- npm run $TEST_TYPE
- fi
-env:
- matrix:
- - TEST_TYPE=lint
- - TEST_TYPE=test
\ No newline at end of file
diff --git a/.umirc.ts b/.umirc.ts
new file mode 100644
index 000000000..f734dd5c1
--- /dev/null
+++ b/.umirc.ts
@@ -0,0 +1,8 @@
+// more config: https://d.umijs.org/config
+import { defineConfig } from 'dumi';
+
+export default defineConfig({
+ themeConfig: {
+ name: 'Tree',
+ },
+});
diff --git a/HISTORY.md b/CHANGELOG.md
similarity index 96%
rename from HISTORY.md
rename to CHANGELOG.md
index 4004aede2..1b34e8905 100644
--- a/HISTORY.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
-# History
----
+# Changelog
+
+- https://github.com/react-component/tree/releases
## 3.2.0 `2020-05-08`
diff --git a/README.md b/README.md
index 5da49ae2b..d56dd7509 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,20 @@
# rc-tree
----
-
Tree component.
[![NPM version][npm-image]][npm-url]
-[![build status][travis-image]][travis-url]
-[![Test coverage][coveralls-image]][coveralls-url]
-[![Dependencies][david-image]][david-url]
-[![DevDependencies][david-dev-image]][david-dev-url]
[![npm download][download-image]][download-url]
+[![build status][github-actions-image]][github-actions-url]
+[![Codecov][codecov-image]][codecov-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
+[![dumi][dumi-image]][dumi-url]
[npm-image]: http://img.shields.io/npm/v/rc-tree.svg?style=flat-square
[npm-url]: http://npmjs.org/package/rc-tree
-[travis-image]: https://img.shields.io/travis/react-component/tree.svg?style=flat-square
-[travis-url]: https://travis-ci.org/react-component/tree
-[coveralls-image]: https://img.shields.io/coveralls/react-component/tree.svg?style=flat-square
-[coveralls-url]: https://coveralls.io/r/react-component/tree?branch=master
+[github-actions-image]: https://github.com/react-component/tree/actions/workflows/main.yml/badge.svg
+[github-actions-url]: https://github.com/react-component/tree/actions/workflows/main.yml
+[codecov-image]: https://img.shields.io/codecov/c/github/react-component/tree/master.svg?style=flat-square
+[codecov-url]: https://codecov.io/gh/react-component/tree/
[david-url]: https://david-dm.org/react-component/tree
[david-image]: https://david-dm.org/react-component/tree/status.svg?style=flat-square
[david-dev-url]: https://david-dm.org/react-component/tree?type=dev
@@ -26,6 +23,8 @@ Tree component.
[download-url]: https://npmjs.org/package/rc-tree
[bundlephobia-url]: https://bundlephobia.com/result?p=rc-tree
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/rc-tree
+[dumi-url]: https://github.com/umijs/dumi
+[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
## Screenshots
@@ -39,13 +38,14 @@ Tree component.
http://localhost:9001/
-online example: http://react-component.github.io/tree/
+online example: https://tree.react-component.now.sh/
-## install
+## Install
[](https://npmjs.org/package/rc-tree)
## Usage
+> Note: `import "rc-tree/assets/index.css"`
see examples
@@ -61,16 +61,16 @@ see examples
| checkStrictly | check node precisely, parent and children nodes are not associated | bool | false |
| className | additional css class of root dom node | String | '' |
| defaultCheckedKeys | default checked treeNodes | String[] | [] |
-| defaultExpandedKeys | expand specific treeNodes | String[] | - |
+| defaultExpandedKeys | expand specific treeNodes | String[] | [] |
| defaultExpandAll | expand all treeNodes | bool | false |
| defaultExpandParent | auto expand parent treeNodes when init | bool | true |
| defaultSelectedKeys | default selected treeNodes | String[] | [] |
| disabled | whether disabled the tree | bool | false |
-| 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 | false |
+| 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 |
| expandedKeys | Controlled expand specific treeNodes | String[] | - |
| filterTreeNode | filter some treeNodes as you need. it should return true | function(node) | - |
| icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - |
-| loadedKeys | Mark node is loaded when `loadData` is true | string[] | - |
+| loadedKeys | Mark node is loaded when `loadData` is true | String[] | - |
| loadData | load data asynchronously and the return value should be a promise | function(node) | - |
| multiple | whether multiple select | bool | false |
| prefixCls | prefix class | String | 'rc-tree' |
@@ -94,6 +94,10 @@ see examples
| onSelect | click the treeNode to fire | function(selectedKeys, e:{selected: bool, selectedNodes, node, event, nativeEvent}) | - |
| switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - |
| virtual | Disable virtual scroll when `false` | boolean | - |
+| allowDrop | Whether to allow drop on node | ({ dragNode, dropNode, dropPosition }) => boolean | - |
+| dropIndicatorRender | The indicator to render when dragging | ({ dropPosition, dropLevelOffset, indent: number, prefixCls }) => ReactNode| - |
+| direction | Display direction of the tree, it may affect dragging behavior | `ltr` \| `rtl` | - |
+| expandAction | Tree open logic, optional: false \| `click` \| `doubleClick` | string \| boolean | `click` |
### TreeNode props
@@ -104,7 +108,7 @@ see examples
| name | description | type | default |
| --- | --- | --- | --- |
| className | additional class to treeNode | String | '' |
-| checkable | control node checkable if Tree is checkable | bool | - |
+| checkable | control node checkable if Tree is checkable | bool | false |
| style | set style to treeNode | Object | '' |
| disabled | whether disabled the treeNode | bool | false |
| disableCheckbox | whether disable the treeNode' checkbox | bool | false |
@@ -114,7 +118,7 @@ see examples
| icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - |
| switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - |
-## note
+## Note
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'}`
@@ -137,9 +141,9 @@ http://localhost:8018/node_modules/rc-server/node_modules/node-jscover/lib/front
rc-tree is released under the MIT license.
-## other tree view
+## Other tree views
-- [ztree](http://www.ztree.me/)
-- [jqTree](http://mbraak.github.io/jqTree/)
-- [jquery.treeselect](http://travistidwell.com/jquery.treeselect.js/)
-- [angular Select Tree](http://a5hik.github.io/angular-multi-select-tree/)
+- [zTree](http://www.treejs.cn/)
+- [jqTree](https://mbraak.github.io/jqTree/)
+- [jquery.treeselect](https://travistidwell.com/jquery.treeselect.js/)
+- [Angular Multi Select Tree](https://a5hik.github.io/angular-multi-select-tree/)
diff --git a/assets/index.less b/assets/index.less
index 808901d1d..b449d4b95 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -13,7 +13,7 @@
.@{treeNodePrefixCls} {
margin: 0;
padding: 0;
- line-height: 20px;
+ line-height: 24px;
white-space: nowrap;
list-style: none;
outline: 0;
@@ -24,25 +24,32 @@
-webkit-user-select: none;
user-select: none;
/* Required to make elements draggable in old WebKit */
- -khtml-user-drag: element;
- -webkit-user-drag: element;
+ // -khtml-user-drag: element;
+ // -webkit-user-drag: element;
}
- &.drag-over {
- > .draggable {
- color: white;
- background-color: #316ac5;
- border: 1px #316ac5 solid;
- opacity: 0.8;
- }
+
+ &.dragging {
+ background: rgba(100, 100, 255, 0.1);
}
- &.drag-over-gap-top {
- > .draggable {
- border-top: 2px blue solid;
+
+ &.drop-container {
+ > .draggable::after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ box-shadow: inset 0 0 0 2px red;
+ content: '';
+ }
+ & ~ .@{treeNodePrefixCls} {
+ border-left: 2px solid chocolate;
}
}
- &.drag-over-gap-bottom {
- > .draggable {
- border-bottom: 2px blue solid;
+ &.drop-target {
+ background-color: yellowgreen;
+ & ~ .@{treeNodePrefixCls} {
+ border-left: none;
}
}
&.filter-node {
@@ -56,10 +63,11 @@
padding: 0 0 0 18px;
}
.@{treePrefixCls}-node-content-wrapper {
+ position: relative;
display: inline-block;
- height: 17px;
+ height: 24px;
margin: 0;
- padding: 1px 3px 0 0;
+ padding: 0;
text-decoration: none;
vertical-align: top;
cursor: pointer;
@@ -73,7 +81,7 @@
height: 16px;
margin-right: 2px;
line-height: 16px;
- vertical-align: middle;
+ vertical-align: -0.125em;
background-color: transparent;
background-image: url('data:image/png;base64,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');
background-repeat: no-repeat;
@@ -187,7 +195,7 @@
}
&-node-selected {
background-color: #ffe6b0;
- border: 1px #ffb951 solid;
+ box-shadow: 0 0 0 1px #ffb951;
opacity: 0.8;
}
&-icon__open {
@@ -209,9 +217,22 @@
margin-right: 2px;
vertical-align: top;
}
+ &-title {
+ display: inline-block;
+ }
+ &-indent {
+ display: inline-block;
+ height: 0;
+ vertical-align: bottom;
+ }
&-indent-unit {
- width: 16px;
display: inline-block;
- padding-left: 18px;
+ width: 16px;
+ }
+
+ &-draggable-icon {
+ display: inline-flex;
+ justify-content: center;
+ width: 16px;
}
}
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 000000000..beb2eddcc
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1 @@
+
diff --git a/docs/demo/animation-draggable.md b/docs/demo/animation-draggable.md
new file mode 100644
index 000000000..cd51870e3
--- /dev/null
+++ b/docs/demo/animation-draggable.md
@@ -0,0 +1,8 @@
+---
+title: Animation Draggable
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/animation.md b/docs/demo/animation.md
new file mode 100644
index 000000000..431fe072f
--- /dev/null
+++ b/docs/demo/animation.md
@@ -0,0 +1,8 @@
+---
+title: Animation
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/basic-controlled.md b/docs/demo/basic-controlled.md
new file mode 100644
index 000000000..9ef9a3a05
--- /dev/null
+++ b/docs/demo/basic-controlled.md
@@ -0,0 +1,8 @@
+---
+title: Basic Controlled
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/basic.md b/docs/demo/basic.md
new file mode 100644
index 000000000..2bfed0cc5
--- /dev/null
+++ b/docs/demo/basic.md
@@ -0,0 +1,8 @@
+---
+title: Basic
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/big-data.md b/docs/demo/big-data.md
new file mode 100644
index 000000000..695474e64
--- /dev/null
+++ b/docs/demo/big-data.md
@@ -0,0 +1,8 @@
+---
+title: Big Data
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/contextmenu.md b/docs/demo/contextmenu.md
new file mode 100644
index 000000000..b088fcebf
--- /dev/null
+++ b/docs/demo/contextmenu.md
@@ -0,0 +1,8 @@
+---
+title: Context Menu
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/custom-switch-icon.md b/docs/demo/custom-switch-icon.md
new file mode 100644
index 000000000..81644e7ea
--- /dev/null
+++ b/docs/demo/custom-switch-icon.md
@@ -0,0 +1,8 @@
+---
+title: Custom Switch Icon
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/draggable-allow-drop.md b/docs/demo/draggable-allow-drop.md
new file mode 100644
index 000000000..2cbe21e44
--- /dev/null
+++ b/docs/demo/draggable-allow-drop.md
@@ -0,0 +1,8 @@
+---
+title: Draggable Allow Drop
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/draggable.md b/docs/demo/draggable.md
new file mode 100644
index 000000000..5f167c755
--- /dev/null
+++ b/docs/demo/draggable.md
@@ -0,0 +1,8 @@
+---
+title: Draggable
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/dropdown.md b/docs/demo/dropdown.md
new file mode 100644
index 000000000..1ee32c191
--- /dev/null
+++ b/docs/demo/dropdown.md
@@ -0,0 +1,8 @@
+---
+title: Dropdown
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/dynamic.md b/docs/demo/dynamic.md
new file mode 100644
index 000000000..8470bbfe6
--- /dev/null
+++ b/docs/demo/dynamic.md
@@ -0,0 +1,8 @@
+---
+title: Dynamic
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/expandAction.md b/docs/demo/expandAction.md
new file mode 100644
index 000000000..902c8714a
--- /dev/null
+++ b/docs/demo/expandAction.md
@@ -0,0 +1,8 @@
+---
+title: Expand Action
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/fieldNames.md b/docs/demo/fieldNames.md
new file mode 100644
index 000000000..05f5f59c9
--- /dev/null
+++ b/docs/demo/fieldNames.md
@@ -0,0 +1,8 @@
+---
+title: Field Names
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/funtionTitle.md b/docs/demo/funtionTitle.md
new file mode 100644
index 000000000..385d6b7c7
--- /dev/null
+++ b/docs/demo/funtionTitle.md
@@ -0,0 +1,8 @@
+---
+title: Function Title
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/icon.md b/docs/demo/icon.md
new file mode 100644
index 000000000..176aad6a9
--- /dev/null
+++ b/docs/demo/icon.md
@@ -0,0 +1,8 @@
+---
+title: Icon
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/docs/demo/selectable.md b/docs/demo/selectable.md
new file mode 100644
index 000000000..ad30c00e6
--- /dev/null
+++ b/docs/demo/selectable.md
@@ -0,0 +1,8 @@
+---
+title: Selectable
+nav:
+ title: Demo
+ path: /demo
+---
+
+
diff --git a/examples/animation-draggable.jsx b/docs/examples/animation-draggable.jsx
similarity index 98%
rename from examples/animation-draggable.jsx
rename to docs/examples/animation-draggable.jsx
index ae5dcc75d..cb2985548 100644
--- a/examples/animation-draggable.jsx
+++ b/docs/examples/animation-draggable.jsx
@@ -2,8 +2,8 @@
react/no-danger, no-param-reassign */
import React from 'react';
import { gData } from './utils/dataUtil';
-import '../assets/index.less';
-import Tree from '../src';
+import '../../assets/index.less';
+import Tree from '@rc-component/tree';
const STYLE = `
.rc-tree-child-tree {
diff --git a/examples/animation.jsx b/docs/examples/animation.jsx
similarity index 67%
rename from examples/animation.jsx
rename to docs/examples/animation.jsx
index d46cea011..7dcb5c3f1 100644
--- a/examples/animation.jsx
+++ b/docs/examples/animation.jsx
@@ -1,8 +1,9 @@
/* eslint no-console:0, react/no-danger: 0 */
-import '../assets/index.less';
-import './animation.less';
+import { Provider } from '@rc-component/motion';
+import Tree from '@rc-component/tree';
import React from 'react';
-import Tree from '../src';
+import '../../assets/index.less';
+import './animation.less';
const STYLE = `
.rc-tree-child-tree {
@@ -108,43 +109,56 @@ function getTreeData() {
const Demo = () => {
const treeRef = React.useRef();
+ const [enableMotion, setEnableMotion] = React.useState(true);
setTimeout(() => {
treeRef.current.scrollTo({ key: '0-9-2' });
}, 100);
return (
-
-
expanded
-
+
+
-
-
-
With Virtual
-
-
-
-
Without Virtual
-
+
+
+
expanded
+
+
+
+
+
With Virtual
+
+
+
+
Without Virtual
+
+
+
-
-
+
+
);
};
diff --git a/examples/animation.less b/docs/examples/animation.less
similarity index 100%
rename from examples/animation.less
rename to docs/examples/animation.less
diff --git a/examples/basic-controlled.jsx b/docs/examples/basic-controlled.jsx
similarity index 96%
rename from examples/basic-controlled.jsx
rename to docs/examples/basic-controlled.jsx
index 18c3a64ec..8a75cb76c 100644
--- a/examples/basic-controlled.jsx
+++ b/docs/examples/basic-controlled.jsx
@@ -1,9 +1,9 @@
/* eslint-disable no-console, react/no-unescaped-entities */
-import '../assets/index.less';
+import '../../assets/index.less';
import React from 'react';
-import 'rc-dialog/assets/index.css';
-import Modal from 'rc-dialog';
-import Tree, { TreeNode } from '../src';
+import '@rc-component/dialog/assets/index.css';
+import Modal from '@rc-component/dialog';
+import Tree, { TreeNode } from '@rc-component/tree';
import { gData, getRadioSelectKeys } from './utils/dataUtil';
class Demo extends React.Component {
diff --git a/examples/basic.jsx b/docs/examples/basic.jsx
similarity index 96%
rename from examples/basic.jsx
rename to docs/examples/basic.jsx
index b6dbe8594..55e96fc5b 100644
--- a/examples/basic.jsx
+++ b/docs/examples/basic.jsx
@@ -1,9 +1,8 @@
-/* eslint-disable jsx-a11y/no-noninteractive-element-interactions,
-no-alert, no-console, react/no-find-dom-node */
+/* eslint-disable no-alert, no-console, react/no-find-dom-node */
import React from 'react';
-import '../assets/index.less';
+import '../../assets/index.less';
import './basic.less';
-import Tree, { TreeNode } from '../src';
+import Tree, { TreeNode } from '@rc-component/tree';
const treeData = [
{
diff --git a/examples/basic.less b/docs/examples/basic.less
similarity index 100%
rename from examples/basic.less
rename to docs/examples/basic.less
diff --git a/examples/big-data-generator.js b/docs/examples/big-data-generator.js
similarity index 100%
rename from examples/big-data-generator.js
rename to docs/examples/big-data-generator.js
diff --git a/examples/big-data.jsx b/docs/examples/big-data.jsx
similarity index 97%
rename from examples/big-data.jsx
rename to docs/examples/big-data.jsx
index e5063e728..64c67fecb 100644
--- a/examples/big-data.jsx
+++ b/docs/examples/big-data.jsx
@@ -1,8 +1,8 @@
/* eslint-disable no-console, prefer-destructuring */
import React from 'react';
import Gen from './big-data-generator';
-import '../assets/index.less';
-import Tree, { TreeNode } from '../src';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
class Demo extends React.Component {
state = {
diff --git a/examples/contextmenu.jsx b/docs/examples/contextmenu.jsx
similarity index 96%
rename from examples/contextmenu.jsx
rename to docs/examples/contextmenu.jsx
index 6932435a9..06507e0ef 100644
--- a/examples/contextmenu.jsx
+++ b/docs/examples/contextmenu.jsx
@@ -1,10 +1,10 @@
/* eslint-disable no-console, react/no-find-dom-node */
import React from 'react';
import ReactDOM from 'react-dom';
-import Tooltip from 'rc-tooltip';
+import Tooltip from '@rc-component/tooltip';
import './contextmenu.less';
-import '../assets/index.less';
-import Tree, { TreeNode } from '../src';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
function contains(root, n) {
let node = n;
diff --git a/examples/contextmenu.less b/docs/examples/contextmenu.less
similarity index 100%
rename from examples/contextmenu.less
rename to docs/examples/contextmenu.less
diff --git a/examples/custom-switch-icon.jsx b/docs/examples/custom-switch-icon.jsx
similarity index 94%
rename from examples/custom-switch-icon.jsx
rename to docs/examples/custom-switch-icon.jsx
index 1af9e1ac7..6d5ed4d44 100644
--- a/examples/custom-switch-icon.jsx
+++ b/docs/examples/custom-switch-icon.jsx
@@ -1,9 +1,8 @@
/* eslint no-console:0 */
/* eslint no-alert:0 */
-/* eslint jsx-a11y/no-noninteractive-element-interactions:0 */
import React from 'react';
-import '../assets/index.less';
-import Tree, { TreeNode } from '../src';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
const arrowPath =
'M869 487.8L491.2 159.9c-2.9-2.5-6.6-3.9-10.5-3.9h-88' +
@@ -42,6 +41,9 @@ class Demo extends React.Component {
render() {
const switcherIcon = obj => {
+ if (obj.data.key?.startsWith('0-0-3')) {
+ return false;
+ }
if (obj.isLeaf) {
return getSvgIcon(
arrowPath,
diff --git a/examples/draggable.jsx b/docs/examples/draggable-allow-drop.jsx
similarity index 61%
rename from examples/draggable.jsx
rename to docs/examples/draggable-allow-drop.jsx
index b933d408a..8c661ae3a 100644
--- a/examples/draggable.jsx
+++ b/docs/examples/draggable-allow-drop.jsx
@@ -2,8 +2,15 @@
import React from 'react';
import { gData } from './utils/dataUtil';
import './draggable.less';
-import '../assets/index.less';
-import Tree, { TreeNode } from '../src';
+import '../../assets/index.less';
+import Tree from '@rc-component/tree';
+
+function allowDrop({ dropNode, dropPosition }) {
+ if (!dropNode.children) {
+ if (dropPosition === 0) return false;
+ }
+ return true;
+}
class Demo extends React.Component {
state = {
@@ -16,18 +23,15 @@ class Demo extends React.Component {
console.log('start', info);
};
- onDragEnter = info => {
- console.log('enter', info);
- this.setState({
- expandedKeys: info.expandedKeys,
- });
+ onDragEnter = () => {
+ console.log('enter');
};
onDrop = info => {
console.log('drop', info);
- const dropKey = info.node.props.eventKey;
- const dragKey = info.dragNode.props.eventKey;
- const dropPos = info.node.props.pos.split('-');
+ const dropKey = info.node.key;
+ const dragKey = info.dragNode.key;
+ const dropPos = info.node.pos.split('-');
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
const loop = (data, key, callback) => {
@@ -50,19 +54,8 @@ class Demo extends React.Component {
dragObj = item;
});
- if (!info.dropToGap) {
+ if (dropPosition === 0) {
// Drop on the content
- loop(data, dropKey, item => {
- // eslint-disable-next-line no-param-reassign
- item.children = item.children || [];
- // where to insert 示例添加到尾部,可以是随意位置
- item.children.push(dragObj);
- });
- } else if (
- (info.node.props.children || []).length > 0 && // Has children
- info.node.props.expanded && // Is expanded
- dropPosition === 1 // On the bottom gap
- ) {
loop(data, dropKey, item => {
// eslint-disable-next-line no-param-reassign
item.children = item.children || [];
@@ -70,7 +63,7 @@ class Demo extends React.Component {
item.children.unshift(dragObj);
});
} else {
- // Drop on the gap
+ // Drop on the gap (insert before or insert after)
let ar;
let i;
loop(data, dropKey, (item, index, arr) => {
@@ -98,33 +91,21 @@ class Demo extends React.Component {
};
render() {
- const loop = data =>
- data.map(item => {
- if (item.children && item.children.length) {
- return (
-
- {loop(item.children)}
-
- );
- }
- return
;
- });
return (
-
draggable
-
drag a node into another node
+
draggable with allow drop
+
node can not be dropped inside a leaf node
- {loop(this.state.gData)}
-
+ treeData={this.state.gData}
+ />
);
diff --git a/docs/examples/draggable.jsx b/docs/examples/draggable.jsx
new file mode 100644
index 000000000..965421eaa
--- /dev/null
+++ b/docs/examples/draggable.jsx
@@ -0,0 +1,132 @@
+/* eslint-disable no-console, react/no-access-state-in-setstate */
+import React from 'react';
+import '../../assets/index.less';
+import Tree from '../../src';
+import './draggable.less';
+import { generateData } from './utils/dataUtil';
+
+const gData = generateData(2, 2, 2);
+
+class Demo extends React.Component {
+ state = {
+ gData,
+ autoExpandParent: true,
+ expandedKeys: [
+ '0-0-key',
+ '0-0-0-key',
+ '0-0-0-0-key',
+ '0-0-0-1-key',
+ '0-0-1-key',
+ '0-0-1-0-key',
+ '0-0-1-1-key',
+ '0-1-key',
+ '0-1-0-key',
+ '0-1-0-0-key',
+ '0-1-0-1-key',
+ '0-1-1-key',
+ '0-1-1-0-key',
+ '0-1-1-1-key',
+ ],
+ };
+
+ onDragStart = info => {
+ console.log('start', info);
+ };
+
+ onDragEnter = () => {
+ console.log('enter');
+ };
+
+ onDrop = info => {
+ console.log('drop', info);
+ const dropKey = info.node.key;
+ const dragKey = info.dragNode.key;
+ const dropPos = info.node.pos.split('-');
+ const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
+
+ const loop = (data, key, callback) => {
+ data.forEach((item, index, arr) => {
+ if (item.key === key) {
+ callback(item, index, arr);
+ return;
+ }
+ if (item.children) {
+ loop(item.children, key, callback);
+ }
+ });
+ };
+ const data = [...this.state.gData];
+
+ // Find dragObject
+ let dragObj;
+ loop(data, dragKey, (item, index, arr) => {
+ arr.splice(index, 1);
+ dragObj = item;
+ });
+
+ if (dropPosition === 0) {
+ // Drop on the content
+ loop(data, dropKey, item => {
+ // eslint-disable-next-line no-param-reassign
+ item.children = item.children || [];
+ // where to insert 示例添加到尾部,可以是随意位置
+ item.children.unshift(dragObj);
+ });
+ } else {
+ // Drop on the gap (insert before or insert after)
+ let ar;
+ let i;
+ loop(data, dropKey, (item, index, arr) => {
+ ar = arr;
+ i = index;
+ });
+ if (dropPosition === -1) {
+ ar.splice(i, 0, dragObj);
+ } else {
+ ar.splice(i + 1, 0, dragObj);
+ }
+ }
+
+ this.setState({
+ gData: data,
+ });
+ };
+
+ onExpand = expandedKeys => {
+ console.log('onExpand', expandedKeys);
+ this.setState({
+ expandedKeys,
+ autoExpandParent: false,
+ });
+ };
+
+ render() {
+ return (
+
+
draggable
+
drag a node into another node
+
+
+
This element is draggable, but it cannot be dragged into tree.
+
+
+ );
+ }
+}
+
+export default Demo;
diff --git a/examples/draggable.less b/docs/examples/draggable.less
similarity index 100%
rename from examples/draggable.less
rename to docs/examples/draggable.less
diff --git a/examples/dropdown.jsx b/docs/examples/dropdown.jsx
similarity index 97%
rename from examples/dropdown.jsx
rename to docs/examples/dropdown.jsx
index 4346a34fa..e7ab29d5b 100644
--- a/examples/dropdown.jsx
+++ b/docs/examples/dropdown.jsx
@@ -2,11 +2,11 @@
/* eslint no-console:0 */
/* eslint react/no-string-refs:0 */
import React from 'react';
-import Trigger from 'rc-trigger';
+import Trigger from '@rc-component/trigger';
import { gData } from './utils/dataUtil';
import './dropdown.less';
-import '../assets/index.less';
-import Tree, { TreeNode } from '../src';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
const placements = {
topLeft: {
diff --git a/examples/dropdown.less b/docs/examples/dropdown.less
similarity index 100%
rename from examples/dropdown.less
rename to docs/examples/dropdown.less
diff --git a/examples/dynamic.jsx b/docs/examples/dynamic.jsx
similarity index 97%
rename from examples/dynamic.jsx
rename to docs/examples/dynamic.jsx
index 269574098..35c8f5f42 100644
--- a/examples/dynamic.jsx
+++ b/docs/examples/dynamic.jsx
@@ -1,7 +1,7 @@
/* eslint-disable no-console, react/no-access-state-in-setstate */
-import '../assets/index.less';
+import '../../assets/index.less';
import React from 'react';
-import Tree from '../src';
+import Tree from '@rc-component/tree';
function generateTreeNodes(treeNode) {
const arr = [];
diff --git a/docs/examples/expandAction.jsx b/docs/examples/expandAction.jsx
new file mode 100644
index 000000000..a735a4739
--- /dev/null
+++ b/docs/examples/expandAction.jsx
@@ -0,0 +1,23 @@
+/* eslint-disable no-console, react/no-access-state-in-setstate */
+import React from 'react';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
+
+const Demo = () => (
+
+
expandAction
+
normal
+
+
+
+
+
+
+
+
+);
+
+export default Demo;
diff --git a/docs/examples/expandAction.less b/docs/examples/expandAction.less
new file mode 100644
index 000000000..b201eb716
--- /dev/null
+++ b/docs/examples/expandAction.less
@@ -0,0 +1,4 @@
+@import '../../assets/index.less';
+.expandAction-demo {
+ padding: 0 20px;
+}
diff --git a/docs/examples/fieldNames.tsx b/docs/examples/fieldNames.tsx
new file mode 100644
index 000000000..70b350fee
--- /dev/null
+++ b/docs/examples/fieldNames.tsx
@@ -0,0 +1,70 @@
+/* eslint-disable no-alert, no-console, react/no-find-dom-node */
+import React from 'react';
+import '../../assets/index.less';
+import './basic.less';
+import Tree from '@rc-component/tree';
+
+const treeData = [
+ {
+ name: 'parent 1',
+ test: '0-0',
+ child: [
+ {
+ name: '张晨成',
+ test: '0-0-0',
+ disabled: true,
+ child: [
+ {
+ name: 'leaf',
+ test: '0-0-0-0',
+ disableCheckbox: true,
+ },
+ {
+ name: 'leaf',
+ test: '0-0-0-1',
+ },
+ ],
+ },
+ {
+ name: 'parent 1-1',
+ test: '0-0-1',
+ child: [
+ {
+ test: '0-0-1-0',
+ name: 'zcvc',
+ },
+ ],
+ },
+ ],
+ },
+];
+
+const Demo = () => {
+ const onSelect = (selectedKeys, info) => {
+ console.log('selected', selectedKeys, info);
+ };
+
+ const onCheck = (checkedKeys, info) => {
+ console.log('onCheck', checkedKeys, info);
+ };
+ const fieldNames = {
+ children: 'child',
+ title: 'name',
+ key: 'test',
+ };
+
+ return (
+
+ );
+};
+
+export default Demo;
diff --git a/examples/funtionTitle.jsx b/docs/examples/funtionTitle.jsx
similarity index 97%
rename from examples/funtionTitle.jsx
rename to docs/examples/funtionTitle.jsx
index 5a1509414..4c6560c8f 100644
--- a/examples/funtionTitle.jsx
+++ b/docs/examples/funtionTitle.jsx
@@ -1,8 +1,8 @@
/* eslint no-console:0, react/no-danger: 0 */
-import '../assets/index.less';
+import '../../assets/index.less';
import './animation.less';
import React, { useState } from 'react';
-import Tree from '../src';
+import Tree from '@rc-component/tree';
import data from './longData.json';
const STYLE = `
diff --git a/examples/icon.jsx b/docs/examples/icon.jsx
similarity index 91%
rename from examples/icon.jsx
rename to docs/examples/icon.jsx
index 55cda45b8..63e428078 100644
--- a/examples/icon.jsx
+++ b/docs/examples/icon.jsx
@@ -2,8 +2,8 @@
/* eslint no-alert:0 */
import React from 'react';
import classNames from 'classnames';
-import Tree, { TreeNode } from '../src';
-import '../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
+import '../../assets/index.less';
import './icon.less';
const Icon = ({ selected }) => (
diff --git a/examples/icon.less b/docs/examples/icon.less
similarity index 100%
rename from examples/icon.less
rename to docs/examples/icon.less
diff --git a/examples/longData.json b/docs/examples/longData.json
similarity index 100%
rename from examples/longData.json
rename to docs/examples/longData.json
diff --git a/docs/examples/selectable.jsx b/docs/examples/selectable.jsx
new file mode 100644
index 000000000..bcde39f7c
--- /dev/null
+++ b/docs/examples/selectable.jsx
@@ -0,0 +1,93 @@
+/* eslint-disable no-console, react/no-access-state-in-setstate */
+import React from 'react';
+import './selectable.less';
+import '../../assets/index.less';
+import Tree, { TreeNode } from '@rc-component/tree';
+
+class Demo extends React.Component {
+ render() {
+ return (
+
+
selectable
+
normal
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
customized tree node style if unselectable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default Demo;
diff --git a/docs/examples/selectable.less b/docs/examples/selectable.less
new file mode 100644
index 000000000..fba839406
--- /dev/null
+++ b/docs/examples/selectable.less
@@ -0,0 +1,14 @@
+@import '../../assets/index.less';
+.selectable-demo {
+ padding: 0 20px;
+ .selectable-container {
+ margin: 10px 30px;
+ overflow: auto;
+ border: 1px solid #ccc;
+ .@{treeNodePrefixCls}[aria-selected="false"] {
+ .@{treePrefixCls}-node-content-wrapper span:last-child {
+ text-decoration: line-through;
+ }
+ }
+ }
+}
diff --git a/examples/utils/dataUtil.js b/docs/examples/utils/dataUtil.js
similarity index 100%
rename from examples/utils/dataUtil.js
rename to docs/examples/utils/dataUtil.js
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 000000000..05ad80f20
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,5 @@
+---
+title: rc-tree
+---
+
+
diff --git a/jest.config.js b/jest.config.js
index 66a2c8753..a4a975728 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,3 +1,3 @@
module.exports = {
- snapshotSerializers: [require.resolve('enzyme-to-json/serializer')],
+ setupFilesAfterEnv: ['
/tests/setupFilesAfterEnv.js'],
};
diff --git a/now.json b/now.json
index 797f30a64..43ed1e5f3 100644
--- a/now.json
+++ b/now.json
@@ -5,7 +5,10 @@
{
"src": "package.json",
"use": "@now/static-build",
- "config": { "distDir": ".doc" }
+ "config": { "distDir": "dist" }
}
+ ],
+ "routes": [
+ { "src": "/(.*)", "dest": "/dist/$1" }
]
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 3de225818..3f7c697bd 100644
--- a/package.json
+++ b/package.json
@@ -1,9 +1,9 @@
{
- "name": "rc-tree",
- "version": "3.11.0",
+ "name": "@rc-component/tree",
+ "version": "1.0.1",
"description": "tree ui component for react",
"engines": {
- "node": ">=8.x"
+ "node": ">=10.x"
},
"keywords": [
"react",
@@ -12,11 +12,7 @@
"tree"
],
"files": [
- "assets/*.css",
- "assets/*.png",
- "assets/*.gif",
- "assets/*.less",
- "dist",
+ "assets",
"es",
"lib"
],
@@ -33,46 +29,60 @@
"main": "./lib/index",
"module": "./es/index",
"scripts": {
- "start": "father doc dev --storybook",
- "build": "father doc build --storybook",
+ "start": "dumi dev",
+ "docs:build": "dumi build",
+ "docs:deploy": "gh-pages -d dist",
"compile": "father build && lessc assets/index.less assets/index.css",
- "prepublishOnly": "npm run compile && np --yolo --no-publish --any-branch",
+ "prepare": "husky",
+ "prepublishOnly": "npm run compile && rc-np",
"postpublish": "npm run gh-pages",
- "lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js",
- "test": "father test",
- "gh-pages": "npm run build && father doc deploy",
- "now-build": "npm run build"
+ "lint": "eslint src/ --ext .tsx,.ts,.jsx,.js",
+ "test": "rc-test",
+ "coverage": "rc-test --coverage",
+ "gh-pages": "npm run docs:build && npm run docs:deploy",
+ "now-build": "npm run docs:build"
+ },
+ "lint-staged": {
+ "*": "prettier --write --ignore-unknown"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
},
"devDependencies": {
- "@types/jest": "^26.0.4",
- "@types/react": "^16.8.19",
- "@types/react-dom": "^16.8.4",
+ "@rc-component/father-plugin": "^2.0.3",
+ "@testing-library/jest-dom": "^6.1.5",
+ "@testing-library/react": "^16.1.0",
+ "@types/jest": "^29.5.10",
+ "@types/node": "^22.7.3",
+ "@types/react": "^19.0.1",
+ "@types/react-dom": "^19.0.1",
"@types/warning": "^3.0.0",
- "@umijs/fabric": "^2.3.1",
- "css-animation": "^1.2.0",
- "enzyme": "^3.3.0",
- "enzyme-adapter-react-16": "^1.1.1",
- "enzyme-to-json": "^3.0.0",
- "eslint": "^7.0.0",
- "father": "^2.29.8",
- "less": "^3.11.1",
- "np": "^6.0.0",
- "rc-dialog": "^8.1.0",
- "rc-tooltip": "4.x",
- "rc-trigger": "^4.0.0",
- "react": "^16.8.0",
- "react-dom": "^16.8.0",
- "typescript": "^4.0.2"
+ "@umijs/fabric": "^4.0.1",
+ "dumi": "^2.1.0",
+ "eslint": "^8.55.0",
+ "eslint-plugin-jest": "^28.8.3",
+ "eslint-plugin-unicorn": "^56.0.1",
+ "father": "^4.4.0",
+ "gh-pages": "^6.1.1",
+ "glob": "^11.0.0",
+ "husky": "^9.1.6",
+ "less": "^4.2.1",
+ "lint-staged": "^15.2.10",
+ "@rc-component/np": "^1.0.0",
+ "prettier": "^3.3.3",
+ "@rc-component/dialog": "^1.0.0",
+ "rc-test": "^7.0.15",
+ "@rc-component/tooltip": "^1.0.0",
+ "@rc-component/trigger": "^3.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.3.3"
},
"dependencies": {
- "@babel/runtime": "^7.10.1",
"classnames": "2.x",
- "rc-motion": "^2.0.1",
- "rc-util": "^5.0.0",
- "rc-virtual-list": "^3.0.1"
+ "@rc-component/motion": "^1.0.0",
+ "@rc-component/util": "^1.2.1",
+ "rc-virtual-list": "^3.5.1"
}
}
diff --git a/src/DropIndicator.tsx b/src/DropIndicator.tsx
new file mode 100644
index 000000000..ec03f990b
--- /dev/null
+++ b/src/DropIndicator.tsx
@@ -0,0 +1,39 @@
+import React from 'react';
+
+export interface DropIndicatorProps {
+ dropPosition: -1 | 0 | 1;
+ dropLevelOffset: number;
+ indent: number;
+}
+
+const DropIndicator: React.FC> = props => {
+ const { dropPosition, dropLevelOffset, indent } = props;
+ const style: React.CSSProperties = {
+ pointerEvents: 'none',
+ position: 'absolute',
+ right: 0,
+ backgroundColor: 'red',
+ height: 2,
+ };
+ switch (dropPosition) {
+ case -1:
+ style.top = 0;
+ style.left = -dropLevelOffset * indent;
+ break;
+ case 1:
+ style.bottom = 0;
+ style.left = -dropLevelOffset * indent;
+ break;
+ case 0:
+ style.bottom = 0;
+ style.left = indent;
+ break;
+ }
+ return ;
+};
+
+if (process.env.NODE_ENV !== 'production') {
+ DropIndicator.displayName = 'DropIndicator';
+}
+
+export default DropIndicator;
diff --git a/src/Indent.tsx b/src/Indent.tsx
index 59d2cb25b..41aac59de 100644
--- a/src/Indent.tsx
+++ b/src/Indent.tsx
@@ -1,5 +1,5 @@
-import * as React from 'react';
import classNames from 'classnames';
+import * as React from 'react';
interface IndentProps {
prefixCls: string;
@@ -9,10 +9,6 @@ interface IndentProps {
}
const Indent: React.FC = ({ prefixCls, level, isStart, isEnd }) => {
- if (!level) {
- return null;
- }
-
const baseClassName = `${prefixCls}-indent-unit`;
const list: React.ReactElement[] = [];
for (let i = 0; i < level; i += 1) {
@@ -34,4 +30,4 @@ const Indent: React.FC = ({ prefixCls, level, isStart, isEnd }) =>
);
};
-export default Indent;
+export default React.memo(Indent);
diff --git a/src/MotionTreeNode.tsx b/src/MotionTreeNode.tsx
index 8169c8b6b..946289c68 100644
--- a/src/MotionTreeNode.tsx
+++ b/src/MotionTreeNode.tsx
@@ -1,11 +1,12 @@
-import * as React from 'react';
-import { useEffect } from 'react';
import classNames from 'classnames';
-import CSSMotion from 'rc-motion';
-import TreeNode, { TreeNodeProps } from './TreeNode';
-import { FlattenNode } from './interface';
-import { getTreeNodeProps, TreeNodeRequiredProps } from './utils/treeUtil';
+import CSSMotion from '@rc-component/motion';
+import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
+import * as React from 'react';
import { TreeContext } from './contextTypes';
+import type { FlattenNode, TreeNodeProps } from './interface';
+import TreeNode from './TreeNode';
+import useUnmount from './useUnmount';
+import { getTreeNodeProps, type TreeNodeRequiredProps } from './utils/treeUtil';
interface MotionTreeNodeProps extends Omit {
active: boolean;
@@ -18,8 +19,8 @@ interface MotionTreeNodeProps extends Omit {
treeNodeRequiredProps: TreeNodeRequiredProps;
}
-const MotionTreeNode: React.ForwardRefRenderFunction = (
- {
+const MotionTreeNode = React.forwardRef((oriProps, ref) => {
+ const {
className,
style,
motion,
@@ -30,39 +31,46 @@ const MotionTreeNode: React.ForwardRefRenderFunction {
+ } = oriProps;
const [visible, setVisible] = React.useState(true);
const { prefixCls } = React.useContext(TreeContext);
- const motionedRef = React.useRef(false);
-
- const onMotionEnd = () => {
- if (!motionedRef.current) {
- onOriginMotionEnd();
- }
- motionedRef.current = true;
- };
+ // Calculate target visible here.
+ // And apply in effect to make `leave` motion work.
+ const targetVisible = motionNodes && motionType !== 'hide';
- useEffect(() => {
- if (motionNodes && motionType === 'hide' && visible) {
- setVisible(false);
+ useLayoutEffect(() => {
+ if (motionNodes) {
+ if (targetVisible !== visible) {
+ setVisible(targetVisible);
+ }
}
}, [motionNodes]);
- useEffect(() => {
- // Trigger motion only when patched
+ const triggerMotionStart = () => {
if (motionNodes) {
onOriginMotionStart();
}
+ };
- return () => {
- if (motionNodes) {
- onMotionEnd();
- }
- };
- }, []);
+ // Should only trigger once
+ const triggerMotionEndRef = React.useRef(false);
+ const triggerMotionEnd = () => {
+ if (motionNodes && !triggerMotionEndRef.current) {
+ triggerMotionEndRef.current = true;
+ onOriginMotionEnd();
+ }
+ };
+
+ // Effect if unmount
+ useUnmount(triggerMotionStart, triggerMotionEnd);
+
+ // Motion end event
+ const onVisibleChanged = (nextVisible: boolean) => {
+ if (targetVisible === nextVisible) {
+ triggerMotionEnd();
+ }
+ };
if (motionNodes) {
return (
@@ -71,8 +79,7 @@ const MotionTreeNode: React.ForwardRefRenderFunction
{({ className: motionClassName, style: motionStyle }, motionRef) => (
- {motionNodes.map((treeNode: FlattenNode) => {
+ {motionNodes.map(treeNode => {
const {
- data: { key, ...restProps },
+ data: { ...restProps },
+ title,
+ key,
isStart,
isEnd,
} = treeNode;
@@ -92,8 +101,9 @@ const MotionTreeNode: React.ForwardRefRenderFunction
)}
{...treeNodeProps}
+ title={title}
active={active}
data={treeNode.data}
key={key}
@@ -108,10 +118,10 @@ const MotionTreeNode: React.ForwardRefRenderFunction;
-};
+});
-MotionTreeNode.displayName = 'MotionTreeNode';
-
-const RefMotionTreeNode = React.forwardRef(MotionTreeNode);
+if (process.env.NODE_ENV !== 'production') {
+ MotionTreeNode.displayName = 'MotionTreeNode';
+}
-export default RefMotionTreeNode;
+export default MotionTreeNode;
diff --git a/src/NodeList.tsx b/src/NodeList.tsx
index 0e6880d25..94d4dea64 100644
--- a/src/NodeList.tsx
+++ b/src/NodeList.tsx
@@ -2,12 +2,21 @@
* Handle virtual list of the TreeNodes.
*/
+import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
+import VirtualList, { type ListRef } from 'rc-virtual-list';
import * as React from 'react';
-import VirtualList, { ListRef } from 'rc-virtual-list';
-import { FlattenNode, Key, DataEntity, DataNode, ScrollTo } from './interface';
import MotionTreeNode from './MotionTreeNode';
+import type {
+ BasicDataNode,
+ DataEntity,
+ DataNode,
+ FlattenNode,
+ Key,
+ KeyEntities,
+ ScrollTo,
+} from './interface';
import { findExpandedKeys, getExpandRange } from './utils/diffUtil';
-import { getTreeNodeProps, getKey } from './utils/treeUtil';
+import { getKey, getTreeNodeProps } from './utils/treeUtil';
const HIDDEN_STYLE = {
width: 0,
@@ -34,6 +43,7 @@ export const MotionEntity: DataEntity = {
index: 0,
pos: '0',
node: MotionNode,
+ nodes: [MotionNode],
};
const MotionFlattenData: FlattenNode = {
@@ -41,6 +51,8 @@ const MotionFlattenData: FlattenNode = {
children: [],
pos: MotionEntity.pos,
data: MotionNode,
+ title: null,
+ key: MOTION_KEY,
/** Hold empty list here since we do not use it */
isStart: [],
isEnd: [],
@@ -48,15 +60,16 @@ const MotionFlattenData: FlattenNode = {
export interface NodeListRef {
scrollTo: ScrollTo;
+ getIndentWidth: () => number;
}
-interface NodeListProps {
+interface NodeListProps {
prefixCls: string;
style: React.CSSProperties;
- data: FlattenNode[];
+ data: FlattenNode[];
motion: any;
focusable?: boolean;
- activeItem: FlattenNode;
+ activeItem: FlattenNode;
focused?: boolean;
tabIndex: number;
checkable?: boolean;
@@ -69,7 +82,7 @@ interface NodeListProps {
loadedKeys: Key[];
loadingKeys: Key[];
halfCheckedKeys: Key[];
- keyEntities: Record;
+ keyEntities: KeyEntities;
dragging: boolean;
dragOverNodeKey: Key;
@@ -79,6 +92,7 @@ interface NodeListProps {
height: number;
itemHeight: number;
virtual?: boolean;
+ scrollWidth?: number;
onKeyDown?: React.KeyboardEventHandler;
onFocus?: React.FocusEventHandler;
@@ -106,10 +120,7 @@ export function getMinimumRangeTransitionRange(
}
function itemKey(item: FlattenNode) {
- const {
- data: { key },
- pos,
- } = item;
+ const { key, pos } = item;
return getKey(key, pos);
}
@@ -125,7 +136,7 @@ function getAccessibilityPath(item: FlattenNode): string {
return path;
}
-const RefNodeList: React.RefForwardingComponent = (props, ref) => {
+const NodeList = React.forwardRef>((props, ref) => {
const {
prefixCls,
data,
@@ -148,6 +159,7 @@ const RefNodeList: React.RefForwardingComponent = (p
height,
itemHeight,
virtual,
+ scrollWidth,
focusable,
activeItem,
@@ -167,10 +179,12 @@ const RefNodeList: React.RefForwardingComponent = (p
// =============================== Ref ================================
const listRef = React.useRef(null);
+ const indentMeasurerRef = React.useRef(null);
React.useImperativeHandle(ref, () => ({
scrollTo: scroll => {
listRef.current.scrollTo(scroll);
},
+ getIndentWidth: () => indentMeasurerRef.current.offsetWidth,
}));
// ============================== Motion ==============================
@@ -180,9 +194,15 @@ const RefNodeList: React.RefForwardingComponent = (p
const [transitionRange, setTransitionRange] = React.useState([]);
const [motionType, setMotionType] = React.useState<'show' | 'hide' | null>(null);
+ // When motion end but data change, this will makes data back to previous one
+ const dataRef = React.useRef(data);
+ dataRef.current = data;
+
function onMotionEnd() {
- setPrevData(data);
- setTransitionData(data);
+ const latestData = dataRef.current;
+
+ setPrevData(latestData);
+ setTransitionData(latestData);
setTransitionRange([]);
setMotionType(null);
@@ -190,14 +210,15 @@ const RefNodeList: React.RefForwardingComponent = (p
}
// Do animation if expanded keys changed
- React.useEffect(() => {
+ // layoutEffect here to avoid blink of node removing
+ useLayoutEffect(() => {
setPrevExpandedKeys(expandedKeys);
const diffExpanded = findExpandedKeys(prevExpandedKeys, expandedKeys);
if (diffExpanded.key !== null) {
if (diffExpanded.add) {
- const keyIndex = prevData.findIndex(({ data: { key } }) => key === diffExpanded.key);
+ const keyIndex = prevData.findIndex(({ key }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(prevData, data, diffExpanded.key),
@@ -213,7 +234,7 @@ const RefNodeList: React.RefForwardingComponent = (p
setTransitionRange(rangeNodes);
setMotionType('show');
} else {
- const keyIndex = data.findIndex(({ data: { key } }) => key === diffExpanded.key);
+ const keyIndex = data.findIndex(({ key }) => key === diffExpanded.key);
const rangeNodes = getMinimumRangeTransitionRange(
getExpandRange(data, prevData, diffExpanded.key),
@@ -265,7 +286,7 @@ const RefNodeList: React.RefForwardingComponent = (p
)}
-
+
= (p
onBlur={onBlur}
value=""
onChange={noop}
+ aria-label="for screen reader"
/>
+
+
{...domProps}
data={mergedData}
@@ -286,26 +326,41 @@ const RefNodeList: React.RefForwardingComponent = (p
fullHeight={false}
virtual={virtual}
itemHeight={itemHeight}
+ scrollWidth={scrollWidth}
prefixCls={`${prefixCls}-list`}
ref={listRef}
+ role="tree"
+ onVisibleChange={originList => {
+ // The best match is using `fullList` - `originList` = `restList`
+ // and check the `restList` to see if has the MOTION_KEY node
+ // but this will cause performance issue for long list compare
+ // we just check `originList` and repeat trigger `onMotionEnd`
+ if (originList.every(item => itemKey(item) !== MOTION_KEY)) {
+ onMotionEnd();
+ }
+ }}
>
- {(treeNode: FlattenNode) => {
+ {treeNode => {
const {
pos,
- data: { key, ...restProps },
+ data: { ...restProps },
+ title,
+ key,
isStart,
isEnd,
} = treeNode;
const mergedKey = getKey(key, pos);
+ delete restProps.key;
delete restProps.children;
const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
return (
)}
{...treeNodeProps}
- active={activeItem && key === activeItem.data.key}
+ title={title}
+ active={!!activeItem && key === activeItem.key}
pos={pos}
data={treeNode.data}
isStart={isStart}
@@ -325,9 +380,10 @@ const RefNodeList: React.RefForwardingComponent = (p
>
);
-};
+});
-const NodeList = React.forwardRef(RefNodeList);
-NodeList.displayName = 'NodeList';
+if (process.env.NODE_ENV !== 'production') {
+ NodeList.displayName = 'NodeList';
+}
export default NodeList;
diff --git a/src/Tree.tsx b/src/Tree.tsx
index 76cad7149..ec675df98 100644
--- a/src/Tree.tsx
+++ b/src/Tree.tsx
@@ -1,78 +1,111 @@
// TODO: https://www.w3.org/TR/2017/NOTE-wai-aria-practices-1.1-20171214/examples/treeview/treeview-2/treeview-2a.html
// Fully accessibility support
-import * as React from 'react';
-import KeyCode from 'rc-util/lib/KeyCode';
-import warning from 'rc-util/lib/warning';
import classNames from 'classnames';
+import KeyCode from '@rc-component/util/lib/KeyCode';
+import pickAttrs from '@rc-component/util/lib/pickAttrs';
+import warning from '@rc-component/util/lib/warning';
+import * as React from 'react';
-import {
- TreeContext,
- NodeMouseEventHandler,
+import type {
NodeDragEventHandler,
NodeDragEventParams,
+ NodeMouseEventHandler,
NodeMouseEventParams,
} from './contextTypes';
-import {
- getDataAndAria,
- getDragNodesKeys,
- parseCheckedKeys,
- conductExpandParent,
- calcSelectedKeys,
- calcDropPosition,
- arrAdd,
- arrDel,
- posToArr,
-} from './util';
-import {
+import { TreeContext } from './contextTypes';
+import DropIndicator from './DropIndicator';
+import type { DropIndicatorProps } from './DropIndicator';
+import type {
+ BasicDataNode,
DataNode,
+ Direction,
+ EventDataNode,
+ FieldNames,
+ FlattenNode,
IconType,
Key,
- FlattenNode,
- DataEntity,
- EventDataNode,
- NodeInstance,
+ KeyEntities,
+ SafeKey,
ScrollTo,
+ TreeNodeProps,
} from './interface';
+import NodeList, { MOTION_KEY, MotionEntity, type NodeListRef } from './NodeList';
+import TreeNode from './TreeNode';
+import {
+ arrAdd,
+ arrDel,
+ calcDropPosition,
+ calcSelectedKeys,
+ conductExpandParent,
+ getDragChildrenKeys,
+ parseCheckedKeys,
+ posToArr,
+} from './util';
+import { conductCheck } from './utils/conductUtil';
+import getEntity from './utils/keyUtil';
import {
- flattenTreeData,
- convertTreeToData,
convertDataToEntities,
- warningWithoutKey,
convertNodePropsToEventData,
+ convertTreeToData,
+ fillFieldNames,
+ flattenTreeData,
getTreeNodeProps,
+ warningWithoutKey,
} from './utils/treeUtil';
-import NodeList, { MOTION_KEY, MotionEntity, NodeListRef } from './NodeList';
-import TreeNode from './TreeNode';
-import { conductCheck } from './utils/conductUtil';
-interface CheckInfo {
+const MAX_RETRY_TIMES = 10;
+
+export interface CheckInfo
{
event: 'check';
- node: EventDataNode;
+ node: EventDataNode;
checked: boolean;
nativeEvent: MouseEvent;
- checkedNodes: DataNode[];
- checkedNodesPositions?: { node: DataNode; pos: string }[];
+ checkedNodes: TreeDataType[];
+ checkedNodesPositions?: { node: TreeDataType; pos: string }[];
halfCheckedKeys?: Key[];
}
-export interface TreeProps {
+export interface AllowDropOptions {
+ dragNode: TreeDataType;
+ dropNode: TreeDataType;
+ dropPosition: -1 | 0 | 1;
+}
+export type AllowDrop = (
+ options: AllowDropOptions,
+) => boolean;
+
+export type DraggableFn = (node: DataNode) => boolean;
+export type DraggableConfig = {
+ icon?: React.ReactNode | false;
+ nodeDraggable?: DraggableFn;
+};
+
+export type ExpandAction = false | 'click' | 'doubleClick';
+
+export type SemanticName = 'itemIcon' | 'item' | 'itemTitle';
+export interface TreeProps {
prefixCls: string;
className?: string;
style?: React.CSSProperties;
+ styles?: Partial>;
+ classNames?: Partial>;
focusable?: boolean;
+ activeKey?: Key | null;
tabIndex?: number;
children?: React.ReactNode;
- treeData?: DataNode[]; // Generate treeNode by children
+ treeData?: TreeDataType[]; // Generate treeNode by children
+ fieldNames?: FieldNames;
showLine?: boolean;
showIcon?: boolean;
icon?: IconType;
selectable?: boolean;
+ expandAction?: ExpandAction;
disabled?: boolean;
multiple?: boolean;
checkable?: boolean | React.ReactNode;
checkStrictly?: boolean;
- draggable?: boolean;
+ draggable?: DraggableFn | boolean | DraggableConfig;
defaultExpandParent?: boolean;
autoExpandParent?: boolean;
defaultExpandAll?: boolean;
@@ -82,29 +115,35 @@ export interface TreeProps {
checkedKeys?: Key[] | { checked: Key[]; halfChecked: Key[] };
defaultSelectedKeys?: Key[];
selectedKeys?: Key[];
- titleRender?: (node: DataNode) => React.ReactNode;
+ allowDrop?: AllowDrop;
+ titleRender?: (node: TreeDataType) => React.ReactNode;
+ dropIndicatorRender?: (props: DropIndicatorProps) => React.ReactNode;
onFocus?: React.FocusEventHandler;
onBlur?: React.FocusEventHandler;
onKeyDown?: React.KeyboardEventHandler;
onContextMenu?: React.MouseEventHandler;
- onClick?: NodeMouseEventHandler;
- onDoubleClick?: NodeMouseEventHandler;
+ onClick?: NodeMouseEventHandler;
+ onDoubleClick?: NodeMouseEventHandler;
+ onScroll?: React.UIEventHandler;
onExpand?: (
expandedKeys: Key[],
info: {
- node: EventDataNode;
+ node: EventDataNode;
expanded: boolean;
nativeEvent: MouseEvent;
},
) => void;
- onCheck?: (checked: { checked: Key[]; halfChecked: Key[] } | Key[], info: CheckInfo) => void;
+ onCheck?: (
+ checked: { checked: Key[]; halfChecked: Key[] } | Key[],
+ info: CheckInfo,
+ ) => void;
onSelect?: (
selectedKeys: Key[],
info: {
event: 'select';
selected: boolean;
- node: EventDataNode;
- selectedNodes: DataNode[];
+ node: EventDataNode;
+ selectedNodes: TreeDataType[];
nativeEvent: MouseEvent;
},
) => void;
@@ -112,22 +151,22 @@ export interface TreeProps {
loadedKeys: Key[],
info: {
event: 'load';
- node: EventDataNode;
+ node: EventDataNode;
},
) => void;
- loadData?: (treeNode: EventDataNode) => Promise;
+ loadData?: (treeNode: EventDataNode) => Promise;
loadedKeys?: Key[];
- onMouseEnter?: (info: NodeMouseEventParams) => void;
- onMouseLeave?: (info: NodeMouseEventParams) => void;
- onRightClick?: (info: { event: React.MouseEvent; node: EventDataNode }) => void;
- onDragStart?: (info: NodeDragEventParams) => void;
- onDragEnter?: (info: NodeDragEventParams & { expandedKeys: Key[] }) => void;
- onDragOver?: (info: NodeDragEventParams) => void;
- onDragLeave?: (info: NodeDragEventParams) => void;
- onDragEnd?: (info: NodeDragEventParams) => void;
+ onMouseEnter?: (info: NodeMouseEventParams) => void;
+ onMouseLeave?: (info: NodeMouseEventParams) => void;
+ onRightClick?: (info: { event: React.MouseEvent; node: EventDataNode }) => void;
+ onDragStart?: (info: NodeDragEventParams) => void;
+ onDragEnter?: (info: NodeDragEventParams & { expandedKeys: Key[] }) => void;
+ onDragOver?: (info: NodeDragEventParams) => void;
+ onDragLeave?: (info: NodeDragEventParams) => void;
+ onDragEnd?: (info: NodeDragEventParams) => void;
onDrop?: (
- info: NodeDragEventParams & {
- dragNode: EventDataNode;
+ info: NodeDragEventParams & {
+ dragNode: EventDataNode;
dragNodesKeys: Key[];
dropPosition: number;
dropToGap: boolean;
@@ -138,18 +177,28 @@ export interface TreeProps {
* Do not use in your production code directly since this will be refactor.
*/
onActiveChange?: (key: Key) => void;
- filterTreeNode?: (treeNode: EventDataNode) => boolean;
+ filterTreeNode?: (treeNode: EventDataNode) => boolean;
motion?: any;
switcherIcon?: IconType;
// Virtual List
height?: number;
itemHeight?: number;
+ scrollWidth?: number;
+ itemScrollOffset?: number;
virtual?: boolean;
+
+ // direction for drag logic
+ direction?: Direction;
+
+ rootClassName?: string;
+ rootStyle?: React.CSSProperties;
}
-interface TreeState {
- keyEntities: Record;
+interface TreeState {
+ keyEntities: KeyEntities;
+
+ indent: number | null;
selectedKeys: Key[];
checkedKeys: Key[];
@@ -158,24 +207,36 @@ interface TreeState {
loadingKeys: Key[];
expandedKeys: Key[];
- dragging: boolean;
- dragNodesKeys: Key[];
- dragOverNodeKey: Key;
- dropPosition: number;
+ draggingNodeKey: Key;
+ dragChildrenKeys: Key[];
+
+ // for details see comment in Tree.state
+ dropPosition: -1 | 0 | 1 | null;
+ dropLevelOffset: number | null;
+ dropContainerKey: Key | null;
+ dropTargetKey: Key | null;
+ dropTargetPos: string | null;
+ dropAllowed: boolean;
+ dragOverNodeKey: Key | null;
- treeData: DataNode[];
- flattenNodes: FlattenNode[];
+ treeData: TreeDataType[];
+ flattenNodes: FlattenNode[];
focused: boolean;
- activeKey: Key;
+ activeKey: Key | null;
// Record if list is changing
listChanging: boolean;
prevProps: TreeProps;
+
+ fieldNames: FieldNames;
}
-class Tree extends React.Component {
+class Tree extends React.Component<
+ TreeProps,
+ TreeState
+> {
static defaultProps = {
prefixCls: 'rc-tree',
showLine: false,
@@ -192,17 +253,24 @@ class Tree extends React.Component {
defaultExpandedKeys: [],
defaultCheckedKeys: [],
defaultSelectedKeys: [],
+ dropIndicatorRender: DropIndicator,
+ allowDrop: () => true,
+ expandAction: false,
};
static TreeNode = TreeNode;
destroyed: boolean = false;
- delayedDragEnterLogic: Record;
+ delayedDragEnterLogic: Record;
+
+ loadingRetryTimes: Record = {};
- state: TreeState = {
+ state: TreeState = {
keyEntities: {},
+ indent: null,
+
selectedKeys: [],
checkedKeys: [],
halfCheckedKeys: [],
@@ -210,10 +278,22 @@ class Tree extends React.Component {
loadingKeys: [],
expandedKeys: [],
- dragging: false,
- dragNodesKeys: [],
+ draggingNodeKey: null,
+ dragChildrenKeys: [],
+
+ // dropTargetKey is the key of abstract-drop-node
+ // the abstract-drop-node is the real drop node when drag and drop
+ // not the DOM drag over node
+ dropTargetKey: null,
+ dropPosition: null, // the drop position of abstract-drop-node, inside 0, top -1, bottom 1
+ dropContainerKey: null, // the container key of abstract-drop-node if dropPosition is -1 or 1
+ dropLevelOffset: null, // the drop level offset of abstract-drag-over-node
+ dropTargetPos: null, // the pos of abstract-drop-node
+ dropAllowed: true, // if drop to abstract-drop-node is allowed
+ // the abstract-drag-over-node
+ // if mouse is on the bottom of top dom node or no the top of the bottom dom node
+ // abstract-drag-over-node is the top node
dragOverNodeKey: null,
- dropPosition: null,
treeData: [],
flattenNodes: [],
@@ -224,12 +304,44 @@ class Tree extends React.Component {
listChanging: false,
prevProps: null,
+
+ fieldNames: fillFieldNames(),
};
- dragNode: NodeInstance;
+ dragStartMousePosition = null;
+
+ dragNodeProps: TreeNodeProps = null;
+
+ currentMouseOverDroppableNodeKey = null;
listRef = React.createRef();
+ componentDidMount(): void {
+ this.destroyed = false;
+ this.onUpdated();
+ }
+
+ componentDidUpdate(): void {
+ this.onUpdated();
+ }
+
+ onUpdated() {
+ const { activeKey, itemScrollOffset = 0 } = this.props;
+
+ if (activeKey !== undefined && activeKey !== this.state.activeKey) {
+ this.setState({ activeKey });
+
+ if (activeKey !== null) {
+ this.scrollTo({ key: activeKey, offset: itemScrollOffset });
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('dragend', this.onWindowDragEnd);
+ this.destroyed = true;
+ }
+
static getDerivedStateFromProps(props: TreeProps, prevState: TreeState) {
const { prevProps } = prevState;
const newState: Partial = {
@@ -237,12 +349,21 @@ class Tree extends React.Component {
};
function needSync(name: string) {
- return (!prevProps && name in props) || (prevProps && prevProps[name] !== props[name]);
+ return (
+ (!prevProps && props.hasOwnProperty(name)) || (prevProps && prevProps[name] !== props[name])
+ );
}
// ================== Tree Node ==================
let treeData: DataNode[];
+ // fieldNames
+ let { fieldNames } = prevState;
+ if (needSync('fieldNames')) {
+ fieldNames = fillFieldNames(props.fieldNames);
+ newState.fieldNames = fieldNames;
+ }
+
// Check if `treeData` or `children` changed and save into the state.
if (needSync('treeData')) {
({ treeData } = props);
@@ -254,7 +375,7 @@ class Tree extends React.Component {
// Save flatten nodes info and convert `treeData` into keyEntities
if (treeData) {
newState.treeData = treeData;
- const entitiesMap = convertDataToEntities(treeData);
+ const entitiesMap = convertDataToEntities(treeData, { fieldNames });
newState.keyEntities = {
[MOTION_KEY]: MotionEntity,
...entitiesMap.keyEntities,
@@ -262,7 +383,7 @@ class Tree extends React.Component {
// Warning if treeNode not provide key
if (process.env.NODE_ENV !== 'production') {
- warningWithoutKey(treeData);
+ warningWithoutKey(treeData, fieldNames);
}
}
@@ -277,7 +398,17 @@ class Tree extends React.Component {
} else if (!prevProps && props.defaultExpandAll) {
const cloneKeyEntities = { ...keyEntities };
delete cloneKeyEntities[MOTION_KEY];
- newState.expandedKeys = Object.keys(cloneKeyEntities).map(key => cloneKeyEntities[key].key);
+
+ // Only take the key who has the children to enhance the performance
+ const nextExpandedKeys: React.Key[] = [];
+ Object.keys(cloneKeyEntities).forEach(key => {
+ const entity = cloneKeyEntities[key];
+ if (entity.children && entity.children.length) {
+ nextExpandedKeys.push(entity.key);
+ }
+ });
+
+ newState.expandedKeys = nextExpandedKeys;
} else if (!prevProps && props.defaultExpandedKeys) {
newState.expandedKeys =
props.autoExpandParent || props.defaultExpandParent
@@ -291,9 +422,10 @@ class Tree extends React.Component {
// ================ flattenNodes =================
if (treeData || newState.expandedKeys) {
- const flattenNodes: FlattenNode[] = flattenTreeData(
+ const flattenNodes = flattenTreeData(
treeData || prevState.treeData,
newState.expandedKeys || prevState.expandedKeys,
+ fieldNames,
);
newState.flattenNodes = flattenNodes;
}
@@ -309,7 +441,7 @@ class Tree extends React.Component {
// ================= checkedKeys =================
if (props.checkable) {
- let checkedKeyEntity;
+ let checkedKeyEntity: { checkedKeys?: Key[]; halfCheckedKeys?: Key[] };
if (needSync('checkedKeys')) {
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
@@ -344,216 +476,393 @@ class Tree extends React.Component {
return newState;
}
- componentWillUnmount() {
- this.destroyed = true;
- }
-
- onNodeDragStart: NodeDragEventHandler = (event, node) => {
+ onNodeDragStart: NodeDragEventHandler = (event, nodeProps) => {
const { expandedKeys, keyEntities } = this.state;
const { onDragStart } = this.props;
- const { eventKey } = node.props;
+ const { eventKey } = nodeProps;
- this.dragNode = node;
+ this.dragNodeProps = nodeProps;
+ this.dragStartMousePosition = {
+ x: event.clientX,
+ y: event.clientY,
+ };
const newExpandedKeys = arrDel(expandedKeys, eventKey);
this.setState({
- dragging: true,
- dragNodesKeys: getDragNodesKeys(eventKey, keyEntities),
+ draggingNodeKey: eventKey,
+ dragChildrenKeys: getDragChildrenKeys(eventKey, keyEntities),
+ indent: this.listRef.current.getIndentWidth(),
});
this.setExpandedKeys(newExpandedKeys);
- if (onDragStart) {
- onDragStart({ event, node: convertNodePropsToEventData(node.props) });
- }
+ window.addEventListener('dragend', this.onWindowDragEnd);
+
+ onDragStart?.({ event, node: convertNodePropsToEventData(nodeProps) });
};
/**
- * [Legacy] Select handler is less small than node,
+ * [Legacy] Select handler is smaller than node,
* so that this will trigger when drag enter node or select handler.
* This is a little tricky if customize css without padding.
* Better for use mouse move event to refresh drag state.
* But let's just keep it to avoid event trigger logic change.
*/
- onNodeDragEnter: NodeDragEventHandler = (event, node) => {
- const { expandedKeys, keyEntities, dragNodesKeys } = this.state;
- const { onDragEnter } = this.props;
- const { pos, eventKey } = node.props;
+ onNodeDragEnter = (
+ event: React.DragEvent,
+ nodeProps: TreeNodeProps,
+ ) => {
+ const { expandedKeys, keyEntities, dragChildrenKeys, flattenNodes, indent } = this.state;
+ const { onDragEnter, onExpand, allowDrop, direction } = this.props;
+ const { pos, eventKey } = nodeProps;
- if (!this.dragNode || dragNodesKeys.indexOf(eventKey) !== -1) return;
+ // record the key of node which is latest entered, used in dragleave event.
+ if (this.currentMouseOverDroppableNodeKey !== eventKey) {
+ this.currentMouseOverDroppableNodeKey = eventKey;
+ }
- const dropPosition = calcDropPosition(event, node);
+ if (!this.dragNodeProps) {
+ this.resetDragState();
+ return;
+ }
- // Skip if drag node is self
- if (this.dragNode.props.eventKey === eventKey && dropPosition === 0) {
- this.setState({
- dragOverNodeKey: '',
- dropPosition: null,
- });
+ const {
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ dragOverNodeKey,
+ } = calcDropPosition(
+ event,
+ this.dragNodeProps,
+ nodeProps,
+ indent,
+ this.dragStartMousePosition,
+ allowDrop,
+ flattenNodes,
+ keyEntities,
+ expandedKeys,
+ direction,
+ );
+
+ if (
+ // don't allow drop inside its children
+ dragChildrenKeys.includes(dropTargetKey) ||
+ // don't allow drop when drop is not allowed caculated by calcDropPosition
+ !dropAllowed
+ ) {
+ this.resetDragState();
return;
}
- // Ref: https://github.com/react-component/tree/issues/132
- // Add timeout to let onDragLevel fire before onDragEnter,
- // so that we can clean drag props for onDragLeave node.
- // Macro task for this:
- // https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
- setTimeout(() => {
- // Update drag over node
- this.setState({
- dragOverNodeKey: eventKey,
- dropPosition,
- });
+ // Side effect for delay drag
+ if (!this.delayedDragEnterLogic) {
+ this.delayedDragEnterLogic = {};
+ }
+ Object.keys(this.delayedDragEnterLogic).forEach(key => {
+ clearTimeout(this.delayedDragEnterLogic[key]);
+ });
- // Side effect for delay drag
- if (!this.delayedDragEnterLogic) {
- this.delayedDragEnterLogic = {};
- }
- Object.keys(this.delayedDragEnterLogic).forEach(key => {
- clearTimeout(this.delayedDragEnterLogic[key]);
- });
+ if (this.dragNodeProps.eventKey !== nodeProps.eventKey) {
+ // hoist expand logic here
+ // since if logic is on the bottom
+ // it will be blocked by abstract dragover node check
+ // => if you dragenter from top, you mouse will still be consider as in the top node
+ event.persist();
this.delayedDragEnterLogic[pos] = window.setTimeout(() => {
- if (!this.state.dragging) return;
+ if (this.state.draggingNodeKey === null) {
+ return;
+ }
let newExpandedKeys = [...expandedKeys];
- const entity = keyEntities[eventKey];
+ const entity = getEntity(keyEntities, nodeProps.eventKey);
if (entity && (entity.children || []).length) {
- newExpandedKeys = arrAdd(expandedKeys, eventKey);
+ newExpandedKeys = arrAdd(expandedKeys, nodeProps.eventKey);
}
- if (!('expandedKeys' in this.props)) {
+ if (!this.props.hasOwnProperty('expandedKeys')) {
this.setExpandedKeys(newExpandedKeys);
}
- if (onDragEnter) {
- onDragEnter({
- event,
- node: convertNodePropsToEventData(node.props),
- expandedKeys: newExpandedKeys,
- });
- }
- }, 400);
- }, 0);
+ onExpand?.(newExpandedKeys, {
+ node: convertNodePropsToEventData(nodeProps),
+ expanded: true,
+ nativeEvent: event.nativeEvent,
+ });
+ }, 800);
+ }
+
+ // Skip if drag node is self
+ if (this.dragNodeProps.eventKey === dropTargetKey && dropLevelOffset === 0) {
+ this.resetDragState();
+ return;
+ }
+
+ // Update drag over node and drag state
+ this.setState({
+ dragOverNodeKey,
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ });
+
+ onDragEnter?.({
+ event,
+ node: convertNodePropsToEventData(nodeProps),
+ expandedKeys,
+ });
};
- onNodeDragOver: NodeDragEventHandler = (event, node) => {
- const { dragNodesKeys } = this.state;
- const { onDragOver } = this.props;
- const { eventKey } = node.props;
+ onNodeDragOver = (
+ event: React.DragEvent,
+ nodeProps: TreeNodeProps,
+ ) => {
+ const { dragChildrenKeys, flattenNodes, keyEntities, expandedKeys, indent } = this.state;
+ const { onDragOver, allowDrop, direction } = this.props;
- if (dragNodesKeys.indexOf(eventKey) !== -1) {
+ if (!this.dragNodeProps) {
return;
}
- // Update drag position
- if (this.dragNode && eventKey === this.state.dragOverNodeKey) {
- const dropPosition = calcDropPosition(event, node);
+ const {
+ dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ dragOverNodeKey,
+ } = calcDropPosition(
+ event,
+ this.dragNodeProps,
+ nodeProps,
+ indent,
+ this.dragStartMousePosition,
+ allowDrop,
+ flattenNodes,
+ keyEntities,
+ expandedKeys,
+ direction,
+ );
- if (dropPosition === this.state.dropPosition) return;
+ if (dragChildrenKeys.includes(dropTargetKey) || !dropAllowed) {
+ // don't allow drop inside its children
+ // don't allow drop when drop is not allowed calculated by calcDropPosition
+ return;
+ }
+ // Update drag position
+
+ if (this.dragNodeProps.eventKey === dropTargetKey && dropLevelOffset === 0) {
+ if (
+ !(
+ this.state.dropPosition === null &&
+ this.state.dropLevelOffset === null &&
+ this.state.dropTargetKey === null &&
+ this.state.dropContainerKey === null &&
+ this.state.dropTargetPos === null &&
+ this.state.dropAllowed === false &&
+ this.state.dragOverNodeKey === null
+ )
+ ) {
+ this.resetDragState();
+ }
+ } else if (
+ !(
+ dropPosition === this.state.dropPosition &&
+ dropLevelOffset === this.state.dropLevelOffset &&
+ dropTargetKey === this.state.dropTargetKey &&
+ dropContainerKey === this.state.dropContainerKey &&
+ dropTargetPos === this.state.dropTargetPos &&
+ dropAllowed === this.state.dropAllowed &&
+ dragOverNodeKey === this.state.dragOverNodeKey
+ )
+ ) {
this.setState({
dropPosition,
+ dropLevelOffset,
+ dropTargetKey,
+ dropContainerKey,
+ dropTargetPos,
+ dropAllowed,
+ dragOverNodeKey,
});
}
- if (onDragOver) {
- onDragOver({ event, node: convertNodePropsToEventData(node.props) });
- }
+ onDragOver?.({ event, node: convertNodePropsToEventData(nodeProps) });
};
- onNodeDragLeave: NodeDragEventHandler = (event, node) => {
+ onNodeDragLeave: NodeDragEventHandler = (event, nodeProps) => {
+ // if it is outside the droppable area
+ // currentMouseOverDroppableNodeKey will be updated in dragenter event when into another droppable receiver.
+ if (
+ this.currentMouseOverDroppableNodeKey === nodeProps.eventKey &&
+ !event.currentTarget.contains(event.relatedTarget as Node)
+ ) {
+ this.resetDragState();
+ this.currentMouseOverDroppableNodeKey = null;
+ }
+
const { onDragLeave } = this.props;
- this.setState({
- dragOverNodeKey: '',
- });
+ onDragLeave?.({ event, node: convertNodePropsToEventData(nodeProps) });
+ };
- if (onDragLeave) {
- onDragLeave({ event, node: convertNodePropsToEventData(node.props) });
- }
+ // since stopPropagation() is called in treeNode
+ // if onWindowDrag is called, whice means state is keeped, drag state should be cleared
+ onWindowDragEnd = event => {
+ this.onNodeDragEnd(event, null, true);
+ window.removeEventListener('dragend', this.onWindowDragEnd);
};
- onNodeDragEnd: NodeDragEventHandler = (event, node) => {
+ // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called
+ onNodeDragEnd: NodeDragEventHandler = (event, nodeProps) => {
const { onDragEnd } = this.props;
this.setState({
- dragOverNodeKey: '',
+ dragOverNodeKey: null,
});
+
this.cleanDragState();
- if (onDragEnd) {
- onDragEnd({ event, node: convertNodePropsToEventData(node.props) });
- }
+ onDragEnd?.({ event, node: convertNodePropsToEventData(nodeProps) });
+
+ this.dragNodeProps = null;
- this.dragNode = null;
+ window.removeEventListener('dragend', this.onWindowDragEnd);
};
- onNodeDrop: NodeDragEventHandler = (event, node) => {
- const { dragNodesKeys = [], dropPosition } = this.state;
+ onNodeDrop = (
+ event: React.DragEvent,
+ _: TreeNodeProps,
+ outsideTree: boolean = false,
+ ) => {
+ const { dragChildrenKeys, dropPosition, dropTargetKey, dropTargetPos, dropAllowed } =
+ this.state;
+
+ if (!dropAllowed) {
+ return;
+ }
+
const { onDrop } = this.props;
- const { eventKey, pos } = node.props;
this.setState({
- dragOverNodeKey: '',
+ dragOverNodeKey: null,
});
this.cleanDragState();
- if (dragNodesKeys.indexOf(eventKey) !== -1) {
- warning(false, "Can not drop to dragNode(include it's children node)");
- return;
- }
+ if (dropTargetKey === null) return;
+
+ const abstractDropNodeProps = {
+ ...getTreeNodeProps(dropTargetKey, this.getTreeNodeRequiredProps()),
+ active: this.getActiveItem()?.key === dropTargetKey,
+ data: getEntity(this.state.keyEntities, dropTargetKey).node,
+ };
+
+ const dropToChild = dragChildrenKeys.includes(dropTargetKey);
+
+ warning(
+ !dropToChild,
+ "Can not drop to dragNode's children node. This is a bug of rc-tree. Please report an issue.",
+ );
- const posArr = posToArr(pos);
+ const posArr = posToArr(dropTargetPos);
const dropResult = {
event,
- node: convertNodePropsToEventData(node.props),
- dragNode: this.dragNode ? convertNodePropsToEventData(this.dragNode.props) : null,
- dragNodesKeys: dragNodesKeys.slice(),
+ node: convertNodePropsToEventData(abstractDropNodeProps),
+ dragNode: this.dragNodeProps ? convertNodePropsToEventData(this.dragNodeProps) : null,
+ dragNodesKeys: [this.dragNodeProps.eventKey].concat(dragChildrenKeys),
+ dropToGap: dropPosition !== 0,
dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
- dropToGap: false,
};
- if (dropPosition !== 0) {
- dropResult.dropToGap = true;
- }
-
- if (onDrop) {
- onDrop(dropResult);
+ if (!outsideTree) {
+ onDrop?.(dropResult);
}
- this.dragNode = null;
+ this.dragNodeProps = null;
};
+ resetDragState() {
+ this.setState({
+ dragOverNodeKey: null,
+ dropPosition: null,
+ dropLevelOffset: null,
+ dropTargetKey: null,
+ dropContainerKey: null,
+ dropTargetPos: null,
+ dropAllowed: false,
+ });
+ }
+
cleanDragState = () => {
- const { dragging } = this.state;
- if (dragging) {
+ const { draggingNodeKey } = this.state;
+ if (draggingNodeKey !== null) {
this.setState({
- dragging: false,
+ draggingNodeKey: null,
+ dropPosition: null,
+ dropContainerKey: null,
+ dropTargetKey: null,
+ dropLevelOffset: null,
+ dropAllowed: true,
+ dragOverNodeKey: null,
});
}
+ this.dragStartMousePosition = null;
+ this.currentMouseOverDroppableNodeKey = null;
+ };
+
+ triggerExpandActionExpand: NodeMouseEventHandler = (e, treeNode) => {
+ const { expandedKeys, flattenNodes } = this.state;
+ const { expanded, key, isLeaf } = treeNode;
+
+ if (isLeaf || e.shiftKey || e.metaKey || e.ctrlKey) {
+ return;
+ }
+
+ const node = flattenNodes.filter(nodeItem => nodeItem.key === key)[0];
+ const eventNode = convertNodePropsToEventData({
+ ...getTreeNodeProps(key, this.getTreeNodeRequiredProps()),
+ data: node.data,
+ });
+
+ this.setExpandedKeys(expanded ? arrDel(expandedKeys, key) : arrAdd(expandedKeys, key));
+ this.onNodeExpand(e as React.MouseEvent, eventNode);
};
- onNodeClick: NodeMouseEventHandler = (e, treeNode) => {
- const { onClick } = this.props;
- if (onClick) {
- onClick(e, treeNode);
+ onNodeClick: NodeMouseEventHandler = (e, treeNode) => {
+ const { onClick, expandAction } = this.props;
+
+ if (expandAction === 'click') {
+ this.triggerExpandActionExpand(e, treeNode);
}
+
+ onClick?.(e, treeNode);
};
- onNodeDoubleClick: NodeMouseEventHandler = (e, treeNode) => {
- const { onDoubleClick } = this.props;
- if (onDoubleClick) {
- onDoubleClick(e, treeNode);
+ onNodeDoubleClick: NodeMouseEventHandler = (e, treeNode) => {
+ const { onDoubleClick, expandAction } = this.props;
+
+ if (expandAction === 'doubleClick') {
+ this.triggerExpandActionExpand(e, treeNode);
}
+
+ onDoubleClick?.(e, treeNode);
};
- onNodeSelect: NodeMouseEventHandler = (e, treeNode) => {
+ onNodeSelect: NodeMouseEventHandler = (e, treeNode) => {
let { selectedKeys } = this.state;
- const { keyEntities } = this.state;
+ const { keyEntities, fieldNames } = this.state;
const { onSelect, multiple } = this.props;
- const { selected, key } = treeNode;
+ const { selected } = treeNode;
+ const key = treeNode[fieldNames.key];
const targetSelected = !selected;
// Update selected keys
@@ -568,29 +877,25 @@ class Tree extends React.Component {
// [Legacy] Not found related usage in doc or upper libs
const selectedNodes = selectedKeys
.map(selectedKey => {
- const entity = keyEntities[selectedKey];
- if (!entity) return null;
-
- return entity.node;
+ const entity = getEntity(keyEntities, selectedKey);
+ return entity ? entity.node : null;
})
- .filter(node => node);
+ .filter(Boolean);
this.setUncontrolledState({ selectedKeys });
- if (onSelect) {
- onSelect(selectedKeys, {
- event: 'select',
- selected: targetSelected,
- node: treeNode,
- selectedNodes,
- nativeEvent: e.nativeEvent,
- });
- }
+ onSelect?.(selectedKeys, {
+ event: 'select',
+ selected: targetSelected,
+ node: treeNode,
+ selectedNodes,
+ nativeEvent: e.nativeEvent,
+ });
};
onNodeCheck = (
e: React.MouseEvent,
- treeNode: EventDataNode,
+ treeNode: EventDataNode,
checked: boolean,
) => {
const {
@@ -602,8 +907,9 @@ class Tree extends React.Component {
const { key } = treeNode;
// Prepare trigger arguments
- let checkedObj;
- const eventObj: Partial = {
+ let checkedObj: { checked: Key[]; halfChecked: Key[] } | React.Key[];
+
+ const eventObj: Partial> = {
event: 'check',
node: treeNode,
checked,
@@ -616,8 +922,8 @@ class Tree extends React.Component {
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };
eventObj.checkedNodes = checkedKeys
- .map(checkedKey => keyEntities[checkedKey])
- .filter(entity => entity)
+ .map(checkedKey => getEntity(keyEntities, checkedKey))
+ .filter(Boolean)
.map(entity => entity.node);
this.setUncontrolledState({ checkedKeys });
@@ -648,7 +954,7 @@ class Tree extends React.Component {
eventObj.halfCheckedKeys = halfCheckedKeys;
checkedKeys.forEach(checkedKey => {
- const entity = keyEntities[checkedKey];
+ const entity = getEntity(keyEntities, checkedKey);
if (!entity) return;
const { node, pos } = entity;
@@ -657,81 +963,101 @@ class Tree extends React.Component {
eventObj.checkedNodesPositions.push({ node, pos });
});
- this.setUncontrolledState(
- {
- checkedKeys,
- },
- false,
- {
- halfCheckedKeys,
- },
- );
+ this.setUncontrolledState({ checkedKeys }, false, { halfCheckedKeys });
}
- if (onCheck) {
- onCheck(checkedObj, eventObj as CheckInfo);
- }
+ onCheck?.(checkedObj, eventObj as CheckInfo);
};
- onNodeLoad = (treeNode: EventDataNode) =>
- new Promise(resolve => {
+ onNodeLoad = (treeNode: EventDataNode) => {
+ const { key } = treeNode;
+ const { keyEntities } = this.state;
+
+ // Skip if has children already
+ const entity = getEntity(keyEntities, key);
+ if (entity?.children?.length) {
+ return;
+ }
+
+ const loadPromise = new Promise((resolve, reject) => {
// We need to get the latest state of loading/loaded keys
- this.setState(({ loadedKeys = [], loadingKeys = [] }): any => {
+ this.setState(({ loadedKeys = [], loadingKeys = [] }) => {
const { loadData, onLoad } = this.props;
- const { key } = treeNode;
- if (!loadData || loadedKeys.indexOf(key) !== -1 || loadingKeys.indexOf(key) !== -1) {
- // react 15 will warn if return null
- return {};
+ if (!loadData || loadedKeys.includes(key) || loadingKeys.includes(key)) {
+ return null;
}
// Process load data
const promise = loadData(treeNode);
- promise.then(() => {
- const { loadedKeys: currentLoadedKeys, loadingKeys: currentLoadingKeys } = this.state;
- const newLoadedKeys = arrAdd(currentLoadedKeys, key);
- const newLoadingKeys = arrDel(currentLoadingKeys, key);
-
- // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
- // https://github.com/ant-design/ant-design/issues/12464
- if (onLoad) {
- onLoad(newLoadedKeys, {
+ promise
+ .then(() => {
+ const { loadedKeys: currentLoadedKeys } = this.state;
+ const newLoadedKeys = arrAdd(currentLoadedKeys, key);
+
+ // onLoad should trigger before internal setState to avoid `loadData` trigger twice.
+ // https://github.com/ant-design/ant-design/issues/12464
+ onLoad?.(newLoadedKeys, {
event: 'load',
node: treeNode,
});
- }
- this.setUncontrolledState({
- loadedKeys: newLoadedKeys,
- });
- this.setState({
- loadingKeys: newLoadingKeys,
+ this.setUncontrolledState({
+ loadedKeys: newLoadedKeys,
+ });
+ this.setState(prevState => ({
+ loadingKeys: arrDel(prevState.loadingKeys, key),
+ }));
+
+ resolve();
+ })
+ .catch(e => {
+ this.setState(prevState => ({
+ loadingKeys: arrDel(prevState.loadingKeys, key),
+ }));
+
+ // If exceed max retry times, we give up retry
+ this.loadingRetryTimes[key as SafeKey] =
+ (this.loadingRetryTimes[key as SafeKey] || 0) + 1;
+ if (this.loadingRetryTimes[key as SafeKey] >= MAX_RETRY_TIMES) {
+ const { loadedKeys: currentLoadedKeys } = this.state;
+
+ warning(false, 'Retry for `loadData` many times but still failed. No more retry.');
+
+ this.setUncontrolledState({
+ loadedKeys: arrAdd(currentLoadedKeys, key),
+ });
+ resolve();
+ }
+
+ reject(e);
});
- resolve();
- });
-
return {
loadingKeys: arrAdd(loadingKeys, key),
};
});
});
- onNodeMouseEnter: NodeMouseEventHandler = (event, node) => {
+ // Not care warning if we ignore this
+ loadPromise.catch(() => {});
+
+ return loadPromise;
+ };
+
+ onNodeMouseEnter: NodeMouseEventHandler = (event, node) => {
const { onMouseEnter } = this.props;
- if (onMouseEnter) {
- onMouseEnter({ event, node });
- }
+
+ onMouseEnter?.({ event, node });
};
- onNodeMouseLeave: NodeMouseEventHandler = (event, node) => {
+ onNodeMouseLeave: NodeMouseEventHandler = (event, node) => {
const { onMouseLeave } = this.props;
- if (onMouseLeave) {
- onMouseLeave({ event, node });
- }
+
+ onMouseLeave?.({ event, node });
};
- onNodeContextMenu: NodeMouseEventHandler = (event, node) => {
+ onNodeContextMenu: NodeMouseEventHandler = (event, node) => {
const { onRightClick } = this.props;
if (onRightClick) {
event.preventDefault();
@@ -743,9 +1069,7 @@ class Tree extends React.Component {
const { onFocus } = this.props;
this.setState({ focused: true });
- if (onFocus) {
- onFocus(...args);
- }
+ onFocus?.(...args);
};
onBlur: React.FocusEventHandler = (...args) => {
@@ -753,9 +1077,7 @@ class Tree extends React.Component {
this.setState({ focused: false });
this.onActiveChange(null);
- if (onBlur) {
- onBlur(...args);
- }
+ onBlur?.(...args);
};
getTreeNodeRequiredProps = () => {
@@ -779,30 +1101,24 @@ class Tree extends React.Component {
halfCheckedKeys: halfCheckedKeys || [],
dragOverNodeKey,
dropPosition,
- keyEntities,
+ keyEntities: keyEntities,
};
};
// =========================== Expanded ===========================
/** Set uncontrolled `expandedKeys`. This will also auto update `flattenNodes`. */
setExpandedKeys = (expandedKeys: Key[]) => {
- const { treeData } = this.state;
-
- const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys);
- this.setUncontrolledState(
- {
- expandedKeys,
- flattenNodes,
- },
- true,
- );
+ const { treeData, fieldNames } = this.state;
+ const flattenNodes = flattenTreeData(treeData, expandedKeys, fieldNames);
+ this.setUncontrolledState({ expandedKeys, flattenNodes }, true);
};
- onNodeExpand = (e: React.MouseEvent, treeNode: EventDataNode) => {
+ onNodeExpand = (e: React.MouseEvent, treeNode: EventDataNode) => {
let { expandedKeys } = this.state;
- const { listChanging } = this.state;
+ const { listChanging, fieldNames } = this.state;
const { onExpand, loadData } = this.props;
- const { key, expanded } = treeNode;
+ const { expanded } = treeNode;
+ const key = treeNode[fieldNames.key];
// Do nothing when motion is in progress
if (listChanging) {
@@ -810,39 +1126,43 @@ class Tree extends React.Component {
}
// Update selected keys
- const index = expandedKeys.indexOf(key);
+ const certain = expandedKeys.includes(key);
const targetExpanded = !expanded;
warning(
- (expanded && index !== -1) || (!expanded && index === -1),
+ (expanded && certain) || (!expanded && !certain),
'Expand state not sync with index check',
);
- if (targetExpanded) {
- expandedKeys = arrAdd(expandedKeys, key);
- } else {
- expandedKeys = arrDel(expandedKeys, key);
- }
+ expandedKeys = targetExpanded ? arrAdd(expandedKeys, key) : arrDel(expandedKeys, key);
this.setExpandedKeys(expandedKeys);
- if (onExpand) {
- onExpand(expandedKeys, {
- node: treeNode,
- expanded: targetExpanded,
- nativeEvent: e.nativeEvent,
- });
- }
+ onExpand?.(expandedKeys, {
+ node: treeNode,
+ expanded: targetExpanded,
+ nativeEvent: e.nativeEvent,
+ });
// Async Load data
if (targetExpanded && loadData) {
const loadPromise = this.onNodeLoad(treeNode);
if (loadPromise) {
- loadPromise.then(() => {
- // [Legacy] Refresh logic
- const newFlattenTreeData = flattenTreeData(this.state.treeData, expandedKeys);
- this.setUncontrolledState({ flattenNodes: newFlattenTreeData });
- });
+ loadPromise
+ .then(() => {
+ // [Legacy] Refresh logic
+ const newFlattenTreeData = flattenTreeData(
+ this.state.treeData,
+ expandedKeys,
+ fieldNames,
+ );
+ this.setUncontrolledState({ flattenNodes: newFlattenTreeData });
+ })
+ .catch(() => {
+ const { expandedKeys: currentExpandedKeys } = this.state;
+ const expandedKeysToRestore = arrDel(currentExpandedKeys, key);
+ this.setExpandedKeys(expandedKeysToRestore);
+ });
}
}
};
@@ -862,9 +1182,9 @@ class Tree extends React.Component {
};
// =========================== Keyboard ===========================
- onActiveChange = (newActiveKey: Key) => {
+ onActiveChange = (newActiveKey: Key | null) => {
const { activeKey } = this.state;
- const { onActiveChange } = this.props;
+ const { onActiveChange, itemScrollOffset = 0 } = this.props;
if (activeKey === newActiveKey) {
return;
@@ -872,12 +1192,10 @@ class Tree extends React.Component {
this.setState({ activeKey: newActiveKey });
if (newActiveKey !== null) {
- this.scrollTo({ key: newActiveKey });
+ this.scrollTo({ key: newActiveKey, offset: itemScrollOffset });
}
- if (onActiveChange) {
- onActiveChange(newActiveKey);
- }
+ onActiveChange?.(newActiveKey);
};
getActiveItem = () => {
@@ -886,13 +1204,13 @@ class Tree extends React.Component {
return null;
}
- return flattenNodes.find(({ data: { key } }) => key === activeKey) || null;
+ return flattenNodes.find(({ key }) => key === activeKey) || null;
};
offsetActiveKey = (offset: number) => {
const { flattenNodes, activeKey } = this.state;
- let index = flattenNodes.findIndex(({ data: { key } }) => key === activeKey);
+ let index = flattenNodes.findIndex(({ key }) => key === activeKey);
// Align with index
if (index === -1 && offset < 0) {
@@ -903,7 +1221,7 @@ class Tree extends React.Component {
const item = flattenNodes[index];
if (item) {
- const { key } = item.data;
+ const { key } = item;
this.onActiveChange(key);
} else {
this.onActiveChange(null);
@@ -911,7 +1229,7 @@ class Tree extends React.Component {
};
onKeyDown: React.KeyboardEventHandler = event => {
- const { activeKey, expandedKeys, checkedKeys } = this.state;
+ const { activeKey, expandedKeys, checkedKeys, fieldNames } = this.state;
const { onKeyDown, checkable, selectable } = this.props;
// >>>>>>>>>> Direction
@@ -934,8 +1252,8 @@ class Tree extends React.Component {
const treeNodeRequiredProps = this.getTreeNodeRequiredProps();
const expandable =
- activeItem.data.isLeaf === false || !!(activeItem.data.children || []).length;
- const eventNode = convertNodePropsToEventData({
+ activeItem.data.isLeaf === false || !!(activeItem.data[fieldNames.children] || []).length;
+ const eventNode = convertNodePropsToEventData({
...getTreeNodeProps(activeKey, treeNodeRequiredProps),
data: activeItem.data,
active: true,
@@ -948,7 +1266,7 @@ class Tree extends React.Component {
if (expandable && expandedKeys.includes(activeKey)) {
this.onNodeExpand({} as React.MouseEvent, eventNode);
} else if (activeItem.parent) {
- this.onActiveChange(activeItem.parent.data.key);
+ this.onActiveChange(activeItem.parent.key);
}
event.preventDefault();
break;
@@ -958,7 +1276,7 @@ class Tree extends React.Component {
if (expandable && !expandedKeys.includes(activeKey)) {
this.onNodeExpand({} as React.MouseEvent, eventNode);
} else if (activeItem.children && activeItem.children.length) {
- this.onActiveChange(activeItem.children[0].data.key);
+ this.onActiveChange(activeItem.children[0].key);
}
event.preventDefault();
break;
@@ -991,42 +1309,38 @@ class Tree extends React.Component {
}
}
- if (onKeyDown) {
- onKeyDown(event);
- }
+ onKeyDown?.(event);
};
/**
* Only update the value which is not in props
*/
setUncontrolledState = (
- state: Partial,
+ state: Partial>,
atomic: boolean = false,
- forceState: Partial | null = null,
+ forceState: Partial> | null = null,
) => {
- if (this.destroyed) {
- return;
- }
+ if (!this.destroyed) {
+ let needSync = false;
+ let allPassed = true;
+ const newState = {};
+
+ Object.keys(state).forEach(name => {
+ if (this.props.hasOwnProperty(name)) {
+ allPassed = false;
+ return;
+ }
- let needSync = false;
- let allPassed = true;
- const newState = {};
+ needSync = true;
+ newState[name] = state[name];
+ });
- Object.keys(state).forEach(name => {
- if (name in this.props) {
- allPassed = false;
- return;
+ if (needSync && (!atomic || allPassed)) {
+ this.setState({
+ ...newState,
+ ...forceState,
+ } as TreeState);
}
-
- needSync = true;
- newState[name] = state[name];
- });
-
- if (needSync && (!atomic || allPassed)) {
- this.setState({
- ...newState,
- ...forceState,
- } as TreeState);
}
};
@@ -1035,11 +1349,25 @@ class Tree extends React.Component {
};
render() {
- const { focused, flattenNodes, keyEntities, dragging, activeKey } = this.state;
+ const {
+ focused,
+ flattenNodes,
+ keyEntities,
+ draggingNodeKey,
+ activeKey,
+ dropLevelOffset,
+ dropContainerKey,
+ dropTargetKey,
+ dropPosition,
+ dragOverNodeKey,
+ indent,
+ } = this.state;
const {
prefixCls,
className,
style,
+ styles,
+ classNames: treeClassNames,
showLine,
focusable,
tabIndex = 0,
@@ -1056,54 +1384,87 @@ class Tree extends React.Component {
filterTreeNode,
height,
itemHeight,
+ scrollWidth,
virtual,
titleRender,
+ dropIndicatorRender,
onContextMenu,
+ onScroll,
+ direction,
+ rootClassName,
+ rootStyle,
} = this.props;
- const domProps: React.HTMLAttributes = getDataAndAria(this.props);
- return (
- = pickAttrs(this.props, {
+ aria: true,
+ data: true,
+ });
+
+ // It's better move to hooks but we just simply keep here
+ let draggableConfig: DraggableConfig;
+ if (draggable) {
+ if (typeof draggable === 'object') {
+ draggableConfig = draggable;
+ } else if (typeof draggable === 'function') {
+ draggableConfig = {
+ nodeDraggable: draggable,
+ };
+ } else {
+ draggableConfig = {};
+ }
+ }
+
+ const contextValue = {
+ styles,
+ classNames: treeClassNames,
+ prefixCls,
+ selectable,
+ showIcon,
+ icon,
+ switcherIcon,
+ draggable: draggableConfig,
+ draggingNodeKey,
+ checkable,
+ checkStrictly,
+ disabled,
+ keyEntities,
+ dropLevelOffset,
+ dropContainerKey,
+ dropTargetKey,
+ dropPosition,
+ dragOverNodeKey,
+ indent,
+ direction,
+ dropIndicatorRender,
+ loadData,
+ filterTreeNode,
+ titleRender,
+ onNodeClick: this.onNodeClick,
+ onNodeDoubleClick: this.onNodeDoubleClick,
+ onNodeExpand: this.onNodeExpand,
+ onNodeSelect: this.onNodeSelect,
+ onNodeCheck: this.onNodeCheck,
+ onNodeLoad: this.onNodeLoad,
+ onNodeMouseEnter: this.onNodeMouseEnter,
+ onNodeMouseLeave: this.onNodeMouseLeave,
+ onNodeContextMenu: this.onNodeContextMenu,
+ onNodeDragStart: this.onNodeDragStart,
+ onNodeDragEnter: this.onNodeDragEnter,
+ onNodeDragOver: this.onNodeDragOver,
+ onNodeDragLeave: this.onNodeDragLeave,
+ onNodeDragEnd: this.onNodeDragEnd,
+ onNodeDrop: this.onNodeDrop,
+ };
- loadData,
- filterTreeNode,
-
- titleRender,
-
- onNodeClick: this.onNodeClick,
- onNodeDoubleClick: this.onNodeDoubleClick,
- onNodeExpand: this.onNodeExpand,
- onNodeSelect: this.onNodeSelect,
- onNodeCheck: this.onNodeCheck,
- onNodeLoad: this.onNodeLoad,
- onNodeMouseEnter: this.onNodeMouseEnter,
- onNodeMouseLeave: this.onNodeMouseLeave,
- onNodeContextMenu: this.onNodeContextMenu,
- onNodeDragStart: this.onNodeDragStart,
- onNodeDragEnter: this.onNodeDragEnter,
- onNodeDragOver: this.onNodeDragOver,
- onNodeDragLeave: this.onNodeDragLeave,
- onNodeDragEnd: this.onNodeDragEnd,
- onNodeDrop: this.onNodeDrop,
- }}
- >
+ return (
+
{
selectable={selectable}
checkable={!!checkable}
motion={motion}
- dragging={dragging}
+ dragging={draggingNodeKey !== null}
height={height}
itemHeight={itemHeight}
virtual={virtual}
@@ -1129,6 +1490,8 @@ class Tree extends React.Component {
onListChangeStart={this.onListChangeStart}
onListChangeEnd={this.onListChangeEnd}
onContextMenu={onContextMenu}
+ onScroll={onScroll}
+ scrollWidth={scrollWidth}
{...this.getTreeNodeRequiredProps()}
{...domProps}
/>
diff --git a/src/TreeNode.tsx b/src/TreeNode.tsx
index 8e4340286..06c648e77 100644
--- a/src/TreeNode.tsx
+++ b/src/TreeNode.tsx
@@ -1,10 +1,10 @@
-import * as React from 'react';
+import React from 'react';
import classNames from 'classnames';
-// @ts-ignore
-import { TreeContext, TreeContextProps } from './contextTypes';
-import { getDataAndAria } from './util';
-import { IconType, Key, DataNode } from './interface';
+import pickAttrs from '@rc-component/util/lib/pickAttrs';
+import { TreeContext, UnstableContext } from './contextTypes';
import Indent from './Indent';
+import type { TreeNodeProps } from './interface';
+import getEntity from './utils/keyUtil';
import { convertNodePropsToEventData } from './utils/treeUtil';
const ICON_OPEN = 'open';
@@ -12,530 +12,461 @@ const ICON_CLOSE = 'close';
const defaultTitle = '---';
-export interface TreeNodeProps {
- eventKey?: Key; // Pass by parent `cloneElement`
- prefixCls?: string;
- className?: string;
- style?: React.CSSProperties;
-
- // By parent
- expanded?: boolean;
- selected?: boolean;
- checked?: boolean;
- loaded?: boolean;
- loading?: boolean;
- halfChecked?: boolean;
- title?: React.ReactNode | ((data: DataNode) => React.ReactNode);
- dragOver?: boolean;
- dragOverGapTop?: boolean;
- dragOverGapBottom?: boolean;
- pos?: string;
- domRef?: React.Ref;
- /** New added in Tree for easy data access */
- data?: DataNode;
- isStart?: boolean[];
- isEnd?: boolean[];
- active?: boolean;
- onMouseMove?: React.MouseEventHandler;
-
- // By user
- isLeaf?: boolean;
- checkable?: boolean;
- selectable?: boolean;
- disabled?: boolean;
- disableCheckbox?: boolean;
- icon?: IconType;
- switcherIcon?: IconType;
- children?: React.ReactNode;
-}
-
-export interface InternalTreeNodeProps extends TreeNodeProps {
- context?: TreeContextProps;
-}
-
-export interface TreeNodeState {
- dragNodeHighlight: boolean;
-}
+export type { TreeNodeProps } from './interface';
+
+const TreeNode: React.FC> = props => {
+ const {
+ eventKey,
+ className,
+ style,
+ dragOver,
+ dragOverGapTop,
+ dragOverGapBottom,
+ isLeaf,
+ isStart,
+ isEnd,
+ expanded,
+ selected,
+ checked,
+ halfChecked,
+ loading,
+ domRef,
+ active,
+ data,
+ onMouseMove,
+ selectable,
+ ...otherProps
+ } = props;
+
+ const context = React.useContext(TreeContext);
+ const { classNames: treeClassNames, styles } = context || {};
+
+ const unstableContext = React.useContext(UnstableContext);
+
+ const selectHandleRef = React.useRef(null);
+
+ const [dragNodeHighlight, setDragNodeHighlight] = React.useState(false);
+
+ // ======= State: Disabled State =======
+ const isDisabled = !!(context.disabled || props.disabled || unstableContext.nodeDisabled?.(data));
+
+ const isCheckable = React.useMemo(() => {
+ // Return false if tree or treeNode is not checkable
+ if (!context.checkable || props.checkable === false) {
+ return false;
+ }
+ return context.checkable;
+ }, [context.checkable, props.checkable]);
-class InternalTreeNode extends React.Component {
- public state = {
- dragNodeHighlight: false,
+ // ======= Event Handlers: Selection and Check =======
+ const onSelect = (e: React.MouseEvent) => {
+ if (isDisabled) {
+ return;
+ }
+ context.onNodeSelect(e, convertNodePropsToEventData(props));
};
- public selectHandle: HTMLSpanElement;
-
- // Isomorphic needn't load data in server side
- componentDidMount() {
- this.syncLoadData(this.props);
- }
+ const onCheck = (e: React.MouseEvent) => {
+ if (isDisabled) {
+ return;
+ }
+ if (!isCheckable || props.disableCheckbox) {
+ return;
+ }
+ context.onNodeCheck(e, convertNodePropsToEventData(props), !checked);
+ };
- componentDidUpdate() {
- this.syncLoadData(this.props);
- }
+ // ======= State: Selectable Check =======
+ const isSelectable = React.useMemo(() => {
+ // Ignore when selectable is undefined or null
+ if (typeof selectable === 'boolean') {
+ return selectable;
+ }
+ return context.selectable;
+ }, [selectable, context.selectable]);
- onSelectorClick = (e: React.MouseEvent) => {
+ const onSelectorClick = (e: React.MouseEvent) => {
// Click trigger before select/check operation
- const {
- context: { onNodeClick },
- } = this.props;
- onNodeClick(e, convertNodePropsToEventData(this.props));
-
- if (this.isSelectable()) {
- this.onSelect(e);
+ context.onNodeClick(e, convertNodePropsToEventData(props));
+ if (isSelectable) {
+ onSelect(e);
} else {
- this.onCheck(e);
+ onCheck(e);
}
};
- onSelectorDoubleClick = (e: React.MouseEvent) => {
- const {
- context: { onNodeDoubleClick },
- } = this.props;
- onNodeDoubleClick(e, convertNodePropsToEventData(this.props));
- };
-
- onSelect = (e: React.MouseEvent) => {
- if (this.isDisabled()) return;
-
- const {
- context: { onNodeSelect },
- } = this.props;
- e.preventDefault();
- onNodeSelect(e, convertNodePropsToEventData(this.props));
- };
-
- onCheck = (e: React.MouseEvent) => {
- if (this.isDisabled()) return;
-
- const { disableCheckbox, checked } = this.props;
- const {
- context: { onNodeCheck },
- } = this.props;
-
- if (!this.isCheckable() || disableCheckbox) return;
-
- e.preventDefault();
- const targetChecked = !checked;
- onNodeCheck(e, convertNodePropsToEventData(this.props), targetChecked);
+ const onSelectorDoubleClick = (e: React.MouseEvent) => {
+ context.onNodeDoubleClick(e, convertNodePropsToEventData(props));
};
- onMouseEnter = (e: React.MouseEvent) => {
- const {
- context: { onNodeMouseEnter },
- } = this.props;
- onNodeMouseEnter(e, convertNodePropsToEventData(this.props));
+ const onMouseEnter = (e: React.MouseEvent) => {
+ context.onNodeMouseEnter(e, convertNodePropsToEventData(props));
};
- onMouseLeave = (e: React.MouseEvent) => {
- const {
- context: { onNodeMouseLeave },
- } = this.props;
- onNodeMouseLeave(e, convertNodePropsToEventData(this.props));
+ const onMouseLeave = (e: React.MouseEvent) => {
+ context.onNodeMouseLeave(e, convertNodePropsToEventData(props));
};
- onContextMenu = (e: React.MouseEvent) => {
- const {
- context: { onNodeContextMenu },
- } = this.props;
- onNodeContextMenu(e, convertNodePropsToEventData(this.props));
+ const onContextMenu = (e: React.MouseEvent) => {
+ context.onNodeContextMenu(e, convertNodePropsToEventData(props));
};
- onDragStart = (e: React.DragEvent) => {
- const {
- context: { onNodeDragStart },
- } = this.props;
+ // ======= Drag: Drag Enabled =======
+ const isDraggable = React.useMemo(() => {
+ return !!(
+ context.draggable &&
+ (!context.draggable.nodeDraggable || context.draggable.nodeDraggable(data))
+ );
+ }, [context.draggable, data]);
+ // ======= Drag: Drag Event Handlers =======
+ const onDragStart = (e: React.DragEvent) => {
e.stopPropagation();
- this.setState({
- dragNodeHighlight: true,
- });
- onNodeDragStart(e, this);
-
+ setDragNodeHighlight(true);
+ context.onNodeDragStart(e, props);
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '');
- } catch (error) {
+ } catch {
// empty
}
};
- onDragEnter = (e: React.DragEvent) => {
- const {
- context: { onNodeDragEnter },
- } = this.props;
-
+ const onDragEnter = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
- onNodeDragEnter(e, this);
+ context.onNodeDragEnter(e, props);
};
- onDragOver = (e: React.DragEvent) => {
- const {
- context: { onNodeDragOver },
- } = this.props;
-
+ const onDragOver = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
- onNodeDragOver(e, this);
+ context.onNodeDragOver(e, props);
};
- onDragLeave = (e: React.DragEvent) => {
- const {
- context: { onNodeDragLeave },
- } = this.props;
-
+ const onDragLeave = (e: React.DragEvent) => {
e.stopPropagation();
- onNodeDragLeave(e, this);
+ context.onNodeDragLeave(e, props);
};
- onDragEnd = (e: React.DragEvent) => {
- const {
- context: { onNodeDragEnd },
- } = this.props;
-
+ const onDragEnd = (e: React.DragEvent) => {
e.stopPropagation();
- this.setState({
- dragNodeHighlight: false,
- });
- onNodeDragEnd(e, this);
+ setDragNodeHighlight(false);
+ context.onNodeDragEnd(e, props);
};
- onDrop = (e: React.DragEvent) => {
- const {
- context: { onNodeDrop },
- } = this.props;
-
+ const onDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
- this.setState({
- dragNodeHighlight: false,
- });
- onNodeDrop(e, this);
- };
-
- // Disabled item still can be switch
- onExpand: React.MouseEventHandler = e => {
- const {
- context: { onNodeExpand },
- } = this.props;
- onNodeExpand(e, convertNodePropsToEventData(this.props));
- };
-
- // Drag usage
- setSelectHandle = node => {
- this.selectHandle = node;
+ setDragNodeHighlight(false);
+ context.onNodeDrop(e, props);
};
- getNodeState = () => {
- const { expanded } = this.props;
-
- if (this.isLeaf()) {
- return null;
+ // ======= Expand: Node Expansion =======
+ const onExpand: React.MouseEventHandler = e => {
+ if (loading) {
+ return;
}
-
- return expanded ? ICON_OPEN : ICON_CLOSE;
- };
-
- hasChildren = () => {
- const { eventKey } = this.props;
- const {
- context: { keyEntities },
- } = this.props;
- const { children } = keyEntities[eventKey] || {};
-
- return !!(children || []).length;
+ context.onNodeExpand(e, convertNodePropsToEventData(props));
};
- isLeaf = () => {
- const { isLeaf, loaded } = this.props;
- const {
- context: { loadData },
- } = this.props;
-
- const hasChildren = this.hasChildren();
+ // ======= State: Has Children =======
+ const hasChildren = React.useMemo(() => {
+ const { children } = getEntity(context.keyEntities, eventKey) || {};
+ return Boolean((children || []).length);
+ }, [context.keyEntities, eventKey]);
+ // ======= State: Leaf Check =======
+ const memoizedIsLeaf = React.useMemo(() => {
if (isLeaf === false) {
return false;
}
+ return (
+ isLeaf ||
+ (!context.loadData && !hasChildren) ||
+ (context.loadData && props.loaded && !hasChildren)
+ );
+ }, [isLeaf, context.loadData, hasChildren, props.loaded]);
- return isLeaf || (!loadData && !hasChildren) || (loadData && loaded && !hasChildren);
- };
-
- isDisabled = () => {
- const { disabled } = this.props;
- const {
- context: { disabled: treeDisabled },
- } = this.props;
-
- return !!(treeDisabled || disabled);
- };
-
- isCheckable = () => {
- const { checkable } = this.props;
- const {
- context: { checkable: treeCheckable },
- } = this.props;
-
- // Return false if tree or treeNode is not checkable
- if (!treeCheckable || checkable === false) return false;
- return treeCheckable;
- };
-
- // Load data to avoid default expanded tree without data
- syncLoadData = props => {
- const { expanded, loading, loaded } = props;
- const {
- context: { loadData, onNodeLoad },
- } = this.props;
-
- if (loading) return;
-
+ // ============== Effect ==============
+ React.useEffect(() => {
+ // Load data to avoid default expanded tree without data
+ if (loading) {
+ return;
+ }
// read from state to avoid loadData at same time
- if (loadData && expanded && !this.isLeaf()) {
+ if (typeof context.loadData === 'function' && expanded && !memoizedIsLeaf && !props.loaded) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
- if (!this.hasChildren() && !loaded) {
- onNodeLoad(convertNodePropsToEventData(this.props));
- }
+ context.onNodeLoad(convertNodePropsToEventData(props));
}
- };
+ }, [loading, context.loadData, context.onNodeLoad, expanded, memoizedIsLeaf, props]);
- isSelectable() {
- const { selectable } = this.props;
- const {
- context: { selectable: treeSelectable },
- } = this.props;
-
- // Ignore when selectable is undefined or null
- if (typeof selectable === 'boolean') {
- return selectable;
+ // ==================== Render: Drag Handler ====================
+ const dragHandlerNode = React.useMemo(() => {
+ if (!context.draggable?.icon) {
+ return null;
}
-
- return treeSelectable;
- }
+ return {context.draggable.icon};
+ }, [context.draggable]);
+
+ // ====================== Render: Switcher ======================
+ const renderSwitcherIconDom = (isInternalLeaf: boolean) => {
+ const switcherIcon = props.switcherIcon || context.switcherIcon;
+ // if switcherIconDom is null, no render switcher span
+ if (typeof switcherIcon === 'function') {
+ return switcherIcon({ ...props, isLeaf: isInternalLeaf });
+ }
+ return switcherIcon;
+ };
// Switcher
- renderSwitcher = () => {
- const { expanded, switcherIcon: switcherIconFromProps } = this.props;
- const {
- context: { prefixCls, switcherIcon: switcherIconFromCtx },
- } = this.props;
-
- const switcherIcon = switcherIconFromProps || switcherIconFromCtx;
-
- if (this.isLeaf()) {
- return (
-
- {typeof switcherIcon === 'function'
- ? switcherIcon({ ...this.props, isLeaf: true })
- : switcherIcon}
+ const renderSwitcher = () => {
+ if (memoizedIsLeaf) {
+ // if switcherIconDom is null, no render switcher span
+ const switcherIconDom = renderSwitcherIconDom(true);
+ return switcherIconDom !== false ? (
+
+ {switcherIconDom}
- );
+ ) : null;
}
-
- const switcherCls = classNames(
- `${prefixCls}-switcher`,
- `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
- );
- return (
-
- {typeof switcherIcon === 'function'
- ? switcherIcon({ ...this.props, isLeaf: false })
- : switcherIcon}
+ const switcherIconDom = renderSwitcherIconDom(false);
+ return switcherIconDom !== false ? (
+
+ {switcherIconDom}
- );
+ ) : null;
};
- // Checkbox
- renderCheckbox = () => {
- const { checked, halfChecked, disableCheckbox } = this.props;
- const {
- context: { prefixCls },
- } = this.props;
- const disabled = this.isDisabled();
- const checkable = this.isCheckable();
-
- if (!checkable) return null;
+ // ====================== Checkbox ======================
+ const checkboxNode = React.useMemo(() => {
+ if (!isCheckable) {
+ return null;
+ }
// [Legacy] Custom element should be separate with `checkable` in future
- const $custom = typeof checkable !== 'boolean' ? checkable : null;
+ const $custom = typeof isCheckable !== 'boolean' ? isCheckable : null;
return (
{$custom}
);
- };
+ }, [isCheckable, checked, halfChecked, isDisabled, props.disableCheckbox, props.title]);
- renderIcon = () => {
- const { loading } = this.props;
- const {
- context: { prefixCls },
- } = this.props;
+ // ============== State: Node State (Open/Close) ==============
+ const nodeState = React.useMemo(() => {
+ if (memoizedIsLeaf) {
+ return null;
+ }
+ return expanded ? ICON_OPEN : ICON_CLOSE;
+ }, [memoizedIsLeaf, expanded]);
+ // ==================== Render: Title + Icon ====================
+ const iconNode = React.useMemo(() => {
return (
);
- };
+ }, [context.prefixCls, nodeState, loading]);
+
+ // =================== Drop Indicator ===================
+ const dropIndicatorNode = React.useMemo(() => {
+ const rootDraggable = Boolean(context.draggable);
+ // allowDrop is calculated in Tree.tsx, there is no need for calc it here
+ const showIndicator = !props.disabled && rootDraggable && context.dragOverNodeKey === eventKey;
+ if (!showIndicator) {
+ return null;
+ }
+ return context.dropIndicatorRender({
+ dropPosition: context.dropPosition,
+ dropLevelOffset: context.dropLevelOffset,
+ indent: context.indent,
+ prefixCls: context.prefixCls,
+ direction: context.direction,
+ });
+ }, [
+ context.dropPosition,
+ context.dropLevelOffset,
+ context.indent,
+ context.prefixCls,
+ context.direction,
+ context.draggable,
+ context.dragOverNodeKey,
+ context.dropIndicatorRender,
+ ]);
// Icon + Title
- renderSelector = () => {
- const { dragNodeHighlight } = this.state;
- const { title, selected, icon, loading, data } = this.props;
- const {
- context: { prefixCls, showIcon, icon: treeIcon, draggable, loadData, titleRender },
- } = this.props;
- const disabled = this.isDisabled();
+ const selectorNode = React.useMemo(() => {
+ const { title = defaultTitle } = props;
- const wrapClass = `${prefixCls}-node-content-wrapper`;
+ const wrapClass = `${context.prefixCls}-node-content-wrapper`;
// Icon - Still show loading icon when loading without showIcon
- let $icon;
+ let $icon: React.ReactNode;
- if (showIcon) {
- const currentIcon = icon || treeIcon;
+ if (context.showIcon) {
+ const currentIcon = props.icon || context.icon;
$icon = currentIcon ? (
-
- {typeof currentIcon === 'function' ? currentIcon(this.props) : currentIcon}
+
+ {typeof currentIcon === 'function' ? currentIcon(props) : currentIcon}
) : (
- this.renderIcon()
+ iconNode
);
- } else if (loadData && loading) {
- $icon = this.renderIcon();
+ } else if (context.loadData && loading) {
+ $icon = iconNode;
}
// Title
let titleNode: React.ReactNode;
if (typeof title === 'function') {
titleNode = title(data);
- } else if (titleRender) {
- titleNode = titleRender(data);
+ } else if (context.titleRender) {
+ titleNode = context.titleRender(data);
} else {
titleNode = title;
}
- const $title = {titleNode};
-
return (
{$icon}
- {$title}
+
+ {titleNode}
+
+ {dropIndicatorNode}
);
- };
-
- render() {
- const {
- eventKey,
- className,
- style,
- dragOver,
- dragOverGapTop,
- dragOverGapBottom,
- isLeaf,
- isStart,
- isEnd,
- expanded,
- selected,
- checked,
- halfChecked,
- loading,
- domRef,
- active,
- onMouseMove,
- ...otherProps
- } = this.props;
- const {
- context: { prefixCls, filterTreeNode, draggable, keyEntities },
- } = this.props;
- const disabled = this.isDisabled();
- const dataOrAriaAttributeProps = getDataAndAria(otherProps);
- const { level } = keyEntities[eventKey] || {};
- const isEndNode = isEnd[isEnd.length - 1];
- return (
-
-
- {this.renderSwitcher()}
- {this.renderCheckbox()}
- {this.renderSelector()}
-
- );
- }
-}
-
-const ContextTreeNode: React.FC = props => (
-
- {context => }
-
-);
-
-ContextTreeNode.displayName = 'TreeNode';
-
-ContextTreeNode.defaultProps = {
- title: defaultTitle,
+ }, [
+ context.prefixCls,
+ context.showIcon,
+ props,
+ context.icon,
+ iconNode,
+ context.titleRender,
+ data,
+ nodeState,
+ onMouseEnter,
+ onMouseLeave,
+ onContextMenu,
+ onSelectorClick,
+ onSelectorDoubleClick,
+ ]);
+
+ const dataOrAriaAttributeProps = pickAttrs(otherProps, { aria: true, data: true });
+
+ const { level } = getEntity(context.keyEntities, eventKey) || {};
+
+ const isEndNode = isEnd[isEnd.length - 1];
+
+ const draggableWithoutDisabled = !isDisabled && isDraggable;
+
+ const dragging = context.draggingNodeKey === eventKey;
+ const ariaSelected = selectable !== undefined ? { 'aria-selected': !!selectable } : undefined;
+ return (
+
+
+ {dragHandlerNode}
+ {renderSwitcher()}
+ {checkboxNode}
+ {selectorNode}
+
+ );
};
-(ContextTreeNode as any).isTreeNode = 1;
+(TreeNode as any).isTreeNode = 1;
-export { InternalTreeNode };
+if (process.env.NODE_ENV !== 'production') {
+ TreeNode.displayName = 'TreeNode';
+}
-export default ContextTreeNode;
+export default TreeNode;
diff --git a/src/contextTypes.ts b/src/contextTypes.ts
index f880b01b3..b41e037a4 100644
--- a/src/contextTypes.ts
+++ b/src/contextTypes.ts
@@ -3,61 +3,98 @@
* When util.js imports the TreeNode for tree generate will cause treeContextTypes be empty.
*/
import * as React from 'react';
-import { IconType, Key, DataEntity, EventDataNode, NodeInstance, DataNode } from './interface';
+import type {
+ BasicDataNode,
+ DataNode,
+ Direction,
+ EventDataNode,
+ IconType,
+ Key,
+ KeyEntities,
+ TreeNodeProps,
+} from './interface';
+import type { DraggableConfig, SemanticName } from './Tree';
-export type NodeMouseEventParams = {
+export type NodeMouseEventParams<
+ TreeDataType extends BasicDataNode = DataNode,
+ T = HTMLSpanElement,
+> = {
event: React.MouseEvent;
- node: EventDataNode;
+ node: EventDataNode;
};
-export type NodeDragEventParams = {
- event: React.MouseEvent;
- node: EventDataNode;
+export type NodeDragEventParams<
+ TreeDataType extends BasicDataNode = DataNode,
+ T = HTMLDivElement,
+> = {
+ event: React.DragEvent;
+ node: EventDataNode;
};
-export type NodeMouseEventHandler = (
- e: React.MouseEvent,
- node: EventDataNode,
-) => void;
-export type NodeDragEventHandler = (
- e: React.MouseEvent,
- node: NodeInstance,
-) => void;
+export type NodeMouseEventHandler<
+ TreeDataType extends BasicDataNode = DataNode,
+ T = HTMLSpanElement,
+> = (e: React.MouseEvent, node: EventDataNode) => void;
+export type NodeDragEventHandler<
+ TreeDataType extends BasicDataNode = DataNode,
+ T = HTMLDivElement,
+> = (e: React.DragEvent, nodeProps: TreeNodeProps, outsideTree?: boolean) => void;
-export interface TreeContextProps {
+export interface TreeContextProps {
+ styles?: Partial>;
+ classNames?: Partial>;
prefixCls: string;
selectable: boolean;
showIcon: boolean;
icon: IconType;
switcherIcon: IconType;
- draggable: boolean;
+ draggable?: DraggableConfig;
+ draggingNodeKey?: Key;
checkable: boolean | React.ReactNode;
checkStrictly: boolean;
disabled: boolean;
- keyEntities: Record;
+ keyEntities: KeyEntities;
+ // for details see comment in Tree.state (Tree.tsx)
+ dropLevelOffset?: number;
+ dropContainerKey: Key | null;
+ dropTargetKey: Key | null;
+ dropPosition: -1 | 0 | 1 | null;
+ indent: number | null;
+ dropIndicatorRender: (props: {
+ dropPosition: -1 | 0 | 1;
+ dropLevelOffset: number;
+ indent: number;
+ prefixCls: string;
+ direction: Direction;
+ }) => React.ReactNode;
+ dragOverNodeKey: Key | null;
+ direction: Direction;
- loadData: (treeNode: EventDataNode) => Promise;
- filterTreeNode: (treeNode: EventDataNode) => boolean;
- titleRender?: (node: DataNode) => React.ReactNode;
+ loadData: (treeNode: EventDataNode) => Promise;
+ filterTreeNode: (treeNode: EventDataNode) => boolean;
+ titleRender?: (node: any) => React.ReactNode;
- onNodeClick: NodeMouseEventHandler;
- onNodeDoubleClick: NodeMouseEventHandler;
- onNodeExpand: NodeMouseEventHandler;
- onNodeSelect: NodeMouseEventHandler;
+ onNodeClick: NodeMouseEventHandler;
+ onNodeDoubleClick: NodeMouseEventHandler;
+ onNodeExpand: NodeMouseEventHandler;
+ onNodeSelect: NodeMouseEventHandler;
onNodeCheck: (
e: React.MouseEvent,
- treeNode: EventDataNode,
+ treeNode: EventDataNode,
checked: boolean,
) => void;
- onNodeLoad: (treeNode: EventDataNode) => void;
- onNodeMouseEnter: NodeMouseEventHandler;
- onNodeMouseLeave: NodeMouseEventHandler;
- onNodeContextMenu: NodeMouseEventHandler;
- onNodeDragStart: NodeDragEventHandler;
- onNodeDragEnter: NodeDragEventHandler;
- onNodeDragOver: NodeDragEventHandler;
- onNodeDragLeave: NodeDragEventHandler;
- onNodeDragEnd: NodeDragEventHandler;
- onNodeDrop: NodeDragEventHandler;
+ onNodeLoad: (treeNode: EventDataNode) => void;
+ onNodeMouseEnter: NodeMouseEventHandler;
+ onNodeMouseLeave: NodeMouseEventHandler;
+ onNodeContextMenu: NodeMouseEventHandler;
+ onNodeDragStart: NodeDragEventHandler;
+ onNodeDragEnter: NodeDragEventHandler;
+ onNodeDragOver: NodeDragEventHandler;
+ onNodeDragLeave: NodeDragEventHandler;
+ onNodeDragEnd: NodeDragEventHandler;
+ onNodeDrop: NodeDragEventHandler;
}
-export const TreeContext: React.Context = React.createContext(null);
+export const TreeContext = React.createContext