fix(ts): 添加ts配置
This commit is contained in:
parent
079df27f4b
commit
f5ff6e760f
@ -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;
|
||||
}
|
||||
|
@ -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 = {
|
||||
|
@ -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`;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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]);
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -31,6 +31,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
||||
...floatButtonProps
|
||||
} = props;
|
||||
|
||||
// @ts-ignore
|
||||
const { direction, getPrefixCls, floatButtonGroup } =
|
||||
useContext<ConfigConsumerProps>(ConfigContext);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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 />
|
||||
|
@ -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 };
|
||||
|
@ -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`;
|
||||
|
@ -156,6 +156,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
|
||||
}
|
||||
classNames={{
|
||||
input: inputNumberClass,
|
||||
// @ts-ignore
|
||||
variant: classNames(
|
||||
{
|
||||
[`${prefixCls}-${variant}`]: enableVariantCls,
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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 =====================
|
||||
|
@ -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"
|
||||
|
@ -865,6 +865,7 @@ export default genStyleHooks(
|
||||
},
|
||||
initComponentToken,
|
||||
{
|
||||
// @ts-ignore
|
||||
resetFont: false,
|
||||
},
|
||||
);
|
||||
|
@ -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'],
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -7,10 +7,12 @@ const getHorizontalStyle: GenerateStyle<MenuToken> = (token) => {
|
||||
const {
|
||||
componentCls,
|
||||
motionDurationSlow,
|
||||
// @ts-ignore
|
||||
horizontalLineHeight,
|
||||
colorSplit,
|
||||
lineWidth,
|
||||
lineType,
|
||||
// @ts-ignore
|
||||
itemPaddingInline,
|
||||
} = token;
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
|
152
packages/meta/src/transfer/ListBody.tsx
Normal file
152
packages/meta/src/transfer/ListBody.tsx
Normal 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);
|
85
packages/meta/src/transfer/ListItem.tsx
Normal file
85
packages/meta/src/transfer/ListItem.tsx
Normal 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
8253
packages/meta/src/transfer/__tests__/__snapshots__/demo.test.ts.snap
Normal file
8253
packages/meta/src/transfer/__tests__/__snapshots__/demo.test.ts.snap
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
`;
|
@ -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>
|
||||
`;
|
64
packages/meta/src/transfer/__tests__/customize.test.tsx
Normal file
64
packages/meta/src/transfer/__tests__/customize.test.tsx
Normal 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.',
|
||||
);
|
||||
});
|
||||
});
|
3
packages/meta/src/transfer/__tests__/demo-extend.test.ts
Normal file
3
packages/meta/src/transfer/__tests__/demo-extend.test.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { extendTest } from '../../../tests/shared/demoTest';
|
||||
|
||||
extendTest('transfer');
|
3
packages/meta/src/transfer/__tests__/demo.test.ts
Normal file
3
packages/meta/src/transfer/__tests__/demo.test.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import demoTest from '../../../tests/shared/demoTest';
|
||||
|
||||
demoTest('transfer');
|
145
packages/meta/src/transfer/__tests__/dropdown.test.tsx
Normal file
145
packages/meta/src/transfer/__tests__/dropdown.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
5
packages/meta/src/transfer/__tests__/image.test.ts
Normal file
5
packages/meta/src/transfer/__tests__/image.test.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { imageDemoTest } from '../../../tests/shared/imageTest';
|
||||
|
||||
describe('Transfer image', () => {
|
||||
imageDemoTest('transfer');
|
||||
});
|
850
packages/meta/src/transfer/__tests__/index.test.tsx
Normal file
850
packages/meta/src/transfer/__tests__/index.test.tsx
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
104
packages/meta/src/transfer/__tests__/list.test.tsx
Normal file
104
packages/meta/src/transfer/__tests__/list.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
122
packages/meta/src/transfer/__tests__/search.test.tsx
Normal file
122
packages/meta/src/transfer/__tests__/search.test.tsx
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
9
packages/meta/src/transfer/demo/advanced.md
Normal file
9
packages/meta/src/transfer/demo/advanced.md
Normal 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.
|
75
packages/meta/src/transfer/demo/advanced.tsx
Normal file
75
packages/meta/src/transfer/demo/advanced.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/basic.md
Normal file
7
packages/meta/src/transfer/demo/basic.md
Normal 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.
|
58
packages/meta/src/transfer/demo/basic.tsx
Normal file
58
packages/meta/src/transfer/demo/basic.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/component-token.md
Normal file
7
packages/meta/src/transfer/demo/component-token.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
Component Token Debug.
|
||||
|
||||
## en-US
|
||||
|
||||
Component Token Debug.
|
215
packages/meta/src/transfer/demo/component-token.tsx
Normal file
215
packages/meta/src/transfer/demo/component-token.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/custom-item.md
Normal file
7
packages/meta/src/transfer/demo/custom-item.md
Normal 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.
|
71
packages/meta/src/transfer/demo/custom-item.tsx
Normal file
71
packages/meta/src/transfer/demo/custom-item.tsx
Normal 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;
|
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
自定义穿梭框全选按钮的文字。
|
||||
|
||||
## en-US
|
||||
|
||||
Custom the labels for select all checkboxes.
|
37
packages/meta/src/transfer/demo/custom-select-all-labels.tsx
Normal file
37
packages/meta/src/transfer/demo/custom-select-all-labels.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/large-data.md
Normal file
7
packages/meta/src/transfer/demo/large-data.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
大数据下使用分页。
|
||||
|
||||
## en-US
|
||||
|
||||
large count of items with pagination.
|
63
packages/meta/src/transfer/demo/large-data.tsx
Normal file
63
packages/meta/src/transfer/demo/large-data.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/oneWay.md
Normal file
7
packages/meta/src/transfer/demo/oneWay.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
通过 `oneWay` 将 Transfer 转为单向样式。
|
||||
|
||||
## en-US
|
||||
|
||||
Use `oneWay` to makes Transfer to one way style.
|
78
packages/meta/src/transfer/demo/oneWay.tsx
Normal file
78
packages/meta/src/transfer/demo/oneWay.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/search.md
Normal file
7
packages/meta/src/transfer/demo/search.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
带搜索框的穿梭框,可以自定义搜索函数。
|
||||
|
||||
## en-US
|
||||
|
||||
Transfer with a search box.
|
63
packages/meta/src/transfer/demo/search.tsx
Normal file
63
packages/meta/src/transfer/demo/search.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/status.md
Normal file
7
packages/meta/src/transfer/demo/status.md
Normal 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`.
|
11
packages/meta/src/transfer/demo/status.tsx
Normal file
11
packages/meta/src/transfer/demo/status.tsx
Normal 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;
|
7
packages/meta/src/transfer/demo/table-transfer.md
Normal file
7
packages/meta/src/transfer/demo/table-transfer.md
Normal file
@ -0,0 +1,7 @@
|
||||
## zh-CN
|
||||
|
||||
使用 Table 组件作为自定义渲染列表。
|
||||
|
||||
## en-US
|
||||
|
||||
Customize render list with Table component.
|
139
packages/meta/src/transfer/demo/table-transfer.tsx
Normal file
139
packages/meta/src/transfer/demo/table-transfer.tsx
Normal 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;
|
14
packages/meta/src/transfer/demo/tree-transfer.md
Normal file
14
packages/meta/src/transfer/demo/tree-transfer.md
Normal 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>
|
102
packages/meta/src/transfer/demo/tree-transfer.tsx
Normal file
102
packages/meta/src/transfer/demo/tree-transfer.tsx
Normal 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;
|
43
packages/meta/src/transfer/hooks/useData.ts
Normal file
43
packages/meta/src/transfer/hooks/useData.ts
Normal 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;
|
63
packages/meta/src/transfer/hooks/useSelection.ts
Normal file
63
packages/meta/src/transfer/hooks/useSelection.ts
Normal 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,
|
||||
];
|
||||
}
|
510
packages/meta/src/transfer/index.tsx
Normal file
510
packages/meta/src/transfer/index.tsx
Normal 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;
|
103
packages/meta/src/transfer/index.zh-CN.md
Normal file
103
packages/meta/src/transfer/index.zh-CN.md
Normal 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>
|
10
packages/meta/src/transfer/interface.ts
Normal file
10
packages/meta/src/transfer/interface.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export type TransferKey = React.Key;
|
||||
|
||||
export type PaginationType =
|
||||
| boolean
|
||||
| {
|
||||
pageSize?: number;
|
||||
simple?: boolean;
|
||||
showSizeChanger?: boolean;
|
||||
showLessItems?: boolean;
|
||||
};
|
397
packages/meta/src/transfer/list.tsx
Normal file
397
packages/meta/src/transfer/list.tsx
Normal 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;
|
66
packages/meta/src/transfer/operation.tsx
Normal file
66
packages/meta/src/transfer/operation.tsx
Normal 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;
|
45
packages/meta/src/transfer/search.tsx
Normal file
45
packages/meta/src/transfer/search.tsx
Normal 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;
|
407
packages/meta/src/transfer/style/index.ts
Normal file
407
packages/meta/src/transfer/style/index.ts
Normal 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,
|
||||
);
|
@ -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(() => {
|
||||
|
@ -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),
|
||||
],
|
||||
|
@ -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: () => {},
|
||||
|
@ -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);
|
||||
};
|
Loading…
Reference in New Issue
Block a user