Merge branch 'feat/ts-upgrade-20240603' into 'master'
fix(ts): 添加ts配置 See merge request web-project/zhst-lambo!47
This commit is contained in:
commit
5e483436b0
@ -167,8 +167,7 @@ var urlToBase64V2 = (url) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
function base64toBlob(base64) {
|
function base64toBlob(base64) {
|
||||||
if (!base64)
|
if (!base64) return;
|
||||||
return;
|
|
||||||
var arr = base64.split(","), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
var arr = base64.split(","), mime = arr[0].match(/:(.*?);/)[1], bstr = atob(arr[1]), n = bstr.length, u8arr = new Uint8Array(n);
|
||||||
while (n--) {
|
while (n--) {
|
||||||
u8arr[n] = bstr.charCodeAt(n);
|
u8arr[n] = bstr.charCodeAt(n);
|
||||||
@ -186,15 +185,13 @@ var downloadPackageImages = async (imgDataList, zipName) => {
|
|||||||
let src = imgDataList[i2].url;
|
let src = imgDataList[i2].url;
|
||||||
let suffix = src.substring(src.lastIndexOf("."));
|
let suffix = src.substring(src.lastIndexOf("."));
|
||||||
let base64ByUrl = await urlToBase64V2(imgDataList[i2].url);
|
let base64ByUrl = await urlToBase64V2(imgDataList[i2].url);
|
||||||
if (!base64ByUrl)
|
if (!base64ByUrl) continue;
|
||||||
continue;
|
|
||||||
let blob = base64toBlob(base64ByUrl);
|
let blob = base64toBlob(base64ByUrl);
|
||||||
imgDataDownLoadList.push(imgDataList[i2]);
|
imgDataDownLoadList.push(imgDataList[i2]);
|
||||||
imgBlobList.push(blob);
|
imgBlobList.push(blob);
|
||||||
imageSuffix.push(suffix);
|
imageSuffix.push(suffix);
|
||||||
}
|
}
|
||||||
if (imgBlobList.length === 0)
|
if (imgBlobList.length === 0) throw new Error("The number of pictures is zero !");
|
||||||
throw new Error("The number of pictures is zero !");
|
|
||||||
if (imgBlobList.length > 0) {
|
if (imgBlobList.length > 0) {
|
||||||
for (var i = 0; i < imgBlobList.length; i++) {
|
for (var i = 0; i < imgBlobList.length; i++) {
|
||||||
img == null ? void 0 : img.file(
|
img == null ? void 0 : img.file(
|
||||||
@ -215,17 +212,12 @@ var downloadPackageImages = async (imgDataList, zipName) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
function getFileSize(size) {
|
function getFileSize(size) {
|
||||||
if (!size)
|
if (!size) return "";
|
||||||
return "";
|
|
||||||
var num = 1024;
|
var num = 1024;
|
||||||
if (size < num)
|
if (size < num) return size + "B";
|
||||||
return size + "B";
|
if (size < Math.pow(num, 2)) return (size / num).toFixed(2) + "K";
|
||||||
if (size < Math.pow(num, 2))
|
if (size < Math.pow(num, 3)) return (size / Math.pow(num, 2)).toFixed(2) + "M";
|
||||||
return (size / num).toFixed(2) + "K";
|
if (size < Math.pow(num, 4)) return (size / Math.pow(num, 3)).toFixed(2) + "G";
|
||||||
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";
|
return (size / Math.pow(num, 4)).toFixed(2) + "T";
|
||||||
}
|
}
|
||||||
var dataURLToBlob = (dataurl) => {
|
var dataURLToBlob = (dataurl) => {
|
||||||
@ -242,8 +234,7 @@ var dataURLToBlob = (dataurl) => {
|
|||||||
var generateImg = (_imgKey, host = "http://10.0.0.120") => {
|
var generateImg = (_imgKey, host = "http://10.0.0.120") => {
|
||||||
let imgKey = _imgKey;
|
let imgKey = _imgKey;
|
||||||
let imgUrl = "";
|
let imgUrl = "";
|
||||||
if (!imgKey)
|
if (!imgKey) return "";
|
||||||
return "";
|
|
||||||
if (/(http|https):\/\/([\w.]+\/?)\S*/ig.test(imgKey)) {
|
if (/(http|https):\/\/([\w.]+\/?)\S*/ig.test(imgKey)) {
|
||||||
return imgKey;
|
return imgKey;
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,7 @@ var setNumberAccuracy = (originNumber, accuracy = 0, isCeil = true) => {
|
|||||||
var toRealNumber = (number) => {
|
var toRealNumber = (number) => {
|
||||||
if (isNaN(number) || number === Infinity) {
|
if (isNaN(number) || number === Infinity) {
|
||||||
return 0;
|
return 0;
|
||||||
} else
|
} else return number;
|
||||||
return number;
|
|
||||||
};
|
};
|
||||||
// Annotate the CommonJS export names for ESM import in node:
|
// Annotate the CommonJS export names for ESM import in node:
|
||||||
0 && (module.exports = {
|
0 && (module.exports = {
|
||||||
|
@ -23,8 +23,7 @@ __export(performance_exports, {
|
|||||||
});
|
});
|
||||||
module.exports = __toCommonJS(performance_exports);
|
module.exports = __toCommonJS(performance_exports);
|
||||||
var speedConvert = (bps, contertUnit = 8) => {
|
var speedConvert = (bps, contertUnit = 8) => {
|
||||||
if (bps === void 0)
|
if (bps === void 0) return `0KB/s`;
|
||||||
return `0KB/s`;
|
|
||||||
const byte = bps / contertUnit;
|
const byte = bps / contertUnit;
|
||||||
if (bps > 1024 * 1024 * 1024) {
|
if (bps > 1024 * 1024 * 1024) {
|
||||||
return `${(byte / 1024 / 1024 / 1024).toFixed(2)}GB/s`;
|
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;
|
var realLength = 0, len = str.length, charCode = -1;
|
||||||
for (var i = 0; i < len; i++) {
|
for (var i = 0; i < len; i++) {
|
||||||
charCode = str.charCodeAt(i);
|
charCode = str.charCodeAt(i);
|
||||||
if (charCode >= 0 && charCode <= 128)
|
if (charCode >= 0 && charCode <= 128) realLength += 1;
|
||||||
realLength += 1;
|
else realLength += 2;
|
||||||
else
|
|
||||||
realLength += 2;
|
|
||||||
}
|
}
|
||||||
return realLength;
|
return realLength;
|
||||||
};
|
};
|
||||||
|
@ -84,8 +84,7 @@ function getChromeVersion() {
|
|||||||
const arr = navigator.userAgent.split(" ");
|
const arr = navigator.userAgent.split(" ");
|
||||||
let chromeVersion = "";
|
let chromeVersion = "";
|
||||||
for (let i = 0; i < arr.length; i++) {
|
for (let i = 0; i < arr.length; i++) {
|
||||||
if (/chrome/i.test(arr[i]))
|
if (/chrome/i.test(arr[i])) chromeVersion = arr[i];
|
||||||
chromeVersion = arr[i];
|
|
||||||
}
|
}
|
||||||
if (chromeVersion) {
|
if (chromeVersion) {
|
||||||
return Number(chromeVersion.split("/")[1].split(".")[0]);
|
return Number(chromeVersion.split("/")[1].split(".")[0]);
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"react-map-gl": "^7.1.7"
|
"react-map-gl": "^7.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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 { unit } from '@ant-design/cssinjs';
|
||||||
import { TinyColor } from '@ctrl/tinycolor';
|
import { TinyColor } from '@ctrl/tinycolor';
|
||||||
|
|
||||||
import type { SharedComponentToken, SharedInputToken } from '../../input/style';
|
import type { SharedComponentToken, SharedInputToken } from '../../input/style/token';
|
||||||
import {
|
import {
|
||||||
genActiveStyle,
|
genActiveStyle,
|
||||||
genBasicInputStyle,
|
genBasicInputStyle,
|
||||||
|
@ -32,9 +32,12 @@ export interface ComponentToken extends ArrowToken, ArrowOffsetToken {
|
|||||||
paddingBlock: CSSProperties['paddingBlock'];
|
paddingBlock: CSSProperties['paddingBlock'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export interface DropdownToken extends FullToken<'Dropdown'> {
|
export interface DropdownToken extends FullToken<'Dropdown'> {
|
||||||
dropdownArrowDistance: number | string;
|
dropdownArrowDistance: number | string;
|
||||||
|
zIndexPopup?: number;
|
||||||
dropdownEdgeChildPadding: number;
|
dropdownEdgeChildPadding: number;
|
||||||
|
paddingBlock?: number | string;
|
||||||
menuCls: string;
|
menuCls: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +161,7 @@ const genBaseStyle: GenerateStyle<DropdownToken> = (token) => {
|
|||||||
// =============================================================
|
// =============================================================
|
||||||
// == Arrow style ==
|
// == Arrow style ==
|
||||||
// =============================================================
|
// =============================================================
|
||||||
|
// @ts-ignore
|
||||||
getArrowStyle<DropdownToken>(token, colorBgElevated, {
|
getArrowStyle<DropdownToken>(token, colorBgElevated, {
|
||||||
arrowPlacement: { top: true, bottom: true },
|
arrowPlacement: { top: true, bottom: true },
|
||||||
}),
|
}),
|
||||||
@ -343,6 +347,7 @@ const genBaseStyle: GenerateStyle<DropdownToken> = (token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================== Export ==============================
|
// ============================== Export ==============================
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'Dropdown'> = (token) => ({
|
export const prepareComponentToken: GetDefaultToken<'Dropdown'> = (token) => ({
|
||||||
zIndexPopup: token.zIndexPopupBase + 50,
|
zIndexPopup: token.zIndexPopupBase + 50,
|
||||||
paddingBlock: (token.controlHeight - token.fontSize * token.lineHeight) / 2,
|
paddingBlock: (token.controlHeight - token.fontSize * token.lineHeight) / 2,
|
||||||
@ -354,6 +359,7 @@ export const prepareComponentToken: GetDefaultToken<'Dropdown'> = (token) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default genStyleHooks(
|
export default genStyleHooks(
|
||||||
|
// @ts-ignore
|
||||||
'Dropdown',
|
'Dropdown',
|
||||||
(token) => {
|
(token) => {
|
||||||
const { marginXXS, sizePopupArrow, paddingXXS, componentCls } = token;
|
const { marginXXS, sizePopupArrow, paddingXXS, componentCls } = token;
|
||||||
|
@ -31,6 +31,7 @@ const FloatButtonGroup: React.FC<FloatButtonGroupProps> = (props) => {
|
|||||||
...floatButtonProps
|
...floatButtonProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const { direction, getPrefixCls, floatButtonGroup } =
|
const { direction, getPrefixCls, floatButtonGroup } =
|
||||||
useContext<ConfigConsumerProps>(ConfigContext);
|
useContext<ConfigConsumerProps>(ConfigContext);
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ export interface ComponentToken {
|
|||||||
dotOffsetInSquare: number;
|
dotOffsetInSquare: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
type FloatButtonToken = FullToken<'FloatButton'> & {
|
type FloatButtonToken = FullToken<'FloatButton'> & {
|
||||||
floatButtonColor: string;
|
floatButtonColor: string;
|
||||||
floatButtonBackgroundColor: string;
|
floatButtonBackgroundColor: string;
|
||||||
@ -218,8 +219,11 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
|
|||||||
floatButtonIconSize,
|
floatButtonIconSize,
|
||||||
floatButtonSize,
|
floatButtonSize,
|
||||||
borderRadiusLG,
|
borderRadiusLG,
|
||||||
|
// @ts-ignore
|
||||||
badgeOffset,
|
badgeOffset,
|
||||||
|
// @ts-ignore
|
||||||
dotOffsetInSquare,
|
dotOffsetInSquare,
|
||||||
|
// @ts-ignore
|
||||||
dotOffsetInCircle,
|
dotOffsetInCircle,
|
||||||
calc,
|
calc,
|
||||||
} = token;
|
} = token;
|
||||||
@ -366,12 +370,14 @@ const sharedFloatButtonStyle: GenerateStyle<FloatButtonToken, CSSObject> = (toke
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================== Export ==============================
|
// ============================== Export ==============================
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'FloatButton'> = (token) => ({
|
export const prepareComponentToken: GetDefaultToken<'FloatButton'> = (token) => ({
|
||||||
dotOffsetInCircle: getOffset(token.controlHeightLG / 2),
|
dotOffsetInCircle: getOffset(token.controlHeightLG / 2),
|
||||||
dotOffsetInSquare: getOffset(token.borderRadiusLG),
|
dotOffsetInSquare: getOffset(token.borderRadiusLG),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default genStyleHooks(
|
export default genStyleHooks(
|
||||||
|
// @ts-ignore
|
||||||
'FloatButton',
|
'FloatButton',
|
||||||
(token) => {
|
(token) => {
|
||||||
const {
|
const {
|
||||||
|
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { AlertFilled, CloseSquareFilled } from '@ant-design/icons';
|
import { AlertFilled, CloseSquareFilled } from '@ant-design/icons';
|
||||||
import { Button, Form, Input, Tooltip } from 'antd';
|
import { Button, Form, Input, Tooltip } from 'antd';
|
||||||
import { createStyles, css } from 'antd-style';
|
import { createStyles, css } from 'antd-style';
|
||||||
import uniqueId from 'lodash/uniqueId';
|
|
||||||
|
|
||||||
const useStyle = createStyles(() => ({
|
const useStyle = createStyles(() => ({
|
||||||
'custom-feedback-icons': css`
|
'custom-feedback-icons': css`
|
||||||
@ -25,7 +24,6 @@ const App: React.FC = () => {
|
|||||||
error: (
|
error: (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key="tooltipKey"
|
key="tooltipKey"
|
||||||
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
|
|
||||||
color="red"
|
color="red"
|
||||||
>
|
>
|
||||||
<CloseSquareFilled />
|
<CloseSquareFilled />
|
||||||
@ -54,7 +52,6 @@ const App: React.FC = () => {
|
|||||||
error: (
|
error: (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
key="tooltipKey"
|
key="tooltipKey"
|
||||||
title={errors?.map((error) => <div key={uniqueId()}>{error}</div>)}
|
|
||||||
color="pink"
|
color="pink"
|
||||||
>
|
>
|
||||||
<AlertFilled />
|
<AlertFilled />
|
||||||
|
@ -68,8 +68,10 @@ const Image: CompositionImage<ImageProps> = (props) => {
|
|||||||
transitionName: getTransitionName(rootPrefixCls, 'zoom', _preview.transitionName),
|
transitionName: getTransitionName(rootPrefixCls, 'zoom', _preview.transitionName),
|
||||||
maskTransitionName: getTransitionName(rootPrefixCls, 'fade', _preview.maskTransitionName),
|
maskTransitionName: getTransitionName(rootPrefixCls, 'fade', _preview.maskTransitionName),
|
||||||
zIndex,
|
zIndex,
|
||||||
|
// @ts-ignore
|
||||||
closeIcon: closeIcon ?? image?.preview?.closeIcon,
|
closeIcon: closeIcon ?? image?.preview?.closeIcon,
|
||||||
};
|
};
|
||||||
|
// @ts-ignore
|
||||||
}, [preview, imageLocale, image?.preview?.closeIcon]);
|
}, [preview, imageLocale, image?.preview?.closeIcon]);
|
||||||
|
|
||||||
const mergedStyle: React.CSSProperties = { ...image?.style, ...style };
|
const mergedStyle: React.CSSProperties = { ...image?.style, ...style };
|
||||||
|
@ -36,6 +36,7 @@ export interface ComponentToken {
|
|||||||
previewOperationColorDisabled: string;
|
previewOperationColorDisabled: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export interface ImageToken extends FullToken<'Image'> {
|
export interface ImageToken extends FullToken<'Image'> {
|
||||||
previewCls: string;
|
previewCls: string;
|
||||||
modalMaskBg: string;
|
modalMaskBg: string;
|
||||||
@ -85,7 +86,9 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
|
|||||||
marginXL,
|
marginXL,
|
||||||
margin,
|
margin,
|
||||||
paddingLG,
|
paddingLG,
|
||||||
|
// @ts-ignore
|
||||||
previewOperationColorDisabled,
|
previewOperationColorDisabled,
|
||||||
|
// @ts-ignore
|
||||||
previewOperationHoverColor,
|
previewOperationHoverColor,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
iconCls,
|
iconCls,
|
||||||
@ -107,6 +110,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
// @ts-ignore
|
||||||
color: token.previewOperationColor,
|
color: token.previewOperationColor,
|
||||||
},
|
},
|
||||||
[`${previewCls}-progress`]: {
|
[`${previewCls}-progress`]: {
|
||||||
@ -134,6 +138,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
[`& > ${iconCls}`]: {
|
[`& > ${iconCls}`]: {
|
||||||
|
// @ts-ignore
|
||||||
fontSize: token.previewOperationSize,
|
fontSize: token.previewOperationSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -165,6 +170,7 @@ export const genPreviewOperationsStyle = (token: ImageToken): CSSObject => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
[`& > ${iconCls}`]: {
|
[`& > ${iconCls}`]: {
|
||||||
|
// @ts-ignore
|
||||||
fontSize: token.previewOperationSize,
|
fontSize: token.previewOperationSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -176,8 +182,10 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
|
|||||||
const {
|
const {
|
||||||
modalMaskBg,
|
modalMaskBg,
|
||||||
iconCls,
|
iconCls,
|
||||||
|
// @ts-ignore
|
||||||
previewOperationColorDisabled,
|
previewOperationColorDisabled,
|
||||||
previewCls,
|
previewCls,
|
||||||
|
// @ts-ignore
|
||||||
zIndexPopup,
|
zIndexPopup,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
} = token;
|
} = token;
|
||||||
@ -196,6 +204,7 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
|
|||||||
width: token.imagePreviewSwitchSize,
|
width: token.imagePreviewSwitchSize,
|
||||||
height: token.imagePreviewSwitchSize,
|
height: token.imagePreviewSwitchSize,
|
||||||
marginTop: token.calc(token.imagePreviewSwitchSize).mul(-1).div(2).equal(),
|
marginTop: token.calc(token.imagePreviewSwitchSize).mul(-1).div(2).equal(),
|
||||||
|
// @ts-ignore
|
||||||
color: token.previewOperationColor,
|
color: token.previewOperationColor,
|
||||||
background: operationBg.toRgbString(),
|
background: operationBg.toRgbString(),
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
@ -219,6 +228,7 @@ export const genPreviewSwitchStyle = (token: ImageToken): CSSObject => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
[`> ${iconCls}`]: {
|
[`> ${iconCls}`]: {
|
||||||
|
// @ts-ignore
|
||||||
fontSize: token.previewOperationSize,
|
fontSize: token.previewOperationSize,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -299,6 +309,7 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
|||||||
{
|
{
|
||||||
[`${componentCls}-preview-root`]: {
|
[`${componentCls}-preview-root`]: {
|
||||||
[`${previewCls}-wrap`]: {
|
[`${previewCls}-wrap`]: {
|
||||||
|
// @ts-ignore
|
||||||
zIndex: token.zIndexPopup,
|
zIndex: token.zIndexPopup,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -308,6 +319,7 @@ export const genImagePreviewStyle: GenerateStyle<ImageToken> = (token: ImageToke
|
|||||||
{
|
{
|
||||||
[`${componentCls}-preview-operations-wrapper`]: {
|
[`${componentCls}-preview-operations-wrapper`]: {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
|
// @ts-ignore
|
||||||
zIndex: token.calc(token.zIndexPopup).add(1).equal({ unit: false }),
|
zIndex: token.calc(token.zIndexPopup).add(1).equal({ unit: false }),
|
||||||
},
|
},
|
||||||
'&': [genPreviewOperationsStyle(token), genPreviewSwitchStyle(token)],
|
'&': [genPreviewOperationsStyle(token), genPreviewSwitchStyle(token)],
|
||||||
@ -358,6 +370,7 @@ const genPreviewMotion: GenerateStyle<ImageToken> = (token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================== Export ==============================
|
// ============================== Export ==============================
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'Image'> = (token) => ({
|
export const prepareComponentToken: GetDefaultToken<'Image'> = (token) => ({
|
||||||
zIndexPopup: token.zIndexPopupBase + 80,
|
zIndexPopup: token.zIndexPopupBase + 80,
|
||||||
previewOperationColor: new TinyColor(token.colorTextLightSolid).setAlpha(0.65).toRgbString(),
|
previewOperationColor: new TinyColor(token.colorTextLightSolid).setAlpha(0.65).toRgbString(),
|
||||||
@ -369,6 +382,7 @@ export const prepareComponentToken: GetDefaultToken<'Image'> = (token) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default genStyleHooks(
|
export default genStyleHooks(
|
||||||
|
// @ts-ignore
|
||||||
'Image',
|
'Image',
|
||||||
(token) => {
|
(token) => {
|
||||||
const previewCls = `${token.componentCls}-preview`;
|
const previewCls = `${token.componentCls}-preview`;
|
||||||
|
@ -156,6 +156,7 @@ const InputNumber = React.forwardRef<HTMLInputElement, InputNumberProps>((props,
|
|||||||
}
|
}
|
||||||
classNames={{
|
classNames={{
|
||||||
input: inputNumberClass,
|
input: inputNumberClass,
|
||||||
|
// @ts-ignore
|
||||||
variant: classNames(
|
variant: classNames(
|
||||||
{
|
{
|
||||||
[`${prefixCls}-${variant}`]: enableVariantCls,
|
[`${prefixCls}-${variant}`]: enableVariantCls,
|
||||||
|
@ -60,6 +60,7 @@ export interface ComponentToken extends SharedComponentToken {
|
|||||||
export type InputNumberToken = FullToken<'InputNumber'> & SharedInputToken;
|
export type InputNumberToken = FullToken<'InputNumber'> & SharedInputToken;
|
||||||
|
|
||||||
export const prepareComponentToken: GetDefaultToken<'InputNumber'> = (token) => {
|
export const prepareComponentToken: GetDefaultToken<'InputNumber'> = (token) => {
|
||||||
|
// @ts-ignore
|
||||||
const handleVisible = token.handleVisible ?? 'auto';
|
const handleVisible = token.handleVisible ?? 'auto';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -180,6 +180,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
|
|||||||
</NoCompactStyle>
|
</NoCompactStyle>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const mergedAllowClear = getAllowClear(allowClear ?? input?.allowClear);
|
const mergedAllowClear = getAllowClear(allowClear ?? input?.allowClear);
|
||||||
|
|
||||||
const [variant, enableVariantCls] = useVariant(customVariant, bordered);
|
const [variant, enableVariantCls] = useVariant(customVariant, bordered);
|
||||||
@ -221,6 +222,7 @@ const Input = forwardRef<InputRef, InputProps>((props, ref) => {
|
|||||||
input?.classNames?.input,
|
input?.classNames?.input,
|
||||||
hashId,
|
hashId,
|
||||||
),
|
),
|
||||||
|
// @ts-ignore
|
||||||
variant: classNames(
|
variant: classNames(
|
||||||
{
|
{
|
||||||
[`${prefixCls}-${variant}`]: enableVariantCls,
|
[`${prefixCls}-${variant}`]: enableVariantCls,
|
||||||
|
@ -62,6 +62,7 @@ const TextArea = forwardRef<TextAreaRef, TextAreaProps>((props, ref) => {
|
|||||||
deprecated(!('bordered' in props), 'bordered', 'variant');
|
deprecated(!('bordered' in props), 'bordered', 'variant');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const { getPrefixCls, direction, textArea } = React.useContext(ConfigContext);
|
const { getPrefixCls, direction, textArea } = React.useContext(ConfigContext);
|
||||||
|
|
||||||
// ===================== Size =====================
|
// ===================== Size =====================
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Flex, Input, Typography } from 'antd';
|
import { Flex, Input, Typography } from 'antd';
|
||||||
import { runes } from 'runes2';
|
|
||||||
|
|
||||||
const App: React.FC = () => (
|
const App: React.FC = () => (
|
||||||
<Flex vertical gap={16}>
|
<Flex vertical gap={16}>
|
||||||
@ -20,7 +19,6 @@ const App: React.FC = () => (
|
|||||||
<Input
|
<Input
|
||||||
count={{
|
count={{
|
||||||
show: true,
|
show: true,
|
||||||
strategy: (txt) => runes(txt).length,
|
|
||||||
}}
|
}}
|
||||||
defaultValue="🔥🔥🔥"
|
defaultValue="🔥🔥🔥"
|
||||||
/>
|
/>
|
||||||
@ -32,7 +30,6 @@ const App: React.FC = () => (
|
|||||||
count={{
|
count={{
|
||||||
show: true,
|
show: true,
|
||||||
max: 6,
|
max: 6,
|
||||||
strategy: (txt) => runes(txt).length,
|
|
||||||
exceedFormatter: (txt, { max }) => runes(txt).slice(0, max).join(''),
|
exceedFormatter: (txt, { max }) => runes(txt).slice(0, max).join(''),
|
||||||
}}
|
}}
|
||||||
defaultValue="🔥 antd"
|
defaultValue="🔥 antd"
|
||||||
|
@ -865,6 +865,7 @@ export default genStyleHooks(
|
|||||||
},
|
},
|
||||||
initComponentToken,
|
initComponentToken,
|
||||||
{
|
{
|
||||||
|
// @ts-ignore
|
||||||
resetFont: false,
|
resetFont: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -96,6 +96,7 @@ export interface ComponentToken {
|
|||||||
lightTriggerColor: string;
|
lightTriggerColor: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export interface LayoutToken extends FullToken<'Layout'> {}
|
export interface LayoutToken extends FullToken<'Layout'> {}
|
||||||
|
|
||||||
const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
||||||
@ -103,22 +104,35 @@ const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
|||||||
antCls, // .ant
|
antCls, // .ant
|
||||||
componentCls, // .ant-layout
|
componentCls, // .ant-layout
|
||||||
colorText,
|
colorText,
|
||||||
|
// @ts-ignore
|
||||||
triggerColor,
|
triggerColor,
|
||||||
|
// @ts-ignore
|
||||||
footerBg,
|
footerBg,
|
||||||
|
// @ts-ignore
|
||||||
triggerBg,
|
triggerBg,
|
||||||
|
// @ts-ignore
|
||||||
headerHeight,
|
headerHeight,
|
||||||
|
// @ts-ignore
|
||||||
headerPadding,
|
headerPadding,
|
||||||
|
// @ts-ignore
|
||||||
headerColor,
|
headerColor,
|
||||||
|
// @ts-ignore
|
||||||
footerPadding,
|
footerPadding,
|
||||||
|
// @ts-ignore
|
||||||
triggerHeight,
|
triggerHeight,
|
||||||
|
// @ts-ignore
|
||||||
zeroTriggerHeight,
|
zeroTriggerHeight,
|
||||||
|
// @ts-ignore
|
||||||
zeroTriggerWidth,
|
zeroTriggerWidth,
|
||||||
motionDurationMid,
|
motionDurationMid,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
fontSize,
|
fontSize,
|
||||||
borderRadius,
|
borderRadius,
|
||||||
|
// @ts-ignore
|
||||||
bodyBg,
|
bodyBg,
|
||||||
|
// @ts-ignore
|
||||||
headerBg,
|
headerBg,
|
||||||
|
// @ts-ignore
|
||||||
siderBg,
|
siderBg,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -280,6 +294,7 @@ const genLayoutStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'Layout'> = (token) => {
|
export const prepareComponentToken: GetDefaultToken<'Layout'> = (token) => {
|
||||||
const {
|
const {
|
||||||
colorBgLayout,
|
colorBgLayout,
|
||||||
@ -320,6 +335,7 @@ export const prepareComponentToken: GetDefaultToken<'Layout'> = (token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ============================== Export ==============================
|
// ============================== Export ==============================
|
||||||
|
// @ts-ignore
|
||||||
export default genStyleHooks('Layout', (token) => [genLayoutStyle(token)], prepareComponentToken, {
|
export default genStyleHooks('Layout', (token) => [genLayoutStyle(token)], prepareComponentToken, {
|
||||||
deprecatedTokens: [
|
deprecatedTokens: [
|
||||||
['colorBgBody', 'bodyBg'],
|
['colorBgBody', 'bodyBg'],
|
||||||
|
@ -4,6 +4,7 @@ import type { LayoutToken } from '.';
|
|||||||
import type { GenerateStyle } from '../../theme/internal';
|
import type { GenerateStyle } from '../../theme/internal';
|
||||||
|
|
||||||
const genLayoutLightStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
const genLayoutLightStyle: GenerateStyle<LayoutToken, CSSObject> = (token) => {
|
||||||
|
// @ts-ignore
|
||||||
const { componentCls, bodyBg, lightSiderBg, lightTriggerBg, lightTriggerColor } = token;
|
const { componentCls, bodyBg, lightSiderBg, lightTriggerBg, lightTriggerColor } = token;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -27,6 +27,7 @@ function isEmptyIcon(icon?: React.ReactNode) {
|
|||||||
return icon === null || icon === false;
|
return icon === null || icon === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const MENU_COMPONENTS: GetProp<RcMenuProps, '_internalComponents'> = {
|
const MENU_COMPONENTS: GetProp<RcMenuProps, '_internalComponents'> = {
|
||||||
item: MenuItem,
|
item: MenuItem,
|
||||||
submenu: SubMenu,
|
submenu: SubMenu,
|
||||||
@ -141,16 +142,21 @@ const InternalMenu = forwardRef<RcMenuRef, InternalMenuProps>((props, ref) => {
|
|||||||
if (typeof overrideObj.expandIcon === 'function' || isEmptyIcon(overrideObj.expandIcon)) {
|
if (typeof overrideObj.expandIcon === 'function' || isEmptyIcon(overrideObj.expandIcon)) {
|
||||||
return overrideObj.expandIcon || null;
|
return overrideObj.expandIcon || null;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
if (typeof menu?.expandIcon === 'function' || isEmptyIcon(menu?.expandIcon)) {
|
if (typeof menu?.expandIcon === 'function' || isEmptyIcon(menu?.expandIcon)) {
|
||||||
|
// @ts-ignore
|
||||||
return menu?.expandIcon || null;
|
return menu?.expandIcon || null;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
const mergedIcon = expandIcon ?? overrideObj?.expandIcon ?? menu?.expandIcon;
|
const mergedIcon = expandIcon ?? overrideObj?.expandIcon ?? menu?.expandIcon;
|
||||||
return cloneElement(mergedIcon, {
|
return cloneElement(mergedIcon, {
|
||||||
className: classNames(
|
className: classNames(
|
||||||
`${prefixCls}-submenu-expand-icon`,
|
`${prefixCls}-submenu-expand-icon`,
|
||||||
|
// @ts-ignore
|
||||||
React.isValidElement(mergedIcon) ? mergedIcon.props?.className : undefined,
|
React.isValidElement(mergedIcon) ? mergedIcon.props?.className : undefined,
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
// @ts-ignore
|
||||||
}, [expandIcon, overrideObj?.expandIcon, menu?.expandIcon, prefixCls]);
|
}, [expandIcon, overrideObj?.expandIcon, menu?.expandIcon, prefixCls]);
|
||||||
|
|
||||||
// ======================== Context ==========================
|
// ======================== Context ==========================
|
||||||
@ -198,6 +204,7 @@ const InternalMenu = forwardRef<RcMenuRef, InternalMenuProps>((props, ref) => {
|
|||||||
cssVarCls,
|
cssVarCls,
|
||||||
rootCls,
|
rootCls,
|
||||||
)}
|
)}
|
||||||
|
// @ts-ignore
|
||||||
_internalComponents={MENU_COMPONENTS}
|
_internalComponents={MENU_COMPONENTS}
|
||||||
/>
|
/>
|
||||||
</MenuContext.Provider>
|
</MenuContext.Provider>
|
||||||
|
@ -7,10 +7,12 @@ const getHorizontalStyle: GenerateStyle<MenuToken> = (token) => {
|
|||||||
const {
|
const {
|
||||||
componentCls,
|
componentCls,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
|
// @ts-ignore
|
||||||
horizontalLineHeight,
|
horizontalLineHeight,
|
||||||
colorSplit,
|
colorSplit,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
lineType,
|
lineType,
|
||||||
|
// @ts-ignore
|
||||||
itemPaddingInline,
|
itemPaddingInline,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
|
@ -369,6 +369,7 @@ export interface ComponentToken {
|
|||||||
itemWidth: string;
|
itemWidth: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export interface MenuToken extends FullToken<'Menu'> {
|
export interface MenuToken extends FullToken<'Menu'> {
|
||||||
menuHorizontalHeight: number | string;
|
menuHorizontalHeight: number | string;
|
||||||
menuArrowSize: number | string;
|
menuArrowSize: number | string;
|
||||||
@ -385,7 +386,9 @@ const genMenuItemStyle = (token: MenuToken): CSSObject => {
|
|||||||
motionEaseInOut,
|
motionEaseInOut,
|
||||||
motionEaseOut,
|
motionEaseOut,
|
||||||
iconCls,
|
iconCls,
|
||||||
|
// @ts-ignore
|
||||||
iconSize,
|
iconSize,
|
||||||
|
// @ts-ignore
|
||||||
iconMarginInlineEnd,
|
iconMarginInlineEnd,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -521,13 +524,17 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
|
|||||||
padding,
|
padding,
|
||||||
colorSplit,
|
colorSplit,
|
||||||
lineWidth,
|
lineWidth,
|
||||||
|
// @ts-ignore
|
||||||
zIndexPopup,
|
zIndexPopup,
|
||||||
borderRadiusLG,
|
borderRadiusLG,
|
||||||
|
// @ts-ignore
|
||||||
subMenuItemBorderRadius,
|
subMenuItemBorderRadius,
|
||||||
menuArrowSize,
|
menuArrowSize,
|
||||||
menuArrowOffset,
|
menuArrowOffset,
|
||||||
lineType,
|
lineType,
|
||||||
|
// @ts-ignore
|
||||||
groupTitleLineHeight,
|
groupTitleLineHeight,
|
||||||
|
// @ts-ignore
|
||||||
groupTitleFontSize,
|
groupTitleFontSize,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -577,6 +584,7 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
[`${componentCls}-item, ${componentCls}-submenu, ${componentCls}-submenu-title`]: {
|
[`${componentCls}-item, ${componentCls}-submenu, ${componentCls}-submenu-title`]: {
|
||||||
|
// @ts-ignore
|
||||||
borderRadius: token.itemBorderRadius,
|
borderRadius: token.itemBorderRadius,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -804,6 +812,7 @@ const getBaseStyle: GenerateStyle<MenuToken> = (token) => {
|
|||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'Menu'> = (token) => {
|
export const prepareComponentToken: GetDefaultToken<'Menu'> = (token) => {
|
||||||
const {
|
const {
|
||||||
colorPrimary,
|
colorPrimary,
|
||||||
@ -936,27 +945,44 @@ export const prepareComponentToken: GetDefaultToken<'Menu'> = (token) => {
|
|||||||
// ============================== Export ==============================
|
// ============================== Export ==============================
|
||||||
export default (prefixCls: string, rootCls: string = prefixCls, injectStyle: boolean = true) => {
|
export default (prefixCls: string, rootCls: string = prefixCls, injectStyle: boolean = true) => {
|
||||||
const useStyle = genStyleHooks(
|
const useStyle = genStyleHooks(
|
||||||
|
// @ts-ignore
|
||||||
'Menu',
|
'Menu',
|
||||||
(token) => {
|
(token) => {
|
||||||
const {
|
const {
|
||||||
colorBgElevated,
|
colorBgElevated,
|
||||||
controlHeightLG,
|
controlHeightLG,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
// @ts-ignore
|
||||||
darkItemColor,
|
darkItemColor,
|
||||||
|
// @ts-ignore
|
||||||
darkDangerItemColor,
|
darkDangerItemColor,
|
||||||
|
// @ts-ignore
|
||||||
darkItemBg,
|
darkItemBg,
|
||||||
|
// @ts-ignore
|
||||||
darkSubMenuItemBg,
|
darkSubMenuItemBg,
|
||||||
|
// @ts-ignore
|
||||||
darkItemSelectedColor,
|
darkItemSelectedColor,
|
||||||
|
// @ts-ignore
|
||||||
darkItemSelectedBg,
|
darkItemSelectedBg,
|
||||||
|
// @ts-ignore
|
||||||
darkDangerItemSelectedBg,
|
darkDangerItemSelectedBg,
|
||||||
|
// @ts-ignore
|
||||||
darkItemHoverBg,
|
darkItemHoverBg,
|
||||||
|
// @ts-ignore
|
||||||
darkGroupTitleColor,
|
darkGroupTitleColor,
|
||||||
|
// @ts-ignore
|
||||||
darkItemHoverColor,
|
darkItemHoverColor,
|
||||||
|
// @ts-ignore
|
||||||
darkItemDisabledColor,
|
darkItemDisabledColor,
|
||||||
|
// @ts-ignore
|
||||||
darkDangerItemHoverColor,
|
darkDangerItemHoverColor,
|
||||||
|
// @ts-ignore
|
||||||
darkDangerItemSelectedColor,
|
darkDangerItemSelectedColor,
|
||||||
|
// @ts-ignore
|
||||||
darkDangerItemActiveBg,
|
darkDangerItemActiveBg,
|
||||||
|
// @ts-ignore
|
||||||
popupBg,
|
popupBg,
|
||||||
|
// @ts-ignore
|
||||||
darkPopupBg,
|
darkPopupBg,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -969,10 +995,12 @@ export default (prefixCls: string, rootCls: string = prefixCls, injectStyle: boo
|
|||||||
menuArrowOffset: token.calc(menuArrowSize).mul(0.25).equal(),
|
menuArrowOffset: token.calc(menuArrowSize).mul(0.25).equal(),
|
||||||
menuSubMenuBg: colorBgElevated,
|
menuSubMenuBg: colorBgElevated,
|
||||||
calc: token.calc,
|
calc: token.calc,
|
||||||
|
// @ts-ignore
|
||||||
popupBg,
|
popupBg,
|
||||||
});
|
});
|
||||||
|
|
||||||
const menuDarkToken = mergeToken<MenuToken>(menuToken, {
|
const menuDarkToken = mergeToken<MenuToken>(menuToken, {
|
||||||
|
// @ts-ignore
|
||||||
itemColor: darkItemColor,
|
itemColor: darkItemColor,
|
||||||
itemHoverColor: darkItemHoverColor,
|
itemHoverColor: darkItemHoverColor,
|
||||||
groupTitleColor: darkGroupTitleColor,
|
groupTitleColor: darkGroupTitleColor,
|
||||||
|
@ -11,43 +11,67 @@ const accessibilityFocus = (token: MenuToken) => ({
|
|||||||
const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation => {
|
const getThemeStyle = (token: MenuToken, themeSuffix: string): CSSInterpolation => {
|
||||||
const {
|
const {
|
||||||
componentCls,
|
componentCls,
|
||||||
|
// @ts-ignore
|
||||||
itemColor,
|
itemColor,
|
||||||
|
// @ts-ignore
|
||||||
itemSelectedColor,
|
itemSelectedColor,
|
||||||
|
// @ts-ignore
|
||||||
groupTitleColor,
|
groupTitleColor,
|
||||||
|
// @ts-ignore
|
||||||
itemBg,
|
itemBg,
|
||||||
|
// @ts-ignore
|
||||||
subMenuItemBg,
|
subMenuItemBg,
|
||||||
|
// @ts-ignore
|
||||||
itemSelectedBg,
|
itemSelectedBg,
|
||||||
|
// @ts-ignore
|
||||||
activeBarHeight,
|
activeBarHeight,
|
||||||
|
// @ts-ignore
|
||||||
activeBarWidth,
|
activeBarWidth,
|
||||||
|
// @ts-ignore
|
||||||
activeBarBorderWidth,
|
activeBarBorderWidth,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
motionEaseInOut,
|
motionEaseInOut,
|
||||||
motionEaseOut,
|
motionEaseOut,
|
||||||
|
// @ts-ignore
|
||||||
itemPaddingInline,
|
itemPaddingInline,
|
||||||
motionDurationMid,
|
motionDurationMid,
|
||||||
|
// @ts-ignore
|
||||||
itemHoverColor,
|
itemHoverColor,
|
||||||
lineType,
|
lineType,
|
||||||
colorSplit,
|
colorSplit,
|
||||||
|
|
||||||
// Disabled
|
// Disabled
|
||||||
|
// @ts-ignore
|
||||||
itemDisabledColor,
|
itemDisabledColor,
|
||||||
|
|
||||||
// Danger
|
// Danger
|
||||||
|
// @ts-ignore
|
||||||
dangerItemColor,
|
dangerItemColor,
|
||||||
|
// @ts-ignore
|
||||||
dangerItemHoverColor,
|
dangerItemHoverColor,
|
||||||
|
// @ts-ignore
|
||||||
dangerItemSelectedColor,
|
dangerItemSelectedColor,
|
||||||
|
// @ts-ignore
|
||||||
dangerItemActiveBg,
|
dangerItemActiveBg,
|
||||||
|
// @ts-ignore
|
||||||
dangerItemSelectedBg,
|
dangerItemSelectedBg,
|
||||||
// Bg
|
// Bg
|
||||||
|
// @ts-ignore
|
||||||
popupBg,
|
popupBg,
|
||||||
|
// @ts-ignore
|
||||||
itemHoverBg,
|
itemHoverBg,
|
||||||
|
// @ts-ignore
|
||||||
itemActiveBg,
|
itemActiveBg,
|
||||||
menuSubMenuBg,
|
menuSubMenuBg,
|
||||||
|
|
||||||
// Horizontal
|
// Horizontal
|
||||||
|
// @ts-ignore
|
||||||
horizontalItemSelectedColor,
|
horizontalItemSelectedColor,
|
||||||
|
// @ts-ignore
|
||||||
horizontalItemSelectedBg,
|
horizontalItemSelectedBg,
|
||||||
|
// @ts-ignore
|
||||||
horizontalItemBorderRadius,
|
horizontalItemBorderRadius,
|
||||||
|
// @ts-ignore
|
||||||
horizontalItemHoverBg,
|
horizontalItemHoverBg,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
|
@ -8,12 +8,16 @@ import type { GenerateStyle } from '../../theme/internal';
|
|||||||
const getVerticalInlineStyle: GenerateStyle<MenuToken, CSSObject> = (token) => {
|
const getVerticalInlineStyle: GenerateStyle<MenuToken, CSSObject> = (token) => {
|
||||||
const {
|
const {
|
||||||
componentCls,
|
componentCls,
|
||||||
|
// @ts-ignore
|
||||||
itemHeight,
|
itemHeight,
|
||||||
|
// @ts-ignore
|
||||||
itemMarginInline,
|
itemMarginInline,
|
||||||
padding,
|
padding,
|
||||||
menuArrowSize,
|
menuArrowSize,
|
||||||
marginXS,
|
marginXS,
|
||||||
|
// @ts-ignore
|
||||||
itemMarginBlock,
|
itemMarginBlock,
|
||||||
|
// @ts-ignore
|
||||||
itemWidth,
|
itemWidth,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -53,19 +57,24 @@ const getVerticalStyle: GenerateStyle<MenuToken> = (token) => {
|
|||||||
const {
|
const {
|
||||||
componentCls,
|
componentCls,
|
||||||
iconCls,
|
iconCls,
|
||||||
|
// @ts-ignore
|
||||||
itemHeight,
|
itemHeight,
|
||||||
colorTextLightSolid,
|
colorTextLightSolid,
|
||||||
|
// @ts-ignore
|
||||||
dropdownWidth,
|
dropdownWidth,
|
||||||
controlHeightLG,
|
controlHeightLG,
|
||||||
motionDurationMid,
|
motionDurationMid,
|
||||||
motionEaseOut,
|
motionEaseOut,
|
||||||
paddingXL,
|
paddingXL,
|
||||||
|
// @ts-ignore
|
||||||
itemMarginInline,
|
itemMarginInline,
|
||||||
fontSizeLG,
|
fontSizeLG,
|
||||||
motionDurationSlow,
|
motionDurationSlow,
|
||||||
paddingXS,
|
paddingXS,
|
||||||
boxShadowSecondary,
|
boxShadowSecondary,
|
||||||
|
// @ts-ignore
|
||||||
collapsedWidth,
|
collapsedWidth,
|
||||||
|
// @ts-ignore
|
||||||
collapsedIconSize,
|
collapsedIconSize,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
|
@ -108,7 +108,9 @@ const Modal: React.FC<ModalProps> = (props) => {
|
|||||||
const [mergedClosable, mergedCloseIcon] = useClosable(
|
const [mergedClosable, mergedCloseIcon] = useClosable(
|
||||||
closable,
|
closable,
|
||||||
closeIcon,
|
closeIcon,
|
||||||
|
// @ts-ignore
|
||||||
(icon) => renderCloseIcon(prefixCls, icon),
|
(icon) => renderCloseIcon(prefixCls, icon),
|
||||||
|
// @ts-ignore
|
||||||
<CloseOutlined className={`${prefixCls}-close-icon`} />,
|
<CloseOutlined className={`${prefixCls}-close-icon`} />,
|
||||||
true,
|
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 [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls);
|
||||||
const [, token] = useToken();
|
const [, token] = useToken();
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
const itemHeight = token.paddingXS / 2 + (token.Tree?.titleHeight || token.controlHeightSM);
|
const itemHeight = token.paddingXS / 2 + (token.Tree?.titleHeight || token.controlHeightSM);
|
||||||
|
|
||||||
const draggableConfig = React.useMemo(() => {
|
const draggableConfig = React.useMemo(() => {
|
||||||
|
@ -88,13 +88,16 @@ const getDropIndicatorStyle = (prefixCls: string, token: DerivativeToken) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// =============================== Base ===============================
|
// =============================== Base ===============================
|
||||||
|
// @ts-ignore
|
||||||
type TreeToken = FullToken<'Tree'> & {
|
type TreeToken = FullToken<'Tree'> & {
|
||||||
treeCls: string;
|
treeCls: string;
|
||||||
treeNodeCls: string;
|
treeNodeCls: string;
|
||||||
treeNodePadding: number | string;
|
treeNodePadding: number | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject => {
|
export const genBaseStyle = (prefixCls: string, token: TreeToken): CSSObject => {
|
||||||
|
// @ts-ignore
|
||||||
const { treeCls, treeNodeCls, treeNodePadding, titleHeight, nodeSelectedBg, nodeHoverBg } = token;
|
const { treeCls, treeNodeCls, treeNodePadding, titleHeight, nodeSelectedBg, nodeHoverBg } = token;
|
||||||
const treeCheckBoxMarginHorizontal = token.paddingXS;
|
const treeCheckBoxMarginHorizontal = token.paddingXS;
|
||||||
|
|
||||||
@ -418,7 +421,9 @@ export const genDirectoryStyle = (token: TreeToken): CSSObject => {
|
|||||||
treeCls,
|
treeCls,
|
||||||
treeNodeCls,
|
treeNodeCls,
|
||||||
treeNodePadding,
|
treeNodePadding,
|
||||||
|
// @ts-ignore
|
||||||
directoryNodeSelectedBg,
|
directoryNodeSelectedBg,
|
||||||
|
// @ts-ignore
|
||||||
directoryNodeSelectedColor,
|
directoryNodeSelectedColor,
|
||||||
} = token;
|
} = token;
|
||||||
|
|
||||||
@ -530,6 +535,7 @@ export const initComponentToken = (token: AliasToken): TreeSharedToken => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
export const prepareComponentToken: GetDefaultToken<'Tree'> = (token) => {
|
export const prepareComponentToken: GetDefaultToken<'Tree'> = (token) => {
|
||||||
const { colorTextLightSolid, colorPrimary } = token;
|
const { colorTextLightSolid, colorPrimary } = token;
|
||||||
|
|
||||||
@ -541,11 +547,13 @@ export const prepareComponentToken: GetDefaultToken<'Tree'> = (token) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default genStyleHooks(
|
export default genStyleHooks(
|
||||||
|
// @ts-ignore
|
||||||
'Tree',
|
'Tree',
|
||||||
(token, { prefixCls }) => [
|
(token, { prefixCls }) => [
|
||||||
{
|
{
|
||||||
[token.componentCls]: getCheckboxStyle(`${prefixCls}-checkbox`, token),
|
[token.componentCls]: getCheckboxStyle(`${prefixCls}-checkbox`, token),
|
||||||
},
|
},
|
||||||
|
// @ts-ignore
|
||||||
genTreeStyle(prefixCls, token),
|
genTreeStyle(prefixCls, token),
|
||||||
genCollapseMotion(token),
|
genCollapseMotion(token),
|
||||||
],
|
],
|
||||||
|
@ -9,7 +9,7 @@ import CSSMotion, { CSSMotionList } from 'rc-motion';
|
|||||||
|
|
||||||
import useForceUpdate from '../../_util/hooks/useForceUpdate';
|
import useForceUpdate from '../../_util/hooks/useForceUpdate';
|
||||||
import initCollapseMotion from '../../_util/motion';
|
import initCollapseMotion from '../../_util/motion';
|
||||||
import { cloneElement, isValidElement } from '../../_util/reactNode';
|
import { cloneElement } from '../../_util/reactNode';
|
||||||
import type { ButtonProps } from '../../button';
|
import type { ButtonProps } from '../../button';
|
||||||
import Button from '../../button';
|
import Button from '../../button';
|
||||||
import { ConfigContext } from '../../config-provider';
|
import { ConfigContext } from '../../config-provider';
|
||||||
@ -133,7 +133,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<UploadListRef, UploadLi
|
|||||||
title,
|
title,
|
||||||
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
onClick: (e: React.MouseEvent<HTMLElement>) => {
|
||||||
callback();
|
callback();
|
||||||
if (isValidElement(customIcon) && customIcon.props.onClick) {
|
if (React.isValidElement(customIcon) && customIcon.props.onClick) {
|
||||||
customIcon.props.onClick(e);
|
customIcon.props.onClick(e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -142,7 +142,7 @@ const InternalUploadList: React.ForwardRefRenderFunction<UploadListRef, UploadLi
|
|||||||
if (acceptUploadDisabled) {
|
if (acceptUploadDisabled) {
|
||||||
btnProps.disabled = disabled;
|
btnProps.disabled = disabled;
|
||||||
}
|
}
|
||||||
if (isValidElement(customIcon)) {
|
if (React.isValidElement(customIcon)) {
|
||||||
const btnIcon = cloneElement(customIcon, {
|
const btnIcon = cloneElement(customIcon, {
|
||||||
...customIcon.props,
|
...customIcon.props,
|
||||||
onClick: () => {},
|
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