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 classNames from 'classnames';
import * as React from 'react'; import * as React from 'react';
import type { KeyWiseTransferItem } from '.'; import type { KeyWiseTransferItem } from '.';
import Pagination from '../pagination';
import type { PaginationType } from './interface'; import type { PaginationType } from './interface';
import type { RenderedItem, TransferListProps } from './list'; import type { RenderedItem, TransferListProps } from './list';
import Pagination from '../pagination';
import ListItem from './ListItem'; import ListItem from './ListItem';
export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'] as const; export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'] as const;
@ -16,12 +16,12 @@ export interface TransferListBodyProps<RecordType> extends PartialTransferListPr
selectedKeys: string[]; selectedKeys: string[];
} }
function parsePagination(pagination?: PaginationType) { const parsePagination = (pagination?: PaginationType) => {
if (!pagination) { if (!pagination) {
return null; return null;
} }
const defaultPagination = { const defaultPagination: PaginationType = {
pageSize: 10, pageSize: 10,
simple: true, simple: true,
showSizeChanger: false, showSizeChanger: false,
@ -29,139 +29,119 @@ function parsePagination(pagination?: PaginationType) {
}; };
if (typeof pagination === 'object') { if (typeof pagination === 'object') {
return { return { ...defaultPagination, ...pagination };
...defaultPagination,
...pagination,
};
} }
return defaultPagination; return defaultPagination;
};
export interface ListBodyRef<RecordType extends KeyWiseTransferItem> {
items?: RenderedItem<RecordType>[];
} }
interface TransferListBodyState { const TransferListBody: React.ForwardRefRenderFunction<
current: number; 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< const [current, setCurrent] = React.useState<number>(1);
TransferListBodyProps<RecordType>,
TransferListBodyState
> {
state = {
current: 1,
};
static getDerivedStateFromProps<T>( React.useEffect(() => {
{ filteredRenderItems, pagination }: TransferListBodyProps<T>,
{ current }: TransferListBodyState,
) {
const mergedPagination = parsePagination(pagination); const mergedPagination = parsePagination(pagination);
if (mergedPagination) { if (mergedPagination) {
// Calculate the page number const maxPageCount = Math.ceil(filteredRenderItems.length / mergedPagination.pageSize!);
const maxPageCount = Math.ceil(filteredRenderItems.length / mergedPagination.pageSize); setCurrent(Math.min(current, maxPageCount));
if (current > maxPageCount) {
return { current: maxPageCount };
}
} }
}, [filteredRenderItems, pagination]);
return null; const onClick = (item: RecordType) => {
} onItemSelect?.(item.key, !selectedKeys.includes(item.key));
onItemSelect = (item: RecordType) => {
const { onItemSelect, selectedKeys } = this.props;
const checked = selectedKeys.includes(item.key);
onItemSelect(item.key, !checked);
}; };
onItemRemove = (item: RecordType) => { const onRemove = (item: RecordType) => {
const { onItemRemove } = this.props;
onItemRemove?.([item.key]); onItemRemove?.([item.key]);
}; };
onPageChange = (current: number) => { const onPageChange = (cur: number) => {
this.setState({ current }); setCurrent(cur);
}; };
getItems = () => { const memoizedItems = React.useMemo<RenderedItem<RecordType>[]>(() => {
const { current } = this.state;
const { pagination, filteredRenderItems } = this.props;
const mergedPagination = parsePagination(pagination); const mergedPagination = parsePagination(pagination);
const displayItems = mergedPagination
let displayItems = filteredRenderItems; ? filteredRenderItems.slice(
(current - 1) * mergedPagination.pageSize!,
if (mergedPagination) { current * mergedPagination.pageSize!,
displayItems = filteredRenderItems.slice( )
(current - 1) * mergedPagination.pageSize, : filteredRenderItems;
current * mergedPagination.pageSize,
);
}
return displayItems; return displayItems;
}; }, [current, filteredRenderItems, pagination]);
render() { React.useImperativeHandle(ref, () => ({ items: memoizedItems }));
const { current } = this.state;
const {
prefixCls,
onScroll,
filteredRenderItems,
selectedKeys,
disabled: globalDisabled,
showRemove,
pagination,
} = this.props;
const mergedPagination = parsePagination(pagination); const mergedPagination = parsePagination(pagination);
let paginationNode: React.ReactNode = null;
if (mergedPagination) { const paginationNode: React.ReactNode = mergedPagination ? (
paginationNode = ( <Pagination
<Pagination size="small"
simple={mergedPagination.simple} disabled={globalDisabled}
showSizeChanger={mergedPagination.showSizeChanger} simple={mergedPagination.simple}
showLessItems={mergedPagination.showLessItems} pageSize={mergedPagination.pageSize}
size="small" showLessItems={mergedPagination.showLessItems}
disabled={globalDisabled} showSizeChanger={mergedPagination.showSizeChanger}
className={`${prefixCls}-pagination`} className={`${prefixCls}-pagination`}
total={filteredRenderItems.length} total={filteredRenderItems.length}
pageSize={mergedPagination.pageSize} current={current}
current={current} onChange={onPageChange}
onChange={this.onPageChange} />
/> ) : null;
);
}
return ( const cls = classNames(`${prefixCls}-content`, {
<> [`${prefixCls}-content-show-remove`]: showRemove,
<ul });
className={classNames(`${prefixCls}-content`, {
[`${prefixCls}-content-show-remove`]: showRemove, return (
})} <>
onScroll={onScroll} <ul className={cls} onScroll={onScroll}>
> {(memoizedItems || []).map(({ renderedEl, renderedText, item }) => (
{this.getItems().map(({ renderedEl, renderedText, item }: RenderedItem<RecordType>) => { <ListItem
const { disabled } = item; key={item.key}
const checked = selectedKeys.includes(item.key); item={item}
return ( renderedText={renderedText}
<ListItem renderedEl={renderedEl}
disabled={globalDisabled || disabled} prefixCls={prefixCls}
key={item.key} showRemove={showRemove}
item={item} onClick={onClick}
renderedText={renderedText} onRemove={onRemove}
renderedEl={renderedEl} checked={selectedKeys.includes(item.key)}
checked={checked} disabled={globalDisabled || item.disabled}
prefixCls={prefixCls} />
onClick={this.onItemSelect} ))}
onRemove={this.onItemRemove} </ul>
showRemove={showRemove} {paginationNode}
/> </>
); );
})} };
</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 React from 'react';
import { render } from '../../../tests/utils'; import type { KeyWiseTransferItem } from '..';
import type { TransferListProps } from '../list'; import type { TransferListProps } from '../list';
import { render } from '../../../tests/utils';
import List from '../list'; import List from '../list';
const listCommonProps: TransferListProps<any> = { const listCommonProps: TransferListProps<KeyWiseTransferItem> = {
prefixCls: 'ant-transfer-list', prefixCls: 'ant-transfer-list',
dataSource: [ dataSource: [
{ key: 'a', title: 'a' }, { key: 'a', title: 'a' },
@ -12,11 +13,11 @@ const listCommonProps: TransferListProps<any> = {
], ],
checkedKeys: ['a'], checkedKeys: ['a'],
notFoundContent: 'Not Found', notFoundContent: 'Not Found',
} as TransferListProps<any>; } as TransferListProps<KeyWiseTransferItem>;
const listProps: TransferListProps<any> = { const listProps: TransferListProps<KeyWiseTransferItem> = {
...listCommonProps, ...listCommonProps,
dataSource: undefined as unknown as any[], dataSource: undefined as unknown as KeyWiseTransferItem[],
}; };
describe('Transfer.List', () => { describe('Transfer.List', () => {

View File

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