feat: Transfer support oneWay (#24041)

* use flex

* show clear btn

* support one way

* add dropdown

* add pagination

* support pagination

* use flex

* operation stretch

* pagination works

* update selection logic

* no need to show checkbox on pagination

* fix tree demo

* support invert selection

* remove current pages

* update style

* update snapshot

* clean up

* update test case

* update test case

* update snapshot

* fix lint

* add deps

* update doc

* update hover style

* update hover checked style

* adjust demo & active region

* fix lint

* update snapshot
This commit is contained in:
二货机器人 2020-05-13 19:15:40 +08:00 committed by GitHub
parent 41dd9560c9
commit b0e528d14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 5878 additions and 1450 deletions

View File

@ -9,6 +9,7 @@ interface TransButtonProps extends React.HTMLAttributes<HTMLDivElement> {
onClick?: (e?: React.MouseEvent<HTMLDivElement>) => void;
noStyle?: boolean;
autoFocus?: boolean;
disabled?: boolean;
}
const inlineStyle: React.CSSProperties = {
@ -63,7 +64,24 @@ class TransButton extends React.Component<TransButtonProps> {
}
render() {
const { style, noStyle, ...restProps } = this.props;
const { style, noStyle, disabled, ...restProps } = this.props;
let mergedStyle: React.CSSProperties = {};
if (!noStyle) {
mergedStyle = {
...inlineStyle,
};
}
if (disabled) {
mergedStyle.pointerEvents = 'none';
}
mergedStyle = {
...mergedStyle,
...style,
};
return (
<div
@ -73,7 +91,7 @@ class TransButton extends React.Component<TransButtonProps> {
{...restProps}
onKeyDown={this.onKeyDown}
onKeyUp={this.onKeyUp}
style={{ ...(!noStyle ? inlineStyle : null), ...style }}
style={mergedStyle}
/>
);
}

View File

@ -34579,16 +34579,34 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -34722,16 +34740,34 @@ exports[`ConfigProvider components Transfer configProvider 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -34816,16 +34852,34 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -34959,16 +35013,34 @@ exports[`ConfigProvider components Transfer configProvider componentSize large 1
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -35053,16 +35125,34 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -35196,16 +35286,34 @@ exports[`ConfigProvider components Transfer configProvider componentSize middle
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down config-dropdown-trigger config-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="config-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
0 item
</span>
<span
class="config-transfer-list-header-title"
/>
</div>
<div
class="config-transfer-list-body"
@ -35290,16 +35398,34 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -35433,16 +35559,34 @@ exports[`ConfigProvider components Transfer configProvider virtual and dropdownM
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -35527,16 +35671,34 @@ exports[`ConfigProvider components Transfer normal 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -35670,16 +35832,34 @@ exports[`ConfigProvider components Transfer normal 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -35764,16 +35944,34 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger prefix-Transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="prefix-Transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="prefix-Transfer-list-header-title"
/>
0 item
</span>
<span
class="prefix-Transfer-list-header-title"
/>
</div>
<div
class="prefix-Transfer-list-body"
@ -35907,16 +36105,34 @@ exports[`ConfigProvider components Transfer prefixCls 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger prefix-Transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="prefix-Transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="prefix-Transfer-list-header-title"
/>
0 item
</span>
<span
class="prefix-Transfer-list-header-title"
/>
</div>
<div
class="prefix-Transfer-list-body"

View File

@ -283,16 +283,34 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -426,16 +444,34 @@ exports[`renders ./components/empty/demo/config-provider.md correctly 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"

View File

@ -44,6 +44,12 @@ const localeValues: Locale = {
searchPlaceholder: 'Search here',
itemUnit: 'item',
itemsUnit: 'items',
remove: 'Remove',
selectCurrent: 'Select current page',
removeCurrent: 'Remove current page',
selectAll: 'Select all data',
removeAll: 'Remove all data',
selectInvert: 'Invert current page',
},
Upload: {
uploading: 'Uploading...',

View File

@ -44,6 +44,12 @@ const localeValues: Locale = {
searchPlaceholder: '请输入搜索内容',
itemUnit: '项',
itemsUnit: '项',
remove: '删除',
selectCurrent: '全选当页',
removeCurrent: '删除当页',
selectAll: '全选所有',
removeAll: '删除全部',
selectInvert: '反选当页',
},
Upload: {
uploading: '文件上传中',

View File

@ -0,0 +1,161 @@
import * as React from 'react';
import classNames from 'classnames';
import { ElementOf, Omit, tuple } from '../_util/type';
import Pagination from '../pagination';
import { TransferItem } from '.';
import { TransferListProps, RenderedItem } from './list';
import ListItem from './ListItem';
import { PaginationType } from './interface';
export const OmitProps = tuple('handleFilter', 'handleClear', 'checkedKeys');
export type OmitProp = ElementOf<typeof OmitProps>;
type PartialTransferListProps = Omit<TransferListProps, OmitProp>;
export interface TransferListBodyProps extends PartialTransferListProps {
filteredItems: TransferItem[];
filteredRenderItems: RenderedItem[];
selectedKeys: string[];
}
function parsePagination(pagination?: PaginationType) {
if (!pagination) {
return null;
}
const defaultPagination = {
pageSize: 10,
};
if (typeof pagination === 'object') {
return {
...defaultPagination,
...pagination,
};
}
return defaultPagination;
}
interface TransferListBodyState {
current: number;
}
class ListBody extends React.Component<TransferListBodyProps, TransferListBodyState> {
state = {
current: 1,
};
static getDerivedStateFromProps(
{ filteredRenderItems, pagination }: TransferListBodyProps,
{ current }: TransferListBodyState,
) {
const mergedPagination = parsePagination(pagination);
if (mergedPagination) {
// Calculate the page number
const maxPageCount = Math.ceil(filteredRenderItems.length / mergedPagination.pageSize);
if (current > maxPageCount) {
return { current: maxPageCount };
}
}
return null;
}
onItemSelect = (item: TransferItem) => {
const { onItemSelect, selectedKeys } = this.props;
const checked = selectedKeys.indexOf(item.key) >= 0;
onItemSelect(item.key, !checked);
};
onItemRemove = (item: TransferItem) => {
const { onItemRemove } = this.props;
onItemRemove?.([item.key]);
};
onPageChange = (current: number) => {
this.setState({ current });
};
getItems = () => {
const { current } = this.state;
const { pagination, filteredRenderItems } = this.props;
const mergedPagination = parsePagination(pagination);
let displayItems = filteredRenderItems;
if (mergedPagination) {
displayItems = filteredRenderItems.slice(
(current - 1) * mergedPagination.pageSize,
current * mergedPagination.pageSize,
);
}
return displayItems;
};
render() {
const { current } = this.state;
const {
prefixCls,
onScroll,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
showRemove,
pagination,
} = this.props;
const mergedPagination = parsePagination(pagination);
let paginationNode: React.ReactNode = null;
if (mergedPagination) {
paginationNode = (
<Pagination
simple
className={`${prefixCls}-pagination`}
total={filteredRenderItems.length}
pageSize={mergedPagination.pageSize}
current={current}
onChange={this.onPageChange}
/>
);
}
return (
<>
<ul
className={classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-show-remove`]: showRemove,
})}
onScroll={onScroll}
>
{this.getItems().map(({ renderedEl, renderedText, item }: RenderedItem) => {
const { disabled } = item;
const checked = selectedKeys.indexOf(item.key) >= 0;
return (
<ListItem
disabled={globalDisabled || disabled}
key={item.key}
item={item}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.onItemSelect}
onRemove={this.onItemRemove}
showRemove={showRemove}
/>
);
})}
</ul>
{paginationNode}
</>
);
}
}
export default ListBody;

View File

@ -1,7 +1,11 @@
import * as React from 'react';
import classNames from 'classnames';
import { TransferItem } from '.';
import { DeleteOutlined } from '@ant-design/icons';
import { TransferItem, TransferLocale } from '.';
import defaultLocale from '../locale/default';
import Checkbox from '../checkbox';
import TransButton from '../_util/transButton';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
type ListItemProps = {
renderedText?: string | number;
@ -10,11 +14,23 @@ type ListItemProps = {
checked?: boolean;
prefixCls: string;
onClick: (item: TransferItem) => void;
onRemove?: (item: TransferItem) => void;
item: TransferItem;
showRemove?: boolean;
};
const ListItem = (props: ListItemProps) => {
const { renderedText, renderedEl, item, checked, disabled, prefixCls, onClick } = props;
const {
renderedText,
renderedEl,
item,
checked,
disabled,
prefixCls,
onClick,
onRemove,
showRemove,
} = props;
const className = classNames({
[`${prefixCls}-content-item`]: true,
@ -27,18 +43,42 @@ const ListItem = (props: ListItemProps) => {
title = String(renderedText);
}
const listItem = (
<li
className={className}
title={title}
onClick={disabled || item.disabled ? undefined : () => onClick(item)}
>
<Checkbox checked={checked} disabled={disabled || item.disabled} />
<span className={`${prefixCls}-content-item-text`}>{renderedEl}</span>
</li>
);
return (
<LocaleReceiver componentName="Transfer" defaultLocale={defaultLocale.Transfer}>
{(transferLocale: TransferLocale) => {
const liProps: React.HTMLAttributes<HTMLLIElement> = { className, title };
const labelNode = <span className={`${prefixCls}-content-item-text`}>{renderedEl}</span>;
return listItem;
// Show remove
if (showRemove) {
return (
<li {...liProps}>
{labelNode}
<TransButton
disabled={disabled || item.disabled}
className={`${prefixCls}-content-item-remove`}
aria-label={transferLocale.remove}
onClick={() => {
onRemove?.(item);
}}
>
<DeleteOutlined />
</TransButton>
</li>
);
}
// Default click to select
liProps.onClick = disabled || item.disabled ? undefined : () => onClick(item);
return (
<li {...liProps}>
<Checkbox checked={checked} disabled={disabled || item.disabled} />
{labelNode}
</li>
);
}}
</LocaleReceiver>
);
};
export default React.memo(ListItem);

File diff suppressed because it is too large Load Diff

View File

@ -25,16 +25,34 @@ exports[`Transfer rtl render component should be rendered correctly in RTL direc
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-dropdown-rtl ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -168,16 +186,34 @@ exports[`Transfer rtl render component should be rendered correctly in RTL direc
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-dropdown-rtl ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
0 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -263,16 +299,34 @@ exports[`Transfer should render correctly 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
1/2 items
</span>
<span
class="ant-transfer-list-header-title"
/>
1/2 items
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -405,16 +459,34 @@ exports[`Transfer should render correctly 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
1 item
</span>
<span
class="ant-transfer-list-header-title"
/>
1 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -450,7 +522,7 @@ exports[`Transfer should render correctly 1`] = `
</div>
`;
exports[`Transfer should show sorted targetkey 1`] = `
exports[`Transfer should show sorted targetKey 1`] = `
<div
class="ant-transfer"
>
@ -475,16 +547,34 @@ exports[`Transfer should show sorted targetkey 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
1 item
</span>
<span
class="ant-transfer-list-header-title"
/>
1 item
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -597,16 +687,34 @@ exports[`Transfer should show sorted targetkey 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
2 items
</span>
<span
class="ant-transfer-list-header-title"
/>
2 items
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
@ -691,7 +799,13 @@ exports[`Transfer should support render value and label in item 1`] = `
Object {
"itemUnit": "item",
"itemsUnit": "items",
"remove": "Remove",
"removeAll": "Remove all data",
"removeCurrent": "Remove current page",
"searchPlaceholder": "Search here",
"selectAll": "Select all data",
"selectCurrent": "Select current page",
"selectInvert": "Invert current page",
"titles": Array [
"",
"",
@ -726,8 +840,14 @@ exports[`Transfer should support render value and label in item 1`] = `
onItemSelectAll={[Function]}
onScroll={[Function]}
prefixCls="ant-transfer-list"
remove="Remove"
removeAll="Remove all data"
removeCurrent="Remove current page"
render={[Function]}
searchPlaceholder="Search here"
selectAll="Select all data"
selectCurrent="Select current page"
selectInvert="Invert current page"
showSearch={false}
titleText=""
titles={
@ -781,18 +901,275 @@ exports[`Transfer should support render value and label in item 1`] = `
</Checkbox>
</label>
</Checkbox>
<Dropdown
className="ant-transfer-list-header-dropdown"
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
overlay={
<Menu>
<MenuItem
onClick={[Function]}
>
Select all data
</MenuItem>
<MenuItem
onClick={[Function]}
>
Invert current page
</MenuItem>
</Menu>
}
>
<ForwardRef(Dropdown)
className="ant-transfer-list-header-dropdown"
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
overlay={[Function]}
overlayClassName=""
placement="bottomLeft"
prefixCls="ant-dropdown"
transitionName="slide-up"
>
<Trigger
action={
Array [
"hover",
]
}
afterPopupVisibleChange={[Function]}
blurDelay={0.15}
builtinPlacements={
Object {
"bottomCenter": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tc",
"bc",
],
"targetOffset": Array [
0,
0,
],
},
"bottomLeft": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tl",
"bl",
],
"targetOffset": Array [
0,
0,
],
},
"bottomRight": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tr",
"br",
],
"targetOffset": Array [
0,
0,
],
},
"topCenter": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"bc",
"tc",
],
"targetOffset": Array [
0,
0,
],
},
"topLeft": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"bl",
"tl",
],
"targetOffset": Array [
0,
0,
],
},
"topRight": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"br",
"tr",
],
"targetOffset": Array [
0,
0,
],
},
}
}
className="ant-transfer-list-header-dropdown"
defaultPopupVisible={false}
destroyPopupOnHide={false}
focusDelay={0}
getDocument={[Function]}
getPopupClassNameFromAlign={[Function]}
hideAction={Array []}
mask={false}
maskClosable={true}
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
onPopupAlign={[Function]}
onPopupVisibleChange={[Function]}
overlay={[Function]}
popup={[Function]}
popupAlign={Object {}}
popupClassName=""
popupPlacement="bottomLeft"
popupStyle={Object {}}
popupTransitionName="slide-up"
prefixCls="ant-dropdown"
showAction={Array []}
stretch="minWidth"
>
<ForwardRef(DownOutlined)
className="ant-dropdown-trigger ant-transfer-list-header-dropdown"
key="trigger"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<AntdIcon
className="ant-dropdown-trigger ant-transfer-list-header-dropdown"
icon={
Object {
"icon": Object {
"attrs": Object {
"focusable": "false",
"viewBox": "64 64 896 896",
},
"children": Array [
Object {
"attrs": Object {
"d": "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z",
},
"tag": "path",
},
],
"tag": "svg",
},
"name": "down",
"theme": "outlined",
}
}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<span
aria-label="down"
className="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="img"
>
<IconReact
className=""
icon={
Object {
"icon": Object {
"attrs": Object {
"focusable": "false",
"viewBox": "64 64 896 896",
},
"children": Array [
Object {
"attrs": Object {
"d": "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z",
},
"tag": "path",
},
],
"tag": "svg",
},
"name": "down",
"theme": "outlined",
}
}
>
<svg
aria-hidden="true"
className=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
key="svg-down"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
key="svg-down-svg-0"
/>
</svg>
</IconReact>
</span>
</AntdIcon>
</ForwardRef(DownOutlined)>
</Trigger>
</ForwardRef(Dropdown)>
</Dropdown>
<span
className="ant-transfer-list-header-selected"
>
<span>
1
item
</span>
<span
className="ant-transfer-list-header-title"
/>
1
item
</span>
<span
className="ant-transfer-list-header-title"
/>
</div>
<div
className="ant-transfer-list-body"
@ -838,8 +1215,14 @@ exports[`Transfer should support render value and label in item 1`] = `
onItemSelectAll={[Function]}
onScroll={[Function]}
prefixCls="ant-transfer-list"
remove="Remove"
removeAll="Remove all data"
removeCurrent="Remove current page"
render={[Function]}
searchPlaceholder="Search here"
selectAll="Select all data"
selectCurrent="Select current page"
selectInvert="Invert current page"
selectedKeys={Array []}
showSearch={false}
titleText=""
@ -864,58 +1247,80 @@ exports[`Transfer should support render value and label in item 1`] = `
}
key="a"
onClick={[Function]}
onRemove={[Function]}
prefixCls="ant-transfer-list"
renderedEl="label"
renderedText="title value"
>
<li
className="ant-transfer-list-content-item"
onClick={[Function]}
title="title value"
<LocaleReceiver
componentName="Transfer"
defaultLocale={
Object {
"itemUnit": "item",
"itemsUnit": "items",
"remove": "Remove",
"removeAll": "Remove all data",
"removeCurrent": "Remove current page",
"searchPlaceholder": "Search here",
"selectAll": "Select all data",
"selectCurrent": "Select current page",
"selectInvert": "Invert current page",
"titles": Array [
"",
"",
],
}
}
>
<Checkbox
checked={false}
indeterminate={false}
<li
className="ant-transfer-list-content-item"
onClick={[Function]}
title="title value"
>
<label
className="ant-checkbox-wrapper"
<Checkbox
checked={false}
indeterminate={false}
>
<Checkbox
checked={false}
className=""
defaultChecked={false}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
prefixCls="ant-checkbox"
style={Object {}}
type="checkbox"
<label
className="ant-checkbox-wrapper"
>
<span
className="ant-checkbox"
<Checkbox
checked={false}
className=""
defaultChecked={false}
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
prefixCls="ant-checkbox"
style={Object {}}
type="checkbox"
>
<input
checked={false}
className="ant-checkbox-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="ant-checkbox-inner"
/>
</span>
</Checkbox>
</label>
</Checkbox>
<span
className="ant-transfer-list-content-item-text"
>
label
</span>
</li>
className="ant-checkbox"
style={Object {}}
>
<input
checked={false}
className="ant-checkbox-input"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
type="checkbox"
/>
<span
className="ant-checkbox-inner"
/>
</span>
</Checkbox>
</label>
</Checkbox>
<span
className="ant-transfer-list-content-item-text"
>
label
</span>
</li>
</LocaleReceiver>
</Memo(ListItem)>
</ul>
</ListBody>
@ -1135,12 +1540,19 @@ exports[`Transfer should support render value and label in item 1`] = `
[Function]
</Context.Consumer>
}
onItemRemove={[Function]}
onItemSelect={[Function]}
onItemSelectAll={[Function]}
onScroll={[Function]}
prefixCls="ant-transfer-list"
remove="Remove"
removeAll="Remove all data"
removeCurrent="Remove current page"
render={[Function]}
searchPlaceholder="Search here"
selectAll="Select all data"
selectCurrent="Select current page"
selectInvert="Invert current page"
showSearch={false}
titleText=""
titles={
@ -1194,18 +1606,275 @@ exports[`Transfer should support render value and label in item 1`] = `
</Checkbox>
</label>
</Checkbox>
<Dropdown
className="ant-transfer-list-header-dropdown"
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
overlay={
<Menu>
<MenuItem
onClick={[Function]}
>
Select all data
</MenuItem>
<MenuItem
onClick={[Function]}
>
Invert current page
</MenuItem>
</Menu>
}
>
<ForwardRef(Dropdown)
className="ant-transfer-list-header-dropdown"
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
overlay={[Function]}
overlayClassName=""
placement="bottomLeft"
prefixCls="ant-dropdown"
transitionName="slide-up"
>
<Trigger
action={
Array [
"hover",
]
}
afterPopupVisibleChange={[Function]}
blurDelay={0.15}
builtinPlacements={
Object {
"bottomCenter": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tc",
"bc",
],
"targetOffset": Array [
0,
0,
],
},
"bottomLeft": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tl",
"bl",
],
"targetOffset": Array [
0,
0,
],
},
"bottomRight": Object {
"offset": Array [
0,
4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"tr",
"br",
],
"targetOffset": Array [
0,
0,
],
},
"topCenter": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"bc",
"tc",
],
"targetOffset": Array [
0,
0,
],
},
"topLeft": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"bl",
"tl",
],
"targetOffset": Array [
0,
0,
],
},
"topRight": Object {
"offset": Array [
0,
-4,
],
"overflow": Object {
"adjustX": 1,
"adjustY": 1,
},
"points": Array [
"br",
"tr",
],
"targetOffset": Array [
0,
0,
],
},
}
}
className="ant-transfer-list-header-dropdown"
defaultPopupVisible={false}
destroyPopupOnHide={false}
focusDelay={0}
getDocument={[Function]}
getPopupClassNameFromAlign={[Function]}
hideAction={Array []}
mask={false}
maskClosable={true}
mouseEnterDelay={0.15}
mouseLeaveDelay={0.1}
onPopupAlign={[Function]}
onPopupVisibleChange={[Function]}
overlay={[Function]}
popup={[Function]}
popupAlign={Object {}}
popupClassName=""
popupPlacement="bottomLeft"
popupStyle={Object {}}
popupTransitionName="slide-up"
prefixCls="ant-dropdown"
showAction={Array []}
stretch="minWidth"
>
<ForwardRef(DownOutlined)
className="ant-dropdown-trigger ant-transfer-list-header-dropdown"
key="trigger"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<AntdIcon
className="ant-dropdown-trigger ant-transfer-list-header-dropdown"
icon={
Object {
"icon": Object {
"attrs": Object {
"focusable": "false",
"viewBox": "64 64 896 896",
},
"children": Array [
Object {
"attrs": Object {
"d": "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z",
},
"tag": "path",
},
],
"tag": "svg",
},
"name": "down",
"theme": "outlined",
}
}
onMouseEnter={[Function]}
onMouseLeave={[Function]}
>
<span
aria-label="down"
className="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
onMouseEnter={[Function]}
onMouseLeave={[Function]}
role="img"
>
<IconReact
className=""
icon={
Object {
"icon": Object {
"attrs": Object {
"focusable": "false",
"viewBox": "64 64 896 896",
},
"children": Array [
Object {
"attrs": Object {
"d": "M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z",
},
"tag": "path",
},
],
"tag": "svg",
},
"name": "down",
"theme": "outlined",
}
}
>
<svg
aria-hidden="true"
className=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
key="svg-down"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
key="svg-down-svg-0"
/>
</svg>
</IconReact>
</span>
</AntdIcon>
</ForwardRef(DownOutlined)>
</Trigger>
</ForwardRef(Dropdown)>
</Dropdown>
<span
className="ant-transfer-list-header-selected"
>
<span>
0
item
</span>
<span
className="ant-transfer-list-header-title"
/>
0
item
</span>
<span
className="ant-transfer-list-header-title"
/>
</div>
<div
className="ant-transfer-list-body"

