diff --git a/.eslintrc.js b/.eslintrc.js
index 9bb4e75ca..a491cd868 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,13 +1,19 @@
-const base = require('@umijs/fabric/dist/eslint');
-
 module.exports = {
-  ...base,
+  extends: [require.resolve('@umijs/fabric/dist/eslint')],
+
   rules: {
-    ...base.rules,
+    'react/sort-comp': 0,
+    'default-case': 0,
     '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,
   },
-};
\ No newline at end of file
+};
diff --git a/.fatherrc.js b/.fatherrc.js
index 767a2abfe..96268ae1e 100644
--- a/.fatherrc.js
+++ b/.fatherrc.js
@@ -1,8 +1,5 @@
-export default {
-  cjs: 'babel',
-  esm: { type: 'babel', importLibToEs: true },
-  preCommit: {
-    eslint: true,
-    prettier: 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 a40be9811..8aae36ae1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .storybook
+.vscode
 *.iml
 *.log
 .doc/
@@ -31,3 +32,11 @@ lib
 /coverage
 yarn.lock
 es/
+!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
new file mode 100644
index 000000000..b04278194
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,10 @@
+{
+  "endOfLine": "lf",
+  "semi": true,
+  "singleQuote": true,
+  "tabWidth": 2,
+  "trailingComma": "all",
+  "proseWrap": "never",
+  "printWidth": 100,
+  "arrowParens": "avoid"
+}
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 5919a533b..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-language: node_js
-
-sudo: false
-
-notifications:
-  email:
-  - hualei5280@gmail.com
-
-node_js:
-- 10
-
-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 92%
rename from HISTORY.md
rename to CHANGELOG.md
index 407250b17..1b34e8905 100644
--- a/HISTORY.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
-# History
----
+# Changelog
+
+- https://github.com/react-component/tree/releases
+
+## 3.2.0 `2020-05-08`
+
+- Upgrade `rc-animate`.
+- Remove `react-lifecycles-compat` and `prop-types`.
 
 ## 2.1.0 / 2019-04-28
 
diff --git a/README.md b/README.md
index a4df01724..d56dd7509 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,30 @@
 # rc-tree
 
----
+Tree component.
 
-tree ui component for react
-
-[![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] [![Test coverage][codecov-image]][codecov-url] [![gemnasium deps][gemnasium-image]][gemnasium-url] [![npm download][download-image]][download-url]
+[![NPM version][npm-image]][npm-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
+[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/branch/master
-[gemnasium-image]: http://img.shields.io/gemnasium/react-component/tree.svg?style=flat-square
-[gemnasium-url]: https://gemnasium.com/react-component/tree
-[node-image]: https://img.shields.io/badge/node.js-%3E=_0.10-green.svg?style=flat-square
-[node-url]: http://nodejs.org/download/
+[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
+[david-dev-image]: https://david-dm.org/react-component/tree/dev-status.svg?style=flat-square
 [download-image]: https://img.shields.io/npm/dm/rc-tree.svg?style=flat-square
 [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
 
@@ -29,15 +36,16 @@ tree ui component for react
 
 ## Example
 
-http://localhost:8018/examples/
+http://localhost:9001/
 
-online example: http://react-component.github.io/tree/examples/
+online example: https://tree.react-component.now.sh/
 
-## install
+## Install
 
 [![rc-tree](https://nodei.co/npm/rc-tree.png)](https://npmjs.org/package/rc-tree)
 
 ## Usage
+> Note: `import "rc-tree/assets/index.css"`
 
 see examples
 
@@ -53,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' |
@@ -85,6 +93,11 @@ see examples
 | onRightClick | select current treeNode and show customized contextmenu | function({event,node}) | - |
 | 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
 
@@ -95,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 |
@@ -105,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 ? <Tree ...>{this.state.treeData.map(t => <TreeNode ... />)}</Tree> : 'loading tree'}`
 
@@ -128,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 9b99ae2da..b449d4b95 100644
--- a/assets/index.less
+++ b/assets/index.less
@@ -1,11 +1,19 @@
-@treePrefixCls: rc-tree;
+@treePrefixCls: ~'rc-tree';
+@treeNodePrefixCls: ~'@{treePrefixCls}-treenode';
+
 .@{treePrefixCls} {
   margin: 0;
+  border: 1px solid transparent;
+
+  &-focused:not(&-active-focused) {
+    border-color: cyan;
+  }
+
   // padding: 5px;
-  [role='treeitem'] {
+  .@{treeNodePrefixCls} {
     margin: 0;
     padding: 0;
-    line-height: 20px;
+    line-height: 24px;
     white-space: nowrap;
     list-style: none;
     outline: 0;
@@ -16,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 {
@@ -48,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;
@@ -65,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,iVBORw0KGgoAAAANSUhEUgAAAKAAAABhCAYAAABRe6o8AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAK0dJREFUeNrsfQl8VNX1/5l9ksm+ELJB2ANECGtYVEAQaZBSFdAW0dpaKbi0WhX9Va1/S/+K2k+1iCztT4sFW6lKkUV2RLZAQHaSQBJCMllJJtvsM2/e75775k3evHkzTCZEAubweczMu/d7ZzLznXPvOff7zsjS7nudhXZaxZd/kKXf//9Cwgkf1xha2QOnS2DzofNw5FwZjM/KgFkTh8Idw/tBz7hImb9xQsV1W9czJf73zTsPek7I5XL3oQCFQkkOBSiV3C2eG/rz9z19Q8Wh7T5+kX3i7c9g6ojekDs6A1796Vg4XVoPe/ILYMnKzbDmxQfZaaMH+pApVFy3Sdupp8cKH6rJ8QQ55pBjvPvcEXJ8To415LDzHbOXH/OAZLK2t/vBbbcFHOOz3LOeMViW5QgYLImwTcrai0MSrdm4H/708ztgwtA0D+6OYb1hysh+kDtuEPxjWx59jUIyhYq7lc2k38HaGk5KtmniR4Au7Z5g34cnZHLF6vTRkyCuzyCAuATurKF+kuFy0aSK4/uXsy5moZuIkkbI94RCplidlZYDvZP7QUx8LD3f1NA46Up1yaRz+qPLSZ+FhIRrvDxgsCTC22DIp1Kp6OORX42GM/ef8sLh9IkeTEwi4fNNyu5Lb7Hf4VW/ZXFaDRV3qxPQcjUfEoaNkWxrLi0CW1MvVhMzOOD74GJci8Nj4lZkzn6UfKAMgLkZdv7+JU/79P95B+IG3gaFm9auNjcZlHKF/EPxGPO2ZC2O0EStmD6aOL4oBixghGpo5EgWr4F+8QOgX69M2Hn889Wkr3LDvefoGPL2kE/syXgcYpRKlQ/5uD7eOFy74fTpj0R8/8kj+sOsCUNofykcThYHLQfhVwW/gi1VW8HG2iVxt7q5GCewLukjLCERmos/g7rjr7PCo/XKVuH6Xa1QqTjyWQwAVytg53tLYfrGWs+x8/+/QNuwD/Z1T9Ve065SoVxx94g5YNY1Q6O9Giz2Vjhy7AA98D6ewzbsg33dUzXnAYMlnzQBFXDn3rsgb8YhihOST0hS3jBwwLVbMM83c/xgWLfrJMydku2DO2g8CJ/b/gNmpQmWXXgL7HY7zB/8sA+us2zTgXNs3oVyv+3jhvSC2XdkyTp7HMZpB5axSy/ww7SQkDXc53ztqUMQ2XsmvW93Mov6jL2TEKwFoPEqrl4o6ahtfBXgvj9yjze+RumSkj0RLh/bt4g88CzqnXbXotv65IBN2wqt5gYyAsfvv489QG//2vo091zkn1wrhyEpo+Hk5SN0DCXvpYIhny8BORx9o7ZPhO9+fNyLfBfmnffBYdSKgUMwz4fR7ZN/2SiJW1exDkyEfGazGaw2B7x77B1YMPQRH1xnGZLmzYW5wBAPxDid4CREcNht4HTYyJfBBn/dWoTE6fRxGKcNXE5ru147YgQBxEOxaX0AWuoAHBbvjg7BuNhG+mDfsvxvHhISUE7G6BmXDk3WBrC5rFBUUsA1uOObMwWn6O2gfoOBdTYA9pWX5T3kIWCw5BMTkMfx5o98QhySA6NWDByu9XzHCrgUixTugfg58PaFZWAlH1JLcxP8aeybkrjONCFpdBHRUF9bQUnjsFlDHkdIvmDGwb7tJSBiPF5SIR+lJMsmV10Tmc+d4FmX4fSOz//PpwUkdIIyNoVihOPJlLJRKo0SjOYWcAHj8Xy88Y+XVj4KDnBCTFgSxXieK1jyyWRiAnI49HxCE5NPiMN83Z6TZUE935bDBbS/FG5G2gz4bf9nQW5Uwp9y3oR5Q+dJ4jqVgALS0CnGTRr+cSjjCMkXzDg8AdtzCAlIUwYOO9isZrBZuIM3vL/7yw30wPsO0sdlsZIp3+UQvw4H+RtsNguZjSx+Xyu22YgntVvtmINxeAgYLPmE+R5vnJxGu/7IJ8RhsnjH8WI4fF4f8Pn2nSyBTQfP0v5SOJ1KR9d8Zx87A49lPwaR2khJ3LXsxIkTbDC3kh++2/PFxPWgj1PS+0Pv/lmUQP7Gv9Y4CUnp7RoHp1PWaWnXIZyCzXbnebPJRDwXruUs9Ghb21k8gQhtw6ibLHksjOuiF/ksDDcGGcRKyP180Wx68MY/ttIvCxmDkpkbQ8l7svaSTwp3LfKhYWoEk8WYr0M8Rq1S5Fu34wQmlT07G6HirmWjRo2SBXMrZeih+GkXSVN84QS9L/Qw7R2H93zBjtPRKbimyby5qUafHR0RAbbmBuKZXBDJr9f37IHpT7m9IQnytDER0FyjpxivXGSdeXN9Y022JloHLfYmEoK4vJ7Pbuden4z4uxhNItQ311CMIA3TfvJ1BIdJ4p/njoOn3v8KXl6zHb49fZm4Zgb2nyqF332wGX617DOYP30UiJPJoeKC8YChmHitxpOmvVOweNptzzh8ENKeQ+gBF28oWllfkA9MeAKARgcOhwOq3+QiZD4arn5rFm3DPtgXMcLXsPP3ZSsvNpyCSCYW1BBGXreDEnbhiSn0wPt4DtuwD/ZFjMcDirfJgrVQcTyZMFmM+TpMmWDUyu/pLnl4ql8PFiruWh4wFBOS5sKpwx7S4JRK5oeQxhGSL5hxAqVhAmF4I7Fvw5kKwxvKo7teSx07BViVHhxNdaBfeg/nZNThoIojgUd8GuiP7gLsixivARuhofZC0xunlAdfy0qZAA2qKmiy14PdxX0x1XItxKgTIF6RAqcqDwL2RQz1irgf90M29IChkLCr5AHL85ezVy9tbtdrTxwwC3qNeVrG7wWP+CA/YtXMjFfG9UtaEjcgGzTRsWR9L6M5QScjA1uTAQyXTkFeSe2yX28tW3ryqTFGib3giIlLU19JHxW/pG/MUNBpogFUMpoTlDtkYLQ1QWnTeag40bDs0CuVS0l/I3JPdqPUMOvX/VM+NfcnDHqyLahqOV8G44dmwL1uVcuebf/VzH94geRXu1sNc33FCISA+J7pyNH3rbtSnxmSHD0pPVbXH9v1jabS89XN+17aW/lX8rAUl3yEgKwEAT1jjHqxxzOJAyInRaeG0zFaqsyldRdb9514u84zBqdFcIsRKj4mEQtDoh+nkYTkLWRVTBaSZDEJDIbcVu7Wie1W6LMsvY1QIeLQkjJzmAm/fg9mj4qCR0Yp4cP7tJB36TJsPnAJlqxUYCBhc/9RPkIG3OtF3KMEt9IXx7Z3DdiRabirjtMeQ0KhRyJELCREexGgkrgvsmBzbzfjtjK2k36B5no6BjkKCdHIGHWSY4BAUdMmRgiSRCwjyvGEiEMSrd+8Hf72eDrcNZDx4Cb3t8HkPlaYOYiBf372Een5Cx81TCi4zloDduVxgjWhJ2OXU3IY3EfQJlrGtWsMjoBuEpU7h4NcoQBFhO/OSNi5J8mHLfoC+MEJBQlF/cd74XhVC08i3AVwhg8CB/HWytbzoGw+CVMyagih5ZJqmPbiuj1gYBu7+pTwYdB6wGMLs6/LGEouE855MEoif3o+JJHLLsqgczgF7auk/cRqGDEO1244ffIkssTdBaxMxeXDokeBMzILNKUrYHLvavjxAC3tj6ICMa46YjocMebBuuLf0W25GelPQmzJmz64W90DXk89oEIuWz0pMx0GpcVBAiflg/pGmFSkN0zaX1ixnHGxAfWAoYzB7ZG5p8+AOkCXRLjvxqEaRkqKxW0oeuMwcLh3mJLinJpUD/k8pJZrwBk1nOJy+1+l/aVwSD6hGuar0q8kcZ2ZB+wK46AeMC5rhOThtKAesOCa47lY1+KYcO3qp340HIYMjAMj+Ug++FpPj3/n6ek5bMM+2DfYMYqauQPv+xuDEpBfSwXaE6YkEm0B8jiaLtg+0Yd8uDMixmHUOq4Xt0Z0cEGSb54qbhzF5SQ30P5SOFTDNBgMYBKoYaRwt7oHvB56QJVCseLROzPBwJDAshVgywE97PhpmudYv1dP27AP9gWRHtDfGLjli0czCQH8jcF5QHfgEFAHiCQS70HzAYfbpNQwYhymTPIuWbjna5X2Uor6AxRzVB/hpYYR4nDaramsgbraq9DS3AjPjXxeEnere0A+ES118HpA8WGsPtSGd9gXTRyQAmQxBVctHGGQdGivFXJ98DG2YR/sixiv1yAaw+bkMHZCODwOHNf7HYPzgO6oNaAOkBLJ6e0B3bhAahgxDvN1m884KQ4DB5nL5kNqxdVvKW5rcaKXGkaIk1LDSOFudQ/Y0a041AP26RELda0oEkDFimB6t3jfxz7YFzHC1yAeg8fh7dGTeg+hpcZQejyZ0xJwb9eFbp11+npAiuPUMMO+zPYRJIhxmCzGfB2mTDBqxYAD1244faIHQxLJLJXwTVkMbC5Ng5cFahghDgOO+QT30Nz/criTT0nibtWdEJvhNGurPwnhkYnQUnIlqNesigwDTVyUlxhBrlCOUqmV0NTgAifrHRpYbS54Ok+Q9CDeMSVeSTHCcf2NgXiefPx44jG4KNidr/OkWvjAgXgTFz3cJHIx3h5QhCvqfRuwh+8PiONVLTRf55DTqFVlugJK/eee6RpJtP5CmqQapr24zvJcN1oRba49CpFpCaAMTw76NTdePAtys9FHD2gnrDET19dGHi5/jOf01dy2b1pyPApRyRStAhewPnpAqTHM1J2Gtb1m8lg8hjsP6E4Wi8jHT58eErGMKA8YGo5LEv+C5vUwZYJRa06yhazdouj0iR4MSSSlhgkF11l5txupiNbE4VruIET16hv086giI8FqqPaagp1W83kSyGWjgspi95ZRWchijvdgP9vRCpFqOSGRE1xWy0VvGkiPgXjEfXpPpOexeAxKQPE2WbAWKo4nk0fVcug8PLnDvad7z1A6fYo92Pp1//QsOXjcFwT3wrdlkNMvA+524/Zs+69sfeFR2nH+wws6de12IxXR2oRsuFq4jkS6MSDzc722DwHDldBQ0uClhjEbajbr65uyI8KiocFI1pPUg3GEaTA0e+7ja4oI14K+vplivLyxaAzOIj2C2jmbbfD5rATJMbrVMG4PeK1bMe7l1dvYVx++nXo+saE065O8RpxaO3Wc2nMfs3IohoiE+KD/XkO5Hpqq9TB09gZOQRCelJzz3s6q2dkZUFjvAIPFQZXNW+e2Te2zvqiGuDAVZCaoYNOpMjj62+kprLm22uMR/IzhtU4k3xGpMZShqlpCxQk8GUzN/Qn1ZLuJJ8srcXuyNjUMCuFcUp7seqphbmZFdFTanVB+dA9oI4LXHmJfhhEs4Sx1DYaSM2/sUitfmzIwFfRyFupMDrjnX3raHE6mzBSdCtKilLDrgh6wL2K852rpMczu6RjH6OFnDDoFv56bLIypgf6TiQ65jEqqX95Y6ukaCKeOwTwj4sgU0+LywqElZeawuc9+AFNHpMKUoT3gsbv7gr7GCPlnC2DZ2m3w1lNzmNrCozLxFIy4F5d/QXG5BLfYF8fyuGCm4I6sAW+0Ijospp+MYXTspbz89kgHIDJxmOfRmFUn7fm/HvGO4+lVGrN93JLstDjIjNeQz1AJODnKwAkGsxW2nqsiHjdvWdnyX7+DGOGIHRnDqzbMtcgn8/cxSZAvPae3uw2g6pjeh3z/+no/vPDj4dAzVkXCczvU110FnUoBM4cnw9j+PeCLvXnwwF3jWCEJQ8V11hqwKyiih+Suvh75RxMhxdIygE/1j731THTGkEm6pHS6TWWq05c2Xz6/r/Ljl4Ravus2hrJd5JNgoCZBS75UMircczQ5vMj36O5HYe3da0mzzGvanfncB/D8rOEQHyGDxsYm8qY7qKQHnw8vNI8k0drdWanw6qovYOPbT+FULxPjHLEuiEiKapsFagjOyvrgOssDYn4OUyTSpqDt3+c4HTHijaiWj3ixQkKSFysBJLV8Ys93PcZQtod8MtHnieTrPTrD4+kqjldA+pheHvJ5uC1YLdIaL9mpkBSrhEZDE9iIFxMGQi6yesUjITERZowaQPoXwdwpo71wzhgWwpLCodqip3vCuC3Xt2d/MLMmiG2ReeE6ywNicjiYPN/3NU6oJpRVwUI2JD1gR8ZQctwJjnw+V7mx3ONH9/4c1k5dK0k+fnze9pDAYfKQHmCxWD2ez2tI8hivzDKZTDAsIx6253FEEuKiMmMp+YRqmGf7PweZyUOgubrJC9eZa8CuMM6Kb1rZ1ro6v+0NBRfg97+5A2JjY2X8+yvaRvPcb29tP946rAcMmnyit8VzJQCSbg+Zbqet9SIfTr+0XYDLLy2DBVMzoIG8aYFSQE5CwrSkCDhbWuWDQ5OqDfP32R/74G71vWAXw8BL8/p5Zg7+YBgXVDZY4W8F5L3aVUGWOo0sT0IpC6W2n4S1Ww/oS8AA5JP5MNCbXVLkqz5WBS5TW1JoTL8MqK4zgVbOXTfsj4TYVtXQCtkDUnxwaFK1YaRwt7oHZJ3cLCKswcPSrTG8pJJ7/C2TCsyWYkpCqXWxuLbfpu3rvNrDlTEwe8KjPrX9vL4IrGtxnC58xaNTMoFRkQWfg3jfZvdSza0HvK1PHKzdV7jaYDIr5TJ5W33AoMknmoJl7j8HPZ/QfMgnDEImZMLpigbQasNAofC9eJ1/LVqtFs5fMcAUsp4T48zVRugb399LDTMkfSgYq4w+uFveAzq8lzE8+Rhyh+G2NaB30SHQl1RDQUGBlOfzqe23fsZJr+Nv0/ZJ1vYTTrsd0gMGSz7xO+NscYKeBB6UhHev9Us+IW5CVj/49lwVNFoZCA/XuasoeC8BwsLCwOiUwb4z5TBh2EAfnKOKrBEJ2XDN99Hsj2BIGkc+W4XFBxeMx7leOyo3YhzGYfd4PtThIflMxPsYyREbEwY/e2AW3Dt5FrBkWm5ubvZd6thdi7BeH1/bz2Zryz1iXT/+oG2kD/ZFjOg1SOoBUQfIawID6gFDIR+PY5oZT57vWuRD+2bHZuWrj98Dh4uugkWmhuiYGEo4lPNrNBqIjo4mLjwMjpc2wgsL7sb+Gikce5WF+rw6qDlYBXWHa4CtZSRxt7wHtNuJp+M+dCQeHrwipcUKEElWIj2HAiWglAlr+1mxhouzLe949NBBepw8eoq2YR9a2y9IPSCSDvWAQn2gWA/IETAE8glxTiOSsJISLxD5+C9MbeFJ5cw7RsCqbefhVIURXJoI6NkzBeThUXCuygJ/21EAU8ZkwdXiUzpB1BQq7tb2gMRjoYdxuPmF5LM6uIO2IzldeCtNQGFtP5uVrKfNjZ42fgr+eNoB2oZ9VGEqT20/D4l5PSD53FHzhwdvSEL+Md5iH7VapAcUb5MFa6HiKJkunVKsX/oErYzwlagywj8emEErI0iQKFTcLesBGeKZcL2HJOTJR3dX3Ao4/OydDHftiN+9aHdtPzKHgEKw8/KH0p+K3CVXZpev7ee1m+NHU4jG6wIl9YDiH48J1kLF8Tb/4QX4tZDhpZNSl0/iPq5QuCDY170m7vuIXrtMjWi7DcxubonJh+f5c5iukSQfV9svG99UK+O992xymL0ehynCweJsq+3nWUcG0BSiHtCzWyWlB/y+1TACcgVVG0ZIQt46Qw3TXusqNaJd7qAhEPnwnMspTcBAtf2qL7d9MRJSe/rU9vN4OD96wDmb6wW9IiX1gJ1WG6YRVPju4CIFoi01XjgkFdaGmbiIqw2zYKQSls8Og2MlZbDtYDG8vEoBq16YZyP9JNUwC9/hasM8QnAf+OK+NzVMV6gR7SJRsMPpSz7P1Mhw60B/UzDW6Yv7NOrVcRHToRkMYMTPT7AG5O2Fs/fT2n55DTu52n6COLjo3cUrY9J2vjo7OwLqyQyOesCZ/6n2eh5eU5igYWBTQT3FwBsPdE5tGCTfhejxnu2SwZX/8YIhiT7dvB1W/yId7uzHgNPWQr6hdsjp7YTx6VaYMdAJ6zd8DPPnPeajhgkF11lrt65QI5rBKJj1Jh8SzsG0BSH2AASUqu23+PjdPrX9eir7+NT2a5tbO6gH5En08fZGdy4u1ic5/WC/7ZK1YertRtiebyZ91ISDsZJqGJngumBUtdxOPN8qQqLbCYlMNgYssj5gDUsBhaUMtLaLMDa1hoZ1i9/dAPtXPONRwwhxlxSJYIhty/XFGKsI7oAPLlgP2F5FNP3z3Z6PtxROfUSlWf7GD2Yc3oIZx2FqhQ/eWndNomKR8fDwcKkm+77flb8zcSmjsY7aTWv7pWnI36EV1PYzN8Hxpt18bb93xEFeh/WAvAcLuCcsURsGyVcA8dB7THxANYy4NsyPyfR5ByGRmZCvUT0STGYH2IzkGyfrCVpCxNjmrwmZ9DBrQAMcPIM1XkZ44YqRfJpYbzVMfH/yLR8PYx07vXDBesCbtUb0b56aAiUlJVS8Ech0ul7Qr5/fS1VNXNHIyk9HvVgTTG0/yTFC1wO6p08pz+fRAUrVhmGMAIr4a6phQCABx4AD13wMmT7R8yH5mpqN5A20YIKTvFFhoFT2B5WtEu7ua4B/H75AiSTEoefzp4ax62VeuM60rlAjOjU1VUaOjv4pIdX2E3nB0PWA/Not0J6wVG0YcBg9ktaAahhhbRgS7WLAgWs3nHbR85lNVjAaLfT58LnDY3uDkyxsRiY1wbO7rvjg0PyqYUS4zrSuoIjuMPM6UNuPtw7rAfmAI+CesFRtGDq1BlbDDLn0IURaUBqVSc9jqgWjVgwccM2H067MrXPgvwBy02V6XfF31ToYN7S3Dw7NnxpGjOss6yqK6GXLlmE8mivVRqbce+fMmRNwHdw16gO6o92AOkCJ2jAyTFy61TD+pFg52iovHOb5MGWCUSsGHGHEC+K0yz03mYJJqB5mLCQvzAK7SlMgd+oQHxwGHLwa5u1j73JqmLShENZQ5oPrLOtCiujcJUuW3CvV8Pnnn+PBXouEbruB9QHdqZaAe8IStWFi7FdhcP3OwGoYidowm88r4FCxEzTOGoghAUecvIK82HBIVNdAgnEnRDDlcKJSA9suJ8PtgtowPC697gBENZd7qWHCGy5DSvkWH9wP3Qj5KAkD5hJDrO13Pcbwqg3jSbUEKrMhXD8QXIyzkeb5ClLnek271POpfXFYuWDl8/NYzNexDhfkkGgXAw5HK0vTNUqwwokqDXxe2AP++uwc2Pv1JjkmlH1wJNrFgMPBBMZ1WxsJ/XhCLy0fKmj4ZSHKqe4YnUbPRak4Ld8HO0+vIF7s76KAJOQx5O7NvA7Vhom2VMOQK/+AIaV/a1vzBcBhknj+vJ/D01tS4I974+A7PQtKVxOcqSZrmkMp8Ny+LHjoocVQV3RM4Y7QOoT7IZt7Gubv+7wnUvUBSUxHD17Th+faWx9QWBcQ7+M5qTE6qTZM5jWxtYXHZJgsxnwdpkwwas0hgcNMsnZ7nkyfxIN5KiOIcd9++Bu6F7zx0HlYwteGmTYUXhBVVOj2fHPEAcsWcR8vLR8h3ZlCwTXcQ7gKqVglYVhmGtQ5OS3fN7Iyr98LFo+BhuMI6wLyJh7je1fDDByQDGNypnleO+bqpPJ1/PSZf3Q3SOzrXjc1zK1ieCESf3kDf421MNVyZdNKmGTYf2/ekv3oBVeOW7aNrsPEtf2E9fx4w3NP57naVR9QXBfQM2mK6wOSD7jdUxUhkCxUnJBUST0zWLO5FaxWE819KVUa0Gp1EB4eCbU1ZV4E5zHtwQmI/oMgoERejz4u/2oV1Odvh3ELngWXTAHHPnkXpz9PIOCt5QuTHF9Ky+eVQLymHtAddEjVB4xLaGNrW3VT6Z9sKCpoK8cbKi6t1+AjrS0N45qb60Gni4aIyDhXz56p8pqaSpfdZpbj+eiYHmxkVHyevrxgfEdxPyQC8rf8FYdIPsOJnTDup08CU1cGNWabaBnvreUT6vf4un78ufbUBxTXBeRNsj5gsCSS+6lDJ4XjZgDWc8mg0JBEKEGKjU12pqX3VvLpoLS03vRWX1HubG2tV2K/64H7oRAQ32uGYTzk029ZA00nd3PkM1RBpcEAVfn7odFsX+/xTpL1AT10gfu/4jR9cvJ5tq8+oHddQN4k9YDBko/+XkgQ5JOTV4uPS4vPwMDMkV44nD7RUwlI5GNp6b2Uej04Gw1VSuyPX+hQcZ31gXcVRTQ/zSLxuAvSuduaHR9By6m9PuSrbDJ/OWfN/oXscg4rpeXjLx/hNX18bT+xlo+3joyhbA/5xJ6M/n4I66KOCL91YvJxfbxxuHbD6dMfiTxkSuultNtMtL8UDn+awWhsBZOphawDLZCQmAKJPVJ9cJ1lXUURzXs/JB6WNMHLKivOvwEG6wbodddMYFobPOQrtmlrFqz5+hEQKlo6oOW7HmMICHht8kkTUAZ1NWVkfTbIh3xCcnsiIhI44NrNswsTwNSacFdLS4NcCmc0tpB2Hfmg7GCzGqG6uowSUIzrTOsKimg0/Kzw0la1Wk01f6f1G+BHD34KX3/2M7BEtYIzn4SefUZDSa3iJMBGLzlVl6gPGCz5fAnYNrXqy4ugb/9hXuQbkpXjg8M3FwOHYN5YGmBUFUvizKZW8o13ksNKK34K1xlCXKcSsAsooo1G4zfLli3zOjesB9C94WG3vwJnDi6FBtvkGiSf0+nc42eYG1sfMFjyiQmIOOGGgxT5VCq1Fw5TJhi18oFDIMN+pL9cCofEsxDPh+TDD0qjDZPEdaZ1BUX00qVLscwFBhVa/tyHr2udxPv9BO9fLrdtfvL9jS8Rz4fyqCbJ9NiNrg8YLPlkMrmP68do15/n48knxGG+DlMmwXzA2A/7S+ESEpPptMuTLzk5QxLXmXajFNEFTw6HwStO8wEIztM1oiHvEz5Y/Afp5z2/Vw7rhqqAcdkBLxmxbwU7+TyRqK3k7RtLlz4muIQvEadStXYEoM9RyNUE64Chd3FrvA7rAYMln7iQEI/DKAyj3YuF30mST4jDZDFGs5gywajV3wur1Jc7TaZmZXR0giQO13v8mi8QrlM94A1URCMJ3Qk/uvMvV2t/YW+8mnbbP0rfEPa7+MLtH9gbagsUYeErhOd5AnMsBvJ5AUdCGyaLFSN1UWn/pgQ06uc4GeaoWsP1kSqw0GE9YCjkE+OQhNciH93LrSmTYbIY83WYMsGoVYpELS31So0mnPbv1bt/yLjOtBuliHZzjouA7fZ0xmb+feyI4Y9oe6SEnX2sX8/bPi6huxyXXph4OPXBpwdXf7k6xlJdEaEM1y0L+EJYemjkSuXc2KQH6be7se79ueBkTpHzwXyrQqsPGAr5OoLDnQpMFmO+DlMmGLUKdzTQgyGJsF9zU12HcZ1hN1IRjcliBXlvXYSFrItZGNM/a2Hi8DGgTeoFFV+tXXRyflqkKkx3T8qMuYm6qHDIePAJKP/io7dMZRcjlZExr0jnEnFGkxHis1qNWjU9PDqHfnh432Gz/ZG02QIVFA21PiAloHCbrD0WKo7fJuP3dDFlglErBg64dsPpEz2YmESh4jrDbqQimpbZUCh0MmCfiUzNeDx13F2gwKXglTOQPu0nwNrMD0cNGgYxWSPJlEPen6gEyJj3K6jY8eXvLZeLFCzretntSbWEwoPJbSznT1gzmbz6RsUPSpYrjPS58L7NdmIWacPoNZzyHthGcovFBvk8kaQekNcCYid/esAf/C8l3Yz2wOA42Su3J8+K0Cg39X7gCVBXFQJgVSvCHohPRdZw921mEj6Ygf5YS+YYEpemwvkX5trlSnU6WQPWnd8jGx4eHb9RE5auZom3ZZytjFyh08T0mJyg1XG/fmM1GZmmum/qXYzJplBGKmTAgM1SYTc3N9w3dCpLF5KjPjj2mylZfd7r1ycRqgXSqzcygUq5cka0aQaSSVxccvkq7Dt3+bcnnhr7vrL747z57MvCRjA5mJo19/YFFaafYhKANRroJRXQWEtIZ+MWdCzNygPoIsBRrYeGvV8DYzbukkfFUXLlnwDn+Amy2KSMB2M0ukHEtVUC66zFbAkwjhLOtWl7KHr0mpkkUyaBXJYKNlMRVBT+uQmxQ6fya1JfPSBvQj0hmlgPKO/+OG9KY3eUtJx5YsvlJaUbPoRWQyPIIuOAddi5MNWMhQYc3E44kjAsBhrPnYKGA9s+VIZHPk/O0A3al96G4l07DM8e27M8z1C9lZWzRmCZCkK+88Qb1nEHuY/nsA37YF/EINYTC0jUB5SqEei3PmC33XxGok3rjpLmtxd/flb2bmvrW7fNnAtMSyOZSO14Fbe7Lje5lWPiTg21B7aBXKVaK1NpCoHlyFHbAPZn33T9KzG2quS3j3yy5LHHh98TlTxM6cLC5wy3ly5TRIJcowBD+RfOj/9+esd7nziWXW2EY07G+yJ1Xz0ggJQmUKwH7PaAN6E9MTIRsnvqIE6riOyXGJGYkZWNmjwy81ro3jhrxws7rJz8GNeBhJg9J9xDSMVsIeQTRjwsIZKtzgAHNu93vH7hfGmpSmEFp9PEJafJgffxHLZhH+yLGBBsgbn1gNT7ovaPP3hDbaDnnNNJyGiR1gN2281hU3pHwsS0yORkjfPtuyeOfJiJiQVTTSklm8tBQk2tjn6wMpZEBFgvtr4cEsdMhLDBoxIr/vXXveTMIEzx4Vg5I8iDPgC/ewI00Yk6tdFE/KcslkyTHL/sWJyInMvoq1Ov+JNB8+c1AEWXAY62VW7zqwf0rRHoqwfs9oA3oT2+pQylvrGT+8U9DGNng8liAauhhu6L4+/yyXQxQEILLlmNsjRTE0BFAYQlpQKZXhPJWbp39uv5AB+9A/Dko6B2srrJkfFjeqq1yYQkPaCp+rITD7yP57AN+2BfxCDWk457d/HK/LJ6qvXTkfDGZneAxcrVCMRbPPActmEf7Ev1gN0EvDnN5HDBL7eU1fzv2eZv2ILDINfFgiw8FhjycWrTB4PVwQJTdRlkvQbT9R/EJ4NLGwtV/1lpIfTED/4cjvPWyyRAJsu0pARI6ZEYkasN76O1m2ohf//emvf/XLIWD7yP57AN+2BfxLz1suAF8XrAC3roH6MkHZSglrNktmXogffxHLZJ1wfstg7ZjVBHMy62edHWy4vMrV+uXJw7drI2dSCZL00gNzZB6cmjrrPl9ed+Fh45TJZ1OzhbGqDuzHFoLS9ZJVMqn+PHK6twLwQB1Ep1i9pS/N+WndsNez78pPGTcAUcxLYt31ZtWfzIlkemz4ibarO0qMmyUo0voIkE2sOHcvjr93vB3RaS3SB1NF7tf+l33zb80gbfLX8uF3Ihawprzd9y4Zktxa8eqbaesjI7P1sgU4ypb7VC/ZkjW+UqzUrcv+ft/oWeu2VapeWxIRklg04WwemSSii+8zau4fhZ+O9f/rfx3DcHG4dfKIMiqxPKeFCJdwGyDv5ecLd1yG6QOhpJeOV/vq193Ow4/qdfGh2x4S31G/brLRvpWnFH9cNNlk1v3De6f6E6Ivpt4pLMwp2v0jZni97oXEEpFJJWGr7mFbY9CRKytBLK+DYp69jvBXdbxwl4g9TRhFCMO7H8C885T80CwFTHQ/6ea/HixfQXqpzkOd3XlTjdAhKVUqmkekDSdgyoHpB1cuonOZXh4fUnvHW8PmC3ddiCUUeHMg5vwnE6Y/+e13XixU3k/sjExESqB6ypqZlDzh3Fdr7P9bRuAl4nC0Yd3d5x/KmjPUHJx4X+hkGpE1Y/wIjXq5xa3mPXrNujIUSbO3r0aKoH/Prrr+cSAqLi1NYZ71t3GuZ6ecAuUC9aYIs+4Yi2yE3Ga5qggIBWrVZPz8jIkOGB9/EcLzruJmAXtcDq6NDG8VVHS3o6VuKAQjPAH+cHJiFZ72kJqbAy1F3kmEYeTyDeb1ZqamoyrvHwwPt4DtuwD/ZFDGK7p+AuYjdQHb3ovQWZoBddKGkm8UGJOwR4dV4m/HFDIV/Pb7HI6w0KDw//Ii4uTo3Bh9VqZTTEBg4cGNvQwF17jvdJgPKujZhWq1WgFzQYDPaWlha88Ol0NwG7gN1IdXQx4cmFAPGmiawIXpydCW9v8iVhZWWlMyIiIpas92KSkpLoD1objUbiee3AE1Cn0ymys7OTSD/6W861tbWwffv2JsR2e8BuAzMhWKvZfzsVVRGP+JcHM+HZzwq9yrLt3r27mEyzz5rN5oUTJkzIwd8cQRIS7+ZZ7yEho6Ki6I+Jnz59mj18+PDR0tLS1fv37y/uJmC3gYXEJiYz47ddp1ZAShgg+cBhbvmHl3c0mezEm/2LTMMlly5dWjJjxox7evXqpcRUjM39K5xIPAxAvvvuOyfpu+PQoUPLCGGPkWnZ3k3AboM0HSFhtPelm612BqpbuURxZqIC1uwrhNbK0i8vvDrzKXjSK5JlCZFshIgHCgoKLH379h2QlpY2kKwFaXKaj44xSX3x4sVS0ud10vf49YyGuwl4E5u16er6d3bCfKm2H93WDyI0cvjnEQ/5Hsn5qMCnrgv+zFdCQgKMHz9ek5iYqMbIlwQbwO8Z81W3sC03N1dz5MgRqK+vx/VjNwF/6Hb6uTtRTvAazrTC84RoZ7J7quDNXYHJR4IPGDt2LAYdaqVSOblPnz49MdDA7bmioiLqAgcNGqTEilvYRqLfyWPGjMlXq9X2Y8eOdRPwh25uUpVKecY3d8H8QORDmzZtGqZesKxbSmRkZC7xcloMQI4ePVqTn5+/FfsQbzczJyenJ7bFxsbmtra2YiGkMsR2E7DbAnlG1P2Z/JEPrampiV/nqck6T028Wsu5c+f2HDhw4BPiBakekKz9tpSXlz+SlZU1lUTIahKc8DnD6/Jauy9M/wFbXFwcfxen4IHEyw2qrq4+3djYWNy7N/djj1euXAHi+fonJycPv3r1ahEJTlBhQyNgMiV3E7DbOvDh+9buwRmRrv2EQYi4zRNCXwfudBOw226o/Z8AAwBphnYirXZBiwAAAABJRU5ErkJggg==');
         background-repeat: no-repeat;
@@ -134,14 +150,14 @@
     }
   }
   &:not(.@{treePrefixCls}-show-line) {
-    [role='treeitem'] {
+    .@{treeNodePrefixCls} {
       .@{treePrefixCls}-switcher-noop {
         background: none;
       }
     }
   }
   &.@{treePrefixCls}-show-line {
-    [role='treeitem']:not(:last-child) {
+    .@{treeNodePrefixCls}:not(:last-child) {
       > ul {
         background: url('data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7')
           0 0 repeat-y;
@@ -150,7 +166,7 @@
         background-position: -56px -18px;
       }
     }
-    [role='treeitem']:last-child {
+    .@{treeNodePrefixCls}:last-child {
       > .@{treePrefixCls}-switcher-noop {
         background-position: -56px -36px;
       }
@@ -170,9 +186,16 @@
       cursor: not-allowed;
     }
   }
+  &-treenode-active {
+    background: rgba(0, 0, 0, 0.1);
+
+    // .@{treePrefixCls}-node-content-wrapper {
+    //   background: rgba(0, 0, 0, 0.1);
+    // }
+  }
   &-node-selected {
     background-color: #ffe6b0;
-    border: 1px #ffb951 solid;
+    box-shadow: 0 0 0 1px #ffb951;
     opacity: 0.8;
   }
   &-icon__open {
@@ -194,8 +217,22 @@
     margin-right: 2px;
     vertical-align: top;
   }
+  &-title {
+    display: inline-block;
+  }
+  &-indent {
+    display: inline-block;
+    height: 0;
+    vertical-align: bottom;
+  }
   &-indent-unit {
     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 @@
+<embed src="../CHANGELOG.md"></embed>
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
+---
+
+<code src="../examples/animation-draggable.jsx"></code>
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
+---
+
+<code src="../examples/animation.jsx"></code>
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
+---
+
+<code src="../examples/basic-controlled.jsx"></code>
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
+---
+
+<code src="../examples/basic.jsx"></code>
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
+---
+
+<code src="../examples/big-data.jsx"></code>
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
+---
+
+<code src="../examples/contextmenu.jsx"></code>
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
+---
+
+<code src="../examples/custom-switch-icon.jsx"></code>
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
+---
+
+<code src="../examples/draggable-allow-drop.jsx"></code>
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
+---
+
+<code src="../examples/draggable.jsx"></code>
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
+---
+
+<code src="../examples/dropdown.jsx"></code>
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
+---
+
+<code src="../examples/dynamic.jsx"></code>
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
+---
+
+<code src="../examples/expandAction.jsx"></code>
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
+---
+
+<code src="../examples/fieldNames.tsx"></code>
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
+---
+
+<code src="../examples/funtionTitle.jsx"></code>
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
+---
+
+<code src="../examples/icon.jsx"></code>
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
+---
+
+<code src="../examples/selectable.jsx"></code>
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/docs/examples/animation.jsx b/docs/examples/animation.jsx
new file mode 100644
index 000000000..7dcb5c3f1
--- /dev/null
+++ b/docs/examples/animation.jsx
@@ -0,0 +1,165 @@
+/* eslint no-console:0, react/no-danger: 0 */
+import { Provider } from '@rc-component/motion';
+import Tree from '@rc-component/tree';
+import React from 'react';
+import '../../assets/index.less';
+import './animation.less';
+
+const STYLE = `
+.rc-tree-child-tree {
+  display: block;
+}
+
+.node-motion {
+  transition: all .3s;
+  overflow-y: hidden;
+}
+`;
+
+const defaultExpandedKeys = ['0', '0-2', '0-9-2'];
+
+const motion = {
+  motionName: 'node-motion',
+  motionAppear: false,
+  onAppearStart: () => ({ height: 0 }),
+  onAppearActive: node => ({ height: node.scrollHeight }),
+  onLeaveStart: node => ({ height: node.offsetHeight }),
+  onLeaveActive: () => ({ height: 0 }),
+};
+
+function getTreeData() {
+  // big-data: generateData(1000, 3, 2)
+  return [
+    {
+      key: '0',
+      title: 'node 0',
+      children: [
+        { key: '0-0', title: 'node 0-0' },
+        { key: '0-1', title: 'node 0-1' },
+        {
+          key: '0-2',
+          title: 'node 0-2',
+          children: [
+            { key: '0-2-0', title: 'node 0-2-0' },
+            { key: '0-2-1', title: 'node 0-2-1' },
+            { key: '0-2-2', title: 'node 0-2-2' },
+          ],
+        },
+        { key: '0-3', title: 'node 0-3' },
+        { key: '0-4', title: 'node 0-4' },
+        { key: '0-5', title: 'node 0-5' },
+        { key: '0-6', title: 'node 0-6' },
+        { key: '0-7', title: 'node 0-7' },
+        { key: '0-8', title: 'node 0-8' },
+        {
+          key: '0-9',
+          title: 'node 0-9',
+          children: [
+            { key: '0-9-0', title: 'node 0-9-0' },
+            {
+              key: '0-9-1',
+              title: 'node 0-9-1',
+              children: [
+                { key: '0-9-1-0', title: 'node 0-9-1-0' },
+                { key: '0-9-1-1', title: 'node 0-9-1-1' },
+                { key: '0-9-1-2', title: 'node 0-9-1-2' },
+                { key: '0-9-1-3', title: 'node 0-9-1-3' },
+                { key: '0-9-1-4', title: 'node 0-9-1-4' },
+              ],
+            },
+            {
+              key: '0-9-2',
+              title: 'node 0-9-2',
+              children: [
+                { key: '0-9-2-0', title: 'node 0-9-2-0' },
+                { key: '0-9-2-1', title: 'node 0-9-2-1' },
+              ],
+            },
+          ],
+        },
+      ],
+    },
+    {
+      key: '1',
+      title: 'node 1',
+      // children: new Array(1000)
+      //   .fill(null)
+      //   .map((_, index) => ({ title: `auto ${index}`, key: `auto-${index}` })),
+      children: [
+        {
+          key: '1-0',
+          title: 'node 1-0',
+          children: [
+            { key: '1-0-0', title: 'node 1-0-0' },
+            {
+              key: '1-0-1',
+              title: 'node 1-0-1',
+              children: [
+                { key: '1-0-1-0', title: 'node 1-0-1-0' },
+                { key: '1-0-1-1', title: 'node 1-0-1-1' },
+              ],
+            },
+            { key: '1-0-2', title: 'node 1-0-2' },
+          ],
+        },
+      ],
+    },
+  ];
+}
+
+const Demo = () => {
+  const treeRef = React.useRef();
+  const [enableMotion, setEnableMotion] = React.useState(true);
+
+  setTimeout(() => {
+    treeRef.current.scrollTo({ key: '0-9-2' });
+  }, 100);
+
+  return (
+    <Provider motion={enableMotion}>
+      <button
+        onClick={() => {
+          setEnableMotion(e => !e);
+        }}
+      >
+        Motion: {String(enableMotion)}
+      </button>
+
+      <React.StrictMode>
+        <div className="animation">
+          <h2>expanded</h2>
+          <style dangerouslySetInnerHTML={{ __html: STYLE }} />
+
+          <div style={{ display: 'flex' }}>
+            <div style={{ flex: '1 1 50%' }}>
+              <h3>With Virtual</h3>
+              <Tree
+                ref={treeRef}
+                // defaultExpandAll={false}
+                defaultExpandAll
+                defaultExpandedKeys={defaultExpandedKeys}
+                motion={motion}
+                height={200}
+                itemHeight={20}
+                style={{ border: '1px solid #000' }}
+                treeData={getTreeData()}
+              />
+            </div>
+            <div style={{ flex: '1 1 50%' }}>
+              <h3>Without Virtual</h3>
+              <Tree
+                defaultExpandAll={false}
+                defaultExpandedKeys={defaultExpandedKeys}
+                motion={motion}
+                style={{ border: '1px solid #000' }}
+                treeData={getTreeData()}
+              />
+            </div>
+          </div>
+        </div>
+      </React.StrictMode>
+    </Provider>
+  );
+};
+
+export default Demo;
diff --git a/docs/examples/animation.less b/docs/examples/animation.less
new file mode 100644
index 000000000..702b7b0cc
--- /dev/null
+++ b/docs/examples/animation.less
@@ -0,0 +1,60 @@
+.animation {
+  .rc-tree-treenode {
+    display: flex;
+
+    .rc-tree-indent {
+      position: relative;
+      align-self: stretch;
+      display: flex;
+      flex-wrap: nowrap;
+      align-items: stretch;
+
+      .rc-tree-indent-unit {
+        position: relative;
+        height: 100%;
+
+        &::before {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          border-right: 1px solid blue;
+          left: 50%;
+          content: '';
+        }
+
+        &-end {
+          &::before {
+            display: none;
+          }
+        }
+
+        // End should ignore
+        // &-end {
+        //   &::before {
+        //     display: none;
+        //   }
+        // }
+      }
+    }
+
+    .rc-tree-switcher-noop {
+      &::before {
+        content: '';
+        display: inline-block;
+        width: 16px;
+        height: 16px;
+        background: red;
+        border-radius: 100%;
+      }
+    }
+
+    // Motion
+    &-motion:not(.node-motion-appear-active) {
+      // .rc-tree-indent-unit {
+      //   &::before {
+      //     display: none;
+      //   }
+      // }
+    }
+  }
+}
diff --git a/examples/basic-controlled.jsx b/docs/examples/basic-controlled.jsx
similarity index 95%
rename from examples/basic-controlled.jsx
rename to docs/examples/basic-controlled.jsx
index a816a7e6d..8a75cb76c 100644
--- a/examples/basic-controlled.jsx
+++ b/docs/examples/basic-controlled.jsx
@@ -1,17 +1,12 @@
 /* eslint-disable no-console, react/no-unescaped-entities */
-import '../assets/index.less';
+import '../../assets/index.less';
 import React from 'react';
-import PropTypes from 'prop-types';
-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 {
-  static propTypes = {
-    multiple: PropTypes.bool,
-  };
-
   static defaultProps = {
     multiple: true,
   };
diff --git a/examples/basic.jsx b/docs/examples/basic.jsx
similarity index 74%
rename from examples/basic.jsx
rename to docs/examples/basic.jsx
index 2909de7ad..55e96fc5b 100644
--- a/examples/basic.jsx
+++ b/docs/examples/basic.jsx
@@ -1,11 +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 PropTypes from 'prop-types';
-import ReactDOM from 'react-dom';
-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 = [
   {
@@ -19,6 +16,15 @@ const treeData = [
         children: [
           { key: '0-0-1-0', title: 'parent 1-2-0', disableCheckbox: true },
           { key: '0-0-1-1', title: 'parent 1-2-1' },
+          { key: '0-0-1-2', title: 'parent 1-2-2' },
+          { key: '0-0-1-3', title: 'parent 1-2-3' },
+          { key: '0-0-1-4', title: 'parent 1-2-4' },
+          { key: '0-0-1-5', title: 'parent 1-2-5' },
+          { key: '0-0-1-6', title: 'parent 1-2-6' },
+          { key: '0-0-1-7', title: 'parent 1-2-7' },
+          { key: '0-0-1-8', title: 'parent 1-2-8' },
+          { key: '0-0-1-9', title: 'parent 1-2-9' },
+          { key: 1128, title: 1128 },
         ],
       },
     ],
@@ -26,10 +32,6 @@ const treeData = [
 ];
 
 class Demo extends React.Component {
-  static propTypes = {
-    keys: PropTypes.array,
-  };
-
   static defaultProps = {
     keys: ['0-0-0-0'],
   };
@@ -42,6 +44,8 @@ class Demo extends React.Component {
       defaultSelectedKeys: keys,
       defaultCheckedKeys: keys,
     };
+
+    this.treeRef = React.createRef();
   }
 
   onExpand = expandedKeys => {
@@ -51,13 +55,6 @@ class Demo extends React.Component {
   onSelect = (selectedKeys, info) => {
     console.log('selected', selectedKeys, info);
     this.selKey = info.node.props.eventKey;
-
-    if (this.tree) {
-      console.log(
-        'Selected DOM node:',
-        selectedKeys.map(key => ReactDOM.findDOMNode(this.tree.domTreeNodes[key])),
-      );
-    }
   };
 
   onCheck = (checkedKeys, info) => {
@@ -102,6 +99,7 @@ class Demo extends React.Component {
     return (
       <div style={{ margin: '0 20px' }}>
         <h2>simple</h2>
+        <input aria-label="good" />
         <Tree
           ref={this.setTreeRef}
           className="myCls"
@@ -114,6 +112,7 @@ class Demo extends React.Component {
           defaultCheckedKeys={this.state.defaultCheckedKeys}
           onSelect={this.onSelect}
           onCheck={this.onCheck}
+          onActiveChange={key => console.log('Active:', key)}
         >
           <TreeNode title="parent 1" key="0-0">
             <TreeNode title={customLabel} key="0-0-0">
@@ -145,6 +144,28 @@ class Demo extends React.Component {
           onCheck={this.onCheck}
           treeData={treeData}
         />
+
+        <h2>Select</h2>
+        <Tree
+          ref={this.treeRef}
+          className="myCls"
+          defaultExpandAll
+          treeData={treeData}
+          onSelect={this.onSelect}
+          height={150}
+        />
+
+        <button
+          type="button"
+          onClick={() => {
+            setTimeout(() => {
+              console.log('scroll!!!');
+              this.treeRef.current.scrollTo({ key: '0-0-1-9' });
+            }, 100);
+          }}
+        >
+          Scroll Last
+        </button>
       </div>
     );
   }
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 91%
rename from examples/big-data-generator.js
rename to docs/examples/big-data-generator.js
index de4f2fedb..f87d30284 100644
--- a/examples/big-data-generator.js
+++ b/docs/examples/big-data-generator.js
@@ -1,17 +1,9 @@
 /* eslint react/no-string-refs:0 */
 
 import React from 'react';
-import PropTypes from 'prop-types';
 import { generateData, calcTotal } from './utils/dataUtil';
 
 class Gen extends React.Component {
-  static propTypes = {
-    onGen: PropTypes.func,
-    x: PropTypes.number,
-    y: PropTypes.number,
-    z: PropTypes.number,
-  };
-
   static defaultProps = {
     onGen: () => {},
     x: 20,
diff --git a/examples/big-data.jsx b/docs/examples/big-data.jsx
similarity index 94%
rename from examples/big-data.jsx
rename to docs/examples/big-data.jsx
index d8ae9f1a7..64c67fecb 100644
--- a/examples/big-data.jsx
+++ b/docs/examples/big-data.jsx
@@ -1,15 +1,10 @@
 /* eslint-disable no-console, prefer-destructuring */
 import React from 'react';
-import PropTypes from 'prop-types';
 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 {
-  static propTypes = {
-    multiple: PropTypes.bool,
-  };
-
   state = {
     gData: [],
     expandedKeys: [],
@@ -18,7 +13,7 @@ class Demo extends React.Component {
     selectedKeys: [],
   };
 
-  componentWillUpdate(nextProps, nextState) {
+  componentDidUpdate(nextProps, nextState) {
     // invoked immediately before rendering with new props or state, not for initial 'render'
     // see componentWillReceiveProps if you need to call setState
     // console.log(nextState.gData === this.state.gData);
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 92%
rename from examples/custom-switch-icon.jsx
rename to docs/examples/custom-switch-icon.jsx
index 1f8d16b4f..6d5ed4d44 100644
--- a/examples/custom-switch-icon.jsx
+++ b/docs/examples/custom-switch-icon.jsx
@@ -1,10 +1,8 @@
 /* eslint no-console:0 */
 /* eslint no-alert:0 */
-/* eslint jsx-a11y/no-noninteractive-element-interactions:0 */
 import React from 'react';
-import PropTypes from 'prop-types';
-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' +
@@ -27,10 +25,6 @@ const getSvgIcon = (path, iStyle = {}, style = {}) => (
 );
 
 class Demo extends React.Component {
-  static propTypes = {
-    keys: PropTypes.array,
-  };
-
   static defaultProps = {
     keys: ['0-0-0-0'],
   };
@@ -47,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 62%
rename from examples/draggable.jsx
rename to docs/examples/draggable-allow-drop.jsx
index 7a907d3f5..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,25 +54,16 @@ class Demo extends React.Component {
       dragObj = item;
     });
 
-    if (!info.dropToGap) {
+    if (dropPosition === 0) {
       // Drop on the content
       loop(data, dropKey, item => {
-        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 || [];
         // where to insert 示例添加到尾部，可以是随意位置
         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) => {
@@ -96,33 +91,21 @@ class Demo extends React.Component {
   };
 
   render() {
-    const loop = data =>
-      data.map(item => {
-        if (item.children && item.children.length) {
-          return (
-            <TreeNode key={item.key} title={item.title}>
-              {loop(item.children)}
-            </TreeNode>
-          );
-        }
-        return <TreeNode key={item.key} title={item.title} />;
-      });
     return (
       <div className="draggable-demo">
-        <h2>draggable</h2>
-        <p>drag a node into another node</p>
+        <h2>draggable with allow drop</h2>
+        <p>node can not be dropped inside a leaf node</p>
         <div className="draggable-container">
           <Tree
+            allowDrop={allowDrop}
             expandedKeys={this.state.expandedKeys}
             onExpand={this.onExpand}
             autoExpandParent={this.state.autoExpandParent}
             draggable
             onDragStart={this.onDragStart}
-            onDragEnter={this.onDragEnter}
             onDrop={this.onDrop}
-          >
-            {loop(this.state.gData)}
-          </Tree>
+            treeData={this.state.gData}
+          />
         </div>
       </div>
     );
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 (
+      <div className="draggable-demo">
+        <h2>draggable</h2>
+        <p>drag a node into another node</p>
+        <div style={{ border: '1px solid red' }}>
+          <Tree
+            expandedKeys={this.state.expandedKeys}
+            onExpand={this.onExpand}
+            autoExpandParent={this.state.autoExpandParent}
+            draggable={{
+              icon: '↕️',
+            }}
+            onDragStart={this.onDragStart}
+            onDragEnter={this.onDragEnter}
+            onDrop={this.onDrop}
+            treeData={this.state.gData}
+            // Virtual
+            height={200}
+            itemHeight={20}
+            virtual={false}
+          />
+          <div draggable>This element is draggable, but it cannot be dragged into tree.</div>
+        </div>
+      </div>
+    );
+  }
+}
+
+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 88%
rename from examples/dropdown.jsx
rename to docs/examples/dropdown.jsx
index a9b236aa5..e7ab29d5b 100644
--- a/examples/dropdown.jsx
+++ b/docs/examples/dropdown.jsx
@@ -2,12 +2,11 @@
 /* eslint no-console:0 */
 /* eslint react/no-string-refs:0 */
 import React from 'react';
-import PropTypes from 'prop-types';
-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: {
@@ -30,22 +29,6 @@ const placements = {
   },
 };
 class DropdownTree extends React.Component {
-  static propTypes = {
-    onVisibleChange: PropTypes.func,
-    prefixCls: PropTypes.string,
-    children: PropTypes.any,
-    transitionName: PropTypes.string,
-    overlay: PropTypes.object,
-    overlayClassName: PropTypes.string,
-    animation: PropTypes.any,
-    align: PropTypes.object,
-    overlayStyle: PropTypes.object,
-    placement: PropTypes.string,
-    trigger: PropTypes.array,
-    defaultVisible: PropTypes.bool,
-    visible: PropTypes.bool,
-  };
-
   static defaultProps = {
     prefixCls: 'demo-dropdown-tree',
     trigger: ['hover'],
@@ -69,12 +52,13 @@ class DropdownTree extends React.Component {
     };
   }
 
-  componentWillReceiveProps(props) {
+  static getDerivedStateFromProps(props) {
     if ('visible' in props) {
-      this.setState({
+      return {
         visible: props.visible,
-      });
+      };
     }
+    return null;
   }
 
   onChange = value => {
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 93%
rename from examples/dynamic.jsx
rename to docs/examples/dynamic.jsx
index efb4b03c7..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 = [];
@@ -26,6 +26,7 @@ function setLeaf(treeData, curKey, level) {
       if (item.children) {
         loopLeaf(item.children, l);
       } else if (l < 1) {
+        // eslint-disable-next-line no-param-reassign
         item.isLeaf = true;
       }
     });
@@ -41,6 +42,7 @@ function getNewTreeData(treeData, curKey, child, level) {
         if (item.children) {
           loop(item.children);
         } else {
+          // eslint-disable-next-line no-param-reassign
           item.children = child;
         }
       }
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 = () => (
+  <div className="expandAction-demo">
+    <h2>expandAction</h2>
+    <p>normal</p>
+    <Tree defaultExpandedKeys={['0-0']} expandAction="click" selectable={false}>
+      <TreeNode title="parent 1" key="0-0">
+        <TreeNode
+          title="click title can trigger expand even selectable is false because expandAction is 'click'"
+          key="0-0-0"
+        >
+          <TreeNode title="leaf-1" key="0-0-0-0" />
+        </TreeNode>
+      </TreeNode>
+    </Tree>
+  </div>
+);
+
+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 (
+    <Tree
+      checkable
+      fieldNames={fieldNames}
+      defaultExpandedKeys={['0-0-0', '0-0-1']}
+      defaultSelectedKeys={['0-0-0', '0-0-1']}
+      defaultCheckedKeys={['0-0-0', '0-0-1']}
+      onSelect={onSelect}
+      onCheck={onCheck}
+      treeData={treeData as any}
+    />
+  );
+};
+
+export default Demo;
diff --git a/examples/funtionTitle.jsx b/docs/examples/funtionTitle.jsx
similarity index 77%
rename from examples/funtionTitle.jsx
rename to docs/examples/funtionTitle.jsx
index 6fec7eca3..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 = `
@@ -25,11 +25,9 @@ const motion = {
   onLeaveActive: () => ({ height: 0 }),
 };
 
-const renderTitle = title => {
-  console.log('run');
-  return title;
-};
-
+const renderTitle = title =>
+  // console.log('run');
+  title;
 const groupList = (list, targetVar) => {
   const obj = {};
   list.forEach(item => {
@@ -43,7 +41,7 @@ const groupList = (list, targetVar) => {
       disabled,
     });
   });
-  console.log(obj);
+  // console.log(obj);
   return (
     Object.keys(obj)
       .map(key => ({
@@ -56,6 +54,11 @@ const groupList = (list, targetVar) => {
 };
 
 function getTreeData() {
+  // return [
+  //   { key: '00', children: [{ key: '000' }, { key: '001' }] },
+  //   { key: '01', children: [{ key: '010' }, { key: '011' }] },
+  // ];
+
   return groupList(
     data.map(item => ({
       title: () => renderTitle(item.fieldName),
@@ -70,8 +73,10 @@ function getTreeData() {
 
 const Demo = () => {
   const [keys, setKeys] = useState(data.map(item => item.fieldName));
+  // const [keys, setKeys] = useState(['00', '01']);
+
   return (
-    <div>
+    <div className="animation">
       <h2>expanded</h2>
       <style dangerouslySetInnerHTML={{ __html: STYLE }} />
 
@@ -85,7 +90,10 @@ const Demo = () => {
             height={200}
             checkedKeys={keys}
             itemHeight={20}
-            onCheck={checkedKeys => setKeys(checkedKeys)}
+            onCheck={checkedKeys => {
+              console.log('onCheck:', checkedKeys);
+              setKeys(checkedKeys);
+            }}
             style={{ border: '1px solid #000' }}
             treeData={getTreeData()}
           />
diff --git a/examples/icon.jsx b/docs/examples/icon.jsx
similarity index 85%
rename from examples/icon.jsx
rename to docs/examples/icon.jsx
index 9072fac45..63e428078 100644
--- a/examples/icon.jsx
+++ b/docs/examples/icon.jsx
@@ -1,18 +1,14 @@
 /* eslint no-console:0 */
 /* eslint no-alert:0 */
 import React from 'react';
-import PropTypes from 'prop-types';
 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 }) => (
   <span className={classNames('customize-icon', selected && 'selected-icon')} />
 );
-Icon.propTypes = {
-  selected: PropTypes.bool,
-};
 
 const Demo = () => (
   <div>
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 (
+      <div className="selectable-demo">
+        <h2>selectable</h2>
+        <p>normal</p>
+        <div>
+          <Tree defaultExpandAll showLine>
+            <TreeNode title="root node" key="0-0">
+              <TreeNode title="parent 1 default value(true)" key="0-0-0">
+                <TreeNode
+                  title="use parent 1 value if dont set selectable obviously"
+                  key="0-0-0-0"
+                />
+                <TreeNode selectable={true} title="set selectable is true" key="0-0-0-1" />
+              </TreeNode>
+              <TreeNode title="parent 2 selectable is false" selectable={false} key="0-0-1">
+                <TreeNode
+                  selectable={false}
+                  title="use parent 2 value if dont set selectable obviously"
+                  key="0-0-1-0"
+                />
+                <TreeNode
+                  selectable={true}
+                  title="if set selectable obviously, it does't affect by parent"
+                  key="0-0-1-1"
+                />
+              </TreeNode>
+              <TreeNode title="parent 3 disabled" key="0-0-2" disabled>
+                <TreeNode
+                  selectable={false}
+                  title="parent is disable ,and selectable is false"
+                  key="0-0-2-0"
+                />
+                <TreeNode
+                  selectable={true}
+                  title="parent is disable ,and selectable is true"
+                  key="0-0-2-1"
+                />
+              </TreeNode>
+            </TreeNode>
+          </Tree>
+        </div>
+        <p>customized tree node style if unselectable</p>
+        <div className="selectable-container">
+          <Tree prefixCls="rc-tree" defaultExpandAll showLine>
+            <TreeNode title="root node" key="0-0">
+              <TreeNode title="parent 1 default value(true)" key="0-0-0">
+                <TreeNode
+                  title="use parent 1 value if dont set selectable obviously"
+                  key="0-0-0-0"
+                />
+                <TreeNode selectable={true} title="set selectable is true" key="0-0-0-1" />
+              </TreeNode>
+              <TreeNode title="parent 2 selectable is false" selectable={false} key="0-0-1">
+                <TreeNode
+                  selectable={false}
+                  title="use parent 2 value if dont set selectable obviously"
+                  key="0-0-1-0"
+                />
+                <TreeNode
+                  selectable={true}
+                  title="if set selectable obviously, it does't affect by parent"
+                  key="0-0-1-1"
+                />
+              </TreeNode>
+              <TreeNode title="parent 3 disabled" key="0-0-2" disabled>
+                <TreeNode
+                  selectable={false}
+                  title="parent is disable ,and selectable is false"
+                  key="0-0-2-0"
+                />
+                <TreeNode
+                  selectable={true}
+                  title="parent is disable ,and selectable is true"
+                  key="0-0-2-1"
+                />
+              </TreeNode>
+            </TreeNode>
+          </Tree>
+        </div>
+      </div>
+    );
+  }
+}
+
+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
+---
+
+<embed src="../README.md"></embed>
diff --git a/examples/animation.jsx b/examples/animation.jsx
deleted file mode 100644
index 15d82540f..000000000
--- a/examples/animation.jsx
+++ /dev/null
@@ -1,122 +0,0 @@
-/* eslint no-console:0, react/no-danger: 0 */
-import '../assets/index.less';
-import './animation.less';
-import React from 'react';
-import Tree from '../src';
-
-const STYLE = `
-.rc-tree-child-tree {
-  display: block;
-}
-
-.node-motion {
-  transition: all .3s;
-  overflow-y: hidden;
-}
-`;
-
-const defaultExpandedKeys = ['0', '0-2', '0-9-2'];
-
-const motion = {
-  motionName: 'node-motion',
-  motionAppear: false,
-  onAppearStart: () => ({ height: 0 }),
-  onAppearActive: node => ({ height: node.scrollHeight }),
-  onLeaveStart: node => ({ height: node.offsetHeight }),
-  onLeaveActive: () => ({ height: 0 }),
-};
-
-function getTreeData() {
-  return [
-    {
-      key: '0',
-      title: 'node 0',
-      children: [
-        { key: '0-0', title: 'node 0-0' },
-        { key: '0-1', title: 'node 0-1' },
-        {
-          key: '0-2',
-          title: 'node 0-2',
-          children: [
-            { key: '0-2-0', title: 'node 0-2-0' },
-            { key: '0-2-1', title: 'node 0-2-1' },
-            { key: '0-2-2', title: 'node 0-2-2' },
-          ],
-        },
-        { key: '0-3', title: 'node 0-3' },
-        { key: '0-4', title: 'node 0-4' },
-        { key: '0-5', title: 'node 0-5' },
-        { key: '0-6', title: 'node 0-6' },
-        { key: '0-7', title: 'node 0-7' },
-        { key: '0-8', title: 'node 0-8' },
-        {
-          key: '0-9',
-          title: 'node 0-9',
-          children: [
-            { key: '0-9-0', title: 'node 0-9-0' },
-            {
-              key: '0-9-1',
-              title: 'node 0-9-1',
-              children: [
-                { key: '0-9-1-0', title: 'node 0-9-1-0' },
-                { key: '0-9-1-1', title: 'node 0-9-1-1' },
-                { key: '0-9-1-2', title: 'node 0-9-1-2' },
-                { key: '0-9-1-3', title: 'node 0-9-1-3' },
-                { key: '0-9-1-4', title: 'node 0-9-1-4' },
-              ],
-            },
-            {
-              key: '0-9-2',
-              title: 'node 0-9-2',
-              children: [
-                { key: '0-9-2-0', title: 'node 0-9-2-0' },
-                { key: '0-9-2-1', title: 'node 0-9-2-1' },
-              ],
-            },
-          ],
-        },
-      ],
-    },
-    {
-      key: '1',
-      title: 'node 1',
-      children: new Array(1000)
-        .fill(null)
-        .map((_, index) => ({ title: `auto ${index}`, key: `auto-${index}` })),
-    },
-  ];
-}
-
-const Demo = () => (
-  <div>
-    <h2>expanded</h2>
-    <style dangerouslySetInnerHTML={{ __html: STYLE }} />
-
-    <div style={{ display: 'flex' }}>
-      <div style={{ flex: '1 1 50%' }}>
-        <h3>With Virtual</h3>
-        <Tree
-          defaultExpandAll={false}
-          defaultExpandedKeys={defaultExpandedKeys}
-          motion={motion}
-          height={200}
-          itemHeight={20}
-          style={{ border: '1px solid #000' }}
-          treeData={getTreeData()}
-        />
-      </div>
-      <div style={{ flex: '1 1 50%' }}>
-        <h3>Without Virtual</h3>
-        <Tree
-          defaultExpandAll={false}
-          defaultExpandedKeys={defaultExpandedKeys}
-          motion={motion}
-          style={{ border: '1px solid #000' }}
-          treeData={getTreeData()}
-        />
-      </div>
-    </div>
-  </div>
-);
-
-export default Demo;
diff --git a/examples/animation.less b/examples/animation.less
deleted file mode 100644
index fefd1c5ec..000000000
--- a/examples/animation.less
+++ /dev/null
@@ -1,38 +0,0 @@
-.rc-tree-treenode {
-  display: flex;
-
-  .rc-tree-indent {
-    position: relative;
-    align-self: stretch;
-
-    .rc-tree-indent-unit {
-      position: relative;
-      height: 100%;
-
-      &::before {
-        position: absolute;
-        top: 100%;
-        right: -26px;
-        bottom: -100%;
-        border-right: 1px solid blue;
-        content: '';
-      }
-
-      // End should ignore
-      &-end {
-        &::before {
-          display: none;
-        }
-      }
-    }
-  }
-
-  // Motion
-  &-motion:not(.node-motion-appear-active) {
-    .rc-tree-indent-unit {
-      &::before {
-        display: none;
-      }
-    }
-  }
-}
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: ['<rootDir>/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 4114f9e9f..4ef541066 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,10 @@
 {
-  "name": "rc-tree",
-  "version": "3.0.0-alpha.14",
+  "name": "@rc-component/tree",
+  "version": "1.0.1",
   "description": "tree ui component for react",
+  "engines": {
+    "node": ">=10.x"
+  },
   "keywords": [
     "react",
     "react-component",
@@ -9,11 +12,7 @@
     "tree"
   ],
   "files": [
-    "assets/*.css",
-    "assets/*.png",
-    "assets/*.gif",
-    "assets/*.less",
-    "dist",
+    "assets",
     "es",
     "lib"
   ],
@@ -30,44 +29,60 @@
   "main": "./lib/index",
   "module": "./es/index",
   "scripts": {
-    "start": "cross-env NODE_ENV=development father doc dev --storybook",
-    "build": "father doc build --storybook",
-    "compile": "father build",
-    "prepublishOnly": "npm run compile && np --no-cleanup --yolo --no-publish",
-    "lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js",
-    "test": "father test",
-    "now-build": "npm run build"
+    "start": "dumi dev",
+    "docs:build": "dumi build",
+    "docs:deploy": "gh-pages -d dist",
+    "compile": "father build && lessc assets/index.less assets/index.css",
+    "prepare": "husky",
+    "prepublishOnly": "npm run compile && rc-np",
+    "postpublish": "npm run gh-pages",
+    "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/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": "^30.0.0",
+    "@types/node": "^24.0.10",
+    "@types/react": "^19.0.1",
+    "@types/react-dom": "^19.0.1",
     "@types/warning": "^3.0.0",
-    "cross-env": "^5.2.0",
-    "css-animation": "^1.2.0",
-    "enzyme": "^3.3.0",
-    "enzyme-adapter-react-16": "^1.1.1",
-    "enzyme-to-json": "^3.0.0",
-    "father": "^2.13.2",
-    "np": "^5.0.3",
-    "rc-dialog": "^7.3.1",
-    "rc-tools": "^9.x",
-    "rc-tooltip": "3.x",
-    "rc-trigger": "^2.0.0",
-    "react": "^16.8.0",
-    "react-dom": "^16.8.0",
-    "typescript": "^3.5.2"
+    "@umijs/fabric": "^4.0.1",
+    "dumi": "^2.1.0",
+    "eslint": "^8.55.0",
+    "eslint-plugin-jest": "^29.0.1",
+    "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": "^16.1.2",
+    "@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": {
     "classnames": "2.x",
-    "prop-types": "^15.5.8",
-    "rc-animate": "^2.9.2",
-    "rc-util": "^4.9.0",
-    "rc-virtual-list": "^0.0.0-alpha.18",
-    "react-lifecycles-compat": "^3.0.4",
-    "warning": "^4.0.3"
+    "@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<Readonly<DropIndicatorProps>> = 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 <div style={style} />;
+};
+
+if (process.env.NODE_ENV !== 'production') {
+  DropIndicator.displayName = 'DropIndicator';
+}
+
+export default DropIndicator;
diff --git a/src/Indent.tsx b/src/Indent.tsx
index a7ade7ea4..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<IndentProps> = ({ 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) {
@@ -20,8 +16,8 @@ const Indent: React.FC<IndentProps> = ({ prefixCls, level, isStart, isEnd }) =>
       <span
         key={i}
         className={classNames(baseClassName, {
-          [`${baseClassName}-start`]: isStart[i + 1],
-          [`${baseClassName}-end`]: isEnd[i + 1],
+          [`${baseClassName}-start`]: isStart[i],
+          [`${baseClassName}-end`]: isEnd[i],
         })}
       />,
     );
@@ -34,4 +30,4 @@ const Indent: React.FC<IndentProps> = ({ prefixCls, level, isStart, isEnd }) =>
   );
 };
 
-export default Indent;
+export default React.memo(Indent);
diff --git a/src/MotionTreeNode.tsx b/src/MotionTreeNode.tsx
index ee306dcaf..946289c68 100644
--- a/src/MotionTreeNode.tsx
+++ b/src/MotionTreeNode.tsx
@@ -1,43 +1,77 @@
-import * as React from 'react';
 import classNames from 'classnames';
-// @ts-ignore
-import CSSMotion from 'rc-animate/lib/CSSMotion';
-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<TreeNodeProps, 'domRef'> {
+  active: boolean;
   motion?: any;
   motionNodes?: FlattenNode[];
+  onMotionStart: () => void;
   onMotionEnd: () => void;
   motionType?: 'show' | 'hide';
 
   treeNodeRequiredProps: TreeNodeRequiredProps;
 }
 
-const MotionTreeNode: React.FC<MotionTreeNodeProps> = (
-  {
+const MotionTreeNode = React.forwardRef<HTMLDivElement, MotionTreeNodeProps>((oriProps, ref) => {
+  const {
     className,
     style,
     motion,
     motionNodes,
     motionType,
-    onMotionEnd,
+    onMotionStart: onOriginMotionStart,
+    onMotionEnd: onOriginMotionEnd,
+    active,
     treeNodeRequiredProps,
     ...props
-  },
-  ref,
-) => {
+  } = oriProps;
   const [visible, setVisible] = React.useState(true);
   const { prefixCls } = React.useContext(TreeContext);
 
-  React.useEffect(() => {
-    if (motionNodes && motionType === 'hide' && visible) {
-      setVisible(false);
+  // Calculate target visible here.
+  // And apply in effect to make `leave` motion work.
+  const targetVisible = motionNodes && motionType !== 'hide';
+
+  useLayoutEffect(() => {
+    if (motionNodes) {
+      if (targetVisible !== visible) {
+        setVisible(targetVisible);
+      }
     }
   }, [motionNodes]);
 
+  const triggerMotionStart = () => {
+    if (motionNodes) {
+      onOriginMotionStart();
+    }
+  };
+
+  // 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 (
       <CSSMotion
@@ -45,8 +79,7 @@ const MotionTreeNode: React.FC<MotionTreeNodeProps> = (
         visible={visible}
         {...motion}
         motionAppear={motionType === 'show'}
-        onAppearEnd={onMotionEnd}
-        onLeaveEnd={onMotionEnd}
+        onVisibleChanged={onVisibleChanged}
       >
         {({ className: motionClassName, style: motionStyle }, motionRef) => (
           <div
@@ -54,9 +87,11 @@ const MotionTreeNode: React.FC<MotionTreeNodeProps> = (
             className={classNames(`${prefixCls}-treenode-motion`, motionClassName)}
             style={motionStyle}
           >
-            {motionNodes.map((treeNode: FlattenNode) => {
+            {motionNodes.map(treeNode => {
               const {
-                data: { key, ...restProps },
+                data: { ...restProps },
+                title,
+                key,
                 isStart,
                 isEnd,
               } = treeNode;
@@ -66,8 +101,10 @@ const MotionTreeNode: React.FC<MotionTreeNodeProps> = (
 
               return (
                 <TreeNode
-                  {...restProps}
+                  {...(restProps as Omit<typeof restProps, 'children'>)}
                   {...treeNodeProps}
+                  title={title}
+                  active={active}
                   data={treeNode.data}
                   key={key}
                   isStart={isStart}
@@ -80,11 +117,11 @@ const MotionTreeNode: React.FC<MotionTreeNodeProps> = (
       </CSSMotion>
     );
   }
-  return <TreeNode domRef={ref} className={className} style={style} {...props} />;
-};
-
-MotionTreeNode.displayName = 'MotionTreeNode';
+  return <TreeNode domRef={ref} className={className} style={style} {...props} active={active} />;
+});
 
-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 eadf06787..94d4dea64 100644
--- a/src/NodeList.tsx
+++ b/src/NodeList.tsx
@@ -2,12 +2,34 @@
  * 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 from 'rc-virtual-list';
-import { FlattenNode, Key, DataEntity, DataNode } 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,
+  height: 0,
+  display: 'flex',
+  overflow: 'hidden',
+  opacity: 0,
+  border: 0,
+  padding: 0,
+  margin: 0,
+};
+
+const noop = () => {};
 
 export const MOTION_KEY = `RC_TREE_MOTION_${Math.random()}`;
 
@@ -21,6 +43,7 @@ export const MotionEntity: DataEntity = {
   index: 0,
   pos: '0',
   node: MotionNode,
+  nodes: [MotionNode],
 };
 
 const MotionFlattenData: FlattenNode = {
@@ -28,17 +51,30 @@ 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: [],
 };
 
-interface NodeListProps {
+export interface NodeListRef {
+  scrollTo: ScrollTo;
+  getIndentWidth: () => number;
+}
+
+interface NodeListProps<TreeDataType extends BasicDataNode> {
   prefixCls: string;
-  className: string;
   style: React.CSSProperties;
-  data: FlattenNode[];
+  data: FlattenNode<TreeDataType>[];
   motion: any;
+  focusable?: boolean;
+  activeItem: FlattenNode<TreeDataType>;
+  focused?: boolean;
+  tabIndex: number;
+  checkable?: boolean;
+  selectable?: boolean;
+  disabled?: boolean;
 
   expandedKeys: Key[];
   selectedKeys: Key[];
@@ -46,7 +82,7 @@ interface NodeListProps {
   loadedKeys: Key[];
   loadingKeys: Key[];
   halfCheckedKeys: Key[];
-  keyEntities: Record<Key, DataEntity>;
+  keyEntities: KeyEntities;
 
   dragging: boolean;
   dragOverNodeKey: Key;
@@ -55,6 +91,16 @@ interface NodeListProps {
   // Virtual list
   height: number;
   itemHeight: number;
+  virtual?: boolean;
+  scrollWidth?: number;
+
+  onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
+  onFocus?: React.FocusEventHandler<HTMLDivElement>;
+  onBlur?: React.FocusEventHandler<HTMLDivElement>;
+  onActiveChange: (key: Key) => void;
+
+  onListChangeStart: () => void;
+  onListChangeEnd: () => void;
 }
 
 /**
@@ -62,10 +108,11 @@ interface NodeListProps {
  */
 export function getMinimumRangeTransitionRange(
   list: FlattenNode[],
+  virtual: boolean,
   height: number,
   itemHeight: number,
 ) {
-  if (!height) {
+  if (virtual === false || !height) {
     return list;
   }
 
@@ -73,62 +120,109 @@ export function getMinimumRangeTransitionRange(
 }
 
 function itemKey(item: FlattenNode) {
-  const {
-    data: { key },
-    pos,
-  } = item;
+  const { key, pos } = item;
   return getKey(key, pos);
 }
 
-const NodeList: React.FC<NodeListProps> = ({
-  prefixCls,
-  data,
-  expandedKeys,
-  selectedKeys,
-  checkedKeys,
-  loadedKeys,
-  loadingKeys,
-  halfCheckedKeys,
-  keyEntities,
-
-  dragging,
-  dragOverNodeKey,
-  dropPosition,
-  motion,
-
-  height,
-  itemHeight,
-
-  ...domProps
-}) => {
-  const [disableVirtual, setDisableVirtual] = React.useState(false);
+function getAccessibilityPath(item: FlattenNode): string {
+  let path = String(item.data.key);
+  let current = item;
+
+  while (current.parent) {
+    current = current.parent;
+    path = `${current.data.key} > ${path}`;
+  }
+
+  return path;
+}
+
+const NodeList = React.forwardRef<NodeListRef, NodeListProps<any>>((props, ref) => {
+  const {
+    prefixCls,
+    data,
+    selectable,
+    checkable,
+    expandedKeys,
+    selectedKeys,
+    checkedKeys,
+    loadedKeys,
+    loadingKeys,
+    halfCheckedKeys,
+    keyEntities,
+    disabled,
+
+    dragging,
+    dragOverNodeKey,
+    dropPosition,
+    motion,
+
+    height,
+    itemHeight,
+    virtual,
+    scrollWidth,
+
+    focusable,
+    activeItem,
+    focused,
+    tabIndex,
+
+    onKeyDown,
+    onFocus,
+    onBlur,
+    onActiveChange,
+
+    onListChangeStart,
+    onListChangeEnd,
+
+    ...domProps
+  } = props;
+
+  // =============================== Ref ================================
+  const listRef = React.useRef<ListRef>(null);
+  const indentMeasurerRef = React.useRef<HTMLDivElement>(null);
+  React.useImperativeHandle(ref, () => ({
+    scrollTo: scroll => {
+      listRef.current.scrollTo(scroll);
+    },
+    getIndentWidth: () => indentMeasurerRef.current.offsetWidth,
+  }));
+
+  // ============================== Motion ==============================
   const [prevExpandedKeys, setPrevExpandedKeys] = React.useState(expandedKeys);
   const [prevData, setPrevData] = React.useState(data);
   const [transitionData, setTransitionData] = React.useState(data);
   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);
-    setDisableVirtual(false);
+
+    onListChangeEnd();
   }
 
   // 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);
 
-        if (motion) setDisableVirtual(true);
         const rangeNodes = getMinimumRangeTransitionRange(
           getExpandRange(prevData, data, diffExpanded.key),
+          virtual,
           height,
           itemHeight,
         );
@@ -140,11 +234,11 @@ const NodeList: React.FC<NodeListProps> = ({
         setTransitionRange(rangeNodes);
         setMotionType('show');
       } else {
-        const keyIndex = data.findIndex(({ data: { key } }) => key === diffExpanded.key);
+        const keyIndex = data.findIndex(({ key }) => key === diffExpanded.key);
 
-        if (motion) setDisableVirtual(true);
         const rangeNodes = getMinimumRangeTransitionRange(
           getExpandRange(data, prevData, diffExpanded.key),
+          virtual,
           height,
           itemHeight,
         );
@@ -185,47 +279,111 @@ const NodeList: React.FC<NodeListProps> = ({
   };
 
   return (
-    <VirtualList
-      {...domProps}
-      disabled={disableVirtual}
-      role="tree"
-      data={mergedData}
-      itemKey={itemKey}
-      height={height}
-      itemHeight={itemHeight}
-      onSkipRender={onMotionEnd}
-      prefixCls={`${prefixCls}-list`}
-    >
-      {(treeNode: FlattenNode) => {
-        const {
-          pos,
-          data: { key, ...restProps },
-          isStart,
-          isEnd,
-        } = treeNode;
-        const mergedKey = getKey(key, pos);
-        delete restProps.children;
-
-        const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
-
-        return (
-          <MotionTreeNode
-            {...restProps}
-            {...treeNodeProps}
-            pos={pos}
-            data={treeNode.data}
-            isStart={isStart}
-            isEnd={isEnd}
-            motion={motion}
-            motionNodes={key === MOTION_KEY ? transitionRange : null}
-            motionType={motionType}
-            onMotionEnd={onMotionEnd}
-            treeNodeRequiredProps={treeNodeRequiredProps}
-          />
-        );
-      }}
-    </VirtualList>
+    <>
+      {focused && activeItem && (
+        <span style={HIDDEN_STYLE} aria-live="assertive">
+          {getAccessibilityPath(activeItem)}
+        </span>
+      )}
+
+      <div>
+        <input
+          style={HIDDEN_STYLE}
+          disabled={focusable === false || disabled}
+          tabIndex={focusable !== false ? tabIndex : null}
+          onKeyDown={onKeyDown}
+          onFocus={onFocus}
+          onBlur={onBlur}
+          value=""
+          onChange={noop}
+          aria-label="for screen reader"
+        />
+      </div>
+
+      <div
+        className={`${prefixCls}-treenode`}
+        aria-hidden
+        style={{
+          position: 'absolute',
+          pointerEvents: 'none',
+          visibility: 'hidden',
+          height: 0,
+          overflow: 'hidden',
+          border: 0,
+          padding: 0,
+        }}
+      >
+        <div className={`${prefixCls}-indent`}>
+          <div ref={indentMeasurerRef} className={`${prefixCls}-indent-unit`} />
+        </div>
+      </div>
+
+      <VirtualList<FlattenNode>
+        {...domProps}
+        data={mergedData}
+        itemKey={itemKey}
+        height={height}
+        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 => {
+          const {
+            pos,
+            data: { ...restProps },
+            title,
+            key,
+            isStart,
+            isEnd,
+          } = treeNode;
+          const mergedKey = getKey(key, pos);
+          delete restProps.key;
+          delete restProps.children;
+
+          const treeNodeProps = getTreeNodeProps(mergedKey, treeNodeRequiredProps);
+
+          return (
+            <MotionTreeNode
+              {...(restProps as Omit<typeof restProps, 'children'>)}
+              {...treeNodeProps}
+              title={title}
+              active={!!activeItem && key === activeItem.key}
+              pos={pos}
+              data={treeNode.data}
+              isStart={isStart}
+              isEnd={isEnd}
+              motion={motion}
+              motionNodes={key === MOTION_KEY ? transitionRange : null}
+              motionType={motionType}
+              onMotionStart={onListChangeStart}
+              onMotionEnd={onMotionEnd}
+              treeNodeRequiredProps={treeNodeRequiredProps}
+              onMouseMove={() => {
+                onActiveChange(null);
+              }}
+            />
+          );
+        }}
+      </VirtualList>
+    </>
   );
-};
+});
+
+if (process.env.NODE_ENV !== 'production') {
+  NodeList.displayName = 'NodeList';
+}
 
 export default NodeList;
diff --git a/src/Tree.tsx b/src/Tree.tsx
index 3c43b7ab8..ec675df98 100644
--- a/src/Tree.tsx
+++ b/src/Tree.tsx
@@ -1,88 +1,149 @@
 // 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 PropTypes from 'prop-types';
-import warning from 'warning';
 import classNames from 'classnames';
-import { polyfill } from 'react-lifecycles-compat';
+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 type {
+  NodeDragEventHandler,
+  NodeDragEventParams,
+  NodeMouseEventHandler,
+  NodeMouseEventParams,
+} from './contextTypes';
 import { TreeContext } from './contextTypes';
+import DropIndicator from './DropIndicator';
+import type { DropIndicatorProps } from './DropIndicator';
+import type {
+  BasicDataNode,
+  DataNode,
+  Direction,
+  EventDataNode,
+  FieldNames,
+  FlattenNode,
+  IconType,
+  Key,
+  KeyEntities,
+  SafeKey,
+  ScrollTo,
+  TreeNodeProps,
+} from './interface';
+import NodeList, { MOTION_KEY, MotionEntity, type NodeListRef } from './NodeList';
+import TreeNode from './TreeNode';
 import {
-  getDataAndAria,
-  getDragNodesKeys,
-  parseCheckedKeys,
-  conductExpandParent,
-  calcSelectedKeys,
-  calcDropPosition,
   arrAdd,
   arrDel,
+  calcDropPosition,
+  calcSelectedKeys,
+  conductExpandParent,
+  getDragChildrenKeys,
+  parseCheckedKeys,
   posToArr,
-  conductCheck,
 } from './util';
-import { DataNode, IconType, Key, FlattenNode, DataEntity, NodeInstance } from './interface';
+import { conductCheck } from './utils/conductUtil';
+import getEntity from './utils/keyUtil';
 import {
-  flattenTreeData,
-  convertTreeToData,
   convertDataToEntities,
+  convertNodePropsToEventData,
+  convertTreeToData,
+  fillFieldNames,
+  flattenTreeData,
+  getTreeNodeProps,
   warningWithoutKey,
 } from './utils/treeUtil';
-import NodeList, { MOTION_KEY, MotionEntity } from './NodeList';
 
-interface CheckInfo {
+const MAX_RETRY_TIMES = 10;
+
+export interface CheckInfo<TreeDataType extends BasicDataNode = DataNode> {
   event: 'check';
-  node: NodeInstance;
+  node: EventDataNode<TreeDataType>;
   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<TreeDataType extends BasicDataNode = DataNode> {
+  dragNode: TreeDataType;
+  dropNode: TreeDataType;
+  dropPosition: -1 | 0 | 1;
+}
+export type AllowDrop<TreeDataType extends BasicDataNode = DataNode> = (
+  options: AllowDropOptions<TreeDataType>,
+) => 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<TreeDataType extends BasicDataNode = DataNode> {
   prefixCls: string;
   className?: string;
   style?: React.CSSProperties;
+  styles?: Partial<Record<SemanticName, React.CSSProperties>>;
+  classNames?: Partial<Record<SemanticName, string>>;
+  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;
-  focusable?: boolean;
   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;
   defaultExpandedKeys?: Key[];
   expandedKeys?: Key[];
   defaultCheckedKeys?: Key[];
-  checkedKeys?: (Key)[] | { checked: (Key)[]; halfChecked: Key[] };
+  checkedKeys?: Key[] | { checked: Key[]; halfChecked: Key[] };
   defaultSelectedKeys?: Key[];
   selectedKeys?: Key[];
-  onClick?: (e: React.MouseEvent, treeNode: DataNode) => void;
-  onDoubleClick?: (e: React.MouseEvent, treeNode: DataNode) => void;
+  allowDrop?: AllowDrop<TreeDataType>;
+  titleRender?: (node: TreeDataType) => React.ReactNode;
+  dropIndicatorRender?: (props: DropIndicatorProps) => React.ReactNode;
+  onFocus?: React.FocusEventHandler<HTMLDivElement>;
+  onBlur?: React.FocusEventHandler<HTMLDivElement>;
+  onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
+  onContextMenu?: React.MouseEventHandler<HTMLDivElement>;
+  onClick?: NodeMouseEventHandler<TreeDataType>;
+  onDoubleClick?: NodeMouseEventHandler<TreeDataType>;
+  onScroll?: React.UIEventHandler<HTMLElement>;
   onExpand?: (
     expandedKeys: Key[],
     info: {
-      node: NodeInstance;
+      node: EventDataNode<TreeDataType>;
       expanded: boolean;
       nativeEvent: MouseEvent;
     },
   ) => void;
-  onCheck?: (checked: { checked: Key[]; halfChecked: Key[] } | Key[], info: CheckInfo) => void;
+  onCheck?: (
+    checked: { checked: Key[]; halfChecked: Key[] } | Key[],
+    info: CheckInfo<TreeDataType>,
+  ) => void;
   onSelect?: (
     selectedKeys: Key[],
     info: {
       event: 'select';
       selected: boolean;
-      node: NodeInstance;
-      selectedNodes: DataNode[];
+      node: EventDataNode<TreeDataType>;
+      selectedNodes: TreeDataType[];
       nativeEvent: MouseEvent;
     },
   ) => void;
@@ -90,42 +151,54 @@ export interface TreeProps {
     loadedKeys: Key[],
     info: {
       event: 'load';
-      node: NodeInstance;
+      node: EventDataNode<TreeDataType>;
     },
   ) => void;
-  loadData?: (treeNode: NodeInstance) => Promise<void>;
+  loadData?: (treeNode: EventDataNode<TreeDataType>) => Promise<any>;
   loadedKeys?: Key[];
-  onMouseEnter?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onMouseLeave?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onRightClick?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onDragStart?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onDragEnter?: (info: {
-    event: React.MouseEvent;
-    node: NodeInstance;
-    expandedKeys: Key[];
-  }) => void;
-  onDragOver?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onDragLeave?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onDragEnd?: (info: { event: React.MouseEvent; node: NodeInstance }) => void;
-  onDrop?: (info: {
-    event: React.MouseEvent;
-    node: NodeInstance;
-    dragNode: NodeInstance;
-    dragNodesKeys: Key[];
-    dropPosition: number;
-    dropToGap: boolean;
-  }) => void;
-  filterTreeNode?: (treeNode: NodeInstance) => boolean;
+  onMouseEnter?: (info: NodeMouseEventParams<TreeDataType>) => void;
+  onMouseLeave?: (info: NodeMouseEventParams<TreeDataType>) => void;
+  onRightClick?: (info: { event: React.MouseEvent; node: EventDataNode<TreeDataType> }) => void;
+  onDragStart?: (info: NodeDragEventParams<TreeDataType>) => void;
+  onDragEnter?: (info: NodeDragEventParams<TreeDataType> & { expandedKeys: Key[] }) => void;
+  onDragOver?: (info: NodeDragEventParams<TreeDataType>) => void;
+  onDragLeave?: (info: NodeDragEventParams<TreeDataType>) => void;
+  onDragEnd?: (info: NodeDragEventParams<TreeDataType>) => void;
+  onDrop?: (
+    info: NodeDragEventParams<TreeDataType> & {
+      dragNode: EventDataNode<TreeDataType>;
+      dragNodesKeys: Key[];
+      dropPosition: number;
+      dropToGap: boolean;
+    },
+  ) => void;
+  /**
+   * Used for `rc-tree-select` only.
+   * Do not use in your production code directly since this will be refactor.
+   */
+  onActiveChange?: (key: Key) => void;
+  filterTreeNode?: (treeNode: EventDataNode<TreeDataType>) => 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<Key, DataEntity>;
+interface TreeState<TreeDataType extends BasicDataNode = DataNode> {
+  keyEntities: KeyEntities<TreeDataType>;
+
+  indent: number | null;
 
   selectedKeys: Key[];
   checkedKeys: Key[];
@@ -134,69 +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: TreeDataType[];
+  flattenNodes: FlattenNode<TreeDataType>[];
 
-  treeData: DataNode[];
-  flattenNodes: FlattenNode[];
+  focused: boolean;
+  activeKey: Key | null;
+
+  // Record if list is changing
+  listChanging: boolean;
 
   prevProps: TreeProps;
-}
 
-class Tree extends React.Component<TreeProps, TreeState> {
-  static propTypes = {
-    prefixCls: PropTypes.string,
-    className: PropTypes.string,
-    style: PropTypes.object,
-    tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-    children: PropTypes.any,
-    treeData: PropTypes.array, // Generate treeNode by children
-    showLine: PropTypes.bool,
-    showIcon: PropTypes.bool,
-    icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
-    focusable: PropTypes.bool,
-    selectable: PropTypes.bool,
-    disabled: PropTypes.bool,
-    multiple: PropTypes.bool,
-    checkable: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]),
-    checkStrictly: PropTypes.bool,
-    draggable: PropTypes.bool,
-    defaultExpandParent: PropTypes.bool,
-    autoExpandParent: PropTypes.bool,
-    defaultExpandAll: PropTypes.bool,
-    defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
-    expandedKeys: PropTypes.arrayOf(PropTypes.string),
-    defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
-    checkedKeys: PropTypes.oneOfType([
-      PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
-      PropTypes.object,
-    ]),
-    defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
-    selectedKeys: PropTypes.arrayOf(PropTypes.string),
-    onClick: PropTypes.func,
-    onDoubleClick: PropTypes.func,
-    onExpand: PropTypes.func,
-    onCheck: PropTypes.func,
-    onSelect: PropTypes.func,
-    onLoad: PropTypes.func,
-    loadData: PropTypes.func,
-    loadedKeys: PropTypes.arrayOf(PropTypes.string),
-    onMouseEnter: PropTypes.func,
-    onMouseLeave: PropTypes.func,
-    onRightClick: PropTypes.func,
-    onDragStart: PropTypes.func,
-    onDragEnter: PropTypes.func,
-    onDragOver: PropTypes.func,
-    onDragLeave: PropTypes.func,
-    onDragEnd: PropTypes.func,
-    onDrop: PropTypes.func,
-    filterTreeNode: PropTypes.func,
-    motion: PropTypes.object,
-    switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
-  };
+  fieldNames: FieldNames;
+}
 
+class Tree<TreeDataType extends DataNode | BasicDataNode = DataNode> extends React.Component<
+  TreeProps<TreeDataType>,
+  TreeState<TreeDataType>
+> {
   static defaultProps = {
     prefixCls: 'rc-tree',
     showLine: false,
@@ -213,13 +253,24 @@ class Tree extends React.Component<TreeProps, TreeState> {
     defaultExpandedKeys: [],
     defaultCheckedKeys: [],
     defaultSelectedKeys: [],
+    dropIndicatorRender: DropIndicator,
+    allowDrop: () => true,
+    expandAction: false,
   };
 
-  delayedDragEnterLogic: Record<Key, number>;
+  static TreeNode = TreeNode;
+
+  destroyed: boolean = false;
+
+  delayedDragEnterLogic: Record<SafeKey, number>;
+
+  loadingRetryTimes: Record<SafeKey, number> = {};
 
-  state: TreeState = {
+  state: TreeState<TreeDataType> = {
     keyEntities: {},
 
+    indent: null,
+
     selectedKeys: [],
     checkedKeys: [],
     halfCheckedKeys: [],
@@ -227,18 +278,69 @@ class Tree extends React.Component<TreeProps, TreeState> {
     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: [],
 
+    focused: false,
+    activeKey: null,
+
+    listChanging: false,
+
     prevProps: null,
+
+    fieldNames: fillFieldNames(),
   };
 
-  dragNode: NodeInstance;
+  dragStartMousePosition = null;
+
+  dragNodeProps: TreeNodeProps<TreeDataType> = null;
+
+  currentMouseOverDroppableNodeKey = null;
+
+  listRef = React.createRef<NodeListRef>();
+
+  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;
@@ -247,12 +349,21 @@ class Tree extends React.Component<TreeProps, TreeState> {
     };
 
     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);
@@ -264,7 +375,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
     // 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,
@@ -272,7 +383,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
 
       // Warning if treeNode not provide key
       if (process.env.NODE_ENV !== 'production') {
-        warningWithoutKey(treeData);
+        warningWithoutKey(treeData, fieldNames);
       }
     }
 
@@ -287,7 +398,17 @@ class Tree extends React.Component<TreeProps, TreeState> {
     } else if (!prevProps && props.defaultExpandAll) {
       const cloneKeyEntities = { ...keyEntities };
       delete cloneKeyEntities[MOTION_KEY];
-      newState.expandedKeys = Object.keys(cloneKeyEntities);
+
+      // 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
@@ -295,11 +416,16 @@ class Tree extends React.Component<TreeProps, TreeState> {
           : props.defaultExpandedKeys;
     }
 
+    if (!newState.expandedKeys) {
+      delete newState.expandedKeys;
+    }
+
     // ================ flattenNodes =================
     if (treeData || newState.expandedKeys) {
-      const flattenNodes: FlattenNode[] = flattenTreeData(
+      const flattenNodes = flattenTreeData<DataNode>(
         treeData || prevState.treeData,
         newState.expandedKeys || prevState.expandedKeys,
+        fieldNames,
       );
       newState.flattenNodes = flattenNodes;
     }
@@ -315,7 +441,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
 
     // ================= checkedKeys =================
     if (props.checkable) {
-      let checkedKeyEntity;
+      let checkedKeyEntity: { checkedKeys?: Key[]; halfCheckedKeys?: Key[] };
 
       if (needSync('checkedKeys')) {
         checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {};
@@ -341,6 +467,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
         newState.halfCheckedKeys = halfCheckedKeys;
       }
     }
+
     // ================= loadedKeys ==================
     if (needSync('loadedKeys')) {
       newState.loadedKeys = props.loadedKeys;
@@ -349,250 +476,440 @@ class Tree extends React.Component<TreeProps, TreeState> {
     return newState;
   }
 
-  onNodeDragStart = (event, node: NodeInstance) => {
+  onNodeDragStart: NodeDragEventHandler<TreeDataType, HTMLDivElement> = (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),
-      expandedKeys: arrDel(expandedKeys, eventKey),
+      draggingNodeKey: eventKey,
+      dragChildrenKeys: getDragChildrenKeys(eventKey, keyEntities),
+      indent: this.listRef.current.getIndentWidth(),
     });
 
-    if (onDragStart) {
-      onDragStart({ event, node });
-    }
+    this.setExpandedKeys(newExpandedKeys);
+
+    window.addEventListener('dragend', this.onWindowDragEnd);
+
+    onDragStart?.({ event, node: convertNodePropsToEventData<TreeDataType>(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 = (event, node) => {
-    const { expandedKeys, keyEntities } = this.state;
-    const { onDragEnter } = this.props;
-    const { pos, eventKey } = node.props;
+  onNodeDragEnter = (
+    event: React.DragEvent<HTMLDivElement>,
+    nodeProps: TreeNodeProps<TreeDataType>,
+  ) => {
+    const { expandedKeys, keyEntities, dragChildrenKeys, flattenNodes, indent } = this.state;
+    const { onDragEnter, onExpand, allowDrop, direction } = this.props;
+    const { pos, eventKey } = nodeProps;
+
+    // record the key of node which is latest entered, used in dragleave event.
+    if (this.currentMouseOverDroppableNodeKey !== eventKey) {
+      this.currentMouseOverDroppableNodeKey = eventKey;
+    }
 
-    if (!this.dragNode) return;
+    if (!this.dragNodeProps) {
+      this.resetDragState();
+      return;
+    }
 
-    const dropPosition = calcDropPosition(event, node);
+    const {
+      dropPosition,
+      dropLevelOffset,
+      dropTargetKey,
+      dropContainerKey,
+      dropTargetPos,
+      dropAllowed,
+      dragOverNodeKey,
+    } = calcDropPosition<TreeDataType>(
+      event,
+      this.dragNodeProps,
+      nodeProps,
+      indent,
+      this.dragStartMousePosition,
+      allowDrop,
+      flattenNodes,
+      keyEntities,
+      expandedKeys,
+      direction,
+    );
 
-    // Skip if drag node is self
-    if (this.dragNode.props.eventKey === eventKey && dropPosition === 0) {
-      this.setState({
-        dragOverNodeKey: '',
-        dropPosition: null,
-      });
+    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)) {
-          this.setState({
-            expandedKeys: newExpandedKeys,
-          });
+        if (!this.props.hasOwnProperty('expandedKeys')) {
+          this.setExpandedKeys(newExpandedKeys);
         }
 
-        if (onDragEnter) {
-          onDragEnter({ event, node, expandedKeys: newExpandedKeys });
-        }
-      }, 400);
-    }, 0);
+        onExpand?.(newExpandedKeys, {
+          node: convertNodePropsToEventData<TreeDataType>(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<TreeDataType>(nodeProps),
+      expandedKeys,
+    });
   };
 
-  onNodeDragOver = (event, node) => {
-    const { onDragOver } = this.props;
-    const { eventKey } = node.props;
+  onNodeDragOver = (
+    event: React.DragEvent<HTMLDivElement>,
+    nodeProps: TreeNodeProps<TreeDataType>,
+  ) => {
+    const { dragChildrenKeys, flattenNodes, keyEntities, expandedKeys, indent } = this.state;
+    const { onDragOver, allowDrop, direction } = this.props;
 
-    // Update drag position
-    if (this.dragNode && eventKey === this.state.dragOverNodeKey) {
-      const dropPosition = calcDropPosition(event, node);
+    if (!this.dragNodeProps) {
+      return;
+    }
 
-      if (dropPosition === this.state.dropPosition) return;
+    const {
+      dropPosition,
+      dropLevelOffset,
+      dropTargetKey,
+      dropContainerKey,
+      dropTargetPos,
+      dropAllowed,
+      dragOverNodeKey,
+    } = calcDropPosition<TreeDataType>(
+      event,
+      this.dragNodeProps,
+      nodeProps,
+      indent,
+      this.dragStartMousePosition,
+      allowDrop,
+      flattenNodes,
+      keyEntities,
+      expandedKeys,
+      direction,
+    );
+
+    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 });
-    }
+    onDragOver?.({ event, node: convertNodePropsToEventData<TreeDataType>(nodeProps) });
   };
 
-  onNodeDragLeave = (event, node) => {
+  onNodeDragLeave: NodeDragEventHandler<TreeDataType> = (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<TreeDataType>(nodeProps) });
+  };
 
-    if (onDragLeave) {
-      onDragLeave({ event, node });
-    }
+  // 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 = (event, node) => {
+  // if onNodeDragEnd is called, onWindowDragEnd won't be called since stopPropagation() is called
+  onNodeDragEnd: NodeDragEventHandler<TreeDataType> = (event, nodeProps) => {
     const { onDragEnd } = this.props;
     this.setState({
-      dragOverNodeKey: '',
+      dragOverNodeKey: null,
     });
+
     this.cleanDragState();
 
-    if (onDragEnd) {
-      onDragEnd({ event, node });
-    }
+    onDragEnd?.({ event, node: convertNodePropsToEventData<TreeDataType>(nodeProps) });
+
+    this.dragNodeProps = null;
 
-    this.dragNode = null;
+    window.removeEventListener('dragend', this.onWindowDragEnd);
   };
 
-  onNodeDrop = (event, node) => {
-    const { dragNodesKeys = [], dropPosition } = this.state;
+  onNodeDrop = (
+    event: React.DragEvent<HTMLDivElement>,
+    _: TreeNodeProps<TreeDataType>,
+    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 posArr = posToArr(pos);
+    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(dropTargetPos);
 
     const dropResult = {
       event,
-      node,
-      dragNode: this.dragNode,
-      dragNodesKeys: dragNodesKeys.slice(),
+      node: convertNodePropsToEventData<TreeDataType>(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;
   };
 
-  onNodeClick = (e, treeNode) => {
-    const { onClick } = this.props;
-    if (onClick) {
-      onClick(e, treeNode);
+  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<TreeDataType>({
+      ...getTreeNodeProps(key, this.getTreeNodeRequiredProps()),
+      data: node.data,
+    });
+
+    this.setExpandedKeys(expanded ? arrDel(expandedKeys, key) : arrAdd(expandedKeys, key));
+    this.onNodeExpand(e as React.MouseEvent<HTMLDivElement>, eventNode);
+  };
+
+  onNodeClick: NodeMouseEventHandler<TreeDataType> = (e, treeNode) => {
+    const { onClick, expandAction } = this.props;
+
+    if (expandAction === 'click') {
+      this.triggerExpandActionExpand(e, treeNode);
     }
+
+    onClick?.(e, treeNode);
   };
 
-  onNodeDoubleClick = (e, treeNode) => {
-    const { onDoubleClick } = this.props;
-    if (onDoubleClick) {
-      onDoubleClick(e, treeNode);
+  onNodeDoubleClick: NodeMouseEventHandler<TreeDataType> = (e, treeNode) => {
+    const { onDoubleClick, expandAction } = this.props;
+
+    if (expandAction === 'doubleClick') {
+      this.triggerExpandActionExpand(e, treeNode);
     }
+
+    onDoubleClick?.(e, treeNode);
   };
 
-  onNodeSelect = (e, treeNode) => {
+  onNodeSelect: NodeMouseEventHandler<TreeDataType> = (e, treeNode) => {
     let { selectedKeys } = this.state;
-    const { keyEntities } = this.state;
+    const { keyEntities, fieldNames } = this.state;
     const { onSelect, multiple } = this.props;
-    const { selected, eventKey } = treeNode.props;
+    const { selected } = treeNode;
+    const key = treeNode[fieldNames.key];
     const targetSelected = !selected;
 
     // Update selected keys
     if (!targetSelected) {
-      selectedKeys = arrDel(selectedKeys, eventKey);
+      selectedKeys = arrDel(selectedKeys, key);
     } else if (!multiple) {
-      selectedKeys = [eventKey];
+      selectedKeys = [key];
     } else {
-      selectedKeys = arrAdd(selectedKeys, eventKey);
+      selectedKeys = arrAdd(selectedKeys, key);
     }
 
     // [Legacy] Not found related usage in doc or upper libs
     const selectedNodes = selectedKeys
-      .map(key => {
-        const entity = keyEntities[key];
-        if (!entity) return null;
-
-        return entity.node;
+      .map(selectedKey => {
+        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, treeNode, checked) => {
+  onNodeCheck = (
+    e: React.MouseEvent<HTMLSpanElement>,
+    treeNode: EventDataNode<TreeDataType>,
+    checked: boolean,
+  ) => {
     const {
       keyEntities,
       checkedKeys: oriCheckedKeys,
       halfCheckedKeys: oriHalfCheckedKeys,
     } = this.state;
     const { checkStrictly, onCheck } = this.props;
-    const {
-      props: { eventKey },
-    } = treeNode;
+    const { key } = treeNode;
 
     // Prepare trigger arguments
-    let checkedObj;
-    const eventObj: Partial<CheckInfo> = {
+    let checkedObj: { checked: Key[]; halfChecked: Key[] } | React.Key[];
+
+    const eventObj: Partial<CheckInfo<TreeDataType>> = {
       event: 'check',
       node: treeNode,
       checked,
@@ -600,23 +917,34 @@ class Tree extends React.Component<TreeProps, TreeState> {
     };
 
     if (checkStrictly) {
-      const checkedKeys = checked
-        ? arrAdd(oriCheckedKeys, eventKey)
-        : arrDel(oriCheckedKeys, eventKey);
-      const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey);
+      const checkedKeys = checked ? arrAdd(oriCheckedKeys, key) : arrDel(oriCheckedKeys, key);
+      const halfCheckedKeys = arrDel(oriHalfCheckedKeys, key);
       checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys };
 
       eventObj.checkedNodes = checkedKeys
-        .map(key => keyEntities[key])
-        .filter(entity => entity)
+        .map(checkedKey => getEntity(keyEntities, checkedKey))
+        .filter(Boolean)
         .map(entity => entity.node);
 
       this.setUncontrolledState({ checkedKeys });
     } else {
-      const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
-        checkedKeys: oriCheckedKeys,
-        halfCheckedKeys: oriHalfCheckedKeys,
-      });
+      // Always fill first
+      let { checkedKeys, halfCheckedKeys } = conductCheck(
+        [...oriCheckedKeys, key],
+        true,
+        keyEntities,
+      );
+
+      // If remove, we do it again to correction
+      if (!checked) {
+        const keySet = new Set(checkedKeys);
+        keySet.delete(key);
+        ({ checkedKeys, halfCheckedKeys } = conductCheck(
+          Array.from(keySet),
+          { checked: false, halfCheckedKeys },
+          keyEntities,
+        ));
+      }
 
       checkedObj = checkedKeys;
 
@@ -625,8 +953,8 @@ class Tree extends React.Component<TreeProps, TreeState> {
       eventObj.checkedNodesPositions = [];
       eventObj.halfCheckedKeys = halfCheckedKeys;
 
-      checkedKeys.forEach(key => {
-        const entity = keyEntities[key];
+      checkedKeys.forEach(checkedKey => {
+        const entity = getEntity(keyEntities, checkedKey);
         if (!entity) return;
 
         const { node, pos } = entity;
@@ -635,173 +963,413 @@ class Tree extends React.Component<TreeProps, TreeState> {
         eventObj.checkedNodesPositions.push({ node, pos });
       });
 
-      this.setUncontrolledState({
-        checkedKeys,
-        halfCheckedKeys,
-      });
+      this.setUncontrolledState({ checkedKeys }, false, { halfCheckedKeys });
     }
 
-    if (onCheck) {
-      onCheck(checkedObj, eventObj as CheckInfo);
-    }
+    onCheck?.(checkedObj, eventObj as CheckInfo<TreeDataType>);
   };
 
-  onNodeLoad = treeNode =>
-    new Promise(resolve => {
+  onNodeLoad = (treeNode: EventDataNode<TreeDataType>) => {
+    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<void>((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 { eventKey } = treeNode.props;
-
-        if (
-          !loadData ||
-          loadedKeys.indexOf(eventKey) !== -1 ||
-          loadingKeys.indexOf(eventKey) !== -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, eventKey);
-          const newLoadingKeys = arrDel(currentLoadingKeys, eventKey);
-
-          // 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();
-        });
+            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);
+          });
 
         return {
-          loadingKeys: arrAdd(loadingKeys, eventKey),
+          loadingKeys: arrAdd(loadingKeys, key),
         };
       });
     });
 
-  onNodeExpand = (e, treeNode) => {
+    // Not care warning if we ignore this
+    loadPromise.catch(() => {});
+
+    return loadPromise;
+  };
+
+  onNodeMouseEnter: NodeMouseEventHandler<TreeDataType> = (event, node) => {
+    const { onMouseEnter } = this.props;
+
+    onMouseEnter?.({ event, node });
+  };
+
+  onNodeMouseLeave: NodeMouseEventHandler<TreeDataType> = (event, node) => {
+    const { onMouseLeave } = this.props;
+
+    onMouseLeave?.({ event, node });
+  };
+
+  onNodeContextMenu: NodeMouseEventHandler<TreeDataType> = (event, node) => {
+    const { onRightClick } = this.props;
+    if (onRightClick) {
+      event.preventDefault();
+      onRightClick({ event, node });
+    }
+  };
+
+  onFocus: React.FocusEventHandler<HTMLDivElement> = (...args) => {
+    const { onFocus } = this.props;
+    this.setState({ focused: true });
+
+    onFocus?.(...args);
+  };
+
+  onBlur: React.FocusEventHandler<HTMLDivElement> = (...args) => {
+    const { onBlur } = this.props;
+    this.setState({ focused: false });
+    this.onActiveChange(null);
+
+    onBlur?.(...args);
+  };
+
+  getTreeNodeRequiredProps = () => {
+    const {
+      expandedKeys,
+      selectedKeys,
+      loadedKeys,
+      loadingKeys,
+      checkedKeys,
+      halfCheckedKeys,
+      dragOverNodeKey,
+      dropPosition,
+      keyEntities,
+    } = this.state;
+    return {
+      expandedKeys: expandedKeys || [],
+      selectedKeys: selectedKeys || [],
+      loadedKeys: loadedKeys || [],
+      loadingKeys: loadingKeys || [],
+      checkedKeys: checkedKeys || [],
+      halfCheckedKeys: halfCheckedKeys || [],
+      dragOverNodeKey,
+      dropPosition,
+      keyEntities: keyEntities,
+    };
+  };
+
+  // =========================== Expanded ===========================
+  /** Set uncontrolled `expandedKeys`. This will also auto update `flattenNodes`. */
+  setExpandedKeys = (expandedKeys: Key[]) => {
+    const { treeData, fieldNames } = this.state;
+    const flattenNodes = flattenTreeData<TreeDataType>(treeData, expandedKeys, fieldNames);
+    this.setUncontrolledState({ expandedKeys, flattenNodes }, true);
+  };
+
+  onNodeExpand = (e: React.MouseEvent<HTMLDivElement>, treeNode: EventDataNode<TreeDataType>) => {
     let { expandedKeys } = this.state;
-    const { treeData } = this.state;
+    const { listChanging, fieldNames } = this.state;
     const { onExpand, loadData } = this.props;
-    const { eventKey, expanded } = treeNode.props;
+    const { expanded } = treeNode;
+    const key = treeNode[fieldNames.key];
+
+    // Do nothing when motion is in progress
+    if (listChanging) {
+      return;
+    }
 
     // Update selected keys
-    const index = expandedKeys.indexOf(eventKey);
+    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, eventKey);
-    } else {
-      expandedKeys = arrDel(expandedKeys, eventKey);
-    }
+    expandedKeys = targetExpanded ? arrAdd(expandedKeys, key) : arrDel(expandedKeys, key);
 
-    const flattenNodes: FlattenNode[] = flattenTreeData(treeData, expandedKeys);
-    this.setUncontrolledState({ expandedKeys, flattenNodes });
+    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);
-      return loadPromise
-        ? loadPromise.then(() => {
+      if (loadPromise) {
+        loadPromise
+          .then(() => {
             // [Legacy] Refresh logic
-            const newFlattenTreeData = flattenTreeData(this.state.treeData, expandedKeys);
+            const newFlattenTreeData = flattenTreeData<TreeDataType>(
+              this.state.treeData,
+              expandedKeys,
+              fieldNames,
+            );
             this.setUncontrolledState({ flattenNodes: newFlattenTreeData });
           })
-        : null;
+          .catch(() => {
+            const { expandedKeys: currentExpandedKeys } = this.state;
+            const expandedKeysToRestore = arrDel(currentExpandedKeys, key);
+            this.setExpandedKeys(expandedKeysToRestore);
+          });
+      }
     }
+  };
 
-    return null;
+  onListChangeStart = () => {
+    this.setUncontrolledState({
+      listChanging: true,
+    });
   };
 
-  onNodeMouseEnter = (event, node) => {
-    const { onMouseEnter } = this.props;
-    if (onMouseEnter) {
-      onMouseEnter({ event, node });
+  onListChangeEnd = () => {
+    setTimeout(() => {
+      this.setUncontrolledState({
+        listChanging: false,
+      });
+    });
+  };
+
+  // =========================== Keyboard ===========================
+  onActiveChange = (newActiveKey: Key | null) => {
+    const { activeKey } = this.state;
+    const { onActiveChange, itemScrollOffset = 0 } = this.props;
+
+    if (activeKey === newActiveKey) {
+      return;
+    }
+
+    this.setState({ activeKey: newActiveKey });
+    if (newActiveKey !== null) {
+      this.scrollTo({ key: newActiveKey, offset: itemScrollOffset });
     }
+
+    onActiveChange?.(newActiveKey);
   };
 
-  onNodeMouseLeave = (event, node) => {
-    const { onMouseLeave } = this.props;
-    if (onMouseLeave) {
-      onMouseLeave({ event, node });
+  getActiveItem = () => {
+    const { activeKey, flattenNodes } = this.state;
+    if (activeKey === null) {
+      return null;
     }
+
+    return flattenNodes.find(({ key }) => key === activeKey) || null;
   };
 
-  onNodeContextMenu = (event, node) => {
-    const { onRightClick } = this.props;
-    if (onRightClick) {
-      event.preventDefault();
-      onRightClick({ event, node });
+  offsetActiveKey = (offset: number) => {
+    const { flattenNodes, activeKey } = this.state;
+
+    let index = flattenNodes.findIndex(({ key }) => key === activeKey);
+
+    // Align with index
+    if (index === -1 && offset < 0) {
+      index = flattenNodes.length;
+    }
+
+    index = (index + offset + flattenNodes.length) % flattenNodes.length;
+
+    const item = flattenNodes[index];
+    if (item) {
+      const { key } = item;
+      this.onActiveChange(key);
+    } else {
+      this.onActiveChange(null);
     }
   };
 
+  onKeyDown: React.KeyboardEventHandler<HTMLDivElement> = event => {
+    const { activeKey, expandedKeys, checkedKeys, fieldNames } = this.state;
+    const { onKeyDown, checkable, selectable } = this.props;
+
+    // >>>>>>>>>> Direction
+    switch (event.which) {
+      case KeyCode.UP: {
+        this.offsetActiveKey(-1);
+        event.preventDefault();
+        break;
+      }
+      case KeyCode.DOWN: {
+        this.offsetActiveKey(1);
+        event.preventDefault();
+        break;
+      }
+    }
+
+    // >>>>>>>>>> Expand & Selection
+    const activeItem = this.getActiveItem();
+    if (activeItem && activeItem.data) {
+      const treeNodeRequiredProps = this.getTreeNodeRequiredProps();
+
+      const expandable =
+        activeItem.data.isLeaf === false || !!(activeItem.data[fieldNames.children] || []).length;
+      const eventNode = convertNodePropsToEventData<TreeDataType>({
+        ...getTreeNodeProps(activeKey, treeNodeRequiredProps),
+        data: activeItem.data,
+        active: true,
+      });
+
+      switch (event.which) {
+        // >>> Expand
+        case KeyCode.LEFT: {
+          // Collapse if possible
+          if (expandable && expandedKeys.includes(activeKey)) {
+            this.onNodeExpand({} as React.MouseEvent<HTMLDivElement>, eventNode);
+          } else if (activeItem.parent) {
+            this.onActiveChange(activeItem.parent.key);
+          }
+          event.preventDefault();
+          break;
+        }
+        case KeyCode.RIGHT: {
+          // Expand if possible
+          if (expandable && !expandedKeys.includes(activeKey)) {
+            this.onNodeExpand({} as React.MouseEvent<HTMLDivElement>, eventNode);
+          } else if (activeItem.children && activeItem.children.length) {
+            this.onActiveChange(activeItem.children[0].key);
+          }
+          event.preventDefault();
+          break;
+        }
+
+        // Selection
+        case KeyCode.ENTER:
+        case KeyCode.SPACE: {
+          if (
+            checkable &&
+            !eventNode.disabled &&
+            eventNode.checkable !== false &&
+            !eventNode.disableCheckbox
+          ) {
+            this.onNodeCheck(
+              {} as React.MouseEvent<HTMLDivElement>,
+              eventNode,
+              !checkedKeys.includes(activeKey),
+            );
+          } else if (
+            !checkable &&
+            selectable &&
+            !eventNode.disabled &&
+            eventNode.selectable !== false
+          ) {
+            this.onNodeSelect({} as React.MouseEvent<HTMLDivElement>, eventNode);
+          }
+          break;
+        }
+      }
+    }
+
+    onKeyDown?.(event);
+  };
+
   /**
    * Only update the value which is not in props
    */
-  setUncontrolledState = state => {
-    let needSync = false;
-    const newState = {};
-
-    Object.keys(state).forEach(name => {
-      if (name in this.props) return;
+  setUncontrolledState = (
+    state: Partial<TreeState<TreeDataType>>,
+    atomic: boolean = false,
+    forceState: Partial<TreeState<TreeDataType>> | null = null,
+  ) => {
+    if (!this.destroyed) {
+      let needSync = false;
+      let allPassed = true;
+      const newState = {};
+
+      Object.keys(state).forEach(name => {
+        if (this.props.hasOwnProperty(name)) {
+          allPassed = false;
+          return;
+        }
 
-      needSync = true;
-      newState[name] = state[name];
-    });
+        needSync = true;
+        newState[name] = state[name];
+      });
 
-    if (needSync) {
-      this.setState(newState);
+      if (needSync && (!atomic || allPassed)) {
+        this.setState({
+          ...newState,
+          ...forceState,
+        } as TreeState<TreeDataType>);
+      }
     }
   };
 
+  scrollTo: ScrollTo = scroll => {
+    this.listRef.current.scrollTo(scroll);
+  };
+
   render() {
     const {
+      focused,
       flattenNodes,
       keyEntities,
-      expandedKeys,
-      selectedKeys,
-      checkedKeys,
-      loadedKeys,
-      loadingKeys,
-      halfCheckedKeys,
-      dragging,
-      dragOverNodeKey,
+      draggingNodeKey,
+      activeKey,
+      dropLevelOffset,
+      dropContainerKey,
+      dropTargetKey,
       dropPosition,
+      dragOverNodeKey,
+      indent,
     } = this.state;
     const {
       prefixCls,
       className,
-      focusable,
       style,
+      styles,
+      classNames: treeClassNames,
       showLine,
+      focusable,
       tabIndex = 0,
       selectable,
       showIcon,
@@ -816,73 +1384,121 @@ class Tree extends React.Component<TreeProps, TreeState> {
       filterTreeNode,
       height,
       itemHeight,
+      scrollWidth,
+      virtual,
+      titleRender,
+      dropIndicatorRender,
+      onContextMenu,
+      onScroll,
+      direction,
+      rootClassName,
+      rootStyle,
     } = this.props;
-    const domProps: React.HTMLAttributes<HTMLUListElement> = getDataAndAria(this.props);
 
-    if (focusable) {
-      domProps.tabIndex = tabIndex;
+    const domProps: React.HTMLAttributes<HTMLDivElement> = 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 = {};
+      }
     }
 
-    return (
-      <TreeContext.Provider
-        value={{
-          prefixCls,
-          selectable,
-          showIcon,
-          icon,
-          switcherIcon,
-          draggable,
-          checkable,
-          checkStrictly,
-          disabled,
-          keyEntities,
+    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,
-
-          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,
-        }}
-      >
-        <NodeList
-          className={classNames(prefixCls, className, {
+    return (
+      <TreeContext.Provider value={contextValue}>
+        <div
+          className={classNames(prefixCls, className, rootClassName, {
             [`${prefixCls}-show-line`]: showLine,
+            [`${prefixCls}-focused`]: focused,
+            [`${prefixCls}-active-focused`]: activeKey !== null,
           })}
-          prefixCls={prefixCls}
-          style={style}
-          data={flattenNodes}
-          keyEntities={keyEntities}
-          expandedKeys={expandedKeys}
-          selectedKeys={selectedKeys}
-          checkedKeys={checkedKeys}
-          loadedKeys={loadedKeys}
-          loadingKeys={loadingKeys}
-          halfCheckedKeys={halfCheckedKeys}
-          dragOverNodeKey={dragOverNodeKey}
-          dropPosition={dropPosition}
-          motion={motion}
-          dragging={dragging}
-          height={height}
-          itemHeight={itemHeight}
-        />
+          style={rootStyle}
+        >
+          <NodeList
+            ref={this.listRef}
+            prefixCls={prefixCls}
+            style={style}
+            data={flattenNodes}
+            disabled={disabled}
+            selectable={selectable}
+            checkable={!!checkable}
+            motion={motion}
+            dragging={draggingNodeKey !== null}
+            height={height}
+            itemHeight={itemHeight}
+            virtual={virtual}
+            focusable={focusable}
+            focused={focused}
+            tabIndex={tabIndex}
+            activeItem={this.getActiveItem()}
+            onFocus={this.onFocus}
+            onBlur={this.onBlur}
+            onKeyDown={this.onKeyDown}
+            onActiveChange={this.onActiveChange}
+            onListChangeStart={this.onListChangeStart}
+            onListChangeEnd={this.onListChangeEnd}
+            onContextMenu={onContextMenu}
+            onScroll={onScroll}
+            scrollWidth={scrollWidth}
+            {...this.getTreeNodeRequiredProps()}
+            {...domProps}
+          />
+        </div>
       </TreeContext.Provider>
     );
   }
 }
 
-polyfill(Tree);
-
 export default Tree;
diff --git a/src/TreeNode.tsx b/src/TreeNode.tsx
index d94628ead..06c648e77 100644
--- a/src/TreeNode.tsx
+++ b/src/TreeNode.tsx
@@ -1,569 +1,472 @@
-import * as React from 'react';
-import PropTypes from 'prop-types';
+import React from 'react';
 import classNames from 'classnames';
-// @ts-ignore
-import { polyfill } from 'react-lifecycles-compat';
-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';
 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<HTMLDivElement>;
-  /** New added in Tree for easy data access */
-  data?: DataNode;
-  isStart?: boolean[];
-  isEnd?: boolean[];
-
-  // 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<Readonly<TreeNodeProps>> = 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<HTMLSpanElement>(null);
+
+  const [dragNodeHighlight, setDragNodeHighlight] = React.useState<boolean>(false);
+
+  // ======= State: Disabled State =======
+  const isDisabled = !!(context.disabled || props.disabled || unstableContext.nodeDisabled?.(data));
+
+  const isCheckable = React.useMemo<React.ReactNode>(() => {
+    // 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<InternalTreeNodeProps, TreeNodeState> {
-  static propTypes = {
-    eventKey: PropTypes.string, // Pass by parent `cloneElement`
-    prefixCls: PropTypes.string,
-    className: PropTypes.string,
-    style: PropTypes.object,
-    onSelect: PropTypes.func,
-
-    // By parent
-    expanded: PropTypes.bool,
-    selected: PropTypes.bool,
-    checked: PropTypes.bool,
-    loaded: PropTypes.bool,
-    loading: PropTypes.bool,
-    halfChecked: PropTypes.bool,
-    title: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
-    dragOver: PropTypes.bool,
-    dragOverGapTop: PropTypes.bool,
-    dragOverGapBottom: PropTypes.bool,
-    pos: PropTypes.string,
-
-    // By user
-    isLeaf: PropTypes.bool,
-    checkable: PropTypes.bool,
-    selectable: PropTypes.bool,
-    disabled: PropTypes.bool,
-    disableCheckbox: PropTypes.bool,
-    icon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
-    switcherIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
+  // ======= Event Handlers: Selection and Check =======
+  const onSelect = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    if (isDisabled) {
+      return;
+    }
+    context.onNodeSelect(e, convertNodePropsToEventData(props));
   };
 
-  public state = {
-    dragNodeHighlight: false,
+  const onCheck = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    if (isDisabled) {
+      return;
+    }
+    if (!isCheckable || props.disableCheckbox) {
+      return;
+    }
+    context.onNodeCheck(e, convertNodePropsToEventData(props), !checked);
   };
 
-  public selectHandle: HTMLSpanElement;
-
-  // Isomorphic needn't load data in server side
-  componentDidMount() {
-    this.syncLoadData(this.props);
-  }
-
-  componentDidUpdate() {
-    this.syncLoadData(this.props);
-  }
+  // ======= State: Selectable Check =======
+  const isSelectable = React.useMemo<boolean>(() => {
+    // Ignore when selectable is undefined or null
+    if (typeof selectable === 'boolean') {
+      return selectable;
+    }
+    return context.selectable;
+  }, [selectable, context.selectable]);
 
-  onSelectorClick = e => {
+  const onSelectorClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
     // Click trigger before select/check operation
-    const {
-      context: { onNodeClick },
-    } = this.props;
-    onNodeClick(e, this);
-
-    if (this.isSelectable()) {
-      this.onSelect(e);
+    context.onNodeClick(e, convertNodePropsToEventData(props));
+    if (isSelectable) {
+      onSelect(e);
     } else {
-      this.onCheck(e);
+      onCheck(e);
     }
   };
 
-  onSelectorDoubleClick = e => {
-    const {
-      context: { onNodeDoubleClick },
-    } = this.props;
-    onNodeDoubleClick(e, this);
+  const onSelectorDoubleClick = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    context.onNodeDoubleClick(e, convertNodePropsToEventData(props));
   };
 
-  onSelect = e => {
-    if (this.isDisabled()) return;
-
-    const {
-      context: { onNodeSelect },
-    } = this.props;
-    e.preventDefault();
-    onNodeSelect(e, this);
+  const onMouseEnter = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    context.onNodeMouseEnter(e, convertNodePropsToEventData(props));
   };
 
-  onCheck = e => {
-    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, this, targetChecked);
-  };
-
-  onMouseEnter = e => {
-    const {
-      context: { onNodeMouseEnter },
-    } = this.props;
-    onNodeMouseEnter(e, this);
+  const onMouseLeave = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    context.onNodeMouseLeave(e, convertNodePropsToEventData(props));
   };
 
-  onMouseLeave = e => {
-    const {
-      context: { onNodeMouseLeave },
-    } = this.props;
-    onNodeMouseLeave(e, this);
+  const onContextMenu = (e: React.MouseEvent<HTMLSpanElement, MouseEvent>) => {
+    context.onNodeContextMenu(e, convertNodePropsToEventData(props));
   };
 
-  onContextMenu = e => {
-    const {
-      context: { onNodeContextMenu },
-    } = this.props;
-    onNodeContextMenu(e, this);
-  };
-
-  onDragStart = e => {
-    const {
-      context: { onNodeDragStart },
-    } = this.props;
+  // ======= Drag: Drag Enabled =======
+  const isDraggable = React.useMemo<boolean>(() => {
+    return !!(
+      context.draggable &&
+      (!context.draggable.nodeDraggable || context.draggable.nodeDraggable(data))
+    );
+  }, [context.draggable, data]);
 
+  // ======= Drag: Drag Event Handlers =======
+  const onDragStart = (e: React.DragEvent<HTMLDivElement>) => {
     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 => {
-    const {
-      context: { onNodeDragEnter },
-    } = this.props;
-
+  const onDragEnter = (e: React.DragEvent<HTMLDivElement>) => {
     e.preventDefault();
     e.stopPropagation();
-    onNodeDragEnter(e, this);
+    context.onNodeDragEnter(e, props);
   };
 
-  onDragOver = e => {
-    const {
-      context: { onNodeDragOver },
-    } = this.props;
-
+  const onDragOver = (e: React.DragEvent<HTMLDivElement>) => {
     e.preventDefault();
     e.stopPropagation();
-    onNodeDragOver(e, this);
+    context.onNodeDragOver(e, props);
   };
 
-  onDragLeave = e => {
-    const {
-      context: { onNodeDragLeave },
-    } = this.props;
-
+  const onDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
     e.stopPropagation();
-    onNodeDragLeave(e, this);
+    context.onNodeDragLeave(e, props);
   };
 
-  onDragEnd = e => {
-    const {
-      context: { onNodeDragEnd },
-    } = this.props;
-
+  const onDragEnd = (e: React.DragEvent<HTMLDivElement>) => {
     e.stopPropagation();
-    this.setState({
-      dragNodeHighlight: false,
-    });
-    onNodeDragEnd(e, this);
+    setDragNodeHighlight(false);
+    context.onNodeDragEnd(e, props);
   };
 
-  onDrop = e => {
-    const {
-      context: { onNodeDrop },
-    } = this.props;
-
+  const onDrop = (e: React.DragEvent<HTMLDivElement>) => {
     e.preventDefault();
     e.stopPropagation();
-    this.setState({
-      dragNodeHighlight: false,
-    });
-    onNodeDrop(e, this);
+    setDragNodeHighlight(false);
+    context.onNodeDrop(e, props);
   };
 
-  // Disabled item still can be switch
-  onExpand = e => {
-    const {
-      context: { onNodeExpand },
-    } = this.props;
-    onNodeExpand(e, this);
-  };
-
-  // Drag usage
-  setSelectHandle = node => {
-    this.selectHandle = node;
-  };
-
-  getNodeState = () => {
-    const { expanded } = this.props;
-
-    if (this.isLeaf()) {
-      return null;
+  // ======= Expand: Node Expansion =======
+  const onExpand: React.MouseEventHandler<HTMLDivElement> = 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<boolean>(() => {
+    const { children } = getEntity(context.keyEntities, eventKey) || {};
+    return Boolean((children || []).length);
+  }, [context.keyEntities, eventKey]);
 
+  // ======= State: Leaf Check =======
+  const memoizedIsLeaf = React.useMemo<boolean>(() => {
     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;
-
-    // Follow the logic of Selectable
-    if (disabled === false) {
-      return false;
+  // ============== Effect ==============
+  React.useEffect(() => {
+    // Load data to avoid default expanded tree without data
+    if (loading) {
+      return;
     }
-
-    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;
-
     // 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(this);
-      }
+      context.onNodeLoad(convertNodePropsToEventData(props));
     }
-  };
-
-  isSelectable() {
-    const { selectable } = this.props;
-    const {
-      context: { selectable: treeSelectable },
-    } = this.props;
+  }, [loading, context.loadData, context.onNodeLoad, expanded, memoizedIsLeaf, props]);
 
-    // Ignore when selectable is undefined or null
-    if (typeof selectable === 'boolean') {
-      return selectable;
+  // ==================== Render: Drag Handler ====================
+  const dragHandlerNode = React.useMemo<React.ReactNode>(() => {
+    if (!context.draggable?.icon) {
+      return null;
     }
-
-    return treeSelectable;
-  }
+    return <span className={`${context.prefixCls}-draggable-icon`}>{context.draggable.icon}</span>;
+  }, [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 (
-        <span className={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
-          {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 ? (
+        <span
+          className={classNames(
+            `${context.prefixCls}-switcher`,
+            `${context.prefixCls}-switcher-noop`,
+          )}
+        >
+          {switcherIconDom}
         </span>
-      );
+      ) : null;
     }
-
-    const switcherCls = classNames(
-      `${prefixCls}-switcher`,
-      `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
-    );
-    return (
-      <span onClick={this.onExpand} className={switcherCls}>
-        {typeof switcherIcon === 'function'
-          ? switcherIcon({ ...this.props, isLeaf: false })
-          : switcherIcon}
+    const switcherIconDom = renderSwitcherIconDom(false);
+    return switcherIconDom !== false ? (
+      <span
+        onClick={onExpand}
+        className={classNames(
+          `${context.prefixCls}-switcher`,
+          `${context.prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
+        )}
+      >
+        {switcherIconDom}
       </span>
-    );
+    ) : 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<React.ReactNode>(() => {
+    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 (
       <span
-        className={classNames(
-          `${prefixCls}-checkbox`,
-          checked && `${prefixCls}-checkbox-checked`,
-          !checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
-          (disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
-        )}
-        onClick={this.onCheck}
+        className={classNames(`${context.prefixCls}-checkbox`, {
+          [`${context.prefixCls}-checkbox-checked`]: checked,
+          [`${context.prefixCls}-checkbox-indeterminate`]: !checked && halfChecked,
+          [`${context.prefixCls}-checkbox-disabled`]: isDisabled || props.disableCheckbox,
+        })}
+        onClick={onCheck}
+        role="checkbox"
+        aria-checked={halfChecked ? 'mixed' : checked}
+        aria-disabled={isDisabled || props.disableCheckbox}
+        aria-label={`Select ${typeof props.title === 'string' ? props.title : 'tree node'}`}
       >
         {$custom}
       </span>
     );
-  };
+  }, [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<typeof ICON_OPEN | typeof ICON_CLOSE>(() => {
+    if (memoizedIsLeaf) {
+      return null;
+    }
+    return expanded ? ICON_OPEN : ICON_CLOSE;
+  }, [memoizedIsLeaf, expanded]);
 
+  // ==================== Render: Title + Icon ====================
+  const iconNode = React.useMemo<React.ReactNode>(() => {
     return (
       <span
         className={classNames(
-          `${prefixCls}-iconEle`,
-          `${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
-          loading && `${prefixCls}-icon_loading`,
+          treeClassNames?.itemIcon,
+          `${context.prefixCls}-iconEle`,
+          `${context.prefixCls}-icon__${nodeState || 'docu'}`,
+          { [`${context.prefixCls}-icon_loading`]: loading },
         )}
+        style={styles?.itemIcon}
       />
     );
-  };
+  }, [context.prefixCls, nodeState, loading]);
+
+  // =================== Drop Indicator ===================
+  const dropIndicatorNode = React.useMemo<React.ReactNode>(() => {
+    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 },
-    } = this.props;
-    const disabled = this.isDisabled();
+  const selectorNode = React.useMemo<React.ReactNode>(() => {
+    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 ? (
-        <span className={classNames(`${prefixCls}-iconEle`, `${prefixCls}-icon__customize`)}>
-          {typeof currentIcon === 'function' ? currentIcon(this.props) : currentIcon}
+        <span
+          className={classNames(
+            treeClassNames?.itemIcon,
+            `${context.prefixCls}-iconEle`,
+            `${context.prefixCls}-icon__customize`,
+          )}
+          style={styles?.itemIcon}
+        >
+          {typeof currentIcon === 'function' ? currentIcon(props) : currentIcon}
         </span>
       ) : (
-        this.renderIcon()
+        iconNode
       );
-    } else if (loadData && loading) {
-      $icon = this.renderIcon();
+    } else if (context.loadData && loading) {
+      $icon = iconNode;
     }
 
     // Title
-    const $title = (
-      <span className={`${prefixCls}-title`}>
-        {typeof title === 'function' ? title(data) : title}
-      </span>
-    );
+    let titleNode: React.ReactNode;
+    if (typeof title === 'function') {
+      titleNode = title(data);
+    } else if (context.titleRender) {
+      titleNode = context.titleRender(data);
+    } else {
+      titleNode = title;
+    }
 
     return (
       <span
-        ref={this.setSelectHandle}
+        ref={selectHandleRef}
         title={typeof title === 'string' ? title : ''}
-        className={classNames(
-          `${wrapClass}`,
-          `${wrapClass}-${this.getNodeState() || 'normal'}`,
-          !disabled && (selected || dragNodeHighlight) && `${prefixCls}-node-selected`,
-          !disabled && draggable && 'draggable',
-        )}
-        draggable={(!disabled && draggable) || undefined}
-        aria-grabbed={(!disabled && draggable) || undefined}
-        onMouseEnter={this.onMouseEnter}
-        onMouseLeave={this.onMouseLeave}
-        onContextMenu={this.onContextMenu}
-        onClick={this.onSelectorClick}
-        onDoubleClick={this.onSelectorDoubleClick}
-        onDragStart={draggable ? this.onDragStart : undefined}
+        className={classNames(wrapClass, `${wrapClass}-${nodeState || 'normal'}`, {
+          [`${context.prefixCls}-node-selected`]: !isDisabled && (selected || dragNodeHighlight),
+        })}
+        onMouseEnter={onMouseEnter}
+        onMouseLeave={onMouseLeave}
+        onContextMenu={onContextMenu}
+        onClick={onSelectorClick}
+        onDoubleClick={onSelectorDoubleClick}
       >
         {$icon}
-        {$title}
+        <span
+          className={classNames(`${context.prefixCls}-title`, treeClassNames?.itemTitle)}
+          style={styles?.itemTitle}
+        >
+          {titleNode}
+        </span>
+        {dropIndicatorNode}
       </span>
     );
-  };
-
-  render() {
-    const {
-      eventKey,
-      className,
-      style,
-      dragOver,
-      dragOverGapTop,
-      dragOverGapBottom,
-      isLeaf,
-      isStart,
-      isEnd,
-      expanded,
-      selected,
-      checked,
-      halfChecked,
-      loading,
-      domRef,
-      ...otherProps
-    } = this.props;
-    const {
-      context: { prefixCls, filterTreeNode, draggable, keyEntities },
-    } = this.props;
-    const disabled = this.isDisabled();
-    const dataOrAriaAttributeProps = getDataAndAria(otherProps);
-    const { level } = keyEntities[eventKey];
-
-    return (
-      <div
-        ref={domRef}
-        className={classNames(className, `${prefixCls}-treenode`, {
-          [`${prefixCls}-treenode-disabled`]: disabled,
-          [`${prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
-          [`${prefixCls}-treenode-checkbox-checked`]: checked,
-          [`${prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
-          [`${prefixCls}-treenode-selected`]: selected,
-          [`${prefixCls}-treenode-loading`]: loading,
-
-          'drag-over': !disabled && dragOver,
-          'drag-over-gap-top': !disabled && dragOverGapTop,
-          'drag-over-gap-bottom': !disabled && dragOverGapBottom,
-          'filter-node': filterTreeNode && filterTreeNode(this),
-        })}
-        style={style}
-        role="treeitem"
-        onDragEnter={draggable ? this.onDragEnter : undefined}
-        onDragOver={draggable ? this.onDragOver : undefined}
-        onDragLeave={draggable ? this.onDragLeave : undefined}
-        onDrop={draggable ? this.onDrop : undefined}
-        onDragEnd={draggable ? this.onDragEnd : undefined}
-        {...dataOrAriaAttributeProps}
-        tabIndex={0}
-      >
-        <Indent prefixCls={prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
-        {this.renderSwitcher()}
-        {this.renderCheckbox()}
-        {this.renderSelector()}
-      </div>
-    );
-  }
-}
-
-polyfill(InternalTreeNode);
-
-const ContextTreeNode: React.FC<TreeNodeProps> = props => (
-  <TreeContext.Consumer>
-    {context => <InternalTreeNode {...props} context={context} />}
-  </TreeContext.Consumer>
-);
-
-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 (
+    <div
+      ref={domRef}
+      role="treeitem"
+      aria-expanded={isLeaf ? undefined : expanded}
+      className={classNames(className, `${context.prefixCls}-treenode`, treeClassNames?.item, {
+        [`${context.prefixCls}-treenode-disabled`]: isDisabled,
+        [`${context.prefixCls}-treenode-switcher-${expanded ? 'open' : 'close'}`]: !isLeaf,
+        [`${context.prefixCls}-treenode-checkbox-checked`]: checked,
+        [`${context.prefixCls}-treenode-checkbox-indeterminate`]: halfChecked,
+        [`${context.prefixCls}-treenode-selected`]: selected,
+        [`${context.prefixCls}-treenode-loading`]: loading,
+        [`${context.prefixCls}-treenode-active`]: active,
+        [`${context.prefixCls}-treenode-leaf-last`]: isEndNode,
+        [`${context.prefixCls}-treenode-draggable`]: isDraggable,
+        dragging,
+        'drop-target': context.dropTargetKey === eventKey,
+        'drop-container': context.dropContainerKey === eventKey,
+        'drag-over': !isDisabled && dragOver,
+        'drag-over-gap-top': !isDisabled && dragOverGapTop,
+        'drag-over-gap-bottom': !isDisabled && dragOverGapBottom,
+        'filter-node': context.filterTreeNode?.(convertNodePropsToEventData(props)),
+        [`${context.prefixCls}-treenode-leaf`]: memoizedIsLeaf,
+      })}
+      style={{ ...style, ...styles?.item }}
+      // Draggable config
+      draggable={draggableWithoutDisabled}
+      onDragStart={draggableWithoutDisabled ? onDragStart : undefined}
+      // Drop config
+      onDragEnter={isDraggable ? onDragEnter : undefined}
+      onDragOver={isDraggable ? onDragOver : undefined}
+      onDragLeave={isDraggable ? onDragLeave : undefined}
+      onDrop={isDraggable ? onDrop : undefined}
+      onDragEnd={isDraggable ? onDragEnd : undefined}
+      onMouseMove={onMouseMove}
+      {...ariaSelected}
+      {...dataOrAriaAttributeProps}
+    >
+      <Indent prefixCls={context.prefixCls} level={level} isStart={isStart} isEnd={isEnd} />
+      {dragHandlerNode}
+      {renderSwitcher()}
+      {checkboxNode}
+      {selectorNode}
+    </div>
+  );
 };
 
-(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 8a491cf2d..b41e037a4 100644
--- a/src/contextTypes.ts
+++ b/src/contextTypes.ts
@@ -3,41 +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, NodeInstance } from './interface';
-import { InternalTreeNodeProps } from './TreeNode';
+import type {
+  BasicDataNode,
+  DataNode,
+  Direction,
+  EventDataNode,
+  IconType,
+  Key,
+  KeyEntities,
+  TreeNodeProps,
+} from './interface';
+import type { DraggableConfig, SemanticName } from './Tree';
 
-type NodeMouseEventHandler = (e: MouseEvent, node: React.Component<InternalTreeNodeProps>) => void;
+export type NodeMouseEventParams<
+  TreeDataType extends BasicDataNode = DataNode,
+  T = HTMLSpanElement,
+> = {
+  event: React.MouseEvent<T>;
+  node: EventDataNode<TreeDataType>;
+};
+export type NodeDragEventParams<
+  TreeDataType extends BasicDataNode = DataNode,
+  T = HTMLDivElement,
+> = {
+  event: React.DragEvent<T>;
+  node: EventDataNode<TreeDataType>;
+};
 
-export interface TreeContextProps {
+export type NodeMouseEventHandler<
+  TreeDataType extends BasicDataNode = DataNode,
+  T = HTMLSpanElement,
+> = (e: React.MouseEvent<T>, node: EventDataNode<TreeDataType>) => void;
+export type NodeDragEventHandler<
+  TreeDataType extends BasicDataNode = DataNode,
+  T = HTMLDivElement,
+> = (e: React.DragEvent<T>, nodeProps: TreeNodeProps<TreeDataType>, outsideTree?: boolean) => void;
+
+export interface TreeContextProps<TreeDataType extends BasicDataNode = DataNode> {
+  styles?: Partial<Record<SemanticName, React.CSSProperties>>;
+  classNames?: Partial<Record<SemanticName, string>>;
   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<Key, DataEntity>;
+  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: NodeInstance) => Promise<void>;
-  filterTreeNode: (treeNode: NodeInstance) => boolean;
+  loadData: (treeNode: EventDataNode<TreeDataType>) => Promise<void>;
+  filterTreeNode: (treeNode: EventDataNode<TreeDataType>) => boolean;
+  titleRender?: (node: any) => React.ReactNode;
 
-  onNodeClick: NodeMouseEventHandler;
-  onNodeDoubleClick: NodeMouseEventHandler;
-  onNodeExpand: NodeMouseEventHandler;
-  onNodeSelect: NodeMouseEventHandler;
-  onNodeCheck: (e: MouseEvent, treeNode: NodeInstance, checked: boolean) => void;
-  onNodeLoad: (treeNode: NodeInstance) => void;
-  onNodeMouseEnter: NodeMouseEventHandler;
-  onNodeMouseLeave: NodeMouseEventHandler;
-  onNodeContextMenu: NodeMouseEventHandler;
-  onNodeDragStart: NodeMouseEventHandler;
-  onNodeDragEnter: NodeMouseEventHandler;
-  onNodeDragOver: NodeMouseEventHandler;
-  onNodeDragLeave: NodeMouseEventHandler;
-  onNodeDragEnd: NodeMouseEventHandler;
-  onNodeDrop: NodeMouseEventHandler;
+  onNodeClick: NodeMouseEventHandler<TreeDataType>;
+  onNodeDoubleClick: NodeMouseEventHandler<TreeDataType>;
+  onNodeExpand: NodeMouseEventHandler<TreeDataType>;
+  onNodeSelect: NodeMouseEventHandler<TreeDataType>;
+  onNodeCheck: (
+    e: React.MouseEvent<HTMLSpanElement>,
+    treeNode: EventDataNode<TreeDataType>,
+    checked: boolean,
+  ) => void;
+  onNodeLoad: (treeNode: EventDataNode<TreeDataType>) => void;
+  onNodeMouseEnter: NodeMouseEventHandler<TreeDataType>;
+  onNodeMouseLeave: NodeMouseEventHandler<TreeDataType>;
+  onNodeContextMenu: NodeMouseEventHandler<TreeDataType>;
+  onNodeDragStart: NodeDragEventHandler<any, any>;
+  onNodeDragEnter: NodeDragEventHandler<any, any>;
+  onNodeDragOver: NodeDragEventHandler<any, any>;
+  onNodeDragLeave: NodeDragEventHandler<any, any>;
+  onNodeDragEnd: NodeDragEventHandler<any, any>;
+  onNodeDrop: NodeDragEventHandler<any, any>;
 }
 
-export const TreeContext: React.Context<TreeContextProps | null> = React.createContext(null);
+export const TreeContext = React.createContext<TreeContextProps<any>>(null);
+
+/** Internal usage, safe to remove. Do not use in prod */
+export const UnstableContext = React.createContext<{ nodeDisabled?: (n: DataNode) => boolean }>({});
diff --git a/src/index.ts b/src/index.ts
index f3f771f6b..7f8954ac6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,13 +1,9 @@
-import OriginTree, { TreeProps } from './Tree';
-import TreeNode, { TreeNodeProps } from './TreeNode';
+import Tree from './Tree';
+import TreeNode from './TreeNode';
+import type { TreeProps } from './Tree';
+import type { TreeNodeProps, BasicDataNode, FieldDataNode } from './interface';
+import { UnstableContext } from './contextTypes';
 
-type OriginTreeType = typeof OriginTree;
-interface TreeType extends OriginTreeType {
-  TreeNode: typeof TreeNode;
-}
-
-const Tree = OriginTree as TreeType;
-Tree.TreeNode = TreeNode;
-
-export { TreeNode, TreeProps, TreeNodeProps };
+export { TreeNode, UnstableContext };
+export type { TreeProps, TreeNodeProps, BasicDataNode, FieldDataNode };
 export default Tree;
diff --git a/src/interface.tsx b/src/interface.tsx
index d9df3b5f7..f3d2ec5df 100644
--- a/src/interface.tsx
+++ b/src/interface.tsx
@@ -1,15 +1,51 @@
 import * as React from 'react';
-import { TreeNodeProps } from './TreeNode';
 
-export interface DataNode {
+export type { ScrollTo } from 'rc-virtual-list/lib/List';
+export interface TreeNodeProps<TreeDataType extends BasicDataNode = DataNode> {
+  eventKey?: Key; // Pass by parent `cloneElement`
+  prefixCls?: string;
+  className?: string;
+  style?: React.CSSProperties;
+  id?: string;
+
+  // By parent
+  expanded?: boolean;
+  selected?: boolean;
+  checked?: boolean;
+  loaded?: boolean;
+  loading?: boolean;
+  halfChecked?: boolean;
+  title?: React.ReactNode | ((data: TreeDataType) => React.ReactNode);
+  dragOver?: boolean;
+  dragOverGapTop?: boolean;
+  dragOverGapBottom?: boolean;
+  pos?: string;
+  domRef?: React.Ref<HTMLDivElement>;
+  /** New added in Tree for easy data access */
+  data?: TreeDataType;
+  isStart?: boolean[];
+  isEnd?: boolean[];
+  active?: boolean;
+  onMouseMove?: React.MouseEventHandler<HTMLDivElement>;
+
+  // By user
+  isLeaf?: boolean;
+  checkable?: boolean;
+  selectable?: boolean;
+  disabled?: boolean;
+  disableCheckbox?: boolean;
+  icon?: IconType;
+  switcherIcon?: IconType;
+  children?: React.ReactNode;
+}
+
+/** For fieldNames, we provides a abstract interface */
+export interface BasicDataNode {
   checkable?: boolean;
-  children?: DataNode[];
   disabled?: boolean;
   disableCheckbox?: boolean;
   icon?: IconType;
   isLeaf?: boolean;
-  key: string | number;
-  title?: React.ReactNode;
   selectable?: boolean;
   switcherIcon?: IconType;
 
@@ -18,9 +54,48 @@ export interface DataNode {
   style?: React.CSSProperties;
 }
 
-export type IconType = React.ReactNode | ((props: TreeNodeProps) => React.ReactNode);
+/** Provide a wrap type define for developer to wrap with customize fieldNames data type */
+export type FieldDataNode<T, ChildFieldName extends string = 'children'> = BasicDataNode &
+  T &
+  Partial<Record<ChildFieldName, FieldDataNode<T, ChildFieldName>[]>>;
+
+export type Key = React.Key;
+
+/**
+ * Typescript not support `bigint` as index type yet.
+ * We use this to mark the `bigint` type is for `Key` usage.
+ * It's safe to remove this when typescript fix:
+ * https://github.com/microsoft/TypeScript/issues/50217
+ */
+export type SafeKey = Exclude<Key, bigint>;
 
-export type Key = string | number;
+export type KeyEntities<DateType extends BasicDataNode = any> = Record<
+  SafeKey,
+  DataEntity<DateType>
+>;
+
+export type DataNode = FieldDataNode<{
+  key: Key;
+  title?: React.ReactNode | ((data: DataNode) => React.ReactNode);
+}>;
+
+export type EventDataNode<TreeDataType> = {
+  key: Key;
+  expanded: boolean;
+  selected: boolean;
+  checked: boolean;
+  loaded: boolean;
+  loading: boolean;
+  halfChecked: boolean;
+  dragOver: boolean;
+  dragOverGapTop: boolean;
+  dragOverGapBottom: boolean;
+  pos: string;
+  active: boolean;
+} & TreeDataType &
+  BasicDataNode;
+
+export type IconType = React.ReactNode | ((props: TreeNodeProps) => React.ReactNode);
 
 export type NodeElement = React.ReactElement<TreeNodeProps> & {
   selectHandle?: HTMLSpanElement;
@@ -29,7 +104,11 @@ export type NodeElement = React.ReactElement<TreeNodeProps> & {
   };
 };
 
-export type NodeInstance = React.Component<TreeNodeProps>;
+export type NodeInstance<TreeDataType extends BasicDataNode = DataNode> = React.Component<
+  TreeNodeProps<TreeDataType>
+> & {
+  selectHandle?: HTMLSpanElement;
+};
 
 export interface Entity {
   node: NodeElement;
@@ -40,18 +119,36 @@ export interface Entity {
   children?: Entity[];
 }
 
-export interface DataEntity extends Omit<Entity, 'node' | 'parent' | 'children'> {
-  node: DataNode;
-  parent?: DataEntity;
-  children?: DataEntity[];
+export interface DataEntity<TreeDataType extends BasicDataNode = DataNode>
+  extends Omit<Entity, 'node' | 'parent' | 'children'> {
+  node: TreeDataType;
+  nodes: TreeDataType[];
+  parent?: DataEntity<TreeDataType>;
+  children?: DataEntity<TreeDataType>[];
   level: number;
 }
 
-export interface FlattenNode {
-  parent: FlattenNode | null;
-  children: FlattenNode[];
+export interface FlattenNode<TreeDataType extends BasicDataNode = DataNode> {
+  parent: FlattenNode<TreeDataType> | null;
+  children: FlattenNode<TreeDataType>[];
   pos: string;
-  data: DataNode;
+  data: TreeDataType;
+  title: React.ReactNode;
+  key: Key;
   isStart: boolean[];
   isEnd: boolean[];
 }
+
+export type GetKey<RecordType> = (record: RecordType, index?: number) => Key;
+
+export type GetCheckDisabled<RecordType> = (record: RecordType) => boolean;
+
+export type Direction = 'ltr' | 'rtl' | undefined;
+
+export interface FieldNames {
+  title?: string;
+  /** @private Internal usage for `rc-tree-select`, safe to remove if no need */
+  _title?: string[];
+  key?: string;
+  children?: string;
+}
diff --git a/src/useUnmount.ts b/src/useUnmount.ts
new file mode 100644
index 000000000..b113c4edc
--- /dev/null
+++ b/src/useUnmount.ts
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import useLayoutEffect from '@rc-component/util/lib/hooks/useLayoutEffect';
+
+/**
+ * Trigger only when component unmount
+ */
+function useUnmount(triggerStart: VoidFunction, triggerEnd: VoidFunction) {
+  const [firstMount, setFirstMount] = React.useState(false);
+
+  useLayoutEffect(() => {
+    if (firstMount) {
+      triggerStart();
+
+      return () => {
+        triggerEnd();
+      };
+    }
+  }, [firstMount]);
+
+  useLayoutEffect(() => {
+    setFirstMount(true);
+
+    return () => {
+      setFirstMount(false);
+    };
+  }, []);
+}
+
+export default useUnmount;
diff --git a/src/util.tsx b/src/util.tsx
index a3140917a..0aa3c03c0 100644
--- a/src/util.tsx
+++ b/src/util.tsx
@@ -1,17 +1,29 @@
+/* eslint-disable no-lonely-if */
 /**
  * Legacy code. Should avoid to use if you are new to import these code.
  */
 
+import warning from '@rc-component/util/lib/warning';
 import React from 'react';
-import warning from 'warning';
-import TreeNode, { TreeNodeProps } from './TreeNode';
-import { NodeElement, Key, DataNode, Entity, DataEntity } from './interface';
-import { TreeProps } from './Tree';
-
-const DRAG_SIDE_RANGE = 0.25;
-const DRAG_MIN_GAP = 2;
+import type {
+  BasicDataNode,
+  DataEntity,
+  DataNode,
+  Direction,
+  FlattenNode,
+  Key,
+  KeyEntities,
+  NodeElement,
+  TreeNodeProps,
+} from './interface';
+import type { AllowDrop, TreeProps } from './Tree';
+import TreeNode from './TreeNode';
+import getEntity from './utils/keyUtil';
+
+export { getPosition, isTreeNode } from './utils/treeUtil';
 
 export function arrDel(list: Key[], value: Key) {
+  if (!list) return [];
   const clone = list.slice();
   const index = clone.indexOf(value);
   if (index >= 0) {
@@ -21,7 +33,7 @@ export function arrDel(list: Key[], value: Key) {
 }
 
 export function arrAdd(list: Key[], value: Key) {
-  const clone = list.slice();
+  const clone = (list || []).slice();
   if (clone.indexOf(value) === -1) {
     clone.push(value);
   }
@@ -32,49 +44,220 @@ export function posToArr(pos: string) {
   return pos.split('-');
 }
 
-export function getPosition(level: string | number, index: number) {
-  return `${level}-${index}`;
-}
-
-export function isTreeNode(node: NodeElement) {
-  return node && node.type && node.type.isTreeNode;
-}
-
-export function isCheckDisabled(node: DataNode) {
-  const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
-  return !!(disabled || disableCheckbox) || checkable === false;
-}
-
-export function getDragNodesKeys(dragNodeKey: Key, keyEntities: Record<Key, DataEntity>): Key[] {
-  const dragNodesKeys = [dragNodeKey];
+export function getDragChildrenKeys<TreeDataType extends BasicDataNode = DataNode>(
+  dragNodeKey: Key,
+  keyEntities: KeyEntities<TreeDataType>,
+): Key[] {
+  // not contains self
+  // self for left or right drag
+  const dragChildrenKeys = [];
 
-  const entity = keyEntities[dragNodeKey];
-  function dig(list: DataEntity[] = []) {
+  const entity = getEntity(keyEntities, dragNodeKey);
+  function dig(list: DataEntity<TreeDataType>[] = []) {
     list.forEach(({ key, children }) => {
-      dragNodesKeys.push(key);
+      dragChildrenKeys.push(key);
       dig(children);
     });
   }
 
   dig(entity.children);
 
-  return dragNodesKeys;
+  return dragChildrenKeys;
+}
+
+export function isLastChild<TreeDataType extends BasicDataNode = DataNode>(
+  treeNodeEntity: DataEntity<TreeDataType>,
+) {
+  if (treeNodeEntity.parent) {
+    const posArr = posToArr(treeNodeEntity.pos);
+    return Number(posArr[posArr.length - 1]) === treeNodeEntity.parent.children.length - 1;
+  }
+  return false;
+}
+
+export function isFirstChild<TreeDataType extends BasicDataNode = DataNode>(
+  treeNodeEntity: DataEntity<TreeDataType>,
+) {
+  const posArr = posToArr(treeNodeEntity.pos);
+  return Number(posArr[posArr.length - 1]) === 0;
 }
 
 // Only used when drag, not affect SSR.
-export function calcDropPosition(event: React.MouseEvent, treeNode: NodeElement) {
-  const { clientY } = event;
-  const { top, bottom, height } = treeNode.selectHandle.getBoundingClientRect();
-  const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP);
+export function calcDropPosition<TreeDataType extends BasicDataNode = DataNode>(
+  event: React.MouseEvent,
+  dragNodeProps: TreeNodeProps<TreeDataType>,
+  targetNodeProps: TreeNodeProps<TreeDataType>,
+  indent: number,
+  startMousePosition: {
+    x: number;
+    y: number;
+  },
+  allowDrop: AllowDrop<TreeDataType>,
+  flattenedNodes: FlattenNode<TreeDataType>[],
+  keyEntities: KeyEntities<TreeDataType>,
+  expandKeys: Key[],
+  direction: Direction,
+): {
+  dropPosition: -1 | 0 | 1;
+  dropLevelOffset: number;
+  dropTargetKey: Key;
+  dropTargetPos: string;
+  dropContainerKey: Key;
+  dragOverNodeKey: Key;
+  dropAllowed: boolean;
+} {
+  const { clientX, clientY } = event;
+  const { top, height } = (event.target as HTMLElement).getBoundingClientRect();
+  // optional chain for testing
+  const horizontalMouseOffset =
+    (direction === 'rtl' ? -1 : 1) * ((startMousePosition?.x || 0) - clientX);
+  const rawDropLevelOffset = (horizontalMouseOffset - 12) / indent;
+
+  // Filter the expanded keys to exclude the node that not has children currently (like async nodes).
+  const filteredExpandKeys = expandKeys.filter((key: string) => keyEntities[key]?.children?.length);
+
+  // find abstract drop node by horizontal offset
+  let abstractDropNodeEntity: DataEntity<TreeDataType> = getEntity(
+    keyEntities,
+    targetNodeProps.eventKey,
+  );
+
+  if (clientY < top + height / 2) {
+    // first half, set abstract drop node to previous node
+    const nodeIndex = flattenedNodes.findIndex(
+      flattenedNode => flattenedNode.key === abstractDropNodeEntity.key,
+    );
+    const prevNodeIndex = nodeIndex <= 0 ? 0 : nodeIndex - 1;
+    const prevNodeKey = flattenedNodes[prevNodeIndex].key;
+    abstractDropNodeEntity = getEntity(keyEntities, prevNodeKey);
+  }
+
+  const initialAbstractDropNodeKey = abstractDropNodeEntity.key;
+
+  const abstractDragOverEntity = abstractDropNodeEntity;
+  const dragOverNodeKey = abstractDropNodeEntity.key;
+
+  let dropPosition: -1 | 0 | 1 = 0;
+  let dropLevelOffset = 0;
 
-  if (clientY <= top + des) {
-    return -1;
+  // Only allow cross level drop when dragging on a non-expanded node
+  if (!filteredExpandKeys.includes(initialAbstractDropNodeKey)) {
+    for (let i = 0; i < rawDropLevelOffset; i += 1) {
+      if (isLastChild(abstractDropNodeEntity)) {
+        abstractDropNodeEntity = abstractDropNodeEntity.parent;
+        dropLevelOffset += 1;
+      } else {
+        break;
+      }
+    }
   }
-  if (clientY >= bottom - des) {
-    return 1;
+
+  const abstractDragDataNode = dragNodeProps.data;
+  const abstractDropDataNode = abstractDropNodeEntity.node;
+  let dropAllowed = true;
+  if (
+    isFirstChild(abstractDropNodeEntity) &&
+    abstractDropNodeEntity.level === 0 &&
+    clientY < top + height / 2 &&
+    allowDrop({
+      dragNode: abstractDragDataNode,
+      dropNode: abstractDropDataNode,
+      dropPosition: -1,
+    }) &&
+    abstractDropNodeEntity.key === targetNodeProps.eventKey
+  ) {
+    // first half of first node in first level
+    dropPosition = -1;
+  } else if (
+    (abstractDragOverEntity.children || []).length &&
+    filteredExpandKeys.includes(dragOverNodeKey)
+  ) {
+    // drop on expanded node
+    // only allow drop inside
+    if (
+      allowDrop({
+        dragNode: abstractDragDataNode,
+        dropNode: abstractDropDataNode,
+        dropPosition: 0,
+      })
+    ) {
+      dropPosition = 0;
+    } else {
+      dropAllowed = false;
+    }
+  } else if (dropLevelOffset === 0) {
+    if (rawDropLevelOffset > -1.5) {
+      // | Node     | <- abstractDropNode
+      // | -^-===== | <- mousePosition
+      // 1. try drop after
+      // 2. do not allow drop
+      if (
+        allowDrop({
+          dragNode: abstractDragDataNode,
+          dropNode: abstractDropDataNode,
+          dropPosition: 1,
+        })
+      ) {
+        dropPosition = 1;
+      } else {
+        dropAllowed = false;
+      }
+    } else {
+      // | Node     | <- abstractDropNode
+      // | ---==^== | <- mousePosition
+      // whether it has children or doesn't has children
+      // always
+      // 1. try drop inside
+      // 2. try drop after
+      // 3. do not allow drop
+      if (
+        allowDrop({
+          dragNode: abstractDragDataNode,
+          dropNode: abstractDropDataNode,
+          dropPosition: 0,
+        })
+      ) {
+        dropPosition = 0;
+      } else if (
+        allowDrop({
+          dragNode: abstractDragDataNode,
+          dropNode: abstractDropDataNode,
+          dropPosition: 1,
+        })
+      ) {
+        dropPosition = 1;
+      } else {
+        dropAllowed = false;
+      }
+    }
+  } else {
+    // | Node1 | <- abstractDropNode
+    //      |  Node2  |
+    // --^--|----=====| <- mousePosition
+    // 1. try insert after Node1
+    // 2. do not allow drop
+    if (
+      allowDrop({
+        dragNode: abstractDragDataNode,
+        dropNode: abstractDropDataNode,
+        dropPosition: 1,
+      })
+    ) {
+      dropPosition = 1;
+    } else {
+      dropAllowed = false;
+    }
   }
 
-  return 0;
+  return {
+    dropPosition,
+    dropLevelOffset,
+    dropTargetKey: abstractDropNodeEntity.key,
+    dropTargetPos: abstractDropNodeEntity.pos,
+    dragOverNodeKey,
+    dropContainerKey: dropPosition === 0 ? null : abstractDropNodeEntity.parent?.key || null,
+    dropAllowed,
+  };
 }
 
 /**
@@ -97,16 +280,7 @@ export function calcSelectedKeys(selectedKeys: Key[], props: TreeProps) {
   return selectedKeys;
 }
 
-/**
- * Since React internal will convert key to string,
- * we need do this to avoid `checkStrictly` use number match
- */
-function keyListToString(keyList: Key[]) {
-  if (!keyList) return keyList;
-  return keyList.map(key => String(key));
-}
-
-const internalProcessProps = (props: DataNode): Partial<TreeNodeProps> => props;
+const internalProcessProps = (props: DataNode): any => props;
 export function convertDataToTree(
   treeData: DataNode[],
   processor?: { processProps: (prop: DataNode) => any },
@@ -115,19 +289,15 @@ export function convertDataToTree(
 
   const { processProps = internalProcessProps } = processor || {};
   const list = Array.isArray(treeData) ? treeData : [treeData];
-  return list.map(
-    ({ children, ...props }): NodeElement => {
-      const childrenNodes = convertDataToTree(children, processor);
-
-      return <TreeNode {...processProps(props)}>{childrenNodes}</TreeNode>;
-    },
-  );
-}
-
-// TODO: ========================= NEW LOGIC =========================
-interface Wrapper {
-  posEntities: Record<string, Entity>;
-  keyEntities: Record<Key, Entity>;
+  return list.map(({ children, ...props }): NodeElement => {
+    const childrenNodes = convertDataToTree(children, processor);
+
+    return (
+      <TreeNode key={props.key} {...processProps(props)}>
+        {childrenNodes}
+      </TreeNode>
+    );
+  });
 }
 
 /**
@@ -139,7 +309,7 @@ export function parseCheckedKeys(keys: Key[] | { checked: Key[]; halfChecked: Ke
   }
 
   // Convert keys to object format
-  let keyProps;
+  let keyProps: { checkedKeys?: Key[]; halfCheckedKeys?: Key[] };
   if (Array.isArray(keys)) {
     // [Legacy] Follow the api doc
     keyProps = {
@@ -156,171 +326,24 @@ export function parseCheckedKeys(keys: Key[] | { checked: Key[]; halfChecked: Ke
     return null;
   }
 
-  keyProps.checkedKeys = keyListToString(keyProps.checkedKeys);
-  keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys);
-
   return keyProps;
 }
 
-/**
- * Conduct check state by the keyList. It will conduct up & from the provided key.
- * If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
- */
-export function conductCheck(
-  /** list of keys */
-  keyList: Key[],
-  /** is check the node or not */
-  isCheck: boolean,
-  /** parsed by `convertTreeToEntities` function in Tree */
-  keyEntities: Record<Key, DataEntity>,
-  /** Can pass current checked status for process (usually for uncheck operation) */
-  checkStatus: { checkedKeys?: Key[]; halfCheckedKeys?: Key[] } = {},
-) {
-  const warningMissKeys: Key[] = [];
-
-  const checkedKeys = {};
-  const halfCheckedKeys = {}; // Record the key has some child checked (include child half checked)
-
-  (checkStatus.checkedKeys || []).forEach(key => {
-    checkedKeys[key] = true;
-  });
-
-  (checkStatus.halfCheckedKeys || []).forEach(key => {
-    halfCheckedKeys[key] = true;
-  });
-
-  // Conduct up
-  function conductUp(key: Key) {
-    if (checkedKeys[key] === isCheck) return;
-
-    const entity = keyEntities[key];
-    if (!entity) return;
-
-    const { children, parent, node } = entity;
-
-    if (isCheckDisabled(node)) return;
-
-    // Check child node checked status
-    let everyChildChecked = true;
-    let someChildChecked = false; // Child checked or half checked
-
-    (children || [])
-      .filter(child => !isCheckDisabled(child.node))
-      .forEach(({ key: childKey }) => {
-        const childChecked = checkedKeys[childKey];
-        const childHalfChecked = halfCheckedKeys[childKey];
-
-        if (childChecked || childHalfChecked) someChildChecked = true;
-        if (!childChecked) everyChildChecked = false;
-      });
-
-    // Update checked status
-    if (isCheck) {
-      checkedKeys[key] = everyChildChecked;
-    } else {
-      checkedKeys[key] = false;
-    }
-    halfCheckedKeys[key] = someChildChecked;
-
-    if (parent) {
-      conductUp(parent.key);
-    }
-  }
-
-  // Conduct down
-  function conductDown(key: Key) {
-    if (checkedKeys[key] === isCheck) return;
-
-    const entity = keyEntities[key];
-    if (!entity) return;
-
-    const { children, node } = entity;
-
-    if (isCheckDisabled(node)) return;
-
-    checkedKeys[key] = isCheck;
-
-    (children || []).forEach(child => {
-      conductDown(child.key);
-    });
-  }
-
-  function conduct(key: Key) {
-    const entity = keyEntities[key];
-
-    if (!entity) {
-      warningMissKeys.push(key);
-      return;
-    }
-
-    const { children, parent, node } = entity;
-    checkedKeys[key] = isCheck;
-
-    if (isCheckDisabled(node)) return;
-
-    // Conduct down
-    (children || [])
-      .filter(child => !isCheckDisabled(child.node))
-      .forEach(child => {
-        conductDown(child.key);
-      });
-
-    // Conduct up
-    if (parent) {
-      conductUp(parent.key);
-    }
-  }
-
-  (keyList || []).forEach(key => {
-    conduct(key);
-  });
-
-  const checkedKeyList = [];
-  const halfCheckedKeyList = [];
-
-  // Fill checked list
-  Object.keys(checkedKeys).forEach(key => {
-    if (checkedKeys[key]) {
-      checkedKeyList.push(key);
-    }
-  });
-
-  // Fill half checked list
-  Object.keys(halfCheckedKeys).forEach(key => {
-    if (!checkedKeys[key] && halfCheckedKeys[key]) {
-      halfCheckedKeyList.push(key);
-    }
-  });
-
-  warning(
-    !warningMissKeys.length,
-    `Tree missing follow keys: ${warningMissKeys
-      .slice(0, 100)
-      .map(key => `'${key}'`)
-      .join(', ')}`,
-  );
-
-  return {
-    checkedKeys: checkedKeyList,
-    halfCheckedKeys: halfCheckedKeyList,
-  };
-}
-
 /**
  * If user use `autoExpandParent` we should get the list of parent node
  * @param keyList
  * @param keyEntities
  */
-export function conductExpandParent(keyList: Key[], keyEntities: Record<Key, DataEntity>) {
-  const expandedKeys = {};
+export function conductExpandParent(keyList: Key[], keyEntities: KeyEntities): Key[] {
+  const expandedKeys = new Set<Key>();
 
   function conductUp(key: Key) {
-    if (expandedKeys[key]) return;
+    if (expandedKeys.has(key)) return;
 
-    const entity = keyEntities[key];
+    const entity = getEntity(keyEntities, key);
     if (!entity) return;
 
-    expandedKeys[key] = true;
+    expandedKeys.add(key);
 
     const { parent, node } = entity;
 
@@ -335,19 +358,5 @@ export function conductExpandParent(keyList: Key[], keyEntities: Record<Key, Dat
     conductUp(key);
   });
 
-  return Object.keys(expandedKeys);
-}
-
-/**
- * Returns only the data- and aria- key/value pairs
- */
-export function getDataAndAria(props: Partial<TreeProps | TreeNodeProps>) {
-  const omitProps: Record<string, string> = {};
-  Object.keys(props).forEach(key => {
-    if (key.startsWith('data-') || key.startsWith('aria-')) {
-      omitProps[key] = props[key];
-    }
-  });
-
-  return omitProps;
+  return [...expandedKeys];
 }
diff --git a/src/utils/conductUtil.ts b/src/utils/conductUtil.ts
new file mode 100644
index 000000000..c45e4d74b
--- /dev/null
+++ b/src/utils/conductUtil.ts
@@ -0,0 +1,265 @@
+import warning from '@rc-component/util/lib/warning';
+import type {
+  BasicDataNode,
+  DataEntity,
+  DataNode,
+  GetCheckDisabled,
+  Key,
+  KeyEntities,
+} from '../interface';
+import getEntity from './keyUtil';
+
+interface ConductReturnType {
+  checkedKeys: Key[];
+  halfCheckedKeys: Key[];
+}
+
+function removeFromCheckedKeys(halfCheckedKeys: Set<Key>, checkedKeys: Set<Key>) {
+  const filteredKeys = new Set<Key>();
+  halfCheckedKeys.forEach(key => {
+    if (!checkedKeys.has(key)) {
+      filteredKeys.add(key);
+    }
+  });
+  return filteredKeys;
+}
+
+export function isCheckDisabled<TreeDataType>(node: TreeDataType) {
+  const { disabled, disableCheckbox, checkable } = (node || {}) as DataNode;
+  return !!(disabled || disableCheckbox) || checkable === false;
+}
+
+// Fill miss keys
+function fillConductCheck<TreeDataType extends BasicDataNode = DataNode>(
+  keys: Set<Key>,
+  levelEntities: Map<number, Set<DataEntity<TreeDataType>>>,
+  maxLevel: number,
+  syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>,
+): ConductReturnType {
+  const checkedKeys = new Set<Key>(keys);
+  const halfCheckedKeys = new Set<Key>();
+
+  // Add checked keys top to bottom
+  for (let level = 0; level <= maxLevel; level += 1) {
+    const entities = levelEntities.get(level) || new Set();
+    entities.forEach(entity => {
+      const { key, node, children = [] } = entity;
+
+      if (checkedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
+        children
+          .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
+          .forEach(childEntity => {
+            checkedKeys.add(childEntity.key);
+          });
+      }
+    });
+  }
+
+  // Add checked keys from bottom to top
+  const visitedKeys = new Set<Key>();
+  for (let level = maxLevel; level >= 0; level -= 1) {
+    const entities = levelEntities.get(level) || new Set();
+    entities.forEach(entity => {
+      const { parent, node } = entity;
+
+      // Skip if no need to check
+      if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
+        return;
+      }
+
+      // Skip if parent is disabled
+      if (syntheticGetCheckDisabled(entity.parent.node)) {
+        visitedKeys.add(parent.key);
+        return;
+      }
+
+      let allChecked = true;
+      let partialChecked = false;
+
+      (parent.children || [])
+        .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
+        .forEach(({ key }) => {
+          const checked = checkedKeys.has(key);
+          if (allChecked && !checked) {
+            allChecked = false;
+          }
+          if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
+            partialChecked = true;
+          }
+        });
+
+      if (allChecked) {
+        checkedKeys.add(parent.key);
+      }
+      if (partialChecked) {
+        halfCheckedKeys.add(parent.key);
+      }
+
+      visitedKeys.add(parent.key);
+    });
+  }
+
+  return {
+    checkedKeys: Array.from(checkedKeys),
+    halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
+  };
+}
+
+// Remove useless key
+function cleanConductCheck<TreeDataType extends BasicDataNode = DataNode>(
+  keys: Set<Key>,
+  halfKeys: Key[],
+  levelEntities: Map<number, Set<DataEntity<TreeDataType>>>,
+  maxLevel: number,
+  syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>,
+): ConductReturnType {
+  const checkedKeys = new Set<Key>(keys);
+  let halfCheckedKeys = new Set<Key>(halfKeys);
+
+  // Remove checked keys from top to bottom
+  for (let level = 0; level <= maxLevel; level += 1) {
+    const entities = levelEntities.get(level) || new Set();
+    entities.forEach(entity => {
+      const { key, node, children = [] } = entity;
+
+      if (!checkedKeys.has(key) && !halfCheckedKeys.has(key) && !syntheticGetCheckDisabled(node)) {
+        children
+          .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
+          .forEach(childEntity => {
+            checkedKeys.delete(childEntity.key);
+          });
+      }
+    });
+  }
+
+  // Remove checked keys form bottom to top
+  halfCheckedKeys = new Set<Key>();
+  const visitedKeys = new Set<Key>();
+  for (let level = maxLevel; level >= 0; level -= 1) {
+    const entities = levelEntities.get(level) || new Set();
+
+    entities.forEach(entity => {
+      const { parent, node } = entity;
+
+      // Skip if no need to check
+      if (syntheticGetCheckDisabled(node) || !entity.parent || visitedKeys.has(entity.parent.key)) {
+        return;
+      }
+
+      // Skip if parent is disabled
+      if (syntheticGetCheckDisabled(entity.parent.node)) {
+        visitedKeys.add(parent.key);
+        return;
+      }
+
+      let allChecked = true;
+      let partialChecked = false;
+
+      (parent.children || [])
+        .filter(childEntity => !syntheticGetCheckDisabled(childEntity.node))
+        .forEach(({ key }) => {
+          const checked = checkedKeys.has(key);
+          if (allChecked && !checked) {
+            allChecked = false;
+          }
+          if (!partialChecked && (checked || halfCheckedKeys.has(key))) {
+            partialChecked = true;
+          }
+        });
+
+      if (!allChecked) {
+        checkedKeys.delete(parent.key);
+      }
+      if (partialChecked) {
+        halfCheckedKeys.add(parent.key);
+      }
+
+      visitedKeys.add(parent.key);
+    });
+  }
+
+  return {
+    checkedKeys: Array.from(checkedKeys),
+    halfCheckedKeys: Array.from(removeFromCheckedKeys(halfCheckedKeys, checkedKeys)),
+  };
+}
+
+/**
+ * Conduct with keys.
+ * @param keyList current key list
+ * @param keyEntities key - dataEntity map
+ * @param mode `fill` to fill missing key, `clean` to remove useless key
+ */
+export function conductCheck<TreeDataType extends BasicDataNode = DataNode>(
+  keyList: Key[],
+  checked: true | { checked: false; halfCheckedKeys: Key[] },
+  keyEntities: KeyEntities<TreeDataType>,
+  getCheckDisabled?: GetCheckDisabled<TreeDataType>,
+): ConductReturnType {
+  const warningMissKeys: Key[] = [];
+
+  let syntheticGetCheckDisabled: GetCheckDisabled<TreeDataType>;
+  if (getCheckDisabled) {
+    syntheticGetCheckDisabled = getCheckDisabled;
+  } else {
+    syntheticGetCheckDisabled = isCheckDisabled;
+  }
+
+  // We only handle exist keys
+  const keys = new Set<Key>(
+    keyList.filter(key => {
+      const hasEntity = !!getEntity(keyEntities, key);
+      if (!hasEntity) {
+        warningMissKeys.push(key);
+      }
+
+      return hasEntity;
+    }),
+  );
+  const levelEntities = new Map<number, Set<DataEntity<TreeDataType>>>();
+  let maxLevel = 0;
+
+  // Convert entities by level for calculation
+  Object.keys(keyEntities).forEach(key => {
+    const entity = keyEntities[key];
+    const { level } = entity;
+
+    let levelSet: Set<DataEntity<TreeDataType>> = levelEntities.get(level);
+    if (!levelSet) {
+      levelSet = new Set();
+      levelEntities.set(level, levelSet);
+    }
+
+    levelSet.add(entity);
+
+    maxLevel = Math.max(maxLevel, level);
+  });
+
+  warning(
+    !warningMissKeys.length,
+    `Tree missing follow keys: ${warningMissKeys
+      .slice(0, 100)
+      .map(key => `'${key}'`)
+      .join(', ')}`,
+  );
+
+  let result: ConductReturnType;
+  if (checked === true) {
+    result = fillConductCheck<TreeDataType>(
+      keys,
+      levelEntities,
+      maxLevel,
+      syntheticGetCheckDisabled,
+    );
+  } else {
+    result = cleanConductCheck(
+      keys,
+      checked.halfCheckedKeys,
+      levelEntities,
+      maxLevel,
+      syntheticGetCheckDisabled,
+    );
+  }
+
+  return result;
+}
diff --git a/src/utils/diffUtil.ts b/src/utils/diffUtil.ts
index 6dd83a6a0..8f2372e43 100644
--- a/src/utils/diffUtil.ts
+++ b/src/utils/diffUtil.ts
@@ -1,4 +1,4 @@
-import { Key, FlattenNode } from '../interface';
+import type { Key, FlattenNode } from '../interface';
 
 export function findExpandedKeys(prev: Key[] = [], next: Key[] = []) {
   const prevLen = prev.length;
@@ -33,12 +33,12 @@ export function findExpandedKeys(prev: Key[] = [], next: Key[] = []) {
 }
 
 export function getExpandRange(shorter: FlattenNode[], longer: FlattenNode[], key: Key) {
-  const shorterStartIndex = shorter.findIndex(({ data }) => data.key === key);
+  const shorterStartIndex = shorter.findIndex(data => data.key === key);
   const shorterEndNode = shorter[shorterStartIndex + 1];
-  const longerStartIndex = longer.findIndex(({ data }) => data.key === key);
+  const longerStartIndex = longer.findIndex(data => data.key === key);
 
   if (shorterEndNode) {
-    const longerEndIndex = longer.findIndex(({ data }) => data.key === shorterEndNode.data.key);
+    const longerEndIndex = longer.findIndex(data => data.key === shorterEndNode.key);
     return longer.slice(longerStartIndex + 1, longerEndIndex);
   }
   return longer.slice(longerStartIndex + 1);
diff --git a/src/utils/keyUtil.ts b/src/utils/keyUtil.ts
new file mode 100644
index 000000000..ae464e166
--- /dev/null
+++ b/src/utils/keyUtil.ts
@@ -0,0 +1,5 @@
+import type { Key, KeyEntities, SafeKey } from '../interface';
+
+export default function getEntity<T = any>(keyEntities: KeyEntities<T>, key: Key) {
+  return keyEntities[key as SafeKey];
+}
diff --git a/src/utils/treeUtil.ts b/src/utils/treeUtil.ts
index d4ad6be7c..ec8959461 100644
--- a/src/utils/treeUtil.ts
+++ b/src/utils/treeUtil.ts
@@ -1,25 +1,60 @@
+import toArray from '@rc-component/util/lib/Children/toArray';
+import omit from '@rc-component/util/lib/omit';
+import warning from '@rc-component/util/lib/warning';
 import * as React from 'react';
-import toArray from 'rc-util/lib/Children/toArray';
-import warning from 'warning';
-import { DataNode, FlattenNode, NodeElement, DataEntity, Key } from '../interface';
-import { getPosition, isTreeNode } from '../util';
+import type {
+  BasicDataNode,
+  DataEntity,
+  DataNode,
+  EventDataNode,
+  FieldNames,
+  FlattenNode,
+  GetKey,
+  Key,
+  KeyEntities,
+  NodeElement,
+  SafeKey,
+  TreeNodeProps,
+} from '../interface';
+import getEntity from './keyUtil';
+
+export function getPosition(level: string | number, index: number) {
+  return `${level}-${index}`;
+}
+
+export function isTreeNode(node: NodeElement) {
+  return node && node.type && node.type.isTreeNode;
+}
 
 export function getKey(key: Key, pos: string) {
   if (key !== null && key !== undefined) {
-    return String(key);
+    return key;
   }
   return pos;
 }
 
+export function fillFieldNames(fieldNames?: FieldNames): Required<FieldNames> {
+  const { title, _title, key, children } = fieldNames || {};
+  const mergedTitle = title || 'title';
+
+  return {
+    title: mergedTitle,
+    _title: _title || [mergedTitle],
+    key: key || 'key',
+    children: children || 'children',
+  };
+}
+
 /**
  * Warning if TreeNode do not provides key
  */
-export function warningWithoutKey(treeData: DataNode[] = []) {
+export function warningWithoutKey(treeData: DataNode[], fieldNames: FieldNames) {
   const keys: Map<string, boolean> = new Map();
 
   function dig(list: DataNode[], path: string = '') {
     (list || []).forEach(treeNode => {
-      const { key, children } = treeNode;
+      const key = treeNode[fieldNames.key];
+      const children = treeNode[fieldNames.children];
       warning(
         key !== null && key !== undefined,
         `Tree node must have a certain key: [${path}${key}]`,
@@ -44,7 +79,7 @@ export function warningWithoutKey(treeData: DataNode[] = []) {
  */
 export function convertTreeToData(rootNodes: React.ReactNode): DataNode[] {
   function dig(node: React.ReactNode): DataNode[] {
-    const treeNodes: NodeElement[] = toArray(node);
+    const treeNodes = toArray(node) as NodeElement[];
     return treeNodes
       .map(treeNode => {
         // Filter invalidate node
@@ -57,7 +92,7 @@ export function convertTreeToData(rootNodes: React.ReactNode): DataNode[] {
         const { children, ...rest } = treeNode.props;
 
         const dataNode: DataNode = {
-          key,
+          key: key as Key,
           ...rest,
         };
 
@@ -76,34 +111,62 @@ export function convertTreeToData(rootNodes: React.ReactNode): DataNode[] {
 
 /**
  * Flat nest tree data into flatten list. This is used for virtual list render.
+ * @param treeNodeList Origin data node list
+ * @param expandedKeys
+ * need expanded keys, provides `true` means all expanded (used in `rc-tree-select`).
  */
-export function flattenTreeData(
-  treeNodeList: DataNode[] = [],
-  expandedKeys: Key[] = [],
-): FlattenNode[] {
-  const flattenList: FlattenNode[] = [];
-
-  function dig(list: DataNode[], parent: FlattenNode = null): FlattenNode[] {
+export function flattenTreeData<TreeDataType extends BasicDataNode = DataNode>(
+  treeNodeList: TreeDataType[],
+  expandedKeys: Key[] | true,
+  fieldNames: FieldNames,
+): FlattenNode<TreeDataType>[] {
+  const {
+    _title: fieldTitles,
+    key: fieldKey,
+    children: fieldChildren,
+  } = fillFieldNames(fieldNames);
+
+  const expandedKeySet = new Set(expandedKeys === true ? [] : expandedKeys);
+  const flattenList: FlattenNode<TreeDataType>[] = [];
+
+  function dig(
+    list: TreeDataType[],
+    parent: FlattenNode<TreeDataType> = null,
+  ): FlattenNode<TreeDataType>[] {
     return list.map((treeNode, index) => {
       const pos: string = getPosition(parent ? parent.pos : '0', index);
-      const mergedKey = getKey(treeNode.key, pos);
+      const mergedKey = getKey(treeNode[fieldKey], pos);
+
+      // Pick matched title in field title list
+      let mergedTitle: React.ReactNode;
+      for (let i = 0; i < fieldTitles.length; i += 1) {
+        const fieldTitle = fieldTitles[i];
+        if (treeNode[fieldTitle] !== undefined) {
+          mergedTitle = treeNode[fieldTitle];
+          break;
+        }
+      }
 
       // Add FlattenDataNode into list
-      const flattenNode: FlattenNode = {
-        ...treeNode,
-        parent,
-        pos,
-        children: null,
-        data: treeNode,
-        isStart: [...(parent ? parent.isStart : []), index === 0],
-        isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
-      };
-
+      // We use `Object.assign` here to save perf since babel's `objectSpread` has perf issue
+      const flattenNode: FlattenNode<TreeDataType> = Object.assign(
+        omit(treeNode, [...fieldTitles, fieldKey, fieldChildren] as any),
+        {
+          title: mergedTitle,
+          key: mergedKey,
+          parent,
+          pos,
+          children: null,
+          data: treeNode,
+          isStart: [...(parent ? parent.isStart : []), index === 0],
+          isEnd: [...(parent ? parent.isEnd : []), index === list.length - 1],
+        },
+      );
       flattenList.push(flattenNode);
 
       // Loop treeNode children
-      if (expandedKeys.includes(mergedKey)) {
-        flattenNode.children = dig(treeNode.children || [], flattenNode);
+      if (expandedKeys === true || expandedKeySet.has(mergedKey)) {
+        flattenNode.children = dig(treeNode[fieldChildren] || [], flattenNode);
       } else {
         flattenNode.children = [];
       }
@@ -117,6 +180,14 @@ export function flattenTreeData(
   return flattenList;
 }
 
+type ExternalGetKey = GetKey<DataNode> | string;
+
+interface TraverseDataNodesConfig {
+  childrenPropName?: string;
+  externalGetKey?: ExternalGetKey;
+  fieldNames?: FieldNames;
+}
+
 /**
  * Traverse all the data by `treeData`.
  * Please not use it out of the `rc-tree` since we may refactor this code.
@@ -130,25 +201,60 @@ export function traverseDataNodes(
     key: Key;
     parentPos: string | number;
     level: number;
+    nodes: DataNode[];
   }) => void,
+  // To avoid too many params, let use config instead of origin param
+  config?: TraverseDataNodesConfig | string,
 ) {
+  let mergedConfig: TraverseDataNodesConfig = {};
+  if (typeof config === 'object') {
+    mergedConfig = config;
+  } else {
+    mergedConfig = { externalGetKey: config };
+  }
+  mergedConfig = mergedConfig || {};
+
+  // Init config
+  const { childrenPropName, externalGetKey, fieldNames } = mergedConfig;
+
+  const { key: fieldKey, children: fieldChildren } = fillFieldNames(fieldNames);
+
+  const mergeChildrenPropName = childrenPropName || fieldChildren;
+
+  // Get keys
+  let syntheticGetKey: (node: DataNode, pos?: string) => Key;
+  if (externalGetKey) {
+    if (typeof externalGetKey === 'string') {
+      syntheticGetKey = (node: DataNode) => (node as any)[externalGetKey as string];
+    } else if (typeof externalGetKey === 'function') {
+      syntheticGetKey = (node: DataNode) => (externalGetKey as GetKey<DataNode>)(node);
+    }
+  } else {
+    syntheticGetKey = (node, pos) => getKey(node[fieldKey], pos);
+  }
+
+  // Process
   function processNode(
     node: DataNode,
     index?: number,
     parent?: { node: DataNode; pos: string; level: number },
+    pathNodes?: DataNode[],
   ) {
-    const children = node ? node.children : dataNodes;
+    const children = node ? node[mergeChildrenPropName] : dataNodes;
     const pos = node ? getPosition(parent.pos, index) : '0';
+    const connectNodes = node ? [...pathNodes, node] : [];
 
     // Process node if is not root
     if (node) {
+      const key: Key = syntheticGetKey(node, pos);
       const data = {
         node,
         index,
         pos,
-        key: node.key || pos,
+        key,
         parentPos: parent.node ? parent.pos : null,
         level: parent.level + 1,
+        nodes: connectNodes,
       };
 
       callback(data);
@@ -157,7 +263,16 @@ export function traverseDataNodes(
     // Process children node
     if (children) {
       children.forEach((subNode, subIndex) => {
-        processNode(subNode, subIndex, { node, pos, level: parent ? parent.level + 1 : -1 });
+        processNode(
+          subNode,
+          subIndex,
+          {
+            node,
+            pos,
+            level: parent ? parent.level + 1 : -1,
+          },
+          connectNodes,
+        );
       });
     }
   }
@@ -167,7 +282,7 @@ export function traverseDataNodes(
 
 interface Wrapper {
   posEntities: Record<string, DataEntity>;
-  keyEntities: Record<Key, DataEntity>;
+  keyEntities: KeyEntities;
 }
 
 /**
@@ -179,15 +294,26 @@ export function convertDataToEntities(
     initWrapper,
     processEntity,
     onProcessFinished,
+    externalGetKey,
+    childrenPropName,
+    fieldNames,
   }: {
     initWrapper?: (wrapper: Wrapper) => Wrapper;
     processEntity?: (entity: DataEntity, wrapper: Wrapper) => void;
     onProcessFinished?: (wrapper: Wrapper) => void;
+    externalGetKey?: ExternalGetKey;
+    childrenPropName?: string;
+    fieldNames?: FieldNames;
   } = {},
+  /** @deprecated Use `config.externalGetKey` instead */
+  legacyExternalGetKey?: ExternalGetKey,
 ) {
+  // Init config
+  const mergedExternalGetKey = externalGetKey || legacyExternalGetKey;
+
   const posEntities = {};
   const keyEntities = {};
-  let wrapper = {
+  let wrapper: Wrapper = {
     posEntities,
     keyEntities,
   };
@@ -196,26 +322,30 @@ export function convertDataToEntities(
     wrapper = initWrapper(wrapper) || wrapper;
   }
 
-  traverseDataNodes(dataNodes, item => {
-    const { node, index, pos, key, parentPos, level } = item;
-    const entity: DataEntity = { node, index, key, pos, level };
+  traverseDataNodes(
+    dataNodes,
+    item => {
+      const { node, index, pos, key, parentPos, level, nodes } = item;
+      const entity: DataEntity = { node, nodes, index, key, pos, level };
 
-    const mergedKey = getKey(key, pos);
+      const mergedKey = getKey(key, pos);
 
-    posEntities[pos] = entity;
-    keyEntities[mergedKey] = entity;
+      posEntities[pos] = entity;
+      keyEntities[mergedKey as SafeKey] = entity;
 
-    // Fill children
-    entity.parent = posEntities[parentPos];
-    if (entity.parent) {
-      entity.parent.children = entity.parent.children || [];
-      entity.parent.children.push(entity);
-    }
+      // Fill children
+      entity.parent = posEntities[parentPos];
+      if (entity.parent) {
+        entity.parent.children = entity.parent.children || [];
+        entity.parent.children.push(entity);
+      }
 
-    if (processEntity) {
-      processEntity(entity, wrapper);
-    }
-  });
+      if (processEntity) {
+        processEntity(entity, wrapper);
+      }
+    },
+    { externalGetKey: mergedExternalGetKey, childrenPropName, fieldNames },
+  );
 
   if (onProcessFinished) {
     onProcessFinished(wrapper);
@@ -224,7 +354,7 @@ export function convertDataToEntities(
   return wrapper;
 }
 
-export interface TreeNodeRequiredProps {
+export interface TreeNodeRequiredProps<TreeDataType extends BasicDataNode = DataNode> {
   expandedKeys: Key[];
   selectedKeys: Key[];
   loadedKeys: Key[];
@@ -233,13 +363,13 @@ export interface TreeNodeRequiredProps {
   halfCheckedKeys: Key[];
   dragOverNodeKey: Key;
   dropPosition: number;
-  keyEntities: Record<Key, DataEntity>;
+  keyEntities: KeyEntities<TreeDataType>;
 }
 
 /**
  * Get TreeNode props with Tree props.
  */
-export function getTreeNodeProps(
+export function getTreeNodeProps<TreeDataType extends BasicDataNode = DataNode>(
   key: Key,
   {
     expandedKeys,
@@ -251,8 +381,10 @@ export function getTreeNodeProps(
     dragOverNodeKey,
     dropPosition,
     keyEntities,
-  }: TreeNodeRequiredProps,
+  }: TreeNodeRequiredProps<TreeDataType>,
 ) {
+  const entity = getEntity(keyEntities, key);
+
   const treeNodeProps = {
     eventKey: key,
     expanded: expandedKeys.indexOf(key) !== -1,
@@ -261,9 +393,11 @@ export function getTreeNodeProps(
     loading: loadingKeys.indexOf(key) !== -1,
     checked: checkedKeys.indexOf(key) !== -1,
     halfChecked: halfCheckedKeys.indexOf(key) !== -1,
-    pos: String(keyEntities[key].pos),
+    pos: String(entity ? entity.pos : ''),
 
     // [Legacy] Drag props
+    // Since the interaction of drag is changed, the semantic of the props are
+    // not accuracy, I think it should be finally removed
     dragOver: dragOverNodeKey === key && dropPosition === 0,
     dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
     dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
@@ -271,3 +405,53 @@ export function getTreeNodeProps(
 
   return treeNodeProps;
 }
+
+export function convertNodePropsToEventData<TreeDataType extends BasicDataNode = DataNode>(
+  props: TreeNodeProps<TreeDataType>,
+): EventDataNode<TreeDataType> {
+  const {
+    data,
+    expanded,
+    selected,
+    checked,
+    loaded,
+    loading,
+    halfChecked,
+    dragOver,
+    dragOverGapTop,
+    dragOverGapBottom,
+    pos,
+    active,
+    eventKey,
+  } = props;
+
+  const eventData = {
+    ...data,
+    expanded,
+    selected,
+    checked,
+    loaded,
+    loading,
+    halfChecked,
+    dragOver,
+    dragOverGapTop,
+    dragOverGapBottom,
+    pos,
+    active,
+    key: eventKey,
+  };
+
+  if (!('props' in eventData)) {
+    Object.defineProperty(eventData, 'props', {
+      get() {
+        warning(
+          false,
+          'Second param return from event is node data instead of TreeNode instance. Please read value directly instead of reading from `props`.',
+        );
+        return props;
+      },
+    });
+  }
+
+  return eventData;
+}
diff --git a/tests/Accessibility.spec.tsx b/tests/Accessibility.spec.tsx
new file mode 100644
index 000000000..739dd6171
--- /dev/null
+++ b/tests/Accessibility.spec.tsx
@@ -0,0 +1,280 @@
+/* eslint-disable no-undef, react/no-multi-comp */
+import { fireEvent, render } from '@testing-library/react';
+import KeyCode from '@rc-component/util/lib/KeyCode';
+import React from 'react';
+import Tree, { FieldDataNode } from '../src';
+import { spyConsole } from './util';
+
+describe('Tree Accessibility', () => {
+  spyConsole();
+
+  describe('key operation', () => {
+    function typeTest(props, spaceCallback, enterCallback) {
+      const onExpand = jest.fn();
+      const onFocus = jest.fn();
+      const onBlur = jest.fn();
+      const onKeyDown = jest.fn();
+      const onActiveChange = jest.fn();
+
+      function checkKeyDownTrigger() {
+        expect(onKeyDown).toHaveBeenCalled();
+        onKeyDown.mockReset();
+      }
+
+      function checkActiveTrigger(key) {
+        expect(onActiveChange).toHaveBeenCalledWith(key);
+        onActiveChange.mockReset();
+      }
+
+      const { container } = render(
+        <Tree
+          {...props}
+          onExpand={onExpand}
+          onFocus={onFocus}
+          onBlur={onBlur}
+          onKeyDown={onKeyDown}
+          onActiveChange={onActiveChange}
+          defaultExpandedKeys={['parent', 'child 1', 'child 2']}
+          treeData={[{ key: 'parent', children: [{ key: 'child 1' }, { key: 'child 2' }] }]}
+        />,
+      );
+
+      function keyDown(keyCode: number) {
+        fireEvent.keyDown(container.querySelector('input'), {
+          keyCode,
+        });
+      }
+
+      function getTreeNode(index: number) {
+        const treeNodes = container
+          .querySelector('.rc-tree-list-holder')
+          .querySelectorAll('.rc-tree-treenode');
+
+        return treeNodes[(index + treeNodes.length) % treeNodes.length];
+      }
+
+      // Focus
+      fireEvent.focus(container.querySelector('input'));
+      expect(onFocus).toHaveBeenCalled();
+
+      // Arrow up: last one
+      keyDown(KeyCode.UP);
+      expect(getTreeNode(-1)).toHaveClass('rc-tree-treenode-active');
+      checkKeyDownTrigger();
+      checkActiveTrigger('child 2');
+
+      // Arrow down: first one
+      keyDown(KeyCode.DOWN);
+      expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
+      checkKeyDownTrigger();
+      checkActiveTrigger('parent');
+
+      // Arrow up: last one again
+      keyDown(KeyCode.UP);
+      expect(getTreeNode(-1)).toHaveClass('rc-tree-treenode-active');
+      checkKeyDownTrigger();
+      checkActiveTrigger('child 2');
+
+      // Arrow left: parent
+      keyDown(KeyCode.LEFT);
+      expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
+      checkKeyDownTrigger();
+      checkActiveTrigger('parent');
+
+      // Arrow left: collapse
+      keyDown(KeyCode.LEFT);
+      expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
+      expect(onExpand).toHaveBeenCalledWith(['child 1', 'child 2'], expect.anything());
+      checkKeyDownTrigger();
+
+      // Arrow right: expand
+      onExpand.mockReset();
+      keyDown(KeyCode.RIGHT);
+      expect(getTreeNode(0)).toHaveClass('rc-tree-treenode-active');
+      expect(onExpand).toHaveBeenCalledWith(['child 1', 'child 2', 'parent'], expect.anything());
+      checkKeyDownTrigger();
+
+      // Arrow right: first child
+      onExpand.mockReset();
+      keyDown(KeyCode.RIGHT);
+      expect(getTreeNode(1)).toHaveClass('rc-tree-treenode-active');
+      checkKeyDownTrigger();
+      checkActiveTrigger('child 1');
+
+      // SPACE: confirm
+      keyDown(KeyCode.SPACE);
+      spaceCallback();
+      checkKeyDownTrigger();
+
+      // ENTER: confirm again
+      keyDown(KeyCode.ENTER);
+      enterCallback();
+      checkKeyDownTrigger();
+
+      // Blur
+      fireEvent.blur(container.querySelector('input'));
+      expect(onBlur).toHaveBeenCalled();
+
+      // null activeKey
+      fireEvent.mouseMove(getTreeNode(0));
+      checkActiveTrigger(null);
+
+      for (let i = 0; i < 10; i += 1) {
+        fireEvent.mouseMove(getTreeNode(0));
+        expect(onActiveChange).not.toHaveBeenCalled();
+      }
+    }
+
+    it('onSelect', () => {
+      const onSelect = jest.fn();
+      typeTest(
+        { onSelect },
+        () => {
+          expect(onSelect).toHaveBeenCalledWith(['child 1'], expect.anything());
+          onSelect.mockReset();
+        },
+        () => {
+          expect(onSelect).toHaveBeenCalledWith([], expect.anything());
+        },
+      );
+    });
+
+    it('onCheck', () => {
+      const onCheck = jest.fn();
+      typeTest(
+        { onCheck, checkable: true, selectable: false },
+        () => {
+          expect(onCheck).toHaveBeenCalledWith(['child 1'], expect.anything());
+          onCheck.mockReset();
+        },
+        () => {
+          expect(onCheck).toHaveBeenCalledWith([], expect.anything());
+        },
+      );
+    });
+
+    it('not crash if not exist', () => {
+      const { container } = render(<Tree defaultExpandAll treeData={[]} />);
+
+      fireEvent.focus(container.querySelector('input'));
+
+      // Arrow should not work
+      fireEvent.keyDown(container.querySelector('input'), {
+        keyCode: KeyCode.UP,
+      });
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
+    });
+
+    it('remove active if mouse hover', () => {
+      const { container } = render(<Tree defaultExpandAll treeData={[{ key: 'parent' }]} />);
+
+      fireEvent.focus(container.querySelector('input'));
+
+      fireEvent.keyDown(container.querySelector('input'), {
+        keyCode: KeyCode.UP,
+      });
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
+
+      // Mouse move
+      fireEvent.mouseMove(container.querySelectorAll('.rc-tree-treenode')[1]);
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
+    });
+
+    it('fieldNames should also work', () => {
+      const onActiveChange = jest.fn();
+      const onSelect = jest.fn();
+
+      const { container } = render(
+        <Tree<FieldDataNode<{ value: string }>>
+          defaultExpandAll
+          treeData={[{ value: 'first' }, { value: 'second' }]}
+          fieldNames={{ key: 'value' }}
+          onActiveChange={onActiveChange}
+          onSelect={onSelect}
+        />,
+      );
+
+      fireEvent.focus(container.querySelector('input'));
+
+      fireEvent.keyDown(container.querySelector('input'), {
+        keyCode: KeyCode.DOWN,
+      });
+      expect(onActiveChange).toHaveBeenCalledWith('first');
+
+      fireEvent.keyDown(container.querySelector('input'), {
+        keyCode: KeyCode.DOWN,
+      });
+      expect(onActiveChange).toHaveBeenCalledWith('second');
+
+      fireEvent.keyDown(container.querySelector('input'), {
+        keyCode: KeyCode.ENTER,
+      });
+      expect(onSelect).toHaveBeenCalledWith(['second'], expect.anything());
+    });
+  });
+
+  it('disabled should prevent keyboard', () => {
+    const { container } = render(<Tree disabled />);
+    expect(container.querySelector('input')).toHaveAttribute('disabled');
+  });
+
+  describe('activeKey in control', () => {
+    it('basic', () => {
+      const { container, rerender } = render(
+        <Tree
+          treeData={[
+            {
+              title: 'Parent',
+              key: 'parent',
+            },
+          ]}
+        />,
+      );
+
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
+
+      rerender(
+        <Tree
+          treeData={[
+            {
+              title: 'Parent',
+              key: 'parent',
+            },
+          ]}
+          activeKey="parent"
+        />,
+      );
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
+    });
+
+    it('with fieldNames', () => {
+      const { container, rerender } = render(
+        <Tree<FieldDataNode<{ title: string; value: string }>>
+          fieldNames={{ key: 'value' }}
+          treeData={[
+            {
+              title: 'Parent',
+              value: 'parent',
+            },
+          ]}
+        />,
+      );
+
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeFalsy();
+
+      rerender(
+        <Tree<FieldDataNode<{ title: string; value: string }>>
+          fieldNames={{ key: 'value' }}
+          treeData={[
+            {
+              title: 'Parent',
+              value: 'parent',
+            },
+          ]}
+          activeKey="parent"
+        />,
+      );
+      expect(container.querySelector('.rc-tree-treenode-active')).toBeTruthy();
+    });
+  });
+});
diff --git a/tests/FieldNames.spec.tsx b/tests/FieldNames.spec.tsx
new file mode 100644
index 000000000..e98efc8d2
--- /dev/null
+++ b/tests/FieldNames.spec.tsx
@@ -0,0 +1,199 @@
+/* eslint-disable no-undef, react/no-multi-comp */
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import Tree from '../src';
+import { spyConsole } from './util';
+
+describe('FieldNames', () => {
+  spyConsole();
+
+  it('customize fieldNames', () => {
+    const onSelect = jest.fn();
+    const { container } = render(
+      <Tree
+        treeData={
+          [
+            {
+              myTitle: 'Title',
+              myKey: 'title',
+              myChildren: [
+                { myTitle: 'Sub1', myKey: 'sub_1' },
+                { myTitle: 'Sub2', myKey: 'sub_2' },
+              ],
+            },
+          ] as any
+        }
+        defaultExpandAll
+        fieldNames={{
+          title: 'myTitle',
+          key: 'myKey',
+          children: 'myChildren',
+        }}
+        onSelect={onSelect}
+        multiple
+      />,
+    );
+
+    // Title
+    const titleList = Array.from(
+      container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode'),
+    ).map(node => node.textContent);
+
+    expect(titleList).toEqual(['Title', 'Sub1', 'Sub2']);
+
+    // Key
+    container.querySelectorAll('.rc-tree-title').forEach(ele => {
+      fireEvent.click(ele);
+    });
+
+    expect(onSelect).toHaveBeenCalledWith(['title', 'sub_1', 'sub_2'], expect.anything());
+  });
+
+  it('dynamic change fieldNames', () => {
+    const renderTree = (props?: any) => (
+      <Tree
+        defaultExpandAll
+        treeData={[
+          {
+            title: 'Origin Title',
+            key: 'parent',
+            children: [
+              {
+                title: 'Origin Sub 1',
+                key: 'sub_1',
+              },
+              {
+                title: 'Origin Sub 2',
+                key: 'sub_2',
+              },
+            ],
+          },
+        ]}
+        {...props}
+      />
+    );
+
+    const { container, rerender } = render(renderTree());
+
+    // Title
+    expect(
+      Array.from(container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode')).map(
+        node => node.textContent,
+      ),
+    ).toEqual(['Origin Title', 'Origin Sub 1', 'Origin Sub 2']);
+
+    // Change it
+    rerender(
+      renderTree({
+        treeData: [
+          {
+            myTitle: 'New Title',
+            myKey: 'parent',
+            myChildren: [
+              {
+                myTitle: 'New Sub 1',
+                myKey: 'sub_1',
+              },
+              {
+                myTitle: 'New Sub 2',
+                myKey: 'sub_2',
+              },
+            ],
+          },
+          {
+            myTitle: 'New Title 2',
+            myKey: 'parent2',
+            myChildren: [
+              {
+                myTitle: 'New Sub 3',
+                myKey: 'sub_3',
+              },
+            ],
+          },
+        ],
+        fieldNames: {
+          title: 'myTitle',
+          key: 'myKey',
+          children: 'myChildren',
+        },
+      }),
+    );
+
+    expect(
+      Array.from(container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode')).map(
+        node => node.textContent,
+      ),
+    ).toEqual(['New Title', 'New Sub 1', 'New Sub 2', 'New Title 2']);
+  });
+
+  it('checkable should work', () => {
+    const onCheck = jest.fn();
+
+    const { container } = render(
+      <Tree
+        checkable
+        onCheck={onCheck}
+        fieldNames={{ key: 'val', children: 'child' }}
+        treeData={
+          [
+            {
+              val: 'parent',
+              child: [
+                {
+                  val: 'child',
+                },
+              ],
+            },
+          ] as any
+        }
+      />,
+    );
+
+    fireEvent.click(container.querySelector('.rc-tree-checkbox'));
+    expect(onCheck).toHaveBeenCalledWith(['parent', 'child'], expect.anything());
+  });
+
+  // Internal usage. Safe to remove
+  it('fieldNames using _title', () => {
+    const onSelect = jest.fn();
+
+    const { container } = render(
+      <Tree
+        treeData={
+          [
+            {
+              myTitle: 'Title',
+              myKey: 'title',
+              myChildren: [
+                { myTitle: 'Sub1', myKey: 'sub_1' },
+                { myTitle: 'Sub2', myKey: 'sub_2' },
+              ],
+            },
+          ] as any
+        }
+        defaultExpandAll
+        fieldNames={{
+          _title: ['notExist', 'myTitle'],
+          key: 'myKey',
+          children: 'myChildren',
+        }}
+        onSelect={onSelect}
+        multiple
+      />,
+    );
+
+    // Title
+    const titleList = Array.from(
+      container.querySelectorAll('.rc-tree-list-holder div.rc-tree-treenode'),
+    ).map(node => node.textContent);
+
+    expect(titleList).toEqual(['Title', 'Sub1', 'Sub2']);
+
+    // Key
+    container.querySelectorAll('.rc-tree-title').forEach(ele => {
+      fireEvent.click(ele);
+    });
+
+    expect(onSelect).toHaveBeenCalledWith(['title', 'sub_1', 'sub_2'], expect.anything());
+  });
+});
diff --git a/tests/MyTree.tsx b/tests/MyTree.tsx
new file mode 100644
index 000000000..db4c1616f
--- /dev/null
+++ b/tests/MyTree.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import Tree from '../src';
+
+const MyTree = props => {
+  const { treeData, checkable, checkedKeys, onCheck } = props;
+  return (
+    <Tree
+      data-testid="tree"
+      className="my-tree"
+      checkable={checkable}
+      checkedKeys={checkedKeys}
+      onCheck={onCheck}
+      treeData={treeData}
+    />
+  );
+};
+
+export default MyTree;
diff --git a/tests/React18.spec.tsx b/tests/React18.spec.tsx
new file mode 100644
index 000000000..2db0146da
--- /dev/null
+++ b/tests/React18.spec.tsx
@@ -0,0 +1,88 @@
+import { fireEvent, render } from '@testing-library/react';
+import React from 'react';
+import Tree from '../src';
+
+describe('React 18', () => {
+  // This does not real work since waring is in 18.3.0 but current still is next version.
+  it('no warning', () => {
+    const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
+    render(
+      <React.StrictMode>
+        <Tree
+          defaultExpandAll
+          treeData={[
+            {
+              title: 'Parent',
+              key: 'parent',
+            },
+          ]}
+        />
+      </React.StrictMode>,
+    );
+
+    expect(errSpy).not.toHaveBeenCalled();
+  });
+
+  it('expand work', () => {
+    const onExpand = jest.fn();
+    const { container } = render(
+      <React.StrictMode>
+        <Tree
+          defaultExpandAll
+          onExpand={onExpand}
+          treeData={[
+            {
+              title: 'Parent',
+              key: 'parent',
+              children: [
+                {
+                  title: 'Child',
+                  key: 'child',
+                },
+              ],
+            },
+          ]}
+        />
+      </React.StrictMode>,
+    );
+
+    // All opened
+    expect(onExpand).not.toHaveBeenCalled();
+    expect(
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+    ).toHaveLength(2);
+
+    // Collapse one
+    fireEvent.click(container.querySelector('.rc-tree-switcher_open'));
+    expect(onExpand).toHaveBeenCalled();
+    expect(
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+    ).toHaveLength(1);
+  });
+
+  it('checkable work', () => {
+    const onCheck = jest.fn();
+    const { container } = render(
+      <React.StrictMode>
+        <Tree
+          defaultExpandAll
+          onCheck={onCheck}
+          checkable
+          treeData={[
+            {
+              title: 'Parent',
+              key: 'parent',
+            },
+          ]}
+        />
+      </React.StrictMode>,
+    );
+
+    expect(onCheck).not.toHaveBeenCalled();
+    expect(container.querySelector('.rc-tree-checkbox-checked')).toBeFalsy();
+
+    fireEvent.click(container.querySelector('.rc-tree-checkbox'));
+    expect(onCheck).toHaveBeenCalled();
+    expect(container.querySelector('.rc-tree-checkbox-checked')).toBeTruthy();
+  });
+});
diff --git a/tests/Tree.spec.js b/tests/Tree.spec.js
deleted file mode 100644
index d19a337c4..000000000
--- a/tests/Tree.spec.js
+++ /dev/null
@@ -1,1227 +0,0 @@
-/* eslint-disable no-undef, react/no-multi-comp, no-console,
-react/no-unused-state, react/prop-types, no-return-assign, import/no-named-as-default-member */
-import React from 'react';
-import { render, mount } from 'enzyme';
-import { renderToJson } from 'enzyme-to-json';
-import Tree, { TreeNode } from '../src';
-import { InternalTreeNode } from '../src/TreeNode';
-import { objectMatcher, spyConsole, spyError } from './util';
-
-const OPEN_CLASSNAME = '.rc-tree-switcher_open';
-const CHECKED_CLASSNAME = '.rc-tree-checkbox-checked';
-const SELECTED_CLASSNAME = '.rc-tree-node-selected';
-
-describe('Tree Basic', () => {
-  spyConsole();
-
-  it('TreeNode is in Tree', () => {
-    expect(TreeNode).toBe(Tree.TreeNode);
-  });
-
-  it('renders correctly', () => {
-    const wrapper = render(
-      <Tree
-        className="forTest"
-        selectable
-        checkable
-        defaultExpandAll
-        showIcon
-        showLine
-        multiple
-        focusable
-      >
-        <TreeNode title="parent 1" key="0-0" className="spe">
-          <TreeNode title="leaf 1" key="0-0-0" disabled>
-            <TreeNode title="leaf" key="random" />
-            <TreeNode title="leaf" />
-            {null /* Supports conditional rendering */}
-          </TreeNode>
-          <TreeNode title="leaf 2" key="0-0-1" disableCheckbox />
-        </TreeNode>
-      </Tree>,
-    );
-    expect(renderToJson(wrapper)).toMatchSnapshot();
-  });
-
-  describe('expanded', () => {
-    it('expands all keys', () => {
-      const wrapper = mount(
-        <Tree defaultExpandAll>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const switcher = wrapper.find('.rc-tree-switcher').first();
-      expect(switcher.is(OPEN_CLASSNAME)).toBe(true);
-    });
-
-    it('expands default expanded keys', () => {
-      const wrapper = mount(
-        <Tree defaultExpandedKeys={['0-0']}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const switcher = wrapper.find('.rc-tree-switcher').first();
-      expect(switcher.is(OPEN_CLASSNAME)).toBe(true);
-    });
-
-    it('controlled by expanded keys', () => {
-      const wrapper = mount(
-        <Tree expandedKeys={[]}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const getSwitcher = () => wrapper.find('.rc-tree-switcher').first();
-      expect(getSwitcher().is(OPEN_CLASSNAME)).toBe(false);
-      wrapper.setProps({ expandedKeys: ['0-0'] });
-      expect(getSwitcher().is(OPEN_CLASSNAME)).toBe(true);
-    });
-
-    it('expands parent node when child node is expanded', () => {
-      const wrapper = mount(
-        <Tree expandedKeys={['0-0-0']}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0">
-              <TreeNode title="leaf" key="0-0-0-0" />
-            </TreeNode>
-          </TreeNode>
-        </Tree>,
-      );
-
-      const switcher = wrapper.find('.rc-tree-switcher').first();
-      expect(switcher.is(OPEN_CLASSNAME)).toBe(true);
-    });
-
-    it('does not expand parent node when autoExpandParent is false', () => {
-      const wrapper = mount(
-        <Tree expandedKeys={['0-0-0']} defaultExpandParent={false}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0">
-              <TreeNode title="leaf" key="0-0-0-0" />
-            </TreeNode>
-          </TreeNode>
-        </Tree>,
-      );
-      const switcher = wrapper.find('.rc-tree-switcher').first();
-      expect(switcher.is(OPEN_CLASSNAME)).toBe(false);
-    });
-
-    it('update to expand parent node with autoExpandParent', () => {
-      const wrapper = mount(
-        <Tree expandedKeys={['0-0-0']} defaultExpandParent={false}>
-          <TreeNode title="parent 1" checkable={false} key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0">
-              <TreeNode title="leaf" key="0-0-0-0" />
-            </TreeNode>
-          </TreeNode>
-        </Tree>,
-      );
-      let parentSwitcher = wrapper.find('.rc-tree-switcher').first();
-      expect(parentSwitcher.is(OPEN_CLASSNAME)).toBe(false);
-
-      wrapper.setProps({ autoExpandParent: true });
-
-      parentSwitcher = wrapper.find('.rc-tree-switcher').first();
-      const childSwitcher = wrapper.find('.rc-tree-switcher').at(1);
-      expect(parentSwitcher.is(OPEN_CLASSNAME)).toBe(true);
-      expect(childSwitcher.is(OPEN_CLASSNAME)).toBe(true);
-    });
-
-    it('skip only if disabled with autoExpandParent', () => {
-      const wrapper = mount(
-        <Tree expandedKeys={['0-0-0-0']} defaultExpandParent>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" disabled key="0-0-0">
-              <TreeNode title="leaf" key="0-0-0-0" />
-            </TreeNode>
-          </TreeNode>
-        </Tree>,
-      );
-
-      expect(wrapper.state().expandedKeys.sort()).toEqual(['0-0-0', '0-0-0-0']);
-    });
-
-    it('fires expand event', () => {
-      const handleExpand = jest.fn();
-      const wrapper = mount(
-        <Tree onExpand={handleExpand}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const switcher = wrapper.find('.rc-tree-switcher');
-      const node = wrapper.find(InternalTreeNode).instance();
-
-      switcher.simulate('click');
-      expect(handleExpand).toHaveBeenCalledWith(['0-0'], {
-        expanded: true,
-        node,
-        nativeEvent: expect.objectContaining({}),
-      });
-
-      switcher.simulate('click');
-      expect(handleExpand).toHaveBeenCalledWith([], {
-        expanded: false,
-        node,
-        nativeEvent: expect.objectContaining({}),
-      });
-    });
-  });
-
-  describe('check', () => {
-    it('basic render', () => {
-      const wrapper = render(
-        <Tree checkable defaultExpandAll>
-          <TreeNode key="0-0">
-            <TreeNode key="0-0-0" disabled />
-            <TreeNode key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-
-      expect(wrapper).toMatchSnapshot();
-    });
-
-    it('checks default checked keys', () => {
-      const wrapper = mount(
-        <Tree checkable defaultCheckedKeys={['0-0']}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      wrapper.find('.rc-tree-checkbox').forEach(checkbox => {
-        expect(checkbox.is(CHECKED_CLASSNAME)).toBe(true);
-      });
-    });
-
-    it("ignore disabled children when calculate parent's checked status", () => {
-      const wrapper = mount(
-        <Tree checkable defaultCheckedKeys={['0-0-0']}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" disableCheckbox />
-            <TreeNode title="leaf 1" key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-      const firstCheckboxClassNames = wrapper.find('.rc-tree-checkbox').instance().classList;
-      expect([].slice.call(firstCheckboxClassNames).includes(CHECKED_CLASSNAME.slice(1))).toBe(
-        false,
-      );
-    });
-
-    it('controlled by checkedKeys', () => {
-      const wrapper = mount(
-        <Tree checkable checkedKeys={[]}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const getCheckbox = () => wrapper.find('.rc-tree-checkbox');
-      expect(getCheckbox().is(CHECKED_CLASSNAME)).toBe(false);
-      wrapper.setProps({ checkedKeys: ['0-0'] });
-      expect(getCheckbox().is(CHECKED_CLASSNAME)).toBe(true);
-    });
-
-    it('trurns parent node to checked when all children are checked', () => {
-      const wrapper = mount(
-        <Tree checkable>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      wrapper
-        .find('.rc-tree-checkbox')
-        .at(1)
-        .simulate('click');
-      wrapper
-        .find('.rc-tree-checkbox')
-        .at(2)
-        .simulate('click');
-      expect(
-        wrapper
-          .find('.rc-tree-checkbox')
-          .first()
-          .is(CHECKED_CLASSNAME),
-      ).toBe(true);
-    });
-
-    it('turns parent node to half checked when child is checked', () => {
-      const wrapper = mount(
-        <Tree checkable>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      wrapper
-        .find('.rc-tree-checkbox')
-        .last()
-        .simulate('click');
-      expect(
-        wrapper
-          .find('.rc-tree-checkbox')
-          .first()
-          .is('.rc-tree-checkbox-indeterminate'),
-      ).toBe(true);
-    });
-
-    it('turns parent node to checked if it is half checked', () => {
-      const wrapper = mount(
-        <Tree checkable>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      wrapper
-        .find('.rc-tree-checkbox')
-        .last()
-        .simulate('click');
-      wrapper
-        .find('.rc-tree-checkbox')
-        .first()
-        .simulate('click');
-      wrapper.find('.rc-tree-checkbox').forEach(checkbox => {
-        expect(checkbox.is(CHECKED_CLASSNAME)).toBe(true);
-      });
-    });
-
-    it('fires check event', () => {
-      const handleCheck = jest.fn();
-      const wrapper = mount(
-        <Tree checkable onCheck={handleCheck}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      const treeNode1 = wrapper.find(InternalTreeNode).first();
-      const treeNode2 = wrapper.find(InternalTreeNode).last();
-      // const treeElm1 = wrapper.find(Tree).props().children;
-      // const treeElm2 = treeNode1.props().children;
-
-      const nodeData00 = { title: 'parent 1', key: '0-0' };
-      const nodeData000 = { title: 'leaf 1', key: '0-0-0' };
-
-      wrapper
-        .find('.rc-tree-checkbox')
-        .first()
-        .simulate('click');
-
-      expect(handleCheck).toHaveBeenCalledWith(
-        ['0-0', '0-0-0'],
-        objectMatcher({
-          checked: true,
-          checkedNodes: [nodeData00, nodeData000],
-          checkedNodesPositions: [
-            { node: nodeData00, pos: '0-0' },
-            { node: nodeData000, pos: '0-0-0' },
-          ],
-          event: 'check',
-          halfCheckedKeys: [],
-          node: treeNode1.instance(),
-          nativeEvent: {},
-        }),
-      );
-
-      wrapper
-        .find('.rc-tree-checkbox')
-        .last()
-        .simulate('click');
-      expect(handleCheck).toHaveBeenCalledWith(
-        [],
-        objectMatcher({
-          checked: false,
-          checkedNodes: [],
-          checkedNodesPositions: [],
-          event: 'check',
-          halfCheckedKeys: [],
-          node: treeNode2.instance(),
-          nativeEvent: {},
-        }),
-      );
-    });
-
-    // https://github.com/react-component/tree/issues/117
-    it('check works correctly after dragging children under another node', () => {
-      const wrapper = mount(
-        <Tree defaultExpandAll checkable>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper
-        .find('.rc-tree-checkbox')
-        .at(1)
-        .simulate('click');
-      wrapper.setProps({
-        children: (
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 2" key="0-0-1">
-              <TreeNode title="leaf 1" key="0-0-0" />
-            </TreeNode>
-          </TreeNode>
-        ),
-      });
-      expect(() =>
-        wrapper
-          .find('.rc-tree-checkbox')
-          .at(2)
-          .simulate('click'),
-      ).not.toThrow();
-    });
-    // https://github.com/react-component/tree/issues/90
-    it('check works correctly after adding children dynamically', () => {
-      const wrapper = mount(
-        <Tree defaultExpandAll checkable>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper
-        .find('.rc-tree-checkbox')
-        .at(1)
-        .simulate('click');
-      wrapper.setProps({
-        children: (
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        ),
-      });
-      expect(() =>
-        wrapper
-          .find('.rc-tree-checkbox')
-          .at(2)
-          .simulate('click'),
-      ).not.toThrow();
-    });
-
-    // https://github.com/ant-design/ant-design/issues/7353
-    it('check children after changing from children[disableCheckbox] from true to false', () => {
-      let checkedKeys = null;
-      const mockHandleCheck = keys => (checkedKeys = keys);
-      function Test({ disableCheckbox }) {
-        return (
-          <Tree checkable onCheck={mockHandleCheck}>
-            <TreeNode title="parent 1" key="0-0">
-              <TreeNode title="leaf 1" key="0-0-1" disableCheckbox={disableCheckbox} />
-              <TreeNode title="leaf 2" key="0-0-2" disableCheckbox={disableCheckbox} />
-              <TreeNode title="leaf 3" key="0-0-3" disableCheckbox={disableCheckbox} />
-            </TreeNode>
-          </Tree>
-        );
-      }
-      const wrapper = mount(<Test disableCheckbox />);
-      wrapper
-        .find('.rc-tree-checkbox')
-        .first()
-        .simulate('click');
-      expect(checkedKeys).toEqual(['0-0']);
-      wrapper.setProps({ disableCheckbox: false });
-      wrapper
-        .find('.rc-tree-checkbox')
-        .first()
-        .simulate('click')
-        .simulate('click');
-      expect(checkedKeys).toEqual(['0-0', '0-0-1', '0-0-2', '0-0-3']);
-    });
-
-    it('check dynamic children when their parent is checked', () => {
-      function mockLoadData() {}
-      const wrapper = mount(
-        <Tree checkable defaultCheckedKeys={['0-0']} loadData={mockLoadData}>
-          <TreeNode title="parent 1" key="0-0" />
-        </Tree>,
-      );
-      wrapper.setProps({
-        children: (
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-            <TreeNode title="leaf 2" key="0-0-1" />
-          </TreeNode>
-        ),
-      });
-      expect(wrapper.state('checkedKeys')).toEqual(['0-0', '0-0-0', '0-0-1']);
-    });
-
-    // https://github.com/ant-design/ant-design/issues/10132
-    it('check update when Tree trigger componentWillReceiveProps', () => {
-      class Test extends React.Component {
-        state = {};
-
-        onCheck = () => {
-          this.setState({ whatever: 1 });
-        };
-
-        render() {
-          return (
-            <Tree checkable>
-              <TreeNode title="parent 1" key="0-0" />
-            </Tree>
-          );
-        }
-      }
-      const wrapper = mount(<Test />);
-      wrapper
-        .find('.rc-tree-checkbox')
-        .first()
-        .simulate('click');
-
-      expect(renderToJson(wrapper.render())).toMatchSnapshot();
-    });
-
-    describe('check after data ready', () => {
-      const errorSpy = spyError();
-
-      it('works', () => {
-        const checkedKeys = ['0-0-0'];
-        const wrapper = mount(<Tree checkable checkedKeys={checkedKeys} />);
-        expect(errorSpy()).toHaveBeenCalledWith("Warning: Tree missing follow keys: '0-0-0'");
-
-        wrapper.setProps({
-          expandedKeys: ['0-0'],
-          children: (
-            <TreeNode key="0-0" title="Light">
-              <TreeNode key="0-0-0" title="Bamboo" />
-            </TreeNode>
-          ),
-        });
-
-        expect(wrapper.render()).toMatchSnapshot();
-      });
-    });
-
-    it('should ignore !checkable node', () => {
-      const onCheck = jest.fn();
-      const wrapper = mount(
-        <Tree checkable defaultExpandAll onCheck={onCheck}>
-          <TreeNode key="0-0">
-            <TreeNode key="0-0-0" checkable={false} />
-            <TreeNode key="0-0-1" />
-          </TreeNode>
-        </Tree>,
-      );
-
-      wrapper
-        .find('.rc-tree-checkbox')
-        .last()
-        .simulate('click');
-
-      expect(onCheck.mock.calls[0][0].sort()).toEqual(['0-0', '0-0-1']);
-    });
-
-    describe('strictly', () => {
-      it('checks strictly', () => {
-        const wrapper = mount(
-          <Tree checkable checkStrictly>
-            <TreeNode title="parent 1" key="0-0">
-              <TreeNode title="leaf 1" key="0-0-0" />
-            </TreeNode>
-          </Tree>,
-        );
-        wrapper.find('.rc-tree-switcher').simulate('click');
-        wrapper
-          .find('.rc-tree-checkbox')
-          .first()
-          .simulate('click');
-        expect(
-          wrapper
-            .find('.rc-tree-checkbox')
-            .first()
-            .is(CHECKED_CLASSNAME),
-        ).toBe(true);
-        expect(
-          wrapper
-            .find('.rc-tree-checkbox')
-            .last()
-            .is(CHECKED_CLASSNAME),
-        ).toBe(false);
-      });
-
-      describe('controlled mode', () => {
-        class App extends React.Component {
-          state = {
-            checkedKeys: {
-              checked: [],
-              halfChecked: [],
-            },
-          };
-
-          handleCheck = checkedKeys => {
-            this.setState({ checkedKeys });
-          };
-
-          render() {
-            return (
-              <Tree
-                checkable
-                checkStrictly
-                checkedKeys={this.state.checkedKeys}
-                onCheck={this.handleCheck}
-              >
-                <TreeNode title="parent 1" key="0-0">
-                  <TreeNode title="leaf 1" key="0-0-0" />
-                </TreeNode>
-              </Tree>
-            );
-          }
-        }
-
-        it('do not check children', () => {
-          const wrapper = mount(<App />);
-          wrapper.find('.rc-tree-switcher').simulate('click');
-          wrapper
-            .find('.rc-tree-checkbox')
-            .first()
-            .simulate('click');
-          expect(
-            wrapper
-              .find('.rc-tree-checkbox')
-              .first()
-              .is(CHECKED_CLASSNAME),
-          ).toBe(true);
-          expect(
-            wrapper
-              .find('.rc-tree-checkbox')
-              .last()
-              .is(CHECKED_CLASSNAME),
-          ).toBe(false);
-        });
-
-        it('do not uncheck parent', () => {
-          const wrapper = mount(<App />);
-          wrapper.setState({ checkedKeys: { checked: ['0-0', '0-0-0'], halfChecked: [] } });
-          wrapper.find('.rc-tree-switcher').simulate('click');
-          wrapper
-            .find('.rc-tree-checkbox')
-            .last()
-            .simulate('click');
-          expect(
-            wrapper
-              .find('.rc-tree-checkbox')
-              .first()
-              .is(CHECKED_CLASSNAME),
-          ).toBe(true);
-          expect(
-            wrapper
-              .find('.rc-tree-checkbox')
-              .last()
-              .is(CHECKED_CLASSNAME),
-          ).toBe(false);
-        });
-      });
-    });
-
-    // https://github.com/ant-design/ant-design/issues/6891
-    it('should ignore disableCheckbox children items when check parent', () => {
-      const wrapper = mount(
-        <Tree checkable defaultExpandAll>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="node" key="0-0-0" disableCheckbox>
-              <TreeNode title="node" key="0-0-0-0" disableCheckbox />
-              <TreeNode title="node" key="0-0-0-1" disableCheckbox />
-            </TreeNode>
-            <TreeNode title="node" key="0-0-1" />
-            <TreeNode title="node" key="0-0-2" />
-          </TreeNode>
-        </Tree>,
-      );
-
-      const firstNode = () => wrapper.find('.rc-tree-checkbox').first();
-      firstNode().simulate('click');
-
-      expect(firstNode().hasClass('rc-tree-checkbox-checked')).toBe(true);
-      expect(firstNode().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(
-        wrapper
-          .state()
-          .checkedKeys.slice()
-          .sort(),
-      ).toEqual(['0-0-1', '0-0-2', '0-0'].sort());
-
-      firstNode().simulate('click');
-      expect(firstNode().hasClass('rc-tree-checkbox-checked')).toBe(false);
-      expect(firstNode().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(wrapper.state().checkedKeys).toEqual([]);
-    });
-
-    it('should ignore disabled children items when check parent', () => {
-      const wrapper = mount(
-        <Tree checkable defaultExpandAll>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="node" key="0-0-0" disabled>
-              <TreeNode title="node" key="0-0-0-0" disabled />
-              <TreeNode title="node" key="0-0-0-1" disabled />
-            </TreeNode>
-            <TreeNode title="node" key="0-0-1" />
-            <TreeNode title="node" key="0-0-2" />
-          </TreeNode>
-        </Tree>,
-      );
-
-      const firstNode = () =>
-        wrapper
-          .find(TreeNode)
-          .first()
-          .find('.rc-tree-checkbox')
-          .first();
-
-      firstNode().simulate('click');
-      expect(firstNode().hasClass('rc-tree-checkbox-checked')).toBe(true);
-      expect(firstNode().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(
-        wrapper
-          .state()
-          .checkedKeys.slice()
-          .sort(),
-      ).toEqual(['0-0-1', '0-0-2', '0-0'].sort());
-
-      firstNode().simulate('click');
-      expect(firstNode().hasClass('rc-tree-checkbox-checked')).toBe(false);
-      expect(firstNode().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(wrapper.state().checkedKeys).toEqual([]);
-    });
-
-    // https://github.com/react-component/tree/pull/106#issuecomment-316779889
-    it('should render parent check state correctly', () => {
-      const wrapper = mount(
-        <Tree checkable defaultExpandAll>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="node" key="0-0-0">
-              <TreeNode title="node" key="0-0-0-0" />
-              <TreeNode title="node" key="0-0-0-1" disableCheckbox />
-            </TreeNode>
-            <TreeNode title="node" key="0-0-1" />
-            <TreeNode title="node" key="0-0-2" />
-          </TreeNode>
-        </Tree>,
-      );
-      const getParent = () =>
-        wrapper
-          .find(TreeNode)
-          .at(1)
-          .find('.rc-tree-checkbox')
-          .first();
-      const getNode = () =>
-        wrapper
-          .find(TreeNode)
-          .at(2)
-          .find('.rc-tree-checkbox')
-          .first();
-
-      getNode().simulate('click');
-      expect(getNode().hasClass('rc-tree-checkbox-checked')).toBe(true);
-      expect(getParent().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(getParent().hasClass('rc-tree-checkbox-checked')).toBe(true);
-      getNode().simulate('click');
-      expect(getNode().hasClass('rc-tree-checkbox-checked')).toBe(false);
-      expect(getParent().hasClass('rc-tree-checkbox-indeterminate')).toBe(false);
-      expect(getParent().hasClass('rc-tree-checkbox-checked')).toBe(false);
-    });
-  });
-
-  describe('select', () => {
-    it('selects default selected keys', () => {
-      const wrapper = mount(
-        <Tree selectable defaultSelectedKeys={['0-0']}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      expect(wrapper.find('.rc-tree-node-content-wrapper').is(SELECTED_CLASSNAME)).toBe(true);
-    });
-
-    it('controlled by selectedKeys', () => {
-      const wrapper = mount(
-        <Tree selectable selectedKeys={[]}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      expect(wrapper.find('.rc-tree-node-content-wrapper').is(SELECTED_CLASSNAME)).toBe(false);
-      wrapper.setProps({ selectedKeys: ['0-0'] });
-      expect(wrapper.find('.rc-tree-node-content-wrapper').is(SELECTED_CLASSNAME)).toBe(true);
-    });
-
-    it('fires select event', () => {
-      const handleSelect = jest.fn();
-      const wrapper = mount(
-        <Tree selectable onSelect={handleSelect}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      const nodeContent = wrapper.find('.rc-tree-node-content-wrapper');
-      const node = wrapper
-        .find(InternalTreeNode)
-        .first()
-        .instance();
-      const nodeElm = wrapper.find(Tree).props().children;
-
-      nodeContent.simulate('click');
-      expect(handleSelect).toHaveBeenCalledWith(
-        ['0-0'],
-        objectMatcher({
-          event: 'select',
-          node,
-          selected: true,
-          selectedNodes: [{ title: 'parent 1', key: '0-0' }],
-          nativeEvent: {},
-        }),
-      );
-
-      nodeContent.simulate('click');
-      expect(handleSelect).toHaveBeenCalledWith(
-        [],
-        objectMatcher({
-          event: 'select',
-          node,
-          selected: false,
-          selectedNodes: [],
-          nativeEvent: {},
-        }),
-      );
-    });
-  });
-
-  describe('checkable but not selectable', () => {
-    it('fires check event when click on TreeNode', () => {
-      const handleCheck = jest.fn();
-      const wrapper = mount(
-        <Tree checkable selectable={false} onCheck={handleCheck}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode title="leaf 1" key="0-0-0" />
-          </TreeNode>
-        </Tree>,
-      );
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      const treeNode1 = wrapper.find(InternalTreeNode).first();
-      const treeNode2 = wrapper.find(InternalTreeNode).last();
-
-      const dataNode1 = { title: 'parent 1', key: '0-0' };
-      const dataNode2 = { title: 'leaf 1', key: '0-0-0' };
-
-      wrapper
-        .find('.rc-tree-node-content-wrapper')
-        .first()
-        .simulate('click');
-      expect(handleCheck).toHaveBeenCalledWith(
-        ['0-0', '0-0-0'],
-        objectMatcher({
-          checked: true,
-          checkedNodes: [dataNode1, dataNode2],
-          checkedNodesPositions: [
-            { node: dataNode1, pos: '0-0' },
-            { node: dataNode2, pos: '0-0-0' },
-          ],
-          event: 'check',
-          halfCheckedKeys: [],
-          node: treeNode1.instance(),
-          nativeEvent: {},
-        }),
-      );
-
-      wrapper
-        .find('.rc-tree-node-content-wrapper')
-        .last()
-        .simulate('click');
-      expect(handleCheck).toHaveBeenCalledWith(
-        [],
-        objectMatcher({
-          checked: false,
-          checkedNodes: [],
-          checkedNodesPositions: [],
-          event: 'check',
-          halfCheckedKeys: [],
-          node: treeNode2.instance(),
-          nativeEvent: {},
-        }),
-      );
-    });
-  });
-
-  it('fires rightClick event', () => {
-    const handleRightClick = jest.fn();
-    const wrapper = mount(
-      <Tree onRightClick={handleRightClick}>
-        <TreeNode title="parent 1" key="0-0">
-          <TreeNode title="leaf 1" key="0-0-0" />
-        </TreeNode>
-      </Tree>,
-    );
-    wrapper.find('.rc-tree-node-content-wrapper').simulate('contextMenu');
-    expect(handleRightClick.mock.calls[0][0].node).toBe(wrapper.find(InternalTreeNode).instance());
-  });
-
-  it('fires rightClick should not change selected item', () => {
-    const handleRightClick = jest.fn();
-    const wrapper = mount(
-      <Tree onRightClick={handleRightClick}>
-        <TreeNode title="parent 1" key="0-0">
-          <TreeNode title="leaf 1" key="0-0-0" />
-        </TreeNode>
-      </Tree>,
-    );
-    wrapper.find('.rc-tree-node-content-wrapper').simulate('contextMenu');
-    expect(wrapper.state().selectedKeys.length).toBe(0);
-  });
-
-  it('fires mouseEnter events', () => {
-    const handleMouseEnter = jest.fn();
-    const wrapper = mount(
-      <Tree onMouseEnter={handleMouseEnter}>
-        <TreeNode title="parent 1" key="0-0">
-          <TreeNode title="leaf 1" key="0-0-0" />
-        </TreeNode>
-      </Tree>,
-    );
-    wrapper.find('.rc-tree-node-content-wrapper').simulate('mouseEnter');
-    expect(handleMouseEnter.mock.calls[0][0].node).toBe(wrapper.find(InternalTreeNode).instance());
-  });
-
-  it('fires mouseLeave events', () => {
-    const handleMouseLeave = jest.fn();
-    const wrapper = mount(
-      <Tree onMouseLeave={handleMouseLeave}>
-        <TreeNode title="parent 1" key="0-0">
-          <TreeNode title="leaf 1" key="0-0-0" />
-        </TreeNode>
-      </Tree>,
-    );
-    wrapper.find('.rc-tree-node-content-wrapper').simulate('mouseLeave');
-    expect(handleMouseLeave.mock.calls[0][0].node).toBe(wrapper.find(InternalTreeNode).instance());
-  });
-
-  it('filters nodes', () => {
-    function filterTreeNode(treeNode) {
-      return treeNode.props.title.indexOf('parent') > -1;
-    }
-    const wrapper = mount(
-      <Tree filterTreeNode={filterTreeNode}>
-        <TreeNode title="parent 1" key="0-0">
-          <TreeNode title="leaf 1" key="0-0-0">
-            <TreeNode title="meck" key="0-0-0-0" />
-            <TreeNode title="leaf" key="0-0-0-1" />
-          </TreeNode>
-          <TreeNode title="leaf 2" key="0-0-1" />
-        </TreeNode>
-      </Tree>,
-    );
-
-    expect(
-      wrapper
-        .find(InternalTreeNode)
-        .find('div')
-        .is('.filter-node'),
-    ).toBe(true);
-  });
-
-  it('loads nodes asynchronously', () => {
-    const then = jest.fn(() => Promise.resolve());
-    const loadData = jest.fn(() => ({ then }));
-    const wrapper = mount(
-      <Tree loadData={loadData}>
-        <TreeNode title="parent 1" key="0-0" />
-      </Tree>,
-    );
-    wrapper.find('.rc-tree-switcher').simulate('click');
-    expect(loadData).toHaveBeenCalledWith(wrapper.find(InternalTreeNode).instance());
-    expect(then).toHaveBeenCalled();
-  });
-
-  describe('drag and drop', () => {
-    function createTree(props) {
-      return (
-        <Tree draggable defaultExpandAll {...props}>
-          <TreeNode title="parent 1" key="0-0">
-            <TreeNode className="dragTarget" title="leaf" key="0-0-0-0" />
-            <TreeNode className="dropTarget" title="leaf" key="0-0-0-1" />
-          </TreeNode>
-        </Tree>
-      );
-    }
-
-    it('fires dragStart event', () => {
-      const onDragStart = jest.fn();
-      const wrapper = mount(createTree({ onDragStart }));
-      const treeNode = wrapper.find('.dragTarget > .rc-tree-node-content-wrapper');
-      treeNode.simulate('dragStart');
-      const event = onDragStart.mock.calls[0][0];
-      expect(event.node).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(1)
-          .instance(),
-      );
-    });
-
-    it('fires dragEnter event', done => {
-      const onDragEnter = jest.fn();
-      const wrapper = mount(createTree({ onDragEnter }));
-      wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart');
-      wrapper
-        .find('.dropTarget')
-        .at(0)
-        .simulate('dragEnter');
-      expect(onDragEnter).not.toHaveBeenCalled();
-      setTimeout(() => {
-        const event = onDragEnter.mock.calls[0][0];
-        expect(event.node).toBe(
-          wrapper
-            .find(InternalTreeNode)
-            .at(2)
-            .instance(),
-        );
-        expect(event.expandedKeys).toEqual(['0-0', '0-0-0-1']);
-        done();
-      }, 500);
-    });
-
-    it('fires dragOver event', () => {
-      const onDragOver = jest.fn();
-      const wrapper = mount(createTree({ onDragOver }));
-      wrapper
-        .find('.dropTarget')
-        .at(0)
-        .simulate('dragOver');
-      const event = onDragOver.mock.calls[0][0];
-      expect(event.node).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(2)
-          .instance(),
-      );
-    });
-
-    it('fires dragLeave event', () => {
-      const onDragLeave = jest.fn();
-      const wrapper = mount(createTree({ onDragLeave }));
-      wrapper
-        .find('.dropTarget')
-        .at(0)
-        .simulate('dragLeave');
-      const event = onDragLeave.mock.calls[0][0];
-      expect(event.node).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(2)
-          .instance(),
-      );
-    });
-
-    it('fires drop event', () => {
-      const onDrop = jest.fn();
-      const wrapper = mount(createTree({ onDrop }));
-      wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart');
-      wrapper
-        .find('.dropTarget')
-        .at(0)
-        .simulate('drop');
-      const event = onDrop.mock.calls[0][0];
-      expect(event.node).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(2)
-          .instance(),
-      );
-      expect(event.dragNode).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(1)
-          .instance(),
-      );
-      expect(event.dragNodesKeys).toEqual(['0-0-0-0']);
-    });
-
-    it('fires dropEnd event', () => {
-      const onDragEnd = jest.fn();
-      const wrapper = mount(createTree({ onDragEnd }));
-      wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragEnd');
-      const event = onDragEnd.mock.calls[0][0];
-      expect(event.node).toBe(
-        wrapper
-          .find(InternalTreeNode)
-          .at(1)
-          .instance(),
-      );
-    });
-
-    it('do not throw error when drag into another non-drag-able tree', () => {
-      const wrapper = mount(
-        <div>
-          {createTree()}
-          {createTree({ draggable: false })}
-        </div>,
-      );
-
-      const dragTree = wrapper.find(Tree).at(0);
-      const normalTree = wrapper.find(Tree).at(1);
-
-      dragTree.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart');
-      normalTree
-        .find('.dropTarget')
-        .at(0)
-        .simulate('dragEnter');
-      normalTree
-        .find('.dropTarget')
-        .at(0)
-        .simulate('dragOver');
-      normalTree
-        .find('.dropTarget')
-        .at(0)
-        .simulate('drop');
-    });
-
-    describe('full steps', () => {
-      function dropTarget(targetSelector) {
-        return new Promise(resolve => {
-          const wrapper = mount(
-            <Tree draggable defaultExpandAll>
-              <TreeNode key="0-0" className="dragTarget">
-                <TreeNode key="0-0-0" className="dragTargetChild" />
-              </TreeNode>
-              <TreeNode key="0-1" className="dropTarget">
-                <TreeNode key="0-1-0" />
-              </TreeNode>
-            </Tree>,
-          );
-
-          wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart');
-
-          // 1. Move into target (first in the middle of the node)
-          wrapper.find(targetSelector).simulate('dragEnter', { clientY: 10 });
-          setTimeout(() => {
-            wrapper.find(targetSelector).simulate('dragOver', { clientY: 999 });
-
-            // 2. Move out of target
-            wrapper.find(targetSelector).simulate('dragLeave');
-
-            // 3. Move in again
-            wrapper.find(targetSelector).simulate('dragEnter', { clientY: 0 });
-            setTimeout(() => {
-              wrapper.find(targetSelector).simulate('dragOver', { clientY: 999 });
-
-              // 4. Drop
-              wrapper.find(targetSelector).simulate('drop');
-              wrapper.find('div.dragTarget').simulate('dragEnd');
-
-              resolve();
-            }, 500);
-          }, 10);
-        });
-      }
-
-      const { getBoundingClientRect } = Element.prototype;
-      beforeEach(() => {
-        Element.prototype.getBoundingClientRect = jest.fn(() => ({
-          width: 100,
-          height: 20,
-          top: 0,
-          left: 0,
-          bottom: 20,
-          right: 100,
-        }));
-      });
-
-      afterEach(() => {
-        Element.prototype.getBoundingClientRect = getBoundingClientRect;
-      });
-
-      it('self', () => dropTarget('div.dragTarget'));
-
-      it('target', () => dropTarget('div.dropTarget'));
-    });
-  });
-
-  it('renders without errors', () => {
-    expect(() => {
-      mount(
-        <Tree>
-          {[0, 1].map(i => (
-            <TreeNode title={i} key={i}>
-              {[2, 3].map(j => (
-                <TreeNode title={j} key={`${i}_${j}`} />
-              ))}
-              <TreeNode title="4" key={`${i}_4`} />
-            </TreeNode>
-          ))}
-        </Tree>,
-      );
-    }).not.toThrow();
-  });
-
-  it('renders opaque children correctly', () => {
-    const wrapper = render(
-      <Tree>
-        <TreeNode title="0" key="0">
-          {[1, 2].map(i => (
-            <TreeNode title={i} key={i} />
-          ))}
-          <TreeNode title="3" key="3" />
-        </TreeNode>
-      </Tree>,
-    );
-
-    expect(renderToJson(wrapper)).toMatchSnapshot();
-  });
-
-  describe('ignore illegal node as Tree children', () => {
-    const errorSpy = spyError();
-
-    it('Direct TreeNode', () => {
-      const wrapper = mount(
-        <Tree defaultExpandAll>
-          <TreeNode key="00" title="00" />
-          <span>Hide Me</span>
-          <TreeNode key="02" title="02" />
-        </Tree>,
-      );
-      expect(wrapper.render()).toMatchSnapshot();
-      expect(errorSpy()).toHaveBeenCalledWith(
-        'Warning: Tree/TreeNode can only accept TreeNode as children.',
-      );
-    });
-
-    it('Sub TreeNode', () => {
-      const wrapper = mount(
-        <Tree defaultExpandAll>
-          <TreeNode key="00" title="00" />
-          <TreeNode key="01" title="01">
-            <TreeNode key="010" title="010" />
-            <span>I AM INVISIBLE</span>
-            <TreeNode key="012" title="012" />
-          </TreeNode>
-        </Tree>,
-      );
-      expect(wrapper.render()).toMatchSnapshot();
-      expect(errorSpy()).toHaveBeenCalledWith(
-        'Warning: Tree/TreeNode can only accept TreeNode as children.',
-      );
-    });
-  });
-});
diff --git a/tests/Tree.spec.tsx b/tests/Tree.spec.tsx
new file mode 100644
index 000000000..fd8f6e350
--- /dev/null
+++ b/tests/Tree.spec.tsx
@@ -0,0 +1,1414 @@
+/* eslint-disable no-undef, react/no-multi-comp, no-console,
+react/no-unused-state, react/prop-types, no-return-assign */
+import React from 'react';
+import { render, fireEvent, act } from '@testing-library/react';
+import { resetWarned } from '@rc-component/util/lib/warning';
+import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
+import Tree, { TreeNode } from '../src';
+import { objectMatcher, spyConsole, spyError } from './util';
+import { UnstableContext } from '../src';
+
+const OPEN_CLASSNAME = 'rc-tree-switcher_open';
+const CHECKED_CLASSNAME = 'rc-tree-checkbox-checked';
+const SELECTED_CLASSNAME = 'rc-tree-node-selected';
+
+const delay = (timeout = 0) =>
+  new Promise(resolve => {
+    setTimeout(resolve, timeout);
+  });
+
+describe('Tree Basic', () => {
+  spyConsole();
+
+  it('TreeNode is in Tree', () => {
+    expect(TreeNode).toBe(Tree.TreeNode);
+  });
+
+  it('renders correctly', () => {
+    const { asFragment } = render(
+      <Tree
+        className="forTest"
+        selectable
+        checkable
+        defaultExpandAll
+        showIcon
+        showLine
+        multiple
+        focusable
+      >
+        <TreeNode title="parent 1" key="0-0" className="spe">
+          <TreeNode title="leaf 1" key="0-0-0" disabled>
+            <TreeNode title="leaf" key="random" />
+            <TreeNode title="leaf" />
+            {null /* Supports conditional rendering */}
+          </TreeNode>
+          <TreeNode title="leaf 2" key="0-0-1" disableCheckbox />
+        </TreeNode>
+      </Tree>,
+    );
+    expect(asFragment().firstChild).toMatchSnapshot();
+  });
+
+  it('switcherIcon = null, no render rc-tree-switcher null', () => {
+    const { container } = render(
+      <Tree className="forTest" defaultExpandAll switcherIcon={() => false}>
+        <TreeNode title="parent 1" key="0-0" className="spe">
+          <TreeNode title="leaf 1" key="0-0-0" disabled>
+            <TreeNode title="leaf" key="random" />
+            <TreeNode title="leaf" />
+          </TreeNode>
+          <TreeNode title="leaf 2" key="0-0-1" disableCheckbox />
+        </TreeNode>
+      </Tree>,
+    );
+    expect(container.querySelector('.rc-tree-switcher')).toBeFalsy();
+  });
+
+  describe('expanded', () => {
+    it('expands all keys', () => {
+      const { container } = render(
+        <Tree defaultExpandAll>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    });
+
+    it('expands default expanded keys', () => {
+      const { container } = render(
+        <Tree defaultExpandedKeys={['0-0']}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    });
+
+    it('controlled by expanded keys', () => {
+      const renderTree = (props?: any) => (
+        <Tree expandedKeys={[]} {...props}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>
+      );
+
+      const { container, rerender } = render(renderTree());
+      expect(container.querySelector(`.${OPEN_CLASSNAME}`)).toBeFalsy();
+      rerender(renderTree({ expandedKeys: ['0-0'] }));
+      expect(container.querySelector(`.${OPEN_CLASSNAME}`)).toBeTruthy();
+    });
+
+    it('expands parent node when child node is expanded', () => {
+      const { container } = render(
+        <Tree expandedKeys={['0-0-0']}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0">
+              <TreeNode title="leaf" key="0-0-0-0" />
+            </TreeNode>
+          </TreeNode>
+        </Tree>,
+      );
+
+      expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    });
+
+    it('use treeData to expand the parent node when the parent node key type is numeric', () => {
+      const Demo = () => (
+        <Tree
+          defaultExpandParent
+          defaultExpandedKeys={[22]}
+          treeData={[{ key: 11, title: 11, children: [{ key: 22, title: 22 }] }]}
+        />
+      );
+
+      const { container } = render(<Demo />);
+
+      expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    });
+
+    it('does not expand parent node when autoExpandParent is false', () => {
+      const { container } = render(
+        <Tree expandedKeys={['0-0-0']} defaultExpandParent={false}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0">
+              <TreeNode title="leaf" key="0-0-0-0" />
+            </TreeNode>
+          </TreeNode>
+        </Tree>,
+      );
+      expect(container.querySelector(`.${OPEN_CLASSNAME}`)).toBeFalsy();
+    });
+
+    it('update to expand parent node with autoExpandParent', () => {
+      const renderTree = (props?: any) => (
+        <Tree expandedKeys={['0-0-0']} defaultExpandParent={false} {...props}>
+          <TreeNode title="parent 1" checkable={false} key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0">
+              <TreeNode title="leaf" key="0-0-0-0" />
+            </TreeNode>
+          </TreeNode>
+        </Tree>
+      );
+      const { container, rerender } = render(renderTree());
+      expect(container.querySelector(`.${OPEN_CLASSNAME}`)).toBeFalsy();
+
+      rerender(renderTree({ autoExpandParent: true }));
+
+      // All opened
+      expect(container.querySelectorAll('.rc-tree-switcher')[0]).toHaveClass(OPEN_CLASSNAME);
+      expect(container.querySelectorAll('.rc-tree-switcher')[1]).toHaveClass(OPEN_CLASSNAME);
+    });
+
+    it('skip only if disabled with autoExpandParent', () => {
+      const { container } = render(
+        <Tree expandedKeys={['0-0-0-0']} defaultExpandParent>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" disabled key="0-0-0">
+              <TreeNode title="leaf" key="0-0-0-0" />
+            </TreeNode>
+          </TreeNode>
+        </Tree>,
+      );
+
+      expect(
+        container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+      ).toHaveLength(1);
+    });
+
+    it('fires expand event', () => {
+      const handleExpand = jest.fn();
+      const { container } = render(
+        <Tree onExpand={handleExpand}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      expect(handleExpand).toHaveBeenCalledWith(['0-0'], {
+        expanded: true,
+        node: expect.objectContaining({
+          key: '0-0',
+          title: 'parent 1',
+        }),
+        nativeEvent: expect.anything(),
+      });
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      expect(handleExpand).toHaveBeenCalledWith([], {
+        expanded: false,
+        node: expect.objectContaining({
+          key: '0-0',
+          title: 'parent 1',
+        }),
+        nativeEvent: expect.anything(),
+      });
+    });
+  });
+
+  describe('check', () => {
+    it('basic render', () => {
+      const { asFragment } = render(
+        <Tree checkable defaultExpandAll>
+          <TreeNode key="0-0">
+            <TreeNode key="0-0-0" disabled />
+            <TreeNode key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      expect(asFragment().firstChild).toMatchSnapshot();
+    });
+
+    it('checks default checked keys', () => {
+      const { container } = render(
+        <Tree checkable defaultCheckedKeys={['0-0']}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      expect(container.querySelectorAll('.rc-tree-checkbox')).toHaveLength(2);
+      container.querySelectorAll('.rc-tree-checkbox').forEach(checkbox => {
+        expect(checkbox).toHaveClass(CHECKED_CLASSNAME);
+      });
+    });
+
+    it("ignore disabled children when calculate parent's checked status", () => {
+      const { container } = render(
+        <Tree checkable defaultCheckedKeys={['0-0-0']}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" disableCheckbox />
+            <TreeNode title="leaf 1" key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+      expect(container.querySelector(`.${CHECKED_CLASSNAME}`)).toBeFalsy();
+    });
+
+    it('controlled by checkedKeys', () => {
+      const renderTree = (props?: any) => (
+        <Tree checkable checkedKeys={[]} {...props}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>
+      );
+      const { container, rerender } = render(renderTree());
+      expect(container.querySelector(`.${CHECKED_CLASSNAME}`)).toBeFalsy();
+
+      rerender(renderTree({ checkedKeys: ['0-0'] }));
+      expect(container.querySelector(`.${CHECKED_CLASSNAME}`)).toBeTruthy();
+    });
+
+    it('turn parent node to checked when all children are checked', () => {
+      const { container } = render(
+        <Tree checkable>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      expect(container.querySelectorAll('.rc-tree-checkbox')[0]).toHaveClass(CHECKED_CLASSNAME);
+    });
+
+    it('turns parent node to half checked when child is checked', () => {
+      const { container } = render(
+        <Tree checkable>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      expect(container.querySelectorAll('.rc-tree-checkbox')[0]).toHaveClass(
+        'rc-tree-checkbox-indeterminate',
+      );
+    });
+
+    it('turns parent node to checked if it is half checked', () => {
+      const { container } = render(
+        <Tree checkable>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+      container.querySelectorAll('.rc-tree-checkbox').forEach(ele => {
+        expect(ele).toHaveClass(CHECKED_CLASSNAME);
+      });
+    });
+
+    it('fires check event', () => {
+      const handleCheck = jest.fn();
+      const { container } = render(
+        <Tree checkable onCheck={handleCheck}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+
+      const nodeData00 = { title: 'parent 1', key: '0-0' };
+      const nodeData000 = { title: 'leaf 1', key: '0-0-0' };
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+      expect(handleCheck).toHaveBeenCalledWith(
+        ['0-0', '0-0-0'],
+        objectMatcher({
+          checked: true,
+          checkedNodes: [nodeData00, nodeData000],
+          checkedNodesPositions: [
+            { node: nodeData00, pos: '0-0' },
+            { node: nodeData000, pos: '0-0-0' },
+          ],
+          event: 'check',
+          halfCheckedKeys: [],
+          node: {
+            title: 'parent 1',
+            key: '0-0',
+          },
+          nativeEvent: {},
+        }),
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+      expect(handleCheck).toHaveBeenCalledWith(
+        [],
+        objectMatcher({
+          checked: false,
+          checkedNodes: [],
+          checkedNodesPositions: [],
+          event: 'check',
+          halfCheckedKeys: [],
+          node: {
+            title: 'leaf 1',
+            key: '0-0-0',
+          },
+          nativeEvent: {},
+        }),
+      );
+    });
+
+    // https://github.com/react-component/tree/issues/117
+    it('check works correctly after dragging children under another node', () => {
+      const renderTree = (children: React.ReactNode) => (
+        <Tree defaultExpandedKeys={['0-0', '0-0-1']} checkable>
+          {children}
+        </Tree>
+      );
+      const { container, rerender } = render(
+        renderTree(
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>,
+        ),
+      );
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+
+      rerender(
+        renderTree(
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 2" key="0-0-1">
+              <TreeNode title="leaf 1" key="0-0-0" />
+            </TreeNode>
+          </TreeNode>,
+        ),
+      );
+
+      expect(() => {
+        fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      }).not.toThrow();
+    });
+
+    // https://github.com/react-component/tree/issues/90
+    it('check works correctly after adding children dynamically', () => {
+      const renderTree = (children: React.ReactNode) => (
+        <Tree defaultExpandAll checkable>
+          {children}
+        </Tree>
+      );
+      const { container, rerender } = render(
+        renderTree(
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>,
+        ),
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+      rerender(
+        renderTree(
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>,
+        ),
+      );
+      expect(() => {
+        fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      }).not.toThrow();
+    });
+
+    // https://github.com/ant-design/ant-design/issues/7353
+    it('check children after changing from children[disableCheckbox] from true to false', () => {
+      let checkedKeys = null;
+      const mockHandleCheck = (keys: string[]) => (checkedKeys = keys);
+      function Test({ disableCheckbox }) {
+        return (
+          <Tree checkable onCheck={mockHandleCheck}>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-1" disableCheckbox={disableCheckbox} />
+              <TreeNode title="leaf 2" key="0-0-2" disableCheckbox={disableCheckbox} />
+              <TreeNode title="leaf 3" key="0-0-3" disableCheckbox={disableCheckbox} />
+            </TreeNode>
+          </Tree>
+        );
+      }
+      const { container, rerender } = render(<Test disableCheckbox />);
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+      expect(checkedKeys).toEqual(['0-0']);
+
+      rerender(<Test disableCheckbox={false} />);
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+      expect(checkedKeys).toEqual(['0-0', '0-0-1', '0-0-2', '0-0-3']);
+    });
+
+    it('check dynamic children when their parent is checked', () => {
+      const mockLoadData: any = () => new Promise(() => {});
+      const renderTree = (children?: React.ReactNode) => (
+        <Tree checkable defaultCheckedKeys={['0-0']} loadData={mockLoadData} expandedKeys={['0-0']}>
+          {children}
+        </Tree>
+      );
+
+      const { container, rerender } = render(renderTree(<TreeNode title="parent 1" key="0-0" />));
+      rerender(
+        renderTree(
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+            <TreeNode title="leaf 2" key="0-0-1" />
+          </TreeNode>,
+        ),
+      );
+
+      expect(container.querySelectorAll(`.${CHECKED_CLASSNAME}`)).toHaveLength(3);
+    });
+
+    // https://github.com/ant-design/ant-design/issues/10132
+    it('check update when Tree trigger componentWillReceiveProps', () => {
+      class Test extends React.Component {
+        state = {};
+
+        onCheck = () => {
+          this.setState({ whatever: 1 });
+        };
+
+        render() {
+          return (
+            <Tree checkable>
+              <TreeNode title="parent 1" key="0-0" />
+            </Tree>
+          );
+        }
+      }
+      const { container } = render(<Test />);
+      fireEvent.click(container.querySelector('.rc-tree-checkbox'));
+
+      expect(container.firstChild).toMatchSnapshot();
+    });
+
+    describe('check after data ready', () => {
+      const errorSpy = spyError();
+
+      it('works', () => {
+        const checkedKeys = ['0-0-0'];
+        const { container, rerender } = render(<Tree checkable checkedKeys={checkedKeys} />);
+        expect(errorSpy()).toHaveBeenCalledWith("Warning: Tree missing follow keys: '0-0-0'");
+
+        rerender(
+          <Tree checkable checkedKeys={checkedKeys} expandedKeys={['0-0']}>
+            <TreeNode key="0-0" title="Light">
+              <TreeNode key="0-0-0" title="Bamboo" />
+            </TreeNode>
+          </Tree>,
+        );
+
+        expect(container.firstChild).toMatchSnapshot();
+      });
+    });
+
+    it('should ignore !checkable node', () => {
+      const onCheck = jest.fn();
+      const { container } = render(
+        <Tree checkable defaultExpandAll onCheck={onCheck}>
+          <TreeNode key="0-0">
+            <TreeNode key="0-0-0" checkable={false} />
+            <TreeNode key="0-0-1" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+
+      expect(onCheck.mock.calls[0][0].sort()).toEqual(['0-0', '0-0-1']);
+    });
+
+    describe('strictly', () => {
+      it('checks strictly', () => {
+        const { container } = render(
+          <Tree checkable checkStrictly>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-0" />
+            </TreeNode>
+          </Tree>,
+        );
+        fireEvent.click(container.querySelector('.rc-tree-switcher'));
+        fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+        expect(container.querySelectorAll('.rc-tree-checkbox')[0]).toHaveClass(CHECKED_CLASSNAME);
+        expect(container.querySelectorAll('.rc-tree-checkbox')[1]).not.toHaveClass(
+          CHECKED_CLASSNAME,
+        );
+      });
+
+      describe('controlled mode', () => {
+        class App extends React.Component {
+          state = {
+            checkedKeys: {
+              checked: [],
+              halfChecked: [],
+            },
+          };
+
+          handleCheck = checkedKeys => {
+            this.setState({ checkedKeys });
+          };
+
+          render() {
+            return (
+              <>
+                <Tree
+                  checkable
+                  checkStrictly
+                  checkedKeys={this.state.checkedKeys}
+                  onCheck={this.handleCheck}
+                >
+                  <TreeNode title="parent 1" key="0-0">
+                    <TreeNode title="leaf 1" key="0-0-0" />
+                  </TreeNode>
+                </Tree>
+                <button
+                  onClick={() => {
+                    this.setState({ checkedKeys: { checked: ['0-0', '0-0-0'], halfChecked: [] } });
+                  }}
+                />
+              </>
+            );
+          }
+        }
+
+        it('do not check children', () => {
+          const { container } = render(<App />);
+          fireEvent.click(container.querySelector('.rc-tree-switcher'));
+          fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[0]);
+          expect(container.querySelectorAll('.rc-tree-checkbox')[0]).toHaveClass(CHECKED_CLASSNAME);
+          expect(container.querySelectorAll('.rc-tree-checkbox')[1]).not.toHaveClass(
+            CHECKED_CLASSNAME,
+          );
+        });
+
+        it('do not uncheck parent', () => {
+          const { container } = render(<App />);
+          fireEvent.click(container.querySelector('button'));
+          fireEvent.click(container.querySelector('.rc-tree-switcher'));
+          fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+          expect(container.querySelectorAll('.rc-tree-checkbox')[0]).toHaveClass(CHECKED_CLASSNAME);
+          expect(container.querySelectorAll('.rc-tree-checkbox')[1]).not.toHaveClass(
+            CHECKED_CLASSNAME,
+          );
+        });
+      });
+    });
+
+    // https://github.com/ant-design/ant-design/issues/6891
+    it('should ignore disableCheckbox children items when check parent', () => {
+      const onCheck = jest.fn();
+      const { container } = render(
+        <Tree checkable defaultExpandAll onCheck={onCheck}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="node" key="0-0-0" disableCheckbox>
+              <TreeNode title="node" key="0-0-0-0" disableCheckbox />
+              <TreeNode title="node" key="0-0-0-1" disableCheckbox />
+            </TreeNode>
+            <TreeNode title="node" key="0-0-1" />
+            <TreeNode title="node" key="0-0-2" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      const firstNode = container.querySelectorAll('.rc-tree-checkbox')[0];
+      fireEvent.click(firstNode);
+
+      expect(firstNode).toHaveClass('rc-tree-checkbox-checked');
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(onCheck).toHaveBeenCalledWith(['0-0', '0-0-1', '0-0-2'], expect.anything());
+
+      fireEvent.click(firstNode);
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-checked');
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(onCheck).toHaveBeenCalledWith([], expect.anything());
+    });
+
+    it('should ignore disabled children items when check parent', () => {
+      const onCheck = jest.fn();
+      const { container } = render(
+        <Tree checkable defaultExpandAll onCheck={onCheck}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="node" key="0-0-0" disabled>
+              <TreeNode title="node" key="0-0-0-0" disabled />
+              <TreeNode title="node" key="0-0-0-1" disabled />
+            </TreeNode>
+            <TreeNode title="node" key="0-0-1" />
+            <TreeNode title="node" key="0-0-2" />
+          </TreeNode>
+        </Tree>,
+      );
+
+      const firstNode = container.querySelector('.rc-tree-checkbox');
+
+      fireEvent.click(firstNode);
+      expect(firstNode).toHaveClass('rc-tree-checkbox-checked');
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(onCheck).toHaveBeenCalledWith(['0-0', '0-0-1', '0-0-2'], expect.anything());
+
+      fireEvent.click(firstNode);
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-checked');
+      expect(firstNode).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(onCheck).toHaveBeenCalledWith([], expect.anything());
+    });
+
+    // https://github.com/react-component/tree/pull/106#issuecomment-316779889
+    it('should render parent check state correctly', () => {
+      const { container } = render(
+        <Tree checkable defaultExpandAll>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="node" key="0-0-0">
+              <TreeNode title="node" key="0-0-0-0" />
+              <TreeNode title="node" key="0-0-0-1" disableCheckbox />
+            </TreeNode>
+            <TreeNode title="node" key="0-0-1" />
+            <TreeNode title="node" key="0-0-2" />
+          </TreeNode>
+        </Tree>,
+      );
+      const parent = container.querySelectorAll('.rc-tree-checkbox')[1];
+      const node = container.querySelectorAll('.rc-tree-checkbox')[2];
+
+      fireEvent.click(node);
+      expect(node).toHaveClass('rc-tree-checkbox-checked');
+      expect(parent).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(parent).toHaveClass('rc-tree-checkbox-checked');
+
+      fireEvent.click(node);
+      expect(node).not.toHaveClass('rc-tree-checkbox-checked');
+      expect(parent).not.toHaveClass('rc-tree-checkbox-indeterminate');
+      expect(parent).not.toHaveClass('rc-tree-checkbox-checked');
+    });
+  });
+
+  describe('select', () => {
+    it('selects default selected keys', () => {
+      const { container } = render(
+        <Tree selectable defaultSelectedKeys={['0-0']}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      expect(container.querySelector('.rc-tree-node-content-wrapper')).toHaveClass(
+        SELECTED_CLASSNAME,
+      );
+    });
+
+    it('controlled by selectedKeys', () => {
+      const renderTree = (props?: any) => (
+        <Tree selectable selectedKeys={[]} {...props}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>
+      );
+
+      const { container, rerender } = render(renderTree());
+      expect(container.querySelector('.rc-tree-node-content-wrapper')).not.toHaveClass(
+        SELECTED_CLASSNAME,
+      );
+
+      rerender(renderTree({ selectedKeys: ['0-0'] }));
+      expect(container.querySelector('.rc-tree-node-content-wrapper')).toHaveClass(
+        SELECTED_CLASSNAME,
+      );
+    });
+
+    it('fires select event', () => {
+      const handleSelect = jest.fn();
+      const { container } = render(
+        <Tree selectable onSelect={handleSelect}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      const nodeContent = container.querySelector('.rc-tree-node-content-wrapper');
+
+      fireEvent.click(nodeContent);
+      expect(handleSelect).toHaveBeenCalledWith(
+        ['0-0'],
+        objectMatcher({
+          event: 'select',
+          node: {
+            title: 'parent 1',
+            key: '0-0',
+          },
+          selected: true,
+          selectedNodes: [{ title: 'parent 1', key: '0-0' }],
+          nativeEvent: {},
+        }),
+      );
+
+      fireEvent.click(nodeContent);
+      expect(handleSelect).toHaveBeenCalledWith(
+        [],
+        objectMatcher({
+          event: 'select',
+          node: {
+            title: 'parent 1',
+            key: '0-0',
+          },
+          selected: false,
+          selectedNodes: [],
+          nativeEvent: {},
+        }),
+      );
+    });
+  });
+
+  describe('checkable but not selectable', () => {
+    it('fires check event when click on TreeNode', () => {
+      const handleCheck = jest.fn();
+      const { container } = render(
+        <Tree checkable selectable={false} onCheck={handleCheck}>
+          <TreeNode title="parent 1" key="0-0">
+            <TreeNode title="leaf 1" key="0-0-0" />
+          </TreeNode>
+        </Tree>,
+      );
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+
+      const dataNode1 = { title: 'parent 1', key: '0-0' };
+      const dataNode2 = { title: 'leaf 1', key: '0-0-0' };
+
+      fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
+      expect(handleCheck).toHaveBeenCalledWith(
+        ['0-0', '0-0-0'],
+        objectMatcher({
+          checked: true,
+          checkedNodes: [dataNode1, dataNode2],
+          checkedNodesPositions: [
+            { node: dataNode1, pos: '0-0' },
+            { node: dataNode2, pos: '0-0-0' },
+          ],
+          event: 'check',
+          halfCheckedKeys: [],
+          node: { title: 'parent 1', key: '0-0' },
+          nativeEvent: {},
+        }),
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
+      expect(handleCheck).toHaveBeenCalledWith(
+        [],
+        objectMatcher({
+          checked: false,
+          checkedNodes: [],
+          checkedNodesPositions: [],
+          event: 'check',
+          halfCheckedKeys: [],
+          node: {
+            title: 'leaf 1',
+            key: '0-0-0',
+          },
+          nativeEvent: {},
+        }),
+      );
+    });
+  });
+
+  it('fires rightClick event', () => {
+    const handleRightClick = jest.fn();
+    const { container } = render(
+      <Tree onRightClick={handleRightClick}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+    fireEvent.contextMenu(container.querySelector('.rc-tree-node-content-wrapper'));
+    expect(handleRightClick.mock.calls[0][0].node).toEqual(
+      expect.objectContaining({
+        title: 'parent 1',
+        key: '0-0',
+      }),
+    );
+  });
+
+  it('fires rightClick should not change selected item', () => {
+    const onRightClick = jest.fn();
+    const onSelect = jest.fn();
+    const { container } = render(
+      <Tree onRightClick={onRightClick} selectedKeys={['0-0-0']} onSelect={onSelect}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+    fireEvent.contextMenu(container.querySelector('.rc-tree-node-content-wrapper'));
+    expect(onRightClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(container.querySelector('.rc-tree-node-selected')).toBeFalsy();
+  });
+
+  it('fires mouseEnter events', () => {
+    const handleMouseEnter = jest.fn();
+    const { container } = render(
+      <Tree onMouseEnter={handleMouseEnter}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+    fireEvent.mouseEnter(container.querySelector('.rc-tree-node-content-wrapper'));
+    expect(handleMouseEnter.mock.calls[0][0].node).toEqual(
+      expect.objectContaining({
+        key: '0-0',
+      }),
+    );
+  });
+
+  it('fires mouseLeave events', () => {
+    const handleMouseLeave = jest.fn();
+    const { container } = render(
+      <Tree onMouseLeave={handleMouseLeave}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    fireEvent.mouseLeave(container.querySelector('.rc-tree-node-content-wrapper'));
+    expect(handleMouseLeave.mock.calls[0][0].node).toEqual(
+      expect.objectContaining({
+        key: '0-0',
+      }),
+    );
+  });
+
+  it('filters nodes', () => {
+    function filterTreeNode(treeNode) {
+      return treeNode.title.indexOf('parent') > -1;
+    }
+    const { container } = render(
+      <Tree filterTreeNode={filterTreeNode}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0">
+            <TreeNode title="meck" key="0-0-0-0" />
+            <TreeNode title="leaf" key="0-0-0-1" />
+          </TreeNode>
+          <TreeNode title="leaf 2" key="0-0-1" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    expect(container.querySelector('.filter-node')).toBeTruthy();
+  });
+
+  it('loads nodes asynchronously', () => {
+    const then = jest.fn(() => Promise.resolve());
+    const loadData: any = jest.fn(() => ({ then }));
+    const { container } = render(
+      <Tree loadData={loadData}>
+        <TreeNode title="parent 1" key="0-0" />
+      </Tree>,
+    );
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    expect(loadData).toHaveBeenCalledWith(
+      expect.objectContaining({
+        key: '0-0',
+      }),
+    );
+    expect(then).toHaveBeenCalled();
+  });
+
+  it('renders without errors', () => {
+    expect(() => {
+      render(
+        <Tree>
+          {[0, 1].map(i => (
+            <TreeNode title={i} key={i}>
+              {[2, 3].map(j => (
+                <TreeNode title={j} key={`${i}_${j}`} />
+              ))}
+              <TreeNode title="4" key={`${i}_4`} />
+            </TreeNode>
+          ))}
+        </Tree>,
+      );
+    }).not.toThrow();
+  });
+
+  it('renders opaque children correctly', () => {
+    const { asFragment } = render(
+      <Tree>
+        <TreeNode title="0" key="0">
+          {[1, 2].map(i => (
+            <TreeNode title={i} key={i} />
+          ))}
+          <TreeNode title="3" key="3" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    expect(asFragment().firstChild).toMatchSnapshot();
+  });
+
+  describe('ignore illegal node as Tree children', () => {
+    const errorSpy = spyError();
+
+    it('Direct TreeNode', () => {
+      const { asFragment } = render(
+        <Tree defaultExpandAll>
+          <TreeNode key="00" title="00" />
+          <span>Hide Me</span>
+          <TreeNode key="02" title="02" />
+        </Tree>,
+      );
+      expect(asFragment().firstChild).toMatchSnapshot();
+      expect(errorSpy()).toHaveBeenCalledWith(
+        'Warning: Tree/TreeNode can only accept TreeNode as children.',
+      );
+    });
+
+    it('Sub TreeNode', () => {
+      resetWarned();
+      const { asFragment } = render(
+        <Tree defaultExpandAll>
+          <TreeNode key="00" title="00" />
+          <TreeNode key="01" title="01">
+            <TreeNode key="010" title="010" />
+            <span>I AM INVISIBLE</span>
+            <TreeNode key="012" title="012" />
+          </TreeNode>
+        </Tree>,
+      );
+      expect(asFragment().firstChild).toMatchSnapshot();
+      expect(errorSpy()).toHaveBeenCalledWith(
+        'Warning: Tree/TreeNode can only accept TreeNode as children.',
+      );
+    });
+  });
+
+  it('Number key', () => {
+    const onCheck = jest.fn();
+
+    const Demo = () => {
+      const [checkedKeys, setCheckedKeys] = React.useState<any>([]);
+
+      return (
+        <Tree
+          checkable
+          defaultExpandAll
+          checkedKeys={checkedKeys}
+          onCheck={keys => {
+            setCheckedKeys(keys);
+            onCheck(keys);
+          }}
+          treeData={[{ key: 11, title: 11, children: [{ key: 22, title: 22 }] }]}
+        />
+      );
+    };
+
+    const { container } = render(<Demo />);
+
+    fireEvent.click(container.querySelector('.rc-tree-checkbox'));
+    expect(onCheck).toHaveBeenCalledWith([11, 22]);
+
+    onCheck.mockReset();
+    fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+    expect(onCheck).toHaveBeenCalledWith([]);
+  });
+
+  describe('scrollTo should work', () => {
+    let domSpy: ReturnType<typeof spyElementPrototypes>;
+    let called = false;
+
+    beforeAll(() => {
+      domSpy = spyElementPrototypes(HTMLDivElement, {
+        scrollTop: {
+          get: () => 233,
+          set: () => {
+            called = true;
+          },
+        },
+        clientHeight: {
+          get: () => 100,
+        },
+      });
+    });
+
+    afterAll(() => {
+      domSpy.mockRestore();
+    });
+
+    it('work', () => {
+      jest.useFakeTimers();
+      const treeRef = React.createRef<any>();
+      render(<Tree ref={treeRef} />);
+
+      act(() => {
+        treeRef.current.scrollTo({ key: 'light', align: 'top' });
+      });
+
+      jest.runAllTimers();
+
+      expect(called).toBeTruthy();
+      jest.useRealTimers();
+    });
+  });
+
+  describe('offset should work', () => {
+    let domSpy: ReturnType<typeof spyElementPrototypes>;
+
+    beforeAll(() => {
+      domSpy = spyElementPrototypes(HTMLDivElement, {
+        clientHeight: {
+          get: () => 100,
+        },
+      });
+    });
+
+    afterAll(() => {
+      domSpy.mockRestore();
+    });
+
+    it('work', () => {
+      jest.useFakeTimers();
+
+      const data = [];
+      for (let i = 0; i < 10; i++) {
+        data.push({
+          key: i,
+          title: i,
+        });
+      }
+
+      const treeRef = React.createRef<any>();
+
+      const { container } = render(
+        <Tree ref={treeRef} treeData={data} virtual itemHeight={20} height={100} />,
+      );
+
+      act(() => {
+        treeRef.current.scrollTo({ key: 5, offset: 5 });
+        jest.runAllTimers();
+      });
+
+      expect(container.querySelector('.rc-tree-list-holder').scrollTop).toEqual(25);
+
+      jest.useRealTimers();
+    });
+  });
+
+  it('not crash if expandedKeys is null', () => {
+    render(
+      <Tree expandedKeys={null}>
+        <TreeNode key="test" title="alive" />
+      </Tree>,
+    );
+  });
+
+  it('not crash if expandedKeys change to undefined', () => {
+    const renderTree = (props?: any) => (
+      <Tree expandedKeys={['light']} {...props}>
+        <TreeNode key="light" title="light">
+          <TreeNode key="bamboo" title="bamboo" />
+        </TreeNode>
+      </Tree>
+    );
+    const { container, rerender } = render(renderTree());
+    expect(
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+    ).toHaveLength(2);
+
+    rerender(renderTree({ expandedKeys: undefined }));
+
+    // Click should not crash
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+  });
+
+  it('controlled should block expanded', () => {
+    const { container } = render(
+      <Tree
+        expandedKeys={['1']}
+        treeData={[{ key: '1', title: 1, children: [{ key: '2', title: 2 }] }]}
+      />,
+    );
+
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    expect(
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+    ).toHaveLength(2);
+  });
+
+  // https://github.com/ant-design/ant-design/issues/28349
+  it('should not trigger expend when loading data', () => {
+    const then = jest.fn(() => Promise.resolve());
+    const loadData: any = jest.fn(() => ({ then }));
+    const onExpand = jest.fn();
+    const { container } = render(
+      <Tree loadData={loadData} onExpand={onExpand}>
+        <TreeNode title="parent 1" key="0-0" />
+      </Tree>,
+    );
+
+    // trigger click to expand node
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    expect(onExpand).toBeCalled();
+
+    // click again
+    onExpand.mockReset();
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    expect(container.querySelector('.rc-tree-switcher')).toHaveClass(OPEN_CLASSNAME);
+    expect(onExpand).not.toHaveBeenCalled();
+  });
+
+  // https://github.com/ant-design/ant-design/issues/31141
+  it('should restore tree node loading and expand status when loadData fail', async () => {
+    const loadData = () => new Promise((_, reject) => setTimeout(reject));
+    const { container } = render(
+      <Tree loadData={loadData}>
+        <TreeNode title="parent 1" key="0-0" />
+      </Tree>,
+    );
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    expect(container.querySelector('.rc-tree-switcher')).toHaveClass('rc-tree-switcher_open');
+    await delay(100);
+    expect(container.querySelector('.rc-tree-switcher')).toHaveClass('rc-tree-switcher_close');
+  });
+
+  // https://github.com/ant-design/ant-design/issues/31141
+  it('should support onScroll', async () => {
+    const onScroll = jest.fn();
+    const data = [];
+    for (let i = 0; i < 99; i += 1) {
+      data.push({
+        key: i,
+        title: i,
+      });
+    }
+    const { container } = render(
+      <Tree itemHeight={10} height={100} treeData={data} onScroll={onScroll} />,
+    );
+    fireEvent.scroll(container.querySelector('.rc-tree-list-holder'));
+    expect(onScroll).toBeCalled();
+  });
+
+  it('should support rootStyle and rootClassName', () => {
+    const data = [];
+    for (let i = 0; i < 9; i += 1) {
+      data.push({
+        key: i,
+        title: i,
+      });
+    }
+    const { container } = render(
+      <Tree treeData={data} rootClassName="root-tree" rootStyle={{ backgroundColor: 'cyan' }} />,
+    );
+
+    expect(container.firstChild).toMatchSnapshot();
+    expect(container.firstChild).toHaveClass('root-tree');
+    expect(container.firstChild).toHaveStyle({ backgroundColor: 'cyan' });
+  });
+
+  describe('nodeDisabled', () => {
+    it('should disable node by nodeDisabled function', () => {
+      const { getByRole } = render(
+        <UnstableContext.Provider
+          value={{
+            nodeDisabled: node => node.key === '0-0-0',
+          }}
+        >
+          <Tree defaultExpandAll>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-0" />
+              <TreeNode title="leaf 2" key="0-0-1" />
+            </TreeNode>
+          </Tree>
+        </UnstableContext.Provider>,
+      );
+
+      expect(getByRole('treeitem', { name: 'leaf 1' })).toHaveClass('rc-tree-treenode-disabled');
+      expect(getByRole('treeitem', { name: 'leaf 2' })).not.toHaveClass(
+        'rc-tree-treenode-disabled',
+      );
+    });
+
+    it('should work with checkable tree', () => {
+      const onCheck = jest.fn();
+      const { container } = render(
+        <UnstableContext.Provider
+          value={{
+            nodeDisabled: node => node.key === '0-0-0',
+          }}
+        >
+          <Tree checkable defaultExpandAll onCheck={onCheck}>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-0" />
+              <TreeNode title="leaf 2" key="0-0-1" />
+            </TreeNode>
+          </Tree>
+        </UnstableContext.Provider>,
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[1]);
+      expect(onCheck).not.toHaveBeenCalled();
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-checkbox')[2]);
+      expect(onCheck).toHaveBeenCalled();
+    });
+
+    it('should work with selectable tree', () => {
+      const onSelect = jest.fn();
+      const { container } = render(
+        <UnstableContext.Provider
+          value={{
+            nodeDisabled: node => node.key === '0-0-0',
+          }}
+        >
+          <Tree selectable defaultExpandAll onSelect={onSelect}>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-0" />
+              <TreeNode title="leaf 2" key="0-0-1" />
+            </TreeNode>
+          </Tree>
+        </UnstableContext.Provider>,
+      );
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
+      expect(onSelect).not.toHaveBeenCalled();
+
+      fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[2]);
+      expect(onSelect).toHaveBeenCalled();
+    });
+
+    it('should override disabled prop', () => {
+      const { getByRole } = render(
+        <UnstableContext.Provider
+          value={{
+            nodeDisabled: node => node.key === '0-0-0',
+          }}
+        >
+          <Tree defaultExpandAll>
+            <TreeNode title="parent 1" key="0-0">
+              <TreeNode title="leaf 1" key="0-0-0" disabled={false} />
+              <TreeNode title="leaf 2" key="0-0-1" disabled />
+            </TreeNode>
+          </Tree>
+        </UnstableContext.Provider>,
+      );
+
+      expect(getByRole('treeitem', { name: 'leaf 1' })).toHaveClass('rc-tree-treenode-disabled');
+      expect(getByRole('treeitem', { name: 'leaf 2' })).toHaveClass('rc-tree-treenode-disabled');
+    });
+
+    it('correct handle closure', () => {
+      let disabled = false;
+      const nodeDisabled = () => disabled;
+
+      const singleTreeData = [
+        {
+          title: '0',
+          key: '0',
+        },
+      ];
+
+      const renderDemo = () => (
+        <UnstableContext.Provider
+          value={{
+            nodeDisabled,
+          }}
+        >
+          <Tree defaultExpandAll treeData={singleTreeData} />
+        </UnstableContext.Provider>
+      );
+
+      const { container, rerender } = render(renderDemo());
+      expect(container.querySelector('.rc-tree-treenode-disabled')).toBeFalsy();
+
+      disabled = true;
+      rerender(renderDemo());
+      expect(container.querySelector('.rc-tree-treenode-disabled')).toBeTruthy();
+    });
+  });
+  it('leaf className', () => {
+    const data = [
+      {
+        title: '0-0',
+        key: '0-0',
+        children: [
+          { title: '0-0-0', key: '0-0-0' },
+          {
+            title: '0-0-1',
+            key: '0-0-1',
+            children: [
+              { title: '0-0-1-0', key: '0-0-1-0' },
+              { title: '0-0-1-1', key: '0-0-1-1' },
+            ],
+          },
+        ],
+      },
+      { title: '0-1', key: '0-1' },
+    ];
+    const { container } = render(<Tree treeData={data} expandedKeys={['0-0', '0-0-1']} />);
+    const treeNodes = container.querySelectorAll('.rc-tree-treenode-leaf');
+    expect(treeNodes.length).toBe(4);
+  });
+  it('support classNames and styles', () => {
+    const data = [
+      {
+        title: '0-0',
+        key: '0-0',
+        children: [
+          { title: '0-0-0', key: '0-0-0' },
+          {
+            title: '0-0-1',
+            key: '0-0-1',
+            children: [
+              { title: '0-0-1-0', key: '0-0-1-0' },
+              { title: '0-0-1-1', key: '0-0-1-1' },
+            ],
+          },
+        ],
+      },
+      { title: '0-1', key: '0-1' },
+    ];
+    const testClassNames = {
+      item: 'test-item',
+      itemIcon: 'test-icon',
+      itemTitle: 'test-title',
+    };
+    const testStyles = {
+      item: { background: 'red' },
+      itemIcon: { color: 'blue' },
+      itemTitle: { color: 'yellow' },
+    };
+    const { container } = render(
+      <Tree
+        treeData={data}
+        expandedKeys={['0-0', '0-0-1']}
+        styles={testStyles}
+        classNames={testClassNames}
+      />,
+    );
+    const item = container.querySelector(`.${testClassNames.item}`);
+    const icon = container.querySelector('.rc-tree-iconEle');
+    const title = container.querySelector('.rc-tree-title');
+
+    expect(icon).toHaveStyle(testStyles.itemIcon);
+    expect(icon).toHaveClass(testClassNames.itemIcon);
+    expect(title).toHaveStyle(testStyles.itemTitle);
+    expect(title).toHaveClass(testClassNames.itemTitle);
+    expect(item).toHaveStyle(testStyles.item);
+  });
+});
diff --git a/tests/TreeDraggable.spec.tsx b/tests/TreeDraggable.spec.tsx
new file mode 100644
index 000000000..5fb082d2d
--- /dev/null
+++ b/tests/TreeDraggable.spec.tsx
@@ -0,0 +1,1200 @@
+/* eslint-disable no-undef, react/no-multi-comp, no-console,
+react/no-unused-state, react/prop-types, no-return-assign */
+import { act, createEvent, fireEvent, render } from '@testing-library/react';
+import { spyElementPrototypes } from '@rc-component/util/lib/test/domHook';
+import React from 'react';
+import Tree, { FieldDataNode, TreeNode, TreeProps } from '../src';
+import { spyConsole } from './util';
+
+const delay = timeout =>
+  new Promise(resolve => {
+    setTimeout(resolve, timeout);
+  });
+
+describe('Tree Draggable', () => {
+  spyConsole();
+
+  function createTree(props?: Partial<TreeProps>) {
+    return (
+      <Tree draggable defaultExpandAll {...props}>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode className="dragTarget" title="leaf" key="0-0-0-0" />
+          <TreeNode className="dropTarget" title="leaf" key="0-0-0-1" />
+        </TreeNode>
+      </Tree>
+    );
+  }
+
+  function fireDragEvent(ele: HTMLElement, eventName: string, data: object = {}) {
+    const event = createEvent[eventName](ele);
+    Object.keys(data).forEach(key => {
+      event[key] = data[key];
+    });
+    fireEvent(ele, event);
+  }
+
+  it('fires dragStart event', () => {
+    const onDragStart = jest.fn();
+    const { container } = render(createTree({ onDragStart }));
+    const treeNode = container.querySelector('.dragTarget > .rc-tree-node-content-wrapper');
+    fireEvent.dragStart(treeNode);
+    const event = onDragStart.mock.calls[0][0];
+    expect(event.node).toEqual(
+      expect.objectContaining({
+        key: '0-0-0-0',
+      }),
+    );
+  });
+
+  it('fires dragEnter event', async () => {
+    const onDragEnter = jest.fn();
+    const { container } = render(createTree({ onDragEnter }));
+
+    fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+
+    // Not trigger self
+    fireEvent.dragEnter(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+
+    await act(async () => {
+      await delay(900);
+    });
+    expect(onDragEnter).not.toHaveBeenCalled();
+
+    fireEvent.dragEnter(container.querySelector('.dropTarget'));
+    expect(onDragEnter).toHaveBeenCalled();
+
+    await act(async () => {
+      await delay(900);
+    });
+    const event = onDragEnter.mock.calls[0][0];
+    expect(event.node.key).toEqual('0-0-0-1');
+    expect(event.expandedKeys).toEqual(['0-0']);
+    expect(onDragEnter).toHaveBeenCalledTimes(1);
+  });
+
+  it('fires dragOver event', () => {
+    const onDragOver = jest.fn();
+    const { container } = render(createTree({ onDragOver }));
+    fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    fireEvent.dragOver(container.querySelector('.dropTarget'));
+    const event = onDragOver.mock.calls[0][0];
+    expect(event.node).toEqual(
+      expect.objectContaining({
+        key: '0-0-0-1',
+      }),
+    );
+  });
+
+  it('fires dragLeave event', () => {
+    const onDragLeave = jest.fn();
+    const { container } = render(createTree({ onDragLeave }));
+    fireEvent.dragLeave(container.querySelector('.dropTarget'));
+    const event = onDragLeave.mock.calls[0][0];
+    expect(event.node).toEqual(
+      expect.objectContaining({
+        key: '0-0-0-1',
+      }),
+    );
+  });
+
+  it('fires drop event', () => {
+    const onDrop = jest.fn();
+    const { container } = render(createTree({ onDrop }));
+    fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    fireEvent.dragEnter(container.querySelector('.dropTarget'));
+    fireEvent.dragOver(container.querySelector('.dropTarget'));
+    fireEvent.drop(container.querySelector('.dropTarget'));
+    const event = onDrop.mock.calls[0][0];
+    expect(event.node).toEqual(expect.objectContaining({ key: '0-0-0-1' }));
+    expect(event.dragNode).toEqual(expect.objectContaining({ key: '0-0-0-0' }));
+    expect(event.dragNodesKeys).toEqual(['0-0-0-0']);
+  });
+
+  it('fires dropEnd event', () => {
+    const onDragEnd = jest.fn();
+    const { container } = render(createTree({ onDragEnd }));
+    fireEvent.dragEnd(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    const event = onDragEnd.mock.calls[0][0];
+    expect(event.node).toEqual(
+      expect.objectContaining({
+        key: '0-0-0-0',
+      }),
+    );
+  });
+
+  it('do not throw error when drag into another non-drag-able tree', () => {
+    const { container } = render(
+      <>
+        <div className="tree1">{createTree()}</div>
+        <div className="tree2">{createTree({ draggable: false })}</div>
+      </>,
+    );
+
+    const dragTree = container.querySelector('.tree1');
+    const normalTree = container.querySelector('.tree2');
+
+    fireEvent.dragStart(dragTree.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    fireEvent.dragEnter(normalTree.querySelector('.dropTarget'));
+    fireEvent.dragOver(normalTree.querySelector('.dropTarget'));
+    fireEvent.drop(normalTree.querySelector('.dropTarget'));
+  });
+
+  describe('full steps', () => {
+    function dropTarget(targetSelector: string) {
+      return new Promise<void>(resolve => {
+        const { container } = render(
+          <Tree draggable={() => true} defaultExpandAll onExpand={() => {}}>
+            <TreeNode key="0-0" className="dragTarget">
+              <TreeNode key="0-0-0" className="dragTargetChild" />
+            </TreeNode>
+            <TreeNode key="0-1">
+              <TreeNode key="0-1-0" />
+            </TreeNode>
+            <TreeNode key="0-2" className="dropTarget">
+              <TreeNode key="0-2-0" />
+            </TreeNode>
+          </Tree>,
+        );
+
+        fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+
+        // 1. Move into target (first in the middle of the node)
+        fireEvent.dragEnter(container.querySelector(targetSelector), { clientY: 10 });
+        setTimeout(() => {
+          fireEvent.dragOver(container.querySelector(targetSelector), { clientY: 999 });
+
+          // 2. Move out of target
+          fireEvent.dragLeave(container.querySelector(targetSelector));
+
+          // 3. Move in again
+          fireEvent.dragEnter(container.querySelector(targetSelector), { clientY: 0 });
+
+          setTimeout(() => {
+            fireEvent.dragOver(container.querySelector(targetSelector), { clientY: 999 });
+
+            // 4. Drop
+            fireEvent.drop(container.querySelector(targetSelector));
+            fireEvent.dragEnd(container.querySelector('.dragTarget'));
+
+            resolve();
+          }, 1000);
+        }, 10);
+      });
+    }
+
+    let domSpy;
+    beforeEach(() => {
+      domSpy = spyElementPrototypes(HTMLElement, {
+        offsetWidth: {
+          get() {
+            return 24;
+          },
+        },
+        getBoundingClientRect: jest.fn(() => ({
+          width: 100,
+          height: 20,
+          top: 0,
+          left: 0,
+          bottom: 20,
+          right: 100,
+        })),
+      });
+    });
+
+    afterEach(() => {
+      domSpy.mockRestore();
+    });
+
+    it('self', () => dropTarget('div.dragTarget'));
+
+    it('target', () => dropTarget('div.dropTarget'));
+  });
+
+  describe('new drop logic', () => {
+    let domSpy;
+    beforeEach(() => {
+      domSpy = spyElementPrototypes(HTMLElement, {
+        getBoundingClientRect: () => ({
+          width: 100,
+          height: 20,
+          top: 0,
+          left: 0,
+          bottom: 20,
+          right: 100,
+        }),
+        offsetWidth: {
+          get() {
+            return 24;
+          },
+        },
+      });
+    });
+
+    afterEach(() => {
+      domSpy.mockRestore();
+    });
+
+    (['ltr', 'rtl'] as const).forEach(dir => {
+      const base = dir === 'ltr' ? 1 : -1;
+
+      describe(dir, () => {
+        it('not allow cross level dnd on expanded nodes', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable onDrop={onDrop} direction={dir} expandedKeys={['0-0', '0-1', '0-1-0']}>
+              <TreeNode key="0-0">
+                <TreeNode key="0-0-0" className="dropTarget1">
+                  <TreeNode key="0-0-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-1">
+                <TreeNode key="0-1-0" className="dropTarget2">
+                  <TreeNode key="0-1-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-2" className="dragTarget" />
+            </Tree>,
+          );
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget1 > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget1 > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireEvent.drop(container.querySelector('.dropTarget1 > .rc-tree-node-content-wrapper'));
+
+          // insert after 0-0, since 0-0-0 is not expanded
+          expect(onDrop.mock.calls[0][0].node.key).toEqual('0-0');
+          expect(onDrop.mock.calls[0][0].dropPosition).toEqual(1);
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget2 > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget2 > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireEvent.drop(container.querySelector('.dropTarget2 > .rc-tree-node-content-wrapper'));
+
+          // insert into 0-1-0, since it is expanded, do not allow cross level dnd
+          expect(onDrop.mock.calls[1][0].node.key).toEqual('0-1-0');
+          expect(onDrop.mock.calls[1][0].dropPosition).toEqual(0);
+        });
+
+        it('allowDrop all nodes', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-1">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" className="dropTarget" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-2" />
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 400,
+              clientY: 500,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+
+          expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1-0-0');
+          // (in ltr) drag from right to left, should be insert after, so drop position is 1
+          expect(onDrop.mock.calls[0][0].dropPosition).toEqual(1);
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+
+          expect(onDrop.mock.calls[1][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[1][0].dropPosition).toEqual(1);
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[2][0].dropPosition).toEqual(0);
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[2][0].dropPosition).toEqual(0);
+        });
+
+        it('allowDrop no node', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree
+              draggable
+              defaultExpandAll
+              onDrop={onDrop}
+              allowDrop={() => false}
+              direction={dir}
+            >
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-1">
+                <TreeNode key="0-1-0" className="dropTargetParent">
+                  <TreeNode key="0-1-0-0" className="dropTarget" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-2" />
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTargetParent > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTargetParent > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(
+            container.querySelector('.dropTargetParent > .rc-tree-node-content-wrapper'),
+          );
+          // not allow any dropPosition except 0 on expanded node
+          expect(onDrop).not.toHaveBeenCalled();
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+        });
+
+        it('drop to top half of first node', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-1" className="dropTarget">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 0,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: -1000,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1');
+          expect(onDrop.mock.calls[0][0].dropPosition).toEqual(-1);
+        });
+
+        it('can drop on its direct parent', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-1" className="dropTarget">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTargetParent > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTargetParent > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireEvent.drop(
+            container.querySelector('.dragTargetParent > .rc-tree-node-content-wrapper'),
+          );
+          expect(onDrop).toHaveBeenCalled();
+        });
+
+        it('cover window dragend & componentWillUnmount', () => {
+          const { container, unmount } = render(
+            <Tree draggable defaultExpandAll direction={dir}>
+              <TreeNode key="0-1" className="dropTarget">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          window.dispatchEvent(new Event('dragend'));
+          unmount();
+        });
+
+        it('dragover first half of non-first child', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+                <TreeNode key="0-0-1" />
+                <TreeNode key="0-0-2" className="dropTarget" />
+              </TreeNode>
+            </Tree>,
+          );
+          // wrapper.find('.dragTarget > .rc-tree-node-content-wrapper').simulate('dragStart', {
+          //   clientX: base * 500,
+          //   clientY: 500,
+          // });
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          // wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragEnter', {
+          //   clientX: base * 500,
+          //   clientY: 1,
+          // });
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 500,
+              clientY: 1,
+            },
+          );
+          // wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('dragOver', {
+          //   clientX: base * 500,
+          //   clientY: 1,
+          // });
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 500,
+              clientY: 1,
+            },
+          );
+          // wrapper.find('.dropTarget > .rc-tree-node-content-wrapper').simulate('drop');
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[0][0].node.key).toEqual('0-0-1');
+          expect(onDrop.mock.calls[0][0].dropPosition).toEqual(2);
+        });
+
+        it('dragover self', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-1" className="dropTarget">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-0" className="dragTargetParent">
+                <TreeNode key="0-0-0" className="dragTarget">
+                  <TreeNode key="0-0-0-0" className="dragTargetChild" />
+                </TreeNode>
+              </TreeNode>
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 500,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+        });
+
+        it('not allowDrop on node which has children', () => {
+          const onDrop = jest.fn();
+          const allowDrop = ({ dropNode, dropPosition }) => {
+            if (!dropNode.children) {
+              if (dropPosition === 0) return false;
+            }
+            return true;
+          };
+          const { container } = render(
+            <Tree draggable defaultExpandAll allowDrop={allowDrop} onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-0" className="dragTarget">
+                <TreeNode key="0-0-0" className="dragTargetChild" />
+              </TreeNode>
+              <TreeNode key="0-1">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" className="dropTarget" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-2" />
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 400,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[0][0].node.key).toEqual('0-1-0-0');
+          // (in ltr) drag from right to left, should be insert after, so drop position is 1
+          expect(onDrop.mock.calls[0][0].dropPosition).toEqual(1);
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 500,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[1][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[1][0].dropPosition).toEqual(1);
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 550,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[2][0].dropPosition).toEqual(1);
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 500,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 600,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop.mock.calls[2][0].node.key).toEqual('0-1-0-0');
+          expect(onDrop.mock.calls[2][0].dropPosition).toEqual(1);
+        });
+
+        it('allowDrop should pass dragNode and dropNode', () => {
+          const onDrop = jest.fn();
+          const allowDrop = jest.fn();
+          const { container } = render(
+            <Tree draggable defaultExpandAll allowDrop={allowDrop} onDrop={onDrop} direction={dir}>
+              <TreeNode key="0-0" className="dragTarget">
+                <TreeNode key="0-0-0" className="dragTargetChild" />
+              </TreeNode>
+              <TreeNode key="0-1">
+                <TreeNode key="0-1-0">
+                  <TreeNode key="0-1-0-0" className="dropTarget" />
+                </TreeNode>
+              </TreeNode>
+              <TreeNode key="0-2" />
+            </Tree>,
+          );
+          fireDragEvent(
+            container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'),
+            'dragStart',
+            {
+              clientX: base * 400,
+              clientY: 500,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragEnter',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireDragEvent(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+            'dragOver',
+            {
+              clientX: base * 400,
+              clientY: 600,
+            },
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(allowDrop.mock.calls[0][0].dragNode.key).toEqual('0-0');
+          expect(allowDrop.mock.calls[0][0].dropNode.key).toEqual('0-1-0-0');
+        });
+
+        it('not allow dragging elements outside into tree', () => {
+          const onDrop = jest.fn();
+          const { container } = render(
+            <div>
+              <Tree draggable defaultExpandAll onDrop={onDrop} direction={dir}>
+                <TreeNode key="0-0">
+                  <TreeNode key="0-0-0" className="dropTarget" />
+                </TreeNode>
+              </Tree>
+              <div className="dragTarget">Element outside</div>
+            </div>,
+          );
+          fireEvent.dragStart(container.querySelector('.dragTarget'));
+          fireEvent.dragEnter(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+          );
+          fireEvent.dragOver(
+            container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'),
+          );
+          fireEvent.drop(container.querySelector('.dropTarget > .rc-tree-node-content-wrapper'));
+          expect(onDrop).not.toHaveBeenCalled();
+        });
+      });
+    });
+  });
+
+  it('render handler', () => {
+    const { container } = render(
+      <Tree
+        draggable={{
+          icon: <span className="handler" />,
+        }}
+        defaultExpandAll
+        treeData={[
+          {
+            title: 'Parent',
+            key: 'parent',
+            children: [
+              {
+                title: 'Child',
+                key: 'child',
+              },
+            ],
+          },
+        ]}
+      />,
+    );
+
+    expect(container.querySelectorAll('.handler')).toHaveLength(2);
+  });
+
+  it('not break with fieldNames', () => {
+    const onDrop = jest.fn();
+
+    const { container } = render(
+      <Tree<FieldDataNode<{ name: string }, 'childList'>>
+        draggable
+        defaultExpandAll
+        onDrop={onDrop}
+        treeData={[
+          {
+            name: 'parent',
+            childList: [
+              {
+                className: 'dragTarget',
+                name: 'child 1',
+              },
+              {
+                className: 'dropTarget',
+                name: 'child 2',
+              },
+            ],
+          },
+        ]}
+        fieldNames={{
+          children: 'childList',
+          title: 'name',
+          key: 'name',
+        }}
+      />,
+    );
+
+    fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    fireDragEvent(container.querySelector('.dropTarget'), 'dragEnter', {
+      clientY: -9999999,
+    });
+    fireEvent.dragOver(container.querySelector('.dropTarget'));
+    fireEvent.drop(container.querySelector('.dropTarget'));
+
+    expect(onDrop).toHaveBeenCalled();
+  });
+
+  it('not break of other drop node', () => {
+    const onDragStart = jest.fn();
+    const onDragEnd = jest.fn();
+    const { container } = render(
+      <div>
+        {createTree({ onDragStart, onDragEnd })}
+        <div className="test" />
+      </div>,
+    );
+    const treeNode = container.querySelector('.dragTarget > .rc-tree-node-content-wrapper');
+    fireEvent.dragStart(treeNode);
+    expect(onDragStart).toHaveBeenCalled();
+
+    fireEvent.dragEnd(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+    expect(onDragEnd).toHaveBeenCalled();
+
+    // Should not break
+    fireEvent.dragEnd(container.querySelector('.test'));
+  });
+
+  it('disabled item should have draggable className', () => {
+    const { container } = render(
+      <Tree draggable defaultExpandAll>
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf" key="0-0-0" disabled />
+          <TreeNode title="leaf" key="0-0-1" />
+        </TreeNode>
+      </Tree>,
+    );
+    expect(container.querySelectorAll('.rc-tree-treenode-draggable').length).toBe(3);
+  });
+
+  it('not conflict with defaultExpandAll', () => {
+    const treeData = [
+      {
+        key: 1,
+        title: 'p1',
+        children: [
+          {
+            key: 2,
+            title: 'p2',
+            children: [],
+            className: 'dropTarget',
+          },
+        ],
+      },
+      {
+        key: 3,
+        title: 'p3',
+        children: [],
+      },
+      {
+        key: 4,
+        title: 'p4',
+        children: [],
+        className: 'dragTarget',
+      },
+    ];
+
+    const { container } = render(createTree({ defaultExpandAll: true, treeData }));
+    fireEvent.dragStart(container.querySelector('.dragTarget > .rc-tree-node-content-wrapper'));
+
+    // Drag over
+    fireDragEvent(container.querySelector('.dropTarget'), 'dragOver', {
+      clientX: -9999999,
+    });
+
+    expect(container.querySelector('.drop-target').textContent).toEqual('p1');
+  });
+});
diff --git a/tests/TreeMotion.spec.js b/tests/TreeMotion.spec.js
deleted file mode 100644
index 52b8eae59..000000000
--- a/tests/TreeMotion.spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import { act } from 'react-dom/test-utils';
-import VirtualList from 'rc-virtual-list';
-import Tree from '../src';
-import { getMinimumRangeTransitionRange } from '../src/NodeList';
-
-describe('Tree Motion', () => {
-  it('hide item', done => {
-    const wrapper = mount(
-      <Tree
-        treeData={[{ key: '0-0', children: [{ key: '0-0-0' }] }]}
-        expandedKeys={['0-0']}
-        motion={{}}
-      />,
-    );
-    wrapper.setProps({ expandedKeys: [] });
-
-    setTimeout(() => {
-      wrapper.update();
-      // Confirm CSSMotion is prepare to hide
-      expect(wrapper.find('CSSMotion').props().visible).toBeFalsy();
-      done();
-    }, 100);
-  });
-
-  /**
-   * Mock show item
-   */
-  it('`onSkipRender` should clean up', () => {
-    const wrapper = mount(
-      <Tree
-        treeData={[{ key: '0-0', children: [{ key: '0-0-0' }] }]}
-        expandedKeys={[]}
-        motion={{}}
-      />,
-    );
-
-    wrapper.setProps({ expandedKeys: ['0-0'] });
-    act(() => {
-      wrapper
-        .find(VirtualList)
-        .props()
-        .onSkipRender();
-    });
-
-    const { data } = wrapper.find(VirtualList).props();
-    expect(data.length).toBe(1);
-    expect(data[0].key).toBe('0-0');
-  });
-
-  it('getMinimumRangeTransitionRange', () => {
-    const visibleList = getMinimumRangeTransitionRange(
-      new Array(100).fill(null).map((_, index) => index),
-      100,
-      20,
-    );
-
-    expect(visibleList.length < 10).toBeTruthy();
-  });
-});
diff --git a/tests/TreeMotion.spec.tsx b/tests/TreeMotion.spec.tsx
new file mode 100644
index 000000000..7cbcb91bc
--- /dev/null
+++ b/tests/TreeMotion.spec.tsx
@@ -0,0 +1,328 @@
+import { act, fireEvent, render } from '@testing-library/react';
+import React from 'react';
+import Tree, { FieldDataNode, TreeNode } from '../src';
+import { TreeContext } from '../src/contextTypes';
+import MotionTreeNode from '../src/MotionTreeNode';
+import { getMinimumRangeTransitionRange } from '../src/NodeList';
+
+jest.mock('@rc-component/motion/lib/util/motion', () => {
+  const origin = jest.requireActual('@rc-component/motion/lib/util/motion');
+
+  return {
+    ...origin,
+    supportTransition: () => true,
+  };
+});
+
+describe('Tree Motion', () => {
+  beforeEach(() => {
+    jest.useFakeTimers();
+  });
+
+  afterEach(() => {
+    jest.clearAllTimers();
+    jest.useRealTimers();
+  });
+
+  it('basic', () => {
+    const motion = {
+      motionName: 'bamboo',
+    };
+    const { container } = render(
+      <Tree motion={motion} height={10000}>
+        <TreeNode key="0-0">
+          <TreeNode key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+
+    expect(container.querySelector('.bamboo-appear')).toBeTruthy();
+  });
+
+  it('hide item', () => {
+    const renderTree = (props?: any) => (
+      <Tree
+        treeData={[{ key: '0-0', children: [{ key: '0-0-0' }] }]}
+        expandedKeys={['0-0']}
+        motion={{}}
+        {...props}
+      />
+    );
+
+    const { container, rerender } = render(renderTree());
+
+    rerender(renderTree({ expandedKeys: [] }));
+
+    expect(container.querySelector('.bamboo-appear')).toBeFalsy();
+  });
+
+  it('getMinimumRangeTransitionRange', () => {
+    const visibleList = getMinimumRangeTransitionRange(
+      new Array(100).fill(null).map((_, index) => index) as any,
+      true,
+      100,
+      20,
+    );
+
+    expect(visibleList.length < 10).toBeTruthy();
+  });
+
+  it('not crash', () => {
+    const renderTree = (props?: any) => (
+      <Tree
+        treeData={[{ key: '0-0', children: [{ key: '0-0-0' }] }]}
+        expandedKeys={['0-0']}
+        motion={{}}
+        {...props}
+      />
+    );
+    const { rerender } = render(renderTree());
+
+    rerender(renderTree({ treeData: [] }));
+  });
+
+  it('should not expanded when in motion', () => {
+    // const raf = jest
+    //   .spyOn(window, 'requestAnimationFrame')
+    //   .mockImplementation(fn => window.setTimeout(fn, 16));
+
+    const onExpand = jest.fn();
+    const { container } = render(
+      <Tree
+        onExpand={onExpand}
+        motion={{
+          motionName: 'bamboo',
+          motionDeadline: 1000,
+          motionAppear: true,
+        }}
+      >
+        <TreeNode key="0-0">
+          <TreeNode key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    function doExpand() {
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    }
+
+    // First click should work
+    doExpand();
+    expect(onExpand).toHaveBeenCalled();
+    onExpand.mockReset();
+
+    // Not trigger when in motion
+    doExpand();
+    expect(onExpand).not.toHaveBeenCalled();
+  });
+
+  describe('MotionTreeNode should always trigger motion end', () => {
+    it('with motionNodes', () => {
+      const onMotionStart = jest.fn();
+      const onMotionEnd = jest.fn();
+      const { unmount } = render(
+        <React.StrictMode>
+          <TreeContext.Provider value={{ prefixCls: 'test' } as any}>
+            <MotionTreeNode
+              motionNodes={[]}
+              onMotionStart={onMotionStart}
+              onMotionEnd={onMotionEnd}
+              motion={{
+                motionName: 'bamboo',
+              }}
+              motionType="hide"
+              {...({} as any)} // Ignore TS warning
+            />
+          </TreeContext.Provider>
+        </React.StrictMode>,
+      );
+
+      expect(onMotionStart).toHaveBeenCalled();
+      expect(onMotionEnd).not.toHaveBeenCalled();
+
+      unmount();
+      act(() => {
+        jest.runAllTimers();
+      });
+      expect(onMotionEnd).toHaveBeenCalled();
+    });
+
+    it('without motionNodes', () => {
+      const onMotionStart = jest.fn();
+      const onMotionEnd = jest.fn();
+      const { unmount } = render(
+        <TreeContext.Provider
+          value={
+            {
+              prefixCls: 'test',
+              keyEntities: {},
+              dropIndicatorRender: () => null,
+            } as any
+          }
+        >
+          <MotionTreeNode
+            onMotionStart={onMotionStart}
+            onMotionEnd={onMotionEnd}
+            isEnd={[false]}
+            {...({} as any)} // Ignore TS warning
+          />
+        </TreeContext.Provider>,
+      );
+
+      expect(onMotionStart).not.toHaveBeenCalled();
+      expect(onMotionEnd).not.toHaveBeenCalled();
+
+      unmount();
+      act(() => {
+        jest.runAllTimers();
+      });
+      expect(onMotionStart).not.toHaveBeenCalled();
+      expect(onMotionEnd).not.toHaveBeenCalled();
+    });
+  });
+
+  it('motion should work well with fieldNames', () => {
+    const Demo = () => (
+      <Tree<FieldDataNode<{ id: string; name: string }, 'sub'>>
+        defaultExpandAll
+        fieldNames={{
+          title: 'name',
+          key: 'id',
+          children: 'sub',
+        }}
+        motion={{
+          motionName: 'bamboo',
+        }}
+        treeData={[
+          {
+            id: '1',
+            name: 'A',
+            sub: [
+              {
+                id: '2',
+                name: 'B',
+                sub: [],
+              },
+            ],
+          },
+        ]}
+      />
+    );
+
+    const { container } = render(<Demo />);
+    expect(container.querySelector('[title="B"]')).toBeTruthy();
+
+    fireEvent.click(container.querySelector('.rc-tree-switcher'));
+    act(() => {
+      jest.runAllTimers();
+    });
+    fireEvent.animationEnd(container.querySelector('.bamboo-leave-active'));
+    expect(container.querySelector('[title="B"]')).toBeFalsy();
+  });
+
+  it('motion should not revert flatten list', () => {
+    const renderTree = (props?: any) => (
+      <Tree
+        motion={{
+          motionName: 'little',
+        }}
+        height={100}
+        itemHeight={10}
+        treeData={[
+          {
+            key: 'parent',
+            title: 'parent',
+            children: [
+              {
+                key: 'child',
+                title: 'child',
+              },
+            ],
+          },
+        ]}
+        {...props}
+      />
+    );
+
+    const { container, rerender } = render(renderTree());
+
+    rerender(
+      renderTree({
+        expandedKeys: ['parent'],
+      }),
+    );
+
+    for (let i = 0; i < 10; i += 1) {
+      act(() => {
+        jest.runAllTimers();
+      });
+    }
+
+    rerender(
+      renderTree({
+        treeData: [
+          {
+            key: 'parent',
+            title: 'parent2',
+            children: [
+              {
+                key: 'child',
+                title: 'child2',
+              },
+            ],
+          },
+        ],
+      }),
+    );
+
+    expect(container.querySelectorAll('span.rc-tree-title')[0].textContent).toEqual('parent2');
+    expect(container.querySelectorAll('span.rc-tree-title')[1].textContent).toEqual('child2');
+  });
+
+  // https://github.com/ant-design/ant-design/issues/43282
+  it('dynamic modify data should stop motion', () => {
+    const motion = {
+      motionName: 'bamboo',
+    };
+
+    const getTreeData = () => [
+      {
+        title: 'parent',
+        key: 'parent',
+        children: [
+          {
+            title: 'child',
+            key: 'child',
+          },
+        ],
+      },
+    ];
+
+    const oriData = getTreeData();
+
+    const { container, rerender } = render(<Tree motion={motion} treeData={oriData} />);
+    rerender(<Tree motion={motion} treeData={oriData} expandedKeys={['parent']} />);
+
+    // Replace `treeData` before motion end
+    const onExpand = jest.fn();
+    rerender(
+      <Tree
+        onExpand={onExpand}
+        motion={motion}
+        treeData={getTreeData()}
+        expandedKeys={['parent']}
+      />,
+    );
+
+    // Delay for clean up
+    act(() => {
+      jest.advanceTimersByTime(100);
+    });
+
+    // Click should trigger event
+    fireEvent.click(container.querySelector('.rc-tree-switcher_open'));
+    expect(onExpand).toHaveBeenCalled();
+  });
+});
diff --git a/tests/TreeNodeProps.spec.js b/tests/TreeNodeProps.spec.tsx
similarity index 53%
rename from tests/TreeNodeProps.spec.js
rename to tests/TreeNodeProps.spec.tsx
index c9ba542dc..0ecac5b21 100644
--- a/tests/TreeNodeProps.spec.js
+++ b/tests/TreeNodeProps.spec.tsx
@@ -1,7 +1,6 @@
 /* eslint-disable no-undef */
 import React from 'react';
-import { render, mount } from 'enzyme';
-import { renderToJson } from 'enzyme-to-json';
+import { render, fireEvent } from '@testing-library/react';
 import Tree, { TreeNode } from '../src';
 import { spyConsole } from './util';
 
@@ -16,7 +15,7 @@ describe('TreeNode Props', () => {
   // expanded - is defined by Tree, TreeNode can not change it
 
   it('className', () => {
-    const wrapper = render(
+    const { container } = render(
       <Tree defaultExpandAll>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" className="tree-node-cls">
@@ -27,7 +26,7 @@ describe('TreeNode Props', () => {
         <TreeNode key="0-1" />
       </Tree>,
     );
-    expect(renderToJson(wrapper)).toMatchSnapshot();
+    expect(container.firstChild).toMatchSnapshot();
   });
 
   // disabled - is already full test in Tree.spec.js
@@ -42,7 +41,7 @@ describe('TreeNode Props', () => {
         </TreeNode>
       </Tree>,
     );
-    expect(renderToJson(withoutLoadData)).toMatchSnapshot();
+    expect(withoutLoadData.container.firstChild).toMatchSnapshot();
 
     const withLoadData = render(
       <Tree defaultExpandAll>
@@ -51,7 +50,7 @@ describe('TreeNode Props', () => {
         </TreeNode>
       </Tree>,
     );
-    expect(renderToJson(withLoadData)).toMatchSnapshot();
+    expect(withLoadData.container.firstChild).toMatchSnapshot();
 
     const forceNoLeaf = render(
       <Tree defaultExpandAll>
@@ -60,16 +59,16 @@ describe('TreeNode Props', () => {
         </TreeNode>
       </Tree>,
     );
-    expect(renderToJson(forceNoLeaf)).toMatchSnapshot();
+    expect(forceNoLeaf.container.firstChild).toMatchSnapshot();
   });
 
   it('title function', () => {
-    const wrapper = render(
+    const { container } = render(
       <Tree defaultExpandAll>
         <TreeNode title={() => <a id="test-title">title</a>} />
       </Tree>,
     );
-    expect(wrapper.find('#test-title').text()).toEqual('title');
+    expect(container.querySelector('#test-title').textContent).toEqual('title');
   });
 
   describe('customize icon', () => {
@@ -79,7 +78,7 @@ describe('TreeNode Props', () => {
           <TreeNode icon={<span className="cust-icon" />} />
         </Tree>,
       );
-      expect(renderToJson(withoutLoadData)).toMatchSnapshot();
+      expect(withoutLoadData.container.firstChild).toMatchSnapshot();
     });
 
     it('component', () => {
@@ -90,7 +89,7 @@ describe('TreeNode Props', () => {
           <TreeNode icon={Icon} />
         </Tree>,
       );
-      expect(renderToJson(withoutLoadData)).toMatchSnapshot();
+      expect(withoutLoadData.container.firstChild).toMatchSnapshot();
     });
 
     it('hide icon', () => {
@@ -99,23 +98,23 @@ describe('TreeNode Props', () => {
           <TreeNode icon={<span className="cust-icon" />} />
         </Tree>,
       );
-      expect(renderToJson(withoutLoadData)).toMatchSnapshot();
+      expect(withoutLoadData.container.firstChild).toMatchSnapshot();
     });
 
     it('get props when loading', () => {
       const then = jest.fn(() => Promise.resolve());
-      const loadData = jest.fn(() => ({ then }));
-      const iconFn = jest.fn(() => null);
-      const wrapper = mount(
+      const loadData: any = jest.fn(() => ({ then }));
+      const iconFn: any = jest.fn(() => null);
+      const { container } = render(
         <Tree loadData={loadData}>
           <TreeNode icon={iconFn} title="parent 1" key="0-0" />
         </Tree>,
       );
 
-      wrapper.find('.rc-tree-switcher').simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
 
       expect(iconFn.mock.calls[0][0].loading).toBe(false);
-      expect(iconFn.mock.calls[iconFn.mock.calls.length - 1][0].loading).toBe(true);
+      expect(iconFn.mock.calls[iconFn.mock.calls.length - 1][0].loading).toBeTruthy();
     });
   });
 
@@ -132,7 +131,7 @@ describe('TreeNode Props', () => {
           <TreeNode key="0-1" data-test="0-1" />
         </Tree>,
       );
-      expect(renderToJson(wrapper)).toMatchSnapshot();
+      expect(wrapper.container.firstChild).toMatchSnapshot();
     });
 
     it('renders aria attributes on li', () => {
@@ -147,7 +146,7 @@ describe('TreeNode Props', () => {
           <TreeNode key="0-1" aria-label="0-1" />
         </Tree>,
       );
-      expect(renderToJson(wrapper)).toMatchSnapshot();
+      expect(wrapper.container.firstChild).toMatchSnapshot();
     });
   });
 
@@ -155,15 +154,89 @@ describe('TreeNode Props', () => {
     const onClick = jest.fn();
     const onSelect = jest.fn();
 
-    const wrapper = mount(
+    const { container } = render(
       <Tree selectable onClick={onClick} onSelect={onSelect}>
         <TreeNode selectable={false} />
       </Tree>,
     );
 
-    wrapper.find('.rc-tree-node-content-wrapper').simulate('click');
+    fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
 
     expect(onClick).toHaveBeenCalled();
     expect(onSelect).not.toHaveBeenCalled();
   });
+
+  it('unselectable', () => {
+    const onClick = jest.fn();
+    const onSelect = jest.fn();
+
+    class Demo extends React.Component {
+      state = {
+        treeSelectable: false,
+        treeNodeSelectable: false,
+      };
+
+      render() {
+        return (
+          <div>
+            <Tree selectable={this.state.treeSelectable} onClick={onClick} onSelect={onSelect}>
+              <TreeNode />
+              <TreeNode selectable={this.state.treeNodeSelectable} />
+            </Tree>
+            <button
+              className="test-button"
+              onClick={() => {
+                this.setState({
+                  treeSelectable: true,
+                  treeNodeSelectable: false,
+                });
+              }}
+            />
+          </div>
+        );
+      }
+    }
+
+    const { container } = render(<Demo />);
+    // tree selectable is false ,then children should be selectable = false if not set selectable alone.
+    expect(container.querySelectorAll('[aria-selected=false]')).toHaveLength(1);
+
+    fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+
+    // only set tree node use state.
+    fireEvent.click(container.querySelector('.test-button'));
+    onClick.mockRestore();
+    onSelect.mockRestore();
+    expect(container.querySelectorAll('[aria-selected=false]')).toHaveLength(1);
+    fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+  });
+
+  it('elements within the title should be able to respond to clicks', () => {
+    const onClick = jest.fn();
+
+    const Demo: React.FC<{ filed: string; checkable: boolean }> = ({ filed, checkable }) => (
+      <Tree>
+        <TreeNode
+          checkable={checkable}
+          title={
+            <label className={filed}>
+              <input type="radio" onClick={onClick} />
+            </label>
+          }
+        />
+      </Tree>
+    );
+
+    render(<Demo filed="test-check" checkable={true} />);
+    render(<Demo filed="test-click" checkable={false} />);
+    
+    fireEvent.click(document.querySelector('.test-check'));
+    fireEvent.click(document.querySelector('.test-click'));
+
+    expect(onClick).toHaveBeenCalledTimes(2);
+  });
 });
diff --git a/tests/TreeProps.spec.js b/tests/TreeProps.spec.tsx
similarity index 57%
rename from tests/TreeProps.spec.js
rename to tests/TreeProps.spec.tsx
index 68e1f4eca..1e02cb4f2 100644
--- a/tests/TreeProps.spec.js
+++ b/tests/TreeProps.spec.tsx
@@ -1,10 +1,8 @@
 /* eslint-disable no-undef, react/no-multi-comp */
+import { act, fireEvent, render } from '@testing-library/react';
+import { resetWarned } from '@rc-component/util/lib/warning';
 import React from 'react';
-import { render, mount } from 'enzyme';
-import { renderToJson } from 'enzyme-to-json';
-import PropTypes from 'prop-types';
-import Tree, { TreeNode } from '../src';
-import { InternalTreeNode } from '../src/TreeNode';
+import Tree, { FieldDataNode, TreeNode } from '../src';
 import { objectMatcher, spyConsole, spyError } from './util';
 
 /**
@@ -24,16 +22,16 @@ describe('Tree Props', () => {
   // prefixCls
   it('prefixCls', () => {
     const withoutPrefix = render(<Tree />);
-    expect(renderToJson(withoutPrefix)).toMatchSnapshot();
+    expect(withoutPrefix.container.firstChild).toMatchSnapshot();
 
     const withPrefix = render(<Tree prefixCls="test-prefix" />);
-    expect(renderToJson(withPrefix)).toMatchSnapshot();
+    expect(withPrefix.container.firstChild).toMatchSnapshot();
   });
 
   // showLine
   it('showLine', () => {
     const wrapper = render(<Tree showLine />);
-    expect(renderToJson(wrapper)).toMatchSnapshot();
+    expect(wrapper.container.firstChild).toMatchSnapshot();
   });
 
   // showIcon
@@ -49,7 +47,7 @@ describe('Tree Props', () => {
         <TreeNode />
       </Tree>,
     );
-    expect(renderToJson(withIcon)).toMatchSnapshot();
+    expect(withIcon.container.firstChild).toMatchSnapshot();
 
     const withoutIcon = render(
       <Tree showIcon={false}>
@@ -62,7 +60,7 @@ describe('Tree Props', () => {
         <TreeNode />
       </Tree>,
     );
-    expect(renderToJson(withoutIcon)).toMatchSnapshot();
+    expect(withoutIcon.container.firstChild).toMatchSnapshot();
 
     const withOpenIcon = render(
       <Tree defaultExpandedKeys={['0-0']}>
@@ -75,7 +73,7 @@ describe('Tree Props', () => {
         <TreeNode />
       </Tree>,
     );
-    expect(renderToJson(withOpenIcon)).toMatchSnapshot();
+    expect(withOpenIcon.container.firstChild).toMatchSnapshot();
   });
 
   describe('selectable', () => {
@@ -84,21 +82,17 @@ describe('Tree Props', () => {
       const handleOnSelect = jest.fn();
       const handleOnCheck = jest.fn();
 
-      const withoutSelectableBase = (
+      const { container } = render(
         <Tree onSelect={handleOnSelect} defaultExpandedKeys={['0-0']} selectable={false}>
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" />
           </TreeNode>
-        </Tree>
+        </Tree>,
       );
 
-      expect(renderToJson(render(withoutSelectableBase))).toMatchSnapshot();
-
-      const withoutSelectable = mount(withoutSelectableBase);
-      const parentNode = withoutSelectable.find(InternalTreeNode).first();
-      const targetNode = parentNode.find(InternalTreeNode).last();
+      expect(container.firstChild).toMatchSnapshot();
 
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-node-content-wrapper'));
 
       expect(handleOnSelect).not.toHaveBeenCalled();
       expect(handleOnCheck).not.toHaveBeenCalled(); // Will test in checkable
@@ -108,22 +102,23 @@ describe('Tree Props', () => {
     it('with selectable', () => {
       const handleOnSelect = jest.fn();
 
-      const withSelectableBase = (
+      const { container } = render(
         <Tree onSelect={handleOnSelect} defaultExpandedKeys={['0-0']}>
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" />
           </TreeNode>
-        </Tree>
+        </Tree>,
       );
 
-      expect(renderToJson(render(withSelectableBase))).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
 
-      const withSelectable = mount(withSelectableBase);
-      const parentNode = withSelectable.find(InternalTreeNode).first();
-      const targetNode = withSelectable.find(InternalTreeNode).last();
+      const getNodes = () =>
+        container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode');
+      const getTargetNode = () => getNodes()[getNodes().length - 1];
+      const getParentNode = () => getNodes()[0];
 
       // Select leaf
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
 
       // traverseTreeNodes loops origin TreeNode and
       // onSelect trigger on `cloneElement` which is not the same instance
@@ -132,7 +127,7 @@ describe('Tree Props', () => {
         objectMatcher({
           event: 'select',
           selected: true,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           selectedNodes: [{ key: '0-0-0' }],
           nativeEvent: {},
         }),
@@ -140,13 +135,13 @@ describe('Tree Props', () => {
       handleOnSelect.mockReset();
 
       // un-select leaf
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
       expect(handleOnSelect).toHaveBeenCalledWith(
         [],
         objectMatcher({
           event: 'select',
           selected: false,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           selectedNodes: [],
           nativeEvent: {},
         }),
@@ -154,29 +149,26 @@ describe('Tree Props', () => {
       handleOnSelect.mockReset();
 
       // Select leaf and then parent
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
       expect(handleOnSelect).toHaveBeenCalledWith(
         ['0-0-0'],
         objectMatcher({
           event: 'select',
           selected: true,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           selectedNodes: [{ key: '0-0-0' }],
           nativeEvent: {},
         }),
       );
       handleOnSelect.mockReset();
 
-      parentNode
-        .find('.rc-tree-node-content-wrapper')
-        .first()
-        .simulate('click');
+      fireEvent.click(getParentNode().querySelector('.rc-tree-node-content-wrapper'));
       expect(handleOnSelect).toHaveBeenCalledWith(
         ['0-0'],
         objectMatcher({
           event: 'select',
           selected: true,
-          node: parentNode.instance(),
+          node: { key: '0-0' },
           selectedNodes: [{ key: '0-0' }],
           nativeEvent: {},
         }),
@@ -188,28 +180,29 @@ describe('Tree Props', () => {
   it('multiple', () => {
     const handleOnSelect = jest.fn();
 
-    const multipleBase = (
+    const { container } = render(
       <Tree onSelect={handleOnSelect} defaultExpandAll multiple>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" />
         </TreeNode>
-      </Tree>
+      </Tree>,
     );
 
-    expect(renderToJson(render(multipleBase))).toMatchSnapshot();
+    expect(container.firstChild).toMatchSnapshot();
 
-    const wrapper = mount(multipleBase);
-    const parentNode = wrapper.find(InternalTreeNode).first();
-    const targetNode = wrapper.find(InternalTreeNode).last();
+    const getNodes = () =>
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode');
+    const getTargetNode = () => getNodes()[getNodes().length - 1];
+    const getParentNode = () => getNodes()[0];
 
     // Leaf select
-    targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+    fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
     expect(handleOnSelect).toHaveBeenCalledWith(
       ['0-0-0'],
       objectMatcher({
         event: 'select',
         selected: true,
-        node: targetNode.instance(),
+        node: { key: '0-0-0' },
         selectedNodes: [{ key: '0-0-0' }],
         nativeEvent: {},
       }),
@@ -217,16 +210,13 @@ describe('Tree Props', () => {
     handleOnSelect.mockReset();
 
     // Parent select
-    parentNode
-      .find('.rc-tree-node-content-wrapper')
-      .first()
-      .simulate('click');
+    fireEvent.click(getParentNode().querySelector('.rc-tree-node-content-wrapper'));
     expect(handleOnSelect).toHaveBeenCalledWith(
       ['0-0-0', '0-0'],
       objectMatcher({
         event: 'select',
         selected: true,
-        node: parentNode.instance(),
+        node: { key: '0-0' },
         selectedNodes: [{ key: '0-0-0' }, { key: '0-0' }],
         nativeEvent: {},
       }),
@@ -234,13 +224,13 @@ describe('Tree Props', () => {
     handleOnSelect.mockReset();
 
     // Leaf un-select
-    targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+    fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
     expect(handleOnSelect).toHaveBeenCalledWith(
       ['0-0'],
       objectMatcher({
         event: 'select',
         selected: false,
-        node: targetNode.instance(),
+        node: { key: '0-0-0' },
         selectedNodes: [{ key: '0-0' }],
         nativeEvent: {},
       }),
@@ -253,7 +243,7 @@ describe('Tree Props', () => {
       const handleOnSelect = jest.fn();
       const handleOnCheck = jest.fn();
 
-      const withCheckableBase = (
+      const { container } = render(
         <Tree
           onSelect={handleOnSelect}
           onCheck={handleOnCheck}
@@ -263,22 +253,23 @@ describe('Tree Props', () => {
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" />
           </TreeNode>
-        </Tree>
+        </Tree>,
       );
 
-      expect(renderToJson(render(withCheckableBase))).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
 
-      const withCheckable = mount(withCheckableBase);
-      const targetNode = withCheckable.find(InternalTreeNode).last();
+      const getNodes = () =>
+        container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode');
+      const getTargetNode = () => getNodes()[getNodes().length - 1];
 
       // Click leaf
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
       expect(handleOnSelect).toHaveBeenCalledWith(
         ['0-0-0'],
         objectMatcher({
           event: 'select',
           selected: true,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           selectedNodes: [{ key: '0-0-0' }],
           nativeEvent: {},
         }),
@@ -290,14 +281,14 @@ describe('Tree Props', () => {
       handleOnSelect.mockReset();
 
       // Click checkbox
-      targetNode.find('.rc-tree-checkbox').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-checkbox'));
 
       expect(handleOnCheck).toHaveBeenCalledWith(
         ['0-0-0', '0-0'],
         objectMatcher({
           event: 'check',
           checked: true,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           checkedNodes: [{ key: '0-0-0' }, { key: '0-0' }],
           nativeEvent: {},
         }),
@@ -309,7 +300,7 @@ describe('Tree Props', () => {
       const handleOnSelect = jest.fn();
       const handleOnCheck = jest.fn();
 
-      const withCheckableBase = (
+      const { container } = render(
         <Tree
           onSelect={handleOnSelect}
           onCheck={handleOnCheck}
@@ -320,22 +311,23 @@ describe('Tree Props', () => {
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" />
           </TreeNode>
-        </Tree>
+        </Tree>,
       );
 
-      expect(renderToJson(render(withCheckableBase))).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
 
-      const withCheckable = mount(withCheckableBase);
-      const targetNode = withCheckable.find(InternalTreeNode).last();
+      const getNodes = () =>
+        container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode');
+      const getTargetNode = () => getNodes()[getNodes().length - 1];
 
       // Click leaf
-      targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+      fireEvent.click(getTargetNode().querySelector('.rc-tree-node-content-wrapper'));
       expect(handleOnCheck).toHaveBeenCalledWith(
         ['0-0-0', '0-0'],
         objectMatcher({
           event: 'check',
           checked: true,
-          node: targetNode.instance(),
+          node: { key: '0-0-0' },
           checkedNodes: [{ key: '0-0-0' }, { key: '0-0' }],
           nativeEvent: {},
         }),
@@ -344,7 +336,7 @@ describe('Tree Props', () => {
     });
 
     it('node set checkable to `false`', () => {
-      const wrapper = mount(
+      const { container } = render(
         <Tree checkable defaultExpandAll>
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" checkable={false} />
@@ -352,18 +344,7 @@ describe('Tree Props', () => {
         </Tree>,
       );
 
-      expect(
-        wrapper
-          .find('TreeNode')
-          .at(0)
-          .find('.rc-tree-checkbox').length,
-      ).toBeTruthy();
-      expect(
-        wrapper
-          .find('TreeNode')
-          .at(1)
-          .find('.rc-tree-checkbox').length,
-      ).toBeFalsy();
+      expect(container.querySelectorAll('.rc-tree-checkbox')).toHaveLength(1);
     });
   });
 
@@ -372,7 +353,7 @@ describe('Tree Props', () => {
     const errorSpy = spyError();
 
     const genWrapper = checkedKeys =>
-      mount(
+      render(
         <Tree checkedKeys={checkedKeys} defaultExpandAll checkable>
           <TreeNode key="0-0">
             <TreeNode key="0-0-0" />
@@ -381,19 +362,19 @@ describe('Tree Props', () => {
       );
 
     it('null', () => {
-      const wrapper = genWrapper(null);
+      const { container } = genWrapper(null);
       expect(errorSpy()).not.toHaveBeenCalledWith(
         'Warning: `checkedKeys` is not an array or an object',
       );
-      expect(wrapper.render()).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
 
     it('number', () => {
-      const wrapper = genWrapper(123);
+      const { container } = genWrapper(123);
       expect(errorSpy()).toHaveBeenCalledWith(
         'Warning: `checkedKeys` is not an array or an object',
       );
-      expect(wrapper.render()).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
   });
 
@@ -401,7 +382,7 @@ describe('Tree Props', () => {
   it('checkStrictly', () => {
     const handleOnCheck = jest.fn();
 
-    const wrapper = mount(
+    const { container } = render(
       <Tree onCheck={handleOnCheck} defaultExpandedKeys={['0-0']} checkable checkStrictly>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" />
@@ -409,12 +390,14 @@ describe('Tree Props', () => {
       </Tree>,
     );
 
-    expect(wrapper.render()).toMatchSnapshot();
+    expect(container.firstChild).toMatchSnapshot();
 
-    const targetNode = wrapper.find(InternalTreeNode).last();
+    const getNodes = () =>
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode');
+    const getTargetNode = () => getNodes()[getNodes().length - 1];
 
     // Click Leaf
-    targetNode.find('.rc-tree-checkbox').simulate('click');
+    fireEvent.click(getTargetNode().querySelector('.rc-tree-checkbox'));
     expect(handleOnCheck).toHaveBeenCalledWith(
       {
         checked: ['0-0-0'],
@@ -423,7 +406,9 @@ describe('Tree Props', () => {
       objectMatcher({
         event: 'check',
         checked: true,
-        node: targetNode.instance(),
+        node: {
+          key: '0-0-0',
+        },
         checkedNodes: [{ key: '0-0-0' }],
         nativeEvent: {},
       }),
@@ -433,24 +418,25 @@ describe('Tree Props', () => {
   // draggable - is already full test in Tree.spec.js
   // autoExpandParent - is already full test in Tree.spec.js
 
-  // defaultExpandAll
   it('defaultExpandAll', () => {
-    const wrapper = render(
+    const { container } = render(
       <Tree defaultExpandAll>
-        <TreeNode key="0-0">
-          <TreeNode key="0-0-0" />
+        <TreeNode key="0-0" title="parent">
+          <TreeNode key="0-0-0" title="child" />
         </TreeNode>
       </Tree>,
     );
 
-    expect(renderToJson(wrapper)).toMatchSnapshot();
+    expect(container.firstChild).toMatchSnapshot();
   });
 
   // defaultCheckedKeys - is already full test in Tree.spec.js
   // defaultSelectedKeys - is already full test in Tree.spec.js
 
   describe('loadData', () => {
-    it('basic', () => {
+    const errorSpy = spyError();
+
+    it('basic', async () => {
       let called = 0;
 
       const handleLoadData = jest.fn();
@@ -460,7 +446,7 @@ describe('Tree Props', () => {
           loaded: false,
         };
 
-        loadData = (...args) => {
+        loadData = (...args: any[]) => {
           called += 1;
           handleLoadData(...args);
 
@@ -479,26 +465,34 @@ describe('Tree Props', () => {
         }
       }
 
-      const wrapper = mount(<Demo />);
+      const { container } = render(<Demo />);
 
       expect(handleLoadData).not.toHaveBeenCalled();
 
-      const switcher = wrapper.find('.rc-tree-switcher');
-      const node = wrapper.find(InternalTreeNode).instance();
-      switcher.simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
 
-      return timeoutPromise().then(() => {
-        expect(handleLoadData).toHaveBeenCalledWith(node);
-        expect(called).toBe(1);
-        expect(renderToJson(wrapper.render())).toMatchSnapshot();
-      });
+      await timeoutPromise(100);
+
+      expect(handleLoadData).toHaveBeenCalledWith(
+        expect.objectContaining({
+          key: '0-0',
+        }),
+      );
+      expect(called).toBe(1);
+      expect(container.firstChild).toMatchSnapshot();
     });
 
     // https://github.com/ant-design/ant-design/issues/11689#issuecomment-411712770
-    it('with expandedKeys', () => {
+    it('with expandedKeys', async () => {
       let called = 0;
       const keys = {};
+
+      resetWarned();
       const loadData = ({ props: { eventKey } }) => {
+        resetWarned();
+        expect(errorSpy()).toHaveBeenCalledWith(
+          'Warning: Second param return from event is node data instead of TreeNode instance. Please read value directly instead of reading from `props`.',
+        );
         keys[eventKey] = (keys[eventKey] || 0) + 1;
 
         return new Promise(() => {
@@ -506,28 +500,29 @@ describe('Tree Props', () => {
         });
       };
 
-      const tree = mount(
-        <Tree loadData={loadData} expandedKeys={['0', '1', '2']}>
+      const renderTree = (props?: any) => (
+        <Tree loadData={loadData} expandedKeys={['0', '1', '2']} {...props}>
           <TreeNode key="0" />
           <TreeNode key="1" />
           <TreeNode key="2" />
-        </Tree>,
+        </Tree>
       );
+      const { rerender } = render(renderTree());
 
-      tree.setProps({ expandedKeys: ['0', '1', '2'] });
+      rerender(renderTree({ expandedKeys: ['0', '1', '2'] }));
 
-      return timeoutPromise().then(() => {
-        expect(called).toBe(3);
-        expect(keys[0]).toBe(1);
-        expect(keys[1]).toBe(1);
-        expect(keys[2]).toBe(1);
-      });
+      await timeoutPromise();
+
+      expect(called).toBe(3);
+      expect(keys[0]).toBe(1);
+      expect(keys[1]).toBe(1);
+      expect(keys[2]).toBe(1);
     });
 
-    it('with defaultExpandedKeys', () => {
+    it('with defaultExpandedKeys', async () => {
       let called = 0;
       const keys = {};
-      const loadData = ({ props: { eventKey } }) => {
+      const loadData: any = ({ props: { eventKey } }) => {
         keys[eventKey] = (keys[eventKey] || 0) + 1;
 
         return new Promise(() => {
@@ -535,7 +530,7 @@ describe('Tree Props', () => {
         });
       };
 
-      const wrapper = mount(
+      const { container } = render(
         <Tree loadData={loadData} defaultExpandedKeys={['0', '1', '2']}>
           <TreeNode key="0" />
           <TreeNode key="1" />
@@ -544,34 +539,27 @@ describe('Tree Props', () => {
       );
 
       // Do not trigger loadData
-      wrapper
-        .find('.rc-tree-switcher')
-        .at(0)
-        .simulate('click');
-      wrapper
-        .find('.rc-tree-switcher')
-        .at(0)
-        .simulate('click');
-
-      return timeoutPromise().then(() => {
-        expect(called).toBe(3);
-        expect(keys[0]).toBe(1);
-        expect(keys[1]).toBe(1);
-        expect(keys[2]).toBe(1);
-      });
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+
+      await timeoutPromise();
+
+      expect(called).toBe(3);
+      expect(keys[0]).toBe(1);
+      expect(keys[1]).toBe(1);
+      expect(keys[2]).toBe(1);
     });
 
     // https://github.com/ant-design/ant-design/issues/12217
     it('node has false isLeaf & no loadData function', () => {
       const onExpand = jest.fn();
-      const wrapper = mount(
+      const { container } = render(
         <Tree onExpand={onExpand}>
           <TreeNode key="0" isLeaf={false} />
         </Tree>,
       );
 
-      const switcher = wrapper.find('.rc-tree-switcher');
-      switcher.simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-switcher')!);
 
       expect(onExpand).toHaveBeenCalled();
 
@@ -623,29 +611,85 @@ describe('Tree Props', () => {
         }
       }
 
-      const wrapper = mount(<Test />);
+      const { container } = render(<Test />);
 
       // Parent click
-      wrapper.find('.rc-tree-switcher').simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-switcher')!);
 
       setTimeout(() => {
         // Child click
-        wrapper
-          .find('.rc-tree-switcher')
-          .at(1)
-          .simulate('click');
+        fireEvent.click(container.querySelectorAll('.rc-tree-switcher')[1]);
 
         setTimeout(() => {
-          expect(count).toBe(2);
+          expect(count).toBe(1);
           done();
         }, 500);
       }, 500);
     });
+
+    it('reject load', async () => {
+      const { container } = render(
+        <Tree
+          loadData={() => Promise.reject()}
+          expandedKeys={['parent']}
+          treeData={[
+            {
+              title: 'parent',
+              key: 'parent',
+            },
+          ]}
+        />,
+      );
+
+      // Do delay
+      for (let i = 0; i < 20; i += 1) {
+        await act(async () => {
+          await timeoutPromise();
+        });
+      }
+
+      expect(container.querySelector('.rc-tree-icon_loading')).toBeFalsy();
+
+      expect(errorSpy()).toHaveBeenCalledWith(
+        'Warning: Retry for `loadData` many times but still failed. No more retry.',
+      );
+    });
+
+    // https://github.com/ant-design/ant-design/issues/48796
+    it('skip load if has children', () => {
+      const loadData = jest.fn(async info => {
+        console.log('->', info.key);
+      });
+
+      const { container } = render(
+        <Tree
+          loadedKeys={[]}
+          loadData={loadData}
+          treeData={[
+            {
+              title: 'parent',
+              key: 'parent',
+              isLeaf: false,
+              children: [
+                {
+                  title: 'child',
+                  key: 'child',
+                  isLeaf: true,
+                },
+              ],
+            },
+          ]}
+        />,
+      );
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher')!);
+      expect(loadData).not.toHaveBeenCalled();
+    });
   });
 
   it('icon', () => {
     // Node icon has much higher priority
-    const wrapper = render(
+    const { container } = render(
       <Tree defaultExpandAll icon={<span>ROOT ICON</span>}>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" icon={<span>CUSTOMIZE ICON</span>} />
@@ -653,13 +697,13 @@ describe('Tree Props', () => {
       </Tree>,
     );
 
-    expect(renderToJson(wrapper)).toMatchSnapshot();
+    expect(container.firstChild).toMatchSnapshot();
   });
 
   it('onClick', () => {
     const onClick = jest.fn();
 
-    const wrapper = mount(
+    const { container } = render(
       <Tree onClick={onClick} defaultExpandedKeys={['0-0']}>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" />
@@ -667,20 +711,22 @@ describe('Tree Props', () => {
       </Tree>,
     );
 
-    const parentNode = wrapper.find(InternalTreeNode).first();
-    const targetNode = parentNode.find(InternalTreeNode).last();
-
     // Select leaf
-    targetNode.find('.rc-tree-node-content-wrapper').simulate('click');
+    fireEvent.click(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
 
-    expect(onClick).toHaveBeenCalledWith(expect.objectContaining({}), targetNode.instance());
+    expect(onClick).toHaveBeenCalledWith(
+      expect.objectContaining({}),
+      expect.objectContaining({
+        key: '0-0-0',
+      }),
+    );
   });
 
   it('onDoubleClick', () => {
     const onClick = jest.fn();
     const onDoubleClick = jest.fn();
 
-    const wrapper = mount(
+    const { container } = render(
       <Tree onClick={onClick} onDoubleClick={onDoubleClick} defaultExpandedKeys={['0-0']}>
         <TreeNode key="0-0">
           <TreeNode key="0-0-0" />
@@ -688,14 +734,35 @@ describe('Tree Props', () => {
       </Tree>,
     );
 
-    const parentNode = wrapper.find(InternalTreeNode).first();
-    const targetNode = parentNode.find(InternalTreeNode).last();
+    // Select leaf
+    fireEvent.doubleClick(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
+
+    expect(onClick).not.toHaveBeenCalled();
+    expect(onDoubleClick).toHaveBeenCalledWith(
+      expect.objectContaining({}),
+      expect.objectContaining({
+        key: '0-0-0',
+      }),
+    );
+  });
+
+  it('onContextMenu', () => {
+    const onClick = jest.fn();
+    const onContextMenu = jest.fn();
+
+    const { container } = render(
+      <Tree onClick={onClick} onContextMenu={onContextMenu} defaultExpandedKeys={['0-0']}>
+        <TreeNode key="0-0">
+          <TreeNode key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
 
     // Select leaf
-    targetNode.find('.rc-tree-node-content-wrapper').simulate('doubleclick');
+    fireEvent.contextMenu(container.querySelectorAll('.rc-tree-node-content-wrapper')[1]);
 
     expect(onClick).not.toHaveBeenCalled();
-    expect(onDoubleClick).toHaveBeenCalledWith(expect.objectContaining({}), targetNode.instance());
+    expect(onContextMenu).toHaveBeenCalled();
   });
 
   describe('loadedKeys & onLoad', () => {
@@ -703,20 +770,22 @@ describe('Tree Props', () => {
       const loadData = jest.fn(() => Promise.resolve());
       const onLoad = jest.fn();
 
-      const wrapper = mount(
+      const { container } = render(
         <Tree loadedKeys={['0-0']} loadData={loadData} onLoad={onLoad}>
           <TreeNode key="0-0" />
         </Tree>,
       );
 
-      wrapper.find('.rc-tree-switcher').simulate('click');
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
       expect(loadData).not.toHaveBeenCalled();
       expect(onLoad).not.toHaveBeenCalled();
     });
 
     it('reset loadedKeys', () => {
       class FakePromise {
-        constructor(val) {
+        val: any;
+
+        constructor(val?: any) {
           this.val = val;
         }
 
@@ -724,26 +793,42 @@ describe('Tree Props', () => {
           const ret = func(this.val);
           return new FakePromise(ret);
         };
+
+        catch = () => {};
       }
 
-      let wrapper;
+      // eslint-disable-next-line prefer-const
+      let cacheRerender: any;
+      // eslint-disable-next-line prefer-const
+      let renderTree: any;
 
-      const loadData = jest.fn(() => new FakePromise());
       const onLoad = jest.fn(() => {
-        wrapper.setProps({ loadedKeys: ['0-0'] });
+        cacheRerender(renderTree({ loadedKeys: ['0-0'] }));
       });
 
-      wrapper = mount(
-        <Tree loadedKeys={['0-0']} loadData={loadData} onLoad={onLoad}>
+      const loadData: any = jest.fn(() => new FakePromise());
+      renderTree = (props?: any) => (
+        <Tree loadedKeys={['0-0']} loadData={loadData} onLoad={onLoad} {...props}>
           <TreeNode key="0-0" />
-        </Tree>,
+        </Tree>
+      );
+
+      const { rerender, container } = render(renderTree());
+      cacheRerender = rerender;
+
+      rerender(renderTree({ loadedKeys: [] }));
+
+      fireEvent.click(container.querySelector('.rc-tree-switcher'));
+      expect(loadData).toHaveBeenCalledWith(
+        expect.objectContaining({
+          key: '0-0',
+        }),
       );
-      wrapper.setProps({ loadedKeys: [] });
-      wrapper.find('.rc-tree-switcher').simulate('click');
-      expect(loadData).toHaveBeenCalledWith(wrapper.find(InternalTreeNode).instance());
       expect(onLoad).toHaveBeenCalledWith(['0-0'], {
         event: 'load',
-        node: wrapper.find(InternalTreeNode).instance(),
+        node: expect.objectContaining({
+          key: '0-0',
+        }),
       });
     });
   });
@@ -759,73 +844,56 @@ describe('Tree Props', () => {
           {
             key: 'K11',
             title: 'T11',
-            children: [{ key: 'K110', title: 'T110' }, { key: 'K111', title: 'T111' }],
+            children: [
+              { key: 'K110', title: 'T110' },
+              { key: 'K111', title: 'T111' },
+            ],
           },
           { key: 'K12', title: 'T12' },
         ],
       },
     ];
-    const wrapper = mount(<Tree treeData={treeData} defaultExpandAll />);
-    expect(wrapper.render()).toMatchSnapshot();
+    const { container, rerender } = render(<Tree treeData={treeData} defaultExpandAll />);
+    expect(container.firstChild).toMatchSnapshot();
 
-    const newTreeData = [{ key: 'K0', title: 'T0' }];
-    wrapper.setProps({ treeData: newTreeData });
-    wrapper.update();
-    expect(wrapper.render()).toMatchSnapshot();
+    rerender(<Tree treeData={[{ key: 'K0', title: 'T0' }]} defaultExpandAll />);
+    expect(container.firstChild).toMatchSnapshot();
   });
 
   describe('disabled', () => {
     it('basic', () => {
-      const wrapper = render(
+      const { container } = render(
         <Tree defaultExpandAll disabled>
           <TreeNode key="0-0" />
         </Tree>,
       );
-      expect(wrapper).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
 
-    it('treeNode not disabled', () => {
-      const wrapper = render(
+    it('treeNode should disabled when tree disabled', () => {
+      const { container } = render(
         <Tree defaultExpandAll disabled>
           <TreeNode key="0-0" disabled={false} />
         </Tree>,
       );
-      expect(wrapper).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
   });
 
-  it('motion', () => {
-    const motion = {
-      motionName: 'bamboo',
-    };
-    const wrapper = mount(
-      <Tree motion={motion}>
-        <TreeNode key="0-0">
-          <TreeNode key="0-0-0" />
-        </TreeNode>
-      </Tree>,
-    );
-
-    const switcher = wrapper.find('.rc-tree-switcher');
-    switcher.simulate('click');
-
-    expect(wrapper.find('CSSMotion').props()).toMatchObject(motion);
-  });
-
   describe('data and aria props', () => {
     it('renders data attributes', () => {
-      const wrapper = render(<Tree data-test="tree" />);
-      expect(renderToJson(wrapper)).toMatchSnapshot();
+      const { container } = render(<Tree data-test="tree" />);
+      expect(container.firstChild).toMatchSnapshot();
     });
 
     it('renders aria attributes', () => {
-      const wrapper = render(<Tree aria-label="name" />);
-      expect(renderToJson(wrapper)).toMatchSnapshot();
+      const { container } = render(<Tree aria-label="name" />);
+      expect(container.firstChild).toMatchSnapshot();
     });
   });
 
   describe('custom switcher icon', () => {
-    function switcherIcon(text, testLeaf) {
+    function switcherIcon(text: React.ReactNode, testLeaf?: boolean) {
       const sfc = ({ isLeaf }) => {
         if (testLeaf) {
           return isLeaf ? <span>{text}</span> : null;
@@ -833,14 +901,11 @@ describe('Tree Props', () => {
         return isLeaf ? null : <span>{text}</span>;
       };
 
-      sfc.propTypes = {
-        isLeaf: PropTypes.bool,
-      };
-
       return sfc;
     }
+
     it('switcher icon', () => {
-      const wrapper = render(
+      const { container } = render(
         <Tree defaultExpandAll switcherIcon={switcherIcon('switcherIcon')}>
           <TreeNode key="0-0" />
           <TreeNode key="0-1" switcherIcon={switcherIcon('switcherIconFromNode0-1')}>
@@ -849,11 +914,11 @@ describe('Tree Props', () => {
           </TreeNode>
         </Tree>,
       );
-      expect(wrapper).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
 
     it('switcher leaf icon', () => {
-      const wrapper = render(
+      const { container } = render(
         <Tree defaultExpandAll switcherIcon={switcherIcon('switcherLeafIcon', true)}>
           <TreeNode key="0-0" />
           <TreeNode key="0-1" switcherIcon={switcherIcon('switcherLeafIconFromNode0-1', true)} />
@@ -867,13 +932,33 @@ describe('Tree Props', () => {
           <TreeNode key="0-3" />
         </Tree>,
       );
-      expect(wrapper).toMatchSnapshot();
+      expect(container.firstChild).toMatchSnapshot();
     });
   });
 
   it('should style work', () => {
     const style = { background: 'red' };
-    const wrapper = mount(<Tree style={style} />);
-    expect(wrapper.props().style).toEqual(style);
+    const { container } = render(<Tree style={style} />);
+    expect(container.querySelector('.rc-tree-list')).toHaveStyle(style);
+  });
+
+  it('titleRender', () => {
+    const { container } = render(
+      <Tree<FieldDataNode<{ value: string; title?: any }>>
+        defaultExpandAll
+        titleRender={({ value }) => <span className="bamboo-span">{value}</span>}
+        treeData={[
+          { title: ({ value }) => <span className="light-span">{value}</span>, value: 'light' },
+          { value: 'bamboo' },
+        ]}
+      />,
+    );
+
+    expect(
+      container.querySelectorAll('.rc-tree-title')[0].querySelector('.light-span').textContent,
+    ).toEqual('light');
+    expect(
+      container.querySelectorAll('.rc-tree-title')[1].querySelector('.bamboo-span').textContent,
+    ).toEqual('bamboo');
   });
 });
diff --git a/tests/TreeVirtual.spec.tsx b/tests/TreeVirtual.spec.tsx
new file mode 100644
index 000000000..6d2415880
--- /dev/null
+++ b/tests/TreeVirtual.spec.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import {render} from '@testing-library/react';
+import Tree from '../src';
+
+jest.mock('rc-virtual-list', () => jest.requireActual('rc-virtual-list'));
+
+describe('Tree Virtual', () => {
+  it('should display all nodes when for nonvirtual tree', () => {
+    const data = [];
+    for (let i = 0; i < 99; i += 1) {
+      data.push({
+        key: i,
+        title: i,
+      });
+    }
+
+    const { container } = render(
+      <Tree itemHeight={10} height={100} treeData={data} virtual={false} />,
+    );
+
+    expect(
+      container.querySelector('.rc-tree-list-holder').querySelectorAll('.rc-tree-treenode'),
+    ).toHaveLength(99);
+  });
+
+  it('should support scrollWidth parameter and show horizontal scroll', () => {  
+    const data = [{key: 1, title: 'title'}];
+    const { container } = render(
+      <Tree treeData={data} virtual={true} scrollWidth={400} itemHeight={20} height={100} />,
+    );
+    
+    expect(container.querySelector('.rc-tree-list-scrollbar-horizontal')).toBeInTheDocument();
+  
+    jest.useRealTimers();
+  });
+});
diff --git a/tests/__mocks__/rc-util/lib/raf.ts b/tests/__mocks__/rc-util/lib/raf.ts
new file mode 100644
index 000000000..58f2e5771
--- /dev/null
+++ b/tests/__mocks__/rc-util/lib/raf.ts
@@ -0,0 +1,9 @@
+function raf(callback: Function) {
+  return setTimeout(callback);
+}
+
+raf.cancel = (id: number) => {
+  clearTimeout(id);
+};
+
+export default raf;
diff --git a/tests/__snapshots__/Tree.spec.js.snap b/tests/__snapshots__/Tree.spec.js.snap
deleted file mode 100644
index aa9988388..000000000
--- a/tests/__snapshots__/Tree.spec.js.snap
+++ /dev/null
@@ -1,613 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Tree Basic check basic render 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-disabled"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic check check after data ready works 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-checkbox-checked"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-checked"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="Light"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            Light
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-checkbox-checked"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-checked"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="Bamboo"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            Bamboo
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic check check update when Tree trigger componentWillReceiveProps 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-checkbox-checked"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-checked"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="parent 1"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            parent 1
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic ignore illegal node as Tree children Direct TreeNode 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="00"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            00
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="02"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            02
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic ignore illegal node as Tree children Sub TreeNode 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="00"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            00
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="01"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            01
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="010"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            010
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="012"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            012
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic renders correctly 1`] = `
-<div
-  class="rc-tree-list rc-tree forTest rc-tree-show-line"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="spe rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="parent 1"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            parent 1
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-disabled"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="leaf 1"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            leaf 1
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="leaf"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            leaf
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="leaf"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            leaf
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox rc-tree-checkbox-disabled"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="leaf 2"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            leaf 2
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Basic renders opaque children correctly 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_close"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
-          title="0"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__close"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            0
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
diff --git a/tests/__snapshots__/Tree.spec.tsx.snap b/tests/__snapshots__/Tree.spec.tsx.snap
new file mode 100644
index 000000000..60bd99989
--- /dev/null
+++ b/tests/__snapshots__/Tree.spec.tsx.snap
@@ -0,0 +1,1202 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tree Basic check basic render 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-disabled="true"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox rc-tree-checkbox-disabled"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic check check after data ready works 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-checkbox-checked rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="true"
+              aria-label="Select Light"
+              class="rc-tree-checkbox rc-tree-checkbox-checked"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="Light"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                Light
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-checkbox-checked rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="true"
+              aria-label="Select Bamboo"
+              class="rc-tree-checkbox rc-tree-checkbox-checked"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="Bamboo"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                Bamboo
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic check check update when Tree trigger componentWillReceiveProps 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-checkbox-checked rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="true"
+              aria-label="Select parent 1"
+              class="rc-tree-checkbox rc-tree-checkbox-checked"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="parent 1"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                parent 1
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic ignore illegal node as Tree children Direct TreeNode 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="00"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                00
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="02"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                02
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic ignore illegal node as Tree children Sub TreeNode 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="00"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                00
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="01"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                01
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="010"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                010
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="012"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                012
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic renders correctly 1`] = `
+<div
+  class="rc-tree forTest rc-tree-show-line"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="spe rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select parent 1"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="parent 1"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                parent 1
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-disabled="true"
+              aria-label="Select leaf 1"
+              class="rc-tree-checkbox rc-tree-checkbox-disabled"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="leaf 1"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                leaf 1
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select leaf"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="leaf"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                leaf
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select leaf"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="leaf"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                leaf
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-disabled="true"
+              aria-label="Select leaf 2"
+              class="rc-tree-checkbox rc-tree-checkbox-disabled"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="leaf 2"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                leaf 2
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic renders opaque children correctly 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_close"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
+              title="0"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__close"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                0
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Basic should support rootStyle and rootClassName 1`] = `
+<div
+  class="rc-tree root-tree"
+  style="background-color: cyan;"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                0
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                1
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                2
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                3
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                4
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                5
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                6
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                7
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title=""
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                8
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/tests/__snapshots__/TreeNodeProps.spec.js.snap b/tests/__snapshots__/TreeNodeProps.spec.js.snap
deleted file mode 100644
index 36ae56b1a..000000000
--- a/tests/__snapshots__/TreeNodeProps.spec.js.snap
+++ /dev/null
@@ -1,778 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`TreeNode Props className 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props customize icon component 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__customize"
-          >
-            <span
-              class="cust-icon"
-            />
-          </span>
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props customize icon element 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__customize"
-          >
-            <span
-              class="cust-icon"
-            />
-          </span>
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props customize icon hide icon 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props data and aria props renders aria attributes on li 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        aria-label="0-0"
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        aria-label="0-0-0"
-        class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        aria-label="0-0-0-0"
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        aria-label="0-0-1"
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        aria-label="0-1"
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props data and aria props renders data attributes on li 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        data-test="0-0"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
-        data-test="0-0-0"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        data-test="0-0-0-0"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        data-test="0-0-1"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        data-test="0-1"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props isLeaf 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props isLeaf 2`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`TreeNode Props isLeaf 3`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
diff --git a/tests/__snapshots__/TreeNodeProps.spec.tsx.snap b/tests/__snapshots__/TreeNodeProps.spec.tsx.snap
new file mode 100644
index 000000000..4bf68f3f7
--- /dev/null
+++ b/tests/__snapshots__/TreeNodeProps.spec.tsx.snap
@@ -0,0 +1,1116 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`TreeNode Props className 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props customize icon component 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__customize"
+              >
+                <span
+                  class="cust-icon"
+                />
+              </span>
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props customize icon element 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__customize"
+              >
+                <span
+                  class="cust-icon"
+                />
+              </span>
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props customize icon hide icon 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props data and aria props renders aria attributes on li 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            aria-label="0-0"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            aria-label="0-0-0"
+            class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            aria-label="0-0-0-0"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            aria-label="0-0-1"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            aria-label="0-1"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props data and aria props renders data attributes on li 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            data-test="0-0"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="tree-node-cls rc-tree-treenode rc-tree-treenode-switcher-open"
+            data-test="0-0-0"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            data-test="0-0-0-0"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            data-test="0-0-1"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            data-test="0-1"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props isLeaf 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            class="rc-tree-treenode rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            class="rc-tree-treenode rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props isLeaf 2`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            class="rc-tree-treenode rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            class="rc-tree-treenode rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`TreeNode Props isLeaf 3`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_close"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__close"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/tests/__snapshots__/TreeProps.spec.js.snap b/tests/__snapshots__/TreeProps.spec.js.snap
deleted file mode 100644
index 917ae6cea..000000000
--- a/tests/__snapshots__/TreeProps.spec.js.snap
+++ /dev/null
@@ -1,1744 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Tree Props checkStrictly 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props checkable default 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props checkable without selectable 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props custom switcher icon switcher icon 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        >
-          <span>
-            switcherIconFromNode0-1
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props custom switcher icon switcher leaf icon 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        >
-          <span>
-            switcherLeafIcon
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        >
-          <span>
-            switcherLeafIconFromNode0-1
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        >
-          <span>
-            switcherLeafIcon
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        >
-          <span>
-            switcherLeafIconFromNode0-2-1
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        >
-          <span>
-            switcherLeafIcon
-          </span>
-        </span>
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props data and aria props renders aria attributes 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    />
-  </div>
-</div>
-`;
-
-exports[`Tree Props data and aria props renders data attributes 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    />
-  </div>
-</div>
-`;
-
-exports[`Tree Props defaultExpandAll 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props disabled basic 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props disabled treeNode not disabled 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props icon 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__customize"
-          >
-            <span>
-              ROOT ICON
-            </span>
-          </span>
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__customize"
-          >
-            <span>
-              CUSTOMIZE ICON
-            </span>
-          </span>
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props invalidate checkedKeys null 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props invalidate checkedKeys number 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-checkbox"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props loadData basic 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_close"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props multiple 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props prefixCls 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    />
-  </div>
-</div>
-`;
-
-exports[`Tree Props prefixCls 2`] = `
-<div
-  class="test-prefix-list test-prefix"
-  role="tree"
->
-  <div>
-    <div
-      class="test-prefix-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    />
-  </div>
-</div>
-`;
-
-exports[`Tree Props selectable with selectable 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props selectable without selectable 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props showIcon 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_close"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__close"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props showIcon 2`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_close"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props showIcon 3`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-close"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="---"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            ---
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props showLine 1`] = `
-<div
-  class="rc-tree-list rc-tree rc-tree-show-line"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display:flex;flex-direction:column"
-    />
-  </div>
-</div>
-`;
-
-exports[`Tree Props treeData 1`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T0"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T0
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="T1"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T1
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T10"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T10
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher_open"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
-          title="T11"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__open"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T11
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-start"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T110"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T110
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit"
-          />
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T111"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T111
-          </span>
-        </span>
-      </div>
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          aria-hidden="true"
-          class="rc-tree-indent"
-        >
-          <span
-            class="rc-tree-indent-unit rc-tree-indent-unit-end"
-          />
-        </span>
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T12"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T12
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
-
-exports[`Tree Props treeData 2`] = `
-<div
-  class="rc-tree-list rc-tree"
-  role="tree"
->
-  <div>
-    <div
-      class="rc-tree-list-holder-inner"
-      style="display: flex; flex-direction: column;"
-    >
-      <div
-        class="rc-tree-treenode rc-tree-treenode-switcher-open"
-        role="treeitem"
-        tabindex="0"
-      >
-        <span
-          class="rc-tree-switcher rc-tree-switcher-noop"
-        />
-        <span
-          class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
-          title="T0"
-        >
-          <span
-            class="rc-tree-iconEle rc-tree-icon__docu"
-          />
-          <span
-            class="rc-tree-title"
-          >
-            T0
-          </span>
-        </span>
-      </div>
-    </div>
-  </div>
-</div>
-`;
diff --git a/tests/__snapshots__/TreeProps.spec.tsx.snap b/tests/__snapshots__/TreeProps.spec.tsx.snap
new file mode 100644
index 000000000..6a001df69
--- /dev/null
+++ b/tests/__snapshots__/TreeProps.spec.tsx.snap
@@ -0,0 +1,2687 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tree Props checkStrictly 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props checkable default 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props checkable without selectable 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props custom switcher icon switcher icon 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            >
+              <span>
+                switcherIconFromNode0-1
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props custom switcher icon switcher leaf icon 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            >
+              <span>
+                switcherLeafIcon
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            >
+              <span>
+                switcherLeafIconFromNode0-1
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            >
+              <span>
+                switcherLeafIcon
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            >
+              <span>
+                switcherLeafIconFromNode0-2-1
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            >
+              <span>
+                switcherLeafIcon
+              </span>
+            </span>
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props data and aria props renders aria attributes 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    aria-label="name"
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        />
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props data and aria props renders data attributes 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    data-test="tree"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        />
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props defaultExpandAll 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="parent"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                parent
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="child"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                child
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props disabled basic 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      disabled=""
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props disabled treeNode should disabled when tree disabled 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      disabled=""
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-disabled rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props icon 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__customize"
+              >
+                <span>
+                  ROOT ICON
+                </span>
+              </span>
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__customize"
+              >
+                <span>
+                  CUSTOMIZE ICON
+                </span>
+              </span>
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props invalidate checkedKeys null 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props invalidate checkedKeys number 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              aria-checked="false"
+              aria-label="Select tree node"
+              class="rc-tree-checkbox"
+              role="checkbox"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props loadData basic 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_close"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props multiple 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props prefixCls 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        />
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props prefixCls 2`] = `
+<div
+  class="test-prefix"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="test-prefix-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="test-prefix-indent"
+    >
+      <div
+        class="test-prefix-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="test-prefix-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="test-prefix-list-holder"
+    >
+      <div>
+        <div
+          class="test-prefix-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        />
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props selectable with selectable 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props selectable without selectable 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props showIcon 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_close"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__close"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props showIcon 2`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_close"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-close"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props showIcon 3`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-start"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="---"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                ---
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props showLine 1`] = `
+<div
+  class="rc-tree rc-tree-show-line"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        />
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props treeData 1`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T0"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T0
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open rc-tree-treenode-leaf-last"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="T1"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T1
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T10"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T10
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="true"
+            class="rc-tree-treenode rc-tree-treenode-switcher-open"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher_open"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-open"
+              title="T11"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__open"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T11
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+              <span
+                class="rc-tree-indent-unit"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T110"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T110
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+              <span
+                class="rc-tree-indent-unit"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T111"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T111
+              </span>
+            </span>
+          </div>
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            >
+              <span
+                class="rc-tree-indent-unit rc-tree-indent-unit-end"
+              />
+            </span>
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T12"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T12
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
+
+exports[`Tree Props treeData 2`] = `
+<div
+  class="rc-tree"
+>
+  <div>
+    <input
+      aria-label="for screen reader"
+      style="width: 0px; height: 0px; display: flex; overflow: hidden; opacity: 0; border: 0px; padding: 0px; margin: 0px;"
+      tabindex="0"
+      value=""
+    />
+  </div>
+  <div
+    aria-hidden="true"
+    class="rc-tree-treenode"
+    style="position: absolute; pointer-events: none; visibility: hidden; height: 0px; overflow: hidden; border: 0px; padding: 0px;"
+  >
+    <div
+      class="rc-tree-indent"
+    >
+      <div
+        class="rc-tree-indent-unit"
+      />
+    </div>
+  </div>
+  <div
+    class="rc-tree-list"
+    role="tree"
+    style="position: relative;"
+  >
+    <div
+      class="rc-tree-list-holder"
+    >
+      <div>
+        <div
+          class="rc-tree-list-holder-inner"
+          style="display: flex; flex-direction: column;"
+        >
+          <div
+            aria-expanded="false"
+            class="rc-tree-treenode rc-tree-treenode-switcher-close rc-tree-treenode-leaf-last rc-tree-treenode-leaf"
+            draggable="false"
+            role="treeitem"
+          >
+            <span
+              aria-hidden="true"
+              class="rc-tree-indent"
+            />
+            <span
+              class="rc-tree-switcher rc-tree-switcher-noop"
+            />
+            <span
+              class="rc-tree-node-content-wrapper rc-tree-node-content-wrapper-normal"
+              title="T0"
+            >
+              <span
+                class="rc-tree-iconEle rc-tree-icon__docu"
+              />
+              <span
+                class="rc-tree-title"
+              >
+                T0
+              </span>
+            </span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>
+`;
diff --git a/tests/expandAction.spec.tsx b/tests/expandAction.spec.tsx
new file mode 100644
index 000000000..97da758fc
--- /dev/null
+++ b/tests/expandAction.spec.tsx
@@ -0,0 +1,177 @@
+/* eslint-disable no-undef, react/no-multi-comp */
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import Tree, { TreeNode } from '../src';
+import { spyConsole } from './util';
+
+describe('ExpandAction', () => {
+  spyConsole();
+
+  it('title expandable when selectable is false and expandAction is "click"', () => {
+    const onClick = jest.fn();
+    const onSelect = jest.fn();
+    const onExpand = jest.fn();
+
+    const { container } = render(
+      <Tree
+        onClick={onClick}
+        onSelect={onSelect}
+        onExpand={onExpand}
+        defaultExpandedKeys={['0-0']}
+        selectable={false}
+        expandAction="click"
+      >
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0">
+            <TreeNode title="leaf-1" key="0-0-0-0" />
+          </TreeNode>
+
+          <TreeNode title="leaf 2" key="0-1-0">
+            <TreeNode title="leaf-2" key="0-1-0-0" />
+          </TreeNode>
+        </TreeNode>
+      </Tree>,
+    );
+
+    // test trigger expand when click title
+    fireEvent.click(container.querySelector('[title="leaf 1"]'));
+
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(onExpand).toHaveBeenCalledWith(['0-0', '0-0-0'], {
+      expanded: true,
+      node: expect.anything(),
+      nativeEvent: expect.anything(),
+    });
+
+    onClick.mockReset();
+    onSelect.mockReset();
+    onExpand.mockReset();
+
+    // test trigger un-expand when click title again
+    fireEvent.click(container.querySelector('[title="leaf 1"]'));
+
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(onExpand).toHaveBeenCalledWith(['0-0'], {
+      expanded: false,
+      node: expect.anything(),
+      nativeEvent: expect.anything(),
+    });
+  });
+
+  it('title expandable when selectable is false and expandAction is "doubleClick"', () => {
+    const onDoubleClick = jest.fn();
+    const onSelect = jest.fn();
+    const onExpand = jest.fn();
+
+    const { container } = render(
+      <Tree
+        onDoubleClick={onDoubleClick}
+        onSelect={onSelect}
+        onExpand={onExpand}
+        defaultExpandedKeys={['0-0']}
+        selectable={false}
+        expandAction="doubleClick"
+      >
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0">
+            <TreeNode title="leaf-1" key="0-0-0-0" />
+          </TreeNode>
+
+          <TreeNode title="leaf 2" key="0-1-0">
+            <TreeNode title="leaf-2" key="0-1-0-0" />
+          </TreeNode>
+        </TreeNode>
+      </Tree>,
+    );
+
+    // test trigger expand when double click title
+    fireEvent.doubleClick(container.querySelector('[title="leaf 1"]'));
+
+    expect(onDoubleClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(onExpand).toHaveBeenCalledWith(['0-0', '0-0-0'], {
+      expanded: true,
+      node: expect.anything(),
+      nativeEvent: expect.anything(),
+    });
+
+    onDoubleClick.mockReset();
+    onSelect.mockReset();
+    onExpand.mockReset();
+
+    // test trigger un-expand when double click title again
+    fireEvent.doubleClick(container.querySelector('[title="leaf 1"]'));
+
+    expect(onDoubleClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(onExpand).toHaveBeenCalledWith(['0-0'], {
+      expanded: false,
+      node: expect.anything(),
+      nativeEvent: expect.anything(),
+    });
+  });
+
+  it('title un-expandable when selectable is false and expandAction is false', () => {
+    const onClick = jest.fn();
+    const onSelect = jest.fn();
+    const onExpand = jest.fn();
+
+    const { container } = render(
+      <Tree
+        onClick={onClick}
+        onSelect={onSelect}
+        onExpand={onExpand}
+        defaultExpandedKeys={['0-0']}
+        selectable={false}
+        expandAction={false}
+      >
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf 1" key="0-0-0">
+            <TreeNode title="leaf-1" key="0-0-0-0" />
+          </TreeNode>
+
+          <TreeNode title="leaf 2" key="0-1-0">
+            <TreeNode title="leaf-2" key="0-1-0-0" />
+          </TreeNode>
+        </TreeNode>
+      </Tree>,
+    );
+
+    // test won't trigger expand when click title if expandAction is false
+    fireEvent.click(container.querySelector('[title="leaf 2"]'));
+
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).not.toHaveBeenCalled();
+    expect(onExpand).not.toHaveBeenCalled();
+  });
+
+  it('not trigger expand when ctrl pressed', () => {
+    const onClick = jest.fn();
+    const onSelect = jest.fn();
+    const onExpand = jest.fn();
+
+    const { container } = render(
+      <Tree
+        onClick={onClick}
+        onSelect={onSelect}
+        onExpand={onExpand}
+        selectable
+        expandAction={'click'}
+      >
+        <TreeNode title="parent 1" key="0-0">
+          <TreeNode title="leaf-1" key="0-0-0" />
+        </TreeNode>
+      </Tree>,
+    );
+
+    fireEvent.click(container.querySelector('[title="parent 1"]'), {
+      ctrlKey: true,
+    });
+
+    expect(onClick).toHaveBeenCalled();
+    expect(onSelect).toHaveBeenCalled();
+    expect(onExpand).not.toHaveBeenCalled();
+  });
+});
diff --git a/tests/setup.js b/tests/setup.js
deleted file mode 100644
index b2e8b4df4..000000000
--- a/tests/setup.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/* eslint-disable global-require */
-
-global.requestAnimationFrame = global.requestAnimationFrame || function requestAnimationFrame(cb) {
-  return setTimeout(cb, 0);
-};
-
-const Enzyme = require('enzyme');
-
-let Adapter;
-
-if (process.env.REACT === '15') {
-  Adapter = require('enzyme-adapter-react-15');
-} else {
-  Adapter = require('enzyme-adapter-react-16');
-}
-
-Enzyme.configure({ adapter: new Adapter() });
diff --git a/tests/setupFilesAfterEnv.js b/tests/setupFilesAfterEnv.js
new file mode 100644
index 000000000..7b0828bfa
--- /dev/null
+++ b/tests/setupFilesAfterEnv.js
@@ -0,0 +1 @@
+import '@testing-library/jest-dom';
diff --git a/tests/type.spec.tsx b/tests/type.spec.tsx
new file mode 100644
index 000000000..7ba1c0549
--- /dev/null
+++ b/tests/type.spec.tsx
@@ -0,0 +1,29 @@
+/* eslint-disable no-undef, react/no-multi-comp */
+import React from 'react';
+import { render } from '@testing-library/react';
+import Tree, { BasicDataNode } from '../src';
+
+describe('Tree.TypeScript', () => {
+  it('fieldNames', () => {
+    interface DataType extends BasicDataNode {
+      label: string;
+      value: string;
+      list?: DataType[];
+    }
+
+    render(
+      <Tree<DataType>
+        treeData={[
+          {
+            label: 'parent',
+            value: 'parent',
+            list: [],
+          },
+        ]}
+        onSelect={(selectedKeys, info) => {
+          console.log('info', info.node.isLeaf);
+        }}
+      />,
+    );
+  });
+});
diff --git a/tests/util.spec.js b/tests/util.spec.js
index 4c5d83dc3..a0b735301 100644
--- a/tests/util.spec.js
+++ b/tests/util.spec.js
@@ -1,18 +1,20 @@
 /* eslint-disable no-undef, react/no-multi-comp,
 react/no-unused-state, react/prop-types, no-return-assign */
-import React from 'react';
-import { mount } from 'enzyme';
 import Tree, { TreeNode } from '../src';
-import { InternalTreeNode } from '../src/TreeNode';
 import {
-  convertDataToTree,
-  conductCheck,
   conductExpandParent,
-  getDragNodesKeys,
-  getDataAndAria,
+  convertDataToTree,
+  getDragChildrenKeys,
   parseCheckedKeys,
 } from '../src/util';
-import { flattenTreeData, convertTreeToData, convertDataToEntities } from '../src/utils/treeUtil';
+import { conductCheck } from '../src/utils/conductUtil';
+import {
+  convertDataToEntities,
+  convertTreeToData,
+  flattenTreeData,
+  getTreeNodeProps,
+  traverseDataNodes,
+} from '../src/utils/treeUtil';
 import { spyConsole, spyError } from './util';
 
 describe('Util', () => {
@@ -55,7 +57,7 @@ describe('Util', () => {
           },
         ],
       },
-      { key: '!', title: '---', really: true },
+      { key: '!', really: true },
     ]);
   });
 
@@ -109,6 +111,87 @@ describe('Util', () => {
     expect(treeNodes[0].props.children[0].props.value).toBe('0-0-0');
   });
 
+  describe('convertDataToEntities', () => {
+    it('basic convert', () => {
+      const entities = convertDataToEntities([
+        { key: 'parent', children: [{ key: 0 }, { key: 1 }] },
+      ]);
+      expect(Object.keys(entities.keyEntities).sort()).toEqual(['0', '1', 'parent']);
+
+      // Used for `rc-cascader` to get fully path
+      expect(entities.keyEntities.parent.nodes).toEqual([
+        expect.objectContaining({ key: 'parent' }),
+      ]);
+      expect(entities.keyEntities[0].nodes).toEqual([
+        expect.objectContaining({ key: 'parent' }),
+        expect.objectContaining({ key: 0 }),
+      ]);
+    });
+
+    it('with string externalGetKey', () => {
+      const entities = convertDataToEntities(
+        [
+          {
+            key: 'parent',
+            notKey: 'let it be',
+            children: [
+              { key: 0, notKey: 'penny lane' },
+              { key: 1, notKey: 'please please me' },
+            ],
+          },
+        ],
+        undefined,
+        'notKey',
+      );
+      expect(Object.keys(entities.keyEntities).sort()).toEqual([
+        'let it be',
+        'penny lane',
+        'please please me',
+      ]);
+    });
+
+    it('with function externalGetKey', () => {
+      const entities = convertDataToEntities(
+        [
+          {
+            key: 'parent',
+            notKey: 'let it be',
+            children: [
+              { key: 0, notKey: 'penny lane' },
+              { key: 1, notKey: 'please please me' },
+            ],
+          },
+        ],
+        undefined,
+        entity => entity.notKey,
+      );
+      expect(Object.keys(entities.keyEntities).sort()).toEqual([
+        'let it be',
+        'penny lane',
+        'please please me',
+      ]);
+    });
+
+    it('with childrenPropName', () => {
+      const entities = convertDataToEntities(
+        [
+          {
+            rawKey: 'light',
+            childList: [{ rawKey: 'bamboo' }],
+          },
+        ],
+        {
+          externalGetKey: entity => entity.rawKey,
+          childrenPropName: 'childList',
+        },
+      );
+      expect(Object.keys(entities.keyEntities).sort()).toEqual(['bamboo', 'light']);
+
+      expect(entities.keyEntities.light.children).toEqual([entities.keyEntities.bamboo]);
+      expect(entities.keyEntities.bamboo.parent).toBe(entities.keyEntities.light);
+    });
+  });
+
   it('convertTreeToEntities with additional handler', () => {
     const onProcessFinished = jest.fn();
 
@@ -126,6 +209,7 @@ describe('Util', () => {
           valueEntities: {},
         }),
         processEntity: (entity, wrapper) => {
+          // eslint-disable-next-line no-param-reassign
           wrapper.valueEntities[entity.node.value] = entity;
         },
         onProcessFinished,
@@ -175,14 +259,39 @@ describe('Util', () => {
         const allCheckedKeys = conductCheck(['greed', 'good'], true, keyEntities).checkedKeys;
         expect(allCheckedKeys.length).toEqual(11);
 
-        // Then uncheck one of then
-        const { checkedKeys, halfCheckedKeys } = conductCheck(['spoon'], false, keyEntities, {
-          checkedKeys: allCheckedKeys,
-        });
+        // Then un-check one of then
+        const removedKeys = allCheckedKeys.filter(key => key !== 'spoon');
+        const { checkedKeys, halfCheckedKeys } = conductCheck(
+          removedKeys,
+          { checked: false, halfCheckedKeys: [] },
+          keyEntities,
+        );
         expect(checkedKeys.sort()).toEqual(['greed', 'there', 'no'].sort());
         expect(halfCheckedKeys.sort()).toEqual(['good', 'is'].sort());
       });
 
+      it('uncheck partial', () => {
+        const tree = (
+          <Tree>
+            <TreeNode key="0-0" title="0-0">
+              <TreeNode key="0-0-0" title="0-0-0" />
+              <TreeNode key="0-0-1" title="0-0-1" />
+            </TreeNode>
+          </Tree>
+        );
+        const { keyEntities } = convertDataToEntities(convertTreeToData(tree.props.children));
+        const { checkedKeys, halfCheckedKeys } = conductCheck(
+          [],
+          {
+            checked: false,
+            halfCheckedKeys: ['0-0', '0-0-0'],
+          },
+          keyEntities,
+        );
+        expect(checkedKeys).toEqual([]);
+        expect(halfCheckedKeys).toEqual([]);
+      });
+
       describe('not exist', () => {
         const errorSpy = spyError();
 
@@ -235,13 +344,92 @@ describe('Util', () => {
         expect(allCheckedKeys.length).toEqual(6);
 
         // Then uncheck one of then
-        const result1 = conductCheck(['not'], false, keyEntities, { checkedKeys: allCheckedKeys });
+        const result1 = conductCheck(
+          allCheckedKeys.filter(key => key !== 'not'),
+          { checked: false, halfCheckedKeys: [] },
+          keyEntities,
+        );
         expect(result1.checkedKeys.sort()).toEqual(['are', 'to', 'be'].sort());
         expect(result1.halfCheckedKeys.sort()).toEqual(['war'].sort());
 
-        const result2 = conductCheck(['to', 'be'], false, keyEntities, {
-          checkedKeys: allCheckedKeys,
-        });
+        const result2 = conductCheck(
+          allCheckedKeys.filter(key => key !== 'to' && key !== 'be'),
+          { checked: false, halfCheckedKeys: [] },
+          keyEntities,
+        );
+        expect(result2.checkedKeys.sort()).toEqual(['war', 'are', 'not', 'it'].sort());
+        expect(result2.halfCheckedKeys.sort()).toEqual([].sort());
+      });
+
+      const genNonDisabledTree = props => (
+        <Tree {...props}>
+          <TreeNode key="war">
+            <TreeNode key="are" />
+            <TreeNode key="not">
+              <TreeNode key="it" />
+              <TreeNode key="used">
+                <TreeNode key="to" />
+                <TreeNode key="be" />
+              </TreeNode>
+            </TreeNode>
+          </TreeNode>
+        </Tree>
+      );
+
+      it('check with custom checkbox disabled', () => {
+        const tree = genNonDisabledTree();
+        const { keyEntities } = convertDataToEntities(
+          convertTreeToData(tree.props.children),
+          undefined,
+          undefined,
+        );
+
+        const getCheckDisabled = data => data.key === 'used';
+
+        const result1 = conductCheck(['not'], true, keyEntities, getCheckDisabled);
+        expect(result1.checkedKeys.sort()).toEqual(['not', 'it'].sort());
+        expect(result1.halfCheckedKeys.sort()).toEqual(['war'].sort());
+
+        const result2 = conductCheck(['to', 'be'], true, keyEntities, getCheckDisabled);
+        expect(result2.checkedKeys.sort()).toEqual(['to', 'be'].sort());
+        expect(result2.halfCheckedKeys.sort()).toEqual([].sort());
+      });
+
+      it('uncheck with custom checkbox disabled', () => {
+        const tree = genNonDisabledTree();
+        const { keyEntities } = convertDataToEntities(
+          convertTreeToData(tree.props.children),
+          undefined,
+          undefined,
+        );
+
+        const getCheckDisabled = data => data.key === 'used';
+
+        // First, we check all
+        const allCheckedKeys = conductCheck(
+          ['war', 'to', 'be'],
+          true,
+          keyEntities,
+          getCheckDisabled,
+        ).checkedKeys;
+        expect(allCheckedKeys.length).toEqual(6);
+
+        // Then uncheck one of then
+        const result1 = conductCheck(
+          allCheckedKeys.filter(key => key !== 'not'),
+          { checked: false, halfCheckedKeys: [] },
+          keyEntities,
+          getCheckDisabled,
+        );
+        expect(result1.checkedKeys.sort()).toEqual(['are', 'to', 'be'].sort());
+        expect(result1.halfCheckedKeys.sort()).toEqual(['war'].sort());
+
+        const result2 = conductCheck(
+          allCheckedKeys.filter(key => key !== 'to' && key !== 'be'),
+          { checked: false, halfCheckedKeys: [] },
+          keyEntities,
+          getCheckDisabled,
+        );
         expect(result2.checkedKeys.sort()).toEqual(['war', 'are', 'not', 'it'].sort());
         expect(result2.halfCheckedKeys.sort()).toEqual([].sort());
       });
@@ -264,7 +452,7 @@ describe('Util', () => {
     expect(keys.sort()).toEqual(['bamboo', 'is', 'good'].sort());
   });
 
-  it('getDragNodesKeys', () => {
+  it('getDragChildrenKeys', () => {
     const tree = (
       <Tree defaultExpandAll>
         <TreeNode key="000">
@@ -277,26 +465,11 @@ describe('Util', () => {
     );
 
     const { keyEntities } = convertDataToEntities(convertTreeToData(tree.props.children));
-    const keys0 = getDragNodesKeys('000', keyEntities);
-    expect(keys0.sort()).toEqual(['000', '111', '222', '333'].sort());
-
-    const keys1 = getDragNodesKeys('111', keyEntities);
-    expect(keys1.sort()).toEqual(['111', '222', '333'].sort());
-  });
+    const keys0 = getDragChildrenKeys('000', keyEntities);
+    expect(keys0.sort()).toEqual(['111', '222', '333'].sort());
 
-  describe('getDataAndAria', () => {
-    it('should return only data- and aria- properties', () => {
-      const props = {
-        'data-test': 'name',
-        'aria-label': 'name',
-        dataSource: '/api',
-        ariaLabel: 'some-label',
-      };
-      expect(getDataAndAria(props)).toEqual({
-        'data-test': 'name',
-        'aria-label': 'name',
-      });
-    });
+    const keys1 = getDragChildrenKeys('111', keyEntities);
+    expect(keys1.sort()).toEqual(['222', '333'].sort());
   });
 
   it('parseCheckedKeys warning', () => {
@@ -309,7 +482,7 @@ describe('Util', () => {
     errorSpy.mockRestore();
   });
 
-  it('flatten treeNode', () => {
+  describe('flatten treeNode', () => {
     function getNode(key, children) {
       return {
         key,
@@ -318,26 +491,132 @@ describe('Util', () => {
       };
     }
 
-    const flattenList = flattenTreeData(
-      [
-        getNode('0', [
-          getNode('0-0'),
-          getNode('0-1'),
-          getNode('0-2', [getNode('0-2-0'), getNode('0-2-1')]),
-        ]),
-        getNode('1'),
-      ],
-      ['0-2', '0'],
+    it('with expanded keys', () => {
+      const flattenList = flattenTreeData(
+        [
+          getNode('0', [
+            getNode('0-0'),
+            getNode('0-1'),
+            getNode('0-2', [getNode('0-2-0'), getNode('0-2-1')]),
+          ]),
+          getNode('1'),
+        ],
+        ['0-2', '0'],
+      );
+
+      expect(flattenList.map(({ key }) => key)).toEqual([
+        '0',
+        '0-0',
+        '0-1',
+        '0-2',
+        '0-2-0',
+        '0-2-1',
+        '1',
+      ]);
+    });
+
+    it('with all', () => {
+      const flattenList = flattenTreeData(
+        [
+          getNode('0', [
+            getNode('0-0'),
+            getNode('0-1'),
+            getNode('0-2', [getNode('0-2-0'), getNode('0-2-1')]),
+          ]),
+          getNode('1'),
+        ],
+        true,
+      );
+
+      expect(flattenList.map(({ key }) => key)).toEqual([
+        '0',
+        '0-0',
+        '0-1',
+        '0-2',
+        '0-2-0',
+        '0-2-1',
+        '1',
+      ]);
+    });
+
+    it('isEnd should be correct', () => {
+      const flattenList = flattenTreeData(
+        [
+          getNode('0', [
+            getNode('0-0'),
+            getNode('0-1'),
+            getNode('0-2', [
+              // Break lines
+              getNode('0-2-0'),
+            ]),
+          ]),
+          getNode('1', [
+            // Break lines
+            getNode('1-0'),
+            getNode('1-1'),
+          ]),
+        ],
+        true,
+      );
+
+      expect(
+        flattenList.map(({ isStart, isEnd }) => ({
+          isStart,
+          isEnd,
+        })),
+      ).toEqual([
+        { isStart: [true], isEnd: [false] },
+        { isStart: [true, true], isEnd: [false, false] },
+        { isStart: [true, false], isEnd: [false, false] },
+        { isStart: [true, false], isEnd: [false, true] },
+        { isStart: [true, false, true], isEnd: [false, true, true] },
+
+        { isStart: [false], isEnd: [true] },
+        { isStart: [false, true], isEnd: [true, false] },
+        { isStart: [false, false], isEnd: [true, true] },
+      ]);
+    });
+  });
+
+  it('not crash if node not exist with getTreeNodeProps', () => {
+    const nodeProps = getTreeNodeProps('not-exist', {
+      expandedKeys: [],
+      selectedKeys: [],
+      loadedKeys: [],
+      loadingKeys: [],
+      checkedKeys: [],
+      halfCheckedKeys: [],
+      dragOverNodeKey: null,
+      dropPosition: null,
+      keyEntities: {},
+    });
+
+    expect(nodeProps).toEqual(
+      expect.objectContaining({
+        checked: false,
+        eventKey: 'not-exist',
+        expanded: false,
+        halfChecked: false,
+        loaded: false,
+        loading: false,
+        pos: '',
+        selected: false,
+      }),
     );
+  });
 
-    expect(flattenList.map(({ key }) => key)).toEqual([
-      '0',
-      '0-0',
-      '0-1',
-      '0-2',
-      '0-2-0',
-      '0-2-1',
-      '1',
-    ]);
+  it('traverseDataNodes legacy externalGetKey', () => {
+    let count = 0;
+
+    traverseDataNodes(
+      [{ rawKey: 'light' }],
+      data => {
+        count += 1;
+        expect(data.key).toBe('light');
+      },
+      'rawKey',
+    );
+
+    expect(count).toEqual(1);
   });
 });
diff --git a/tests/util.js b/tests/util.ts
similarity index 81%
rename from tests/util.js
rename to tests/util.ts
index 2b9239d8c..32782c3c6 100644
--- a/tests/util.js
+++ b/tests/util.ts
@@ -1,17 +1,11 @@
-import { ReactWrapper } from 'enzyme';
 import { Component } from 'react';
 
-export function objectMatcher(item) {
+export function objectMatcher(item: object) {
   const result = Array.isArray(item) ? [] : {};
 
   Object.keys(item).forEach(key => {
     const value = item[key];
-    if (
-      value &&
-      typeof value === 'object' &&
-      !(value instanceof ReactWrapper) &&
-      !(value instanceof Component)
-    ) {
+    if (value && typeof value === 'object' && !(value instanceof Component)) {
       result[key] = objectMatcher(value);
     } else {
       result[key] = value;
@@ -27,9 +21,11 @@ export function spyConsole() {
     'Warning: `children` of Tree is deprecated. Please use `treeData` instead.',
   ];
 
+  // eslint-disable-next-line no-console
   const originConsoleErr = console.error;
 
   beforeAll(() => {
+    // eslint-disable-next-line no-console
     console.error = jest.fn().mockImplementation((...args) => {
       if (errorList.some(tmpl => args[0].includes(tmpl))) {
         return;
@@ -40,6 +36,7 @@ export function spyConsole() {
   });
 
   afterAll(() => {
+    // eslint-disable-next-line no-console
     console.error = originConsoleErr;
   });
 }
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 000000000..38b8cdfd5
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "target": "esnext",
+    "moduleResolution": "node",
+    "baseUrl": "./",
+    "jsx": "react",
+    "declaration": true,
+    "skipLibCheck": true,
+    "esModuleInterop": true,
+    "paths": { "@/*": ["src/*"], "@@/*": ["src/.umi/*"], "@rc-component/tree": ["src/index.ts"] }
+  },
+  "include": [
+    ".dumirc.ts",
+    "./src/**/*.ts",
+    "./src/**/*.tsx",
+    "./docs/**/*.tsx",
+    "./tests/**/*.tsx"
+  ]
+}
diff --git a/typings.d.ts b/typings.d.ts
new file mode 100644
index 000000000..71e0e9f4c
--- /dev/null
+++ b/typings.d.ts
@@ -0,0 +1,2 @@
+declare module '*.css';
+declare module '*.less';
diff --git a/update-example.js b/update-example.js
new file mode 100644
index 000000000..1924fffa6
--- /dev/null
+++ b/update-example.js
@@ -0,0 +1,29 @@
+/*
+  用于 dumi 改造使用，
+  可用于将 examples 的文件批量修改为 demo 引入形式，
+  其他项目根据具体情况使用。
+*/
+
+const fs = require('fs');
+const glob = require('glob');
+
+const paths = glob.sync('./docs/examples/*.jsx');
+
+paths.forEach(path => {
+  const name = path.split('/').pop().split('.')[0];
+  fs.writeFile(
+    `./docs/demo/${name}.md`,
+    `## ${name}
+
+<code src="../examples/${name}.jsx">
+`,
+    'utf8',
+    function(error) {
+      if(error){
+        console.log(error);
+        return false;
+      }
+      console.log(`${name} 更新成功~`);
+    }
+  )
+});
