Merge pull request #21226 from ant-design/feature

chore: Merge feature into master
This commit is contained in:
偏右 2020-02-04 22:59:21 +08:00 committed by GitHub
commit 74f01d8485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1023 additions and 195 deletions

View File

@ -0,0 +1,18 @@
import * as React from 'react';
export default function usePatchElement(): [
React.ReactElement[],
(element: React.ReactElement) => Function,
] {
const [elements, setElements] = React.useState<React.ReactElement[]>([]);
function patchElement(element: React.ReactElement) {
setElements(originElements => [...originElements, element]);
return () => {
setElements(originElements => originElements.filter(ele => ele !== element));
};
}
return [elements, patchElement];
}

View File

@ -85,6 +85,7 @@ Form field component for data bidirectional binding, validation, layout, and so
| rules | Rules for field validation. Click [here](#components-form-demo-basic) to see an example | [Rule](#Rule)[] | - |
| shouldUpdate | Custom field update logic. See [bellow](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false |
| trigger | When to collect the value of children node | string | onChange |
| validateFirst | Whether stop validate on first rule of error for this field | boolean | false |
| validateStatus | The validation status. If not provided, it will be generated by validation rule. options: 'success' 'warning' 'error' 'validating' | string | - |
| validateTrigger | When to validate the value of children node | string \| string[] | onChange |
| valuePropName | Props of children node, for example, the prop of Switch is 'checked' | string | 'value' |

View File

@ -86,6 +86,7 @@ const validateMessages = {
| rules | 校验规则,设置字段的校验逻辑。点击[此处](#components-form-demo-basic)查看示例 | [Rule](#Rule)[] | - |
| shouldUpdate | 自定义字段更新逻辑,说明[见下](#shouldUpdate) | boolean \| (prevValue, curValue) => boolean | false |
| trigger | 设置收集字段值变更的时机 | string | onChange |
| validateFirst | 当某一规则校验不通过时,是否停止剩下的规则的校验 | boolean | false |
| validateStatus | 校验状态,如不设置,则会根据校验规则自动生成,可选:'success' 'warning' 'error' 'validating' | string | - |
| validateTrigger | 设置字段校验的时机 | string \| string[] | onChange |
| valuePropName | 子节点的值的属性,如 Switch 的是 'checked' | string | 'value' |

View File

@ -0,0 +1,117 @@
import * as React from 'react';
import classNames from 'classnames';
import Dialog, { ModalFuncProps } from './Modal';
import ActionButton from './ActionButton';
import warning from '../_util/warning';
interface ConfirmDialogProps extends ModalFuncProps {
afterClose?: () => void;
close: (...args: any[]) => void;
autoFocusButton?: null | 'ok' | 'cancel';
}
const ConfirmDialog = (props: ConfirmDialogProps) => {
const {
icon,
onCancel,
onOk,
close,
zIndex,
afterClose,
visible,
keyboard,
centered,
getContainer,
maskStyle,
okText,
okButtonProps,
cancelText,
cancelButtonProps,
} = props;
warning(
!(typeof icon === 'string' && icon.length > 2),
'Modal',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
// 支持传入{ icon: null }来隐藏`Modal.confirm`默认的Icon
const okType = props.okType || 'primary';
const prefixCls = props.prefixCls || 'ant-modal';
const contentPrefixCls = `${prefixCls}-confirm`;
// 默认为 true保持向下兼容
const okCancel = 'okCancel' in props ? props.okCancel! : true;
const width = props.width || 416;
const style = props.style || {};
const mask = props.mask === undefined ? true : props.mask;
// 默认为 false保持旧版默认行为
const maskClosable = props.maskClosable === undefined ? false : props.maskClosable;
const autoFocusButton = props.autoFocusButton === null ? false : props.autoFocusButton || 'ok';
const transitionName = props.transitionName || 'zoom';
const maskTransitionName = props.maskTransitionName || 'fade';
const classString = classNames(
contentPrefixCls,
`${contentPrefixCls}-${props.type}`,
props.className,
);
const cancelButton = okCancel && (
<ActionButton
actionFn={onCancel}
closeModal={close}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
>
{cancelText}
</ActionButton>
);
return (
<Dialog
prefixCls={prefixCls}
className={classString}
wrapClassName={classNames({ [`${contentPrefixCls}-centered`]: !!props.centered })}
onCancel={() => close({ triggerCancel: true })}
visible={visible}
title=""
transitionName={transitionName}
footer=""
maskTransitionName={maskTransitionName}
mask={mask}
maskClosable={maskClosable}
maskStyle={maskStyle}
style={style}
width={width}
zIndex={zIndex}
afterClose={afterClose}
keyboard={keyboard}
centered={centered}
getContainer={getContainer}
>
<div className={`${contentPrefixCls}-body-wrapper`}>
<div className={`${contentPrefixCls}-body`}>
{icon}
{props.title === undefined ? null : (
<span className={`${contentPrefixCls}-title`}>{props.title}</span>
)}
<div className={`${contentPrefixCls}-content`}>{props.content}</div>
</div>
<div className={`${contentPrefixCls}-btns`}>
{cancelButton}
<ActionButton
type={okType}
actionFn={onOk}
closeModal={close}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
>
{okText}
</ActionButton>
</div>
</div>
</Dialog>
);
};
export default ConfirmDialog;

View File

@ -4,6 +4,7 @@ import classNames from 'classnames';
import addEventListener from 'rc-util/lib/Dom/addEventListener';
import { CloseOutlined } from '@ant-design/icons';
import useModal from './useModal';
import { getConfirmLocale } from './locale';
import Button from '../button';
import { ButtonType, NativeButtonProps } from '../button/button';
@ -113,13 +114,6 @@ export interface ModalFuncProps {
maskTransitionName?: string;
}
export type ModalFunc = (
props: ModalFuncProps,
) => {
destroy: () => void;
update: (newConfig: ModalFuncProps) => void;
};
export interface ModalLocale {
okText: string;
cancelText: string;
@ -127,20 +121,10 @@ export interface ModalLocale {
}
export default class Modal extends React.Component<ModalProps, {}> {
static info: ModalFunc;
static success: ModalFunc;
static error: ModalFunc;
static warn: ModalFunc;
static warning: ModalFunc;
static confirm: ModalFunc;
static destroyAll: () => void;
static useModal = useModal;
static defaultProps = {
width: 520,
transitionName: 'zoom',

View File

@ -0,0 +1,54 @@
import React from 'react';
import { mount } from 'enzyme';
import Modal from '..';
import Button from '../../button';
jest.mock('rc-util/lib/Portal');
describe('Modal.hook', () => {
it('hooks support context', () => {
jest.useFakeTimers();
const Context = React.createContext('light');
let instance;
const Demo = () => {
const [modal, contextHolder] = Modal.useModal();
return (
<Context.Provider value="bamboo">
<Button
onClick={() => {
instance = modal.confirm({
content: (
<Context.Consumer>
{name => <div className="test-hook">{name}</div>}
</Context.Consumer>
),
});
}}
/>
{contextHolder}
</Context.Provider>
);
};
const wrapper = mount(<Demo />);
wrapper.find('button').simulate('click');
expect(wrapper.find('.test-hook').text()).toEqual('bamboo');
// Update instance
instance.update({
content: <div className="updated-content" />,
});
wrapper.update();
expect(wrapper.find('.updated-content')).toHaveLength(1);
// Destroy
instance.destroy();
jest.runAllTimers();
wrapper.update();
expect(wrapper.find('Modal')).toHaveLength(0);
jest.useRealTimers();
});
});

View File

@ -1,125 +1,31 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import classNames from 'classnames';
import Dialog, { ModalFuncProps, destroyFns } from './Modal';
import ActionButton from './ActionButton';
import {
InfoCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import { getConfirmLocale } from './locale';
import warning from '../_util/warning';
import { ModalFuncProps, destroyFns } from './Modal';
import ConfirmDialog from './ConfirmDialog';
interface ConfirmDialogProps extends ModalFuncProps {
afterClose?: () => void;
close: (...args: any[]) => void;
autoFocusButton?: null | 'ok' | 'cancel';
}
const IS_REACT_16 = !!ReactDOM.createPortal;
const ConfirmDialog = (props: ConfirmDialogProps) => {
const {
icon,
onCancel,
onOk,
close,
zIndex,
afterClose,
visible,
keyboard,
centered,
getContainer,
maskStyle,
okButtonProps,
cancelButtonProps,
} = props;
warning(
!(typeof icon === 'string' && icon.length > 2),
'Modal',
`\`icon\` is using ReactNode instead of string naming in v4. Please check \`${icon}\` at https://ant.design/components/icon`,
);
// 支持传入{ icon: null }来隐藏`Modal.confirm`默认的Icon
const okType = props.okType || 'primary';
const prefixCls = props.prefixCls || 'ant-modal';
const contentPrefixCls = `${prefixCls}-confirm`;
// 默认为 true保持向下兼容
const okCancel = 'okCancel' in props ? props.okCancel! : true;
const width = props.width || 416;
const style = props.style || {};
const mask = props.mask === undefined ? true : props.mask;
// 默认为 false保持旧版默认行为
const maskClosable = props.maskClosable === undefined ? false : props.maskClosable;
const runtimeLocale = getConfirmLocale();
const okText = props.okText || (okCancel ? runtimeLocale.okText : runtimeLocale.justOkText);
const cancelText = props.cancelText || runtimeLocale.cancelText;
const autoFocusButton = props.autoFocusButton === null ? false : props.autoFocusButton || 'ok';
const transitionName = props.transitionName || 'zoom';
const maskTransitionName = props.maskTransitionName || 'fade';
const classString = classNames(
contentPrefixCls,
`${contentPrefixCls}-${props.type}`,
props.className,
);
const cancelButton = okCancel && (
<ActionButton
actionFn={onCancel}
closeModal={close}
autoFocus={autoFocusButton === 'cancel'}
buttonProps={cancelButtonProps}
>
{cancelText}
</ActionButton>
);
return (
<Dialog
prefixCls={prefixCls}
className={classString}
wrapClassName={classNames({ [`${contentPrefixCls}-centered`]: !!props.centered })}
onCancel={() => close({ triggerCancel: true })}
visible={visible}
title=""
transitionName={transitionName}
footer=""
maskTransitionName={maskTransitionName}
mask={mask}
maskClosable={maskClosable}
maskStyle={maskStyle}
style={style}
width={width}
zIndex={zIndex}
afterClose={afterClose}
keyboard={keyboard}
centered={centered}
getContainer={getContainer}
>
<div className={`${contentPrefixCls}-body-wrapper`}>
<div className={`${contentPrefixCls}-body`}>
{icon}
{props.title === undefined ? null : (
<span className={`${contentPrefixCls}-title`}>{props.title}</span>
)}
<div className={`${contentPrefixCls}-content`}>{props.content}</div>
</div>
<div className={`${contentPrefixCls}-btns`}>
{cancelButton}
<ActionButton
type={okType}
actionFn={onOk}
closeModal={close}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
>
{okText}
</ActionButton>
</div>
</div>
</Dialog>
);
export type ModalFunc = (
props: ModalFuncProps,
) => {
destroy: () => void;
update: (newConfig: ModalFuncProps) => void;
};
export interface ModalStaticFunctions {
info: ModalFunc;
success: ModalFunc;
error: ModalFunc;
warn: ModalFunc;
warning: ModalFunc;
confirm: ModalFunc;
}
export default function confirm(config: ModalFuncProps) {
const div = document.createElement('div');
document.body.appendChild(div);
@ -145,8 +51,16 @@ export default function confirm(config: ModalFuncProps) {
}
}
function render(props: any) {
ReactDOM.render(<ConfirmDialog {...props} />, div);
function render({ okText, cancelText, ...props }: any) {
const runtimeLocale = getConfirmLocale();
ReactDOM.render(
<ConfirmDialog
{...props}
okText={okText || (props.okCancel ? runtimeLocale.okText : runtimeLocale.justOkText)}
cancelText={cancelText || runtimeLocale.cancelText}
/>,
div,
);
}
function close(...args: any[]) {
@ -155,11 +69,7 @@ export default function confirm(config: ModalFuncProps) {
visible: false,
afterClose: destroy.bind(this, ...args),
};
if (IS_REACT_16) {
render(currentConfig);
} else {
destroy(...args);
}
}
function update(newConfig: ModalFuncProps) {
@ -179,3 +89,47 @@ export default function confirm(config: ModalFuncProps) {
update,
};
}
export function withWarn(props: ModalFuncProps): ModalFuncProps {
return {
type: 'warning',
icon: <ExclamationCircleOutlined />,
okCancel: false,
...props,
};
}
export function withInfo(props: ModalFuncProps): ModalFuncProps {
return {
type: 'info',
icon: <InfoCircleOutlined />,
okCancel: false,
...props,
};
}
export function withSuccess(props: ModalFuncProps): ModalFuncProps {
return {
type: 'success',
icon: <CheckCircleOutlined />,
okCancel: false,
...props,
};
}
export function withError(props: ModalFuncProps): ModalFuncProps {
return {
type: 'error',
icon: <CloseCircleOutlined />,
okCancel: false,
...props,
};
}
export function withConfirm(props: ModalFuncProps): ModalFuncProps {
return {
type: 'confirm',
okCancel: true,
...props,
};
}

View File

@ -0,0 +1,77 @@
---
order: 12
title:
zh-CN: 使用 hooks 获得上下文
en-US: Use hooks to get context
---
## zh-CN
通过 `Modal.useModal` 创建支持读取 context 的 `contextHolder`
## en-US
Use `Modal.useModal` to get `contextHolder` with context accessible issue.
```jsx
import { Modal, Button } from 'antd';
const ReachableContext = React.createContext();
const UnreachableContext = React.createContext();
const config = {
title: 'Use Hook!',
content: (
<div>
<ReachableContext.Consumer>{name => `Reachable: ${name}!`}</ReachableContext.Consumer>
<br />
<UnreachableContext.Consumer>{name => `Unreachable: ${name}!`}</UnreachableContext.Consumer>
</div>
),
};
const App = () => {
const [modal, contextHolder] = Modal.useModal();
return (
<ReachableContext.Provider value="Light">
<Button
onClick={() => {
modal.confirm(config);
}}
>
Confirm
</Button>
<Button
onClick={() => {
modal.warning(config);
}}
>
Warning
</Button>
<Button
onClick={() => {
modal.info(config);
}}
>
Info
</Button>
<Button
onClick={() => {
modal.error(config);
}}
>
Error
</Button>
{/* `contextHolder` should always under the context you want to access */}
{contextHolder}
{/* Can not access this context since `contextHolder` is not in it */}
<UnreachableContext.Provider value="Bamboo" />
</ReachableContext.Provider>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -109,3 +109,19 @@ browserHistory.listen(() => {
Modal.destroyAll();
});
```
### Modal.useModal()
When you need using Context, you can use `contextHolder` which created by `Modal.useModal` to insert into children. Modal created by hooks will get all the context where `contextHolder` are. Created `modal` has the same creating function with `Modal.method`](<#Modal.method()>).
```jsx
const [modal, contextHolder] = Modal.useModal();
React.useEffect(() => {
modal.confirm({
// ...
});
}, []);
return <div>{contextHolder}</div>;
```

View File

@ -1,55 +1,33 @@
import * as React from 'react';
import {
InfoCircleOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import Modal, { ModalFuncProps, destroyFns } from './Modal';
import confirm from './confirm';
import OriginModal, { ModalFuncProps, destroyFns } from './Modal';
import confirm, {
withWarn,
withInfo,
withSuccess,
withError,
withConfirm,
ModalStaticFunctions,
} from './confirm';
export { ActionButtonProps } from './ActionButton';
export { ModalProps, ModalFuncProps } from './Modal';
function modalWarn(props: ModalFuncProps) {
const config = {
type: 'warning',
icon: <ExclamationCircleOutlined />,
okCancel: false,
...props,
};
return confirm(config);
return confirm(withWarn(props));
}
type Modal = typeof OriginModal & ModalStaticFunctions;
const Modal = OriginModal as Modal;
Modal.info = function infoFn(props: ModalFuncProps) {
const config = {
type: 'info',
icon: <InfoCircleOutlined />,
okCancel: false,
...props,
};
return confirm(config);
return confirm(withInfo(props));
};
Modal.success = function successFn(props: ModalFuncProps) {
const config = {
type: 'success',
icon: <CheckCircleOutlined />,
okCancel: false,
...props,
};
return confirm(config);
return confirm(withSuccess(props));
};
Modal.error = function errorFn(props: ModalFuncProps) {
const config = {
type: 'error',
icon: <CloseCircleOutlined />,
okCancel: false,
...props,
};
return confirm(config);
return confirm(withError(props));
};
Modal.warning = modalWarn;
@ -57,12 +35,7 @@ Modal.warning = modalWarn;
Modal.warn = modalWarn;
Modal.confirm = function confirmFn(props: ModalFuncProps) {
const config = {
type: 'confirm',
okCancel: true,
...props,
};
return confirm(config);
return confirm(withConfirm(props));
};
Modal.destroyAll = function destroyAllFn() {

View File

@ -111,3 +111,19 @@ browserHistory.listen(() => {
Modal.destroyAll();
});
```
### Modal.useModal()
当你需要使用 Context 时,可以通过 `Modal.useModal` 创建一个 `contextHolder` 插入子节点中。通过 hooks 创建的临时 Modal 将会得到 `contextHolder` 所在位置的所有上下文。创建的 `modal` 对象拥有与 [`Modal.method`](<#Modal.method()>) 相同的创建通知方法。
```jsx
const [modal, contextHolder] = Modal.useModal();
React.useEffect(() => {
modal.confirm({
// ...
});
}, []);
return <div>{contextHolder}</div>;
```

View File

@ -0,0 +1,63 @@
import * as React from 'react';
import { ModalFuncProps } from '../Modal';
import ConfirmDialog from '../ConfirmDialog';
import defaultLocale from '../../locale/default';
import LocaleReceiver from '../../locale-provider/LocaleReceiver';
export interface HookModalProps {
afterClose: () => void;
config: ModalFuncProps;
}
export interface HookModalRef {
destroy: () => void;
update: (config: ModalFuncProps) => void;
}
interface ModalLocale {
okText: string;
cancelText: string;
justOkText: string;
}
const HookModal: React.RefForwardingComponent<HookModalRef, HookModalProps> = (
{ afterClose, config },
ref,
) => {
const [visible, setVisible] = React.useState(true);
const [innerConfig, setInnerConfig] = React.useState(config);
function close() {
setVisible(false);
}
React.useImperativeHandle(ref, () => ({
destroy: close,
update: (newConfig: ModalFuncProps) => {
setInnerConfig(originConfig => ({
...originConfig,
...newConfig,
}));
},
}));
return (
<LocaleReceiver componentName="Modal" defaultLocale={defaultLocale.Modal}>
{(modalLocale: ModalLocale) => (
<ConfirmDialog
{...innerConfig}
close={close}
visible={visible}
afterClose={afterClose}
okText={
innerConfig.okText ||
(innerConfig.okCancel ? modalLocale.okText : modalLocale.justOkText)
}
cancelText={innerConfig.cancelText || modalLocale.cancelText}
/>
)}
</LocaleReceiver>
);
};
export default React.forwardRef(HookModal);

View File

@ -0,0 +1,64 @@
import * as React from 'react';
import { ModalFuncProps } from '../Modal';
import usePatchElement from '../../_util/usePatchElement';
import HookModal, { HookModalRef } from './HookModal';
import {
withConfirm,
ModalStaticFunctions,
withInfo,
withSuccess,
withError,
withWarn,
} from '../confirm';
let uuid = 0;
export default function useModal(): [Omit<ModalStaticFunctions, 'warn'>, React.ReactElement] {
const [elements, patchElement] = usePatchElement();
function getConfirmFunc(withFunc: (config: ModalFuncProps) => ModalFuncProps) {
return function hookConfirm(config: ModalFuncProps) {
uuid += 1;
const modalRef = React.createRef<HookModalRef>();
let closeFunc: Function;
const modal = (
<HookModal
key={`modal-${uuid}`}
config={withFunc(config)}
ref={modalRef}
afterClose={() => {
closeFunc();
}}
/>
);
closeFunc = patchElement(modal);
return {
destroy: () => {
if (modalRef.current) {
modalRef.current.destroy();
}
},
update: (newConfig: ModalFuncProps) => {
if (modalRef.current) {
modalRef.current.update(newConfig);
}
},
};
};
}
return [
{
info: getConfirmFunc(withInfo),
success: getConfirmFunc(withSuccess),
error: getConfirmFunc(withError),
warning: getConfirmFunc(withWarn),
confirm: getConfirmFunc(withConfirm),
},
<>{elements}</>,
];
}

View File

@ -1253,6 +1253,401 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = `
</div>
`;
exports[`renders ./components/transfer/demo/custom-select-all-labels.md correctly 1`] = `
<div
class="ant-transfer"
>
<div
class="ant-transfer-list"
>
<div
class="ant-transfer-list-header"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-header-selected"
>
<span>
Select All
</span>
<span
class="ant-transfer-list-header-title"
/>
</span>
</div>
<div
class="ant-transfer-list-body"
>
<ul
class="ant-transfer-list-content"
>
<li
class="ant-transfer-list-content-item"
title="content1"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content1
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content2"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content2
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content4"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content4
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content5"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content5
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content7"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content7
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content8"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content8
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content10"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content10
</span>
</li>
</ul>
</div>
</div>
<div
class="ant-transfer-operation"
>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only"
disabled=""
type="button"
>
<span
aria-label="right"
class="anticon anticon-right"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="right"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z"
/>
</svg>
</span>
</button>
<button
class="ant-btn ant-btn-primary ant-btn-sm ant-btn-icon-only"
disabled=""
type="button"
>
<span
aria-label="left"
class="anticon anticon-left"
role="img"
>
<svg
aria-hidden="true"
class=""
data-icon="left"
fill="currentColor"
focusable="false"
height="1em"
viewBox="64 64 896 896"
width="1em"
>
<path
d="M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z"
/>
</svg>
</span>
</button>
</div>
<div
class="ant-transfer-list"
>
<div
class="ant-transfer-list-header"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-header-selected"
>
<span>
0/3
</span>
<span
class="ant-transfer-list-header-title"
/>
</span>
</div>
<div
class="ant-transfer-list-body"
>
<ul
class="ant-transfer-list-content"
>
<li
class="ant-transfer-list-content-item"
title="content3"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content3
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content6"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content6
</span>
</li>
<li
class="ant-transfer-list-content-item"
title="content9"
>
<label
class="ant-checkbox-wrapper"
>
<span
class="ant-checkbox"
>
<input
class="ant-checkbox-input"
type="checkbox"
/>
<span
class="ant-checkbox-inner"
/>
</span>
</label>
<span
class="ant-transfer-list-content-item-text"
>
content9
</span>
</li>
</ul>
</div>
</div>
</div>
`;
exports[`renders ./components/transfer/demo/large-data.md correctly 1`] = `
<div
class="ant-transfer"

View File

@ -519,4 +519,22 @@ describe('Transfer', () => {
);
expect(component).toMatchSnapshot();
});
it('should render correct checkbox label when checkboxLabel is defined', () => {
const selectAllLabels = ['Checkbox Label'];
const wrapper = mount(<Transfer {...listCommonProps} selectAllLabels={selectAllLabels} />);
expect(headerText(wrapper)).toEqual('Checkbox Label');
});
it('should render correct checkbox label when checkboxLabel is a function', () => {
const selectAllLabels = [
({ selectedCount, totalCount }) => (
<span>
{selectedCount} of {totalCount}
</span>
),
];
const wrapper = mount(<Transfer {...listCommonProps} selectAllLabels={selectAllLabels} />);
expect(headerText(wrapper)).toEqual('1 of 2');
});
});

View File

@ -0,0 +1,51 @@
---
order: 99
debug: true
title:
zh-CN: 自定义全选文字
en-US: Custom Select All Labels
---
## zh-CN
自定义穿梭框全选按钮的文字。
## en-US
Custom the labels for select all checkboxs.
```jsx
import React, { useState } from 'react';
import { Transfer } from 'antd';
const mockData = [];
for (let i = 0; i < 10; i++) {
mockData.push({
key: i.toString(),
title: `content${i + 1}`,
description: `description of content${i + 1}`,
});
}
const oriTargetKeys = mockData.filter(item => +item.key % 3 > 1).map(item => item.key);
const selectAllLabels = [
'Select All',
({ selectedCount, totalCount }) => `${selectedCount}/${totalCount}`,
];
const App = () => {
const [targetKeys, setTargetKeys] = useState(oriTargetKeys);
return (
<Transfer
dataSource={mockData}
targetKeys={targetKeys}
onChange={setTargetKeys}
render={item => item.title}
selectAllLabels={selectAllLabels}
/>
);
};
ReactDOM.render(<App />, mountNode);
```

View File

@ -36,6 +36,7 @@ One or more elements can be selected from either column, one click on the proper
| style | A custom CSS style used for rendering wrapper element. | object | | |
| targetKeys | A set of keys of elements that are listed on the right column. | string\[] | \[] | |
| titles | A set of titles that are sorted from left to right. | ReactNode\[] | - | |
| selectAllLabels | A set of customized labels for select all checkboxs on the header | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | | |
| onChange | A callback function that is executed when the transfer between columns is complete. | (targetKeys, direction, moveKeys): void | | |
| onScroll | A callback function which is executed when scroll options list | (direction, event): void | | |
| onSearch | A callback function which is executed when search field are changed | (direction: 'left'\|'right', value: string): void | - | |

View File

@ -35,6 +35,10 @@ export interface ListStyle {
direction: TransferDirection;
}
export type SelectAllLabel =
| React.ReactNode
| ((info: { selectedCount: number; totalCount: number }) => React.ReactNode);
export interface TransferProps {
prefixCls?: string;
className?: string;
@ -59,6 +63,7 @@ export interface TransferProps {
onScroll?: (direction: TransferDirection, e: React.SyntheticEvent<HTMLUListElement>) => void;
children?: (props: TransferListBodyProps) => React.ReactNode;
showSelectAll?: boolean;
selectAllLabels?: SelectAllLabel[];
}
export interface TransferLocale {
@ -332,6 +337,7 @@ class Transfer extends React.Component<TransferProps, any> {
});
const titles = this.props.titles || locale.titles;
const selectAllLabels = this.props.selectAllLabels || [];
return (
<div className={cls} style={style}>
<List
@ -353,6 +359,7 @@ class Transfer extends React.Component<TransferProps, any> {
disabled={disabled}
direction="left"
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[0]}
{...locale}
/>
<Operation
@ -386,6 +393,7 @@ class Transfer extends React.Component<TransferProps, any> {
disabled={disabled}
direction="right"
showSelectAll={showSelectAll}
selectAllLabel={selectAllLabels[1]}
{...locale}
/>
</div>

View File

@ -38,6 +38,7 @@ title: Transfer
| style | 容器的自定义样式 | object | | |
| targetKeys | 显示在右侧框数据的 key 集合 | string\[] | \[] | |
| titles | 标题集合,顺序从左至右 | ReactNode\[] | \['', ''] | |
| selectAllLabels | 自定义顶部多选框标题的集合 | (ReactNode \| (info: { selectedCount: number, totalCount: number }) => ReactNode)[] | | |
| onChange | 选项在两栏之间转移时的回调函数 | (targetKeys, direction, moveKeys): void | | |
| onScroll | 选项列表滚动时的回调函数 | (direction, event): void | | |
| onSearch | 搜索框内容时改变时的回调函数 | (direction: 'left'\|'right', value: string): void | - | |

View File

@ -3,7 +3,13 @@ import omit from 'omit.js';
import classNames from 'classnames';
import PureRenderMixin from 'rc-util/lib/PureRenderMixin';
import Checkbox from '../checkbox';
import { TransferItem, TransferDirection, RenderResult, RenderResultObject } from './index';
import {
TransferItem,
TransferDirection,
RenderResult,
RenderResultObject,
SelectAllLabel,
} from './index';
import Search from './search';
import defaultRenderList, { TransferListBodyProps, OmitProps } from './renderListBody';
@ -48,6 +54,7 @@ export interface TransferListProps {
disabled?: boolean;
direction: TransferDirection;
showSelectAll?: boolean;
selectAllLabel?: SelectAllLabel;
}
interface TransferListState {
@ -246,6 +253,21 @@ export default class TransferList extends React.Component<TransferListProps, Tra
};
};
getSelectAllLabel = (selectedCount: number, totalCount: number): React.ReactNode => {
const { itemsUnit, itemUnit, selectAllLabel } = this.props;
if (selectAllLabel) {
return typeof selectAllLabel === 'function'
? selectAllLabel({ selectedCount, totalCount })
: selectAllLabel;
}
const unit = totalCount > 1 ? itemsUnit : itemUnit;
return (
<>
{(selectedCount > 0 ? `${selectedCount}/` : '') + totalCount} {unit}
</>
);
};
render() {
const { filterValue } = this.state;
const {
@ -259,8 +281,6 @@ export default class TransferList extends React.Component<TransferListProps, Tra
style,
searchPlaceholder,
notFoundContent,
itemUnit,
itemsUnit,
renderList,
onItemSelectAll,
showSelectAll,
@ -278,7 +298,6 @@ export default class TransferList extends React.Component<TransferListProps, Tra
const { filteredItems, filteredRenderItems } = this.getFilteredItems(dataSource, filterValue);
// ================================= List Body =================================
const unit = filteredItems.length > 1 ? itemsUnit : itemUnit;
const listBody = this.getListBody(
prefixCls,
@ -310,10 +329,7 @@ export default class TransferList extends React.Component<TransferListProps, Tra
<div className={`${prefixCls}-header`}>
{checkAllCheckbox}
<span className={`${prefixCls}-header-selected`}>
<span>
{(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '}
{unit}
</span>
<span>{this.getSelectAllLabel(checkedKeys.length, filteredItems.length)}</span>
<span className={`${prefixCls}-header-title`}>{titleText}</span>
</span>
</div>

View File

@ -108,7 +108,7 @@
"rc-dialog": "~7.6.0",
"rc-drawer": "~3.1.1",
"rc-dropdown": "~3.0.0-alpha.0",
"rc-field-form": "^0.0.0-rc.0",
"rc-field-form": "^0.0.0-rc.1",
"rc-input-number": "~4.5.0",
"rc-mentions": "~1.0.0-alpha.3",
"rc-menu": "~8.0.0-alpha.7",