Browse Source

feat: [Transfer] Transfer adds renderSourceHeader and renderSelectedH… (#1408)

* feat: [Transfer] Transfer adds renderSourceHeader and renderSelectedHeader APIs to support users to customize panel header information

* fix: [Tranfer] modify renderSourceHeader & renderSelectedHeader related params
YyumeiZhang 2 years ago
parent
commit
94c9810f00

+ 75 - 0
content/input/transfer/index-en-US.md

@@ -350,6 +350,79 @@ import { IconHandle, IconClose } from '@douyinfe/semi-icons';
 };
 ```
 
+### Custom rendering header information in panel
+
+Semi has provided `renderSourceHeader` and `renderSelectedHeader` parameter allows users to customize the header information of the left and right panels since version 2.29.0.   
+`renderSourceHeader: (props: SourceHeaderProps) => ReactNode`   
+`renderSelectedHeader: (props: SelectedHeaderProps) => ReactNode`   
+The parameter types are as follows:
+
+```ts
+type SourceHeaderProps = {
+    num: number; // The total number of data or the number of filtered results
+    showButton: boolean; // Whether to show select all/unselect all buttons
+    allChecked: boolean; // Whether the current data has been selected
+    onAllClick: () => void // Function that should be called after clicking the select/unselect all button
+}
+
+type SelectedHeaderProps = {
+    num: number; // The total number of selected data
+    showButton: boolean; // Whether to show the clear button
+    onClear: () => void // Function that should be called after clicking the clear button
+}
+```
+
+The example is as follows:
+
+```jsx live=true dir="column"
+import React from 'react';
+import { Transfer, Button } from '@douyinfe/semi-ui';
+
+() => {
+    const data = Array.from({ length: 30 }, (v, i) => {
+        return {
+            label: `Item ${i}`,
+            value: i,
+            disabled: false,
+            key: i,
+        };
+    });
+
+    const renderSourceHeader = (props) => {
+        const { num, showButton, allChecked, onAllClick } = props;
+        return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+            <span>Total {num} items</span>
+            {showButton && <Button
+                theme="borderless"
+                type="tertiary"
+                size="small" 
+                onClick={onAllClick}>{ allChecked ? 'Unselect all' : 'Select all' }</Button>}
+        </div>;
+    };
+
+    const renderSelectedHeader = (props) => {
+        const { num, showButton, onClear } = props;
+        return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+            <span>{num} items selected</span>
+            {showButton && <Button
+                theme="borderless"
+                type="tertiary"
+                size="small"
+                onClick={onClear}>Clear</Button>}
+        </div>;
+    };
+
+    return (
+        <Transfer 
+            style={{ width: 568, height: 416 }}
+            dataSource={data}
+            renderSourceHeader={renderSourceHeader}
+            renderSelectedHeader={renderSelectedHeader}
+        />
+    );
+};
+```
+
 ### Fully custom rendering
 
 Semi provides `renderSourcePanel` and `renderSelectedPanel` input parameters, allowing you to completely customize the rendering structure of the left and right panels
@@ -931,8 +1004,10 @@ import { Transfer } from '@douyinfe/semi-ui';
 | onDeselect | Callback when unchecking | (item: Item) => void | | |
 | onSearch | Called when the input content of the search box changes | (inputValue: string) => void | | |
 | onSelect | Callback when checked | (item: Item) => void | | |
+| renderSelectedHeader | Customize the rendering of the header information on the right panel | (props: SelectedHeaderProps) => ReactNode |  | 2.29.0 |
 | renderSelectedItem | Customize the rendering of a single selected item on the right | (item: {onRemove, sortableHandle} & Item) => ReactNode | | |
 | renderSelectedPanel | Customize the rendering of the selected panel on the right | (selectedPanelProps) => ReactNode | | 1.11.0 |
+| renderSourceHeader | Customize the rendering of the header information on the left panel | (props: SourceHeaderProps) => ReactNode |  | 2.29.0 |
 | renderSourceItem | Customize the rendering of a single candidate item on the left | (item: {onChange, checked} & Item) => ReactNode | | |
 | renderSourcePanel | Customize the rendering of the left candidate panel | (sourcePanelProps) => ReactNode | | 1.11.0 |
 | showPath | When the type is `treeList`, control whether the selected item on the right shows the selection path | boolean | false | 1.20.0 |

+ 75 - 0
content/input/transfer/index.md

@@ -352,6 +352,79 @@ import { IconHandle, IconClose } from '@douyinfe/semi-icons';
 };
 ```
 
