refactor: (TransferBody) CC => FC (#39988)

* fix

* fix

* fix type

* rerun ci

* fix

* fix

* fix

* fix

* rename

* add useMemo

* fix

* Code style optimization

* Code style optimization

* fix

* fix

* remove useMemo
This commit is contained in:
lijianan 2023-01-05 10:37:35 +08:00 committed by GitHub
parent 81a1ffd53c
commit c5ab5971fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 174 additions and 242 deletions

View File

@ -1,9 +1,9 @@
import classNames from 'classnames';
import * as React from 'react';
import type { KeyWiseTransferItem } from '.';
import Pagination from '../pagination';
import type { PaginationType } from './interface';
import type { RenderedItem, TransferListProps } from './list';
import Pagination from '../pagination';
import ListItem from './ListItem';
export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'] as const;
@ -16,12 +16,12 @@ export interface TransferListBodyProps<RecordType> extends PartialTransferListPr
selectedKeys: string[];
}
function parsePagination(pagination?: PaginationType) {
const parsePagination = (pagination?: PaginationType) => {
if (!pagination) {
return null;
}
const defaultPagination = {
const defaultPagination: PaginationType = {
pageSize: 10,
simple: true,
showSizeChanger: false,
@ -29,139 +29,119 @@ function parsePagination(pagination?: PaginationType) {
};
if (typeof pagination === 'object') {
return {
...defaultPagination,
...pagination,
};
return { ...defaultPagination, ...pagination };
}
return defaultPagination;
};
export interface ListBodyRef<RecordType extends KeyWiseTransferItem> {
items?: RenderedItem<RecordType>[];
}
interface TransferListBodyState {
current: number;
}
const TransferListBody: React.ForwardRefRenderFunction<
ListBodyRef<KeyWiseTransferItem>,
TransferListBodyProps<KeyWiseTransferItem>
> = <RecordType extends KeyWiseTransferItem>(
props: TransferListBodyProps<RecordType>,
ref: React.ForwardedRef<ListBodyRef<RecordType>>,
) => {
const {
prefixCls,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
showRemove,
pagination,
onScroll,
onItemSelect,
onItemRemove,
} = props;
class ListBody<RecordType extends KeyWiseTransferItem> extends React.Component<
TransferListBodyProps<RecordType>,
TransferListBodyState
> {
state = {
current: 1,
};
const [current, setCurrent] = React.useState<number>(1);
static getDerivedStateFromProps<T>(
{ filteredRenderItems, pagination }: TransferListBodyProps<T>,
{ current }: TransferListBodyState,
) {
React.useEffect(() => {
const mergedPagination = parsePagination(pagination);
if (mergedPagination) {
// Calculate the page number
const maxPageCount = Math.ceil(filteredRenderItems.length / mergedPagination.pageSize);
if (current > maxPageCount) {
return { current: maxPageCount };
}
const maxPageCount = Math.ceil(filteredRenderItems.length / mergedPagination.pageSize!);
setCurrent(Math.min(current, maxPageCount));
}
}, [filteredRenderItems, pagination]);
return null;
}
onItemSelect = (item: RecordType) => {
const { onItemSelect, selectedKeys } = this.props;
const checked = selectedKeys.includes(item.key);
onItemSelect(item.key, !checked);
const onClick = (item: RecordType) => {
onItemSelect?.(item.key, !selectedKeys.includes(item.key));
};
onItemRemove = (item: RecordType) => {
const { onItemRemove } = this.props;
const onRemove = (item: RecordType) => {
onItemRemove?.([item.key]);
};
onPageChange = (current: number) => {
this.setState({ current });
const onPageChange = (cur: number) => {
setCurrent(cur);
};
getItems = () => {
const { current } = this.state;
const { pagination, filteredRenderItems } = this.props;
const memoizedItems = React.useMemo<RenderedItem<RecordType>[]>(() => {
const mergedPagination = parsePagination(pagination);
let displayItems = filteredRenderItems;
if (mergedPagination) {
displayItems = filteredRenderItems.slice(
(current - 1) * mergedPagination.pageSize,
current * mergedPagination.pageSize,
);
}
const displayItems = mergedPagination
? filteredRenderItems.slice(
(current - 1) * mergedPagination.pageSize!,
current * mergedPagination.pageSize!,
)
: filteredRenderItems;
return displayItems;
};
}, [current, filteredRenderItems, pagination]);
render() {
const { current } = this.state;
const {
prefixCls,
onScroll,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
showRemove,
pagination,
} = this.props;
React.useImperativeHandle(ref, () => ({ items: memoizedItems }));
const mergedPagination = parsePagination(pagination);
let paginationNode: React.ReactNode = null;
const mergedPagination = parsePagination(pagination);
if (mergedPagination) {
paginationNode = (
<Pagination
simple={mergedPagination.simple}
showSizeChanger={mergedPagination.showSizeChanger}
showLessItems={mergedPagination.showLessItems}
size="small"
disabled={globalDisabled}
className={`${prefixCls}-pagination`}
total={filteredRenderItems.length}
pageSize={mergedPagination.pageSize}
current={current}
onChange={this.onPageChange}
/>
);
}
const paginationNode: React.ReactNode = mergedPagination ? (
<Pagination
size="small"
disabled={globalDisabled}
simple={mergedPagination.simple}
pageSize={mergedPagination.pageSize}
showLessItems={mergedPagination.showLessItems}
showSizeChanger={mergedPagination.showSizeChanger}
className={`${prefixCls}-pagination`}
total={filteredRenderItems.length}
current={current}
onChange={onPageChange}
/>
) : null;
return (
<>
<ul
className={classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-show-remove`]: showRemove,
})}
onScroll={onScroll}
>
{this.getItems().map(({ renderedEl, renderedText, item }: RenderedItem<RecordType>) => {
const { disabled } = item;
const checked = selectedKeys.includes(item.key);
return (
<ListItem
disabled={globalDisabled || disabled}
key={item.key}
item={item}
renderedText={renderedText}
renderedEl={renderedEl}
checked={checked}
prefixCls={prefixCls}
onClick={this.onItemSelect}
onRemove={this.onItemRemove}
showRemove={showRemove}
/>
);
})}
</ul>
{paginationNode}
</>
);
}
const cls = classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-show-remove`]: showRemove,
});
return (
<>
<ul className={cls} onScroll={onScroll}>
{(memoizedItems || []).map(({ renderedEl, renderedText, item }) => (
<ListItem
key={item.key}
item={item}
renderedText={renderedText}
renderedEl={renderedEl}
prefixCls={prefixCls}
showRemove={showRemove}
onClick={onClick}
onRemove={onRemove}
checked={selectedKeys.includes(item.key)}
disabled={globalDisabled || item.disabled}
/>
))}
</ul>
{paginationNode}
</>
);
};
if (process.env.NODE_ENV !== 'production') {
TransferListBody.displayName = 'TransferListBody';
}
export default ListBody;
export default React.forwardRef<
ListBodyRef<KeyWiseTransferItem>,
TransferListBodyProps<KeyWiseTransferItem>
>(TransferListBody);

View File

@ -1,9 +1,10 @@
import React from 'react';
import { render } from '../../../tests/utils';
import type { KeyWiseTransferItem } from '..';
import type { TransferListProps } from '../list';
import { render } from '../../../tests/utils';
import List from '../list';
const listCommonProps: TransferListProps<any> = {
const listCommonProps: TransferListProps<KeyWiseTransferItem> = {
prefixCls: 'ant-transfer-list',
dataSource: [
{ key: 'a', title: 'a' },
@ -12,11 +13,11 @@ const listCommonProps: TransferListProps<any> = {
],
checkedKeys: ['a'],
notFoundContent: 'Not Found',
} as TransferListProps<any>;
} as TransferListProps<KeyWiseTransferItem>;
const listProps: TransferListProps<any> = {
const listProps: TransferListProps<KeyWiseTransferItem> = {
...listCommonProps,
dataSource: undefined as unknown as any[],
dataSource: undefined as unknown as KeyWiseTransferItem[],
};
describe('Transfer.List', () => {

View File

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-shadow */
import DownOutlined from '@ant-design/icons/DownOutlined';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import * as React from 'react';
import React, { useMemo, useRef, useState } from 'react';
import Checkbox from '../checkbox';
import Dropdown from '../dropdown';
import type { MenuProps } from '../menu';
@ -17,7 +16,7 @@ import type {
TransferLocale,
} from './index';
import type { PaginationType } from './interface';
import type { TransferListBodyProps } from './ListBody';
import type { ListBodyRef, TransferListBodyProps } from './ListBody';
import DefaultListBody, { OmitProps } from './ListBody';
import Search from './search';
@ -66,7 +65,7 @@ export interface TransferListProps<RecordType> extends TransferLocale {
props: TransferListProps<RecordType>,
info?: { direction: TransferDirection },
) => React.ReactNode;
onScroll: (e: React.UIEvent<HTMLUListElement>) => void;
onScroll: (e: React.UIEvent<HTMLUListElement, UIEvent>) => void;
disabled?: boolean;
direction: TransferDirection;
showSelectAll?: boolean;
@ -110,20 +109,9 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
render = defaultRender,
} = props;
const [filterValue, setFilterValue] = React.useState<string>('');
const [filterValue, setFilterValue] = useState<string>('');
const defaultListBodyRef = React.useRef<DefaultListBody<RecordType>>(null);
const getCheckStatus = (filteredItems: RecordType[]) => {
if (checkedKeys.length === 0) {
return 'none';
}
const checkedKeysMap = groupKeysMap(checkedKeys);
if (filteredItems.every((item) => checkedKeysMap.has(item.key) || !!item.disabled)) {
return 'all';
}
return 'part';
};
const listBodyRef = useRef<ListBodyRef<RecordType>>({});
const internalHandleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterValue(e.target.value);
@ -142,14 +130,11 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
return text.includes(filterValue);
};
const renderListBody = (
renderList: RenderListFunction<RecordType> | undefined,
props: TransferListBodyProps<RecordType>,
) => {
let bodyContent: React.ReactNode = renderList ? renderList(props) : null;
const renderListBody = (listProps: TransferListBodyProps<RecordType>) => {
let bodyContent: React.ReactNode = renderList ? renderList(listProps) : null;
const customize: boolean = !!bodyContent;
if (!customize) {
bodyContent = <DefaultListBody ref={defaultListBodyRef} {...props} />;
bodyContent = <DefaultListBody ref={listBodyRef} {...listProps} />;
}
return { customize, bodyContent };
};
@ -158,51 +143,46 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
const renderResult = render(item);
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
return {
renderedText: isRenderResultPlain
? (renderResult as RenderResultObject).value
: (renderResult as string),
renderedEl: isRenderResultPlain ? (renderResult as RenderResultObject).label : renderResult,
item,
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
renderedText: isRenderResultPlain ? renderResult.value : (renderResult as string),
};
};
const getFilteredItems = (
dataSource: RecordType[],
filterValue: string,
): {
filteredItems: RecordType[];
filteredRenderItems: RenderedItem<RecordType>[];
} => {
const filteredItems: RecordType[] = [];
const filteredRenderItems: RenderedItem<RecordType>[] = [];
const notFoundContentEle = useMemo<React.ReactNode>(
() =>
Array.isArray(notFoundContent)
? notFoundContent[direction === 'left' ? 0 : 1]
: notFoundContent,
[notFoundContent, direction],
);
const [filteredItems, filteredRenderItems] = useMemo(() => {
const filterItems: RecordType[] = [];
const filterRenderItems: RenderedItem<RecordType>[] = [];
dataSource.forEach((item) => {
const renderedItem = renderItem(item);
const { renderedText } = renderedItem;
// Filter skip
if (filterValue && !matchFilter(renderedText, item)) {
return null;
if (filterValue && !matchFilter(renderedItem.renderedText, item)) {
return;
}
filteredItems.push(item);
filteredRenderItems.push(renderedItem);
filterItems.push(item);
filterRenderItems.push(renderedItem);
});
return { filteredItems, filteredRenderItems };
};
return [filterItems, filterRenderItems] as const;
}, [dataSource, filterValue]);
const getListBody = (
prefixCls: string,
searchPlaceholder: string,
filterValue: string,
filteredItems: RecordType[],
notFoundContent: React.ReactNode | React.ReactNode[],
filteredRenderItems: RenderedItem<RecordType>[],
checkedKeys: string[],
renderList?: RenderListFunction<RecordType>,
showSearch?: boolean,
disabled?: boolean,
): React.ReactNode => {
const checkStatus = useMemo<string>(() => {
if (checkedKeys.length === 0) {
return 'none';
}
const checkedKeysMap = groupKeysMap(checkedKeys);
if (filteredItems.every((item) => checkedKeysMap.has(item.key) || !!item.disabled)) {
return 'all';
}
return 'part';
}, [checkedKeys, filteredItems]);
const listBody = useMemo<React.ReactNode>(() => {
const search = showSearch ? (
<div className={`${prefixCls}-body-search-wrapper`}>
<Search
@ -216,18 +196,13 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
</div>
) : null;
const { bodyContent, customize } = renderListBody(renderList, {
const { customize, bodyContent } = renderListBody({
...omit(props, OmitProps),
filteredItems,
filteredRenderItems,
selectedKeys: checkedKeys,
});
const getNotFoundContent = () =>
Array.isArray(notFoundContent)
? notFoundContent[direction === 'left' ? 0 : 1]
: notFoundContent;
let bodyNode: React.ReactNode;
// We should wrap customize list body in a classNamed div to use flex layout.
if (customize) {
@ -236,10 +211,9 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
bodyNode = filteredItems.length ? (
bodyContent
) : (
<div className={`${prefixCls}-body-not-found`}>{getNotFoundContent()}</div>
<div className={`${prefixCls}-body-not-found`}>{notFoundContentEle}</div>
);
}
return (
<div
className={classNames(
@ -250,38 +224,33 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
{bodyNode}
</div>
);
};
const getCheckBox = ({
filteredItems,
onItemSelectAll,
disabled,
}, [
showSearch,
prefixCls,
}: {
filteredItems: RecordType[];
onItemSelectAll: (dataSource: string[], checkAll: boolean) => void;
disabled?: boolean;
prefixCls?: string;
}): false | React.ReactNode => {
const checkStatus = getCheckStatus(filteredItems);
const checkedAll = checkStatus === 'all';
const checkAllCheckbox: React.ReactNode = (
<Checkbox
disabled={disabled}
checked={checkedAll}
indeterminate={checkStatus === 'part'}
className={`${prefixCls}-checkbox`}
onChange={() => {
// Only select enabled items
onItemSelectAll(
filteredItems.filter((item) => !item.disabled).map(({ key }) => key),
!checkedAll,
);
}}
/>
);
return checkAllCheckbox;
};
searchPlaceholder,
filterValue,
disabled,
checkedKeys,
filteredItems,
filteredRenderItems,
notFoundContentEle,
]);
const checkBox = (
<Checkbox
disabled={disabled}
checked={checkStatus === 'all'}
indeterminate={checkStatus === 'part'}
className={`${prefixCls}-checkbox`}
onChange={() => {
// Only select enabled items
onItemSelectAll?.(
filteredItems.filter((item) => !item.disabled).map(({ key }) => key),
checkStatus !== 'all',
);
}}
/>
);
const getSelectAllLabel = (selectedCount: number, totalCount: number): React.ReactNode => {
if (selectAllLabel) {
@ -307,27 +276,9 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
// ====================== Get filtered, checked item list ======================
const { filteredItems, filteredRenderItems } = getFilteredItems(dataSource, filterValue);
const listBody = getListBody(
prefixCls,
searchPlaceholder,
filterValue,
filteredItems,
notFoundContent,
filteredRenderItems,
checkedKeys,
renderList,
showSearch,
disabled,
);
const listFooter = footerDom ? <div className={`${prefixCls}-footer`}>{footerDom}</div> : null;
const checkAllCheckbox =
!showRemove &&
!pagination &&
getCheckBox({ filteredItems, onItemSelectAll, disabled, prefixCls });
const checkAllCheckbox = !showRemove && !pagination && checkBox;
let items: MenuProps['items'];
@ -340,7 +291,7 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
label: removeCurrent,
onClick() {
const pageKeys = getEnabledItemKeys(
(defaultListBodyRef.current?.getItems() || []).map((entity) => entity.item),
(listBodyRef.current?.items || []).map((entity) => entity.item),
);
onItemRemove?.(pageKeys);
},
@ -362,7 +313,7 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
label: selectAll,
onClick() {
const keys = getEnabledItemKeys(filteredItems);
onItemSelectAll(keys, keys.length !== checkedKeys.length);
onItemSelectAll?.(keys, keys.length !== checkedKeys.length);
},
},
pagination
@ -370,8 +321,8 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
key: 'selectCurrent',
label: selectCurrent,
onClick() {
const pageItems = defaultListBodyRef.current?.getItems() || [];
onItemSelectAll(getEnabledItemKeys(pageItems.map((entity) => entity.item)), true);
const pageItems = listBodyRef.current?.items || [];
onItemSelectAll?.(getEnabledItemKeys(pageItems.map((entity) => entity.item)), true);
},
}
: null,
@ -381,7 +332,7 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
onClick() {
const availableKeys = getEnabledItemKeys(
pagination
? (defaultListBodyRef.current?.getItems() || []).map((entity) => entity.item)
? (listBodyRef.current?.items || []).map((entity) => entity.item)
: filteredItems,
);
const checkedKeySet = new Set<string>(checkedKeys);
@ -394,14 +345,14 @@ const TransferList = <RecordType extends KeyWiseTransferItem>(
newCheckedKeys.push(key);
}
});
onItemSelectAll(newCheckedKeys, true);
onItemSelectAll(newUnCheckedKeys, false);
onItemSelectAll?.(newCheckedKeys, true);
onItemSelectAll?.(newUnCheckedKeys, false);
},
},
];
}
const dropdown = (
const dropdown: React.ReactNode = (
<Dropdown className={`${prefixCls}-header-dropdown`} menu={{ items }} disabled={disabled}>
<DownOutlined />
</Dropdown>