Browse Source

feat: [Cascader] support leafOnly #256 (#405)

* feat: [Cascader] support leafOnly #256

* docs: [Cascader]refactor to hook demo

Co-authored-by: chenyuling <[email protected]>
Co-authored-by: pointhalo <[email protected]>
boomboomchen 3 years ago
parent
commit
0c4640c589

+ 129 - 67
content/input/cascader/index-en-US.md

@@ -1009,76 +1009,137 @@ class Demo extends React.Component {
 
 ### Auto Merge Value
 
-In the multi-selection (multiple=true) scenario, when we select the ancestor node, if we want the value not to include its corresponding descendant nodes, we can set it by `autoMergeValue`, and the default is true.
+In the multi-selection (multiple=true) scenario, when we select the ancestor node, if we want the value not to include its corresponding descendant nodes, we can set it by `autoMergeValue`, and the default is true. When `autoMergeValue` and `leafOnly` are turned on at the same time, the latter has a higher priority.
 
 ```jsx live=true
-import React from 'react';
+import React, { useState } from 'react';
 import { Cascader } from '@douyinfe/semi-ui';
 
-class Demo extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            value: ['impressionism','visualArts']
-        };
-    }
-    onChange(value) {
-        this.setState({value});
-    }
-    render() {
-        const treeData = [
-            {
-                label: 'Impressionism',
-                value: 'impressionism',
-                children: [
-                    {
-                        label: 'Visual Arts',
-                        value: 'visualArts',
-                        children: [
-                            {
-                                label: 'Claude Monet',
-                                value: 'Monet',
-                            },
-                            {
-                                label: 'Pierre-Auguste Renoir',
-                                value: 'Renoir',
-                            },
-                            {
-                                label: 'Édouard Manet',
-                                value: 'Manet',
-                            },
-                        ],
-                    },
-                    {
-                        label: 'Music',
-                        value: 'music',
-                        children: [
-                            {
-                                label: 'Claude Debussy',
-                                value: 'Debussy',
-                            },
-                            {
-                                label: 'Maurice Ravel',
-                                value: 'Ravel',
-                            }
-                        ]
-                    }
-                ],
-            }
-        ];
-        return (
-            <Cascader
-                style={{ width: 300 }}
-                treeData={treeData}
-                placeholder="Please select"
-                value={this.state.value}
-                multiple
-                autoMergeValue={false}
-                onChange={e => this.onChange(e)}
-            />
-        );
-    }
-}
+() => {
+    const [value, setValue] = useState(['impressionism','visualArts']);
+    const onChange = value => {
+        setValue(value);
+    };
+    const treeData = [
+        {
+            label: 'Impressionism',
+            value: 'impressionism',
+            children: [
+                {
+                    label: 'Visual Arts',
+                    value: 'visualArts',
+                    children: [
+                        {
+                            label: 'Claude Monet',
+                            value: 'Monet',
+                        },
+                        {
+                            label: 'Pierre-Auguste Renoir',
+                            value: 'Renoir',
+                        },
+                        {
+                            label: 'Édouard Manet',
+                            value: 'Manet',
+                        },
+                    ],
+                },
+                {
+                    label: 'Music',
+                    value: 'music',
+                    children: [
+                        {
+                            label: 'Claude Debussy',
+                            value: 'Debussy',
+                        },
+                        {
+                            label: 'Maurice Ravel',
+                            value: 'Ravel',
+                        }
+                    ]
+                }
+            ],
+        }
+    ];
+    return (
+        <Cascader
+            style={{ width: 300 }}
+            treeData={treeData}
+            placeholder="Please select"
+            value={value}
+            multiple
+            autoMergeValue={false}
+            onChange={e => onChange(e)}
+        />
+    );
+};
+```
+
+### Leaf Only
+version: >=2.2.0
+
+In multiple selection, you can set the value to include only leaf nodes by turning on leafOnly, that is, the displayed Tag and onChange parameter values only include value. 
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Cascader } from '@douyinfe/semi-ui';
+
+() => {
+    const [value, setValue] = useState(['impressionism','visualArts']);
+    const onChange = value => {
+        setValue(value);
+    };
+    const treeData = [
+        {
+            label: 'Impressionism',
+            value: 'impressionism',
+            children: [
+                {
+                    label: 'Visual Arts',
+                    value: 'visualArts',
+                    children: [
+                        {
+                            label: 'Claude Monet',
+                            value: 'Monet',
+                        },
+                        {
+                            label: 'Pierre-Auguste Renoir',
+                            value: 'Renoir',
+                        },
+                        {
+                            label: 'Édouard Manet',
+                            value: 'Manet',
+                        },
+                    ],
+                },
+                {
+                    label: 'Music',
+                    value: 'music',
+                    children: [
+                        {
+                            label: 'Claude Debussy',
+                            value: 'Debussy',
+                        },
+                        {
+                            label: 'Maurice Ravel',
+                            value: 'Ravel',
+                        }
+                    ]
+                }
+            ],
+        }
+    ];
+    return (
+        <Cascader
+            style={{ width: 300 }}
+            treeData={treeData}
+            placeholder="Please select"
+            value={value}
+            multiple
+            leafOnly
+            onChange={e => onChange(e)}
+        />
+    );
+};
 ```
 
 ### Dynamic Update of Data
@@ -1400,7 +1461,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 to automatically adjust the expansion direction of the dropdown for automatic adjustment of the expansion direction during edge occlusion | boolean | true | - |
-| autoMergeValue | Auto merge value. Specifically, after opening, when a parent node is selected, the value will not include the descendants of the node | boolean | true |  1.28.0 |
+| autoMergeValue | Auto merge value. Specifically, after opening, when a parent node is selected, the value will not include the descendants of the node. Does not support dynamic switching | boolean | true |  1.28.0 |
 | bottomSlot | bottom slot | ReactNode | - |  1.27.0 |
 | changeOnSelect     | Toggle whether non-leaf nodes are selectable                                                                                   | boolean                                                              | false                           | -       |
 | className          | ClassName                                                                                                                    | string                                                               | -                               | -       |
@@ -1416,6 +1477,7 @@ function Demo() {
 | filterTreeNode     | Set filter, the value of treeNodeFilterProp is used for searching                | ((inputValue: string, treeNodeString: string) => boolean) \| boolean | false                           | -       |
 | getPopupContainer | Specify the parent DOM, the drop-down box will be rendered into the DOM, the customization needs to set position: relative |() => HTMLElement|() => document.body|-|
 | insetLabel         | Prefix alias, used mainly in Form                                                                                            | ReactNode                                                            | -                               | 0.28.0  |
+| leafOnly         | When multiple selections, the set value only includes leaf nodes, that is, the displayed Tag and onChange value parameters only include leaf nodes. Does not support dynamic switching                             | boolean                                                            | false                               | 2.2.0  |
 | loadData | Load data asynchronously and the return value should be a promise | (selectOptions: TreeNode[]) => Promise< void > |-| 1.8.0|
 | max| In the case of multiple selections, the number of multiple selections is limited, and the onExceed callback will be triggered when max is exceeded | number |-|1.28.0|
 | maxTagCount| When multiple selections, the maximum number of labels to be displayed will be displayed in the form of +N after exceeding| number |-|1.28.0|

+ 131 - 67
content/input/cascader/index.md

@@ -990,79 +990,142 @@ class Demo extends React.Component {
 ### 自动合并 value
 版本: >=1.28.0
 
-在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 `autoMergeValue` 来设置,默认为 true。
+在多选(multiple=true)场景中,当我们选中祖先节点时,如果希望 value 不包含它对应的子孙节点,则可以通过 `autoMergeValue` 来设置,默认为 true。当 autoMergeValue 和 leafOnly 同时开启时,后者优先级更高。
 
 ```jsx live=true
