Przeglądaj źródła

feat: [TreeSelect] support checkRelation #522 (#617)

* feat: [TreeSelect] support checkRelation #522

* chore: [TreeSelect] modify ts for updateState

Co-authored-by: chenyuling <[email protected]>
boomboomchen 3 lat temu
rodzic
commit
02783e4f09

+ 54 - 0
content/input/treeselect/index-en-US.md

@@ -771,6 +771,59 @@ class Demo extends React.Component {
 }
 ```
 
+### Checked RelationShip
+Version: >= 2.5.0
+
+In multiple, `checkRelation` can be used to set the type of node selection relationship, optional: 'related' (default), 'unRelated'. When the selection relationship is 'unRelated', it means that selections between nodes do not affect each other.
+
+```jsx live=true
+import React from 'react';
+import { TreeSelect } from '@douyinfe/semi-ui';
+() => {
+    const treeData = [
+        {
+            label: 'Asia',
+            value: 'Asia',
+            key: '0',
+            children: [
+                {
+                    label: 'China',
+                    value: 'China',
+                    key: '0-0',
+                    children: [
+                        {
+                            label: 'Beijing',
+                            value: 'Beijing',
+                            key: '0-0-0',
+                        },
+                        {
+                            label: 'Shanghai',
+                            value: 'Shanghai',
+                            key: '0-0-1',
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            label: 'North America',
+            value: 'North America',
+            key: '1',
+        }
+    ];
+    return (
+        <TreeSelect
+            defaultValue='Asia'
+            multiple
+            checkRelation='unRelated'
+            style={{ width: 300 }}
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            treeData={treeData}
+        />
+    );
+};
+```
+
 ### Virtualized TreeSelect
 If you need to render large sets of tree structured data, you could use virtualized tree. In virtualized mode, animation / motion is disabled for better performance. 
 
@@ -1180,6 +1233,7 @@ function Demo() {
 | arrowIcon|Customize the right drop-down arrow Icon, when the showClear switch is turned on and there is currently a selected value, hover will give priority to the clear icon| ReactNode | | 1.15.0|
 |autoAdjustOverflow|Whether the pop-up layer automatically adjusts the direction when it is obscured (only vertical direction is supported for the time being, and the inserted parent is body)|boolean | true| 0.34.0|
 | autoExpandParent | Toggle whether to expand parent nodes automatically | boolean | false | 0.34.0 |
+| checkRelation | In multiple, the relationship between the checked states of the nodes, optional: 'related'、'unRelated' | string | 'related' | 2.5.0 |
 | className                | Class name                                                                          | string                                                            | -           | -       |
 | clickToHide  | Whether to close the drop-down layer automatically when selecting, only works in single-selection mode  | boolean    | true | 1.5.0      |
 | defaultExpandAll    | Set whether to expand all nodes during initialization. And if the data (`treeData`) changes, this api cannot affect the expansion of the node. If you need this, you can use `expandAll`    | boolean                     | false   | 0.32.0 |

+ 54 - 0
content/input/treeselect/index.md

@@ -735,6 +735,59 @@ class Demo extends React.Component {
 }
 ```
 
+### 节点选中关系
+版本:>= 2.5.0
+
+多选时,可以使用 `checkRelation` 来设置节点之间选中关系的类型,可选:'related'(默认)、'unRelated'。当选中关系为 'unRelated' 时,意味着节点之间的选中互不影响。
+
+```jsx live=true
+import React from 'react';
+import { TreeSelect } from '@douyinfe/semi-ui';
+() => {
+    const treeData = [
+        {
+            label: '亚洲',
+            value: 'Asia',
+            key: '0',
+            children: [
+                {
+                    label: '中国',
+                    value: 'China',
+                    key: '0-0',
+                    children: [
+                        {
+                            label: '北京',
+                            value: 'Beijing',
+                            key: '0-0-0',
+                        },
+                        {
+                            label: '上海',
+                            value: 'Shanghai',
+                            key: '0-0-1',
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            label: '北美洲',
+            value: 'North America',
+            key: '1',
+        }
+    ];
+    return (
+        <TreeSelect
+            multiple
+            defaultValue='Asia'
+            checkRelation='unRelated'
+            style={{ width: 300 }}
+            dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
+            treeData={treeData}
+        />
+    );
+};
+```
+
 ### 虚拟化
 列表虚拟化,用于大量树节点的情况。开启后,动画效果将被关闭。
 
@@ -1160,6 +1213,7 @@ function Demo() {
 | arrowIcon|自定义右侧下拉箭头Icon,当showClear开关打开且当前有选中值时,hover会优先显示clear icon| ReactNode | | 1.15.0|
 | autoAdjustOverflow|浮层被遮挡时是否自动调整方向(暂时仅支持竖直方向,且插入的父级为 body)|boolean | true| 0.34.0|
 | autoExpandParent | 是否自动展开父节点 | boolean | false | 0.34.0 |
+| checkRelation | 多选时,节点之间选中状态的关系,可选:'related'、'unRelated' | string | 'related' | 2.5.0 |
 | className | 选择框的 `className` 属性 | string | - | - |
 | clickToHide  | 选择后是否自动关闭下拉弹层,仅单选模式有效  | boolean    | true | 1.5.0      |
 | defaultExpandAll | 设置在初始化时是否展开所有节点。而如果后续数据(`treeData`)发生改变,这个 api 是无法影响节点的展开情况的,如果有这个需要可以使用 `expandAll` | boolean | false | 0.32.0 |

+ 89 - 41
packages/semi-foundation/treeSelect/foundation.ts

@@ -31,9 +31,9 @@ export type ValidateStatus = 'error' | 'warning' | 'default';
 export type Size = 'small' | 'large' | 'default';
 
 export type BasicRenderSelectedItemInMultiple = (
-    treeNode: BasicTreeNodeData, 
+    treeNode: BasicTreeNodeData,
     otherProps: { index: number | string; onClose: (tagContent: any, e: any) => void }
-)=> {
+) => {
     isRenderInTag: boolean;
     content: any;
 };
@@ -95,6 +95,7 @@ export interface BasicTreeSelectProps extends Pick<BasicTreeProps,
 | 'expandAll'
 | 'disableStrictly'
 | 'aria-label'
+| 'checkRelation'
 > {
     motion?: Motion;
     mouseEnterDelay?: number;
@@ -156,6 +157,7 @@ export interface BasicTreeSelectInnerData extends Pick<BasicTreeInnerData,
 | 'disabledKeys'
 | 'loadedKeys'
 | 'loadingKeys'
+| 'realCheckedKeys'
 > {
     inputTriggerFocus: boolean;
     isOpen: boolean;
@@ -171,7 +173,7 @@ export interface TreeSelectAdapter<P = Record<string, any>, S = Record<string, a
     registerClickOutsideHandler: (cb: (e: any) => void) => void;
     unregisterClickOutsideHandler: () => void;
     rePositionDropdown: () => void;
-    updateState: (states: Pick<S, keyof S>) => void;
+    updateState: (states: Partial<BasicTreeSelectInnerData>) => void;
     notifySelect: (selectedKeys: string, selected: boolean, selectedNode: BasicTreeNodeData) => void;
     notifySearch: (input: string) => void;
     cacheFlattenNodes: (bool: boolean) => void;
@@ -272,6 +274,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             selectedKeys = [],
             checkedKeys = new Set([]),
             halfCheckedKeys = new Set([]),
+            realCheckedKeys = new Set([]),
             keyEntities = {},
             filteredKeys = new Set([]),
             inputValue = '',
@@ -280,19 +283,29 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             filteredExpandedKeys = new Set([]),
             disabledKeys = new Set([]),
         } = this.getStates();
-        const { treeNodeFilterProp } = this.getProps();
+        const { treeNodeFilterProp, checkRelation } = this.getProps();
         const entity = keyEntities[key];
         const notExist = !entity;
         if (notExist) {
             return null;
         }
+        // if checkRelation is invalid, the checked status of node will be false
+        let realChecked = false;
+        let realHalfChecked = false;
+        if (checkRelation === 'related') {
+            realChecked = checkedKeys.has(key);
+            realHalfChecked = halfCheckedKeys.has(key);
+        } else if (checkRelation === 'unRelated') {
+            realChecked = realCheckedKeys.has(key);
+            realHalfChecked = false;
+        }
         const isSearching = Boolean(inputValue);
         const treeNodeProps: BasicTreeNodeProps = {
             eventKey: key,
             expanded: isSearching ? filteredExpandedKeys.has(key) : expandedKeys.has(key),
             selected: selectedKeys.includes(key),
-            checked: checkedKeys.has(key),
-            halfChecked: halfCheckedKeys.has(key),
+            checked: realChecked,
+            halfChecked: realHalfChecked,
             pos: String(entity ? entity.pos : ''),
             level: entity.level,
             filtered: filteredKeys.has(key),
@@ -327,7 +340,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             if (!this._isLoadControlled()) {
                 this._adapter.updateState({
                     loadedKeys: newLoadedKeys,
-                } as any);
+                });
             }
             this._adapter.setState({
                 loadingKeys: newLoadingKeys,
@@ -345,8 +358,13 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
 
     _notifyMultipleChange(key: string[], e: any) {
         const { keyEntities } = this.getStates();
-        const { leafOnly } = this.getProps();
-        const keyList = normalizeKeyList(key, keyEntities, leafOnly);
+        const { leafOnly, checkRelation } = this.getProps();
+        let keyList = [];
+        if (checkRelation === 'related') {
+            keyList = normalizeKeyList(key, keyEntities, leafOnly);
+        } else if (checkRelation === 'unRelated') {
+            keyList = key as string[];
+        }
         const nodes = keyList.map(i => keyEntities[i].data);
         if (this.getProp('onChangeWithObject')) {
             this._adapter.notifyChangeWithObject(nodes, e);
@@ -402,7 +420,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         this._adapter.unregisterClickOutsideHandler();
         this._notifyBlur(e);
         if (this.getProp('motionExpand')) {
-            this._adapter.updateState({ motionKeys: new Set([]) } as any);
+            this._adapter.updateState({ motionKeys: new Set([]) });
         }
     }
 
@@ -442,7 +460,8 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
                 selectedKeys: [],
                 checkedKeys: new Set(),
                 halfCheckedKeys: new Set(),
-            } as any);
+                realCheckedKeys: new Set([]),
+            });
         }
         // When triggerSearch, clicking the clear button will trigger to clear Input
         if (filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER) {
@@ -466,18 +485,29 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
     }
 
     removeTag(eventKey: BasicTreeNodeData['key']) {
-        const { disableStrictly } = this.getProps();
-        const { keyEntities, disabledKeys } = this.getStates();
+        const { disableStrictly, checkRelation } = this.getProps();
+        const { keyEntities, disabledKeys, realCheckedKeys } = this.getStates();
         const item = keyEntities[eventKey].data;
         if (item.disabled || (disableStrictly && disabledKeys.has(eventKey))) {
             return;
         }
-        const { checkedKeys, halfCheckedKeys } = this.calcCheckedKeys(eventKey, false);
-        this._notifyChange([...checkedKeys], null);
-        if (!this._isControlledComponent()) {
-            this._adapter.updateState({ checkedKeys, halfCheckedKeys } as any);
-            this._adapter.rePositionDropdown();
+        if (checkRelation === 'unRelated') {
+            const newRealCheckedKeys = new Set(realCheckedKeys);
+            newRealCheckedKeys.delete(eventKey);
+            this._notifyChange([...newRealCheckedKeys], null);
+            if (!this._isControlledComponent()) {
+                this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys } as any);
+                this._adapter.rePositionDropdown();
+            }
+        } else if (checkRelation === 'related') {
+            const { checkedKeys, halfCheckedKeys } = this.calcCheckedKeys(eventKey, false);
+            this._notifyChange([...checkedKeys], null);
+            if (!this._isControlledComponent()) {
+                this._adapter.updateState({ checkedKeys, halfCheckedKeys });
+                this._adapter.rePositionDropdown();
+            }
         }
+
         this._adapter.notifySelect(eventKey, false, item);
 
         // reposition dropdown when selected values change
@@ -498,7 +528,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             filteredKeys: new Set([]),
             filteredExpandedKeys: new Set(expandedOptsKeys),
             filteredShownKeys: new Set([])
-        } as any);
+        });
     }
 
     handleInputChange(sugInput: string) {
@@ -534,7 +564,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             filteredKeys: new Set(filteredOptsKeys),
             filteredExpandedKeys: new Set(expandedOptsKeys),
             filteredShownKeys,
-        } as any);
+        });
     }
 
     handleNodeSelect(e: any, treeNode: BasicTreeNodeProps) {
@@ -559,7 +589,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
             selectedKeys = [eventKey];
             this._notifyChange(eventKey, e);
             if (!this._isControlledComponent()) {
-                this._adapter.updateState({ selectedKeys } as any);
+                this._adapter.updateState({ selectedKeys });
             }
         }
         if (clickToHide && (this._isSelectToClose() || !data.children)) {
@@ -581,24 +611,42 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
     }
 
     handleMultipleSelect(e: any, treeNode: BasicTreeNodeProps) {
-        const { searchPosition, disableStrictly } = this.getProps();
-        const { inputValue } = this.getStates();
+        const { searchPosition, disableStrictly, checkRelation } = this.getProps();
+        const { inputValue, realCheckedKeys } = this.getStates();
         const { checked, eventKey, data } = treeNode;
-        const targetStatus = disableStrictly ?
-            this.calcChekcedStatus(!checked, eventKey) :
-            !checked;
-
-        const { checkedKeys, halfCheckedKeys } = disableStrictly ?
-            this.calcNonDisabedCheckedKeys(eventKey, targetStatus) :
-            this.calcCheckedKeys(eventKey, targetStatus);
-        this._adapter.notifySelect(eventKey, targetStatus, data);
-        this._notifyChange([...checkedKeys], e);
-        if (searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue !== '') {
-            this._adapter.updateState({ inputValue: '' } as any);
+        if (checkRelation === 'related') {
+            const targetStatus = disableStrictly ?
+                this.calcChekcedStatus(!checked, eventKey) :
+                !checked;
+
+            const { checkedKeys, halfCheckedKeys } = disableStrictly ?
+                this.calcNonDisabedCheckedKeys(eventKey, targetStatus) :
+                this.calcCheckedKeys(eventKey, targetStatus);
+            this._adapter.notifySelect(eventKey, targetStatus, data);
+            this._notifyChange([...checkedKeys], e);
+            if (!this._isControlledComponent()) {
+                this._adapter.updateState({ checkedKeys, halfCheckedKeys });
+                this._adapter.rePositionDropdown();
+            }
+        } else if (checkRelation === 'unRelated') {
+            const newRealCheckedKeys: Set<string> = new Set(realCheckedKeys);
+            let targetStatus: boolean;
+            if (realCheckedKeys.has(eventKey)) {
+                newRealCheckedKeys.delete(eventKey);
+                targetStatus = false;
+            } else {
+                newRealCheckedKeys.add(eventKey);
+                targetStatus = true;
+            }
+            this._adapter.notifySelect(eventKey, targetStatus, data);
+            this._notifyChange([...newRealCheckedKeys], e);
+            if (!this._isControlledComponent()) {
+                this._adapter.updateState({ realCheckedKeys: newRealCheckedKeys });
+                this._adapter.rePositionDropdown();
+            }
         }
-        if (!this._isControlledComponent()) {
-            this._adapter.updateState({ checkedKeys, halfCheckedKeys } as any);
-            this._adapter.rePositionDropdown();
+        if (searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue !== '') {
+            this._adapter.updateState({ inputValue: '' });
         }
     }
 
@@ -659,7 +707,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
                 motionType,
             };
 
-            this._adapter.updateState(newState as any);
+            this._adapter.updateState(newState);
         }
 
         this._adapter.notifyExpand(filteredExpandedKeys, {
@@ -703,7 +751,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
                 motionKeys: new Set(motionKeys),
                 motionType,
             };
-            this._adapter.updateState(newState as any);
+            this._adapter.updateState(newState);
         }
 
         this._adapter.notifyExpand(expandedKeys, {
@@ -734,7 +782,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
     handleInputTriggerBlur() {
         this._adapter.updateState({
             inputTriggerFocus: false
-        } as any);
+        });
     }
 
     /**
@@ -744,7 +792,7 @@ export default class TreeSelectFoundation<P = Record<string, any>, S = Record<st
         this.clearInput();
         this._adapter.updateState({
             inputTriggerFocus: true
-        } as any);
+        });
     }
 
     setLoadKeys(data: BasicTreeNodeData, resolve: (value?: any) => void) {

+ 94 - 0
packages/semi-ui/treeSelect/__test__/treeMultiple.test.js

@@ -678,4 +678,98 @@ describe('TreeSelect', () => {
         expect(treeSelect.find(`.${BASE_CLASS_PREFIX}-tree-option-highlight`).at(1).instance().textContent).toEqual('北');
 
     });
+
+    it('unRelated', () => {
+        const spyOnChange = sinon.spy(() => { });
+        const tree = getTreeSelect({
+            defaultExpandAll: true,
+            onChange: spyOnChange,
+            checkRelation: 'unRelated',
+        });
+        const nodelevel2 = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`);
+        const selectedNode = nodelevel2.at(0);
+        selectedNode.simulate('click');
+        expect(spyOnChange.calledOnce).toBe(true);
+        expect(spyOnChange.calledWithMatch(['Zhongguo'])).toEqual(true);
+        // Note: selectedNode cannot be used directly here. selectedNode is the original node in the unselected state
+        expect(
+            tree
+            .find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`)
+            .at(0)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-checked`)
+        ).toEqual(true);
+        const nodelevel3 = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`);
+        expect(
+            nodelevel3
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);
+        expect(
+            nodelevel3
+            .at(1)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);  
+    });
+
+    it('unRelated + value', () => {
+        const tree = getTreeSelect({
+            defaultExpandAll: true,
+            checkRelation: 'unRelated',
+            value: 'Zhongguo'
+        });
+        expect(
+            tree
+            .find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`)
+            .at(0)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-checked`)
+        ).toEqual(true);
+        const nodelevel3 = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`);
+        expect(
+            nodelevel3
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);
+        expect(
+            nodelevel3
+            .at(1)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);  
+    });
+
+    it('unRelated + defaultValue', () => {
+        const tree = getTreeSelect({
+            defaultExpandAll: true,
+            checkRelation: 'unRelated',
+            defaultValue: 'Zhongguo'
+        });
+        expect(
+            tree
+            .find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`)
+            .at(0)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-checked`)
+        ).toEqual(true);
+        const nodelevel3 = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-3`);
+        expect(
+            nodelevel3
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);
+        expect(
+            nodelevel3
+            .at(1)
+            .exists(`.${BASE_CLASS_PREFIX}-checkbox-unChecked` )
+        ).toEqual(true);  
+    });
+
+    it('unRelated + onSelect', () => {
+        const spyOnSelect = sinon.spy(() => { });
+        const tree = getTreeSelect({
+            defaultExpandAll: true,
+            onSelect: spyOnSelect,
+            checkRelation: 'unRelated',
+        });
+        const nodelevel2 = tree.find(`.${BASE_CLASS_PREFIX}-tree-option.${BASE_CLASS_PREFIX}-tree-option-level-2`);
+        const selectedNode = nodelevel2.at(0);
+        selectedNode.simulate('click');
+        expect(spyOnSelect.calledOnce).toBe(true);
+        // onSelect first args is key, not value
+        expect(spyOnSelect.calledWithMatch('zhongguo')).toEqual(true);
+    });
 })

+ 242 - 0
packages/semi-ui/treeSelect/_story/treeSelect.stories.js

@@ -1165,3 +1165,245 @@ export const DisabledStrictly = () => (
 DisabledStrictly.story = {
   name: 'disabledStrictly',
 };
+
+
+export const CheckRelationDemo = () => {
+  const treeData = [
+    {
+        label: 'Asia',
+        value: 'Asia',
+        key: '0',
+        children: [
+            {
+                label: 'China',
+                value: 'China',
+                key: '0-0',
+                children: [
+                    {
+                        label: 'Beijing',
+                        value: 'Beijing',
+                        key: '0-0-0',
+                    },
+                    {
+                        label: 'Shanghai',
+                        value: 'Shanghai',
+                        key: '0-0-1',
+                    },
+                    {
+                        label: 'Chengdu',
+                        value: 'Chengdu',
+                        key: '0-0-2',
+                    },
+                ],
+            },
+            {
+                label: 'Japan',
+                value: 'Japan',
+                key: '0-1',
+                children: [
+                    {
+                        label: 'Osaka',
+                        value: 'Osaka',
+                        key: '0-1-0'
+                    }
+                ]
+            },
+        ],
+    },
+    {
+        label: 'North America',
+        value: 'North America',
+        key: '1',
+        children: [
+            {
+                label: 'United States',
+                value: 'United States',
+                key: '1-0'
+            },
+            {
+                label: 'Canada',
+                value: 'Canada',
+                key: '1-1'
+            }
+        ]
+    }
+  ];
+  const [value, setValue] = useState('China');
+  const [value2, setValue2] = useState();
+  const [value3, setValue3] = useState();
+  const style = {
+    width: 300,
+  };
+  const dropdownStyle = {
+    maxHeight: 400,
+    overflow: 'auto'
+  };
+  const handleChange = value => {
+    console.log(value);
+    setValue(value);
+  };
+  const handleChange2 = value => {
+    console.log(value);
+    setValue2(value);
+  };
+  const handleChange3 = value => {
+    console.log(value);
+    setValue3(value);
+  };
+  return (
+    <>
+      <div>checkRelation='unRelated'</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + maxTagCount=2</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        maxTagCount={2}
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + maxTagCount=2 + 开启搜索</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        maxTagCount={2}
+        filterTreeNode
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + maxTagCount=2 + 开启搜索 + searchBox in trigger</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        maxTagCount={2}
+        filterTreeNode
+        checkRelation='unRelated'
+        searchPosition='trigger'
+        defaultExpandAll
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + 中国节点为 disabled</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeDataWithoutValue}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + 中国节点为 disabled + 严格禁用</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeDataWithoutValue}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        disableStrictly
+        style={style}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + defaultValue 为 China</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        defaultValue='China'
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + defaultValue 为 China + 开启搜索</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        filterTreeNode
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        defaultValue='China'
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + defaultValue 为 China + 开启搜索 + searchBox in trigger + showClear</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        filterTreeNode
+        showClear
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        searchPosition='trigger'
+        defaultValue='China'
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + 受控 + value 初始为 China</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        value={value}
+        onChange={handleChange}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + 受控 + onChangeWithObject</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        value={value2}
+        onChangeWithObject
+        onChange={handleChange2}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + 受控 + leafOnly,此时 leafOnly 失效</div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        leafOnly
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        value={value3}
+        onChange={handleChange3}
+      />
+      <br /><br />
+      <div>checkRelation='unRelated' + onSelect </div>
+      <TreeSelect
+        dropdownStyle={dropdownStyle}
+        treeData={treeData}
+        multiple
+        checkRelation='unRelated'
+        defaultExpandAll
+        style={style}
+        onSelect={(value,status,node)=>console.log('select', value, status, node)}
+      />
+    </>
+  );
+};

+ 72 - 40
packages/semi-ui/treeSelect/index.tsx

@@ -84,22 +84,22 @@ export type RenderSelectedItemInMultiple = (
 export type RenderSelectedItem = RenderSelectedItemInSingle | RenderSelectedItemInMultiple;
 
 export type OverrideCommonProps =
-'renderFullLabel'
-| 'renderLabel'
-| 'defaultValue'
-| 'emptyContent'
-| 'filterTreeNode'
-| 'style'
-| 'treeData'
-| 'value'
-| 'onExpand';
+    'renderFullLabel'
+    | 'renderLabel'
+    | 'defaultValue'
+    | 'emptyContent'
+    | 'filterTreeNode'
+    | 'style'
+    | 'treeData'
+    | 'value'
+    | 'onExpand';
 
 /**
 * Type definition description:
 * TreeSelectProps inherits some properties from BasicTreeSelectProps (from foundation) and TreeProps (from semi-ui-react).
 */
 // eslint-disable-next-line max-len