View File

@ -23,16 +23,34 @@ exports[`Transfer.List should render correctly 1`] = `
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
<span>
1/3
</span>
<span
class="ant-transfer-list-header-title"
/>
1/3
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"

View File

@ -13,7 +13,7 @@ describe('Transfer.Customize', () => {
errorSpy.mockRestore();
});
it('props#body doesnot work anymore', () => {
it('props#body does not work anymore', () => {
const body = jest.fn();
mount(<Transfer body={body} />);
@ -49,4 +49,16 @@ describe('Transfer.Customize', () => {
);
});
});
it('warning if use `pagination`', () => {
mount(
<Transfer dataSource={[]} pagination>
{() => null}
</Transfer>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Transfer] `pagination` not support customize render list.',
);
});
});

View File

@ -0,0 +1,110 @@
/* eslint no-use-before-define: "off" */
import React from 'react';
import { act } from 'react-dom/test-utils';
import { mount } from 'enzyme';
import Transfer from '..';
const listProps = {
dataSource: [
{
key: 'a',
title: 'a',
disabled: true,
},
{
key: 'b',
title: 'b',
},
{
key: 'c',
title: 'c',
},
{
key: 'd',
title: 'd',
},
{
key: 'e',
title: 'e',
},
],
selectedKeys: ['b'],
targetKeys: [],
pagination: { pageSize: 4 },
};
describe('Transfer.Dropdown', () => {
function clickItem(wrapper, index) {
wrapper.find('li.ant-dropdown-menu-item').at(index).simulate('click');
}
it('select all', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const wrapper = mount(<Transfer {...listProps} onSelectChange={onSelectChange} />);
wrapper.find('.ant-transfer-list-header-dropdown').first().simulate('mouseenter');
act(() => {
jest.runAllTimers();
});
wrapper.update();
clickItem(wrapper.find('.ant-dropdown-menu').first(), 0);
expect(onSelectChange).toHaveBeenCalledWith(['b', 'c', 'd', 'e'], []);
jest.useRealTimers();
});
it('select current page', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const wrapper = mount(<Transfer {...listProps} onSelectChange={onSelectChange} />);
wrapper.find('.ant-transfer-list-header-dropdown').first().simulate('mouseenter');
act(() => {
jest.runAllTimers();
});
wrapper.update();
clickItem(wrapper.find('.ant-dropdown-menu').first(), 1);
expect(onSelectChange).toHaveBeenCalledWith(['b', 'c', 'd'], []);
jest.useRealTimers();
});
it('select invert', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const wrapper = mount(<Transfer {...listProps} onSelectChange={onSelectChange} />);
wrapper.find('.ant-transfer-list-header-dropdown').first().simulate('mouseenter');
act(() => {
jest.runAllTimers();
});
wrapper.update();
clickItem(wrapper.find('.ant-dropdown-menu').first(), 2);
expect(onSelectChange).toHaveBeenCalledWith(['c', 'd'], []);
jest.useRealTimers();
});
it('oneWay to remove', () => {
jest.useFakeTimers();
const onChange = jest.fn();
const wrapper = mount(
<Transfer {...listProps} targetKeys={['b', 'c']} oneWay onChange={onChange} />,
);
wrapper.find('.ant-transfer-list-header-dropdown').last().simulate('mouseenter');
act(() => {
jest.runAllTimers();
});
wrapper.update();
clickItem(wrapper.find('.ant-dropdown-menu').first(), 0);
expect(onChange).toHaveBeenCalledWith([], 'left', ['b', 'c']);
jest.useRealTimers();
});
});

View File

@ -104,11 +104,7 @@ describe('Transfer', () => {
it('should move selected keys to corresponding list', () => {
const handleChange = jest.fn();
const wrapper = mount(<Transfer {...listCommonProps} onChange={handleChange} />);
wrapper
.find(TransferOperation)
.find(Button)
.at(0)
.simulate('click'); // move selected keys to right list
wrapper.find(TransferOperation).find(Button).at(0).simulate('click'); // move selected keys to right list
expect(handleChange).toHaveBeenCalledWith(['a', 'b'], 'right', ['a']);
});
@ -122,22 +118,14 @@ describe('Transfer', () => {
onChange={handleChange}
/>,
);
wrapper
.find(TransferOperation)
.find(Button)
.at(1)
.simulate('click'); // move selected keys to left list
wrapper.find(TransferOperation).find(Button).at(1).simulate('click'); // move selected keys to left list
expect(handleChange).toHaveBeenCalledWith([], 'left', ['a']);
});
it('should move selected keys expect disabled to corresponding list', () => {
const handleChange = jest.fn();
const wrapper = mount(<Transfer {...listDisabledProps} onChange={handleChange} />);
wrapper
.find(TransferOperation)
.find(Button)
.at(0)
.simulate('click'); // move selected keys to right list
wrapper.find(TransferOperation).find(Button).at(0).simulate('click'); // move selected keys to right list
expect(handleChange).toHaveBeenCalledWith(['b'], 'right', ['b']);
});
@ -211,20 +199,14 @@ describe('Transfer', () => {
.at(0)
.find('input')
.simulate('change', { target: { value: 'a' } });
expect(
wrapper
.find(TransferList)
.at(0)
.find(TransferItem)
.find(Checkbox),
).toHaveLength(1);
expect(wrapper.find(TransferList).at(0).find(TransferItem).find(Checkbox)).toHaveLength(1);
});
const headerText = wrapper =>
wrapper
.find(TransferList)
.at(0)
.find('.ant-transfer-list-header-selected > span')
.find('.ant-transfer-list-header-selected')
.at(0)
.first()
.text()
@ -259,21 +241,11 @@ describe('Transfer', () => {
expect(headerText(wrapper)).toEqual('0 Person');
expect(
wrapper
.find(TransferList)
.at(0)
.find('.ant-transfer-list-search')
.at(0)
.prop('placeholder'),
wrapper.find(TransferList).at(0).find('.ant-transfer-list-search').at(0).prop('placeholder'),
).toEqual('Search');
expect(
wrapper
.find(TransferList)
.at(0)
.find('.ant-transfer-list-body-not-found')
.at(0)
.text(),
wrapper.find(TransferList).at(0).find('.ant-transfer-list-body-not-found').at(0).text(),
).toEqual('Nothing');
});
@ -292,21 +264,11 @@ describe('Transfer', () => {
);
expect(
wrapper
.find(TransferList)
.at(0)
.find('.ant-transfer-list-search')
.at(0)
.prop('placeholder'),
wrapper.find(TransferList).at(0).find('.ant-transfer-list-search').at(0).prop('placeholder'),
).toEqual('new2');
expect(
wrapper
.find(TransferList)
.at(0)
.find('.ant-transfer-list-body-not-found')
.at(0)
.text(),
wrapper.find(TransferList).at(0).find('.ant-transfer-list-body-not-found').at(0).text(),
).toEqual('new1');
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
@ -378,11 +340,7 @@ describe('Transfer', () => {
.find('.ant-transfer-list-header input[type="checkbox"]')
.filterWhere(n => !n.prop('checked'))
.simulate('change');
wrapper
.find(TransferOperation)
.find(Button)
.at(0)
.simulate('click');
wrapper.find(TransferOperation).find(Button).at(0).simulate('click');
expect(handleChange).toHaveBeenCalledWith(['1', '3', '4'], 'right', ['1']);
});
@ -423,7 +381,7 @@ describe('Transfer', () => {
expect(handleSelectChange).toHaveBeenLastCalledWith(['b'], []);
});
it('should show sorted targetkey', () => {
it('should show sorted targetKey', () => {
const sortedTargetKeyProps = {
dataSource: [
{
@ -537,4 +495,51 @@ describe('Transfer', () => {
const wrapper = mount(<Transfer {...listCommonProps} selectAllLabels={selectAllLabels} />);
expect(headerText(wrapper)).toEqual('1 of 2');
});
describe('pagination', () => {
it('boolean', () => {
const wrapper = mount(<Transfer {...listDisabledProps} pagination />);
expect(wrapper.find('Pagination').first().props()).toEqual(
expect.objectContaining({
pageSize: 10,
}),
);
});
it('object', () => {
const wrapper = mount(<Transfer {...listDisabledProps} pagination={{ pageSize: 1 }} />);
expect(
wrapper.find('.ant-transfer-list').first().find('.ant-transfer-list-content-item'),
).toHaveLength(1);
expect(wrapper.find('Pagination').first().props()).toEqual(
expect.objectContaining({
pageSize: 1,
}),
);
});
it('not exceed max size', () => {
const wrapper = mount(<Transfer {...listDisabledProps} pagination={{ pageSize: 1 }} />);
wrapper.find('.ant-pagination-next .ant-pagination-item-link').first().simulate('click');
expect(wrapper.find('Pagination').first().props()).toEqual(
expect.objectContaining({
current: 2,
}),
);
wrapper.setProps({ targetKeys: ['b', 'c'] });
expect(wrapper.find('Pagination').first().props()).toEqual(
expect.objectContaining({
current: 1,
}),
);
});
});
it('remove by click icon', () => {
const onChange = jest.fn();
const wrapper = mount(<Transfer {...listCommonProps} onChange={onChange} oneWay />);
wrapper.find('.ant-transfer-list-content-item-remove').first().simulate('click');
expect(onChange).toHaveBeenCalledWith([], 'left', ['b']);
});
});

View File

@ -14,7 +14,7 @@ title:
The most basic usage of `Transfer` involves providing the source data and target keys arrays, plus the rendering and some callback functions.
```jsx
import { Transfer, Switch } from 'antd';
import { Space, Transfer, Switch } from 'antd';
const mockData = [];
for (let i = 0; i < 20; i++) {
@ -74,13 +74,14 @@ class App extends React.Component {
render={item => item.title}
disabled={disabled}
/>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={this.handleDisable}
style={{ marginTop: 16 }}
/>
<Space style={{ marginTop: 16 }}>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={this.handleDisable}
/>
</Space>
</div>
);
}