-import React from 'react';
+import React, { useState } from 'react';
 import { Cascader } from '@douyinfe/semi-ui';
 
-class Demo extends React.Component {
-    constructor() {
-        super();
-        this.state = {
-            value: ['zhejiang','ningbo']
-        };
-    }
-    onChange(value) {
+() => {
+    const [value, setValue] = useState([]);
+    const onChange = value => {
         console.log(value);
-        this.setState({value});
-    }
-    render() {
-        const treeData = [
-            {
-                label: '浙江省',
-                value: 'zhejiang',
-                children: [
-                    {
-                        label: '杭州市',
-                        value: 'hangzhou',
-                        children: [
-                            {
-                                label: '西湖区',
-                                value: 'xihu',
-                            },
-                            {
-                                label: '萧山区',
-                                value: 'xiaoshan',
-                            },
-                            {
-                                label: '临安区',
-                                value: 'linan',
-                            },
-                        ],
-                    },
-                    {
-                        label: '宁波市',
-                        value: 'ningbo',
-                        children: [
-                            {
-                                label: '海曙区',
-                                value: 'haishu',
-                            },
-                            {
-                                label: '江北区',
-                                value: 'jiangbei',
-                            }
-                        ]
-                    },
-                ],
-            }
-        ];
-        return (
-            <Cascader
-                style={{ width: 300 }}
-                treeData={treeData}
-                placeholder="请选择所在地区"
-                value={this.state.value}
-                multiple
-                autoMergeValue={false}
-                onChange={e => this.onChange(e)}
-            />
-        );
-    }
-}
+        setValue(value);
+    };
+    const treeData = [
+        {
+            label: '浙江省',
+            value: 'zhejiang',
+            children: [
+                {
+                    label: '杭州市',
+                    value: 'hangzhou',
+                    children: [
+                        {
+                            label: '西湖区',
+                            value: 'xihu',
+                        },
+                        {
+                            label: '萧山区',
+                            value: 'xiaoshan',
+                        },
+                        {
+                            label: '临安区',
+                            value: 'linan',
+                        },
+                    ],
+                },
+                {
+                    label: '宁波市',
+                    value: 'ningbo',
+                    children: [
+                        {
+                            label: '海曙区',
+                            value: 'haishu',
+                        },
+                        {
+                            label: '江北区',
+                            value: 'jiangbei',
+                        }
+                    ]
+                },
+            ],
+        }
+    ];
+    return (
+        <Cascader
+            style={{ width: 300 }}
+            treeData={treeData}
+            placeholder="autoMergeValue 为 false"
+            value={value}
+            multiple
+            autoMergeValue={false}
+            onChange={e => onChange(e)}
+        />
+    );
+};
+```
+
+### 仅叶子节点
+版本: >=2.2.0
+
+在多选时,可以通过开启 leafOnly 来设置 value 只包含叶子节点,即显示的 Tag 和 onChange 的参数 value 只包含 value。
+
+```jsx live=true
+import React, { useState } from 'react';
+import { Cascader } from '@douyinfe/semi-ui';
+
+() => {
+    const [value, setValue] = useState([]);
+    const onChange = value => {
+        console.log(value);
+        setValue(value);
+    };
+    const treeData = [
+        {
+            label: '浙江省',
+            value: 'zhejiang',
+            children: [
+                {
+                    label: '杭州市',
+                    value: 'hangzhou',
+                    children: [
+                        {
+                            label: '西湖区',
+                            value: 'xihu',
+                        },
+                        {
+                            label: '萧山区',
+                            value: 'xiaoshan',
+                        },
+                        {
+                            label: '临安区',
+                            value: 'linan',
+                        },
+                    ],
+                },
+                {
+                    label: '宁波市',
+                    value: 'ningbo',
+                    children: [
+                        {
+                            label: '海曙区',
+                            value: 'haishu',
+                        },
+                        {
+                            label: '江北区',
+                            value: 'jiangbei',
+                        }
+                    ]
+                },
+            ],
+        }
+    ];
+    return (
+        <Cascader
+            style={{ width: 300 }}
+            treeData={treeData}
+            placeholder="开启 leafOnly"
+            value={value}
+            multiple
+            leafOnly
+            onChange={e => onChange(e)}
+        />
+    );
+};
 ```
 
+
 ### 动态更新数据
 
 ```jsx live=true hideInDSM