-export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideCommonProps | 'validateStatus' | 'searchRender'>, Pick<TreeProps, OverrideCommonProps>{
+export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideCommonProps | 'validateStatus' | 'searchRender'>, Pick<TreeProps, OverrideCommonProps> {
     'aria-describedby'?: React.AriaAttributes['aria-describedby'];
     'aria-errormessage'?: React.AriaAttributes['aria-errormessage'];
     'aria-invalid'?: React.AriaAttributes['aria-invalid'];
@@ -146,10 +146,10 @@ export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideComm
 }
 
 export type OverrideCommonState =
-'keyEntities'
-| 'treeData'
-| 'disabledKeys'
-| 'flattenNodes';
+    'keyEntities'
+    | 'treeData'
+    | 'disabledKeys'
+    | 'flattenNodes';
 
 // eslint-disable-next-line max-len
 export interface TreeSelectState extends Omit<BasicTreeSelectInnerData, OverrideCommonState | 'prevProps'>, Pick<TreeState, OverrideCommonState> {
@@ -242,7 +242,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         outerBottomSlot: PropTypes.node,
         outerTopSlot: PropTypes.node,
         onVisibleChange: PropTypes.func,
-        expandAction: PropTypes.oneOf(['click' as const, 'doubleClick'  as const, false as const]),
+        expandAction: PropTypes.oneOf(['click' as const, 'doubleClick' as const, false as const]),
         searchPosition: PropTypes.oneOf([strings.SEARCH_POSITION_DROPDOWN, strings.SEARCH_POSITION_TRIGGER]),
         clickToHide: PropTypes.bool,
         renderLabel: PropTypes.func,
@@ -251,6 +251,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         optionListStyle: PropTypes.object,
         searchRender: PropTypes.oneOfType([PropTypes.func, PropTypes.bool]),
         renderSelectedItem: PropTypes.func,
+        checkRelation: PropTypes.string,
         'aria-label': PropTypes.string,
     };
 
@@ -280,6 +281,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         expandAction: false,
         clickToHide: true,
         searchAutoFocus: false,
+        checkRelation: 'related',
         'aria-label': 'TreeSelect'
     };
     inputRef: React.RefObject<typeof Input>;
