1
0
Эх сурвалжийг харах

feat: Tree/TreeSelect add expandIcon API

zhangyumei.0319 8 сар өмнө
parent
commit
4254a00262

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

@@ -1429,6 +1429,7 @@ function Demo() {
 | expandAction             | Expand logic, one of false, 'click', 'doubleClick'. Default is set to false, which means item will not be expanded on clicking except on expand icon    | boolean \| string   | false | - |
 | expandAll | Set whether to expand all nodes by default. If the data (`treeData`) changes, the default expansion will still be affected by this api | boolean | false |- |
 | expandedKeys        | (Controlled)Keys of expanded nodes. Direct child nodes will be displayed.  | string[]                    | -       | - |
+| expandIcon | Custom expand icon, [example](/en-US/navigation/tree#Custom%20expansion%20icon) | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | keyMaps | Customize the key, label, and value fields in the node | object |  - | 2.47.0 |
 | filterTreeNode           | Toggle whether searchable or pass in a function to customize search behavior, data parameter provided since v2.28.0 | boolean\| <ApiType detail='(inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean'>Function</ApiType> | false       | -       |
 | getPopupContainer        | Container to render pop-up, you need to set 'position: relative`  This will change the DOM tree position, but not the view's rendering position.                                                    | function():HTMLElement                                            | -           | -       |

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

@@ -1412,6 +1412,7 @@ function Demo() {
 | expandAction | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开                                                        | boolean \| string | false |
 | expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`)发生改变,默认的展开情况也是会受到这个 api 影响的                                                         | boolean | false |
 | expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级                                                                                                   | string[] | - |
+| expandIcon | 自定义展开图标,使用[示例](/zh-CN/navigation/tree#%E8%87%AA%E5%AE%9A%E4%B9%89%E5%B1%95%E5%BC%80%20Icon) | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | keyMaps | 自定义节点中 key、label、value 的字段。v2.47.0后提供                                                                                                | object |  - |
 | filterTreeNode | 是否根据输入项进行筛选,默认用 `treeNodeFilterProp` 的值作为要筛选的 `TreeNodeData` 的属性值, data 参数自 v2.28.0 开始提供                         | boolean\| <ApiType detail='(inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean'>Function</ApiType> | false |
 | getPopupContainer  | 指定父级 DOM,弹层将会渲染至该 DOM 中,自定义需要设置 `position: relative` 这会改变浮层 DOM 树位置,但不会改变视图渲染位置。                                                                                       | function():HTMLElement | - |

+ 84 - 0
content/navigation/tree/index-en-US.md

@@ -1213,6 +1213,89 @@ class Demo extends React.Component {
 }
 ```
 
+### Custom expansion icon
+
+The expansion Icon can be customized through `expandIcon`. Supports passing in ReactNode or functions. `expandIcon` is supported since 2.75.0.
+
+```ts
+expandIcon: ReactNode | ((props: {
+    onClick: (e: MouseEvent) => void;
+    className: string;
+    expanded: boolean;
+}))
+```
+
+Examples are as follows:
+
+```jsx live=true 
+() => {
+   const treeData = [
+            {
+                label: 'Asia',
+                key: 'Asia',
+                children: [
+                    {
+                        label: 'China',
+                        key: 'China',
+                        children: [
+                            {
+                                label: 'Beijing',
+                                key: 'Beijing',
+                            },
+                            {
+                                label: 'Shanghai',
+                                key: 'Shanghai',
+                            },
+                        ],
+                    },
+                ],
+            },
+            {
+                label: 'North America',
+                value: 'North America',
+            }
+        ];
+    const expandIconFunc = useCallback((props) => {
+        const { expanded, onClick, className } = props;
+        if (expanded) {
+        return <IconMinus size="small" className={className} onClick={onClick}/>
+        } else {
+        return <IconPlus size="small" className={className} onClick={onClick}/>
+        }
+    });
+    const style = {
+        width: 260,
+        height: 200,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+  return (
+    <>
+      <p>ReactNode type</p>
+      <Tree
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['Asia']}
+        treeData={treeData}
+        style={style}
+      />
+      <br />
+      <p>Function type</p>
+      <Tree
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['Asia']}
+        treeData={treeData}
+        style={style}
+      />
+    </>
+  );
+}
+```
+
+
 ### Tree with line
 
 Set the line between nodes through `showLine`, the default is false, supported starting from 2.50.0
@@ -2286,6 +2369,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | expandAction             | Expand logic, one of false, 'click', 'doubleClick'. Default is set to false, which means item will not be expanded on clicking except on expand icon    | boolean \| string   | false | 0.35.0       |
 | expandAll | Set whether to expand all nodes by default. If the subsequent data (`treeData`/`treeDataSimpleJson`) changes, the default expansion will also be affected by this api | boolean | false | 1.30.0 |
 | expandedKeys        | (Controlled)Keys of expanded nodes. Direct child nodes will be displayed.  | string[]                    | -       | - |
+| expandIcon | Custom expand icon | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | keyMaps | Customize the key, label, and value fields in the node | object |  - | 2.47.0 |
 | filterTreeNode      | Toggle whether searchable or pass in a function to customize search behavior, data parameter provided since v2.28.0 | boolean \| ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean)  | false   | - |
 | hideDraggingNode | Toggle whether to hide dragImg of dragging node | boolean | false | 1.8.0 | 

+ 87 - 0
content/navigation/tree/index.md

@@ -1241,6 +1241,92 @@ class Demo extends React.Component {
 }
 ```
 
+### 自定义展开 Icon
+
+可以通过 `expandIcon` 自定义展开 Icon。 支持传入 ReactNode 或者函数。`expandIcon` 自 2.75.0 开始支持。
+
+```ts
+expandIcon: ReactNode | ((props: {
+    onClick: (e: MouseEvent) => void;
+    className: string;
+    expanded: boolean;
+}))
+```
+
+示例如下:
+
+```jsx live=true 
+() => {
+    const treeData = [
+        {
+            label: '亚洲',
+            key: 'yazhou',
+            children: [
+                {
+                    label: '中国',
+                    key: 'zhongguo',
+                    children: [
+                        {
+                            label: '北京',
+                            key: 'beijing',
+                        },
+                        {
+                            label: '上海',
+                            key: 'shanghai',
+                        },
+                    ],
+                },
+                {
+                    label: '日本',
+                    key: 'riben',
+                },
+            ],
+        },
+        {
+            label: '北美洲',
+            key: 'beimeizhou',
+        },
+    ];
+    const expandIconFunc = useCallback((props) => {
+        const { expanded, onClick, className } = props;
+        if (expanded) {
+        return <IconMinus size="small" className={className} onClick={onClick}/>
+        } else {
+        return <IconPlus size="small" className={className} onClick={onClick}/>
+        }
+    });
+    const style = {
+        width: 260,
+        height: 200,
+        border: '1px solid var(--semi-color-border)'
+    };
+
+  return (
+    <>
+      <p>expandIcon 是  ReactNode</p>
+      <Tree
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData}
+        style={style}
+      />
+      <br />
+      <p>expandIcon 是函数 </p>
+      <Tree
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData}
+        style={style}
+      />
+    </>
+  );
+}
+```
+
 ### 连接线
 
 通过 `showLine` 设置节点之间的连接线,默认为 false,从 2.50.0 开始支持
@@ -2301,6 +2387,7 @@ import { IconFixedStroked, IconSectionStroked, IconAbsoluteStroked, IconInnerSec
 | expandAction             | 展开逻辑,可选 false, 'click', 'doubleClick'。默认值为 false,即仅当点击展开按钮时才会展开  | boolean \| string   | false | 0.35.0       |
 | expandAll | 设置是否默认展开所有节点,若后续数据(`treeData`/`treeDataSimpleJson`)发生改变,默认展开情况也是会受到这个 api 影响的 | boolean | false | 1.30.0 |
 | expandedKeys | (受控)展开的节点,默认展开节点显示其直接子级 | string[] | - | - |
+| expandIcon | 自定义展开图标 | ReactNode \| (props: expandProps)=>ReactNode | - | 2.75.0 |
 | keyMaps | 自定义节点中 key、label、value 的字段 | object |  - | 2.47.0 |
 | filterTreeNode | 是否根据输入项进行筛选,默认用 `treeNodeFilterProp` 的值作为要筛选的 `TreeNodeData` 的属性值,  data 参数自 v2.28.0 开始提供 | boolean \| ((inputValue: string, treeNodeString: string, data?: TreeNodeData) => boolean) | false | - |
 | hideDraggingNode | 是否隐藏正在拖拽的节点的 dragImg | boolean | false | 1.8.0 | 

+ 35 - 1
packages/semi-ui/tree/_story/tree.stories.jsx

@@ -1,6 +1,6 @@
 import React, { useRef, useState, useCallback, useMemo, useEffect } from 'react';
 import { difference, isEqual } from 'lodash';
-import { IconEdit, IconMapPin, IconMore } from '@douyinfe/semi-icons';
+import { IconEdit, IconMapPin, IconMore, IconChevronDown, IconPlus, IconMinus } from '@douyinfe/semi-icons';
 import Tree from '../index';
 import AutoSizer from '../autoSizer';
 import { Button, ButtonGroup, Input, Popover, Toast, Space, Select, Switch, Typography, Tag } from '../../index';
@@ -3143,4 +3143,38 @@ export const AutoMerge = () => {
       />
     </>
   )
+}
+
+export const CustomExpandIcon = () => {
+  const expandIconFunc = useCallback((props) => {
+    const { expanded, onClick, className } = props;
+    if (expanded) {
+      return <IconMinus size="small" className={className} onClick={onClick}/>
+    } else {
+      return <IconPlus size="small" className={className} onClick={onClick}/>
+    }
+  });
+
+  return (
+    <>
+      <p>expandIcon 是 ReactNode</p>
+      <Tree
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData1}
+      />
+      <br />
+      <br />
+      <p>expandIcon 是函数</p>
+      <Tree
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData1}
+      />
+    </>
+  );
 }

+ 10 - 2
packages/semi-ui/tree/index.tsx

@@ -665,11 +665,19 @@ class Tree extends BaseComponent<TreeProps, TreeState> {
         if (!treeNodeProps) {
             return null;
         }
-        const { keyMaps, showLine } = this.props;
+        const { keyMaps, showLine, expandIcon } = this.props;
         const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
         const children = data[get(keyMaps, 'children', 'children')];
         !isUndefined(children) && (props.children = children);
-        return <TreeNode {...treeNodeProps} {...data} {...props} showLine={showLine} data={data} style={isEmpty(style) ? {} : style} />;
+        return <TreeNode 
+            {...treeNodeProps} 
+            {...data} 
+            {...props} 
+            showLine={showLine} 
+            data={data}
+            expandIcon={expandIcon}
+            style={isEmpty(style) ? {} : style} 
+        />;
     };
 
     itemKey = (index: number, data: KeyEntity) => {

+ 8 - 2
packages/semi-ui/tree/interface.ts

@@ -85,7 +85,8 @@ export interface TreeProps extends BasicTreeProps {
     renderDraggingNode?: (nodeInstance: HTMLElement, node: TreeNodeData) => HTMLElement;
     renderFullLabel?: (renderFullLabelProps: RenderFullLabelProps) => ReactNode;
     renderLabel?: (label?: ReactNode, treeNode?: TreeNodeData, searchWord?: string) => ReactNode;
-    autoMergeValue?: boolean
+    autoMergeValue?: boolean;
+    expandIcon?: ReactNode
 }
 export interface OptionProps {
     index: number;
@@ -112,7 +113,12 @@ export interface TreeState extends BasicTreeInnerData {
 export interface TreeNodeProps extends BasicTreeNodeProps {
     children?: TreeNodeData[];
     icon?: ReactNode;
-    isEnd?: boolean[]
+    isEnd?: boolean[];
+    expandIcon?: ReactNode | ((props: {
+        onClick: (e: MouseEvent) => void;
+        className: string;
+        expanded: boolean
+    }) => ReactNode)
 }
 export interface TreeNodeState {
     [x: string]: any

+ 22 - 2
packages/semi-ui/tree/treeNode.tsx

@@ -36,7 +36,8 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
         selectedKey: PropTypes.string,
         motionKey: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
         isEnd: PropTypes.arrayOf(PropTypes.bool),
-        showLine: PropTypes.bool
+        showLine: PropTypes.bool,
+        expandIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
     };
 
     static defaultProps = {
@@ -203,11 +204,30 @@ export default class TreeNode extends PureComponent<TreeNodeProps, TreeNodeState
 
     renderArrow() {
         const showIcon = !this.isLeaf();
-        const { loading, expanded, showLine } = this.props;
+        const { loading, expanded, showLine, expandIcon } = this.props;
         if (loading) {
             return <Spin wrapperClassName={`${prefixcls}-spin-icon`} />;
         }
         if (showIcon) {
+            if (expandIcon) {
+                if (typeof expandIcon === 'function') {
+                    return expandIcon({
+                        onClick: this.onExpand,
+                        className: `${prefixcls}-expand-icon`,
+                        expanded
+                    });
+                } else if (React.isValidElement(expandIcon)) {
+                    const className = cls(`${prefixcls}-expand-icon`, {
+                        [expandIcon?.props?.className]: expandIcon?.props?.className
+                    });
+                    return React.cloneElement(expandIcon, {
+                        onClick: this.onExpand,
+                        className,
+                    } as any);
+                } else {
+                    return expandIcon;
+                }
+            }
             return (
                 <IconTreeTriangleDown
                     role='button'

+ 35 - 1
packages/semi-ui/treeSelect/_story/treeSelect.stories.jsx

@@ -4,7 +4,7 @@ import { Icon, Input, Button, Form, Popover, Tag, Typography, CheckboxGroup, Tag
 import TreeSelect from '../index';
 import { flattenDeep, without } from 'lodash';
 import CustomTrigger from './CustomTrigger';
-import { IconCreditCard, IconChevronDown, IconClose } from '@douyinfe/semi-icons';
+import { IconCreditCard, IconChevronDown, IconClose, IconPlus, IconMinus } from '@douyinfe/semi-icons';
 import copy from 'fast-copy';
 
 const TreeNode = TreeSelect.TreeNode;
@@ -2960,4 +2960,38 @@ export const filterAndKeyMaps = () => {
       placeholder="单选可搜索的"
     />
   )
+}
+
+export const CustomExpandIcon = () => {
+  const expandIconFunc = useCallback((props) => {
+    const { expanded, onClick, className } = props;
+    if (expanded) {
+      return <IconMinus size="small" className={className} onClick={onClick}/>
+    } else {
+      return <IconPlus size="small" className={className} onClick={onClick}/>
+    }
+  });
+
+  return (
+    <>
+      <p>expandIcon 是 ReactNode</p>
+      <TreeSelect
+        style={{ width: 300}}
+        expandIcon={<IconChevronDown size="small" className='testCls'/>}
+        multiple
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData2}
+      />
+      <br />
+      <br />
+      <p>expandIcon 是函数</p>
+      <TreeSelect
+        style={{ width: 300}}
+        multiple
+        expandIcon={expandIconFunc}
+        defaultExpandedKeys={['yazhou']}
+        treeData={treeData2}
+      />
+    </>
+  );
 }

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

@@ -141,6 +141,11 @@ export interface TreeSelectProps extends Omit<BasicTreeSelectProps, OverrideComm
     searchPosition?: string;
     stopPropagation?: boolean | string;
     restTagsPopoverProps?: PopoverProps;
+    expandIcon?: React.ReactNode | ((props: {
+        onClick: (e: MouseEvent) => void;
+        className: string;
+        expanded: boolean
+    }) => React.ReactNode);
     searchRender?: boolean | ((inputProps: InputProps) => React.ReactNode);
     onSelect?: (selectedKey: string, selected: boolean, selectedNode: TreeNodeData) => void;
     renderSelectedItem?: RenderSelectedItem;
@@ -1410,10 +1415,18 @@ class TreeSelect extends BaseComponent<TreeSelectProps, TreeSelectState> {
             return null;
         }
         const props: any = pick(treeNode, ['key', 'label', 'disabled', 'isLeaf', 'icon', 'isEnd']);
-        const { keyMaps } = this.props;
+        const { keyMaps, expandIcon } = this.props;
         const children = data[get(keyMaps, 'children', 'children')];
         !isUndefined(children) && (props.children = children);
-        return <TreeNode {...treeNodeProps} {...data} {...props} data={data} style={style} showLine={showLine}/>;
+        return <TreeNode 
+            {...treeNodeProps} 
+            {...data} 
+            {...props} 
+            data={data} 
+            style={style} 
+            showLine={showLine}
+            expandIcon={expandIcon}
+        />;
     };
 
     itemKey = (index: number, data: Record<string, any>) => {