Merge pull request #16457 from ant-design/resolve-feature-conflict

Resolve feature conflict
This commit is contained in:
偏右 2019-05-07 18:06:55 +08:00 committed by GitHub
commit 896011f586
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2379 additions and 202 deletions

View File

@ -564,7 +564,9 @@
// Transfer
// ---
@transfer-header-height: 40px;
@transfer-disabled-bg: @disabled-bg;
@transfer-list-height: 200px;
// Message
// ---

View File

@ -4,7 +4,7 @@ import PureRenderMixin from 'rc-util/lib/PureRenderMixin';
import Lazyload from 'react-lazy-load';
import Checkbox from '../checkbox';
export default class Item extends React.Component<any, any> {
export default class ListItem extends React.Component<any, any> {
shouldComponentUpdate(...args: any[]) {
return PureRenderMixin.shouldComponentUpdate.apply(this, args);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`List should render correctly 1`] = `
exports[`Transfer.List should render correctly 1`] = `
<div
class="ant-transfer-list"
>
@ -16,6 +16,7 @@ exports[`List should render correctly 1`] = `
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
@ -52,6 +53,7 @@ exports[`List should render correctly 1`] = `
checked=""
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
@ -72,6 +74,7 @@ exports[`List should render correctly 1`] = `
<input
class="ant-checkbox-input"
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"
@ -93,6 +96,7 @@ exports[`List should render correctly 1`] = `
class="ant-checkbox-input"
disabled=""
type="checkbox"
value=""
/>
<span
class="ant-checkbox-inner"

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Search should show cross icon when input value exists 1`] = `
exports[`Transfer.Search should show cross icon when input value exists 1`] = `
<Search
placeholder=""
value=""
@ -65,7 +65,7 @@ exports[`Search should show cross icon when input value exists 1`] = `
</Search>
`;
exports[`Search should show cross icon when input value exists 2`] = `
exports[`Transfer.Search should show cross icon when input value exists 2`] = `
<Search
placeholder=""
value="a"

View File

@ -0,0 +1,77 @@
import React from 'react';
import { mount } from 'enzyme';
import Transfer from '../index';
describe('Transfer.Customize', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
it('should warning use body', () => {
mount(<Transfer body={() => null} />);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Transfer] `body` is internal usage and will bre removed, please use `children` instead.',
);
});
describe('deprecated function', () => {
const dataSource = [];
for (let i = 0; i < 10; i += 1) {
dataSource.push({
key: i.toString(),
});
}
const commonProps = {
dataSource,
selectedKeys: ['1'],
targetKeys: ['2'],
};
it('should not exist in render props', () => {
mount(
<Transfer {...commonProps}>
{props => {
expect('handleFilter' in props).toBeFalsy();
expect('handleSelect' in props).toBeFalsy();
expect('handleSelectAll' in props).toBeFalsy();
expect('handleClear' in props).toBeFalsy();
expect('body' in props).toBeFalsy();
expect('checkedKeys' in props).toBeFalsy();
}}
</Transfer>,
);
});
it('should warn if called in body', () => {
let init = true;
mount(
<Transfer
{...commonProps}
body={({ handleSelect, handleSelectAll }) => {
if (init) {
handleSelect('', true);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Transfer] `handleSelect` will be removed, please use `onSelect` instead.',
);
errorSpy.mockReset();
handleSelectAll([], true);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Transfer] `handleSelectAll` will be removed, please use `onSelectAll` instead.',
);
}
init = false;
return null;
}}
/>,
);
});
});
});

View File

@ -5,7 +5,7 @@ import Transfer from '..';
import TransferList from '../list';
import TransferOperation from '../operation';
import TransferSearch from '../search';
import TransferItem from '../item';
import TransferItem from '../ListItem';
import Button from '../../button';
import Checkbox from '../../checkbox';

View File

@ -1,5 +1,5 @@
import React from 'react';
import { render, mount } from 'enzyme';
import { mount } from 'enzyme';
import List from '../list';
import Checkbox from '../../checkbox';
@ -25,10 +25,21 @@ const listCommonProps = {
lazy: false,
};
describe('List', () => {
describe('Transfer.List', () => {
beforeAll(() => {
jest.useFakeTimers();
});
afterAll(() => {
jest.useRealTimers();
});
it('should render correctly', () => {
const wrapper = render(<List {...listCommonProps} />);
expect(wrapper).toMatchSnapshot();
const wrapper = mount(<List {...listCommonProps} />);
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('ListBody').state().mounted).toBeTruthy();
expect(wrapper.render()).toMatchSnapshot();
});
it('should check top Checkbox while all available items are checked', () => {

View File

@ -3,7 +3,7 @@ import { mount } from 'enzyme';
import Search from '../search';
import Transfer from '../index';
describe('Search', () => {
describe('Transfer.Search', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {

View File

@ -0,0 +1,161 @@
---
order: 5
title:
zh-CN: 表格穿梭框
en-US: Table Transfer
---
## zh-CN
使用 Table 组件作为自定义渲染列表。
## en-US
Customize render list with Table component.
```jsx
import { Transfer, Switch, Table, Tag } from 'antd';
import difference from 'lodash/difference';
// Customize Table Transfer
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }) => (
<Transfer {...restProps} showSelectAll={false}>
{({
direction,
filteredItems,
onItemSelectAll,
onItemSelect,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === 'left' ? leftColumns : rightColumns;
const rowSelection = {
getCheckboxProps: item => ({ disabled: listDisabled || item.disabled }),
onSelectAll(selected, selectedRows) {
const treeSelectedKeys = selectedRows
.filter(item => !item.disabled)
.map(({ key }) => key);
const diffKeys = selected
? difference(treeSelectedKeys, listSelectedKeys)
: difference(listSelectedKeys, treeSelectedKeys);
onItemSelectAll(diffKeys, selected);
},
onSelect({ key }, selected) {
onItemSelect(key, selected);
},
selectedRowKeys: listSelectedKeys,
};
return (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredItems}
size="small"
style={{ pointerEvents: listDisabled ? 'none' : null }}
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) return;
onItemSelect(key, !listSelectedKeys.includes(key));
},
})}
/>
);
}}
</Transfer>
);
const mockTags = ['cat', 'dog', 'bird'];
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 % 4 === 0,
tag: mockTags[i % 3],
});
}
const originTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
const leftTableColumns = [
{
dataIndex: 'title',
title: 'Name',
},
{
dataIndex: 'tag',
title: 'Tag',
render: tag => <Tag>{tag}</Tag>,
},
{
dataIndex: 'description',
title: 'Description',
},
];
const rightTableColumns = [
{
dataIndex: 'title',
title: 'Name',
},
];
class App extends React.Component {
state = {
targetKeys: originTargetKeys,
disabled: false,
showSearch: false,
};
onChange = nextTargetKeys => {
this.setState({ targetKeys: nextTargetKeys });
};
triggerDisable = disabled => {
this.setState({ disabled });
};
triggerShowSearch = showSearch => {
this.setState({ showSearch });
};
render() {
const { targetKeys, disabled, showSearch } = this.state;
return (
<div>
<TableTransfer
dataSource={mockData}
targetKeys={targetKeys}
disabled={disabled}
showSearch={showSearch}
onChange={this.onChange}
filterOption={(inputValue, item) =>
item.title.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
}
leftColumns={leftTableColumns}
rightColumns={rightTableColumns}
/>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={this.triggerDisable}
style={{ marginTop: 16 }}
/>
<Switch
unCheckedChildren="showSearch"
checkedChildren="showSearch"
checked={showSearch}
onChange={this.triggerShowSearch}
style={{ marginTop: 16 }}
/>
</div>
);
}
}
ReactDOM.render(<App />, mountNode);
```

View File

@ -0,0 +1,131 @@
---
order: 6
title:
zh-CN: 树穿梭框
en-US: Tree Transfer
---
## zh-CN
使用 Tree 组件作为自定义渲染列表。
## en-US
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)}>
{generateTree(children, checkedKeys)}
</TreeNode>
));
};
const TreeTransfer = ({ dataSource, targetKeys, ...restProps }) => {
const transferDataSource = [];
function flatten(list = []) {
list.forEach(item => {
transferDataSource.push(item);
flatten(item.children);
});
}
flatten(dataSource);
return (
<Transfer
{...restProps}
targetKeys={targetKeys}
dataSource={transferDataSource}
className="tree-transfer"
render={item => item.title}
showSelectAll={false}
>
{({ direction, onItemSelect, selectedKeys }) => {
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...targetKeys];
return (
<Tree
blockNode
checkable
checkStrictly
defaultExpandAll
checkedKeys={checkedKeys}
onCheck={(
_,
{
node: {
props: { eventKey },
},
},
) => {
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
}}
onSelect={(
_,
{
node: {
props: { eventKey },
},
},
) => {
onItemSelect(eventKey, !isChecked(checkedKeys, eventKey));
}}
>
{generateTree(dataSource, targetKeys)}
</Tree>
);
}
}}
</Transfer>
);
};
const treeData = [
{ key: '0-0', title: '0-0' },
{
key: '0-1',
title: '0-1',
children: [{ key: '0-1-0', title: '0-1-0' }, { key: '0-1-1', title: '0-1-1' }],
},
{ key: '0-2', title: '0-3' },
];
class App extends React.Component {
state = {
targetKeys: [],
};
onChange = targetKeys => {
console.log('Target Keys:', targetKeys);
this.setState({ targetKeys });
};
render() {
const { targetKeys } = this.state;
return (
<div>
<TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={this.onChange} />
</div>
);
}
}
ReactDOM.render(<App />, mountNode);
```
<style>
.tree-transfer .ant-transfer-list:first-child {
width: 50%;
flex: none;
}
</style>

