瀏覽代碼

feat: add onSearch & onRemove method for triggerRender of select、treeSelect、cascader(#1516)

* feat: [TreeSelect][Cascader] add onSearch & onRemove method for triggerRender

* feat: [TreeSelect][Cascader] add onSearch & onRemove method for triggerRender

* feat: [TreeSelect][Cascader] add onSearch & onRemove method for triggerRender

* feat: select triggerRender add onSearch、onRemove

---------

Co-authored-by: pointhalo <[email protected]>
YyumeiZhang 2 年之前
父節點
當前提交
2cd5b7cd6d

+ 60 - 25
content/input/cascader/index-en-US.md

@@ -1620,24 +1620,28 @@ interface TriggerRenderProps {
      * The function used to update the value of the input box. You
      *  should call this function when the value of the Input component
      *  customized by triggerRender is updated to synchronize the
-     *  state with Cascader
+     *  state with Cascader, you need to set the filterTreeNode parameter
+     *  to non-false when use it, support since v2.32.0
      */
-    onChange: (inputValue: string) => void;
+    onSearch: (inputValue: string) => void;
     /* Function to clear the value */
     onClear: () => void;
     /* Placeholder of Cascader */
     placeholder?: string;
+    /* Used to delete a single item, the input parameter is value , 
+     * support since v2.32.0
+     */
+    onRemove: (value) => void
 }
 ```
 
 ```jsx live=true
 import React, { useState, useCallback, useMemo } from 'react';
-import { Cascader, Button } from '@douyinfe/semi-ui';
+import { Cascader, Button, Tag, TagInput } from '@douyinfe/semi-ui';
 import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
 
 
 function Demo() {
-    const [value, setValue] = useState([]);
     const treeData = useMemo(() => [
         {
             label: 'Asia',
@@ -1674,34 +1678,65 @@ function Demo() {
             ],
         }
     ], []);
-    const onChange = useCallback((val) => {
-        setValue(val);
-    }, []);
-    const onClear = useCallback(e => {
-        e && e.stopPropagation();
-        setValue([]);
-    }, []);
 
-    const closeIcon = useMemo(() => {
-        return value && value.length ? <IconClose onClick={onClear} /> : <IconChevronDown />;
-    }, [value]);
+    const closeIcon = useCallback((value, onClear) => {
+        return value ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+    }, []);
 
-    const triggerRender = ({ value: innerStateValue, placeholder, ...rest }) => {
+    const triggerRenderSingle = ({ value, placeholder, onClear, ...rest }) => {
         return (
-            <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
-                {value && value.length ? value.join('/') : placeholder}
+            <Button theme={'light'} icon={closeIcon(value, onClear)} iconPosition={'right'}>
+                {value && value.length > 0 ? getLabelFromValue(value) : placeholder}
             </Button>
         );
     };
 
+    const getLabelFromValue = useCallback((value) => {
+        const valueArr = value.split('-').map(item => Number(item));
+        let resultData = treeData;
+        valueArr.forEach((item, index) => {
+            resultData = index === 0 ? resultData[item] : resultData.children[item];
+        });
+        return resultData.label;
+    }, [treeData]);
+
+    const triggerRenderMultiple = useCallback((props) => {
+        const { value, onSearch, onRemove } = props;
+        const onCloseTag = (value, e, tagKey) => {
+            onRemove(tagKey);
+        };
+
+        const renderTagItem = (value) => {
+            const label = getLabelFromValue(value);
+            return <Tag tagKey={value} key={value} closable onClose={onCloseTag} style={{ marginLeft: 2 }}>{label}</Tag>;
+        };
+        
+        return (
+            <TagInput
+                value={Array.from(value)}
+                onInputChange={onSearch}
+                renderTagItem={renderTagItem}
+            />
+        );
+    }, []);
+
     return (
-        <Cascader
-            onChange={onChange}
-            value={value}
-            treeData={treeData}
-            placeholder='Custom Trigger'
-            triggerRender={triggerRender}
-        />
+        <>
+            <Cascader
+                treeData={treeData}
+                placeholder='Custom Trigger'
+                triggerRender={triggerRenderSingle}
+            />
+            <br />
+            <Cascader
+                triggerRender={triggerRenderMultiple}
+                multiple
+                filterTreeNode
+                treeData={treeData}
+                style={{ width: 300 }}
+                placeholder='Custom Trigger'
+            />
+        </>
     );
 }
 ```
@@ -1760,7 +1795,7 @@ function Demo() {
 | topSlot | top slot | ReactNode | - |  1.27.0 |
 | treeData | Render data. Refer to [CascaderData](#CascaderData)  for detailed formatting. | CascaderData[] |  []  | - |
 | treeNodeFilterProp | When searching, the input item filters the corresponding CascaderData property. | string | `label`   | - |
-| triggerRender | Method to create a custom trigger  | (triggerRenderData: object) => ReactNode | - | 0.34.0 |
+| triggerRender | Method to create a custom trigger  | (props: TriggerRenderProps) => ReactNode | - | 0.34.0 |
 | value | Selected value (controlled mode) | string\|number\|CascaderData\|(string\|number\|CascaderData)[][]  | - | -  |
 | validateStatus |The validation status of the trigger only affects the display style. Optional: default、error、warning | string | `default` | - |
 | zIndex | zIndex for dropdown menu | number | 1030 | - |

+ 57 - 27
content/input/cascader/index.md

@@ -1603,23 +1603,24 @@ interface TriggerRenderProps {
     /**
      * 用于更新 input 框值的函数,当你在 triggerRender 自定义的
      * Input 组件值更新时,你应该调用该函数,用于向 Cascader 内部
-     * 同步状态
+     * 同步状态, 使用时需要设置 filterTreeNode 参数非 false
      */
-    onChange: (inputValue: string) => void;
+    onSearch: (inputValue: string) => void;
     /* 用于清空值的函数 */
     onClear: () => void;
     /* Placeholder */
     placeholder?: string;
+    /* 用于删除单个 item , 入参为 value */
+    onRemove: (value) => void
 }
 ```
 
 ```jsx live=true
 import React, { useState, useCallback, useMemo } from 'react';
-import { Cascader, Button } from '@douyinfe/semi-ui';
+import { Cascader, Button, Tag, TagInput } from '@douyinfe/semi-ui';
 import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
 
 function Demo() {
-    const [value, setValue] = useState([]);
     const treeData = useMemo(() => [
         {
             label: '浙江省',
@@ -1660,36 +1661,65 @@ function Demo() {
             ],
         }
     ], []);
-    const onChange = useCallback((val) => {
-        setValue(val);
-    }, []);
-    const onClear = useCallback(e => {
-        e && e.stopPropagation();
-        setValue([]);
-    }, []);
 
-    const closeIcon = useMemo(() => {
-        return value && value.length ? <IconClose onClick={onClear} /> : <IconChevronDown />;
-    }, [value]);
+    const closeIcon = useCallback((value, onClear) => {
+        return value ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+    }, []);
 
-    const triggerRender = ({ value: innerStateValue, placeholder, ...rest }) => {
-        console.log(value);
-        console.log(rest);
+    const triggerRenderSingle = ({ value, placeholder, onClear, ...rest }) => {
         return (
-            <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
-                {value && value.length ? value.join('/') : placeholder}
+            <Button theme={'light'} icon={closeIcon(value, onClear)} iconPosition={'right'}>
+                {value && value.length > 0 ? getLabelFromValue(value) : placeholder}
             </Button>
         );
     };
 
+    const getLabelFromValue = useCallback((value) => {
+        const valueArr = value.split('-').map(item => Number(item));
+        let resultData = treeData;
+        valueArr.forEach((item, index) => {
+            resultData = index === 0 ? resultData[item] : resultData.children[item];
+        });
+        return resultData.label;
+    }, [treeData]);
+
+    const triggerRenderMultiple = useCallback((props) => {
+        const { value, onSearch, onRemove } = props;
+        const onCloseTag = (value, e, tagKey) => {
+            onRemove(tagKey);
+        };
+
+        const renderTagItem = (value) => {
+            const label = getLabelFromValue(value);
+            return <Tag tagKey={value} key={value} closable onClose={onCloseTag} style={{ marginLeft: 2 }}>{label}</Tag>;
+        };
+        
+        return (
+            <TagInput
+                value={Array.from(value)}
+                onInputChange={onSearch}
+                renderTagItem={renderTagItem}
+            />
+        );
+    }, []);
+
     return (
-        <Cascader
-            onChange={onChange}
-            value={value}
-            treeData={treeData}
-            placeholder='Custom Trigger'
-            triggerRender={triggerRender}
-        />
+        <>
+            <Cascader
+                treeData={treeData}
+                placeholder='Custom Trigger'
+                triggerRender={triggerRenderSingle}
+            />
+            <br />
+            <Cascader
+                triggerRender={triggerRenderMultiple}
+                multiple
+                filterTreeNode
+                treeData={treeData}
+                style={{ width: 300 }}
+                placeholder='Custom Trigger'
+            />
+        </>
     );
 }
 ```
@@ -1748,7 +1778,7 @@ function Demo() {
 | topSlot              | 顶部插槽                                                                                                                                  | ReactNode                                                                                 | -                              | 1.27.0 |
 | treeData             | 展示数据,具体属性参考 [CascaderData](#CascaderData)                                                                                       | CascaderData[]                                                                            | []                             | -      |
 | treeNodeFilterProp   | 搜索时输入项过滤对应的 CascaderData 属性                                                                                                  | string                                                                                    | `label`                        | -      |
-| triggerRender        | 自定义触发器渲染方法                                                                                                                      | (triggerRenderData: object) => ReactNode                                                  | -                              | 0.34.0 |
+| triggerRender        | 自定义触发器渲染方法                                                                                                                      | (props: TriggerRenderProps) => ReactNode                                                  | -                              | 0.34.0 |
 | validateStatus       | trigger 的校验状态,仅影响展示样式。可选: default、error、warning                                                                             | string                                                                                    | `default`                      | -      |
 | value                | (受控)选中的条目                                                                                                                          | string\|number\|CascaderData\|(string\|number\|CascaderData)[]                            | -                              | -      |
 | zIndex               | 下拉菜单的 zIndex                                                                                                                         | number                                                                                    | 1030                           | -      |

+ 3 - 2
content/input/select/index-en-US.md

@@ -1062,11 +1062,12 @@ If the default layout style of the selection box does not meet your needs, you c
 The parameters of triggerRender are as follows
 
 ```typescript
-interface triggerRenderProps {
+interface TriggerRenderProps {
   value: array<object> // All currently selected options
   inputValue: string; // The input value of the current input box
-  onChange: (inputValue: string) => void; // The function used to update the value of the input box. You should call this function when the value of the Input component you customize in triggerRender is updated to synchronize the state to the Select internal
+  onSearch: (inputValue: string) => void; // The function used to update the value of the input box. You should call this function when the value of the Input component you customize in triggerRender is updated to synchronize the state to the Select internal. props.filter needs to be true, support after v2.32
   onClear: () => void; // Function to clear the value
+  onRemove: (option: object) => void; // support after v2.32
   disabled: boolean; // Whether to disable Select
   placeholder: string; // Select placeholder
   componentProps: //All props passed to Select by users

+ 4 - 3
content/input/select/index.md

@@ -1126,14 +1126,15 @@ class VirtualizeDemo extends React.Component {
 triggerRender 入参如下
 
 ```typescript
-interface triggerRenderProps {
+interface TriggerRenderProps {
   value: array<object> // 当前所有已选中的options
   inputValue: string; // 当前input框的输入值
-  onChange: (inputValue: string) => void; // 用于更新 input框值的函数,当你在triggerRender自定义的Input组件值更新时你应该调用该函数,用于向Select内部同步状态
+  onSearch: (inputValue: string) => void; // 用于更新 input框值的函数,当你在triggerRender自定义的Input组件值更新时你应该调用该函数,用于向Select内部同步状态。注意 filter 需同时设为true, v2.32 提供
+  onRemove: (option: object) => void; // 用于移除单个已选项,option至少需带有 label、value 两项,v2.32提供
   onClear: () => void; // 用于清空值的函数
   disabled: boolean; // 是否禁用Select
   placeholder: string; // Select的placeholder
-  componentProps: // 所有用户传给Select的props
+  componentProps: object // 所有用户传给Select的props
 }
 ```
 

+ 41 - 21
content/input/treeselect/index-en-US.md

@@ -1217,13 +1217,24 @@ interface triggerRenderProps {
     inputValue: string;             // value of the input box
     onClear: e => void;             // onClear function
     placeholder: string;            // placeholder
+    /* The function called when deleting a single item, 
+     *   with the key of the item as an input parameter, 
+     *  supported from version v2.32.0
+     */
+    onRemove: key => void;
+    /* It is used to start the search when the value of the Input box is updated. 
+     * When you update the value of the Input component customized by triggerRender, 
+     * you should call this function to synchronize the state with the TreeSelect 
+     * internally. you need to set the filterTreeNode parameter to non-false when use it
+     * It is supported from v2.32.0
+     */
+    onSearch: inputValue => void;   
 }
 ```
 
 ```jsx live=true
 import React, { useState, useCallback, useMemo } from 'react';
-import { TreeSelect, Button } from '@douyinfe/semi-ui';
-import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
+import { TreeSelect, Button, Tag, TagInput } from '@douyinfe/semi-ui';
 
 function Demo() {
     const [value, setValue] = useState([]);
@@ -1258,31 +1269,40 @@ function Demo() {
             key: '1',
         }
     ], []);
-    const onChange = useCallback((val) => {
-        setValue(val);
-    }, []);
-    const onClear = useCallback(e => {
-        e && e.stopPropagation();
-        setValue([]);
+    
+    const onValueChange = useCallback((value) => {
+        console.log('onChange', value);
+    });
+
+    const renderTrigger = useCallback((props) => {
+        const { value, onSearch, onRemove } = props;
+        const tagInputValue = value.map(item => item.key);
+        const renderTagInMultiple = (key) => {
+            const label = value.find(item => item.key === key).label;
+            const onCloseTag = (value, e, tagKey) => {
+                onRemove(tagKey);
+            };
+            return <Tag style={{ marginLeft: 2 }} tagKey={key} key={key} onClose={onCloseTag} closable>{label}</Tag>;
+        };
+        return (
+            <TagInput
+                style={{ width: 250 }}
+                value={tagInputValue}
+                onInputChange={onSearch}
+                renderTagItem={renderTagInMultiple}
+            />
+        );
     }, []);
 
-    const closeIcon = useMemo(() => {
-        return value && value.length ? <IconClose onClick={onClear} /> : <IconChevronDown />;
-    }, [value]);
-
     return (
         <TreeSelect
-            onChange={onChange}
-            style={{ width: 300 }}
-            value={value}
+            triggerRender={renderTrigger}
+            filterTreeNode
             multiple
             treeData={treeData}
             placeholder='Custom Trigger'
-            triggerRender={({ placeholder }) => (
-                <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
-                    {value && value.length ? value.join(', ') : placeholder}
-                </Button>
-            )}
+            onChange={onValueChange}
+            style={{ width: 300 }}
         />
     );
 }
@@ -1435,7 +1455,7 @@ function Demo() {
 | treeData                 | Data for treeNodes                                                                  | TreeNodeData[]                                                  | \[]         | -       |
 | treeNodeFilterProp       | Property in a `TreeNodeData` used to search                                             | string                                                            | `label`     | -       |
 | treeNodeLabelProp        | Property in a `TreeNodeData` used to display                                            | string                                                            | `label`     | -       |
-| triggerRender | Method to create a custom trigger  | (TriggerProps) => ReactNode | - | 0.34.0 |
+| triggerRender | Method to create a custom trigger  | (props: TriggerRenderProps) => ReactNode | - | 0.34.0 |
 | validateStatus | Validate status,one of `warning`、`error`、 `default`, only affects the background color of the component | string | - | 0.32.0 |
 | value                    | Value data of current item, used when TreeSelect is a controlled component     | <ApiType detail='string \| number \| TreeNodeData \| (string \| number \| TreeNodeData)[]'>ValueType</ApiType>    | -           | -       |
 | virtualize | Efficiently rendering large lists, refer to Tree - VirtualizeObj. Motion is disabled when tree is rendered as virtualized list. | object | - | 0.32.0 |

+ 41 - 24
content/input/treeselect/index.md

@@ -1178,24 +1178,32 @@ import { TreeSelect } from '@douyinfe/semi-ui';
 triggerRender 入参如下:
 
 ```typescript
-interface triggerRenderProps {
+interface TriggerRenderProps {
     componentProps: TreeSelectProps;// 所有用户传给 TreeSelect 的 props
     disabled: boolean;              // 是否禁用 TreeSelect
     value: TreeNodeData[];              // 已选中的 node 的数据
     inputValue: string;             // 当前 input 框的输入值
     onClear: e => void;             // 用于清空值的函数
     placeholder: string;            // placeholder
+    /* 删除单个 item 时调用的函数,以 item 的 key 作为入参, 
+     * 从 v2.32.0 版本开始支持 
+    */
+    onRemove: key => void;          
+    /**
+     * 用于在 Input 框值更新时候启动搜索,当你在 triggerRender 自定义的
+     * Input 组件值更新时,你应该调用该函数,用于向 TreeSelect 内部
+     * 同步状态, 使用同时需要设置 filterTreeNode 参数非 false, 
+     * 从 v2.32.0 版本开始支持
+    */
+    onSearch: inputValue => void;   
 }
 ```
 
-
 ```jsx live=true
 import React, { useState, useCallback, useMemo } from 'react';
-import { TreeSelect, Button } from '@douyinfe/semi-ui';
-import { IconClose, IconChevronDown } from '@douyinfe/semi-icons';
+import { TreeSelect, Button, Tag, TagInput } from '@douyinfe/semi-ui';
 
 function Demo() {
-    const [value, setValue] = useState([]);
     const treeData = useMemo(() => [
         {
             label: '亚洲',
@@ -1227,31 +1235,40 @@ function Demo() {
             key: '1',
         }
     ], []);
-    const onChange = useCallback((val) => {
-        setValue(val);
-    }, []);
-    const onClear = useCallback(e => {
-        e && e.stopPropagation();
-        setValue([]);
-    }, []);
 
-    const closeIcon = useMemo(() => {
-        return value && value.length ? <IconClose onClick={onClear} /> : <IconChevronDown />;
-    }, [value]);
+    const onValueChange = useCallback((value) => {
+        console.log('onChange', value);
+    });
+
+    const renderTrigger = useCallback((props) => {
+        const { value, onSearch, onRemove } = props;
+        const tagInputValue = value.map(item => item.key);
+        const renderTagInMultiple = (key) => {
+            const label = value.find(item => item.key === key).label;
+            const onCloseTag = (value, e, tagKey) => {
+                onRemove(tagKey);
+            };
+            return <Tag style={{ marginLeft: 2 }} tagKey={key} key={key} onClose={onCloseTag} closable>{label}</Tag>;
+        };
+        return (
+            <TagInput
+                style={{ width: 250 }}
+                value={tagInputValue}
+                onInputChange={onSearch}
+                renderTagItem={renderTagInMultiple}
+            />
+        );
+    }, []);
 
     return (
         <TreeSelect
-            onChange={onChange}
-            style={{ width: 300 }}
-            value={value}
+            triggerRender={renderTrigger}
+            filterTreeNode
             multiple
             treeData={treeData}
             placeholder='Custom Trigger'
-            triggerRender={({ placeholder }) => (
-                <Button theme={'light'} icon={closeIcon} iconPosition={'right'}>
-                    {value && value.length ? value.join(',') : placeholder}
-                </Button>
-            )}
+            onChange={onValueChange}
+            style={{ width: 300 }}
         />
     );
 }
@@ -1418,7 +1435,7 @@ function Demo() {
 | treeData | `treeNodes` 数据,如果设置则不需要手动构造 `TreeNode` 节点(`key` 值在整个树范围内唯一) | TreeNodeData[] | \[] | - |
 | treeNodeFilterProp | 搜索时输入项过滤对应的 `TreeNodeData` 属性 | string | `label` | - |
 | treeNodeLabelProp | 作为显示的 `prop` 设置 | string | `label` | - |
-| triggerRender | 自定义触发器渲染方法  | ({ placeholder: string }) => ReactNode | - | 0.34.0 |
+| triggerRender | 自定义触发器渲染方法  | (props: TriggerRenderProps) => ReactNode | - | 0.34.0 |
 | validateStatus | 校验结果,可选 `warning`、`error`、 `default`(只影响样式背景色) | string | - | 0.32.0 |
 | value | 当前选中的节点的value值,传入该值时将作为受控组件 | <ApiType detail='string \| number \| TreeNodeData \| (string \| number \| TreeNodeData)[]'>ValueType</ApiType>| - | - |
 | virtualize | 列表虚拟化,用于大量树节点的情况,由 height, width, itemSize 组成,参考 Tree - Virtualize Object。开启后将关闭动画效果。 | object | - | 0.32.0 |

+ 8 - 1
packages/semi-foundation/cascader/foundation.ts

@@ -91,9 +91,16 @@ export interface BasicTriggerRenderProps {
      * should call this function when the value of the Input component
      * customized by triggerRender is updated to synchronize the state
      * with Cascader. */
+    onSearch: (inputValue: string) => void;
+    /* This function is the same as onSearch (supported since v2.32.0), 
+     * because this function was used before, and to align with TreeSelect, 
+     * use onSearch instead of onChange is more suitable, 
+     * onChange needs to be deleted in the next Major
+    */
     onChange: (inputValue: string) => void;
     /* Function to clear the value */
-    onClear: (e: any) => void
+    onClear: (e: any) => void;
+    onRemove: (key: string) => void
 }
 
 export interface BasicScrollPanelProps {

+ 3 - 1
packages/semi-foundation/treeSelect/foundation.ts

@@ -47,7 +47,9 @@ export interface BasicTriggerRenderProps {
     inputValue: string;
     placeholder: string;
     value: BasicTreeNodeData[];
-    onClear: (e: any) => void
+    onClear: (e: any) => void;
+    onSearch: (inputValue: string) => void;
+    onRemove: (value: string) => void
 }
 
 export type BasicOnChangeWithObject = (node: BasicTreeNodeData[] | BasicTreeNodeData, e: any) => void;

+ 106 - 2
packages/semi-ui/cascader/_story/cascader.stories.jsx

@@ -1,6 +1,7 @@
-import React, { useState, useCallback, useEffect, useRef } from 'react';
+import React, { useState, useCallback, useEffect, useRef, useMemo } from 'react';
 import CustomTrigger from './CustomTrigger';
-import { Button, Typography, Toast, Cascader, Checkbox } from '../../index';
+import { IconChevronDown, IconClose } from '@douyinfe/semi-icons';
+import { Button, Typography, Toast, Cascader, Checkbox, Input, Tag, TagInput } from '../../index';
 
 const { Text } = Typography;
 
@@ -1981,3 +1982,106 @@ export const setValueInSearch = () => {
       </div>
   );
 }
+
+export const TriggerAddMethods = () => {
+  const treeData = useMemo(() => [
+      {
+          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',
+                      }
+                  ]
+              },
+          ],
+      }
+  ], []);
+
+  const closeIcon = useCallback((value, onClear) => {
+      return value ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+  }, []);
+
+  const triggerRenderSingle = ({ value, placeholder, onClear, ...rest }) => {
+      return (
+          <Button theme={'light'} icon={closeIcon(value, onClear)} iconPosition={'right'}>
+              {value && value.length > 0 ? getLabelFromValue(value) : placeholder}
+          </Button>
+      );
+  };
+
+  const getLabelFromValue = useCallback((value) => {
+      const valueArr = value.split('-').map(item => Number(item));
+      let resultData = treeData;
+      valueArr.forEach((item, index) => {
+          resultData = index === 0 ? resultData[item] : resultData.children[item];
+      });
+      return resultData.label;
+  }, [treeData]);
+
+  const triggerRenderMultiple = useCallback((props) => {
+      const { value, onSearch, onRemove } = props;
+      const onCloseTag = (value, e, tagKey) => {
+          onRemove(tagKey);
+      };
+
+      const renderTagItem = (value) => {
+          const label = getLabelFromValue(value);
+          return <Tag tagKey={value} key={value} closable onClose={onCloseTag} style={{ marginLeft: 2 }}>{label}</Tag>
+      };
+      
+      return (
+          <TagInput
+              value={Array.from(value)}
+              onInputChange={onSearch}
+              renderTagItem={renderTagItem}
+          />
+      );
+  }, []);
+
+  return (
+      <>
+          <Cascader
+              treeData={treeData}
+              placeholder='Custom Trigger'
+              triggerRender={triggerRenderSingle}
+          />
+          <br />
+          <Cascader
+              triggerRender={triggerRenderMultiple}
+              multiple
+              filterTreeNode
+              treeData={treeData}
+              style={{ width: 300 }}
+              placeholder='Custom Trigger'
+          />
+      </>
+  );
+}

+ 7 - 0
packages/semi-ui/cascader/index.tsx

@@ -499,6 +499,11 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
         this.foundation.handleTagRemove(e, tagValuePath);
     };
 
+    handleRemoveByKey = (key) => {
+        const { keyEntities } = this.state;
+        this.handleTagRemove(null, keyEntities[key].valuePath);
+    }
+
     renderTagItem = (value: string | Array<string>, idx: number, type: string) => {
         const { keyEntities, disabledKeys } = this.state;
         const { size, disabled, displayProp, displayRender, disableStrictly } = this.props;
@@ -829,6 +834,8 @@ class Cascader extends BaseComponent<CascaderProps, CascaderState> {
                 triggerRender={triggerRender}
                 componentName={'Cascader'}
                 componentProps={{ ...this.props }}
+                onSearch={this.handleInputChange}
+                onRemove={this.handleRemoveByKey}
             />
         );
     };

+ 23 - 2
packages/semi-ui/select/index.tsx

@@ -37,7 +37,6 @@ import type { Locale } from '../locale/interface';
 import type { Position, TooltipProps } from '../tooltip';
 import type { Subtract } from 'utility-types';
 
-
 export type { OptionProps } from './option';
 export type { OptionGroupProps } from './optionGroup';
 export type { VirtualRowProps } from './virtualRow';
@@ -68,6 +67,25 @@ export interface optionRenderProps {
     [x: string]: any
 }
 
+export interface SelectedItemProps {
+    value: OptionProps['value'];
+    label: OptionProps['label'];
+    _show?: boolean;
+    _selected: boolean;
+    _scrollIndex?: number
+}
+
+export interface TriggerRenderProps {
+    value: SelectedItemProps[];
+    inputValue: string;
+    onSearch: (inputValue: string) => void;
+    onClear: () => void;
+    onRemove: (option: OptionProps) => void;
+    disabled: boolean;
+    placeholder: string;
+    componentProps: Record<string, any>
+}
+
 export interface selectMethod {
     clearInput?: () => void;
     selectAll?: () => void;
@@ -153,7 +171,7 @@ export type SelectProps = {
     onDeselect?: (value: SelectProps['value'], option: Record<string, any>) => void;
     onSelect?: (value: SelectProps['value'], option: Record<string, any>) => void;
     allowCreate?: boolean;
-    triggerRender?: (props?: any) => React.ReactNode;
+    triggerRender?: (props?: TriggerRenderProps) => React.ReactNode;
     onClear?: () => void;
     virtualize?: virtualListProps;
     onFocus?: (e: React.FocusEvent) => void;
@@ -1320,11 +1338,14 @@ class Select extends BaseComponent<SelectProps, SelectState> {
 
         const clear = clearIcon ? clearIcon : <IconClear />;
 
+        // semantics of onSearch are more in line with behavior, onChange is alias of onSearch, will be deprecate next major version
         const inner = useCustomTrigger ? (
             <Trigger
                 value={Array.from(selections.values())}
                 inputValue={inputValue}
                 onChange={this.handleInputChange}
+                onSearch={this.handleInputChange}
+                onRemove={(item) => this.foundation.removeTag(item)}
                 onClear={this.onClear}
                 disabled={disabled}
                 triggerRender={triggerRender}

+ 126 - 4
packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx

@@ -1,9 +1,9 @@
-import React, { useState, useMemo, useRef } from 'react';
-import { Icon, Input, Button, Form, Popover, Tag, Typography, CheckboxGroup } from '../../index';
+import React, { useState, useMemo, useRef, useCallback } from 'react';
+import { Icon, Input, Button, Form, Popover, Tag, Typography, CheckboxGroup, TagInput } from '../../index';
 import TreeSelect from '../index';
 import { flattenDeep } from 'lodash';
 import CustomTrigger from './CustomTrigger';
-import { IconCreditCard } from '@douyinfe/semi-icons';
+import { IconCreditCard, IconChevronDown, IconClose } from '@douyinfe/semi-icons';
 import { setFocusToPreviousMenuItem } from '@douyinfe/semi-foundation/utils/a11y';
 const TreeNode = TreeSelect.TreeNode;
 const { Title } = Typography;
@@ -2126,4 +2126,126 @@ export const clickTriggerToHide = () => (
           clickTriggerToHide={false}
       />
   </>
-);
+);
+export const triggerRenderAddMethod = () => {
+  const treeData = useMemo(() => [
+      {
+          label: '亚洲',
+          value: '亚洲',
+          key: '0',
+          children: [
+              {
+                  label: '中国',
+                  value: '中国',
+                  key: '0-0',
+                  children: [
+                      {
+                          label: '北京',
+                          value: '北京',
+                          key: '0-0-0',
+                      },
+                      {
+                          label: '上海',
+                          value: '上海',
+                          key: '0-0-1',
+                      },
+                  ],
+              },
+          ],
+      },
+      {
+          label: '北美洲',
+          value: '北美洲',
+          key: '1',
+      }
+  ], []);
+
+  const onValueChange = useCallback((value) => {
+      console.log('onChange', value);
+  });
+
+  const closeIcon = useCallback((value, onClear) => {
+      return value && value.length ? <IconClose onClick={onClear} /> : <IconChevronDown />;
+  }, []);
+
+  const renderTagItem = useCallback((item, onRemove) => (
+      <Tag closable key={item.key} onClose={() => { onRemove(item.key); }}>{item.label}</Tag>
+  ), []);
+
+  const renderTrigger1 = useCallback((props) => {
+    const { value, placeholder, onClear } = props;
+    return (
+      <Button theme={'light'} icon={closeIcon(value, onClear)} iconPosition={'right'}>
+          {value && value.length ? value.map(item => item.label).join(',') : placeholder}
+      </Button>
+    );
+  }, []);
+
+  const renderTrigger2 = useCallback((props) => {
+      const { value, onSearch, onRemove, onClear } = props;
+      return (
+          <div style={{ border: '1px solid grey', width: 'fit-content', padding: 5, borderRadius: 5 }}>
+              {value && value.length > 0 && 
+              <div style={{ width: 'fit-content', minWidth: 10, padding: 5 }}>
+                  {value.map(item => renderTagItem(item, onRemove))}
+              </div>
+              }
+              <Input style={{ width: 200 }} onChange={onSearch} />
+              {closeIcon(value, onClear)}
+          </div>
+      );
+  }, []);
+
+  const renderTrigger3 = useCallback((props) => {
+    const { value, onSearch, onRemove } = props;
+    const tagInputValue = value.map(item => item.key);
+    const renderTagInMultiple = (key) => {
+      const label = value.find(item => item.key === key).label;
+      const onCloseTag = (value, e, tagKey) => {
+        onRemove(tagKey);
+      }
+      return <Tag style={{ marginLeft: 2 }} tagKey={key} key={key} onClose={onCloseTag} closable>{label}</Tag>
+    }
+    return (
+      <TagInput
+        style={{ width: 250 }}
+        value={tagInputValue}
+        onInputChange={onSearch}
+        renderTagItem={renderTagInMultiple}
+      />
+    )
+  }, []);
+
+  return (
+    <>
+      <TreeSelect
+          triggerRender={renderTrigger1}
+          multiple
+          treeData={treeData}
+          placeholder='Custom Trigger'
+          onChange={onValueChange}
+          style={{ width: 300 }}
+      />
+      <br />
+      <TreeSelect
+          triggerRender={renderTrigger2}
+          filterTreeNode
+          multiple
+          treeData={treeData}
+          placeholder='Custom Trigger'
+          onChange={onValueChange}
+          style={{ width: 300 }}
+      />
+      <br />
+      <TreeSelect
+          triggerRender={renderTrigger3}
+          filterTreeNode
+          multiple
+          treeData={treeData}
+          placeholder='Custom Trigger'
+          onChange={onValueChange}
+          style={{ width: 300 }}
+      />
+    </>
+  );
+}

+ 2 - 0
packages/semi-ui/treeSelect/index.tsx

@@ -1016,6 +1016,8 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
                 componentName={'TreeSelect'}
                 triggerRender={triggerRender}
                 componentProps={{ ...this.props }}
+                onSearch={this.search}
+                onRemove={this.removeTag}
             />
         ) : (
             [