+### 自定义渲染面板头部信息
+
+Semi 自 2.29.0 版本提供 `renderSourceHeader`, `renderSelectedHeader` 参数允许用户自定义渲染左右两个面板的头部信息。   
+`renderSourceHeader: (props: SourceHeaderProps) => ReactNode`   
+`renderSelectedHeader: (props: SelectedHeaderProps) => ReactNode`   
+参数类型如下:
+
+```ts
+type SourceHeaderProps = {
+    num: number; // 数据总数或筛选结果数目
+    showButton: boolean; // 是否展示全选/取消全选按钮
+    allChecked: boolean; // 当前数据是否已全选
+    onAllClick: () => void // 点击全选/取消全选按钮后应调用的函数
+}
+
+type SelectedHeaderProps = {
+    num: number; // 已选中数据总数
+    showButton: boolean; // 是否展示清空按钮
+    onClear: () => void // 点击清空按钮后应调用的函数
+}
+```
+
+使用示例如下
+
+```jsx live=true dir="column"
+import React from 'react';
+import { Transfer, Button } from '@douyinfe/semi-ui';
+
+() => {
+    const data = Array.from({ length: 30 }, (v, i) => {
+        return {
+            label: `选项名称 ${i}`,
+            value: i,
+            disabled: false,
+            key: i,
+        };
+    });
+
+    const renderSourceHeader = (props) => {
+        const { num, showButton, allChecked, onAllClick } = props;
+        return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+            <span>共 {num} 项</span>
+            {showButton && <Button
+                theme="borderless"
+                type="tertiary"
+                size="small" 
+                onClick={onAllClick}>{ allChecked ? '取消全选' : '全选' }</Button>}
+        </div>;
+    };
+
+    const renderSelectedHeader = (props) => {
+        const { num, showButton, onClear } = props;
+        return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+            <span>{num} 项已选</span>
+            {showButton && <Button
+                theme="borderless"
+                type="tertiary"
+                size="small"
+                onClick={onClear}>清空</Button>}
+        </div>;
+    };
+
+    return (
+        <Transfer
+            style={{ width: 568, height: 416 }}
+            dataSource={data}
+            renderSourceHeader={renderSourceHeader}
+            renderSelectedHeader={renderSelectedHeader}
+        />
+    );
+};
+```
+
 ### 完全自定义渲染
 
 Semi 提供了 `renderSourcePanel`、`renderSelectedPanel` 入参,允许你完全自定义左右侧两个面板的渲染结构  
@@ -933,8 +1006,10 @@ import { Transfer } from '@douyinfe/semi-ui';
 | onDeselect | 取消勾选时的回调 | (item: Item) => void | |  |
 | onSearch | 搜索框输入内容变化时调用 | (inputValue: string) => void | |  |
 | onSelect | 勾选时的回调 | (item: Item) => void | |  |
+| renderSelectedHeader | 自定义右侧面板头部信息的渲染 | (props: SelectedHeaderProps) => ReactNode |  | 2.29.0 |
 | renderSelectedItem | 自定义右侧单个已选项的渲染 | (item: { onRemove, sortableHandle } & Item) => ReactNode |  |  |
 | renderSelectedPanel | 自定义右侧已选面板的渲染 | (selectedPanelProps) => ReactNode |  | 1.11.0 |
+| renderSourceHeader | 自定义左侧面板头部信息的渲染 | (props: SourceHeaderProps) => ReactNode |  | 2.29.0 |
 | renderSourceItem | 自定义左侧单个候选项的渲染 | (item: { onChange, checked } & Item) => ReactNode |  |  |
 | renderSourcePanel | 自定义左侧候选面板的渲染 | (sourcePanelProps) => ReactNode |  | 1.11.0 |
 | showPath | 当 type 为`treeList`时,控制右侧选中项是否显示选择路径 | boolean | false | 1.20.0 |

+ 37 - 1
packages/semi-ui/transfer/_story/transfer.stories.jsx

@@ -1,5 +1,5 @@
 import React, { useState, useRef } from 'react';
-import { Transfer, Button, Popover, SideSheet, Avatar, Checkbox, Tree, Input } from '../../index';
+import { Transfer, Button, Popover, SideSheet, Avatar, Checkbox, Tree, Input, Tag } from '../../index';
 import { omit, values } from 'lodash';
 import './transfer.scss';
 import { SortableContainer, SortableElement, sortableHandle } from 'react-sortable-hoc';
@@ -828,3 +828,39 @@ export const TransferInPopover = () => {
     </div>
   );
 }
+
+export const RenderHeader = () => {
+ 
+  const renderSourceHeader = (props) => {
+    const { num, showButton, allChecked, onAllClick } = props;
+    return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+      <span style={{ marginRight: 10 }} >共 {num} 项</span>
+      {showButton && <Button
+        theme="borderless"
+        type="tertiary"
+        size="small" 
+        onClick={onAllClick}>{ allChecked ? '取消全选' : '全选' }</Button>}
+    </div>;
+  };
+
+  const renderSelectedHeader = (props) => {
+    const { num, showButton, onClear } = props;
+    return <div style={{ margin: '10px 0 0 10px', height: 24, display: 'flex', alignItems: 'center' }}>
+      <span style={{ marginRight: 10 }}>{num} 项已选</span>
+      {showButton && <Button
+        theme="borderless"
+        type="tertiary"
+        size="small"
+        onClick={onClear}>清空</Button>}
+    </div>;
+  };
+
+  return (
+    <Transfer
+      style={{ width: 568, height: 416 }}
+      dataSource={data}
+      renderSourceHeader={renderSourceHeader}
+      renderSelectedHeader={renderSelectedHeader}
+    />
+  );
+};