@@ -1384,7 +1447,7 @@ function Demo() {
 | ------------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | -------------------------------- | ------ |
 | arrowIcon     |   自定义右侧下拉箭头 Icon,当 showClear 开关打开且当前有选中值时,hover 会优先显示 clear icon                                                              | ReactNode                                                                          |                             | 1.15.0      |
 | autoAdjustOverflow | 是否自动调整下拉框展开方向,用于边缘遮挡时自动调整展开方向 | boolean | true | - |
-| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将不包括该节点的子孙节点 | boolean | true |  1.28.0 |
+| autoMergeValue | 设置自动合并 value。具体而言是,开启后,当某个父节点被选中时,value 将不包括该节点的子孙节点。不支持动态切换 | boolean | true |  1.28.0 |
 | bottomSlot | 底部插槽 | ReactNode | - |  1.27.0 |
 | changeOnSelect     | 是否允许选择非叶子节点                                                                   | boolean                                                                          | false                            | -      |
 | className          | 选择框的 className 属性                                                              | string                                                                           | -                                | -      |
@@ -1400,6 +1463,7 @@ function Demo() {
 | filterTreeNode     | 设置筛选,默认用 treeNodeFilterProp 的值作为要筛选的 TreeNode 的属性值 | ((inputValue: string, treeNodeString: string) => boolean) \| boolean | false                            | -      |
 | getPopupContainer | 指定父级 DOM,下拉框将会渲染至该 DOM 中,自定义需要设置 position: relative |() => HTMLElement|() => document.body|-|
 | insetLabel         | 前缀标签别名,主要用于 Form                                                          | ReactNode                                                                        | -                                | 0.28.0 |
+| leafOnly | 多选时设置 value 只包含叶子节点,即显示的 Tag 和 onChange 的 value 参数只包含叶子节点。不支持动态切换 | boolean | false |  2.2.0|
 | loadData | 异步加载数据,需要返回一个Promise | (selectOptions: TreeNode[]) => Promise< void > |- |  1.8.0|
 | max| 多选时,限制多选选中的数量,超出 max 后将触发 onExceed 回调 | number |-|1.28.0|
 | maxTagCount| 多选时,标签的最大展示数量,超出后将以 +N 形式展示| number |-|1.28.0|

+ 4 - 0
packages/semi-foundation/cascader/constants.ts

@@ -12,6 +12,10 @@ const strings = {
     IS_VALUE: 'isValue',
     SHOW_NEXT_BY_CLICK: 'click',
     SHOW_NEXT_BY_HOVER: 'hover',
+    /* Merge Type */
+    LEAF_ONLY_MERGE_TYPE: 'leafOnly',
+    AUTO_MERGE_VALUE_MERGE_TYPE: 'autoMergeValue',
+    NONE_MERGE_TYPE: 'none',
 } as const;
 
 const numbers = {};

+ 21 - 13
packages/semi-foundation/cascader/foundation.ts

@@ -14,8 +14,11 @@ import {
     convertDataToEntities,
     findKeysForValues,
     normalizedArr,
-    isValid
+    isValid,
+    calcMergeType
 } from './util';
+import { strings } from './constants';
+
 export interface BasicData {
     data: BasicCascaderData;
     disabled: boolean;
@@ -138,6 +141,7 @@ export interface BasicCascaderProps {
     topSlot?: any;
     showNext?: ShowNextType;
     disableStrictly?: boolean;
+    leafOnly?: boolean;
     enableLeafClick?: boolean;
     onClear?: () => void;
     triggerRender?: (props: BasicTriggerRenderProps) => any;
@@ -169,7 +173,7 @@ export interface BasicCascaderInnerData {
     isHovering: boolean;
     checkedKeys: Set<string>;
     halfCheckedKeys: Set<string>;
-    mergedCheckedKeys: Set<string>;
+    resolvedCheckedKeys: Set<string>;
     loadedKeys: Set<string>;
     loadingKeys: Set<string>;
     loading: boolean;
@@ -707,8 +711,8 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
 
     _handleMultipleSelect(item: BasicEntity | BasicData) {
         const { key } = item;
-        const { checkedKeys, keyEntities, mergedCheckedKeys } = this.getStates();
-        const { autoMergeValue, max, disableStrictly } = this.getProps();
+        const { checkedKeys, keyEntities, resolvedCheckedKeys } = this.getStates();
+        const { autoMergeValue, max, disableStrictly, leafOnly } = this.getProps();
         // prev checked status
         const prevCheckedStatus = checkedKeys.has(key);
         // next checked status
@@ -723,18 +727,22 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
             this.calcNonDisabedCheckedKeys(key, curCheckedStatus) :
             this.calcCheckedKeys(key, curCheckedStatus);
 
-        const curMergedCheckedKeys = new Set(normalizeKeyList(curCheckedKeys, keyEntities));
+        const mergeType = calcMergeType(autoMergeValue, leafOnly);
+        const isLeafOnlyMerge = mergeType === strings.LEAF_ONLY_MERGE_TYPE;
+        const isNoneMerge = mergeType === strings.NONE_MERGE_TYPE;
+
+        const curResolvedCheckedKeys = new Set(normalizeKeyList(curCheckedKeys, keyEntities, isLeafOnlyMerge));
 
-        const curRealCheckedKeys = autoMergeValue ?
-            curMergedCheckedKeys :
-            curCheckedKeys;
+        const curRealCheckedKeys = isNoneMerge
+            ? curCheckedKeys
+            : curResolvedCheckedKeys;
 
         if (isNumber(max)) {
-            if (autoMergeValue) {
+            if (!isNoneMerge) {
                 // When it exceeds max, the quantity is allowed to be reduced, and no further increase is allowed
-                if (mergedCheckedKeys.size < curMergedCheckedKeys.size && curMergedCheckedKeys.size > max) {
+                if (resolvedCheckedKeys.size < curResolvedCheckedKeys.size && curResolvedCheckedKeys.size > max) {
                     const checkedEntities: BasicEntity[] = [];
-                    curMergedCheckedKeys.forEach(itemKey => {
+                    curResolvedCheckedKeys.forEach(itemKey => {
                         checkedEntities.push(keyEntities[itemKey]);
                     });
                     this._adapter.notifyOnExceed(checkedEntities);
@@ -756,7 +764,7 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
             this._adapter.updateStates({
                 checkedKeys: curCheckedKeys,
                 halfCheckedKeys: curHalfCheckedKeys,
-                mergedCheckedKeys: curMergedCheckedKeys
+                resolvedCheckedKeys: curResolvedCheckedKeys
             });
         }
 
@@ -872,7 +880,7 @@ export default class CascaderFoundation extends BaseFoundation<CascaderAdapter,
             newState.halfCheckedKeys = new Set([]);
             newState.selectedKeys = new Set([]);
             newState.activeKeys = new Set([]);
-            newState.mergedCheckedKeys = new Set([]);
+            newState.resolvedCheckedKeys = new Set([]);
             this._adapter.notifyChange([]);
         } else {
             // if click clearBtn when not searching, clear selected and active values as well

+ 13 - 0
packages/semi-foundation/cascader/util.ts

@@ -3,6 +3,7 @@ import {
     isUndefined,
     isEqual
 } from 'lodash';
+import { strings } from './constants';
 
 function getPosition(level: any, index: any) {
     return `${level}-${index}`;
@@ -79,4 +80,16 @@ export function findKeysForValues(value: any, keyEntities: any) {
         .filter((item: any) => isEqual(item.valuePath, valuePath))
         .map((item: any) => item.key);
     return res;
+}
+
+export function calcMergeType(autoMergeValue: boolean, leafOnly: boolean): string {
+    let mergeType: string;
+    if (leafOnly) {
+        mergeType = strings.LEAF_ONLY_MERGE_TYPE;
+    } else if (autoMergeValue) {
+        mergeType = strings.AUTO_MERGE_VALUE_MERGE_TYPE;
+    } else {
+        mergeType = strings.NONE_MERGE_TYPE;
+    }
+    return mergeType;
 }

+ 6 - 2
packages/semi-foundation/tree/treeUtil.ts

@@ -427,7 +427,7 @@ export function normalizedArr(val: any) {
 }
 
 export function normalizeKeyList(keyList: any, keyEntities: KeyEntities, leafOnly = false) {
-    let res: string[] = [];
+    const res: string[] = [];
     const keyListSet = new Set(keyList);
     if (!leafOnly) {
         keyList.forEach((key: string) => {
@@ -441,7 +441,11 @@ export function normalizeKeyList(keyList: any, keyEntities: KeyEntities, leafOnl
             res.push(key);
         });
     } else {
-        res = (Array.from(keyList) as string[]).filter(key => keyEntities[key] && !isValid(keyEntities[key].children));
+        keyList.forEach(key => {
+            if (keyEntities[key] && !isValid(keyEntities[key].children)) {
+                res.push(key);
+            }
+        });
     }
     return res;
 }

+ 78 - 0
packages/semi-ui/cascader/__test__/cascader.test.js

@@ -1149,4 +1149,82 @@ describe('Cascader', () => {
         expect(args2.value).toEqual(new Set(['0','0-0','0-0-1','0-0-0']));
         cascaderNoAutoMerge.unmount();
     });
+
+    it('autoMergeValue', () => {
+        const cascader = render({
+            multiple: true,
+            autoMergeValue: false,
+            defaultValue: 'Yazhou',
+        });
+        const tags = cascader.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags.length).toEqual(4);
+        cascader.unmount();
+
+        const cascaderAutoMerge = render({
+            multiple: true,
+            autoMergeValue: true,
+            defaultValue: 'Yazhou',
+        });
+        const tags2 = cascaderAutoMerge.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags2.length).toEqual(1);
+        expect(
+            tags2
+                .find(`.${BASE_CLASS_PREFIX}-tag-content`)
+                .getDOMNode()
+                .textContent
+        ).toEqual('亚洲');
+        cascaderAutoMerge.unmount();
+    });
+
+    it('leafOnly', () => {
+        /* autoMergeValue and leafOnly are both false */
+        const cascader = render({
+            multiple: true,
+            autoMergeValue: false,
+            leafOnly: false,
+            defaultValue: 'Yazhou',
+        });
+        const tags = cascader.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags.length).toEqual(4);
+        cascader.unmount();
+
+        /* autoMergeValue and leafOnly are both true */
+        const cascader2 = render({
+            multiple: true,
+            autoMergeValue: true,
+            leafOnly: true,
+            defaultValue: 'Yazhou',
+        });
+        const tags2 = cascader2.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags2.length).toEqual(2);
+        cascader2.unmount();
+
+        /* autoMergeValue is false, leafOnly is true */
+        const cascader3 = render({
+            multiple: true,
+            autoMergeValue: false,
+            leafOnly: true,
+            defaultValue: 'Yazhou',
+        });
+        const tags3 = cascader3.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags3.length).toEqual(2);
+        cascader3.unmount();
+
+        /* autoMergeValue is true, leafOnly is false */
+        const cascader4 = render({
+            multiple: true,
+            autoMergeValue: true,
+            leafOnly: false,
+            defaultValue: 'Yazhou',
+        });
+        const tags4 = cascader4.find(`.${BASE_CLASS_PREFIX}-cascader-selection .${BASE_CLASS_PREFIX}-tag`)
+        expect(tags4.length).toEqual(1);
+        expect(
+            tags4
+                .find(`.${BASE_CLASS_PREFIX}-tag-content`)
+                .getDOMNode()
+                .textContent
+        ).toEqual('亚洲');
+        cascader4.unmount();
+    });
 });

+ 88 - 0
packages/semi-ui/cascader/_story/cascader.stories.js

@@ -1261,3 +1261,91 @@ export const OnChangeWithObject = () => (
     />
   </>
 );
+
+export const LeafOnly = () => {
+  const [value, setValue] = useState([])
+  return (
+      <div>
+          <div>autoMergeValue=false,leafOnly=false</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              autoMergeValue={false}
+              leafOnly={false}
+              defaultValue={['zhejiang']}
+          />
+          <br />
+          <br />
+          <div>autoMergeValue=false,leafOnly=true, leafOnly生效</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              autoMergeValue={false}
+              leafOnly={true}
+              defaultValue={['zhejiang']}
+          />
+          <br />
+          <br />
+          <div>受控,autoMergeValue=false,leafOnly=true, leafOnly生效</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              onChange={v=>{
+                  console.log(v);
+                  setValue(v)
+              }}
+              autoMergeValue={false}
+              leafOnly={true}
+              value={value}
+          />
+          <br />
+          <br />
+          <div>受控 onChangeWithObject, autoMergeValue=false,leafOnly=true, leafOnly生效</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              onChange={v=>{
+                  console.log(v);
+                  setValue(v)
+              }}
+              onChangeWithObject
+              autoMergeValue={false}
+              leafOnly={true}
+              value={value}
+          />
+          <br />
+          <br />
+          <div>autoMergeValue=true,leafOnly=false</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              autoMergeValue={true}
+              leafOnly={false}
+              defaultValue={['zhejiang']}
+          />
+          <br />
+          <br />
+          <br />
+          <div>autoMergeValue=true,leafOnly=true</div>
+          <Cascader
+              style={{ width: 300 }}
+              treeData={treeData4}
+              placeholder="请选择所在地区"
+              multiple
+              autoMergeValue={true}
+              leafOnly={true}
+              defaultValue={['zhejiang']}
+          />
+      </div>
+  );
+}

+ 31 - 21
packages/semi-ui/cascader/index.tsx

@@ -14,10 +14,10 @@ import CascaderFoundation, {
 } from '@douyinfe/semi-foundation/cascader/foundation';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/cascader/constants';
 import { numbers as popoverNumbers } from '@douyinfe/semi-foundation/popover/constants';
-import { isEqual, isString, isEmpty, isFunction, isNumber, noop } from 'lodash';
+import { isEqual, isString, isEmpty, isFunction, isNumber, noop, flatten } from 'lodash';
 import '@douyinfe/semi-foundation/cascader/cascader.scss';
 import { IconClear, IconChevronDown } from '@douyinfe/semi-icons';
-import { findKeysForValues, convertDataToEntities } from '@douyinfe/semi-foundation/cascader/util';
+import { findKeysForValues, convertDataToEntities, calcMergeType } from '@douyinfe/semi-foundation/cascader/util';
 import { calcCheckedKeys, normalizeKeyList, calcDisabledKeys } from '@douyinfe/semi-foundation/tree/treeUtil';
 import ConfigContext from '../configProvider/context';
 import BaseComponent, { ValidateStatus } from '../_base/baseComponent';
@@ -149,10 +149,12 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         onLoad: PropTypes.func,
         loadedKeys: PropTypes.array,
         disableStrictly: PropTypes.bool,
+        leafOnly: PropTypes.bool,
         enableLeafClick: PropTypes.bool,
     };
 
     static defaultProps = {
+        leafOnly: false,
         arrowIcon: <IconChevronDown />,
         stopPropagation: true,
         motion: true,
@@ -187,6 +189,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     triggerRef: React.RefObject<HTMLDivElement>;
     optionsRef: React.RefObject<any>;
     clickOutsideHandler: any;
+    mergeType: string;
 
     constructor(props: CascaderProps) {
         super(props);
@@ -217,8 +220,8 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             checkedKeys: new Set([]),
             /* Key of half checked node, when multiple */
             halfCheckedKeys: new Set([]),
-            /* Auto merged checkedKeys, when multiple */
-            mergedCheckedKeys: new Set([]),
+            /* Auto merged checkedKeys or leaf checkedKeys, when multiple */
+            resolvedCheckedKeys: new Set([]),
             /* Keys of loaded item */
             loadedKeys: new Set(),
             /* Keys of loading item */
@@ -228,6 +231,7 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         };
         this.options = {};
         this.isEmpty = false;
+        this.mergeType = calcMergeType(props.autoMergeValue, props.leafOnly);
         this.inputRef = React.createRef();
         this.triggerRef = React.createRef();
         this.optionsRef = React.createRef();
@@ -348,7 +352,9 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             multiple,
             value,
             defaultValue,
-            onChangeWithObject
+            onChangeWithObject,
+            leafOnly,
+            autoMergeValue,
         } = props;
         const { prevProps } = prevState;
         let keyEntities = prevState.keyEntities || {};
@@ -404,21 +410,18 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
                     });
                     realKeys = formatKeys;
                 }
-                let checkedKeys = new Set([]);
-                let halfCheckedKeys = new Set([]);
-                realKeys.forEach(v => {
-                    const calRes = calcCheckedKeys(v, keyEntities);
-                    checkedKeys = new Set([...checkedKeys, ...calRes.checkedKeys]);
-                    halfCheckedKeys = new Set([...halfCheckedKeys, ...calRes.halfCheckedKeys]);
-                });
+                const calRes = calcCheckedKeys(flatten(realKeys as string[]), keyEntities);
+                const checkedKeys = new Set(calRes.checkedKeys);
+                const halfCheckedKeys = new Set(calRes.halfCheckedKeys);
                 // disableStrictly
                 if (props.disableStrictly) {
                     newState.disabledKeys = calcDisabledKeys(keyEntities);
                 }
+                const isLeafOnlyMerge = calcMergeType(autoMergeValue, leafOnly) === strings.LEAF_ONLY_MERGE_TYPE;
                 newState.prevProps = props;
                 newState.checkedKeys = checkedKeys;
                 newState.halfCheckedKeys = halfCheckedKeys;
-                newState.mergedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities));
+                newState.resolvedCheckedKeys = new Set(normalizeKeyList(checkedKeys, keyEntities, isLeafOnlyMerge));
             }
         }
         return newState;
