diff --git a/components/_util/transButton.tsx b/components/_util/transButton.tsx index 11d153b4ad..e321398592 100644 --- a/components/_util/transButton.tsx +++ b/components/_util/transButton.tsx @@ -9,6 +9,7 @@ interface TransButtonProps extends React.HTMLAttributes { onClick?: (e?: React.MouseEvent) => void; noStyle?: boolean; autoFocus?: boolean; + disabled?: boolean; } const inlineStyle: React.CSSProperties = { @@ -63,7 +64,24 @@ class TransButton extends React.Component { } 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 (
{ {...restProps} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} - style={{ ...(!noStyle ? inlineStyle : null), ...style }} + style={mergedStyle} /> ); } diff --git a/components/config-provider/__tests__/__snapshots__/components.test.js.snap b/components/config-provider/__tests__/__snapshots__/components.test.js.snap index 0eb19a54f3..79e0f3e3e1 100644 --- a/components/config-provider/__tests__/__snapshots__/components.test.js.snap +++ b/components/config-provider/__tests__/__snapshots__/components.test.js.snap @@ -34579,16 +34579,34 @@ exports[`ConfigProvider components Transfer configProvider 1`] = ` /> + + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 عنصر - - + 0 عنصر +
+ + + - - 6 items - - - Target - + 6 items + + + Target
- + + + `; @@ -1040,16 +1120,34 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = ` /> + + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - Select All - - + Select All +
+ + + - - 0/3 - - + 0/3 +
+Array [
- - - + 0 item - -
-
+
- - - - - + + + + - - + +
+

+ No Data +

-

- No Data -

- -
- - -
-
- - + +
+
+
+ + + + 0 item - -
-
+
- - - - - + + + + - - + +
+

+ No Data +

-

- No Data -

+ , +
, + , +] +`; + +exports[`renders ./components/transfer/demo/oneWay.md correctly 1`] = ` +
+
+
+
+ + + + + + 14 items + + + Source + +
+
+
    +
  • + + + content1 + +
  • +
  • + + + content2 + +
  • +
  • + + + content4 + +
  • +
  • + + + content5 + +
  • +
  • + + + content7 + +
  • +
  • + + + content8 + +
  • +
  • + + + content10 + +
  • +
  • + + + content11 + +
  • +
  • + + + content13 + +
  • +
  • + + + content14 + +
  • +
  • + + + content16 + +
  • +
  • + + + content17 + +
  • +
  • + + + content19 + +
  • +
  • + + + content20 + +
  • +
+
+
+
+ +
+
+
+ + + + + 6 items + + + Target + +
+
+
    +
  • + + content3 + +
    + + + +
    +
  • +
  • + + content6 + +
    + + + +
    +
  • +
  • + + content9 + +
    + + + +
    +
  • +
  • + + content12 + +
    + + + +
    +
  • +
  • + + content15 + +
    + + + +
    +
  • +
  • + + content18 + +
    + + + +
    +
  • +
+
+
+
+
+
+ +
`; @@ -1910,16 +2835,34 @@ exports[`renders ./components/transfer/demo/search.md correctly 1`] = ` />
+ + + - - 0 item - - + 0 item +
+ + + - - 6 items - - + 6 items +
+ + + - - 5 items - - + 5 items +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 0 item - - + 0 item +
+ + + - - 1/2 items - - + 1/2 items +
+ + + - - 1 item - - + 1 item +
`; -exports[`Transfer should show sorted targetkey 1`] = ` +exports[`Transfer should show sorted targetKey 1`] = `
@@ -475,16 +547,34 @@ exports[`Transfer should show sorted targetkey 1`] = ` /> + + + - - 1 item - - + 1 item +
+ + + - - 2 items - - + 2 items +
+ + + Select all data + + + Invert current page + + + } + > + + + + + + + + + + + + + + - - 1 - - item - - + 1 + + item +
-
  • - - - - - label - -
  • + className="ant-checkbox" + style={Object {}} + > + + + + + + + + label + + + @@ -1135,12 +1540,19 @@ exports[`Transfer should support render value and label in item 1`] = ` [Function] } + 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`] = ` + + + Select all data + + + Invert current page + + + } + > + + + + + + + + + + + + + + - - 0 - - item - - + 0 + + item +
    + + + - - 1/3 - - + 1/3 +
    { errorSpy.mockRestore(); }); - it('props#body doesnot work anymore', () => { + it('props#body does not work anymore', () => { const body = jest.fn(); mount(); @@ -49,4 +49,16 @@ describe('Transfer.Customize', () => { ); }); }); + + it('warning if use `pagination`', () => { + mount( + + {() => null} + , + ); + + expect(errorSpy).toHaveBeenCalledWith( + 'Warning: [antd: Transfer] `pagination` not support customize render list.', + ); + }); }); diff --git a/components/transfer/__tests__/dropdown.test.js b/components/transfer/__tests__/dropdown.test.js new file mode 100644 index 0000000000..2acd9f0e2d --- /dev/null +++ b/components/transfer/__tests__/dropdown.test.js @@ -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(); + 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(); + 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(); + 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( + , + ); + 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(); + }); +}); diff --git a/components/transfer/__tests__/index.test.js b/components/transfer/__tests__/index.test.js index 2ee2df3f68..5a4ff54e8e 100644 --- a/components/transfer/__tests__/index.test.js +++ b/components/transfer/__tests__/index.test.js @@ -104,11 +104,7 @@ describe('Transfer', () => { it('should move selected keys to corresponding list', () => { const handleChange = jest.fn(); const wrapper = mount(); - 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(); - 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(); expect(headerText(wrapper)).toEqual('1 of 2'); }); + + describe('pagination', () => { + it('boolean', () => { + const wrapper = mount(); + expect(wrapper.find('Pagination').first().props()).toEqual( + expect.objectContaining({ + pageSize: 10, + }), + ); + }); + + it('object', () => { + const wrapper = mount(); + 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(); + 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(); + wrapper.find('.ant-transfer-list-content-item-remove').first().simulate('click'); + expect(onChange).toHaveBeenCalledWith([], 'left', ['b']); + }); }); diff --git a/components/transfer/demo/basic.md b/components/transfer/demo/basic.md index 722a0604fd..7cf5f52df9 100644 --- a/components/transfer/demo/basic.md +++ b/components/transfer/demo/basic.md @@ -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} /> - + + +
    ); } diff --git a/components/transfer/demo/large-data.md b/components/transfer/demo/large-data.md index 5225103f98..0eea898b81 100644 --- a/components/transfer/demo/large-data.md +++ b/components/transfer/demo/large-data.md @@ -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 ( + <> item.title} + oneWay={oneWay} + pagination /> - ); - } -} +
    + + + ); +}; ReactDOM.render(, mountNode); ``` diff --git a/components/transfer/demo/oneWay.md b/components/transfer/demo/oneWay.md new file mode 100644 index 0000000000..f2ba39f746 --- /dev/null +++ b/components/transfer/demo/oneWay.md @@ -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 ( +
    + item.title} + disabled={disabled} + oneWay + /> + + + +
    + ); + } +} + +ReactDOM.render(, mountNode); +``` diff --git a/components/transfer/demo/tree-transfer.md b/components/transfer/demo/tree-transfer.md index a0de9a1d67..35601addb9 100644 --- a/components/transfer/demo/tree-transfer.md +++ b/components/transfer/demo/tree-transfer.md @@ -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 }) => ( - - {generateTree(children, checkedKeys)} - - )); + 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)} - + /> ); } }} @@ -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' }, ]; diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md index a71092dc5c..fea739a6fa 100644 --- a/components/transfer/index.en-US.md +++ b/components/transfer/index.en-US.md @@ -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)[] | | | diff --git a/components/transfer/index.tsx b/components/transfer/index.tsx index c27cedf8bc..f6bb5c6c6b 100644 --- a/components/transfer/index.tsx +++ b/components/transfer/index.tsx @@ -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 { +interface TransferState { + sourceSelectedKeys: string[]; + targetSelectedKeys: string[]; +} + +class Transfer extends React.Component { // For high-level customized Transfer @dqaria static List = List; @@ -89,14 +104,26 @@ class Transfer extends React.Component { 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 { }; } - // 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 { // 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 { 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 { 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 { 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) => { const { onScroll } = this.props; if (onScroll) { @@ -317,11 +358,15 @@ class Transfer extends React.Component { 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 { direction="left" showSelectAll={showSelectAll} selectAllLabel={selectAllLabels[0]} + pagination={mergedPagination} {...locale} /> { style={operationStyle} disabled={disabled} direction={direction} + oneWay={oneWay} /> { 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 { direction="right" showSelectAll={showSelectAll} selectAllLabel={selectAllLabels[1]} + showRemove={oneWay} + pagination={mergedPagination} {...locale} />
    diff --git a/components/transfer/index.zh-CN.md b/components/transfer/index.zh-CN.md index e229748488..f0fd78b349 100644 --- a/components/transfer/index.zh-CN.md +++ b/components/transfer/index.zh-CN.md @@ -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)[] | | | diff --git a/components/transfer/interface.ts b/components/transfer/interface.ts new file mode 100644 index 0000000000..d45cd7a655 --- /dev/null +++ b/components/transfer/interface.ts @@ -0,0 +1,5 @@ +export type PaginationType = + | boolean + | { + pageSize?: number; + }; diff --git a/components/transfer/list.tsx b/components/transfer/list.tsx index 5b86291a83..e89708ff08 100644 --- a/components/transfer/list.tsx +++ b/components/transfer/list.tsx @@ -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) => 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(); + 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) => { + 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 = ; + } + return { + customize, + bodyContent, + }; + }; + getListBody( prefixCls: string, searchPlaceholder: string, @@ -157,7 +200,7 @@ export default class TransferList extends React.PureComponent< ) : 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) => { - 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 ?
    {footerDom}
    : 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 = ( + + {/* Remove Current Page */} + {pagination && ( + { + const pageKeys = getEnabledItemKeys( + (this.defaultListBodyRef.current?.getItems() || []).map(entity => entity.item), + ); + onItemRemove?.(pageKeys); + }} + > + {removeCurrent} + + )} + + {/* Remove All */} + { + onItemRemove?.(getEnabledItemKeys(filteredItems)); + }} + > + {removeAll} + + + ); + } else { + menu = ( + + { + const keys = getEnabledItemKeys(filteredItems); + onItemSelectAll(keys, keys.length !== checkedKeys.length); + }} + > + {selectAll} + + {pagination && ( + { + const pageItems = this.defaultListBodyRef.current?.getItems() || []; + onItemSelectAll(getEnabledItemKeys(pageItems.map(entity => entity.item)), true); + }} + > + {selectCurrent} + + )} + { + 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} + + + ); + } + + const dropdown = ( + + + ); // ================================== Render =================================== @@ -326,10 +440,12 @@ export default class TransferList extends React.PureComponent< {/* Header */}
    {checkAllCheckbox} + {dropdown} - {this.getSelectAllLabel(checkedKeys.length, filteredItems.length)} - {titleText} + {this.getSelectAllLabel(checkedKeys.length, filteredItems.length)} + + {titleText}
    {/* Body */} diff --git a/components/transfer/operation.tsx b/components/transfer/operation.tsx index 87ac7bb46f..56190fa02a 100644 --- a/components/transfer/operation.tsx +++ b/components/transfer/operation.tsx @@ -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) => (
    - + {!oneWay && ( + + )}
    ); diff --git a/components/transfer/renderListBody.tsx b/components/transfer/renderListBody.tsx deleted file mode 100644 index 3c065bff1f..0000000000 --- a/components/transfer/renderListBody.tsx +++ /dev/null @@ -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; -type PartialTransferListProps = Omit; - -export interface TransferListBodyProps extends PartialTransferListProps { - filteredItems: TransferItem[]; - filteredRenderItems: RenderedItem[]; - selectedKeys: string[]; -} - -class ListBody extends React.Component { - 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 ( -
      - {filteredRenderItems.map(({ renderedEl, renderedText, item }: RenderedItem) => { - const { disabled } = item; - const checked = selectedKeys.indexOf(item.key) >= 0; - - return ( - - ); - })} -
    - ); - } -} - -const ListBodyWrapper = (props: TransferListBodyProps) => ; - -export default ListBodyWrapper; diff --git a/components/transfer/style/customize.less b/components/transfer/style/customize.less index 1f14aa8910..1e78a900f2 100644 --- a/components/transfer/style/customize.less +++ b/components/transfer/style/customize.less @@ -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 =================== diff --git a/components/transfer/style/index.less b/components/transfer/style/index.less index 320a93cceb..13d513d49b 100644 --- a/components/transfer/style/index.less +++ b/components/transfer/style/index.less @@ -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; diff --git a/components/transfer/style/index.tsx b/components/transfer/style/index.tsx index b7eb1d7896..0cb16d7ce6 100644 --- a/components/transfer/style/index.tsx +++ b/components/transfer/style/index.tsx @@ -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';