View File

@ -1,35 +1,29 @@
---
order: 4
debug: true
title:
zh-CN: 大数据性能测试
en-US: Performance Test
zh-CN: 分页
en-US: Pagination
---
## zh-CN
2000 条数据
大数据下使用分页
## en-US
2000 items.
large count of items with pagination.
```jsx
import { Transfer } from 'antd';
import { Transfer, Switch } from 'antd';
class App extends React.Component {
state = {
mockData: [],
targetKeys: [],
};
const App = () => {
const [oneWay, setOneWay] = React.useState(false);
const [mockData, setMockData] = React.useState([]);
const [targetKeys, setTargetKeys] = React.useState([]);
componentDidMount() {
this.getMock();
}
getMock = () => {
const targetKeys = [];
const mockData = [];
React.useEffect(() => {
const newTargetKeys = [];
const newMockData = [];
for (let i = 0; i < 2000; i++) {
const data = {
key: i.toString(),
@ -38,29 +32,40 @@ class App extends React.Component {
chosen: Math.random() * 2 > 1,
};
if (data.chosen) {
targetKeys.push(data.key);
newTargetKeys.push(data.key);
}
mockData.push(data);
newMockData.push(data);
}
this.setState({ mockData, targetKeys });
setTargetKeys(newTargetKeys);
setMockData(newMockData);
}, []);
const onChange = (newTargetKeys, direction, moveKeys) => {
console.log(newTargetKeys, direction, moveKeys);
setTargetKeys(newTargetKeys);
};
handleChange = (targetKeys, direction, moveKeys) => {
console.log(targetKeys, direction, moveKeys);
this.setState({ targetKeys });
};
render() {
return (
return (
<>
<Transfer
dataSource={this.state.mockData}
targetKeys={this.state.targetKeys}
onChange={this.handleChange}
dataSource={mockData}
targetKeys={targetKeys}
onChange={onChange}
render={item => item.title}
oneWay={oneWay}
pagination
/>
);
}
}
<br />
<Switch
unCheckedChildren="one way"
checkedChildren="one way"
checked={oneWay}
onChange={setOneWay}
/>
</>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -0,0 +1,92 @@
---
order: 0.1
title:
zh-CN: 单向样式
en-US: One Way
---
## zh-CN
通过 `oneWay` 将 Transfer 转为单向样式。
## en-US
Use `oneWay` to makes Transfer to one way style.
```jsx
import { Space, Transfer, Switch } from 'antd';
const mockData = [];
for (let i = 0; i < 20; i++) {
mockData.push({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 3 < 1,
});
}
const oriTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
class App extends React.Component {
state = {
targetKeys: oriTargetKeys,
selectedKeys: [],
disabled: false,
};
handleChange = (nextTargetKeys, direction, moveKeys) => {
this.setState({ targetKeys: nextTargetKeys });
console.log('targetKeys: ', nextTargetKeys);
console.log('direction: ', direction);
console.log('moveKeys: ', moveKeys);
};
handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => {
this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] });
console.log('sourceSelectedKeys: ', sourceSelectedKeys);
console.log('targetSelectedKeys: ', targetSelectedKeys);
};
handleScroll = (direction, e) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
handleDisable = disabled => {
this.setState({ disabled });
};
render() {
const { targetKeys, selectedKeys, disabled } = this.state;
return (
<div>
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={this.handleChange}
onSelectChange={this.handleSelectChange}
onScroll={this.handleScroll}
render={item => item.title}
disabled={disabled}
oneWay
/>
<Space style={{ marginTop: 16 }}>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={this.handleDisable}
/>
</Space>
</div>
);
}
}
ReactDOM.render(<App />, mountNode);
```

View File

@ -16,19 +16,17 @@ Customize render list with Tree component.
```jsx
import { Transfer, Tree } from 'antd';
const { TreeNode } = Tree;
// Customize Table Transfer
const isChecked = (selectedKeys, eventKey) => {
return selectedKeys.indexOf(eventKey) !== -1;
};
const generateTree = (treeNodes = [], checkedKeys = []) => {
return treeNodes.map(({ children, ...props }) => (
<TreeNode {...props} disabled={checkedKeys.includes(props.key)} key={props.key}>
{generateTree(children, checkedKeys)}
</TreeNode>
));
return treeNodes.map(({ children, ...props }) => ({
...props,
disabled: checkedKeys.includes(props.key),
children: generateTree(children, checkedKeys),
}));
};
const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
@ -60,29 +58,14 @@ const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
checkStrictly
defaultExpandAll
checkedKeys={checkedKeys}
onCheck={(
_,
{
node: {
props: { eventKey },
},
},
) => {
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
treeData={generateTree(dataSource, targetKeys)}
onCheck={(_, { node: { key } }) => {
onItemSelect(key, !isChecked(checkedKeys, key));
}}
onSelect={(
_,
{
node: {
props: { eventKey },
},
},
) => {
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
onSelect={(_, { node: { key } }) => {
onItemSelect(key, !isChecked(checkedKeys, key));
}}
>
{generateTree(dataSource, targetKeys)}
</Tree>
/>
);
}
}}
@ -95,7 +78,10 @@ const treeData = [
{
key: '0-1',
title: '0-1',
children: [{ key: '0-1-0', title: '0-1-0' }, { key: '0-1-1', title: '0-1-1' }],
children: [
{ key: '0-1-0', title: '0-1-0' },
{ key: '0-1-1', title: '0-1-1' },
],
},
{ key: '0-2', title: '0-3' },
];