@@ -308,6 +310,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             selectedKeys: [],
             checkedKeys: new Set(),
             halfCheckedKeys: new Set(),
+            realCheckedKeys: new Set([]),
             disabledKeys: new Set(),
             motionKeys: new Set([]),
             motionType: 'hide',
@@ -473,10 +476,14 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             }
 
             if (checkedKeyValues) {
-                const { checkedKeys, halfCheckedKeys } = calcCheckedKeys(checkedKeyValues, keyEntities);
+                if (props.checkRelation === 'unRelated') {
+                    newState.realCheckedKeys = new Set(checkedKeyValues);
+                } else if (props.checkRelation === 'related') {
+                    const { checkedKeys, halfCheckedKeys } = calcCheckedKeys(checkedKeyValues, keyEntities);
 
-                newState.checkedKeys = checkedKeys;
-                newState.halfCheckedKeys = halfCheckedKeys;
+                    newState.checkedKeys = checkedKeys;
+                    newState.halfCheckedKeys = halfCheckedKeys;
+                }
             }
         }
 
@@ -491,7 +498,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         }
 
         // ================ disableStrictly =================
-        if (treeData && props.disableStrictly) {
+        if (treeData && props.disableStrictly && props.checkRelation === 'related') {
             newState.disabledKeys = calcDisabledKeys(keyEntities);
         }
 
@@ -575,7 +582,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                     this.foundation.handleNodeLoad(loadedKeys, loadingKeys, data, resolve));
             },
             updateState: states => {
-                this.setState({ ...states });
+                this.setState({ ...states } as TreeSelectState);
             },
             openMenu: () => {
                 this.setState({ isOpen: true }, () => {
@@ -616,7 +623,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             toggleHovering: bool => {
                 this.setState({ isHovering: bool });
             },
-            updateInputFocus: bool => {} // eslint-disable-line
+            updateInputFocus: bool => { } // eslint-disable-line
         };
     }
 
@@ -676,24 +683,39 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         this.foundation.handleSelectionEnterPress(e);
     };
 