+ 35 - 4
packages/semi-ui/transfer/index.tsx

@@ -2,7 +2,7 @@ import React from 'react';
 import cls from 'classnames';
 import { SortableContainer, SortableElement, SortableHandle } from 'react-sortable-hoc';
 import PropTypes from 'prop-types';
-import { isEqual, noop, omit, isEmpty, isArray } from 'lodash';
+import { isEqual, noop, omit, isEmpty, isArray, pick } from 'lodash';
 import TransferFoundation, { TransferAdapter, BasicDataItem, OnSortEndProps } from '@douyinfe/semi-foundation/transfer/foundation';
 import { _generateDataByType, _generateSelectedItems } from '@douyinfe/semi-foundation/transfer/transferUtils';
 import { cssClasses, strings } from '@douyinfe/semi-foundation/transfer/constants';
@@ -115,7 +115,22 @@ interface HeaderConfig {
     allContent: string;
     onAllClick: () => void;
     type: string;
-    showButton: boolean
+    showButton: boolean;
+    num: number;
+    allChecked?: boolean
+}
+
+type SourceHeaderProps = {
+    num: number;
+    showButton: boolean;
+    allChecked: boolean;
+    onAllClick: () => void
+}
+
+type SelectedHeaderProps = {
+    num: number;
+    showButton: boolean;
+    onClear: () => void
 }
 
 export interface TransferState {
@@ -147,7 +162,9 @@ export interface TransferProps {
     renderSourceItem?: (item: RenderSourceItemProps) => React.ReactNode;
     renderSelectedItem?: (item: RenderSelectedItemProps) => React.ReactNode;
     renderSourcePanel?: (sourcePanelProps: SourcePanelProps) => React.ReactNode;
-    renderSelectedPanel?: (selectedPanelProps: SelectedPanelProps) => React.ReactNode
+    renderSelectedPanel?: (selectedPanelProps: SelectedPanelProps) => React.ReactNode;
+    renderSourceHeader?: (headProps: SourceHeaderProps) => React.ReactNode;
+    renderSelectedHeader?: (headProps: SelectedHeaderProps) => React.ReactNode
 }
 
 const prefixCls = cssClasses.PREFIX;
@@ -336,13 +353,24 @@ class Transfer extends BaseComponent<TransferProps, TransferState> {
     }
 
     renderHeader(headerConfig: HeaderConfig) {
-        const { disabled } = this.props;
+        const { disabled, renderSourceHeader, renderSelectedHeader } = this.props;
         const { totalContent, allContent, onAllClick, type, showButton } = headerConfig;
         const headerCls = cls({
             [`${prefixCls}-header`]: true,
             [`${prefixCls}-right-header`]: type === 'right',
             [`${prefixCls}-left-header`]: type === 'left',
         });
+
+        if (type === 'left' && typeof renderSourceHeader === 'function') {
+            const { num, showButton, allChecked, onAllClick } = headerConfig;
+            return renderSourceHeader({ num, showButton, allChecked, onAllClick });
+        }
+        
+        if (type === 'right' && typeof renderSelectedHeader === 'function') {
+            const { num, showButton, onAllClick: onClear } = headerConfig;
+            return renderSelectedHeader({ num, showButton, onClear });                 
+        }
+
         return (
             <div className={headerCls}>
                 <span className={`${prefixCls}-header-total`}>{totalContent}</span>
@@ -409,6 +437,8 @@ class Transfer extends BaseComponent<TransferProps, TransferState> {
             onAllClick: () => this.foundation.handleAll(leftContainesNotInSelected),
             type: 'left',
             showButton: type !== strings.TYPE_TREE_TO_LIST,
+            num: showNumber,
+            allChecked: !leftContainesNotInSelected
         };
         const inputCom = this.renderFilter(locale);
         const headerCom = this.renderHeader(headerConfig);
@@ -623,6 +653,7 @@ class Transfer extends BaseComponent<TransferProps, TransferState> {
             onAllClick: () => this.foundation.handleClear(),
             type: 'right',
             showButton: Boolean(selectedData.length),
+            num: selectedData.length,
         };
         const headerCom = this.renderHeader(headerConfig);
         const emptyCom = this.renderEmpty('right', emptyContent.right ? emptyContent.right : locale.emptyRight);