View File

@ -20,7 +20,6 @@ One or more elements can be selected from either column, one click on the proper
| Property | Description | Type | Default | Version |
| --- | --- | --- | --- | --- |
| className | A custom CSS class. | string | \['', ''] | |
| dataSource | Used for setting the source data. The elements that are part of this array will be present the left column. Except the elements whose keys are included in `targetKeys` prop. | [TransferItem](https://git.io/vMM64)\[] | \[] | |
| disabled | Whether disabled transfer | boolean | false | |
| filterOption | A function to determine whether an item should show in search result list | (inputValue, option): boolean | | |
@ -28,12 +27,13 @@ One or more elements can be selected from either column, one click on the proper
| listStyle | A custom CSS style used for rendering the transfer columns. | object\|({direction: 'left'\|'right'}) => object | | |
| locale | i18n text including filter, empty text, item unit, etc | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; } | `{ itemUnit: 'item', itemsUnit: 'items', notFoundContent: 'The list is empty', searchPlaceholder: 'Search here' }` | |
| operations | A set of operations that are sorted from top to bottom. | string\[] | \['>', '<'] | |
| oneWay | Display as single direction style | boolean | false | 4.3.0 |
| operationStyle | A custom CSS style used for rendering the operations column. | object | | |
| pagination | Use pagination. Not work in render props | boolean \| { pageSize: number } | false | 4.3.0 |
| render | The function to generate the item shown on a column. Based on an record (element of the dataSource array), this function should return a React element which is generated from that record. Also, it can return a plain object with `value` and `label`, `label` is a React element and `value` is for title | (record) => ReactNode | | |
| selectedKeys | A set of keys of selected items. | string\[] | \[] | |
| showSearch | If included, a search box is shown on each column. | boolean | false | |
| showSelectAll | Show select all checkbox on the header | boolean | true | |
| style | A custom CSS style used for rendering wrapper element. | CSSProperties | | |
| targetKeys | A set of keys of elements that are listed on the right column. | string\[] | \[] | |
| titles | A set of titles that are sorted from left to right. | ReactNode\[] | - | |
| selectAllLabels | A set of customized labels for select all checkboxs on the header | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | | |

View File

@ -6,7 +6,9 @@ import Search from './search';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale/default';
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
import { TransferListBodyProps } from './renderListBody';
import { TransferListBodyProps } from './ListBody';
import { PaginationType } from './interface';
import warning from '../_util/warning';
export { TransferListProps } from './list';
export { TransferOperationProps } from './operation';
@ -64,6 +66,8 @@ export interface TransferProps {
children?: (props: TransferListBodyProps) => React.ReactNode;
showSelectAll?: boolean;
selectAllLabels?: SelectAllLabel[];
oneWay?: boolean;
pagination?: PaginationType;
}
export interface TransferLocale {
@ -72,9 +76,20 @@ export interface TransferLocale {
searchPlaceholder: string;
itemUnit: string;
itemsUnit: string;
remove: string;
selectAll: string;
selectCurrent: string;
selectInvert: string;
removeAll: string;
removeCurrent: string;
}
class Transfer extends React.Component<TransferProps, any> {
interface TransferState {
sourceSelectedKeys: string[];
targetSelectedKeys: string[];
}
class Transfer extends React.Component<TransferProps, TransferState> {
// For high-level customized Transfer @dqaria
static List = List;
@ -89,14 +104,26 @@ class Transfer extends React.Component<TransferProps, any> {
listStyle: () => {},
};
static getDerivedStateFromProps(nextProps: TransferProps) {
if (nextProps.selectedKeys) {
const targetKeys = nextProps.targetKeys || [];
static getDerivedStateFromProps({
selectedKeys,
targetKeys,
pagination,
children,
}: TransferProps) {
if (selectedKeys) {
const mergedTargetKeys = targetKeys || [];
return {
sourceSelectedKeys: nextProps.selectedKeys.filter(key => !targetKeys.includes(key)),
targetSelectedKeys: nextProps.selectedKeys.filter(key => targetKeys.includes(key)),
sourceSelectedKeys: selectedKeys.filter(key => !mergedTargetKeys.includes(key)),
targetSelectedKeys: selectedKeys.filter(key => mergedTargetKeys.includes(key)),
};
}
warning(
!pagination || !children,
'Transfer',
'`pagination` not support customize render list.',
);
return null;
}
@ -115,10 +142,20 @@ class Transfer extends React.Component<TransferProps, any> {
};
}
// eslint-disable-next-line class-methods-use-this
getSelectedKeysName(direction: TransferDirection) {
return direction === 'left' ? 'sourceSelectedKeys' : 'targetSelectedKeys';
}
setStateKeys = (
direction: TransferDirection,
keys: string[] | ((prevKeys: string[]) => string[]),
) => {
if (direction === 'left') {
this.setState(({ sourceSelectedKeys }) => ({
sourceSelectedKeys: typeof keys === 'function' ? keys(sourceSelectedKeys || []) : keys,
}));
} else {
this.setState(({ targetSelectedKeys }) => ({
targetSelectedKeys: typeof keys === 'function' ? keys(targetSelectedKeys || []) : keys,
}));
}
};
getTitles(transferLocale: TransferLocale): string[] {
const { titles } = this.props;
@ -148,9 +185,7 @@ class Transfer extends React.Component<TransferProps, any> {
// empty checked keys
const oppositeDirection = direction === 'right' ? 'left' : 'right';
this.setState({
[this.getSelectedKeysName(oppositeDirection)]: [],
});
this.setStateKeys(oppositeDirection, []);
this.handleSelectChange(oppositeDirection, []);
if (onChange) {
@ -163,26 +198,20 @@ class Transfer extends React.Component<TransferProps, any> {
moveToRight = () => this.moveTo('right');
onItemSelectAll = (direction: TransferDirection, selectedKeys: string[], checkAll: boolean) => {
const originalSelectedKeys = this.state[this.getSelectedKeysName(direction)] || [];
this.setStateKeys(direction, prevKeys => {
let mergedCheckedKeys = [];
if (checkAll) {
// Merge current keys with origin key
mergedCheckedKeys = Array.from(new Set([...prevKeys, ...selectedKeys]));
} else {
// Remove current keys from origin keys
mergedCheckedKeys = prevKeys.filter((key: string) => selectedKeys.indexOf(key) === -1);
}
let mergedCheckedKeys = [];
if (checkAll) {
// Merge current keys with origin key
mergedCheckedKeys = Array.from(new Set([...originalSelectedKeys, ...selectedKeys]));
} else {
// Remove current keys from origin keys
mergedCheckedKeys = originalSelectedKeys.filter(
(key: string) => selectedKeys.indexOf(key) === -1,
);
}
this.handleSelectChange(direction, mergedCheckedKeys);
this.handleSelectChange(direction, mergedCheckedKeys);
if (!this.props.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: mergedCheckedKeys,
});
}
return mergedCheckedKeys;
});
};
onLeftItemSelectAll = (selectedKeys: string[], checkAll: boolean) =>
@ -227,9 +256,7 @@ class Transfer extends React.Component<TransferProps, any> {
this.handleSelectChange(direction, holder);
if (!this.props.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: holder,
});
this.setStateKeys(direction, holder);
}
};
@ -239,6 +266,20 @@ class Transfer extends React.Component<TransferProps, any> {
onRightItemSelect = (selectedKey: string, checked: boolean) =>
this.onItemSelect('right', selectedKey, checked);
onRightItemRemove = (selectedKeys: string[]) => {
const { targetKeys = [], onChange } = this.props;
this.setStateKeys('right', []);
if (onChange) {
onChange(
targetKeys.filter(key => !selectedKeys.includes(key)),
'left',
[...selectedKeys],
);
}
};
handleScroll = (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => {
const { onScroll } = this.props;
if (onScroll) {
@ -317,11 +358,15 @@ class Transfer extends React.Component<TransferProps, any> {
render,
children,
showSelectAll,
oneWay,
pagination,
} = this.props;
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
const locale = this.getLocale(transferLocale, renderEmpty);
const { sourceSelectedKeys, targetSelectedKeys } = this.state;
const mergedPagination = !children && pagination;
const { leftDataSource, rightDataSource } = this.separateDataSource();
const leftActive = targetSelectedKeys.length > 0;
const rightActive = sourceSelectedKeys.length > 0;
@ -356,6 +401,7 @@ class Transfer extends React.Component<TransferProps, any> {
direction="left"
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[0]}
pagination={mergedPagination}
{...locale}
/>
<Operation
@ -369,6 +415,7 @@ class Transfer extends React.Component<TransferProps, any> {
style={operationStyle}
disabled={disabled}
direction={direction}
oneWay={oneWay}
/>
<List
prefixCls={`${prefixCls}-list`}
@ -381,6 +428,7 @@ class Transfer extends React.Component<TransferProps, any> {
handleClear={this.handleRightClear}
onItemSelect={this.onRightItemSelect}
onItemSelectAll={this.onRightItemSelectAll}
onItemRemove={this.onRightItemRemove}
render={render}
showSearch={showSearch}
renderList={children}
@ -390,6 +438,8 @@ class Transfer extends React.Component<TransferProps, any> {
direction="right"
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[1]}
showRemove={oneWay}
pagination={mergedPagination}
{...locale}
/>
</div>

View File

@ -23,19 +23,19 @@ title: Transfer
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| className | 自定义类 | string | | |
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] | |
| disabled | 是否禁用 | boolean | false | |
| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | | |
| footer | 底部渲染函数 | (props) => ReactNode | | |
| listStyle | 两个穿梭框的自定义样式 | object\|({direction: 'left'\|'right'}) => object | | |
| locale | 各种语言 | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode; } | `{ itemUnit: '项', itemsUnit: '项', searchPlaceholder: '请输入搜索内容' }` | |
| oneWay | 展示为单向样式 | boolean | false | 4.3.0 |
| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] | |
| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number } | false | 4.3.0 |
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象其中 `label` 字段为 ReactElement`value` 字段为 title | (record) => ReactNode | | |
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
| showSearch | 是否显示搜索框 | boolean | false | |
| showSelectAll | 是否展示全选勾选框 | boolean | true | |
| style | 容器的自定义样式 | CSSProperties | | |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
| titles | 标题集合,顺序从左至右 | ReactNode\[] | \['', ''] | |
| selectAllLabels | 自定义顶部多选框标题的集合 | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | | |

View File

@ -0,0 +1,5 @@
export type PaginationType =
| boolean
| {
pageSize?: number;
};

View File

@ -1,16 +1,21 @@
import * as React from 'react';
import omit from 'omit.js';
import classNames from 'classnames';
import { DownOutlined } from '@ant-design/icons';
import Checkbox from '../checkbox';
import Menu from '../menu';
import Dropdown from '../dropdown';
import {
TransferItem,
TransferDirection,
RenderResult,
RenderResultObject,
SelectAllLabel,
TransferLocale,
} from './index';
import Search from './search';
import defaultRenderList, { TransferListBodyProps, OmitProps } from './renderListBody';
import DefaultListBody, { TransferListBodyProps, OmitProps } from './ListBody';
import { PaginationType } from './interface';
const defaultRender = () => null;
@ -22,6 +27,10 @@ function isRenderResultPlainObject(result: RenderResult) {
);
}
function getEnabledItemKeys(items: TransferItem[]) {
return items.filter(data => !data.disabled).map(data => data.key);
}
export interface RenderedItem {
renderedText: string;
renderedEl: React.ReactNode;
@ -30,7 +39,7 @@ export interface RenderedItem {
type RenderListFunction = (props: TransferListBodyProps) => React.ReactNode;
export interface TransferListProps {
export interface TransferListProps extends TransferLocale {
prefixCls: string;
titleText: string;
dataSource: TransferItem[];
@ -40,11 +49,12 @@ export interface TransferListProps {
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
onItemSelect: (key: string, check: boolean) => void;
onItemSelectAll: (dataSource: string[], checkAll: boolean) => void;
onItemRemove?: (keys: string[]) => void;
handleClear: () => void;
/** render item */
render?: (item: TransferItem) => RenderResult;
showSearch?: boolean;
searchPlaceholder: string;
notFoundContent: React.ReactNode;
itemUnit: string;
itemsUnit: string;
renderList?: RenderListFunction;
@ -54,6 +64,8 @@ export interface TransferListProps {
direction: TransferDirection;
showSelectAll?: boolean;
selectAllLabel?: SelectAllLabel;
showRemove?: boolean;
pagination?: PaginationType;
}
interface TransferListState {
@ -61,18 +73,6 @@ interface TransferListState {
filterValue: string;
}
function renderListNode(renderList: RenderListFunction | undefined, props: TransferListBodyProps) {
let bodyContent: React.ReactNode = renderList ? renderList(props) : null;
const customize: boolean = !!bodyContent;
if (!customize) {
bodyContent = defaultRenderList(props);
}
return {
customize,
bodyContent,
};
}
export default class TransferList extends React.PureComponent<
TransferListProps,
TransferListState
@ -87,6 +87,8 @@ export default class TransferList extends React.PureComponent<
triggerScrollTimer: number;
defaultListBodyRef = React.createRef<DefaultListBody>();
constructor(props: TransferListProps) {
super(props);
this.state = {
@ -109,6 +111,7 @@ export default class TransferList extends React.PureComponent<
return 'part';
}
// ================================ Item ================================
getFilteredItems(
dataSource: TransferItem[],
filterValue: string,
@ -132,6 +135,46 @@ export default class TransferList extends React.PureComponent<
return { filteredItems, filteredRenderItems };
}
// =============================== Filter ===============================
handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
const { handleFilter } = this.props;
const {
target: { value: filterValue },
} = e;
this.setState({ filterValue });
handleFilter(e);
};
handleClear = () => {
const { handleClear } = this.props;
this.setState({ filterValue: '' });
handleClear();
};
matchFilter = (text: string, item: TransferItem) => {
const { filterValue } = this.state;
const { filterOption } = this.props;
if (filterOption) {
return filterOption(filterValue, item);
}
return text.indexOf(filterValue) >= 0;
};
getCurrentPageItems = () => {};
// =============================== Render ===============================
renderListBody = (renderList: RenderListFunction | undefined, props: TransferListBodyProps) => {
let bodyContent: React.ReactNode = renderList ? renderList(props) : null;
const customize: boolean = !!bodyContent;
if (!customize) {
bodyContent = <DefaultListBody ref={this.defaultListBodyRef} {...props} />;
}
return {
customize,
bodyContent,
};
};
getListBody(
prefixCls: string,
searchPlaceholder: string,
@ -157,7 +200,7 @@ export default class TransferList extends React.PureComponent<
</div>
) : null;
const { bodyContent, customize } = renderListNode(renderList, {
const { bodyContent, customize } = this.renderListBody(renderList, {
...omit(this.props, OmitProps),
filteredItems,
filteredRenderItems,
@ -214,30 +257,6 @@ export default class TransferList extends React.PureComponent<
return checkAllCheckbox;
}
handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
const { handleFilter } = this.props;
const {
target: { value: filterValue },
} = e;
this.setState({ filterValue });
handleFilter(e);
};
handleClear = () => {
const { handleClear } = this.props;
this.setState({ filterValue: '' });
handleClear();
};
matchFilter = (text: string, item: TransferItem) => {
const { filterValue } = this.state;
const { filterOption } = this.props;
if (filterOption) {
return filterOption(filterValue, item);
}
return text.indexOf(filterValue) >= 0;
};
renderItem = (item: TransferItem): RenderedItem => {
const { render = defaultRender } = this.props;
const renderResult: RenderResult = render(item);
@ -279,16 +298,25 @@ export default class TransferList extends React.PureComponent<
style,
searchPlaceholder,
notFoundContent,
selectAll,
selectCurrent,
selectInvert,
removeAll,
removeCurrent,
renderList,
onItemSelectAll,
onItemRemove,
showSelectAll,
showRemove,
pagination,
} = this.props;
// Custom Layout
const footerDom = footer && footer(this.props);
const listCls = classNames(prefixCls, {
[`${prefixCls}-with-footer`]: !!footerDom,
[`${prefixCls}-with-pagination`]: pagination,
[`${prefixCls}-with-footer`]: footerDom,
});
// ====================== Get filtered, checked item list ======================
@ -313,11 +341,97 @@ export default class TransferList extends React.PureComponent<
// ================================ List Footer ================================
const listFooter = footerDom ? <div className={`${prefixCls}-footer`}>{footerDom}</div> : null;
const checkAllCheckbox = this.getCheckBox(
filteredItems,
onItemSelectAll,
showSelectAll,
disabled,
const checkAllCheckbox =
!showRemove &&
!pagination &&
this.getCheckBox(filteredItems, onItemSelectAll, showSelectAll, disabled);
let menu: React.ReactElement | null = null;
if (showRemove) {
menu = (
<Menu>
{/* Remove Current Page */}
{pagination && (
<Menu.Item
onClick={() => {
const pageKeys = getEnabledItemKeys(
(this.defaultListBodyRef.current?.getItems() || []).map(entity => entity.item),
);
onItemRemove?.(pageKeys);
}}
>
{removeCurrent}
</Menu.Item>
)}
{/* Remove All */}
<Menu.Item
onClick={() => {
onItemRemove?.(getEnabledItemKeys(filteredItems));
}}
>
{removeAll}
</Menu.Item>
</Menu>
);
} else {
menu = (
<Menu>
<Menu.Item
onClick={() => {
const keys = getEnabledItemKeys(filteredItems);
onItemSelectAll(keys, keys.length !== checkedKeys.length);
}}
>
{selectAll}
</Menu.Item>
{pagination && (
<Menu.Item
onClick={() => {
const pageItems = this.defaultListBodyRef.current?.getItems() || [];
onItemSelectAll(getEnabledItemKeys(pageItems.map(entity => entity.item)), true);
}}
>
{selectCurrent}
</Menu.Item>
)}
<Menu.Item
onClick={() => {
let availableKeys: string[];
if (pagination) {
availableKeys = getEnabledItemKeys(
(this.defaultListBodyRef.current?.getItems() || []).map(entity => entity.item),
);
} else {
availableKeys = getEnabledItemKeys(filteredItems);
}
const checkedKeySet = new Set(checkedKeys);
const newCheckedKeys: string[] = [];
const newUnCheckedKeys: string[] = [];
availableKeys.forEach(key => {
if (checkedKeySet.has(key)) {
newUnCheckedKeys.push(key);
} else {
newCheckedKeys.push(key);
}
});
onItemSelectAll(newCheckedKeys, true);
onItemSelectAll(newUnCheckedKeys, false);
}}
>
{selectInvert}
</Menu.Item>
</Menu>
);
}
const dropdown = (
<Dropdown className={`${prefixCls}-header-dropdown`} overlay={menu} disabled={disabled}>
<DownOutlined />
</Dropdown>
);
// ================================== Render ===================================
@ -326,10 +440,12 @@ export default class TransferList extends React.PureComponent<
{/* Header */}
<div className={`${prefixCls}-header`}>
{checkAllCheckbox}
{dropdown}
<span className={`${prefixCls}-header-selected`}>
<span>{this.getSelectAllLabel(checkedKeys.length, filteredItems.length)}</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span>
{this.getSelectAllLabel(checkedKeys.length, filteredItems.length)}
</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span>
</div>
{/* Body */}

View File

@ -14,6 +14,7 @@ export interface TransferOperationProps {
style?: React.CSSProperties;
disabled?: boolean;
direction?: 'ltr' | 'rtl';
oneWay?: boolean;
}
const Operation = ({
@ -27,6 +28,7 @@ const Operation = ({
className,
style,
direction,
oneWay,
}: TransferOperationProps) => (
<div className={className} style={style}>
<Button
@ -38,15 +40,17 @@ const Operation = ({
>
{rightArrowText}
</Button>
<Button
type="primary"
size="small"
disabled={disabled || !leftActive}
onClick={moveToLeft}
icon={direction !== 'rtl' ? <LeftOutlined /> : <RightOutlined />}
>
{leftArrowText}
</Button>
{!oneWay && (
<Button
type="primary"
size="small"
disabled={disabled || !leftActive}
onClick={moveToLeft}
icon={direction !== 'rtl' ? <LeftOutlined /> : <RightOutlined />}
>
{leftArrowText}
</Button>
)}
</div>
);

View File

@ -1,59 +0,0 @@
import * as React from 'react';
import { ElementOf, Omit, tuple } from '../_util/type';
import { TransferItem } from '.';
import { TransferListProps, RenderedItem } from './list';
import ListItem from './ListItem';
export const OmitProps = tuple('handleFilter', 'handleClear', 'checkedKeys');
export type OmitProp = ElementOf<typeof OmitProps>;
type PartialTransferListProps = Omit<TransferListProps, OmitProp>;
export interface TransferListBodyProps extends PartialTransferListProps {
filteredItems: TransferItem[];
filteredRenderItems: RenderedItem[];
selectedKeys: string[];
}
class ListBody extends React.Component<TransferListBodyProps> {
onItemSelect = (item: TransferItem) => {
const { onItemSelect, selectedKeys } = this.props;
const checked = selectedKeys.indexOf(item.key) >= 0;
onItemSelect(item.key, !checked);
};
render() {
const {
prefixCls,
onScroll,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
} = this.props;
return (
<ul className={`${prefixCls}-content`} onScroll={onScroll}>
{filteredRenderItems.map(({ renderedEl, renderedText, item }: RenderedItem) => {
const { disabled } = item;
const checked = selectedKeys.indexOf(item.key) >= 0;
return (
<ListItem
disabled={globalDisabled || disabled}
key={item.key}
item={item}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.onItemSelect}
/>
);
})}
</ul>
);
}
}
const ListBodyWrapper = (props: TransferListBodyProps) => <ListBody {...props} />;
export default ListBodyWrapper;

View File

@ -4,34 +4,11 @@
@input-prefix-cls: ~'@{ant-prefix}-input';
.@{transfer-prefix-cls}-customize-list {
display: flex;
.@{transfer-prefix-cls}-operation {
flex: none;
align-self: center;
}
.@{transfer-prefix-cls}-list {
flex: auto;
flex: 1 1 50%;
width: auto;
height: auto;
min-height: @transfer-list-height;
&-body {
&-with-search {
padding-top: 0;
}
// Search box in customize mode do not need fix top
&-search-wrapper {
position: relative;
padding-bottom: 0;
}
&-customize-wrapper {
padding: 12px;
}
}
}
// =================== Hook Components ===================

View File

@ -13,6 +13,8 @@
.reset-component;
position: relative;
display: flex;
align-items: stretch;
&-disabled {
.@{transfer-prefix-cls}-list {
@ -21,17 +23,16 @@
}
&-list {
position: relative;
display: inline-block;
display: flex;
flex-direction: column;
width: 180px;
height: @transfer-list-height;
padding-top: @transfer-header-height;
vertical-align: middle;
border: @border-width-base @border-style-base @border-color-base;
border-radius: @border-radius-base;
&-with-footer {
padding-bottom: 34px;
&-with-pagination {
width: 250px;
height: auto;
}
&-search {
@ -61,72 +62,120 @@
}
&-header {
position: absolute;
top: 0;
left: 0;
width: 100%;
display: flex;
flex: none;
align-items: center;
height: @transfer-header-height;
// border-top is on the transfer dom. We should minus 1px for this
padding: (@transfer-header-vertical-padding - 1px) @control-padding-horizontal
@transfer-header-vertical-padding;
overflow: hidden;
color: @text-color;
background: @component-background;
border-bottom: @border-width-base @border-style-base @border-color-split;
border-radius: @border-radius-base @border-radius-base 0 0;
&-title {
position: absolute;
right: 12px;
> *:not(:last-child) {
margin-right: 4px;
}
.@{ant-prefix}-checkbox-wrapper + span {
padding-left: 8px;
> * {
flex: none;
}
&-title {
flex: auto;
overflow: hidden;
white-space: nowrap;
text-align: right;
text-overflow: ellipsis;
}
&-dropdown {
transform: translateY(10%);
cursor: pointer;
.iconfont-size-under-12px(10px);
}
}
&-body {
position: relative;
height: 100%;
display: flex;
flex: auto;
flex-direction: column;
overflow: hidden;
font-size: @font-size-base;
&-search-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
position: relative;
flex: none;
padding: @padding-sm;
}
}
&-body-with-search {
padding-top: @input-height-base + @padding-sm * 2;
}
&-content {
height: 100%;
flex: auto;
margin: 0;
padding: 0;
overflow: auto;
list-style: none;
&-item {
display: flex;
align-items: center;
min-height: @transfer-item-height;
padding: @transfer-item-padding-vertical @control-padding-horizontal;
overflow: hidden;
line-height: @transfer-item-height - 2 * @transfer-item-padding-vertical;
white-space: nowrap;
text-overflow: ellipsis;
transition: all 0.3s;
> span {
padding-right: 0;
> *:not(:last-child) {
margin-right: 8px;
}
> * {
flex: none;
}
&-text {
padding-left: 8px;
flex: auto;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&-remove {
.operation-unit();
position: relative;
color: @border-color-base;
&::after {
position: absolute;
top: -@transfer-item-padding-vertical;
right: -50%;
bottom: -@transfer-item-padding-vertical;
left: -50%;
content: '';
}
&:hover {
color: @link-hover-color;
}
}
}
&-item:not(&-item-disabled):hover {
background-color: @transfer-item-hover-bg;
cursor: pointer;
&-item:not(&-item-disabled) {
&:hover {
background-color: @transfer-item-hover-bg;
cursor: pointer;
}
&.@{transfer-prefix-cls}-list-content-item-checked:hover {
background-color: darken(@item-active-bg, 2%);
}
}
// Do not change hover style when `oneWay` mode
&-show-remove &-item:not(&-item-disabled):hover {
background: transparent;
cursor: default;
}
&-item-checked {
@ -139,33 +188,30 @@
}
}
&-pagination {
flex: none;
align-self: flex-end;
padding: @padding-xs 0;
}
&-body-not-found {
position: absolute;
top: 50%;
flex: none;
width: 100%;
padding-top: 0;
margin: auto 0;
color: @disabled-color;
text-align: center;
transform: translateY(-50%);
// with filter should offset the search box height
.@{transfer-prefix-cls}-list-body-with-search & {
margin-top: @input-height-base / 2;
}
}
&-footer {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
border-top: @border-width-base @border-style-base @border-color-split;
border-radius: 0 0 @border-radius-base @border-radius-base;
}
}
&-operation {
display: inline-block;
display: flex;
flex: none;
flex-direction: column;
align-self: center;
margin: 0 8px;
overflow: hidden;
vertical-align: middle;

View File

@ -6,3 +6,6 @@ import '../../empty/style';
import '../../checkbox/style';
import '../../button/style';
import '../../input/style';
import '../../menu/style';
import '../../dropdown/style';
import '../../pagination/style';