fix(ts): 添加ts配置

This commit is contained in:
NICE CODE BY DEV 2024-06-03 14:21:08 +08:00
parent 079df27f4b
commit f5ff6e760f
77 changed files with 26385 additions and 111 deletions

View File

@ -167,8 +167,7 @@ var urlToBase64V2 = (url) => {
});
};
function base64toBlob(base64) {
if (!base64)
return;
if (!base64) return;
var arr = base64.split(","), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
@ -186,15 +185,13 @@ var downloadPackageImages = async (imgDataList, zipName) => {
let src = imgDataList[i2].url;
let suffix = src.substring(src.lastIndexOf("."));
let base64ByUrl = await urlToBase64V2(imgDataList[i2].url);
if (!base64ByUrl)
continue;
if (!base64ByUrl) continue;
let blob = base64toBlob(base64ByUrl);
imgDataDownLoadList.push(imgDataList[i2]);
imgBlobList.push(blob);
imageSuffix.push(suffix);
}
if (imgBlobList.length === 0)
throw new Error("The number of pictures is zero !");
if (imgBlobList.length === 0) throw new Error("The number of pictures is zero !");
if (imgBlobList.length > 0) {
for (var i = 0; i < imgBlobList.length; i++) {
img == null ? void 0 : img.file(
@ -215,17 +212,12 @@ var downloadPackageImages = async (imgDataList, zipName) => {
}
};
function getFileSize(size) {
if (!size)
return "";
if (!size) return "";
var num = 1024;
if (size < num)
return size + "B";
if (size < Math.pow(num, 2))
return (size / num).toFixed(2) + "K";
if (size < Math.pow(num, 3))
return (size / Math.pow(num, 2)).toFixed(2) + "M";
if (size < Math.pow(num, 4))
return (size / Math.pow(num, 3)).toFixed(2) + "G";
if (size < num) return size + "B";
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + "K";
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + "M";
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + "G";
return (size / Math.pow(num, 4)).toFixed(2) + "T";
}
var dataURLToBlob = (dataurl) => {
@ -242,8 +234,7 @@ var dataURLToBlob = (dataurl) => {
var generateImg = (_imgKey, host = "http://10.0.0.120") => {
let imgKey = _imgKey;
let imgUrl = "";
if (!imgKey)
return "";
if (!imgKey) return "";
if (/(http|https):\/\/([\w.]+\/?)\S*/ig.test(imgKey)) {
return imgKey;
}

View File

@ -43,8 +43,7 @@ var setNumberAccuracy = (originNumber, accuracy = 0, isCeil = true) => {
var toRealNumber = (number) => {
if (isNaN(number) || number === Infinity) {
return 0;
} else
return number;
} else return number;
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {

View File

@ -23,8 +23,7 @@ __export(performance_exports, {
});
module.exports = __toCommonJS(performance_exports);
var speedConvert = (bps, contertUnit = 8) => {
if (bps === void 0)
return `0KB/s`;
if (bps === void 0) return `0KB/s`;
const byte = bps / contertUnit;
if (bps > 1024 * 1024 * 1024) {
return `${(byte / 1024 / 1024 / 1024).toFixed(2)}GB/s`;

View File

@ -31,10 +31,8 @@ var getStrLength = function(str) {
var realLength = 0, len = str.length, charCode = -1;
for (var i = 0; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode >= 0 && charCode <= 128)
realLength += 1;
else
realLength += 2;
if (charCode >= 0 && charCode <= 128) realLength += 1;
else realLength += 2;
}
return realLength;
};

View File

@ -84,8 +84,7 @@ function getChromeVersion() {
const arr = navigator.userAgent.split(" ");
let chromeVersion = "";
for (let i = 0; i < arr.length; i++) {
if (/chrome/i.test(arr[i]))
chromeVersion = arr[i];
if (/chrome/i.test(arr[i])) chromeVersion = arr[i];
}
if (chromeVersion) {
return Number(chromeVersion.split("/")[1].split(".")[0]);

View File

@ -51,6 +51,7 @@
"react-map-gl": "^7.1.7"
},
"devDependencies": {
"@types/mapbox__mapbox-gl-draw": "^1.4.6"
"@types/mapbox__mapbox-gl-draw": "^1.4.6",
"axios": "^1.7.2"
}
}

View File

@ -2,7 +2,7 @@ import type { CSSObject } from '@ant-design/cssinjs';
import { unit } from '@ant-design/cssinjs';
import { TinyColor } from '@ctrl/tinycolor';
import type { SharedComponentToken, SharedInputToken } from '../../input/style';
import type { SharedComponentToken, SharedInputToken } from '../../input/style/token';
import {
genActiveStyle,
genBasicInputStyle,

View File

@ -32,9 +32,12 @@ export interface ComponentToken extends ArrowToken, ArrowOffsetToken {
paddingBlock: CSSProperties['paddingBlock'];
}
// @ts-ignore
export interface DropdownToken extends FullToken<'Dropdown'> {
dropdownArrowDistance: number | string;
zIndexPopup?: number;
dropdownEdgeChildPadding: number;
paddingBlock?: number | string;
menuCls: string;
}
@ -158,6 +161,7 @@ const genBaseStyle: GenerateStyle<DropdownToken> = (token) => {
// =============================================================
// == Arrow style ==
// =============================================================
// @ts-ignore
getArrowStyle<DropdownToken>(token, colorBgElevated, {
arrowPlacement: { top: true, bottom: true },
}),
@ -343,6 +347,7 @@ const genBaseStyle: GenerateStyle<DropdownToken> = (token) => {
};
// ============================== Export ==============================
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Dropdown'> = (token) => ({
zIndexPopup: token.zIndexPopupBase + 50,
paddingBlock: (token.controlHeight - token.fontSize * token.lineHeight) / 2,
@ -354,6 +359,7 @@ export const prepareComponentToken: GetDefaultToken<'Dropdown'> = (token) => ({
});
export default genStyleHooks(
// @ts-ignore
'Dropdown',
(token) => {
const { marginXXS, sizePopupArrow, paddingXXS, componentCls } = token;

View File

@ -31,6 +31,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
...floatButtonProps
} = props;
// @ts-ignore
const { direction, getPrefixCls, floatButtonGroup } =
useContext<ConfigConsumerProps>(ConfigContext);

View File

@ -22,6 +22,7 @@ export interface ComponentToken {
dotOffsetInSquare: number;
}
// @ts-ignore
type FloatButtonToken = FullToken<'FloatButton'> & {
floatButtonColor: string;
floatButtonBackgroundColor: string;
@ -218,8 +219,11 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
floatButtonIconSize,
floatButtonSize,
borderRadiusLG,
// @ts-ignore
badgeOffset,
// @ts-ignore
dotOffsetInSquare,
// @ts-ignore
dotOffsetInCircle,
calc,
} = token;
@ -366,12 +370,14 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
};
// ============================== Export ==============================
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'FloatButton'> = (token) => ({
dotOffsetInCircle: getOffset(token.controlHeightLG / 2),
dotOffsetInSquare: getOffset(token.borderRadiusLG),
});
export default genStyleHooks(
// @ts-ignore
'FloatButton',
(token) => {
const {

View File

@ -2,7 +2,6 @@ import React from 'react';
import { AlertFilled, CloseSquareFilled } from '@ant-design/icons';
import { Button, Form, Input, Tooltip } from 'antd';
import { createStyles, css } from 'antd-style';
import uniqueId from 'lodash/uniqueId';
const useStyle = createStyles(() => ({
'custom-feedback-icons': css`
@ -25,7 +24,6 @@ const App: React.FC = () => {
error: (
<Tooltip
key="tooltipKey"
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
color="red"
>
<CloseSquareFilled />
@ -54,7 +52,6 @@ const App: React.FC = () => {
error: (
<Tooltip
key="tooltipKey"
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
color="pink"
>
<AlertFilled />

View File

@ -68,8 +68,10 @@ const Image: CompositionImage<ImageProps> = (props) => {
transitionName: getTransitionName(rootPrefixCls, 'zoom', _preview.transitionName),
maskTransitionName: getTransitionName(rootPrefixCls, 'fade', _preview.maskTransitionName),
zIndex,
// @ts-ignore
closeIcon: closeIcon ?? image?.preview?.closeIcon,
};
// @ts-ignore
}, [preview, imageLocale, image?.preview?.closeIcon]);
const mergedStyle: React.CSSProperties = { ...image?.style, ...style };

View File

@ -36,6 +36,7 @@ export interface ComponentToken {
previewOperationColorDisabled: string;
}
// @ts-ignore
export interface ImageToken extends FullToken<'Image'> {
previewCls: string;
modalMaskBg: string;
@ -85,7 +86,9 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
marginXL,
margin,
paddingLG,
// @ts-ignore
previewOperationColorDisabled,
// @ts-ignore
previewOperationHoverColor,
motionDurationSlow,
iconCls,
@ -107,6 +110,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
// @ts-ignore
color: token.previewOperationColor,
},
[`${previewCls}-progress`]: {
@ -134,6 +138,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
},
[`& > ${iconCls}`]: {
// @ts-ignore
fontSize: token.previewOperationSize,
},
},
@ -165,6 +170,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
},
[`& > ${iconCls}`]: {
// @ts-ignore
fontSize: token.previewOperationSize,
},
},
@ -176,8 +182,10 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
const {
modalMaskBg,
iconCls,
// @ts-ignore
previewOperationColorDisabled,
previewCls,
// @ts-ignore
zIndexPopup,
motionDurationSlow,
} = token;
@ -196,6 +204,7 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
width: token.imagePreviewSwitchSize,
height: token.imagePreviewSwitchSize,
marginTop: token.calc(token.imagePreviewSwitchSize).mul(-1).div(2).equal(),
// @ts-ignore
color: token.previewOperationColor,
background: operationBg.toRgbString(),
borderRadius: '50%',
@ -219,6 +228,7 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
},
},
[`> ${iconCls}`]: {
// @ts-ignore
fontSize: token.previewOperationSize,
},
},
@ -299,6 +309,7 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
{
[`${componentCls}-preview-root`]: {
[`${previewCls}-wrap`]: {
// @ts-ignore
zIndex: token.zIndexPopup,
},
},
@ -308,6 +319,7 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
{
[`${componentCls}-preview-operations-wrapper`]: {
position: 'fixed',
// @ts-ignore
zIndex: token.calc(token.zIndexPopup).add(1).equal({ unit: false }),
},
'&': [genPreviewOperationsStyle(token), genPreviewSwitchStyle(token)],
@ -358,6 +370,7 @@ const genPreviewMotion: GenerateStyle<ImageToken> = (token) => {
};
// ============================== Export ==============================
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Image'> = (token) => ({
zIndexPopup: token.zIndexPopupBase + 80,
previewOperationColor: new TinyColor(token.colorTextLightSolid).setAlpha(0.65).toRgbString(),
@ -369,6 +382,7 @@ export const prepareComponentToken: GetDefaultToken<'Image'> = (token) => ({
});
export default genStyleHooks(
// @ts-ignore
'Image',
(token) => {
const previewCls = `${token.componentCls}-preview`;

View File

@ -156,6 +156,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
}
classNames={{
input: inputNumberClass,
// @ts-ignore
variant: classNames(
{
[`${prefixCls}-${variant}`]: enableVariantCls,

View File

@ -60,6 +60,7 @@ export interface ComponentToken extends SharedComponentToken {
export type InputNumberToken = FullToken<'InputNumber'> & SharedInputToken;
export const prepareComponentToken: GetDefaultToken<'InputNumber'> = (token) => {
// @ts-ignore
const handleVisible = token.handleVisible ?? 'auto';
return {

View File

@ -180,6 +180,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
</NoCompactStyle>
);
// @ts-ignore
const mergedAllowClear = getAllowClear(allowClear ?? input?.allowClear);
const [variant, enableVariantCls] = useVariant(customVariant, bordered);
@ -221,6 +222,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
input?.classNames?.input,
hashId,
),
// @ts-ignore
variant: classNames(
{
[`${prefixCls}-${variant}`]: enableVariantCls,

View File

@ -62,6 +62,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
deprecated(!('bordered' in props), 'bordered', 'variant');
}
// @ts-ignore
const { getPrefixCls, direction, textArea } = React.useContext(ConfigContext);
// ===================== Size =====================

View File

@ -1,6 +1,5 @@
import React from 'react';
import { Flex, Input, Typography } from 'antd';
import { runes } from 'runes2';
const App: React.FC = () => (
<Flex vertical gap={16}>
@ -20,7 +19,6 @@ const App: React.FC = () => (
<Input
count={{
show: true,
strategy: (txt) => runes(txt).length,
}}
defaultValue="🔥🔥🔥"
/>
@ -32,7 +30,6 @@ const App: React.FC = () => (
count={{
show: true,
max: 6,
strategy: (txt) => runes(txt).length,
exceedFormatter: (txt, { max }) => runes(txt).slice(0, max).join(''),
}}
defaultValue="🔥 antd"

View File

@ -865,6 +865,7 @@ export default genStyleHooks(
},
initComponentToken,
{
// @ts-ignore
resetFont: false,
},
);

View File

@ -96,6 +96,7 @@ export interface ComponentToken {
lightTriggerColor: string;
}
// @ts-ignore
export interface LayoutToken extends FullToken<'Layout'> {}
const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
@ -103,22 +104,35 @@ const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
antCls, // .ant
componentCls, // .ant-layout
colorText,
// @ts-ignore
triggerColor,
// @ts-ignore
footerBg,
// @ts-ignore
triggerBg,
// @ts-ignore
headerHeight,
// @ts-ignore
headerPadding,
// @ts-ignore
headerColor,
// @ts-ignore
footerPadding,
// @ts-ignore
triggerHeight,
// @ts-ignore
zeroTriggerHeight,
// @ts-ignore
zeroTriggerWidth,
motionDurationMid,
motionDurationSlow,
fontSize,
borderRadius,
// @ts-ignore
bodyBg,
// @ts-ignore
headerBg,
// @ts-ignore
siderBg,
} = token;
@ -280,6 +294,7 @@ const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
};
};
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Layout'> = (token) => {
const {
colorBgLayout,
@ -320,6 +335,7 @@ export const prepareComponentToken: GetDefaultToken<'Layout'> = (token) => {
};
// ============================== Export ==============================
// @ts-ignore
export default genStyleHooks('Layout', (token) => [genLayoutStyle(token)], prepareComponentToken, {
deprecatedTokens: [
['colorBgBody', 'bodyBg'],

View File

@ -4,6 +4,7 @@ import type { LayoutToken } from '.';
import type { GenerateStyle } from '../../theme/internal';
const genLayoutLightStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
// @ts-ignore
const { componentCls, bodyBg, lightSiderBg, lightTriggerBg, lightTriggerColor } = token;
return {

View File

@ -27,6 +27,7 @@ function isEmptyIcon(icon?: React.ReactNode) {
return icon === null || icon === false;
}
// @ts-ignore
const MENU_COMPONENTS: GetProp<RcMenuProps, '_internalComponents'> = {
item: MenuItem,
submenu: SubMenu,
@ -141,16 +142,21 @@ const InternalMenu = forwardRef<RcMenuRef, InternalMenuProps>((props, ref) => {
if (typeof overrideObj.expandIcon === 'function' || isEmptyIcon(overrideObj.expandIcon)) {
return overrideObj.expandIcon || null;
}
// @ts-ignore
if (typeof menu?.expandIcon === 'function' || isEmptyIcon(menu?.expandIcon)) {
// @ts-ignore
return menu?.expandIcon || null;
}
// @ts-ignore
const mergedIcon = expandIcon ?? overrideObj?.expandIcon ?? menu?.expandIcon;
return cloneElement(mergedIcon, {
className: classNames(
`${prefixCls}-submenu-expand-icon`,
// @ts-ignore
React.isValidElement(mergedIcon) ? mergedIcon.props?.className : undefined,
),
});
// @ts-ignore
}, [expandIcon, overrideObj?.expandIcon, menu?.expandIcon, prefixCls]);
// ======================== Context ==========================
@ -198,6 +204,7 @@ const InternalMenu = forwardRef<RcMenuRef, InternalMenuProps>((props, ref) => {
cssVarCls,
rootCls,
)}
// @ts-ignore
_internalComponents={MENU_COMPONENTS}
/>
</MenuContext.Provider>

View File

@ -7,10 +7,12 @@ const getHorizontalStyle: GenerateStyle<MenuToken> = (token) => {
const {
componentCls,
motionDurationSlow,
// @ts-ignore
horizontalLineHeight,
colorSplit,
lineWidth,
lineType,
// @ts-ignore
itemPaddingInline,
} = token;

View File

@ -369,6 +369,7 @@ export interface ComponentToken {
itemWidth: string;
}
// @ts-ignore
export interface MenuToken extends FullToken<'Menu'> {
menuHorizontalHeight: number | string;
menuArrowSize: number | string;
@ -385,7 +386,9 @@ const genMenuItemStyle = (token: MenuToken): CSSObject => {
motionEaseInOut,
motionEaseOut,
iconCls,
// @ts-ignore
iconSize,
// @ts-ignore
iconMarginInlineEnd,
} = token;
@ -521,13 +524,17 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
padding,
colorSplit,
lineWidth,
// @ts-ignore
zIndexPopup,
borderRadiusLG,
// @ts-ignore
subMenuItemBorderRadius,
menuArrowSize,
menuArrowOffset,
lineType,
// @ts-ignore
groupTitleLineHeight,
// @ts-ignore
groupTitleFontSize,
} = token;
@ -577,6 +584,7 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
},
},
[`${componentCls}-item, ${componentCls}-submenu, ${componentCls}-submenu-title`]: {
// @ts-ignore
borderRadius: token.itemBorderRadius,
},
@ -804,6 +812,7 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
];
};
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Menu'> = (token) => {
const {
colorPrimary,
@ -936,27 +945,44 @@ export const prepareComponentToken: GetDefaultToken<'Menu'> = (token) => {
// ============================== Export ==============================
export default (prefixCls: string, rootCls: string = prefixCls, injectStyle: boolean = true) => {
const useStyle = genStyleHooks(
// @ts-ignore
'Menu',
(token) => {
const {
colorBgElevated,
controlHeightLG,
fontSize,
// @ts-ignore
darkItemColor,
// @ts-ignore
darkDangerItemColor,
// @ts-ignore
darkItemBg,
// @ts-ignore
darkSubMenuItemBg,
// @ts-ignore
darkItemSelectedColor,
// @ts-ignore
darkItemSelectedBg,
// @ts-ignore
darkDangerItemSelectedBg,
// @ts-ignore
darkItemHoverBg,
// @ts-ignore
darkGroupTitleColor,
// @ts-ignore
darkItemHoverColor,
// @ts-ignore
darkItemDisabledColor,
// @ts-ignore
darkDangerItemHoverColor,
// @ts-ignore
darkDangerItemSelectedColor,
// @ts-ignore
darkDangerItemActiveBg,
// @ts-ignore
popupBg,
// @ts-ignore
darkPopupBg,
} = token;
@ -969,10 +995,12 @@ export default (prefixCls: string, rootCls: string = prefixCls, injectStyle: boo
menuArrowOffset: token.calc(menuArrowSize).mul(0.25).equal(),
menuSubMenuBg: colorBgElevated,
calc: token.calc,
// @ts-ignore
popupBg,
});
const menuDarkToken = mergeToken<MenuToken>(menuToken, {
// @ts-ignore
itemColor: darkItemColor,
itemHoverColor: darkItemHoverColor,
groupTitleColor: darkGroupTitleColor,

View File

@ -11,43 +11,67 @@ const accessibilityFocus = (token: MenuToken) => ({
const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation => {
const {
componentCls,
// @ts-ignore
itemColor,
// @ts-ignore
itemSelectedColor,
// @ts-ignore
groupTitleColor,
// @ts-ignore
itemBg,
// @ts-ignore
subMenuItemBg,
// @ts-ignore
itemSelectedBg,
// @ts-ignore
activeBarHeight,
// @ts-ignore
activeBarWidth,
// @ts-ignore
activeBarBorderWidth,
motionDurationSlow,
motionEaseInOut,
motionEaseOut,
// @ts-ignore
itemPaddingInline,
motionDurationMid,
// @ts-ignore
itemHoverColor,
lineType,
colorSplit,
// Disabled
// @ts-ignore
itemDisabledColor,
// Danger
// @ts-ignore
dangerItemColor,
// @ts-ignore
dangerItemHoverColor,
// @ts-ignore
dangerItemSelectedColor,
// @ts-ignore
dangerItemActiveBg,
// @ts-ignore
dangerItemSelectedBg,
// Bg
// @ts-ignore
popupBg,
// @ts-ignore
itemHoverBg,
// @ts-ignore
itemActiveBg,
menuSubMenuBg,
// Horizontal
// @ts-ignore
horizontalItemSelectedColor,
// @ts-ignore
horizontalItemSelectedBg,
// @ts-ignore
horizontalItemBorderRadius,
// @ts-ignore
horizontalItemHoverBg,
} = token;

View File

@ -8,12 +8,16 @@ import type { GenerateStyle } from '../../theme/internal';
const getVerticalInlineStyle: GenerateStyle<MenuToken, CSSObject> = (token) => {
const {
componentCls,
// @ts-ignore
itemHeight,
// @ts-ignore
itemMarginInline,
padding,
menuArrowSize,
marginXS,
// @ts-ignore
itemMarginBlock,
// @ts-ignore
itemWidth,
} = token;
@ -53,19 +57,24 @@ const getVerticalStyle: GenerateStyle<MenuToken> = (token) => {
const {
componentCls,
iconCls,
// @ts-ignore
itemHeight,
colorTextLightSolid,
// @ts-ignore
dropdownWidth,
controlHeightLG,
motionDurationMid,
motionEaseOut,
paddingXL,
// @ts-ignore
itemMarginInline,
fontSizeLG,
motionDurationSlow,
paddingXS,
boxShadowSecondary,
// @ts-ignore
collapsedWidth,
// @ts-ignore
collapsedIconSize,
} = token;

View File

@ -108,7 +108,9 @@ const Modal: React.FC<ModalProps> = (props) => {
const [mergedClosable, mergedCloseIcon] = useClosable(
closable,
closeIcon,
// @ts-ignore
(icon) => renderCloseIcon(prefixCls, icon),
// @ts-ignore
<CloseOutlined className={`${prefixCls}-close-icon`} />,
true,
);

View File

@ -0,0 +1,152 @@
import * as React from 'react';
import classNames from 'classnames';
import useMergedState from 'rc-util/lib/hooks/useMergedState';
import type { KeyWiseTransferItem } from '.';
import Pagination from '../pagination';
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];
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: TransferKey[];
}
const parsePagination = (pagination?: ExistPagination) => {
const defaultPagination: PaginationType = {
simple: true,
showSizeChanger: false,
showLessItems: false,
};
return { ...defaultPagination, ...pagination };
};
export interface ListBodyRef<RecordType extends KeyWiseTransferItem> {
items?: RenderedItem<RecordType>[];
}
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;
const [current, setCurrent] = React.useState<number>(1);
const mergedPagination = React.useMemo(() => {
if (!pagination) {
return null;
}
const convertPagination = typeof pagination === 'object' ? pagination : {};
return parsePagination(convertPagination);
}, [pagination]);
const [pageSize, setPageSize] = useMergedState<number>(10, {
value: mergedPagination?.pageSize,
});
React.useEffect(() => {
if (mergedPagination) {
const maxPageCount = Math.ceil(filteredRenderItems.length / pageSize!);
setCurrent(Math.min(current, maxPageCount));
}
}, [filteredRenderItems, mergedPagination, pageSize]);
const onInternalClick = (item: RecordType, e: React.MouseEvent<Element, MouseEvent>) => {
onItemSelect(item.key, !selectedKeys.includes(item.key), e);
};
const onRemove = (item: RecordType) => {
onItemRemove?.([item.key]);
};
const onPageChange = (cur: number) => {
setCurrent(cur);
};
const onSizeChange = (cur: number, size: number) => {
setCurrent(cur);
setPageSize(size);
};
const memoizedItems = React.useMemo<RenderedItem<RecordType>[]>(() => {
const displayItems = mergedPagination
? filteredRenderItems.slice((current - 1) * pageSize!, current * pageSize!)
: filteredRenderItems;
return displayItems;
}, [current, filteredRenderItems, mergedPagination, pageSize]);
React.useImperativeHandle(ref, () => ({ items: memoizedItems }));
const paginationNode: React.ReactNode = mergedPagination ? (
<Pagination
size="small"
disabled={globalDisabled}
simple={mergedPagination.simple}
pageSize={pageSize}
showLessItems={mergedPagination.showLessItems}
showSizeChanger={mergedPagination.showSizeChanger}
className={`${prefixCls}-pagination`}
total={filteredRenderItems.length}
current={current}
onChange={onPageChange}
onShowSizeChange={onSizeChange}
/>
) : null;
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={onInternalClick as any}
onRemove={onRemove as any}
checked={selectedKeys.includes(item.key)}
disabled={globalDisabled || item.disabled}
/>
))}
</ul>
{paginationNode}
</>
);
};
if (process.env.NODE_ENV !== 'production') {
TransferListBody.displayName = 'TransferListBody';
}
export default React.forwardRef<
ListBodyRef<KeyWiseTransferItem>,
TransferListBodyProps<KeyWiseTransferItem>
>(TransferListBody);

View File

@ -0,0 +1,85 @@
import * as React from 'react';
import DeleteOutlined from '@ant-design/icons/DeleteOutlined';
import classNames from 'classnames';
import type { KeyWiseTransferItem } from '.';
import TransButton from '../_util/transButton';
import Checkbox from '../checkbox';
import { useLocale } from '../locale';
import defaultLocale from '../locale/en_US';
type ListItemProps<RecordType> = {
renderedText?: string | number;
renderedEl: React.ReactNode;
disabled?: boolean;
checked?: boolean;
prefixCls: string;
onClick: (item: RecordType, e: React.MouseEvent<HTMLLIElement, MouseEvent>) => void;
onRemove?: (item: RecordType) => void;
item: RecordType;
showRemove?: boolean;
};
const ListItem = <RecordType extends KeyWiseTransferItem>(props: ListItemProps<RecordType>) => {
const {
renderedText,
renderedEl,
item,
checked,
disabled,
prefixCls,
onClick,
onRemove,
showRemove,
} = props;
const className = classNames(`${prefixCls}-content-item`, {
[`${prefixCls}-content-item-disabled`]: disabled || item.disabled,
[`${prefixCls}-content-item-checked`]: checked,
});
let title: string | undefined;
if (typeof renderedText === 'string' || typeof renderedText === 'number') {
title = String(renderedText);
}
const [contextLocale] = useLocale('Transfer', defaultLocale.Transfer);
const liProps: React.HTMLAttributes<HTMLLIElement> = { className, title };
const labelNode = <span className={`${prefixCls}-content-item-text`}>{renderedEl}</span>;
if (showRemove) {
return (
<li {...liProps}>
{labelNode}
<TransButton
disabled={disabled || item.disabled}
className={`${prefixCls}-content-item-remove`}
aria-label={contextLocale?.remove}
onClick={() => {
onRemove?.(item);
}}
>
<DeleteOutlined />
</TransButton>
</li>
);
}
// Default click to select
liProps.onClick = disabled || item.disabled ? undefined : (event) => onClick(item, event);
return (
<li {...liProps}>
<Checkbox
className={`${prefixCls}-checkbox`}
checked={checked}
disabled={disabled || item.disabled}
/>
{labelNode}
</li>
);
};
export default React.memo(ListItem);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,131 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transfer.List should render correctly 1`] = `
<div
class="ant-transfer-list"
>
<div
class="ant-transfer-list-header"
>
<label
class="ant-checkbox-wrapper ant-transfer-list-checkbox"
>
<span
class="ant-checkbox ant-checkbox-indeterminate ant-wave-target"
>
<input
aria-checked="mixed"
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
aria-label="down"
class="anticon anticon-down ant-dropdown-trigger ant-transfer-list-header-dropdown"
role="img"
>
<svg
aria-hidden="true"
data-icon="down"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"
/>
</svg>
</span>
<span
class="ant-transfer-list-header-selected"
>
1/3
</span>
<span
class="ant-transfer-list-header-title"
/>
</div>
<div
class="ant-transfer-list-body"
>
<ul
class="ant-transfer-list-content"
>
<li
class="ant-transfer-list-content-item ant-transfer-list-content-item-checked"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-checked ant-transfer-list-checkbox"
>
<span
class="ant-checkbox ant-wave-target ant-checkbox-checked"
>
<input
checked=""
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
/>
</li>
<li
class="ant-transfer-list-content-item"
>
<label
class="ant-checkbox-wrapper ant-transfer-list-checkbox"
>
<span
class="ant-checkbox ant-wave-target"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
/>
</li>
<li
class="ant-transfer-list-content-item ant-transfer-list-content-item-disabled"
>
<label
class="ant-checkbox-wrapper ant-checkbox-wrapper-disabled ant-transfer-list-checkbox"
>
<span
class="ant-checkbox ant-wave-target ant-checkbox-disabled"
>
<input
class="ant-checkbox-input"
disabled=""
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
/>
</li>
</ul>
</div>
</div>
`;

View File

@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Transfer.Search should show cross icon when input value exists 1`] = `
<span
class="ant-input-affix-wrapper ant-input-outlined"
>
<span
class="ant-input-prefix"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder=""
type="text"
value=""
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-clear-icon ant-input-clear-icon-hidden"
role="button"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
/>
</svg>
</span>
</span>
</span>
</span>
`;
exports[`Transfer.Search should show cross icon when input value exists 2`] = `
<span
class="ant-input-affix-wrapper ant-input-outlined"
>
<span
class="ant-input-prefix"
>
<span
aria-label="search"
class="anticon anticon-search"
role="img"
>
<svg
aria-hidden="true"
data-icon="search"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M909.6 854.5L649.9 594.8C690.2 542.7 712 479 712 412c0-80.2-31.3-155.4-87.9-212.1-56.6-56.7-132-87.9-212.1-87.9s-155.5 31.3-212.1 87.9C143.2 256.5 112 331.8 112 412c0 80.1 31.3 155.5 87.9 212.1C256.5 680.8 331.8 712 412 712c67 0 130.6-21.8 182.7-62l259.7 259.6a8.2 8.2 0 0011.6 0l43.6-43.5a8.2 8.2 0 000-11.6zM570.4 570.4C528 612.7 471.8 636 412 636s-116-23.3-158.4-65.6C211.3 528 188 471.8 188 412s23.3-116.1 65.6-158.4C296 211.3 352.2 188 412 188s116.1 23.2 158.4 65.6S636 352.2 636 412s-23.3 116.1-65.6 158.4z"
/>
</svg>
</span>
</span>
<input
class="ant-input"
placeholder=""
type="text"
value="a"
/>
<span
class="ant-input-suffix"
>
<span
class="ant-input-clear-icon"
role="button"
tabindex="-1"
>
<span
aria-label="close-circle"
class="anticon anticon-close-circle"
role="img"
>
<svg
aria-hidden="true"
data-icon="close-circle"
fill="currentColor"
fill-rule="evenodd"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M512 64c247.4 0 448 200.6 448 448S759.4 960 512 960 64 759.4 64 512 264.6 64 512 64zm127.98 274.82h-.04l-.08.06L512 466.75 384.14 338.88c-.04-.05-.06-.06-.08-.06a.12.12 0 00-.07 0c-.03 0-.05.01-.09.05l-45.02 45.02a.2.2 0 00-.05.09.12.12 0 000 .07v.02a.27.27 0 00.06.06L466.75 512 338.88 639.86c-.05.04-.06.06-.06.08a.12.12 0 000 .07c0 .03.01.05.05.09l45.02 45.02a.2.2 0 00.09.05.12.12 0 00.07 0c.02 0 .04-.01.08-.05L512 557.25l127.86 127.87c.04.04.06.05.08.05a.12.12 0 00.07 0c.03 0 .05-.01.09-.05l45.02-45.02a.2.2 0 00.05-.09.12.12 0 000-.07v-.02a.27.27 0 00-.05-.06L557.25 512l127.87-127.86c.04-.04.05-.06.05-.08a.12.12 0 000-.07c0-.03-.01-.05-.05-.09l-45.02-45.02a.2.2 0 00-.09-.05.12.12 0 00-.07 0z"
/>
</svg>
</span>
</span>
</span>
</span>
`;

View File

@ -0,0 +1,64 @@
import React from 'react';
import { render } from '../../../tests/utils';
import type { TransferProps } from '../index';
import Transfer from '../index';
describe('Transfer.Customize', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
it('props#body does not work anymore', () => {
const body = jest.fn();
const props = { body } as TransferProps<any>;
render(<Transfer {...props} />);
expect(errorSpy).not.toHaveBeenCalled();
expect(body).not.toHaveBeenCalled();
});
describe('deprecated function', () => {
const dataSource: Record<'key', string>[] = [];
for (let i = 0; i < 10; i += 1) {
dataSource.push({ key: i.toString() });
}
const commonProps = {
dataSource,
selectedKeys: ['1'],
targetKeys: ['2'],
};
it('should not exist in render props', () => {
render(
<Transfer {...commonProps}>
{(props) => {
expect('handleFilter' in props).toBeFalsy();
expect('handleSelect' in props).toBeFalsy();
expect('handleSelectAll' in props).toBeFalsy();
expect('handleClear' in props).toBeFalsy();
expect('body' in props).toBeFalsy();
expect('checkedKeys' in props).toBeFalsy();
return null;
}}
</Transfer>,
);
});
});
it('warning if use `pagination`', () => {
render(
<Transfer dataSource={[]} pagination>
{() => null}
</Transfer>,
);
expect(errorSpy).toHaveBeenCalledWith(
'Warning: [antd: Transfer] `pagination` not support customize render list.',
);
});
});

View File

@ -0,0 +1,3 @@
import { extendTest } from '../../../tests/shared/demoTest';
extendTest('transfer');

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('transfer');

View File

@ -0,0 +1,145 @@
/* eslint no-use-before-define: "off" */
import React from 'react';
import Transfer from '..';
import { act, fireEvent, render } from '../../../tests/utils';
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(container: HTMLElement, index: number) {
const items = Array.from(
container
// Menu
.querySelector('.ant-dropdown-menu')!
// Items
.querySelectorAll('li.ant-dropdown-menu-item'),
);
fireEvent.click(items[index]);
}
it('select all', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const { container } = render(<Transfer {...listProps} onSelectChange={onSelectChange} />);
fireEvent.mouseEnter(container.querySelector('.ant-transfer-list-header-dropdown')!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 0);
expect(onSelectChange).toHaveBeenCalledWith(['b', 'c', 'd', 'e'], []);
jest.useRealTimers();
});
it('select current page', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const { container } = render(<Transfer {...listProps} onSelectChange={onSelectChange} />);
fireEvent.mouseEnter(container.querySelector('.ant-transfer-list-header-dropdown')!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 1);
expect(onSelectChange).toHaveBeenCalledWith(['b', 'c', 'd'], []);
jest.useRealTimers();
});
it('should hide checkbox and dropdown icon when showSelectAll={false}', () => {
const { container } = render(<Transfer {...listProps} showSelectAll={false} />);
expect(container.querySelector('.ant-transfer-list-header-dropdown')).toBeFalsy();
expect(
container.querySelector('.ant-transfer-list-header .ant-transfer-list-checkbox'),
).toBeFalsy();
});
describe('select invert', () => {
it('with pagination', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const { container } = render(
<Transfer {...listProps} selectedKeys={undefined} onSelectChange={onSelectChange} />,
);
fireEvent.mouseEnter(container.querySelector('.ant-transfer-list-header-dropdown')!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 0);
expect(onSelectChange).toHaveBeenCalledWith(['b', 'c', 'd', 'e'], []);
fireEvent.mouseEnter(container.querySelector('.ant-transfer-list-header-dropdown')!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 2);
expect(onSelectChange).toHaveBeenCalledWith(['e'], []);
jest.useRealTimers();
});
it('without pagination', () => {
jest.useFakeTimers();
const onSelectChange = jest.fn();
const { container } = render(
<Transfer {...listProps} pagination={null as any} onSelectChange={onSelectChange} />,
);
fireEvent.mouseEnter(container.querySelector('.ant-transfer-list-header-dropdown')!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 1);
expect(onSelectChange).toHaveBeenCalledWith(['c', 'd', 'e'], []);
jest.useRealTimers();
});
});
describe('oneWay to remove', () => {
[
{ name: 'with pagination', props: listProps },
{ name: 'without pagination', props: { ...listProps, pagination: null as any } },
].forEach(({ name, props }) => {
it(name, () => {
jest.useFakeTimers();
const onChange = jest.fn();
const { container } = render(
<Transfer {...props} targetKeys={['b', 'c']} oneWay onChange={onChange} />,
);
// Right dropdown
fireEvent.mouseEnter(container.querySelectorAll('.ant-transfer-list-header-dropdown')[1]!);
act(() => {
jest.runAllTimers();
});
clickItem(container, 0);
expect(onChange).toHaveBeenCalledWith([], 'left', ['b', 'c']);
jest.useRealTimers();
});
});
});
});

View File

@ -0,0 +1,5 @@
import { imageDemoTest } from '../../../tests/shared/imageTest';
describe('Transfer image', () => {
imageDemoTest('transfer');
});

View File

@ -0,0 +1,850 @@
import React, { useCallback, useEffect, useState } from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import type { DefaultRecordType } from 'rc-table/lib/interface';
import type { SelectAllLabel, TransferProps } from '..';
import Transfer from '..';
import mountTest from '../../../tests/shared/mountTest';
import rtlTest from '../../../tests/shared/rtlTest';
import Button from '../../button';
const listCommonProps: {
dataSource: { key: string; title: string; disabled?: boolean }[];
selectedKeys?: string[];
targetKeys?: string[];
} = {
dataSource: [
{ key: 'a', title: 'a' },
{ key: 'b', title: 'b' },
{ key: 'c', title: 'c', disabled: true },
],
selectedKeys: ['a'],
targetKeys: ['b'],
};
const listDisabledProps = {
dataSource: [
{ key: 'a', title: 'a', disabled: true },
{ key: 'b', title: 'b' },
],
selectedKeys: ['a', 'b'],
targetKeys: [],
};
const searchTransferProps = {
dataSource: [
{
key: '0',
title: 'content1',
description: 'description of content1',
chosen: false,
},
{
key: '1',
title: 'content2',
description: 'description of content2',
chosen: false,
},
{
key: '2',
title: 'content3',
description: 'description of content3',
chosen: false,
},
{
key: '3',
title: 'content4',
description: 'description of content4',
chosen: false,
},
{
key: '4',
title: 'content5',
description: 'description of content5',
chosen: false,
},
{
key: '5',
title: 'content6',
description: 'description of content6',
chosen: false,
},
],
selectedKeys: [],
targetKeys: ['3', '4'],
};
const generateData = (n = 20) => {
const data = [];
for (let i = 0; i < n; i++) {
data.push({
key: `${i}`,
title: `content${i}`,
description: `description of content${i}`,
chosen: false,
});
}
return data;
};
describe('Transfer', () => {
mountTest(Transfer);
rtlTest(Transfer);
it('should render correctly', () => {
const wrapper = render(<Transfer {...listCommonProps} />);
expect(wrapper.container.firstChild).toMatchSnapshot();
});
it('should move selected keys to corresponding list', () => {
const handleChange = jest.fn();
const { container } = render(<Transfer {...listCommonProps} onChange={handleChange} />);
fireEvent.click(container.querySelector('.ant-transfer-operation')?.querySelector('button')!); // move selected keys to right list
expect(handleChange).toHaveBeenCalledWith(['a', 'b'], 'right', ['a']);
});
it('should move selected keys to left list', () => {
const handleChange = jest.fn();
const { container } = render(
<Transfer
{...listCommonProps}
selectedKeys={['a']}
targetKeys={['a']}
onChange={handleChange}
/>,
);
fireEvent.click(
container.querySelector('.ant-transfer-operation')?.querySelectorAll('button')?.[1]!,
); // 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 { container } = render(<Transfer {...listDisabledProps} onChange={handleChange} />);
fireEvent.click(container.querySelector('.ant-transfer-operation')?.querySelector('button')!); // move selected keys to right list
expect(handleChange).toHaveBeenCalledWith(['b'], 'right', ['b']);
});
it('should uncheck checkbox when click on checked item', () => {
const handleSelectChange = jest.fn();
const { getByTitle } = render(
<Transfer
{...listCommonProps}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
getByTitle('a').click();
expect(handleSelectChange).toHaveBeenLastCalledWith([], []);
});
it('should check checkbox when click on unchecked item', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
{...listCommonProps}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
getByText('b').click();
expect(handleSelectChange).toHaveBeenLastCalledWith(['a'], ['b']);
});
it('multiple select/deselect by hold down the shift key', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
dataSource={[
{ key: 'a', title: 'a' },
{ key: 'b', title: 'b' },
{ key: 'c', title: 'c' },
]}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
fireEvent.click(getByText('a'));
expect(handleSelectChange).toHaveBeenLastCalledWith(['a'], []);
fireEvent.click(getByText('c'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith(['a', 'b', 'c'], []);
fireEvent.click(getByText('b'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith(['a'], []);
});
it('multiple select targetKeys by hold down the shift key', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
dataSource={[
{ key: 'a', title: 'a' },
{ key: 'b', title: 'b' },
{ key: 'c', title: 'c' },
]}
targetKeys={['a', 'b', 'c']}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
fireEvent.click(getByText('a'));
expect(handleSelectChange).toHaveBeenLastCalledWith([], ['a']);
fireEvent.click(getByText('c'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith([], ['a', 'b', 'c']);
fireEvent.click(getByText('b'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith([], ['a']);
});
it('reset last select key after deselect', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
dataSource={[
{ key: 'a', title: 'a' },
{ key: 'b', title: 'b' },
{ key: 'c', title: 'c' },
{ key: 'd', title: 'd' },
]}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
fireEvent.click(getByText('a'));
expect(handleSelectChange).toHaveBeenLastCalledWith(['a'], []);
fireEvent.click(getByText('c'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith(['a', 'b', 'c'], []);
fireEvent.click(getByText('c'));
expect(handleSelectChange).toHaveBeenLastCalledWith(['a', 'b'], []);
fireEvent.click(getByText('d'), {
shiftKey: true,
});
expect(handleSelectChange).toHaveBeenLastCalledWith(['a', 'b', 'd'], []);
});
it('should not check checkbox when component disabled', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
{...listCommonProps}
disabled
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
getByText('a').click();
expect(handleSelectChange).not.toHaveBeenCalled();
});
it('should not check checkbox when click on disabled item', () => {
const handleSelectChange = jest.fn();
const { getByText } = render(
<Transfer
{...listCommonProps}
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
getByText('c').click();
expect(handleSelectChange).not.toHaveBeenCalled();
});
it('should check all item when click on check all', () => {
const handleSelectChange = jest.fn();
const { container } = render(
<Transfer {...listCommonProps} onSelectChange={handleSelectChange} />,
);
fireEvent.click(
container
?.querySelectorAll('.ant-transfer-list-header')
?.item(1)
?.querySelector('input[type="checkbox"]')!,
);
expect(handleSelectChange).toHaveBeenCalledWith(['a'], ['b']);
});
it('should uncheck all item when click on uncheck all', () => {
const handleSelectChange = jest.fn();
const { container } = render(
<Transfer {...listCommonProps} onSelectChange={handleSelectChange} />,
);
fireEvent.click(
container
?.querySelectorAll('.ant-transfer-list-header')
?.item(0)
?.querySelector('input[type="checkbox"]')!,
);
expect(handleSelectChange).toHaveBeenCalledWith([], []);
});
it('should call `filterOption` when use input in search box', () => {
const filterOption: TransferProps<any>['filterOption'] = (inputValue, option) =>
inputValue === option.title;
const { container } = render(
<Transfer
{...listCommonProps}
showSearch
filterOption={filterOption}
render={(item) => item.title}
/>,
);
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('input[type="text"]')!,
{ target: { value: 'a' } },
);
expect(
container
.querySelectorAll('.ant-transfer-list')
.item(0)
.querySelectorAll('.ant-transfer-list-content input[type="checkbox"]'),
).toHaveLength(1);
});
it('should display the correct count of items when filter by input', () => {
const filterOption: TransferProps<any>['filterOption'] = (inputValue, option) =>
option.description.includes(inputValue);
const renderFunc: TransferProps<any>['render'] = (item) => item.title;
const { container, getByText } = render(
<Transfer
{...searchTransferProps}
showSearch
filterOption={filterOption}
render={renderFunc}
/>,
);
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('input[type="text"]')!,
{ target: { value: 'content2' } },
);
expect(getByText('1 item')).toBeTruthy();
});
it('should display the correct locale', () => {
const emptyProps = { dataSource: [], selectedKeys: [], targetKeys: [] };
const locale = { itemUnit: 'Person', notFoundContent: 'Nothing', searchPlaceholder: 'Search' };
const { getAllByText, getAllByPlaceholderText } = render(
<Transfer {...listCommonProps} {...emptyProps} showSearch locale={locale} />,
);
expect(getAllByText('0 Person')).toHaveLength(2);
expect(getAllByPlaceholderText('Search')).toHaveLength(2);
expect(getAllByText('Nothing')).toHaveLength(2);
});
it('should display the correct locale and ignore old API', () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const emptyProps = { dataSource: [], selectedKeys: [], targetKeys: [] };
const locale = { notFoundContent: 'old1', searchPlaceholder: 'old2' };
const newLocalProp = { notFoundContent: 'new1', searchPlaceholder: 'new2' };
const { getAllByPlaceholderText, getAllByText } = render(
<Transfer
{...listCommonProps}
{...emptyProps}
{...locale}
locale={newLocalProp}
showSearch
/>,
);
expect(getAllByPlaceholderText('new2')).toHaveLength(2);
expect(getAllByText('new1')).toHaveLength(2);
expect(consoleErrorSpy).not.toHaveBeenCalledWith(
'Warning: [antd: Transfer] `notFoundContent` and `searchPlaceholder` will be removed, please use `locale` instead.',
);
consoleErrorSpy.mockRestore();
});
it('should display the correct items unit', () => {
const { getByText } = render(
<Transfer {...listCommonProps} locale={{ itemsUnit: 'People' }} />,
);
expect(getByText('1/2 People')).toBeTruthy();
});
it('should display the correct notFoundContent', () => {
const { getByText } = render(
<Transfer dataSource={[]} locale={{ notFoundContent: ['No Source', 'No Target'] }} />,
);
expect(getByText('No Source')).toBeTruthy();
expect(getByText('No Target')).toBeTruthy();
});
it('should just check the filtered item when click on check all after search by input', () => {
const filterOption: TransferProps<any>['filterOption'] = (inputValue, option) =>
option.description.includes(inputValue);
const renderFunc: TransferProps<any>['render'] = (item) => item.title;
const handleSelectChange = jest.fn();
const { container, getByTitle } = render(
<Transfer
{...searchTransferProps}
showSearch
filterOption={filterOption}
render={renderFunc}
onSelectChange={handleSelectChange}
/>,
);
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('input[type="text"]')!,
{ target: { value: 'content2' } },
);
getByTitle('content2').click();
expect(handleSelectChange).toHaveBeenCalledWith(['1'], []);
});
it('should transfer just the filtered item after search by input', () => {
const filterOption: TransferProps<any>['filterOption'] = (inputValue, option) =>
option.description.includes(inputValue);
const renderFunc: TransferProps<any>['render'] = (item) => item.title;
const handleChange = jest.fn();
const TransferDemo = () => {
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
searchTransferProps.selectedKeys,
);
const handleSelectChange: TransferProps<any>['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
return (
<Transfer
{...searchTransferProps}
showSearch
filterOption={filterOption}
render={renderFunc}
onSelectChange={handleSelectChange}
onChange={handleChange}
selectedKeys={selectedKeys}
/>
);
};
const { container } = render(<TransferDemo />);
fireEvent.change(
container.querySelector('.ant-transfer-list-search')?.querySelector('input')!,
{ target: { value: 'content2' } },
);
fireEvent.click(
container
?.querySelector('.ant-transfer-list')
?.querySelector('.ant-transfer-list-header input[type="checkbox"]')!,
);
fireEvent.click(container.querySelector('.ant-transfer-operation')?.querySelector('button')!);
expect(handleChange).toHaveBeenCalledWith(['1', '3', '4'], 'right', ['1']);
});
it('should check correctly when there is a search text', () => {
const newProps = { ...listCommonProps };
delete newProps.targetKeys;
delete newProps.selectedKeys;
const handleSelectChange = jest.fn();
const { container, getByText } = render(
<Transfer
{...newProps}
showSearch
onSelectChange={handleSelectChange}
render={(item) => item.title}
/>,
);
getByText('b').click();
expect(handleSelectChange).toHaveBeenLastCalledWith(['b'], []);
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('input[type="text"]')!,
{ target: { value: 'a' } },
);
fireEvent.click(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('.ant-transfer-list-header input[type="checkbox"]')!,
);
expect(handleSelectChange).toHaveBeenLastCalledWith(['b', 'a'], []);
fireEvent.click(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('.ant-transfer-list-header input[type="checkbox"]')!,
);
expect(handleSelectChange).toHaveBeenLastCalledWith(['b'], []);
});
it('should show sorted targetKey', () => {
const sortedTargetKeyProps = {
dataSource: [
{
key: 'a',
title: 'a',
},
{
key: 'b',
title: 'b',
},
{
key: 'c',
title: 'c',
},
],
targetKeys: ['c', 'b'],
lazy: false,
};
const { container } = render(
<Transfer {...sortedTargetKeyProps} render={(item) => item.title} />,
);
expect(container.firstChild).toMatchSnapshot();
});
it('should add custom styles when their props are provided', () => {
const style: React.CSSProperties = {
backgroundColor: 'red',
};
const leftStyle: React.CSSProperties = {
backgroundColor: 'blue',
};
const rightStyle: React.CSSProperties = {
backgroundColor: 'red',
};
const operationStyle: React.CSSProperties = {
backgroundColor: 'yellow',
};
const { container } = render(
<Transfer
{...listCommonProps}
style={style}
listStyle={({ direction }) => (direction === 'left' ? leftStyle : rightStyle)}
operationStyle={operationStyle}
/>,
);
const wrapper = container.querySelector<HTMLDivElement>('.ant-transfer');
const listSource = container.querySelectorAll<HTMLDivElement>('.ant-transfer-list').item(0);
const listTarget = container.querySelectorAll<HTMLDivElement>('.ant-transfer-list').item(1);
const operation = container.querySelectorAll<HTMLDivElement>('.ant-transfer-operation').item(0);
expect(wrapper?.style.backgroundColor).toEqual('red');
expect(listSource.style.backgroundColor).toEqual('blue');
expect(listTarget.style.backgroundColor).toEqual('red');
expect(operation.style.backgroundColor).toEqual('yellow');
});
it('should support onScroll', () => {
const onScroll = jest.fn();
const { container } = render(<Transfer {...listCommonProps} onScroll={onScroll} />);
fireEvent.scroll(
container
.querySelectorAll('.ant-transfer-list')
.item(0)
.querySelectorAll('.ant-transfer-list-content')
.item(0),
);
expect(onScroll).toHaveBeenLastCalledWith('left', expect.anything());
fireEvent.scroll(
container
.querySelectorAll('.ant-transfer-list')
.item(1)
.querySelectorAll('.ant-transfer-list-content')
.item(0),
);
expect(onScroll).toHaveBeenLastCalledWith('right', expect.anything());
});
it('support rowKey', () => {
const onSelectChange = jest.fn();
const Demo = () => {
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
return (
<Transfer
{...listCommonProps}
selectedKeys={selectedKeys}
rowKey={(record) => `key_${record.key}`}
onSelectChange={(keys) => {
onSelectChange(keys);
setSelectedKeys(keys);
}}
/>
);
};
const { container } = render(<Demo />);
fireEvent.click(container.querySelector('.ant-transfer-list-content input')!);
expect(onSelectChange).toHaveBeenCalledWith(['key_a']);
expect(
container.querySelector<HTMLInputElement>('.ant-transfer-list-content input')!.checked,
).toBeTruthy();
});
it('should support render value and label in item', () => {
const { container } = render(
<Transfer
dataSource={[{ key: 'a', title: 'title' }]}
render={(record) => ({
value: `${record.title} value`,
label: 'label' as unknown as React.ReactElement,
})}
/>,
);
expect(container.firstChild).toMatchSnapshot();
});
it('should render correct checkbox label when checkboxLabel is defined', () => {
const selectAllLabels = ['Checkbox Label'];
const { getByText } = render(
<Transfer {...listCommonProps} selectAllLabels={selectAllLabels} />,
);
expect(getByText('Checkbox Label')).toBeTruthy();
});
it('should render correct checkbox label when checkboxLabel is a function', () => {
const selectAllLabels: SelectAllLabel[] = [
({ selectedCount, totalCount }) => (
<span>
{selectedCount} of {totalCount}
</span>
),
];
const { getByText } = render(
<Transfer {...listCommonProps} selectAllLabels={selectAllLabels} />,
);
expect(getByText('1 of 2')).toBeTruthy();
});
describe('pagination', () => {
it('boolean', async () => {
const { getByTitle } = render(<Transfer {...listDisabledProps} pagination />);
await waitFor(() => getByTitle('1/1'));
});
it('object', async () => {
const { container, getByTitle } = render(
<Transfer {...listDisabledProps} pagination={{ pageSize: 1 }} />,
);
expect(
container
.querySelectorAll('.ant-transfer-list')
.item(0)
.querySelectorAll('.ant-transfer-list-content-item'),
).toHaveLength(1);
await waitFor(() => getByTitle('1/2'));
});
it('not exceed max size', async () => {
const { container, getByTitle, getAllByTitle, rerender } = render(
<Transfer {...listDisabledProps} pagination={{ pageSize: 1 }} />,
);
fireEvent.click(container.querySelector('.ant-pagination-next .ant-pagination-item-link')!);
await waitFor(() => getByTitle('2/2'));
rerender(
<Transfer
{...{ ...listDisabledProps, targetKeys: ['b', 'c'] }}
pagination={{ pageSize: 1 }}
/>,
);
await waitFor(() => expect(getAllByTitle('1/1')).toHaveLength(2));
});
it('should support change pageSize', () => {
const dataSource = generateData();
const { container } = render(
<Transfer dataSource={dataSource} pagination={{ showSizeChanger: true, simple: false }} />,
);
fireEvent.mouseDown(container.querySelector('.ant-select-selector')!);
fireEvent.click(container.querySelectorAll('.ant-select-item-option')[1]);
expect(container.querySelectorAll('.ant-transfer-list-content-item').length).toBe(20);
});
it('should be used first when pagination has pagesize', () => {
const dataSource = generateData(30);
const { container } = render(
<Transfer
dataSource={dataSource}
pagination={{ showSizeChanger: true, simple: false, pageSize: 20 }}
/>,
);
fireEvent.mouseDown(container.querySelector('.ant-select-selector')!);
fireEvent.click(container.querySelectorAll('.ant-select-item-option')[2]);
expect(container.querySelectorAll('.ant-transfer-list-content-item').length).toBe(20);
});
});
it('remove by click icon', () => {
const onChange = jest.fn();
const { container } = render(<Transfer {...listCommonProps} onChange={onChange} oneWay />);
fireEvent.click(container.querySelectorAll('.ant-transfer-list-content-item-remove')[0]);
expect(onChange).toHaveBeenCalledWith([], 'left', ['b']);
});
it('control mode select all should not throw warning', () => {
const errSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const App: React.FC = () => {
const [selectedKeys, setSelectedKeys] = useState<TransferProps['selectedKeys']>([]);
const onSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
return (
<Transfer
dataSource={[
{
key: 'a',
title: 'a',
},
]}
selectedKeys={selectedKeys}
onSelectChange={onSelectChange}
/>
);
};
const { container } = render(<App />);
fireEvent.click(container.querySelector('.ant-transfer-list-header input[type="checkbox"]')!);
expect(errSpy).not.toHaveBeenCalled();
errSpy.mockRestore();
});
});
describe('immutable data', () => {
// https://github.com/ant-design/ant-design/issues/28662
it('dataSource is frozen', () => {
const mockData = [Object.freeze({ id: '0', title: `title`, description: `description` })];
const { container } = render(<Transfer rowKey={(item) => item.id} dataSource={mockData} />);
expect(container.firstChild).toMatchSnapshot();
});
it('prevent error when reset data in some cases', () => {
const App: React.FC = () => {
const [mockData, setMockData] = useState<DefaultRecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 2; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: i % 2 === 0,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const handleChange: TransferProps['onChange'] = (newTargetKeys) => {
setTargetKeys(newTargetKeys);
};
const ButtonRender = useCallback(
() => <Button onClick={getMock}>Right button reload</Button>,
[getMock],
);
return (
<Transfer
dataSource={mockData}
operations={['to right', 'to left']}
targetKeys={targetKeys}
onChange={handleChange}
render={(item) => `test-${item}`}
footer={ButtonRender}
/>
);
};
const { container } = render(<App />);
fireEvent.click(container.querySelector('.ant-transfer-list-header input[type="checkbox"]')!);
fireEvent.click(container.querySelector('.ant-transfer-operation .ant-btn')!);
expect(container.querySelectorAll('.ant-transfer-list')[1]).toBeTruthy();
expect(
container
.querySelectorAll('.ant-transfer-list')[1]
.querySelectorAll('.ant-transfer-list-content-item').length,
).toBe(2);
fireEvent.click(
container.querySelectorAll('.ant-transfer-list-header input[type="checkbox"]')![1],
);
expect(container.querySelectorAll('.ant-transfer-list-header-selected')[1]).toContainHTML(
'2/2',
);
fireEvent.click(container.querySelector('.ant-transfer-list-footer .ant-btn')!);
expect(container.querySelectorAll('.ant-transfer-list-header-selected')[1]).toContainHTML(
'1/1',
);
});
});

View File

@ -0,0 +1,104 @@
import React from 'react';
import type { KeyWiseTransferItem } from '..';
import { fireEvent, render } from '../../../tests/utils';
import type { TransferListProps } from '../list';
import List from '../list';
const listCommonProps: TransferListProps<KeyWiseTransferItem> = {
prefixCls: 'ant-transfer-list',
dataSource: [
{ key: 'a', title: 'a' },
{ key: 'b', title: 'b' },
{ key: 'c', title: 'c', disabled: true },
],
checkedKeys: ['a'],
notFoundContent: 'Not Found',
} as TransferListProps<KeyWiseTransferItem>;
const listProps: TransferListProps<KeyWiseTransferItem> = {
...listCommonProps,
dataSource: undefined as unknown as KeyWiseTransferItem[],
};
const emptyListProps: TransferListProps<KeyWiseTransferItem> = {
...listCommonProps,
dataSource: [],
};
describe('Transfer.List', () => {
it('should render correctly', () => {
const { container } = render(<List {...listCommonProps} />);
expect(container.firstChild).toMatchSnapshot();
});
it('should check top Checkbox while all available items are checked', () => {
const { container } = render(<List {...listCommonProps} checkedKeys={['a', 'b']} />);
expect(
container.querySelector<HTMLInputElement>('.ant-transfer-list-header input[type="checkbox"]')
?.checked,
).toBeTruthy();
});
it('should render correctly when dataSource is not exists', () => {
expect(() => {
render(<List {...listProps} />);
}).not.toThrow();
});
it('Checkbox should disabled when dataSource is empty', () => {
const { container } = render(<List {...emptyListProps} />);
expect(container.querySelector<HTMLLabelElement>('label.ant-checkbox-wrapper')).toHaveClass(
'ant-checkbox-wrapper-disabled',
);
expect(container.querySelector<HTMLSpanElement>('span.ant-checkbox')).toHaveClass(
'ant-checkbox-disabled',
);
});
it('Checkbox should not disabled when dataSource not is empty', () => {
const { container } = render(<List {...listCommonProps} />);
expect(container.querySelector<HTMLLabelElement>('label.ant-checkbox-wrapper')).not.toHaveClass(
'ant-checkbox-wrapper-disabled',
);
expect(container.querySelector<HTMLSpanElement>('span.ant-checkbox')).not.toHaveClass(
'ant-checkbox-disabled',
);
});
it('support custom dropdown Icon', () => {
const { container } = render(
<List
{...listCommonProps}
selectionsIcon={<span className="test-dropdown-icon">test</span>}
/>,
);
expect(
container?.querySelector<HTMLSpanElement>(
'.ant-transfer-list .ant-transfer-list-header .test-dropdown-icon',
),
).toBeTruthy();
});
it('onItemSelect should be called correctly', () => {
const onItemSelect = jest.fn();
const { container } = render(
<List
{...listCommonProps}
onItemSelect={onItemSelect}
renderList={(props) => (
<div
className="custom-list-body"
onClick={(e) => {
props.onItemSelect('a', false, e);
}}
>
custom list body
</div>
)}
/>,
);
fireEvent.click(container.querySelector('.custom-list-body')!);
expect(onItemSelect).toHaveBeenCalledWith('a', false);
});
});

View File

@ -0,0 +1,122 @@
import React from 'react';
import { render as testLibRender } from '@testing-library/react';
import { fireEvent, render } from '../../../tests/utils';
import Transfer from '../index';
import Search from '../search';
describe('Transfer.Search', () => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
const dataSource = [
{ key: 'a', title: 'a', description: 'a' },
{ key: 'b', title: 'b', description: 'b' },
{ key: 'c', title: 'c', description: 'c' },
];
afterEach(() => {
errorSpy.mockReset();
});
afterAll(() => {
errorSpy.mockRestore();
});
it('should show cross icon when input value exists', () => {
const { container, rerender } = render(<Search value="" />);
expect(container.firstChild).toMatchSnapshot();
rerender(<Search value="a" />);
expect(container.firstChild).toMatchSnapshot();
});
it('onSearch', () => {
jest.useFakeTimers();
const onSearch = jest.fn();
const { container } = render(
<Transfer
dataSource={dataSource}
selectedKeys={[]}
targetKeys={[]}
render={(item) => item.title}
onSearch={onSearch}
showSearch
/>,
);
fireEvent.change(container.querySelectorAll('.ant-input').item(0), { target: { value: 'a' } });
expect(onSearch).toHaveBeenCalledWith('left', 'a');
onSearch.mockReset();
fireEvent.click(container.querySelectorAll('.ant-input-clear-icon').item(0));
expect(onSearch).toHaveBeenCalledWith('left', '');
jest.useRealTimers();
});
it('legacy props#onSearchChange does not work anymore', () => {
const onSearchChange = jest.fn();
const props = { onSearchChange };
const { container } = render(<Transfer render={(item) => item.title!} {...props} showSearch />);
fireEvent.change(container.querySelector('.ant-input')!, { target: { value: 'a' } });
expect(errorSpy).not.toHaveBeenCalled();
expect(onSearchChange).not.toHaveBeenCalled();
});
// https://github.com/ant-design/ant-design/issues/26208
it('typing space should trigger filterOption', () => {
const filterOption = jest.fn();
// We use origin testing lib here since StrictMode will render multiple times
const { container } = testLibRender(
<Transfer filterOption={filterOption} dataSource={dataSource} showSearch />,
);
fireEvent.change(container.querySelector('.ant-input')!, { target: { value: ' ' } });
expect(filterOption).toHaveBeenCalledTimes(dataSource.length);
});
it('The filterOption parameter is correct when use input in search box', () => {
const filterOption = jest.fn();
const { container } = testLibRender(
<Transfer
filterOption={filterOption}
dataSource={dataSource}
targetKeys={['b']}
showSearch
/>,
);
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(0)
?.querySelector('input[type="text"]')!,
{ target: { value: 'a' } },
);
expect(filterOption).toHaveBeenNthCalledWith(
1,
'a',
{ key: 'a', title: 'a', description: 'a' },
'left',
);
expect(filterOption).toHaveBeenLastCalledWith(
'a',
{ key: 'c', title: 'c', description: 'c' },
'left',
);
filterOption.mockReset();
fireEvent.change(
container
?.querySelectorAll('.ant-transfer-list')
?.item(1)
?.querySelector('input[type="text"]')!,
{ target: { value: 'b' } },
);
expect(filterOption).toHaveBeenCalledWith(
'b',
{ key: 'b', title: 'b', description: 'b' },
'right',
);
});
});

View File

@ -0,0 +1,9 @@
## zh-CN
穿梭框高级用法,可配置操作文案,可定制宽高,可对底部进行自定义渲染。
## en-US
Advanced Usage of Transfer.
You can customize the labels of the transfer buttons, the width and height of the columns, and what should be displayed in the footer.

View File

@ -0,0 +1,75 @@
import React, { useEffect, useState } from 'react';
import { Button, Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: i % 2 === 0,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const handleChange: TransferProps['onChange'] = (newTargetKeys) => {
setTargetKeys(newTargetKeys);
};
const renderFooter: TransferProps['footer'] = (_, info) => {
if (info?.direction === 'left') {
return (
<Button size="small" style={{ float: 'left', margin: 5 }} onClick={getMock}>
Left button reload
</Button>
);
}
return (
<Button size="small" style={{ float: 'right', margin: 5 }} onClick={getMock}>
Right button reload
</Button>
);
};
return (
<Transfer
dataSource={mockData}
showSearch
listStyle={{
width: 250,
height: 300,
}}
operations={['to right', 'to left']}
targetKeys={targetKeys}
onChange={handleChange}
render={(item) => `${item.title}-${item.description}`}
footer={renderFooter}
/>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
最基本的用法,展示了 `dataSource`、`targetKeys`、每行的渲染函数 `render` 以及回调函数 `onChange` `onSelectChange` `onScroll` 的用法。
## en-US
The most basic usage of `Transfer` involves providing the source data and target keys arrays, plus the rendering and some callback functions.

View File

@ -0,0 +1,58 @@
import React, { useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
}
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
}));
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<TransferProps['targetKeys']>([]);
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
console.log('targetKeys:', nextTargetKeys);
console.log('direction:', direction);
console.log('moveKeys:', moveKeys);
setTargetKeys(nextTargetKeys);
};
const onSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
console.log('sourceSelectedKeys:', sourceSelectedKeys);
console.log('targetSelectedKeys:', targetSelectedKeys);
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
const onScroll: TransferProps['onScroll'] = (direction, e) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
return (
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={onChange}
onSelectChange={onSelectChange}
onScroll={onScroll}
render={(item) => item.title}
/>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
Component Token Debug.
## en-US
Component Token Debug.

View File

@ -0,0 +1,215 @@
import React, { useState } from 'react';
import { ConfigProvider, Space, Switch, Table, Tag, Transfer } from 'antd';
import type { GetProp, TableColumnsType, TableProps, TransferProps } from 'antd';
import difference from 'lodash/difference';
type TableRowSelection<T> = TableProps<T>['rowSelection'];
type TransferItem = GetProp<TransferProps, 'dataSource'>[number];
interface RecordType {
key: string;
title: string;
description: string;
disabled: boolean;
tag: string;
}
interface DataType {
key: string;
title: string;
description: string;
disabled: boolean;
tag: string;
}
interface TableTransferProps extends TransferProps<TransferItem> {
dataSource: DataType[];
leftColumns: TableColumnsType<DataType>;
rightColumns: TableColumnsType<DataType>;
}
// Customize Table Transfer
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }: TableTransferProps) => (
<Transfer {...restProps}>
{({
direction,
filteredItems,
onItemSelectAll,
onItemSelect,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === 'left' ? leftColumns : rightColumns;
const rowSelection: TableRowSelection<TransferItem> = {
getCheckboxProps: (item) => ({ disabled: listDisabled || item.disabled }),
onSelectAll(selected, selectedRows) {
const treeSelectedKeys = selectedRows
.filter((item) => !item.disabled)
.map(({ key }) => key);
const diffKeys = selected
? difference(treeSelectedKeys, listSelectedKeys)
: difference(listSelectedKeys, treeSelectedKeys);
onItemSelectAll(diffKeys as string[], selected);
},
onSelect({ key }, selected) {
onItemSelect(key as string, selected);
},
selectedRowKeys: listSelectedKeys,
};
return (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredItems}
size="small"
style={{ pointerEvents: listDisabled ? 'none' : undefined }}
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) {
return;
}
onItemSelect(key as string, !listSelectedKeys.includes(key as string));
},
})}
/>
);
}}
</Transfer>
);
const mockTags = ['cat', 'dog', 'bird'];
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 4 === 0,
tag: mockTags[i % 3],
}));
const leftTableColumns: TableColumnsType<DataType> = [
{
dataIndex: 'title',
title: 'Name',
},
{
dataIndex: 'tag',
title: 'Tag',
render: (tag) => <Tag>{tag}</Tag>,
},
{
dataIndex: 'description',
title: 'Description',
},
];
const rightTableColumns: TableColumnsType<DataType> = [
{
dataIndex: 'title',
title: 'Name',
},
];
const initialTargetKeys = mockData.filter((item) => Number(item.key) > 10).map((item) => item.key);
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<React.Key[]>(initialTargetKeys);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const onChange: TransferProps['onChange'] = (nextTargetKeys, direction, moveKeys) => {
console.log('targetKeys:', nextTargetKeys);
console.log('direction:', direction);
console.log('moveKeys:', moveKeys);
setTargetKeys(nextTargetKeys);
};
const onSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
console.log('sourceSelectedKeys:', sourceSelectedKeys);
console.log('targetSelectedKeys:', targetSelectedKeys);
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
};
const onScroll: TransferProps['onScroll'] = (direction, e) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
const [disabled, setDisabled] = useState(false);
const [showSearch, setShowSearch] = useState(false);
const secondOnChange: TransferProps['onChange'] = (nextTargetKeys) => {
setTargetKeys(nextTargetKeys);
};
const triggerDisable = (checked: boolean) => {
setDisabled(checked);
};
const triggerShowSearch = (checked: boolean) => {
setShowSearch(checked);
};
return (
<ConfigProvider
theme={{
components: {
Transfer: {
listWidth: 40,
listWidthLG: 50,
listHeight: 30,
itemHeight: 20,
itemPaddingBlock: 10,
headerHeight: 18,
},
},
}}
>
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={onChange}
onSelectChange={onSelectChange}
onScroll={onScroll}
render={(item) => item.title}
/>
<Transfer status="error" />
<Transfer status="warning" showSearch />
<TableTransfer
dataSource={mockData}
targetKeys={targetKeys}
disabled={disabled}
showSearch={showSearch}
onChange={secondOnChange}
filterOption={(inputValue, item) =>
item.title!.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
}
leftColumns={leftTableColumns}
rightColumns={rightTableColumns}
/>
<Space style={{ marginTop: 16 }}>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={triggerDisable}
/>
<Switch
unCheckedChildren="showSearch"
checkedChildren="showSearch"
checked={showSearch}
onChange={triggerShowSearch}
/>
</Space>
</ConfigProvider>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义渲染每一个 Transfer Item可用于渲染复杂数据。
## en-US
Custom each Transfer Item, and in this way you can render a complex datasource.

View File

@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: i % 2 === 0,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const handleChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
console.log(newTargetKeys, direction, moveKeys);
setTargetKeys(newTargetKeys);
};
const renderItem = (item: RecordType) => {
const customLabel = (
<span className="custom-item">
{item.title} - {item.description}
</span>
);
return {
label: customLabel, // for displayed item
value: item.title, // for title and filter matching
};
};
return (
<Transfer
dataSource={mockData}
listStyle={{
width: 300,
height: 300,
}}
targetKeys={targetKeys}
onChange={handleChange}
render={renderItem}
/>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
自定义穿梭框全选按钮的文字。
## en-US
Custom the labels for select all checkboxes.

View File

@ -0,0 +1,37 @@
import React, { useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
}
const mockData: RecordType[] = Array.from({ length: 10 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
}));
const oriTargetKeys = mockData.filter((item) => Number(item.key) % 3 > 1).map((item) => item.key);
const selectAllLabels: TransferProps['selectAllLabels'] = [
'Select All',
({ selectedCount, totalCount }) => `${selectedCount}/${totalCount}`,
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<React.Key[]>(oriTargetKeys);
return (
<Transfer
dataSource={mockData}
targetKeys={targetKeys}
onChange={setTargetKeys}
render={(item) => item.title}
selectAllLabels={selectAllLabels}
/>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
大数据下使用分页。
## en-US
large count of items with pagination.

View File

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import { Switch, Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [oneWay, setOneWay] = useState(false);
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<React.Key[]>([]);
useEffect(() => {
const newTargetKeys = [];
const newMockData = [];
for (let i = 0; i < 2000; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: i % 2 === 0,
};
if (data.chosen) {
newTargetKeys.push(data.key);
}
newMockData.push(data);
}
setTargetKeys(newTargetKeys);
setMockData(newMockData);
}, []);
const onChange: TransferProps['onChange'] = (newTargetKeys, direction, moveKeys) => {
console.log(newTargetKeys, direction, moveKeys);
setTargetKeys(newTargetKeys);
};
return (
<>
<Transfer
dataSource={mockData}
targetKeys={targetKeys}
onChange={onChange}
render={(item) => item.title}
oneWay={oneWay}
pagination
/>
<br />
<Switch
unCheckedChildren="one way"
checkedChildren="one way"
checked={oneWay}
onChange={setOneWay}
/>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
通过 `oneWay` 将 Transfer 转为单向样式。
## en-US
Use `oneWay` to makes Transfer to one way style.

View File

@ -0,0 +1,78 @@
import React, { useState } from 'react';
import { Switch, Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
disabled: boolean;
}
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
disabled: i % 3 < 1,
}));
const oriTargetKeys = mockData.filter((item) => Number(item.key) % 3 > 1).map((item) => item.key);
const App: React.FC = () => {
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) => {
setTargetKeys(newTargetKeys);
console.log('targetKeys: ', newTargetKeys);
console.log('direction: ', direction);
console.log('moveKeys: ', moveKeys);
};
const handleSelectChange: TransferProps['onSelectChange'] = (
sourceSelectedKeys,
targetSelectedKeys,
) => {
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys]);
console.log('sourceSelectedKeys: ', sourceSelectedKeys);
console.log('targetSelectedKeys: ', targetSelectedKeys);
};
const handleScroll: TransferProps['onScroll'] = (direction, e) => {
console.log('direction:', direction);
console.log('target:', e.target);
};
const handleDisable = (checked: boolean) => {
setDisabled(checked);
};
return (
<>
<Transfer
dataSource={mockData}
titles={['Source', 'Target']}
targetKeys={targetKeys}
selectedKeys={selectedKeys}
onChange={handleChange}
onSelectChange={handleSelectChange}
onScroll={handleScroll}
render={(item) => item.title}
disabled={disabled}
oneWay
style={{ marginBottom: 16 }}
/>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={handleDisable}
/>
</>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
带搜索框的穿梭框,可以自定义搜索函数。
## en-US
Transfer with a search box.

View File

@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react';
import { Transfer } from 'antd';
import type { TransferProps } from 'antd';
interface RecordType {
key: string;
title: string;
description: string;
chosen: boolean;
}
const App: React.FC = () => {
const [mockData, setMockData] = useState<RecordType[]>([]);
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const getMock = () => {
const tempTargetKeys = [];
const tempMockData = [];
for (let i = 0; i < 20; i++) {
const data = {
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
chosen: i % 2 === 0,
};
if (data.chosen) {
tempTargetKeys.push(data.key);
}
tempMockData.push(data);
}
setMockData(tempMockData);
setTargetKeys(tempTargetKeys);
};
useEffect(() => {
getMock();
}, []);
const filterOption = (inputValue: string, option: RecordType) =>
option.description.indexOf(inputValue) > -1;
const handleChange: TransferProps['onChange'] = (newTargetKeys) => {
setTargetKeys(newTargetKeys);
};
const handleSearch: TransferProps['onSearch'] = (dir, value) => {
console.log('search:', dir, value);
};
return (
<Transfer
dataSource={mockData}
showSearch
filterOption={filterOption}
targetKeys={targetKeys}
onChange={handleChange}
onSearch={handleSearch}
render={(item) => item.title}
/>
);
};
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
使用 `status` 为 Transfer 添加状态,可选 `error` 或者 `warning`
## en-US
Add status to Transfer with `status`, which could be `error` or `warning`.

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Space, Transfer } from 'antd';
const App: React.FC = () => (
<Space direction="vertical">
<Transfer status="error" />
<Transfer status="warning" showSearch />
</Space>
);
export default App;

View File

@ -0,0 +1,7 @@
## zh-CN
使用 Table 组件作为自定义渲染列表。
## en-US
Customize render list with Table component.

View File

@ -0,0 +1,139 @@
import React, { useState } from 'react';
import { Space, Switch, Table, Tag, Transfer } from 'antd';
import type { GetProp, TableColumnsType, TableProps, TransferProps } from 'antd';
type TransferItem = GetProp<TransferProps, 'dataSource'>[number];
type TableRowSelection<T extends object> = TableProps<T>['rowSelection'];
interface RecordType {
key: string;
title: string;
description: string;
tag: string;
}
interface DataType {
key: string;
title: string;
description: string;
tag: string;
}
interface TableTransferProps extends TransferProps<TransferItem> {
dataSource: DataType[];
leftColumns: TableColumnsType<DataType>;
rightColumns: TableColumnsType<DataType>;
}
// Customize Table Transfer
const TableTransfer = ({ leftColumns, rightColumns, ...restProps }: TableTransferProps) => (
<Transfer {...restProps}>
{({
direction,
filteredItems,
onItemSelect,
onItemSelectAll,
selectedKeys: listSelectedKeys,
disabled: listDisabled,
}) => {
const columns = direction === 'left' ? leftColumns : rightColumns;
const rowSelection: TableRowSelection<TransferItem> = {
getCheckboxProps: () => ({ disabled: listDisabled }),
onChange(selectedRowKeys) {
onItemSelectAll(selectedRowKeys, 'replace');
},
selectedRowKeys: listSelectedKeys,
selections: [Table.SELECTION_ALL, Table.SELECTION_INVERT, Table.SELECTION_NONE],
};
return (
<Table
rowSelection={rowSelection}
columns={columns}
dataSource={filteredItems}
size="small"
style={{ pointerEvents: listDisabled ? 'none' : undefined }}
onRow={({ key, disabled: itemDisabled }) => ({
onClick: () => {
if (itemDisabled || listDisabled) {
return;
}
onItemSelect(key, !listSelectedKeys.includes(key));
},
})}
/>
);
}}
</Transfer>
);
const mockTags = ['cat', 'dog', 'bird'];
const mockData: RecordType[] = Array.from({ length: 20 }).map((_, i) => ({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
tag: mockTags[i % 3],
}));
const columns: TableColumnsType<DataType> = [
{
dataIndex: 'title',
title: 'Name',
},
{
dataIndex: 'tag',
title: 'Tag',
render: (tag: string) => (
<Tag style={{ marginInlineEnd: 0 }} color="cyan">
{tag.toUpperCase()}
</Tag>
),
},
{
dataIndex: 'description',
title: 'Description',
},
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
const [disabled, setDisabled] = useState(false);
const onChange: TableTransferProps['onChange'] = (nextTargetKeys) => {
setTargetKeys(nextTargetKeys);
};
const toggleDisabled = (checked: boolean) => {
setDisabled(checked);
};
return (
<>
<TableTransfer
dataSource={mockData}
targetKeys={targetKeys}
disabled={disabled}
showSearch
showSelectAll={false}
onChange={onChange}
filterOption={(inputValue, item) =>
item.title!.indexOf(inputValue) !== -1 || item.tag.indexOf(inputValue) !== -1
}
leftColumns={columns}
rightColumns={columns}
/>
<Space style={{ marginTop: 16 }}>
<Switch
unCheckedChildren="disabled"
checkedChildren="disabled"
checked={disabled}
onChange={toggleDisabled}
/>
</Space>
</>
);
};
export default App;

View File

@ -0,0 +1,14 @@
## zh-CN
使用 Tree 组件作为自定义渲染列表。
## en-US
Customize render list with Tree component.
<style>
.tree-transfer .ant-transfer-list:first-child {
flex: none;
width: 50%;
}
</style>

View File

@ -0,0 +1,102 @@
import React, { useState } from 'react';
import { theme, Transfer, Tree } from 'antd';
import type { GetProp, TransferProps, TreeDataNode } from 'antd';
type TransferItem = GetProp<TransferProps, 'dataSource'>[number];
interface TreeTransferProps {
dataSource: TreeDataNode[];
targetKeys: TransferProps['targetKeys'];
onChange: TransferProps['onChange'];
}
// Customize Table Transfer
const isChecked = (selectedKeys: React.Key[], eventKey: React.Key) =>
selectedKeys.includes(eventKey);
const generateTree = (
treeNodes: TreeDataNode[] = [],
checkedKeys: TreeTransferProps['targetKeys'] = [],
): TreeDataNode[] =>
treeNodes.map(({ children, ...props }) => ({
...props,
disabled: checkedKeys.includes(props.key as string),
children: generateTree(children, checkedKeys),
}));
const TreeTransfer: React.FC<TreeTransferProps> = ({
dataSource,
targetKeys = [],
...restProps
}) => {
const { token } = theme.useToken();
const transferDataSource: TransferItem[] = [];
function flatten(list: TreeDataNode[] = []) {
list.forEach((item) => {
transferDataSource.push(item as TransferItem);
flatten(item.children);
});
}
flatten(dataSource);
return (
<Transfer
{...restProps}
targetKeys={targetKeys}
dataSource={transferDataSource}
className="tree-transfer"
render={(item) => item.title!}
showSelectAll={false}
>
{({ direction, onItemSelect, selectedKeys }) => {
if (direction === 'left') {
const checkedKeys = [...selectedKeys, ...targetKeys];
return (
<div style={{ padding: token.paddingXS }}>
<Tree
blockNode
checkable
checkStrictly
defaultExpandAll
checkedKeys={checkedKeys}
treeData={generateTree(dataSource, targetKeys)}
onCheck={(_, { node: { key } }) => {
onItemSelect(key as string, !isChecked(checkedKeys, key));
}}
onSelect={(_, { node: { key } }) => {
onItemSelect(key as string, !isChecked(checkedKeys, key));
}}
/>
</div>
);
}
}}
</Transfer>
);
};
const treeData: TreeDataNode[] = [
{ key: '0-0', title: '0-0' },
{
key: '0-1',
title: '0-1',
children: [
{ key: '0-1-0', title: '0-1-0' },
{ key: '0-1-1', title: '0-1-1' },
],
},
{ key: '0-2', title: '0-2' },
{ key: '0-3', title: '0-3' },
{ key: '0-4', title: '0-4' },
];
const App: React.FC = () => {
const [targetKeys, setTargetKeys] = useState<TreeTransferProps['targetKeys']>([]);
const onChange: TreeTransferProps['onChange'] = (keys) => {
setTargetKeys(keys);
};
return <TreeTransfer dataSource={treeData} targetKeys={targetKeys} onChange={onChange} />;
};
export default App;

View File

@ -0,0 +1,43 @@
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?: TransferKey[],
) => {
const mergedDataSource = React.useMemo(
() =>
(dataSource || []).map((record) => {
if (rowKey) {
record = { ...record, key: rowKey(record) };
}
return record;
}),
[dataSource, rowKey],
);
const [leftDataSource, rightDataSource] = React.useMemo(() => {
const leftData: KeyWise<RecordType>[] = [];
const rightData: KeyWise<RecordType>[] = new Array((targetKeys || []).length);
const targetKeysMap = groupKeysMap(targetKeys || []);
mergedDataSource.forEach((record) => {
// rightData should be ordered by targetKeys
// leftData should be ordered by dataSource
if (targetKeysMap.has(record.key)) {
(rightData as any)[targetKeysMap.get(record.key) as any] = record;
} else {
leftData.push(record as any);
}
});
return [leftData, rightData] as const;
}, [mergedDataSource, targetKeys, rowKey]);
return [mergedDataSource, leftDataSource, rightDataSource];
};
export default useData;

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import type { TransferKey } from '../interface';
const EMPTY_KEYS: TransferKey[] = [];
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<TransferKey>) {
return Array.from(keys).join(';');
}
export default function useSelection<T extends { key: TransferKey }>(
leftDataSource: T[],
rightDataSource: T[],
selectedKeys: TransferKey[] = EMPTY_KEYS,
): [
sourceSelectedKeys: TransferKey[],
targetSelectedKeys: TransferKey[],
setSourceSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
setTargetSelectedKeys: React.Dispatch<React.SetStateAction<TransferKey[]>>,
] {
// Prepare `dataSource` keys
const [leftKeys, rightKeys] = React.useMemo(
() => [
new Set(leftDataSource.map((src) => src.key)),
new Set(rightDataSource.map((src) => src.key)),
],
[leftDataSource, rightDataSource],
);
// Selected Keys
const [sourceSelectedKeys, setSourceSelectedKeys] = React.useState(() =>
filterKeys(selectedKeys, leftKeys),
);
const [targetSelectedKeys, setTargetSelectedKeys] = React.useState(() =>
filterKeys(selectedKeys, rightKeys),
);
// Fill selected keys
React.useEffect(() => {
setSourceSelectedKeys(filterKeys(selectedKeys, leftKeys));
setTargetSelectedKeys(filterKeys(selectedKeys, rightKeys));
}, [selectedKeys]);
// Reset when data changed
React.useEffect(() => {
setSourceSelectedKeys(filterKeys(sourceSelectedKeys, leftKeys));
setTargetSelectedKeys(filterKeys(targetSelectedKeys, rightKeys));
}, [flattenKeys(leftKeys), flattenKeys(rightKeys)]);
return [
// Keys
sourceSelectedKeys,
targetSelectedKeys,
// Updater
setSourceSelectedKeys,
setTargetSelectedKeys,
];
}

View File

@ -0,0 +1,510 @@
import type { ChangeEvent, CSSProperties } from 'react';
import React, { useCallback, useContext } from 'react';
import classNames from 'classnames';
import type { PrevSelectedIndex } from '../_util/hooks/useMultipleSelect';
import useMultipleSelect from '../_util/hooks/useMultipleSelect';
import type { InputStatus } from '../_util/statusUtils';
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils';
import { groupDisabledKeysMap, groupKeysMap } from '../_util/transKeys';
import { devUseWarning } from '../_util/warning';
import type { ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';
import DefaultRenderEmpty from '../config-provider/defaultRenderEmpty';
import type { FormItemStatusContextProps } from '../form/context';
import { FormItemInputContext } from '../form/context';
import { useLocale } from '../locale';
import defaultLocale from '../locale/en_US';
import useData from './hooks/useData';
import useSelection from './hooks/useSelection';
import type { PaginationType, TransferKey } from './interface';
import type { TransferCustomListBodyProps, TransferListProps } from './list';
import List from './list';
import Operation from './operation';
import Search from './search';
import useStyle from './style';
export type { TransferListProps } from './list';
export type { TransferOperationProps } from './operation';
export type { TransferSearchProps } from './search';
export type TransferDirection = 'left' | 'right';
export interface RenderResultObject {
label: React.ReactElement;
value: string;
}
export type RenderResult = React.ReactElement | RenderResultObject | string | null;
export interface TransferItem {
key?: TransferKey;
title?: string;
description?: string;
disabled?: boolean;
[name: string]: any;
}
export type KeyWise<T> = T & { key: TransferKey };
export type KeyWiseTransferItem = KeyWise<TransferItem>;
type TransferRender<RecordType> = (item: RecordType) => RenderResult;
export interface ListStyle {
direction: TransferDirection;
}
export type SelectAllLabel =
| React.ReactNode
| ((info: { selectedCount: number; totalCount: number }) => React.ReactNode);
export interface TransferLocale {
titles?: React.ReactNode[];
notFoundContent?: React.ReactNode | React.ReactNode[];
searchPlaceholder: string;
itemUnit: string;
itemsUnit: string;
remove?: string;
selectAll?: string;
deselectAll?: string;
selectCurrent?: string;
selectInvert?: string;
removeAll?: string;
removeCurrent?: string;
}
export interface TransferProps<RecordType = any> {
prefixCls?: string;
className?: string;
rootClassName?: string;
disabled?: boolean;
dataSource?: RecordType[];
targetKeys?: TransferKey[];
selectedKeys?: TransferKey[];
render?: TransferRender<RecordType>;
onChange?: (
targetKeys: TransferKey[],
direction: TransferDirection,
moveKeys: TransferKey[],
) => void;
onSelectChange?: (sourceSelectedKeys: TransferKey[], targetSelectedKeys: TransferKey[]) => void;
style?: React.CSSProperties;
listStyle?: ((style: ListStyle) => CSSProperties) | CSSProperties;
operationStyle?: CSSProperties;
titles?: React.ReactNode[];
operations?: string[];
showSearch?: boolean;
filterOption?: (inputValue: string, item: RecordType, direction: TransferDirection) => boolean;
locale?: Partial<TransferLocale>;
footer?: (
props: TransferListProps<RecordType>,
info?: { direction: TransferDirection },
) => React.ReactNode;
rowKey?: (record: RecordType) => TransferKey;
onSearch?: (direction: TransferDirection, value: string) => void;
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => void;
children?: (props: TransferCustomListBodyProps<RecordType>) => React.ReactNode;
showSelectAll?: boolean;
selectAllLabels?: SelectAllLabel[];
oneWay?: boolean;
pagination?: PaginationType;
status?: InputStatus;
selectionsIcon?: React.ReactNode;
}
const Transfer = <RecordType extends TransferItem = TransferItem>(
props: TransferProps<RecordType>,
) => {
const {
dataSource,
targetKeys = [],
selectedKeys,
selectAllLabels = [],
operations = [],
style = {},
listStyle = {},
locale = {},
titles,
disabled,
showSearch = false,
operationStyle,
showSelectAll,
oneWay,
pagination,
status: customStatus,
prefixCls: customizePrefixCls,
className,
rootClassName,
selectionsIcon,
filterOption,
render,
footer,
children,
rowKey,
onScroll,
onChange,
onSearch,
onSelectChange,
} = props;
const {
getPrefixCls,
renderEmpty,
direction: dir,
transfer,
} = useContext<ConfigConsumerProps>(ConfigContext);
const prefixCls = getPrefixCls('transfer', customizePrefixCls);
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
// Fill record with `key`
const [mergedDataSource, leftDataSource, rightDataSource] = useData(
dataSource,
rowKey,
targetKeys,
);
// Get direction selected keys
const [
// Keys
sourceSelectedKeys,
targetSelectedKeys,
// Setters
setSourceSelectedKeys,
setTargetSelectedKeys,
] = useSelection(leftDataSource as any, rightDataSource as any, selectedKeys);
const [leftMultipleSelect, updateLeftPrevSelectedIndex] = useMultipleSelect<
KeyWise<RecordType>,
TransferKey
>((item) => item.key);
const [rightMultipleSelect, updateRightPrevSelectedIndex] = useMultipleSelect<
KeyWise<RecordType>,
TransferKey
>((item) => item.key);
if (process.env.NODE_ENV !== 'production') {
const warning = devUseWarning('Transfer');
warning(!pagination || !children, 'usage', '`pagination` not support customize render list.');
}
const setStateKeys = useCallback(
(
direction: TransferDirection,
keys: TransferKey[] | ((prevKeys: TransferKey[]) => TransferKey[]),
) => {
if (direction === 'left') {
const nextKeys = typeof keys === 'function' ? keys(sourceSelectedKeys || []) : keys;
setSourceSelectedKeys(nextKeys);
} else {
const nextKeys = typeof keys === 'function' ? keys(targetSelectedKeys || []) : keys;
setTargetSelectedKeys(nextKeys);
}
},
[sourceSelectedKeys, targetSelectedKeys],
);
const setPrevSelectedIndex = (direction: TransferDirection, value: PrevSelectedIndex) => {
const isLeftDirection = direction === 'left';
const updatePrevSelectedIndex = isLeftDirection
? updateLeftPrevSelectedIndex
: updateRightPrevSelectedIndex;
updatePrevSelectedIndex(value);
};
const handleSelectChange = useCallback(
(direction: TransferDirection, holder: TransferKey[]) => {
if (direction === 'left') {
onSelectChange?.(holder, targetSelectedKeys);
} else {
onSelectChange?.(sourceSelectedKeys, holder);
}
},
[sourceSelectedKeys, targetSelectedKeys],
);
const getTitles = (transferLocale: TransferLocale): React.ReactNode[] =>
titles ?? transferLocale.titles ?? [];
const handleLeftScroll = (e: React.SyntheticEvent<HTMLUListElement>) => {
onScroll?.('left', e);
};
const handleRightScroll = (e: React.SyntheticEvent<HTMLUListElement>) => {
onScroll?.('right', e);
};
const moveTo = (direction: TransferDirection) => {
const moveKeys = direction === 'right' ? sourceSelectedKeys : targetSelectedKeys;
const dataSourceDisabledKeysMap = groupDisabledKeysMap(mergedDataSource);
// filter the disabled options
const newMoveKeys = moveKeys.filter((key) => !dataSourceDisabledKeysMap.has(key));
const newMoveKeysMap = groupKeysMap(newMoveKeys);
// move items to target box
const newTargetKeys =
direction === 'right'
? newMoveKeys.concat(targetKeys)
: targetKeys.filter((targetKey) => !newMoveKeysMap.has(targetKey));
// empty checked keys
const oppositeDirection = direction === 'right' ? 'left' : 'right';
setStateKeys(oppositeDirection, []);
handleSelectChange(oppositeDirection, []);
onChange?.(newTargetKeys, direction, newMoveKeys);
};
const moveToLeft = () => {
moveTo('left');
setPrevSelectedIndex('left', null);
};
const moveToRight = () => {
moveTo('right');
setPrevSelectedIndex('right', null);
};
const onItemSelectAll = (
direction: TransferDirection,
keys: string[],
checkAll: boolean | 'replace',
) => {
setStateKeys(direction, (prevKeys) => {
let mergedCheckedKeys: TransferKey[] = [];
if (checkAll === 'replace') {
mergedCheckedKeys = keys;
} else if (checkAll) {
// Merge current keys with origin key
mergedCheckedKeys = Array.from(new Set<TransferKey>([...prevKeys, ...keys]));
} else {
const selectedKeysMap = groupKeysMap(keys);
// Remove current keys from origin keys
mergedCheckedKeys = prevKeys.filter((key) => !selectedKeysMap.has(key));
}
handleSelectChange(direction, mergedCheckedKeys);
return mergedCheckedKeys;
});
setPrevSelectedIndex(direction, null);
};
const onLeftItemSelectAll = (keys: string[], checkAll: boolean) => {
onItemSelectAll('left', keys, checkAll);
};
const onRightItemSelectAll = (keys: string[], checkAll: boolean) => {
onItemSelectAll('right', keys, checkAll);
};
const leftFilter = (e: ChangeEvent<HTMLInputElement>) => onSearch?.('left', e.target.value);
const rightFilter = (e: ChangeEvent<HTMLInputElement>) => onSearch?.('right', e.target.value);
const handleLeftClear = () => onSearch?.('left', '');
const handleRightClear = () => onSearch?.('right', '');
const handleSingleSelect = (
direction: TransferDirection,
holder: Set<TransferKey>,
selectedKey: TransferKey,
checked: boolean,
currentSelectedIndex: number,
) => {
const isSelected = holder.has(selectedKey);
if (isSelected) {
holder.delete(selectedKey);
setPrevSelectedIndex(direction, null);
}
if (checked) {
holder.add(selectedKey);
setPrevSelectedIndex(direction, currentSelectedIndex);
}
};
const handleMultipleSelect = (
direction: TransferDirection,
data: KeyWise<RecordType>[],
holder: Set<TransferKey>,
currentSelectedIndex: number,
) => {
const isLeftDirection = direction === 'left';
const multipleSelect = isLeftDirection ? leftMultipleSelect : rightMultipleSelect;
multipleSelect(currentSelectedIndex, data, holder);
};
const onItemSelect = (
direction: TransferDirection,
selectedKey: TransferKey,
checked: boolean,
multiple?: boolean,
) => {
const isLeftDirection = direction === 'left';
const holder = [...(isLeftDirection ? sourceSelectedKeys : targetSelectedKeys)];
const holderSet = new Set(holder);
const data = [...(isLeftDirection ? leftDataSource : rightDataSource)].filter(
(item) => !item?.disabled,
);
const currentSelectedIndex = data.findIndex((item) => item.key === selectedKey);
// multiple select by hold down the shift key
if (multiple && holder.length > 0) {
handleMultipleSelect(direction, data as any, holderSet, currentSelectedIndex);
} else {
handleSingleSelect(direction, holderSet, selectedKey, checked, currentSelectedIndex);
}
const holderArr = Array.from(holderSet);
handleSelectChange(direction, holderArr);
if (!props.selectedKeys) {
setStateKeys(direction, holderArr);
}
};
const onLeftItemSelect: TransferListProps<KeyWise<RecordType>>['onItemSelect'] = (
selectedKey,
checked,
e,
) => {
onItemSelect('left', selectedKey, checked, e?.shiftKey);
};
const onRightItemSelect = (
selectedKey: TransferKey,
checked: boolean,
e?: React.MouseEvent<Element, MouseEvent>,
) => {
onItemSelect('right', selectedKey, checked, e?.shiftKey);
};
const onRightItemRemove = (keys: TransferKey[]) => {
setStateKeys('right', []);
onChange?.(
targetKeys.filter((key) => !keys.includes(key)),
'left',
[...keys],
);
};
const handleListStyle = (direction: TransferDirection): CSSProperties => {
if (typeof listStyle === 'function') {
return listStyle({ direction });
}
return listStyle || {};
};
const formItemContext = useContext<FormItemStatusContextProps>(FormItemInputContext);
const { hasFeedback, status } = formItemContext;
const getLocale = (transferLocale: TransferLocale) => ({
...transferLocale,
notFoundContent: renderEmpty?.('Transfer') || <DefaultRenderEmpty componentName="Transfer" />,
...locale,
});
const mergedStatus = getMergedStatus(status, customStatus);
const mergedPagination = !children && pagination;
const leftActive = targetSelectedKeys.length > 0;
const rightActive = sourceSelectedKeys.length > 0;
const cls = classNames(
prefixCls,
{
[`${prefixCls}-disabled`]: disabled,
[`${prefixCls}-customize-list`]: !!children,
[`${prefixCls}-rtl`]: dir === 'rtl',
},
getStatusClassNames(prefixCls, mergedStatus, hasFeedback),
transfer?.className,
className,
rootClassName,
hashId,
cssVarCls,
);
const [contextLocale] = useLocale('Transfer', defaultLocale.Transfer);
const listLocale = getLocale(contextLocale!);
const [leftTitle, rightTitle] = getTitles(listLocale);
const mergedSelectionsIcon = selectionsIcon ?? transfer?.selectionsIcon;
return wrapCSSVar(
<div className={cls} style={{ ...transfer?.style, ...style }}>
<List<KeyWise<RecordType>>
prefixCls={`${prefixCls}-list`}
titleText={leftTitle}
dataSource={leftDataSource as any}
filterOption={filterOption}
style={handleListStyle('left')}
checkedKeys={sourceSelectedKeys}
handleFilter={leftFilter}
handleClear={handleLeftClear}
onItemSelect={onLeftItemSelect}
onItemSelectAll={onLeftItemSelectAll as any}
render={render}
showSearch={showSearch}
renderList={children as any}
footer={footer as any}
onScroll={handleLeftScroll}
disabled={disabled}
direction={dir === 'rtl' ? 'right' : 'left'}
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[0]}
pagination={mergedPagination}
selectionsIcon={mergedSelectionsIcon}
{...listLocale}
/>
<Operation
className={`${prefixCls}-operation`}
rightActive={rightActive}
rightArrowText={operations[0]}
moveToRight={moveToRight}
leftActive={leftActive}
leftArrowText={operations[1]}
moveToLeft={moveToLeft}
style={operationStyle}
disabled={disabled}
direction={dir}
oneWay={oneWay}
/>
<List<KeyWise<RecordType>>
prefixCls={`${prefixCls}-list`}
titleText={rightTitle}
dataSource={rightDataSource as any}
filterOption={filterOption}
style={handleListStyle('right')}
checkedKeys={targetSelectedKeys}
handleFilter={rightFilter}
handleClear={handleRightClear}
onItemSelect={onRightItemSelect}
onItemSelectAll={onRightItemSelectAll as any}
onItemRemove={onRightItemRemove}
render={render}
showSearch={showSearch}
renderList={children as any}
footer={footer as any}
onScroll={handleRightScroll}
disabled={disabled}
direction={dir === 'rtl' ? 'left' : 'right'}
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[1]}
showRemove={oneWay}
pagination={mergedPagination}
selectionsIcon={mergedSelectionsIcon}
{...listLocale}
/>
</div>,
);
};
if (process.env.NODE_ENV !== 'production') {
Transfer.displayName = 'Transfer';
}
Transfer.List = List;
Transfer.Search = Search;
Transfer.Operation = Operation;
export default Transfer;

View File

@ -0,0 +1,103 @@
---
category: Components
group: 数据录入
title: Transfer 穿梭框
subtitle: 穿梭框
description: 双栏穿梭选择框。
---
## 何时使用
- 需要在多个可选项中进行多选时。
- 比起 Select 和 TreeSelect穿梭框占据更大的空间可以展示可选项的更多信息。
穿梭选择框用直观的方式在两栏中移动元素,完成选择行为。
选择一个或以上的选项后,点击对应的方向键,可以把选中的选项移动到另一栏。其中,左边一栏为 `source`,右边一栏为 `target`API 的设计也反映了这两个概念。
> 注意:穿梭框组件只支持受控使用,不支持非受控模式。
## 代码演示
<!-- prettier-ignore -->
<code src="./demo/basic.tsx">基本用法</code>
<code src="./demo/oneWay.tsx">单向样式</code>
<code src="./demo/search.tsx">带搜索框</code>
<code src="./demo/advanced.tsx">高级用法</code>
<code src="./demo/custom-item.tsx">自定义渲染行数据</code>
<code src="./demo/large-data.tsx">分页</code>
<code src="./demo/table-transfer.tsx">表格穿梭框</code>
<code src="./demo/tree-transfer.tsx">树穿梭框</code>
<code src="./demo/status.tsx">自定义状态</code>
<!-- <code src="./demo/custom-select-all-labels.tsx" debug>自定义全选文字</code> -->
<code src="./demo/component-token.tsx" debug>组件 Token</code>
## API
通用属性参考:[通用属性](/docs/react/common-props)
### Transfer
| 参数 | 说明 | 类型 | 默认值 | 版本 |
| --- | --- | --- | --- | --- |
| dataSource | 数据源,其中的数据将会被渲染到左边一栏中,`targetKeys` 中指定的除外 | [RecordType extends TransferItem = TransferItem](https://github.com/ant-design/ant-design/blob/1bf0bab2a7bc0a774119f501806e3e0e3a6ba283/components/transfer/index.tsx#L12)\[] | \[] | |
| disabled | 是否禁用 | boolean | false | |
| selectionsIcon | 自定义下拉菜单图标 | React.ReactNode | | 5.8.0 |
| filterOption | 根据搜索内容进行筛选,接收 `inputValue` `option` `direction` 三个参数,(`direction` 自5.9.0+支持),当 `option` 符合筛选条件时,应返回 true反之则返回 false | (inputValue, option, direction: `left` \| `right`): boolean | - | |
| footer | 底部渲染函数 | (props, { direction }) => ReactNode | - | direction: 4.17.0 |
| listStyle | 两个穿梭框的自定义样式 | object\|({direction: `left` \| `right`}) => object | - | |
| locale | 各种语言 | { itemUnit: string; itemsUnit: string; searchPlaceholder: string; notFoundContent: ReactNode \| ReactNode[]; } | { itemUnit: `项`, itemsUnit: `项`, searchPlaceholder: `请输入搜索内容` } | |
| oneWay | 展示为单向样式 | boolean | false | 4.3.0 |
| operations | 操作文案集合,顺序从上至下 | string\[] | \[`>`, `<`] | |
| operationStyle | 操作栏的自定义样式 | CSSProperties | - | |
| 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\[] \| number\[] | \[] | |
| showSearch | 是否显示搜索框 | boolean | false | |
| showSelectAll | 是否展示全选勾选框 | boolean | true | |
| status | 设置校验状态 | 'error' \| 'warning' | - | 4.19.0 |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] \| number\[] | \[] | |
| titles | 标题集合,顺序从左至右 | ReactNode\[] | - | |
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | - | |
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | - | |
| onSearch | 搜索框内容时改变时的回调函数 | (direction: `left` \| `right`, value: string): void | - | |
| onSelectChange | 选中项发生改变时的回调函数 | (sourceSelectedKeys, targetSelectedKeys): void | - | |
### Render Props
Transfer 支持接收 `children` 自定义渲染列表,并返回以下参数:
| 参数 | 说明 | 类型 | 版本 |
| --------------- | -------------- | ------------------------------------------------- | ---- |
| direction | 渲染列表的方向 | `left` \| `right` | |
| disabled | 是否禁用列表 | boolean | |
| filteredItems | 过滤后的数据 | RecordType\[] | |
| selectedKeys | 选中的条目 | string\[] \| number\[] | |
| onItemSelect | 勾选条目 | (key: string \| number, selected: boolean) | |
| onItemSelectAll | 勾选一组条目 | (keys: string\[] \| number\[], selected: boolean) | |
#### 参考示例
```js
<Transfer {...props}>{(listProps) => <YourComponent {...listProps} />}</Transfer>
```
## 注意
按照 React 的[规范](http://facebook.github.io/react/docs/lists-and-keys.html#keys),所有的组件数组必须绑定 key。在 Transfer 中,`dataSource` 里的数据值需要指定 `key` 值。对于 `dataSource` 默认将每列数据的 `key` 属性作为唯一的标识。
如果你的数据没有这个属性,务必使用 `rowKey` 来指定数据列的主键。
```js
// 比如你的数据主键是 uid
return <Transfer rowKey={(record) => record.uid} />;
```
## 主题变量Design Token
## FAQ
### 怎样让 Transfer 穿梭框列表支持异步数据加载
为了保持页码同步,在勾选时可以不移除选项而以禁用代替:<https://codesandbox.io/s/objective-wing-6iqbx>

View File

@ -0,0 +1,10 @@
export type TransferKey = React.Key;
export type PaginationType =
| boolean
| {
pageSize?: number;
simple?: boolean;
showSizeChanger?: boolean;
showLessItems?: boolean;
};

View File

@ -0,0 +1,397 @@
import React, { useMemo, useRef, useState } from 'react';
import DownOutlined from '@ant-design/icons/DownOutlined';
import classNames from 'classnames';
import omit from 'rc-util/lib/omit';
import { groupKeysMap } from '../_util/transKeys';
import Checkbox from '../checkbox';
import Dropdown from '../dropdown';
import type { MenuProps } from '../menu';
import type {
KeyWiseTransferItem,
RenderResult,
RenderResultObject,
SelectAllLabel,
TransferDirection,
TransferLocale,
} from './index';
import type { PaginationType, TransferKey } from './interface';
import type { ListBodyRef, TransferListBodyProps } from './ListBody';
import DefaultListBody, { OmitProps } from './ListBody';
import Search from './search';
const defaultRender = () => null;
function isRenderResultPlainObject(result: RenderResult): result is RenderResultObject {
return !!(
result &&
!React.isValidElement(result) &&
Object.prototype.toString.call(result) === '[object Object]'
);
}
function getEnabledItemKeys<RecordType extends KeyWiseTransferItem>(items: RecordType[]) {
return items.filter((data) => !data.disabled).map((data) => data.key);
}
const isValidIcon = (icon: React.ReactNode) => icon !== undefined;
export interface RenderedItem<RecordType> {
renderedText: string;
renderedEl: React.ReactNode;
item: RecordType;
}
type RenderListFunction<T> = (props: TransferListBodyProps<T>) => React.ReactNode;
export interface TransferListProps<RecordType> extends TransferLocale {
prefixCls: string;
titleText: React.ReactNode;
dataSource: RecordType[];
filterOption?: (filterText: string, item: RecordType, direction: TransferDirection) => boolean;
style?: React.CSSProperties;
checkedKeys: TransferKey[];
handleFilter: (e: React.ChangeEvent<HTMLInputElement>) => 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;
showSearch?: boolean;
searchPlaceholder: string;
itemUnit: string;
itemsUnit: string;
renderList?: RenderListFunction<RecordType>;
footer?: (
props: TransferListProps<RecordType>,
info?: { direction: TransferDirection },
) => React.ReactNode;
onScroll: (e: React.UIEvent<HTMLUListElement, UIEvent>) => void;
disabled?: boolean;
direction: TransferDirection;
showSelectAll?: boolean;
selectAllLabel?: SelectAllLabel;
showRemove?: boolean;
pagination?: PaginationType;
selectionsIcon?: React.ReactNode;
}
export interface TransferCustomListBodyProps<T> extends TransferListBodyProps<T> {}
const TransferList = <RecordType extends KeyWiseTransferItem>(
props: TransferListProps<RecordType>,
) => {
const {
prefixCls,
dataSource = [],
titleText = '',
checkedKeys,
disabled,
showSearch = false,
style,
searchPlaceholder,
notFoundContent,
selectAll,
deselectAll,
selectCurrent,
selectInvert,
removeAll,
removeCurrent,
showSelectAll = true,
showRemove,
pagination,
direction,
itemsUnit,
itemUnit,
selectAllLabel,
selectionsIcon,
footer,
renderList,
onItemSelectAll,
onItemRemove,
handleFilter,
handleClear,
filterOption,
render = defaultRender,
} = props;
const [filterValue, setFilterValue] = useState<string>('');
const listBodyRef = useRef<ListBodyRef<RecordType>>({});
const internalHandleFilter = (e: React.ChangeEvent<HTMLInputElement>) => {
setFilterValue(e.target.value);
handleFilter(e);
};
const internalHandleClear = () => {
setFilterValue('');
handleClear();
};
const matchFilter = (text: string, item: RecordType) => {
if (filterOption) {
return filterOption(filterValue, item, direction);
}
return text.includes(filterValue);
};
const renderListBody = (listProps: TransferListBodyProps<RecordType>) => {
let bodyContent: React.ReactNode = renderList
? renderList({
...listProps,
onItemSelect: (key, check) => listProps.onItemSelect(key, check),
})
: null;
const customize: boolean = !!bodyContent;
if (!customize) {
// @ts-ignore
bodyContent = <DefaultListBody ref={listBodyRef} {...listProps} />;
}
return { customize, bodyContent };
};
const renderItem = (item: RecordType): RenderedItem<RecordType> => {
const renderResult = render(item);
const isRenderResultPlain = isRenderResultPlainObject(renderResult);
return {
item,
renderedEl: isRenderResultPlain ? renderResult.label : renderResult,
renderedText: isRenderResultPlain ? renderResult.value : (renderResult as string),
};
};
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);
if (filterValue && !matchFilter(renderedItem.renderedText, item)) {
return;
}
filterItems.push(item);
filterRenderItems.push(renderedItem);
});
return [filterItems, filterRenderItems] as const;
}, [dataSource, filterValue]);
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
prefixCls={`${prefixCls}-search`}
onChange={internalHandleFilter as any}
handleClear={internalHandleClear}
placeholder={searchPlaceholder}
value={filterValue}
disabled={disabled}
/>
</div>
) : null;
const { customize, bodyContent } = renderListBody({
...omit(props, OmitProps),
filteredItems,
filteredRenderItems,
selectedKeys: checkedKeys,
});
let bodyNode: React.ReactNode;
// We should wrap customize list body in a classNamed div to use flex layout.
if (customize) {
bodyNode = <div className={`${prefixCls}-body-customize-wrapper`}>{bodyContent}</div>;
} else {
bodyNode = filteredItems.length ? (
bodyContent
) : (
<div className={`${prefixCls}-body-not-found`}>{notFoundContentEle}</div>
);
}
return (
<div
className={classNames(
showSearch ? `${prefixCls}-body ${prefixCls}-body-with-search` : `${prefixCls}-body`,
)}
>
{search}
{bodyNode}
</div>
);
}, [
showSearch,
prefixCls,
searchPlaceholder,
filterValue,
disabled,
checkedKeys,
filteredItems,
filteredRenderItems,
notFoundContentEle,
]);
const checkBox = (
<Checkbox
disabled={dataSource.length === 0 || 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) {
return typeof selectAllLabel === 'function'
? selectAllLabel({ selectedCount, totalCount })
: selectAllLabel;
}
const unit = totalCount > 1 ? itemsUnit : itemUnit;
return (
<>
{(selectedCount > 0 ? `${selectedCount}/` : '') + totalCount} {unit}
</>
);
};
// Custom Layout
const footerDom = footer && (footer.length < 2 ? footer(props) : footer(props, { direction }));
const listCls = classNames(prefixCls, {
[`${prefixCls}-with-pagination`]: !!pagination,
[`${prefixCls}-with-footer`]: !!footerDom,
});
// ====================== Get filtered, checked item list ======================
const listFooter = footerDom ? <div className={`${prefixCls}-footer`}>{footerDom}</div> : null;
const checkAllCheckbox = !showRemove && !pagination && checkBox;
let items: MenuProps['items'];
if (showRemove) {
items = [
/* Remove Current Page */
pagination
? {
key: 'removeCurrent',
label: removeCurrent,
onClick() {
const pageKeys = getEnabledItemKeys(
(listBodyRef.current?.items || []).map((entity) => entity.item),
);
onItemRemove?.(pageKeys);
},
}
: null,
/* Remove All */
{
key: 'removeAll',
label: removeAll,
onClick() {
onItemRemove?.(getEnabledItemKeys(filteredItems));
},
},
].filter(Boolean);
} else {
items = [
{
key: 'selectAll',
label: checkStatus === 'all' ? deselectAll : selectAll,
onClick() {
const keys = getEnabledItemKeys(filteredItems);
onItemSelectAll?.(keys, keys.length !== checkedKeys.length);
},
},
pagination
? {
key: 'selectCurrent',
label: selectCurrent,
onClick() {
const pageItems = listBodyRef.current?.items || [];
onItemSelectAll?.(getEnabledItemKeys(pageItems.map((entity) => entity.item)), true);
},
}
: null,
{
key: 'selectInvert',
label: selectInvert,
onClick() {
const availablePageItemKeys = getEnabledItemKeys(
(listBodyRef.current?.items || []).map((entity) => entity.item),
);
const checkedKeySet = new Set(checkedKeys);
const newCheckedKeysSet = new Set(checkedKeySet);
availablePageItemKeys.forEach((key) => {
if (checkedKeySet.has(key)) {
newCheckedKeysSet.delete(key);
} else {
newCheckedKeysSet.add(key);
}
});
onItemSelectAll?.(Array.from(newCheckedKeysSet), 'replace');
},
},
];
}
const dropdown: React.ReactNode = (
<Dropdown className={`${prefixCls}-header-dropdown`} menu={{ items }} disabled={disabled}>
{isValidIcon(selectionsIcon) ? selectionsIcon : <DownOutlined />}
</Dropdown>
);
return (
<div className={listCls} style={style}>
{/* Header */}
<div className={`${prefixCls}-header`}>
{showSelectAll ? (
<>
{checkAllCheckbox}
{dropdown}
</>
) : null}
<span className={`${prefixCls}-header-selected`}>
{getSelectAllLabel(checkedKeys.length, filteredItems.length)}
</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span>
</div>
{listBody}
{listFooter}
</div>
);
};
if (process.env.NODE_ENV !== 'production') {
TransferList.displayName = 'TransferList';
}
export default TransferList;

View File

@ -0,0 +1,66 @@
import * as React from 'react';
import LeftOutlined from '@ant-design/icons/LeftOutlined';
import RightOutlined from '@ant-design/icons/RightOutlined';
import Button from '../button';
import type { DirectionType } from '../config-provider';
export interface TransferOperationProps {
className?: string;
leftArrowText?: string;
rightArrowText?: string;
moveToLeft?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
moveToRight?: React.MouseEventHandler<HTMLButtonElement | HTMLAnchorElement>;
leftActive?: boolean;
rightActive?: boolean;
style?: React.CSSProperties;
disabled?: boolean;
direction?: DirectionType;
oneWay?: boolean;
}
const Operation: React.FC<TransferOperationProps> = (props) => {
const {
disabled,
moveToLeft,
moveToRight,
leftArrowText = '',
rightArrowText = '',
leftActive,
rightActive,
className,
style,
direction,
oneWay,
} = props;
return (
<div className={className} style={style}>
<Button
type="primary"
size="small"
disabled={disabled || !rightActive}
onClick={moveToRight}
icon={direction !== 'rtl' ? <RightOutlined /> : <LeftOutlined />}
>
{rightArrowText}
</Button>
{!oneWay && (
<Button
type="primary"
size="small"
disabled={disabled || !leftActive}
onClick={moveToLeft}
icon={direction !== 'rtl' ? <LeftOutlined /> : <RightOutlined />}
>
{leftArrowText}
</Button>
)}
</div>
);
};
if (process.env.NODE_ENV !== 'production') {
Operation.displayName = 'Operation';
}
export default Operation;

View File

@ -0,0 +1,45 @@
import * as React from 'react';
import SearchOutlined from '@ant-design/icons/SearchOutlined';
import Input from '../input';
export interface TransferSearchProps {
prefixCls?: string;
placeholder?: string;
onChange?: (e: React.FormEvent<HTMLElement>) => void;
handleClear?: () => void;
value?: string;
disabled?: boolean;
}
const Search: React.FC<TransferSearchProps> = (props) => {
const { placeholder = '', value, prefixCls, disabled, onChange, handleClear } = props;
const handleChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
if (e.target.value === '') {
handleClear?.();
}
},
[onChange],
);
return (
<Input
placeholder={placeholder}
className={prefixCls}
value={value}
onChange={handleChange}
disabled={disabled}
allowClear
prefix={<SearchOutlined />}
/>
);
};
if (process.env.NODE_ENV !== 'production') {
Search.displayName = 'Search';
}
export default Search;

View File

@ -0,0 +1,407 @@
import { unit } from '@ant-design/cssinjs';
import type { CSSObject } from '@ant-design/cssinjs';
import { resetComponent, resetIcon, textEllipsis } from '../../style';
import type { FullToken, GenerateStyle, GetDefaultToken } from '../../theme/internal';
import { genStyleHooks, mergeToken } from '../../theme/internal';
export interface ComponentToken {
/**
* @desc
* @descEN Width of list
*/
listWidth: number;
/**
* @desc
* @descEN Width of large list
*/
listWidthLG: number;
/**
* @desc
* @descEN Height of list
*/
listHeight: number;
/**
* @desc
* @descEN Height of list item
*/
itemHeight: number;
/**
* @desc
* @descEN Vertical padding of list item
*/
itemPaddingBlock: number;
/**
* @desc
* @descEN Height of header
*/
headerHeight: number;
}
interface TransferToken extends FullToken<'Transfer'> {
transferHeaderVerticalPadding: number;
}
const genTransferCustomizeStyle: GenerateStyle<TransferToken> = (
token: TransferToken,
): CSSObject => {
const { antCls, componentCls, listHeight, controlHeightLG } = token;
const tableCls = `${antCls}-table`;
const inputCls = `${antCls}-input`;
return {
[`${componentCls}-customize-list`]: {
[`${componentCls}-list`]: {
flex: '1 1 50%',
width: 'auto',
height: 'auto',
minHeight: listHeight,
},
// =================== Hook Components ===================
[`${tableCls}-wrapper`]: {
[`${tableCls}-small`]: {
border: 0,
borderRadius: 0,
[`${tableCls}-selection-column`]: {
width: controlHeightLG,
minWidth: controlHeightLG,
},
},
[`${tableCls}-pagination${tableCls}-pagination`]: {
margin: 0,
padding: token.paddingXS,
},
},
[`${inputCls}[disabled]`]: {
backgroundColor: 'transparent',
},
},
};
};
const genTransferStatusColor = (token: TransferToken, color: string): CSSObject => {
const { componentCls, colorBorder } = token;
return {
[`${componentCls}-list`]: {
borderColor: color,
'&-search:not([disabled])': {
borderColor: colorBorder,
},
},
};
};
const genTransferStatusStyle: GenerateStyle<TransferToken> = (token: TransferToken): CSSObject => {
const { componentCls } = token;
return {
[`${componentCls}-status-error`]: {
...genTransferStatusColor(token, token.colorError),
},
[`${componentCls}-status-warning`]: {
...genTransferStatusColor(token, token.colorWarning),
},
};
};
const genTransferListStyle: GenerateStyle<TransferToken> = (token: TransferToken): CSSObject => {
const {
componentCls,
colorBorder,
colorSplit,
lineWidth,
itemHeight,
headerHeight,
transferHeaderVerticalPadding,
itemPaddingBlock,
controlItemBgActive,
colorTextDisabled,
listHeight,
listWidth,
listWidthLG,
fontSizeIcon,
marginXS,
paddingSM,
lineType,
antCls,
iconCls,
motionDurationSlow,
controlItemBgHover,
borderRadiusLG,
colorBgContainer,
colorText,
controlItemBgActiveHover,
} = token;
return {
display: 'flex',
flexDirection: 'column',
width: listWidth,
height: listHeight,
border: `${unit(lineWidth)} ${lineType} ${colorBorder}`,
borderRadius: token.borderRadiusLG,
'&-with-pagination': {
width: listWidthLG,
height: 'auto',
},
'&-search': {
[`${iconCls}-search`]: {
color: colorTextDisabled,
},
},
'&-header': {
display: 'flex',
flex: 'none',
alignItems: 'center',
height: headerHeight,
// border-top is on the transfer dom. We should minus 1px for this
padding: `${unit(token.calc(transferHeaderVerticalPadding).sub(lineWidth).equal())} ${unit(
paddingSM,
)} ${unit(transferHeaderVerticalPadding)}`,
color: colorText,
background: colorBgContainer,
borderBottom: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
borderRadius: `${unit(borderRadiusLG)} ${unit(borderRadiusLG)} 0 0`,
'> *:not(:last-child)': {
marginInlineEnd: 4, // This is magic and fixed number, DO NOT use token since it may change.
},
'> *': {
flex: 'none',
},
'&-title': {
...textEllipsis,
flex: 'auto',
textAlign: 'end',
},
'&-dropdown': {
...resetIcon(),
fontSize: fontSizeIcon,
transform: 'translateY(10%)',
cursor: 'pointer',
'&[disabled]': {
cursor: 'not-allowed',
},
},
},
'&-body': {
display: 'flex',
flex: 'auto',
flexDirection: 'column',
fontSize: token.fontSize,
// https://blog.csdn.net/qq449245884/article/details/107373672/
minHeight: 0,
'&-search-wrapper': {
position: 'relative',
flex: 'none',
padding: paddingSM,
},
},
'&-content': {
flex: 'auto',
margin: 0,
padding: 0,
overflow: 'auto',
listStyle: 'none',
'&-item': {
display: 'flex',
alignItems: 'center',
minHeight: itemHeight,
padding: `${unit(itemPaddingBlock)} ${unit(paddingSM)}`,
transition: `all ${motionDurationSlow}`,
'> *:not(:last-child)': {
marginInlineEnd: marginXS,
},
'> *': {
flex: 'none',
},
'&-text': {
...textEllipsis,
flex: 'auto',
},
'&-remove': {
position: 'relative',
color: colorBorder,
cursor: 'pointer',
transition: `all ${motionDurationSlow}`,
'&:hover': {
color: token.colorLinkHover,
},
'&::after': {
position: 'absolute',
inset: `-${unit(itemPaddingBlock)} -50%`,
content: '""',
},
},
[`&:not(${componentCls}-list-content-item-disabled)`]: {
'&:hover': {
backgroundColor: controlItemBgHover,
cursor: 'pointer',
},
[`&${componentCls}-list-content-item-checked:hover`]: {
backgroundColor: controlItemBgActiveHover,
},
},
'&-checked': {
backgroundColor: controlItemBgActive,
},
'&-disabled': {
color: colorTextDisabled,
cursor: 'not-allowed',
},
},
// Do not change hover style when `oneWay` mode
[`&-show-remove ${componentCls}-list-content-item:not(${componentCls}-list-content-item-disabled):hover`]:
{
background: 'transparent',
cursor: 'default',
},
},
'&-pagination': {
padding: token.paddingXS,
textAlign: 'end',
borderTop: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
[`${antCls}-pagination-options`]: {
paddingInlineEnd: token.paddingXS,
},
},
'&-body-not-found': {
flex: 'none',
width: '100%',
margin: 'auto 0',
color: colorTextDisabled,
textAlign: 'center',
},
'&-footer': {
borderTop: `${unit(lineWidth)} ${lineType} ${colorSplit}`,
},
// fix: https://github.com/ant-design/ant-design/issues/44489
'&-checkbox': {
lineHeight: 1,
},
};
};
const genTransferStyle: GenerateStyle<TransferToken> = (token: TransferToken): CSSObject => {
const {
antCls,
iconCls,
componentCls,
marginXS,
marginXXS,
fontSizeIcon,
colorBgContainerDisabled,
} = token;
return {
[componentCls]: {
...resetComponent(token),
position: 'relative',
display: 'flex',
alignItems: 'stretch',
[`${componentCls}-disabled`]: {
[`${componentCls}-list`]: {
background: colorBgContainerDisabled,
},
},
[`${componentCls}-list`]: genTransferListStyle(token),
[`${componentCls}-operation`]: {
display: 'flex',
flex: 'none',
flexDirection: 'column',
alignSelf: 'center',
margin: `0 ${unit(marginXS)}`,
verticalAlign: 'middle',
[`${antCls}-btn`]: {
display: 'block',
'&:first-child': {
marginBottom: marginXXS,
},
[iconCls]: {
fontSize: fontSizeIcon,
},
},
},
},
};
};
const genTransferRTLStyle: GenerateStyle<TransferToken> = (token: TransferToken): CSSObject => {
const { componentCls } = token;
return {
[`${componentCls}-rtl`]: {
direction: 'rtl',
},
};
};
export const prepareComponentToken: GetDefaultToken<'Transfer'> = (token) => {
const { fontSize, lineHeight, controlHeight, controlHeightLG, lineWidth } = token;
const fontHeight = Math.round(fontSize * lineHeight);
return {
listWidth: 180,
listHeight: 200,
listWidthLG: 250,
headerHeight: controlHeightLG,
itemHeight: controlHeight,
itemPaddingBlock: (controlHeight - fontHeight) / 2,
transferHeaderVerticalPadding: Math.ceil((controlHeightLG - lineWidth - fontHeight) / 2),
};
};
// ============================== Export ==============================
export default genStyleHooks(
'Transfer',
(token) => {
const transferToken = mergeToken<TransferToken>(token);
return [
genTransferStyle(transferToken),
genTransferCustomizeStyle(transferToken),
genTransferStatusStyle(transferToken),
genTransferRTLStyle(transferToken),
];
},
prepareComponentToken,
);

View File

@ -197,6 +197,7 @@ const Tree = React.forwardRef<RcTree, TreeProps>((props, ref) => {
const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
const [, token] = useToken();
// @ts-ignore
const itemHeight = token.paddingXS / 2 + (token.Tree?.titleHeight || token.controlHeightSM);
const draggableConfig = React.useMemo(() => {

View File

@ -88,13 +88,16 @@ const getDropIndicatorStyle = (prefixCls: string, token: DerivativeToken) => ({
});
// =============================== Base ===============================
// @ts-ignore
type TreeToken = FullToken<'Tree'> & {
treeCls: string;
treeNodeCls: string;
treeNodePadding: number | string;
};
// @ts-ignore
export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject => {
// @ts-ignore
const { treeCls, treeNodeCls, treeNodePadding, titleHeight, nodeSelectedBg, nodeHoverBg } = token;
const treeCheckBoxMarginHorizontal = token.paddingXS;
@ -418,7 +421,9 @@ export const genDirectoryStyle = (token: TreeToken): CSSObject => {
treeCls,
treeNodeCls,
treeNodePadding,
// @ts-ignore
directoryNodeSelectedBg,
// @ts-ignore
directoryNodeSelectedColor,
} = token;
@ -530,6 +535,7 @@ export const initComponentToken = (token: AliasToken): TreeSharedToken => {
};
};
// @ts-ignore
export const prepareComponentToken: GetDefaultToken<'Tree'> = (token) => {
const { colorTextLightSolid, colorPrimary } = token;
@ -541,11 +547,13 @@ export const prepareComponentToken: GetDefaultToken<'Tree'> = (token) => {
};
export default genStyleHooks(
// @ts-ignore
'Tree',
(token, { prefixCls }) => [
{
[token.componentCls]: getCheckboxStyle(`${prefixCls}-checkbox`, token),
},
// @ts-ignore
genTreeStyle(prefixCls, token),
genCollapseMotion(token),
],

View File

@ -9,7 +9,7 @@ import CSSMotion, { CSSMotionList } from 'rc-motion';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
import initCollapseMotion from '../../_util/motion';
import { cloneElement, isValidElement } from '../../_util/reactNode';
import { cloneElement } from '../../_util/reactNode';
import type { ButtonProps } from '../../button';
import Button from '../../button';
import { ConfigContext } from '../../config-provider';
@ -133,7 +133,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<UploadListRef, UploadLi
title,
onClick: (e: React.MouseEvent<HTMLElement>) => {
callback();
if (isValidElement(customIcon) && customIcon.props.onClick) {
if (React.isValidElement(customIcon) && customIcon.props.onClick) {
customIcon.props.onClick(e);
}
},
@ -142,7 +142,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<UploadListRef, UploadLi
if (acceptUploadDisabled) {
btnProps.disabled = disabled;
}
if (isValidElement(customIcon)) {
if (React.isValidElement(customIcon)) {
const btnIcon = cloneElement(customIcon, {
...customIcon.props,
onClick: () => {},

View File

@ -1,72 +0,0 @@
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : String(i); }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import { message } from 'antd';
export var reqConfig = function reqConfig(config) {
var _ref = config || {},
authorization = _ref.authorization,
_ref$showMsg = _ref.showMsg,
showMsg = _ref$showMsg === void 0 ? true : _ref$showMsg,
onError = _ref.onError,
onExpired = _ref.onExpired,
_ref$expiredCodes = _ref.expiredCodes,
expiredCodes = _ref$expiredCodes === void 0 ? [403, 401, 203] : _ref$expiredCodes;
return _objectSpread({
timeout: 1000,
baseURL: location.origin,
errorConfig: {
// @ts-ignore
errorHandler: function errorHandler(response) {
var _ref2 = (response === null || response === void 0 ? void 0 : response.data) || {},
code = _ref2.code,
resMessage = _ref2.message;
var msg = resMessage || '请求失败!';
if (response.status !== 200) {
// 服务器错误
msg = '网络异常';
onError === null || onError === void 0 || onError(response);
} else if (expiredCodes.includes(code)) {
// 登录失效
msg = '登录过期,请重新登录';
onExpired === null || onExpired === void 0 || onExpired(response);
}
showMsg && message.error(msg);
},
// @ts-ignore
errorThrower: function errorThrower(res) {
console.log('报错啦:', res);
}
},
// @ts-ignore
responseInterceptors: [
// 一个二元组,第一个元素是 request 拦截器,第二个元素是错误处理
[function (response) {
var _ref3 = (response === null || response === void 0 ? void 0 : response.data) || {},
code = _ref3.code;
if (code !== 200) {
// 报错捕捉
return Promise.reject(response);
}
return (response === null || response === void 0 ? void 0 : response.data) || {};
}]],
// 请求
requestInterceptors: [
// 一个二元组,第一个元素是 request 拦截器,第二个元素是错误处理
[function (url, options) {
var Authorization = authorization || localStorage.getItem('ZHST_AUTH_TOKEN') || '';
return {
url: url,
options: _objectSpread(_objectSpread({}, options), {}, {
headers: {
Authorization: Authorization
}
})
};
}, function (error) {
return Promise.reject(error);
}]]
}, config);
};