View File

@ -18,28 +18,48 @@ One or more elements can be selected from either column, one click on the proper
## API
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| 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 | |
| footer | A function used for rendering the footer. | (props): ReactNode | |
| lazy | property of [react-lazy-load](https://github.com/loktar00/react-lazy-load) for lazy rendering items. Turn off it by set to `false`. | object\|boolean | `{ height: 32, offset: 32 }` |
| listStyle | A custom CSS style used for rendering the transfer columns. | object | |
| locale | i18n text including filter, empty text, item unit, etc | object | `{ 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\[] | \['>', '<'] |
| operationStyle | A custom CSS style used for rendering the operations column. | object | |
| 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 | Function(record) | |
| selectedKeys | A set of keys of selected items. | string\[] | \[] |
| showSearch | If included, a search box is shown on each column. | boolean | false |
| style | A custom CSS style used for rendering wrapper element. | object | |
| 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. | string\[] | - |
| onChange | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | |
| onScroll | A callback function which is executed when scroll options list | (direction, event): void | |
| onSearch | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - |
| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | |
| 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 | | |
| footer | A function used for rendering the footer. | (props): ReactNode | | |
| lazy | property of [react-lazy-load](https://github.com/loktar00/react-lazy-load) for lazy rendering items. Turn off it by set to `false`. | object\|boolean | `{ height: 32, offset: 32 }` | |
| listStyle | A custom CSS style used for rendering the transfer columns. | object | | |
| locale | i18n text including filter, empty text, item unit, etc | object | `{ 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\[] | \['>', '<'] | |
| operationStyle | A custom CSS style used for rendering the operations column. | object | | |
| 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 | Function(record) | | |
| 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 | 3.17.0 |
| style | A custom CSS style used for rendering wrapper element. | object | | |
| 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. | string\[] | - | |
| onChange | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | | |
| onScroll | A callback function which is executed when scroll options list | (direction, event): void | | |
| onSearch | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - | |
| onSelectChange | A callback function which is executed when selected items are changed. | (sourceSelectedKeys, targetSelectedKeys): void | | |
### Render Props
New in 3.17.0. Transfer accept `children` to customize render list, using follow props:
| Property | Description | Type | Version |
| --------------- | ----------------------- | ----------------------------------- | ------- |
| direction | List render direction | 'left' \| 'right' | 3.17.0 |
| disabled | Disable list or not | boolean | 3.17.0 |
| filteredItems | Filtered items | TransferItem[] | 3.17.0 |
| onItemSelect | Select item | (key: string, selected: boolean) | 3.17.0 |
| onItemSelectAll | Select a group of items | (keys: string[], selected: boolean) | 3.17.0 |
| selectedKeys | Selected items | string[] | 3.17.0 |
#### example
```jsx
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
```
## Warning

View File

@ -9,21 +9,29 @@ import LocaleReceiver from '../locale-provider/LocaleReceiver';
import defaultLocale from '../locale-provider/default';
import { ConfigConsumer, ConfigConsumerProps, RenderEmptyHandler } from '../config-provider';
import { polyfill } from 'react-lifecycles-compat';
import { TransferListBodyProps } from './renderListBody';
export { TransferListProps } from './list';
export { TransferOperationProps } from './operation';
export { TransferSearchProps } from './search';
function noop() {}
export type TransferDirection = 'left' | 'right';
type TransferRender = (record: TransferItem) => React.ReactNode;
export interface RenderResultObject {
label: React.ReactElement;
value: string;
}
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
type TransferRender = (item: TransferItem) => RenderResult;
export interface TransferItem {
key: string;
title: string;
description?: string;
disabled?: boolean;
[name: string]: any;
}
export interface TransferProps {
@ -53,6 +61,8 @@ export interface TransferProps {
onSearch?: (direction: TransferDirection, value: string) => void;
lazy?: {} | boolean;
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLDivElement>) => void;
children?: (props: TransferListBodyProps) => React.ReactNode;
showSelectAll?: boolean;
}
export interface TransferLocale {
@ -71,7 +81,6 @@ class Transfer extends React.Component<TransferProps, any> {
static defaultProps = {
dataSource: [],
render: noop as TransferRender,
locale: {},
showSearch: false,
};
@ -127,10 +136,14 @@ class Transfer extends React.Component<TransferProps, any> {
'please use `locale` instead.',
);
warning(
!('body' in props),
'Transfer',
'`body` is internal usage and will bre removed, please use `children` instead.',
);
const { selectedKeys = [], targetKeys = [] } = props;
this.state = {
leftFilter: '',
rightFilter: '',
sourceSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) === -1),
targetSelectedKeys: selectedKeys.filter(key => targetKeys.indexOf(key) > -1),
};
@ -205,43 +218,57 @@ class Transfer extends React.Component<TransferProps, any> {
}
}
onItemSelectAll = (direction: TransferDirection, selectedKeys: string[], checkAll: boolean) => {
const originalSelectedKeys = this.state[this.getSelectedKeysName(direction)] || [];
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);
if (!this.props.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: mergedCheckedKeys,
});
}
};
handleSelectAll = (
direction: TransferDirection,
filteredDataSource: TransferItem[],
checkAll: boolean,
) => {
const originalSelectedKeys = this.state[this.getSelectedKeysName(direction)] || [];
const currentKeys = filteredDataSource.map(item => item.key);
// Only operate current keys from original selected keys
const newKeys1 = originalSelectedKeys.filter((key: string) => currentKeys.indexOf(key) === -1);
const newKeys2 = [...originalSelectedKeys];
currentKeys.forEach(key => {
if (newKeys2.indexOf(key) === -1) {
newKeys2.push(key);
}
});
const holder = checkAll ? newKeys1 : newKeys2;
this.handleSelectChange(direction, holder);
if (!this.props.selectedKeys) {
this.setState({
[this.getSelectedKeysName(direction)]: holder,
});
}
warning(
false,
'Transfer',
'`handleSelectAll` will be removed, please use `onSelectAll` instead.',
);
this.onItemSelectAll(direction, filteredDataSource.map(({ key }) => key), !checkAll);
};
// [Legacy] Old prop `body` pass origin check as arg. It's confusing.
// TODO: Remove this in next version.
handleLeftSelectAll = (filteredDataSource: TransferItem[], checkAll: boolean) =>
this.handleSelectAll('left', filteredDataSource, checkAll);
this.handleSelectAll('left', filteredDataSource, !checkAll);
handleRightSelectAll = (filteredDataSource: TransferItem[], checkAll: boolean) =>
this.handleSelectAll('right', filteredDataSource, checkAll);
this.handleSelectAll('right', filteredDataSource, !checkAll);
onLeftItemSelectAll = (selectedKeys: string[], checkAll: boolean) =>
this.onItemSelectAll('left', selectedKeys, checkAll);
onRightItemSelectAll = (selectedKeys: string[], checkAll: boolean) =>
this.onItemSelectAll('right', selectedKeys, checkAll);
handleFilter = (direction: TransferDirection, e: React.ChangeEvent<HTMLInputElement>) => {
const { onSearchChange, onSearch } = this.props;
const value = e.target.value;
this.setState({
// add filter
[`${direction}Filter`]: value,
});
if (onSearchChange) {
warning(false, 'Transfer', '`onSearchChange` is deprecated. Please use `onSearch` instead.');
onSearchChange(direction, e);
@ -256,9 +283,6 @@ class Transfer extends React.Component<TransferProps, any> {
handleClear = (direction: TransferDirection) => {
const { onSearch } = this.props;
this.setState({
[`${direction}Filter`]: '',
});
if (onSearch) {
onSearch(direction, '');
}
@ -267,15 +291,15 @@ class Transfer extends React.Component<TransferProps, any> {
handleLeftClear = () => this.handleClear('left');
handleRightClear = () => this.handleClear('right');
handleSelect = (direction: TransferDirection, selectedItem: TransferItem, checked: boolean) => {
onItemSelect = (direction: TransferDirection, selectedKey: string, checked: boolean) => {
const { sourceSelectedKeys, targetSelectedKeys } = this.state;
const holder = direction === 'left' ? [...sourceSelectedKeys] : [...targetSelectedKeys];
const index = holder.indexOf(selectedItem.key);
const index = holder.indexOf(selectedKey);
if (index > -1) {
holder.splice(index, 1);
}
if (checked) {
holder.push(selectedItem.key);
holder.push(selectedKey);
}
this.handleSelectChange(direction, holder);
@ -286,13 +310,19 @@ class Transfer extends React.Component<TransferProps, any> {
}
};
handleLeftSelect = (selectedItem: TransferItem, checked: boolean) => {
return this.handleSelect('left', selectedItem, checked);
handleSelect = (direction: TransferDirection, selectedItem: TransferItem, checked: boolean) => {
warning(false, 'Transfer', '`handleSelect` will be removed, please use `onSelect` instead.');
this.onItemSelect(direction, selectedItem.key, checked);
};
handleLeftSelect = (selectedItem: TransferItem, checked: boolean) =>
this.handleSelect('left', selectedItem, checked);
handleRightSelect = (selectedItem: TransferItem, checked: boolean) =>
this.handleSelect('right', selectedItem, checked);
handleRightSelect = (selectedItem: TransferItem, checked: boolean) => {
return this.handleSelect('right', selectedItem, checked);
};
onLeftItemSelect = (selectedKey: string, checked: boolean) =>
this.onItemSelect('left', selectedKey, checked);
onRightItemSelect = (selectedKey: string, checked: boolean) =>
this.onItemSelect('right', selectedKey, checked);
handleScroll = (direction: TransferDirection, e: React.SyntheticEvent<HTMLDivElement>) => {
const { onScroll } = this.props;
@ -348,16 +378,21 @@ class Transfer extends React.Component<TransferProps, any> {
filterOption,
render,
lazy,
children,
showSelectAll,
} = this.props;
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
const locale = this.getLocale(transferLocale, renderEmpty);
const { leftFilter, rightFilter, sourceSelectedKeys, targetSelectedKeys } = this.state;
const { sourceSelectedKeys, targetSelectedKeys } = this.state;
const { leftDataSource, rightDataSource } = this.separateDataSource(this.props);
const leftActive = targetSelectedKeys.length > 0;
const rightActive = sourceSelectedKeys.length > 0;
const cls = classNames(className, prefixCls, disabled && `${prefixCls}-disabled`);
const cls = classNames(className, prefixCls, {
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-customize-list`]: !!children,
});
const titles = this.getTitles(locale);
return (
@ -366,7 +401,6 @@ class Transfer extends React.Component<TransferProps, any> {
prefixCls={`${prefixCls}-list`}
titleText={titles[0]}
dataSource={leftDataSource}
filter={leftFilter}
filterOption={filterOption}
style={listStyle}
checkedKeys={sourceSelectedKeys}
@ -374,13 +408,18 @@ class Transfer extends React.Component<TransferProps, any> {
handleClear={this.handleLeftClear}
handleSelect={this.handleLeftSelect}
handleSelectAll={this.handleLeftSelectAll}
onItemSelect={this.onLeftItemSelect}
onItemSelectAll={this.onLeftItemSelectAll}
render={render}
showSearch={showSearch}
body={body}
renderList={children}
footer={footer}
lazy={lazy}
onScroll={this.handleLeftScroll}
disabled={disabled}
direction="left"
showSelectAll={showSelectAll}
{...locale}
/>
<Operation
@ -398,7 +437,6 @@ class Transfer extends React.Component<TransferProps, any> {
prefixCls={`${prefixCls}-list`}
titleText={titles[1]}
dataSource={rightDataSource}
filter={rightFilter}
filterOption={filterOption}
style={listStyle}
checkedKeys={targetSelectedKeys}
@ -406,13 +444,18 @@ class Transfer extends React.Component<TransferProps, any> {
handleClear={this.handleRightClear}
handleSelect={this.handleRightSelect}
handleSelectAll={this.handleRightSelectAll}
onItemSelect={this.onRightItemSelect}
onItemSelectAll={this.onRightItemSelectAll}
render={render}
showSearch={showSearch}
body={body}
renderList={children}
footer={footer}
lazy={lazy}
onScroll={this.handleRightScroll}
disabled={disabled}
direction="right"
showSelectAll={showSelectAll}
{...locale}
/>
</div>

View File

@ -19,26 +19,48 @@ title: Transfer
## API
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| className | 自定义类 | string | |
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外。 | [TransferItem](https://git.io/vMM64)\[] | \[] |
| disabled | 是否禁用 | boolean | false |
| filterOption | 接收 `inputValue` `option` 两个参数,当 `option` 符合筛选条件时,应返回 `true`,反之则返回 `false`。 | (inputValue, option): boolean | |
| footer | 底部渲染函数 | (props): ReactNode | |
| lazy | Transfer 使用了 [react-lazy-load](https://github.com/loktar00/react-lazy-load) 优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object\|boolean | `{ height: 32, offset: 32 }` |
| listStyle | 两个穿梭框的自定义样式 | object | |
| locale | 各种语言 | object | `{ itemUnit: '项', itemsUnit: '项', notFoundContent: '列表为空', searchPlaceholder: '请输入搜索内容' }` |
| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] |
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象其中 `label` 字段为 ReactElement`value` 字段为 title | Function(record) | |
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] |
| showSearch | 是否显示搜索框 | boolean | false |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] |
| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] |
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | |
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | |
| onSearch | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - |
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | |
### 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 | | |
| lazy | Transfer 使用了 [react-lazy-load](https://github.com/loktar00/react-lazy-load) 优化性能,这里可以设置相关参数。设为 `false` 可以关闭懒加载。 | object\|boolean | `{ height: 32, offset: 32 }` | |
| listStyle | 两个穿梭框的自定义样式 | object | | |
| locale | 各种语言 | object | `{ itemUnit: '项', itemsUnit: '项', notFoundContent: '列表为空', searchPlaceholder: '请输入搜索内容' }` | |
| operations | 操作文案集合,顺序从上至下 | string\[] | \['>', '<'] | |
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象其中 `label` 字段为 ReactElement`value` 字段为 title | Function(record) | | |
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
| showSearch | 是否显示搜索框 | boolean | false | |
| showSelectAll | 是否展示全选勾选框 | boolean | true | 3.17.0 |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
| titles | 标题集合,顺序从左至右 | string\[] | \['', ''] | |
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | | |
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | | |
| onSearch | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - | |
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | | |
### Render Props
3.17.0 新增。Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
| 参数 | 说明 | 类型 | 版本 |
| --------------- | -------------- | ----------------------------------- | ------ |
| direction | 渲染列表的方向 | 'left' \| 'right' | 3.17.0 |
| disabled | 是否禁用列表 | boolean | 3.17.0 |
| filteredItems | 过滤后的数据 | TransferItem[] | 3.17.0 |
| onItemSelect | 勾选条目 | (key: string, selected: boolean) | 3.17.0 |
| onItemSelectAll | 勾选一组条目 | (keys: string[], selected: boolean) | 3.17.0 |
| selectedKeys | 选中的条目 | string[] | 3.17.0 |
#### 参考示例
```jsx
<Transfer {...props}>{listProps => <YourComponent {...listProps} />}</Transfer>
```
## 注意

View File

@ -1,17 +1,17 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import omit from 'omit.js';
import classNames from 'classnames';
import Animate from 'rc-animate';
import PureRenderMixin from 'rc-util/lib/PureRenderMixin';
import Checkbox from '../checkbox';
import { TransferItem } from './index';
import { TransferItem, TransferDirection, RenderResult, RenderResultObject } from './index';
import Search from './search';
import Item from './item';
import defaultRenderList, { TransferListBodyProps, OmitProps } from './renderListBody';
import triggerEvent from '../_util/triggerEvent';
function noop() {}
const defaultRender = () => null;
function isRenderResultPlainObject(result: any) {
function isRenderResultPlainObject(result: RenderResult) {
return (
result &&
!React.isValidElement(result) &&
@ -19,61 +19,80 @@ function isRenderResultPlainObject(result: any) {
);
}
export interface RenderedItem {
renderedText: string;
renderedEl: React.ReactNode;
item: TransferItem;
}
type RenderListFunction = (props: TransferListBodyProps) => React.ReactNode;
export interface TransferListProps {
prefixCls: string;
titleText: string;
dataSource: TransferItem[];
filter: string;
filterOption?: (filterText: any, item: any) => boolean;
filterOption?: (filterText: string, item: TransferItem) => boolean;
style?: React.CSSProperties;
checkedKeys: string[];
handleFilter: (e: any) => void;
handleSelect: (selectedItem: any, checked: boolean) => void;
handleSelectAll: (dataSource: any[], checkAll: boolean) => void;
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
handleSelect: (selectedItem: TransferItem, checked: boolean) => void;
/** [Legacy] Only used when `body` prop used. */
handleSelectAll: (dataSource: TransferItem[], checkAll: boolean) => void;
onItemSelect: (key: string, check: boolean) => void;
onItemSelectAll: (dataSource: string[], checkAll: boolean) => void;
handleClear: () => void;
render?: (item: any) => any;
render?: (item: TransferItem) => RenderResult;
showSearch?: boolean;
searchPlaceholder: string;
notFoundContent: React.ReactNode;
itemUnit: string;
itemsUnit: string;
body?: (props: TransferListProps) => React.ReactNode;
renderList?: RenderListFunction;
footer?: (props: TransferListProps) => React.ReactNode;
lazy?: boolean | {};
onScroll: Function;
disabled?: boolean;
direction: TransferDirection;
showSelectAll?: boolean;
}
export default class TransferList extends React.Component<TransferListProps, any> {
interface TransferListState {
/** Filter input value */
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.Component<TransferListProps, TransferListState> {
static defaultProps = {
dataSource: [],
titleText: '',
showSearch: false,
render: noop,
lazy: {},
};
timer: number;
triggerScrollTimer: number;
notFoundNode: HTMLDivElement;
constructor(props: TransferListProps) {
super(props);
this.state = {
mounted: false,
filterValue: '',
};
}
componentDidMount() {
this.timer = window.setTimeout(() => {
this.setState({
mounted: true,
});
}, 0);
}
componentWillUnmount() {
clearTimeout(this.timer);
clearTimeout(this.triggerScrollTimer);
}
@ -81,25 +100,23 @@ export default class TransferList extends React.Component<TransferListProps, any
return PureRenderMixin.shouldComponentUpdate.apply(this, args);
}
getCheckStatus(filteredDataSource: TransferItem[]) {
getCheckStatus(filteredItems: TransferItem[]) {
const { checkedKeys } = this.props;
if (checkedKeys.length === 0) {
return 'none';
} else if (filteredDataSource.every(item => checkedKeys.indexOf(item.key) >= 0)) {
} else if (filteredItems.every(item => checkedKeys.indexOf(item.key) >= 0 || !!item.disabled)) {
return 'all';
}
return 'part';
}
handleSelect = (selectedItem: TransferItem) => {
const { checkedKeys } = this.props;
const result = checkedKeys.some(key => key === selectedItem.key);
this.props.handleSelect(selectedItem, !result);
};
handleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
const {
target: { value: filterValue },
} = e;
this.setState({ filterValue });
this.props.handleFilter(e);
if (!e.target.value) {
if (!filterValue) {
return;
}
// Manually trigger scroll event for lazy search bug
@ -114,45 +131,51 @@ export default class TransferList extends React.Component<TransferListProps, any
};
handleClear = () => {
this.setState({ filterValue: '' });
this.props.handleClear();
};
matchFilter = (text: string, item: TransferItem) => {
const { filter, filterOption } = this.props;
const { filterValue } = this.state;
const { filterOption } = this.props;
if (filterOption) {
return filterOption(filter, item);
return filterOption(filterValue, item);
}
return text.indexOf(filter) >= 0;
return text.indexOf(filterValue) >= 0;
};
renderItem = (item: TransferItem) => {
const { render = noop } = this.props;
const renderResult = render(item);
renderItem = (item: TransferItem): RenderedItem => {
const { render = defaultRender } = this.props;
const renderResult: RenderResult = render(item);
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
return {
renderedText: isRenderResultPlain ? renderResult.value : renderResult,
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
renderedText: isRenderResultPlain
? (renderResult as RenderResultObject).value
: (renderResult as string),
renderedEl: isRenderResultPlain ? (renderResult as RenderResultObject).label : renderResult,
item,
};
};
render() {
const { filterValue } = this.state;
const {
prefixCls,
dataSource,
titleText,
checkedKeys,
lazy,
disabled,
body,
footer,
showSearch,
style,
filter,
searchPlaceholder,
notFoundContent,
itemUnit,
itemsUnit,
onScroll,
renderList,
onItemSelectAll,
showSelectAll,
} = this.props;
// Custom Layout
@ -163,38 +186,24 @@ export default class TransferList extends React.Component<TransferListProps, any
[`${prefixCls}-with-footer`]: !!footerDom,
});
const filteredDataSource: TransferItem[] = [];
const totalDataSource: TransferItem[] = [];
// ====================== Get filtered, checked item list ======================
const filteredItems: TransferItem[] = [];
const filteredRenderItems: RenderedItem[] = [];
const showItems = dataSource.map(item => {
const { renderedText, renderedEl } = this.renderItem(item);
if (filter && filter.trim() && !this.matchFilter(renderedText, item)) {
dataSource.forEach(item => {
const renderedItem = this.renderItem(item);
const { renderedText } = renderedItem;
// Filter skip
if (filterValue && filterValue.trim() && !this.matchFilter(renderedText, item)) {
return null;
}
// all show items
totalDataSource.push(item);
if (!item.disabled) {
// response to checkAll items
filteredDataSource.push(item);
}
const checked = checkedKeys.indexOf(item.key) >= 0;
return (
<Item
disabled={disabled}
key={item.key}
item={item}
lazy={lazy}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.handleSelect}
/>
);
filteredItems.push(item);
filteredRenderItems.push(renderedItem);
});
// ================================= List Body =================================
const unit = dataSource.length > 1 ? itemsUnit : itemUnit;
const search = showSearch ? (
@ -204,64 +213,86 @@ export default class TransferList extends React.Component<TransferListProps, any
onChange={this.handleFilter}
handleClear={this.handleClear}
placeholder={searchPlaceholder}
value={filter}
value={filterValue}
disabled={disabled}
/>
</div>
) : null;
const searchNotFound = showItems.every(item => item === null) && (
const searchNotFound = !filteredItems.length && (
<div className={`${prefixCls}-body-not-found`}>{notFoundContent}</div>
);
const listBody = bodyDom || (
<div
className={classNames(
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
)}
>
{search}
{!searchNotFound && (
<Animate
component="ul"
componentProps={{ onScroll }}
className={`${prefixCls}-content`}
transitionName={this.state.mounted ? `${prefixCls}-content-item-highlight` : ''}
transitionLeave={false}
>
{showItems}
</Animate>
)}
{searchNotFound}
</div>
);
let listBody: React.ReactNode = bodyDom;
if (!listBody) {
let bodyNode: React.ReactNode = searchNotFound;
if (!bodyNode) {
const { bodyContent, customize } = renderListNode(renderList, {
...omit(this.props, OmitProps),
filteredItems,
filteredRenderItems,
selectedKeys: checkedKeys,
});
// We should wrap customize list body in a classNamed div to use flex layout.
bodyNode = customize ? (
<div className={`${prefixCls}-body-customize-wrapper`}>{bodyContent}</div>
) : (
bodyContent
);
}
listBody = (
<div
className={classNames(
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
)}
>
{search}
{bodyNode}
</div>
);
}
// ================================ List Footer ================================
const listFooter = footerDom ? <div className={`${prefixCls}-footer`}>{footerDom}</div> : null;
const checkStatus = this.getCheckStatus(filteredDataSource);
const checkStatus = this.getCheckStatus(filteredItems);
const checkedAll = checkStatus === 'all';
const checkAllCheckbox = (
const checkAllCheckbox = showSelectAll !== false && (
<Checkbox
disabled={disabled}
checked={checkedAll}
indeterminate={checkStatus === 'part'}
onChange={() => this.props.handleSelectAll(filteredDataSource, checkedAll)}
onChange={() => {
// Only select enabled items
onItemSelectAll(
filteredItems.filter(item => !item.disabled).map(({ key }) => key),
!checkedAll,
);
}}
/>
);
// ================================== Render ===================================
return (
<div className={listCls} style={style}>
{/* Header */}
<div className={`${prefixCls}-header`}>
{checkAllCheckbox}
<span className={`${prefixCls}-header-selected`}>
<span>
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + totalDataSource.length}{' '}
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '}
{unit}
</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span>
</span>
</div>
{/* Body */}
{listBody}
{/* Footer */}
{listFooter}
</div>
);

View File

@ -0,0 +1,84 @@
import * as React from 'react';
import Animate from 'rc-animate';
import raf from '../_util/raf';
import { Omit, tuple } from '../_util/type';
import { TransferItem } from '.';
import { TransferListProps, RenderedItem } from './list';
import ListItem from './ListItem';
export const OmitProps = tuple(
'handleFilter',
'handleSelect',
'handleSelectAll',
'handleClear',
'body',
'checkedKeys',
);
export type OmitProp = (typeof OmitProps)[number];
type PartialTransferListProps = Omit<TransferListProps, OmitProp>;
export interface TransferListBodyProps extends PartialTransferListProps {
filteredItems: TransferItem[];
filteredRenderItems: RenderedItem[];
selectedKeys: string[];
}
class ListBody extends React.Component<TransferListBodyProps> {
state = {
mounted: false,
};
private mountId: number;
componentDidMount() {
this.mountId = raf(() => {
this.setState({ mounted: true });
});
}
componentWillMount() {
raf.cancel(this.mountId);
}
onItemSelect = (item: TransferItem) => {
const { onItemSelect, selectedKeys } = this.props;
const checked = selectedKeys.indexOf(item.key) >= 0;
onItemSelect(item.key, !checked);
};
render() {
const { mounted } = this.state;
const { prefixCls, onScroll, filteredRenderItems, lazy, selectedKeys } = this.props;
return (
<Animate
component="ul"
componentProps={{ onScroll }}
className={`${prefixCls}-content`}
transitionName={mounted ? `${prefixCls}-content-item-highlight` : ''}
transitionLeave={false}
>
{filteredRenderItems.map(({ renderedEl, renderedText, item }: RenderedItem) => {
const { disabled } = item;
const checked = selectedKeys.indexOf(item.key) >= 0;
return (
<ListItem
disabled={disabled}
key={item.key}
item={item}
lazy={lazy}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.onItemSelect}
/>
);
})}
</Animate>
);
}
}
export default (props: TransferListBodyProps) => <ListBody {...props} />;

View File

@ -0,0 +1,62 @@
@import './index.less';
@table-prefix-cls: ~'@{ant-prefix}-table';
.@{transfer-prefix-cls}-customize-list {
display: flex;
.@{transfer-prefix-cls}-operation {
flex: none;
align-self: center;
}
.@{transfer-prefix-cls}-list {
flex: auto;
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 ===================
.@{table-prefix-cls}-wrapper {
.@{table-prefix-cls}-small {
border: 0;
border-radius: 0;
> .@{table-prefix-cls}-content {
// Header background color
> .@{table-prefix-cls}-body > table > .@{table-prefix-cls}-thead > tr > th {
background: @table-header-bg;
}
.@{table-prefix-cls}-row:last-child td {
border-bottom: @border-width-base @border-style-base @border-color-split;
}
}
.@{table-prefix-cls}-body {
margin: 0;
}
}
.@{table-prefix-cls}-pagination.@{ant-prefix}-pagination {
margin: 16px 0 4px;
}
}
}

View File

@ -1,9 +1,14 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';
@import '../../checkbox/style/mixin';
@import './customize.less';
@transfer-prefix-cls: ~'@{ant-prefix}-transfer';
@transfer-header-vertical-padding: (
@transfer-header-height - 1px - @font-size-base * @line-height-base
) / 2;
.@{transfer-prefix-cls} {
.reset-component;
@ -19,8 +24,8 @@
position: relative;
display: inline-block;
width: 180px;
height: 200px;
padding-top: 34px;
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;
@ -33,9 +38,9 @@
padding: 0 @control-padding-horizontal-sm;
&-action {
position: absolute;
top: 4px;
right: 4px;
bottom: 4px;
top: 12px;
right: 12px;
bottom: 12px;
width: 28px;
color: @disabled-color;
line-height: @input-height-base;
@ -58,7 +63,9 @@
top: 0;
left: 0;
width: 100%;
padding: 6px @control-padding-horizontal;
// 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;
@ -81,12 +88,12 @@
top: 0;
left: 0;
width: 100%;
padding: 4px;
padding: 12px;
}
}
&-body-with-search {
padding-top: @input-height-base + 8px;
padding-top: @input-height-base + 24px;
}
&-content {