+    hasValue = (): boolean => {
+        const { multiple, checkRelation } = this.props;
+        const { realCheckedKeys, checkedKeys, selectedKeys } = this.state;
+        let hasValue = false;
+        if (multiple) {
+            if (checkRelation === 'related') {
+                hasValue = Boolean(checkedKeys.size);
+            } else if (checkRelation === 'unRelated') {
+                hasValue = Boolean(realCheckedKeys.size);
+            }
+        } else {
+            hasValue = Boolean(selectedKeys.length);
+        }
+        return hasValue;
+    }
+
     showClearBtn = () => {
-        const { searchPosition } = this.props;
-        const { inputValue } = this.state;
+        const { showClear, disabled, searchPosition } = this.props;
+        const { inputValue, isOpen, isHovering } = this.state;
         const triggerSearchHasInputValue = searchPosition === strings.SEARCH_POSITION_TRIGGER && inputValue;
-        const { showClear, disabled, multiple } = this.props;
-        const { selectedKeys, checkedKeys, isOpen, isHovering } = this.state;
-        const hasValue = multiple ? Boolean(checkedKeys.size) : Boolean(selectedKeys.length);
-        return showClear && (hasValue || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering);
+
+        return showClear && (this.hasValue() || triggerSearchHasInputValue) && !disabled && (isOpen || isHovering);
     };
 
     renderTagList = () => {
-        const { checkedKeys, keyEntities, disabledKeys } = this.state;
+        const { checkedKeys, keyEntities, disabledKeys, realCheckedKeys } = this.state;
         const {
             treeNodeLabelProp,
             leafOnly,
             disabled,
             disableStrictly,
             size,
+            checkRelation,
             renderSelectedItem: propRenderSelectedItem
         } = this.props;
         const renderSelectedItem = isFunction(propRenderSelectedItem) ?
@@ -702,7 +724,12 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 isRenderInTag: true,
                 content: get(item, treeNodeLabelProp, null)
             });