@@ -495,7 +498,6 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         const {
             size,
             disabled,
-            autoMergeValue,
             placeholder,
             maxTagCount,
             showRestTagsPopover,
@@ -505,11 +507,13 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
             inputValue,
             checkedKeys,
             keyEntities,
-            mergedCheckedKeys
+            resolvedCheckedKeys
         } = this.state;
         const tagInputcls = cls(`${prefixcls}-tagInput-wrapper`);
         const tagValue: Array<Array<string>> = [];
-        const realKeys = autoMergeValue ? mergedCheckedKeys : checkedKeys;
+        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
+            ? checkedKeys
+            : resolvedCheckedKeys;
         [...realKeys].forEach(checkedKey => {
             if (!isEmpty(keyEntities[checkedKey])) {
                 tagValue.push(keyEntities[checkedKey].valuePath);
@@ -660,8 +664,10 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
 
     renderMultipleTags = () => {
         const { autoMergeValue, maxTagCount } = this.props;
-        const { checkedKeys, mergedCheckedKeys } = this.state;
-        const realKeys = autoMergeValue ? mergedCheckedKeys : checkedKeys;
+        const { checkedKeys, resolvedCheckedKeys } = this.state;
+        const realKeys = this.mergeType === strings.NONE_MERGE_TYPE
+            ? checkedKeys
+            : resolvedCheckedKeys;
         const displayTag: Array<ReactNode> = [];
         const hiddenTag: Array<ReactNode> = [];
         [...realKeys].forEach((checkedKey, idx) => {
@@ -731,11 +737,15 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
     };
 
     renderCustomTrigger = () => {
-        const { disabled, triggerRender, multiple, autoMergeValue } = this.props;
-        const { selectedKeys, inputValue, inputPlaceHolder, mergedCheckedKeys, checkedKeys } = this.state;
+        const { disabled, triggerRender, multiple } = this.props;
+        const { selectedKeys, inputValue, inputPlaceHolder, resolvedCheckedKeys, checkedKeys } = this.state;
         let realValue;
         if (multiple) {
-            realValue = autoMergeValue ? mergedCheckedKeys : checkedKeys;
+            if (this.mergeType === strings.NONE_MERGE_TYPE) {
+                realValue = checkedKeys;
+            } else {
+                realValue = resolvedCheckedKeys;
+            }
         } else {
             realValue = [...selectedKeys][0];
         }