feat: improve Transfer key type (#47879)

* feat: add Transfer's key type

* docs: update prop

* fix: key type

* fix: cycle dependency
This commit is contained in:
章鱼怪 2024-03-14 21:14:21 +08:00 committed by GitHub
parent 648580cb3c
commit c8635a7bc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 90 additions and 71 deletions

View File

@ -1,5 +1,7 @@
export const groupKeysMap = (keys: string[]) => {
const map = new Map<string, number>();
import type { TransferKey } from '../transfer/interface';
export const groupKeysMap = (keys: TransferKey[]) => {
const map = new Map<TransferKey, number>();
keys.forEach((key, index) => {
map.set(key, index);
});
@ -7,7 +9,7 @@ export const groupKeysMap = (keys: string[]) => {
};
export const groupDisabledKeysMap = <RecordType extends any[]>(dataSource: RecordType) => {
const map = new Map<string, number>();
const map = new Map<TransferKey, number>();
dataSource.forEach(({ disabled, key }, index) => {
if (disabled) {
map.set(key, index);

View File

@ -4,19 +4,19 @@ import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { KeyWiseTransferItem } from '.';
import Pagination from '../pagination';
import type { PaginationType } from './interface';
import type { PaginationType, TransferKey } from './interface';
import type { RenderedItem, TransferListProps } from './list';
import ListItem from './ListItem';
export const OmitProps = ['handleFilter', 'handleClear', 'checkedKeys'] as const;
export type OmitProp = typeof OmitProps[number];
export type OmitProp = (typeof OmitProps)[number];
type PartialTransferListProps<RecordType> = Omit<TransferListProps<RecordType>, OmitProp>;
type ExistPagination = Exclude<PaginationType, boolean>;
export interface TransferListBodyProps<RecordType> extends PartialTransferListProps<RecordType> {
filteredItems: RecordType[];
filteredRenderItems: RenderedItem<RecordType>[];
selectedKeys: string[];
selectedKeys: TransferKey[];
}
const parsePagination = (pagination?: ExistPagination) => {

View File

@ -437,7 +437,9 @@ describe('Transfer', () => {
const renderFunc: TransferProps<any>['render'] = (item) => item.title;
const handleChange = jest.fn();
const TransferDemo = () => {
const [selectedKeys, setSelectedKeys] = useState<string[]>(searchTransferProps.selectedKeys);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
searchTransferProps.selectedKeys,
);
const handleSelectChange: TransferProps<any>['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
@ -600,7 +602,7 @@ describe('Transfer', () => {
const onSelectChange = jest.fn();
const Demo = () => {
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
return (
<Transfer

View File

@ -17,7 +17,7 @@ const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
const [targetKeys, setTargetKeys] = useState<React.Key[]>(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {

View File

@ -114,8 +114,8 @@ const rightTableColumns: TableColumnsType<Pick<DataType, 'title'>> = [
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
console.log('targetKeys:', nextTargetKeys);

View File

@ -11,7 +11,7 @@ interface RecordType {
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
const getMock = () => {
const tempTargetKeys = [];

View File

@ -22,7 +22,7 @@ const selectAllLabels: TransferProps['selectAllLabels'] = [
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState(oriTargetKeys);
const [targetKeys, setTargetKeys] = useState<React.Key[]>(oriTargetKeys);
return (
<Transfer
dataSource={mockData}

View File

@ -12,7 +12,7 @@ interface RecordType {
const App: React.FC = () => {
const [oneWay, setOneWay] = useState(false);
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<string[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
useEffect(() => {
const newTargetKeys = [];

View File

@ -19,8 +19,8 @@ const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
const oriTargetKeys = mockData.filter((item) => Number(item.key) % 3 > 1).map((item) => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<string[]>(oriTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>(oriTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [disabled, setDisabled] = useState(false);
const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {

View File

@ -3,11 +3,12 @@ import * as React from 'react';
import type { KeyWise, TransferProps } from '..';
import { groupKeysMap } from '../../_util/transKeys';
import type { AnyObject } from '../../_util/type';
import type { TransferKey } from '../interface';
const useData = <RecordType extends AnyObject>(
dataSource?: RecordType[],
rowKey?: TransferProps<RecordType>['rowKey'],
targetKeys?: string[],
targetKeys?: TransferKey[],
) => {
const mergedDataSource = React.useMemo(
() =>

View File

@ -1,25 +1,26 @@
import * as React from 'react';
import type { TransferKey } from '../interface';
const EMPTY_KEYS: string[] = [];
const EMPTY_KEYS: TransferKey[] = [];
function filterKeys(keys: string[], dataKeys: Set<string>) {
function filterKeys(keys: TransferKey[], dataKeys: Set<TransferKey>) {
const filteredKeys = keys.filter((key) => dataKeys.has(key));
return keys.length === filteredKeys.length ? keys : filteredKeys;
}
function flattenKeys(keys: Set<string>) {
function flattenKeys(keys: Set<TransferKey>) {
return Array.from(keys).join(';');
}
export default function useSelection<T extends { key: string }>(
export default function useSelection<T extends { key: TransferKey }>(
leftDataSource: T[],
rightDataSource: T[],
selectedKeys: string[] = EMPTY_KEYS,
selectedKeys: TransferKey[] = EMPTY_KEYS,
): [
sourceSelectedKeys: string[],
targetSelectedKeys: string[],
setSourceSelectedKeys: React.Dispatch<React.SetStateAction<string[]>>,
setTargetSelectedKeys: React.Dispatch<React.SetStateAction<string[]>>,
sourceSelectedKeys: TransferKey[],
targetSelectedKeys: TransferKey[],
setSourceSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
setTargetSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
] {
// Prepare `dataSource` keys
const [leftKeys, rightKeys] = React.useMemo(

View File

@ -53,11 +53,11 @@ Common props ref[Common props](/docs/react/common-props)
| pagination | Use pagination. Not work in render props | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | 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 | - | |
| selectAllLabels | A set of customized labels for select all checkboxes on the header | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)\[] | - | |
| selectedKeys | A set of keys of selected items | string\[] | \[] | |
| selectedKeys | A set of keys of selected items | string\[] \| number\[] | \[] | |
| showSearch | If included, a search box is shown on each column | boolean | false | |
| showSelectAll | Show select all checkbox on the header | boolean | true | |
| status | Set validation status | 'error' \| 'warning' | - | 4.19.0 |
| targetKeys | A set of keys of elements that are listed on the right column | string\[] | \[] | |
| targetKeys | A set of keys of elements that are listed on the right column | string\[] \| number\[] | \[] | |
| titles | A set of titles that are sorted from left to right | ReactNode\[] | - | |
| 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 | - | |
@ -68,14 +68,14 @@ Common props ref[Common props](/docs/react/common-props)
Transfer accept `children` to customize render list, using follow props:
| Property | Description | Type | Version |
| --------------- | ----------------------- | ------------------------------------ | ------- |
| direction | List render direction | `left` \| `right` | |
| disabled | Disable list or not | boolean | |
| filteredItems | Filtered items | RecordType\[] | |
| selectedKeys | Selected items | string\[] | |
| onItemSelect | Select item | (key: string, selected: boolean) | |
| onItemSelectAll | Select a group of items | (keys: string\[], selected: boolean) | |
| Property | Description | Type | Version |
| --- | --- | --- | --- |
| direction | List render direction | `left` \| `right` | |
| disabled | Disable list or not | boolean | |
| filteredItems | Filtered items | RecordType\[] | |
| selectedKeys | Selected items | string\[] \| number\[] | |
| onItemSelect | Select item | (key: string \| number, selected: boolean) | |
| onItemSelectAll | Select a group of items | (keys: string\[] \| number\[], selected: boolean) | |
#### example

View File

@ -17,7 +17,7 @@ import { useLocale } from '../locale';
import defaultLocale from '../locale/en_US';
import useData from './hooks/useData';
import useSelection from './hooks/useSelection';
import type { PaginationType } from './interface';
import type { PaginationType, TransferKey } from './interface';
import type { TransferCustomListBodyProps, TransferListProps } from './list';
import List from './list';
import Operation from './operation';
@ -38,14 +38,14 @@ export interface RenderResultObject {
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
export interface TransferItem {
key?: string;
key?: TransferKey;
title?: string;
description?: string;
disabled?: boolean;
[name: string]: any;
}
export type KeyWise<T> = T & { key: string };
export type KeyWise<T> = T & { key: TransferKey };
export type KeyWiseTransferItem = KeyWise<TransferItem>;
@ -79,11 +79,15 @@ export interface TransferProps<RecordType = any> {
rootClassName?: string;
disabled?: boolean;
dataSource?: RecordType[];
targetKeys?: string[];
selectedKeys?: string[];
targetKeys?: TransferKey[];
selectedKeys?: TransferKey[];
render?: TransferRender<RecordType>;
onChange?: (targetKeys: string[], direction: TransferDirection, moveKeys: string[]) => void;
onSelectChange?: (sourceSelectedKeys: string[], targetSelectedKeys: string[]) => void;
onChange?: (
targetKeys: TransferKey[],
direction: TransferDirection,
moveKeys: TransferKey[],
) => void;
onSelectChange?: (sourceSelectedKeys: TransferKey[], targetSelectedKeys: TransferKey[]) => void;
style?: React.CSSProperties;
listStyle?: ((style: ListStyle) => CSSProperties) | CSSProperties;
operationStyle?: CSSProperties;
@ -96,7 +100,7 @@ export interface TransferProps<RecordType = any> {
props: TransferListProps<RecordType>,
info?: { direction: TransferDirection },
) => React.ReactNode;
rowKey?: (record: RecordType) => string;
rowKey?: (record: RecordType) => TransferKey;
onSearch?: (direction: TransferDirection, value: string) => void;
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => void;
children?: (props: TransferCustomListBodyProps<RecordType>) => React.ReactNode;
@ -172,11 +176,11 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
const [leftMultipleSelect, updateLeftPrevSelectedIndex] = useMultipleSelect<
KeyWise<RecordType>,
string
TransferKey
>((item) => item.key);
const [rightMultipleSelect, updateRightPrevSelectedIndex] = useMultipleSelect<
KeyWise<RecordType>,
string
TransferKey
>((item) => item.key);
if (process.env.NODE_ENV !== 'production') {
@ -186,7 +190,10 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
}
const setStateKeys = useCallback(
(direction: TransferDirection, keys: string[] | ((prevKeys: string[]) => string[])) => {
(
direction: TransferDirection,
keys: TransferKey[] | ((prevKeys: TransferKey[]) => TransferKey[]),
) => {
if (direction === 'left') {
const nextKeys = typeof keys === 'function' ? keys(sourceSelectedKeys || []) : keys;
setSourceSelectedKeys(nextKeys);
@ -207,7 +214,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
};
const handleSelectChange = useCallback(
(direction: TransferDirection, holder: string[]) => {
(direction: TransferDirection, holder: TransferKey[]) => {
if (direction === 'left') {
onSelectChange?.(holder, targetSelectedKeys);
} else {
@ -263,12 +270,12 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
checkAll: boolean | 'replace',
) => {
setStateKeys(direction, (prevKeys) => {
let mergedCheckedKeys: string[] = [];
let mergedCheckedKeys: TransferKey[] = [];
if (checkAll === 'replace') {
mergedCheckedKeys = keys;
} else if (checkAll) {
// Merge current keys with origin key
mergedCheckedKeys = Array.from(new Set<string>([...prevKeys, ...keys]));
mergedCheckedKeys = Array.from(new Set<TransferKey>([...prevKeys, ...keys]));
} else {
const selectedKeysMap = groupKeysMap(keys);
// Remove current keys from origin keys
@ -298,8 +305,8 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
const handleSingleSelect = (
direction: TransferDirection,
holder: Set<string>,
selectedKey: string,
holder: Set<TransferKey>,
selectedKey: TransferKey,
checked: boolean,
currentSelectedIndex: number,
) => {
@ -317,7 +324,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
const handleMultipleSelect = (
direction: TransferDirection,
data: KeyWise<RecordType>[],
holder: Set<string>,
holder: Set<TransferKey>,
currentSelectedIndex: number,
) => {
const isLeftDirection = direction === 'left';
@ -327,7 +334,7 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
const onItemSelect = (
direction: TransferDirection,
selectedKey: string,
selectedKey: TransferKey,
checked: boolean,
multiple?: boolean,
) => {
@ -360,14 +367,14 @@ const Transfer = <RecordType extends TransferItem = TransferItem>(
};
const onRightItemSelect = (
selectedKey: string,
selectedKey: TransferKey,
checked: boolean,
e?: React.MouseEvent<Element, MouseEvent>,
) => {
onItemSelect('right', selectedKey, checked, e?.shiftKey);
};
const onRightItemRemove = (keys: string[]) => {
const onRightItemRemove = (keys: TransferKey[]) => {
setStateKeys('right', []);
onChange?.(
targetKeys.filter((key) => !keys.includes(key)),

View File

@ -56,11 +56,11 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*g9vUQq2nkpEAAA
| pagination | 使用分页样式,自定义渲染列表下无效 | boolean \| { pageSize: number, simple: boolean, showSizeChanger?: boolean, showLessItems?: boolean } | false | 4.3.0 |
| render | 每行数据渲染函数,该函数的入参为 `dataSource` 中的项,返回值为 ReactElement。或者返回一个普通对象其中 `label` 字段为 ReactElement`value` 字段为 title | (record) => ReactNode | - | |
| selectAllLabels | 自定义顶部多选框标题的集合 | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)\[] | - | |
| selectedKeys | 设置哪些项应该被选中 | string\[] | \[] | |
| selectedKeys | 设置哪些项应该被选中 | string\[] \| number\[] | \[] | |
| showSearch | 是否显示搜索框 | boolean | false | |
| showSelectAll | 是否展示全选勾选框 | boolean | true | |
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] \| number\[] | \[] | |
| titles | 标题集合,顺序从左至右 | ReactNode\[] | - | |
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | - | |
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | - | |
@ -71,14 +71,14 @@ coverDark: https://mdn.alipayobjects.com/huamei_7uahnr/afts/img/A*g9vUQq2nkpEAAA
Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
| 参数 | 说明 | 类型 | 版本 |
| --------------- | -------------- | ------------------------------------ | ---- |
| direction | 渲染列表的方向 | `left` \| `right` | |
| disabled | 是否禁用列表 | boolean | |
| filteredItems | 过滤后的数据 | RecordType\[] | |
| selectedKeys | 选中的条目 | string\[] | |
| onItemSelect | 勾选条目 | (key: string, selected: boolean) | |
| onItemSelectAll | 勾选一组条目 | (keys: string\[], selected: boolean) | |
| 参数 | 说明 | 类型 | 版本 |
| --------------- | -------------- | ------------------------------------------------- | ---- |
| direction | 渲染列表的方向 | `left` \| `right` | |
| disabled | 是否禁用列表 | boolean | |
| filteredItems | 过滤后的数据 | RecordType\[] | |
| selectedKeys | 选中的条目 | string\[] \| number\[] | |
| onItemSelect | 勾选条目 | (key: string \| number, selected: boolean) | |
| onItemSelectAll | 勾选一组条目 | (keys: string\[] \| number\[], selected: boolean) | |
#### 参考示例

View File

@ -1,3 +1,5 @@
export type TransferKey = React.Key;
export type PaginationType =
| boolean
| {

View File

@ -15,7 +15,7 @@ import type {
TransferDirection,
TransferLocale,
} from './index';
import type { PaginationType } from './interface';
import type { PaginationType, TransferKey } from './interface';
import type { ListBodyRef, TransferListBodyProps } from './ListBody';
import DefaultListBody, { OmitProps } from './ListBody';
import Search from './search';
@ -50,11 +50,15 @@ export interface TransferListProps<RecordType> extends TransferLocale {
dataSource: RecordType[];
filterOption?: (filterText: string, item: RecordType, direction: TransferDirection) => boolean;
style?: React.CSSProperties;
checkedKeys: string[];
checkedKeys: TransferKey[];
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => void;
onItemSelect: (key: string, check: boolean, e?: React.MouseEvent<Element, MouseEvent>) => void;
onItemSelectAll: (dataSource: string[], checkAll: boolean | 'replace') => void;
onItemRemove?: (keys: string[]) => void;
onItemSelect: (
key: TransferKey,
check: boolean,
e?: React.MouseEvent<Element, MouseEvent>,
) => void;
onItemSelectAll: (dataSource: TransferKey[], checkAll: boolean | 'replace') => void;
onItemRemove?: (keys: TransferKey[]) => void;
handleClear: () => void;
/** Render item */
render?: (item: RecordType) => RenderResult;