-        const renderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly);
+        let renderKeys = [];
+        if (checkRelation === 'related') {
+            renderKeys = normalizeKeyList([...checkedKeys], keyEntities, leafOnly);
+        } else if (checkRelation === 'unRelated') {
+            renderKeys = [...realCheckedKeys];
+        }
         const tagList: Array<React.ReactNode> = [];
         // eslint-disable-next-line @typescript-eslint/no-shadow
         renderKeys.forEach((key: TreeNodeData['key']) => {
@@ -778,15 +805,13 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             searchPosition,
             filterTreeNode,
         } = this.props;
-        const { selectedKeys, checkedKeys } = this.state;
-        const hasValue = multiple ? Boolean(checkedKeys.size) : Boolean(selectedKeys.length);
         const isTriggerPositionSearch = filterTreeNode && searchPosition === strings.SEARCH_POSITION_TRIGGER;
         // searchPosition = trigger
         if (isTriggerPositionSearch) {
             return multiple ? this.renderTagInput() : this.renderSingleTriggerSearch();
         }
         // searchPosition = dropdown and single seleciton
-        if (!multiple || !hasValue) {
+        if (!multiple || !this.hasValue()) {
             const renderText = this.foundation.getRenderTextInSingle();
             const spanCls = cls({
                 [`${prefixcls}-selection-placeholder`]: !renderText,
@@ -846,11 +871,11 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
         const clearCls = cls(`${prefixcls}-clearbtn`);
         if (showClearBtn) {
             return (
-                <div 
+                <div
                     role='button'
-                    tabIndex={0} 
-                    aria-label="Clear TreeSelect value" 
-                    className={clearCls} 
+                    tabIndex={0}
+                    aria-label="Clear TreeSelect value"
+                    className={clearCls}
                     onClick={this.handleClear}
                     onKeyPress={this.handleClearEnterPress}
                 >
@@ -962,7 +987,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 onKeyPress={this.handleSelectionEnterPress}
                 aria-invalid={this.props['aria-invalid']}
                 aria-errormessage={this.props['aria-errormessage']}
-                aria-label={this.props['aria-label']} 
+                aria-label={this.props['aria-label']}
                 aria-labelledby={this.props['aria-labelledby']}
                 aria-describedby={this.props['aria-describedby']}
                 aria-required={this.props['aria-required']}
@@ -1014,7 +1039,7 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             });
         if (isFunction(renderSelectedItem)) {
             const { content, isRenderInTag } = treeNodeLabelProp in item && item ?
-                (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: idx, onClose }):
+                (renderSelectedItem as RenderSelectedItemInMultiple)(item, { index: idx, onClose }) :
                 null;
             if (isRenderInTag) {
                 return <Tag {...tagProps}>{content}</Tag>;
@@ -1037,13 +1062,20 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             searchAutoFocus,
             placeholder,
             maxTagCount,
+            checkRelation,
         } = this.props;
         const {
             keyEntities,
             checkedKeys,
-            inputValue
+            inputValue,
+            realCheckedKeys,
         } = this.state;
-        const keyList = normalizeKeyList(checkedKeys, keyEntities, leafOnly);
+        let keyList = [];
+        if (checkRelation === 'related') {
+            keyList = normalizeKeyList(checkedKeys, keyEntities, leafOnly);
+        } else if (checkRelation === 'unRelated') {
+            keyList = [...realCheckedKeys];
+        }
         return (
             <TagInput
                 maxTagCount={maxTagCount}