diff --git a/components/_util/usePatchElement.tsx b/components/_util/usePatchElement.tsx new file mode 100644 index 0000000000..55f74d1686 --- /dev/null +++ b/components/_util/usePatchElement.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; + +export default function usePatchElement(): [ + React.ReactElement[], + (element: React.ReactElement) => Function, +] { + const [elements, setElements] = React.useState([]); + + function patchElement(element: React.ReactElement) { + setElements(originElements => [...originElements, element]); + + return () => { + setElements(originElements => originElements.filter(ele => ele !== element)); + }; + } + + return [elements, patchElement]; +} diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index 7d1a4ed6d6..ce9b66b22f 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -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' | diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index 14593e633f..22841b7e08 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -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' | diff --git a/components/modal/ConfirmDialog.tsx b/components/modal/ConfirmDialog.tsx new file mode 100644 index 0000000000..dbd113e395 --- /dev/null +++ b/components/modal/ConfirmDialog.tsx @@ -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 && ( + + {cancelText} + + ); + + return ( + 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} + > +
+
+ {icon} + {props.title === undefined ? null : ( + {props.title} + )} +
{props.content}
+
+
+ {cancelButton} + + {okText} + +
+
+
+ ); +}; + +export default ConfirmDialog; diff --git a/components/modal/Modal.tsx b/components/modal/Modal.tsx index e421e45f56..012eb615fc 100644 --- a/components/modal/Modal.tsx +++ b/components/modal/Modal.tsx @@ -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 { - 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', diff --git a/components/modal/__tests__/hook.test.js b/components/modal/__tests__/hook.test.js new file mode 100644 index 0000000000..79e4e57e37 --- /dev/null +++ b/components/modal/__tests__/hook.test.js @@ -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 ( + + + + + + + {/* `contextHolder` should always under the context you want to access */} + {contextHolder} + + {/* Can not access this context since `contextHolder` is not in it */} + + + ); +}; + +ReactDOM.render(, mountNode); +``` diff --git a/components/modal/index.en-US.md b/components/modal/index.en-US.md index ac1f1712ca..d56ddb0237 100644 --- a/components/modal/index.en-US.md +++ b/components/modal/index.en-US.md @@ -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
{contextHolder}
; +``` diff --git a/components/modal/index.tsx b/components/modal/index.tsx index 6d0094474a..abaef9cfe3 100644 --- a/components/modal/index.tsx +++ b/components/modal/index.tsx @@ -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: , - 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: , - okCancel: false, - ...props, - }; - return confirm(config); + return confirm(withInfo(props)); }; Modal.success = function successFn(props: ModalFuncProps) { - const config = { - type: 'success', - icon: , - okCancel: false, - ...props, - }; - return confirm(config); + return confirm(withSuccess(props)); }; Modal.error = function errorFn(props: ModalFuncProps) { - const config = { - type: 'error', - icon: , - 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() { diff --git a/components/modal/index.zh-CN.md b/components/modal/index.zh-CN.md index a7f4d282a3..8b12e3708f 100644 --- a/components/modal/index.zh-CN.md +++ b/components/modal/index.zh-CN.md @@ -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
{contextHolder}
; +``` diff --git a/components/modal/useModal/HookModal.tsx b/components/modal/useModal/HookModal.tsx new file mode 100644 index 0000000000..ff3c800c11 --- /dev/null +++ b/components/modal/useModal/HookModal.tsx @@ -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 = ( + { 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 ( + + {(modalLocale: ModalLocale) => ( + + )} + + ); +}; + +export default React.forwardRef(HookModal); diff --git a/components/modal/useModal/index.tsx b/components/modal/useModal/index.tsx new file mode 100644 index 0000000000..553dffce10 --- /dev/null +++ b/components/modal/useModal/index.tsx @@ -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, React.ReactElement] { + const [elements, patchElement] = usePatchElement(); + + function getConfirmFunc(withFunc: (config: ModalFuncProps) => ModalFuncProps) { + return function hookConfirm(config: ModalFuncProps) { + uuid += 1; + + const modalRef = React.createRef(); + + let closeFunc: Function; + const modal = ( + { + 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}, + ]; +} diff --git a/components/transfer/__tests__/__snapshots__/demo.test.js.snap b/components/transfer/__tests__/__snapshots__/demo.test.js.snap index 0c70856006..f304658d05 100644 --- a/components/transfer/__tests__/__snapshots__/demo.test.js.snap +++ b/components/transfer/__tests__/__snapshots__/demo.test.js.snap @@ -1253,6 +1253,401 @@ exports[`renders ./components/transfer/demo/custom-item.md correctly 1`] = ` `; +exports[`renders ./components/transfer/demo/custom-select-all-labels.md correctly 1`] = ` +
+
+
+ + + + Select All + + + +
+
+
    +
  • + + + content1 + +
  • +
  • + + + content2 + +
  • +
  • + + + content4 + +
  • +
  • + + + content5 + +
  • +
  • + + + content7 + +
  • +
  • + + + content8 + +
  • +
  • + + + content10 + +
  • +
+
+
+
+ + +
+
+
+ + + + 0/3 + + + +
+
+
    +
  • + + + content3 + +
  • +
  • + + + content6 + +
  • +
  • + + + content9 + +
  • +
+
+
+
+`; + exports[`renders ./components/transfer/demo/large-data.md correctly 1`] = `
{ ); expect(component).toMatchSnapshot(); }); + + it('should render correct checkbox label when checkboxLabel is defined', () => { + const selectAllLabels = ['Checkbox Label']; + const wrapper = mount(); + expect(headerText(wrapper)).toEqual('Checkbox Label'); + }); + + it('should render correct checkbox label when checkboxLabel is a function', () => { + const selectAllLabels = [ + ({ selectedCount, totalCount }) => ( + + {selectedCount} of {totalCount} + + ), + ]; + const wrapper = mount(); + expect(headerText(wrapper)).toEqual('1 of 2'); + }); }); diff --git a/components/transfer/demo/custom-select-all-labels.md b/components/transfer/demo/custom-select-all-labels.md new file mode 100644 index 0000000000..4c7c1754e3 --- /dev/null +++ b/components/transfer/demo/custom-select-all-labels.md @@ -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 ( + item.title} + selectAllLabels={selectAllLabels} + /> + ); +}; + +ReactDOM.render(, mountNode); +``` diff --git a/components/transfer/index.en-US.md b/components/transfer/index.en-US.md index 3a89d54cd7..6cd0fa9c54 100644 --- a/components/transfer/index.en-US.md +++ b/components/transfer/index.en-US.md @@ -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 | - | | diff --git a/components/transfer/index.tsx b/components/transfer/index.tsx index 989798ec34..ed51b27115 100644 --- a/components/transfer/index.tsx +++ b/components/transfer/index.tsx @@ -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) => void; children?: (props: TransferListBodyProps) => React.ReactNode; showSelectAll?: boolean; + selectAllLabels?: SelectAllLabel[]; } export interface TransferLocale { @@ -332,6 +337,7 @@ class Transfer extends React.Component { }); const titles = this.props.titles || locale.titles; + const selectAllLabels = this.props.selectAllLabels || []; return (
{ disabled={disabled} direction="left" showSelectAll={showSelectAll} + selectAllLabel={selectAllLabels[0]} {...locale} /> { disabled={disabled} direction="right" showSelectAll={showSelectAll} + selectAllLabel={selectAllLabels[1]} {...locale} />
diff --git a/components/transfer/index.zh-CN.md b/components/transfer/index.zh-CN.md index 1e54a14fc7..d412022920 100644 --- a/components/transfer/index.zh-CN.md +++ b/components/transfer/index.zh-CN.md @@ -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 | - | | diff --git a/components/transfer/list.tsx b/components/transfer/list.tsx index 55b6a14b53..60bd80e9ee 100644 --- a/components/transfer/list.tsx +++ b/components/transfer/list.tsx @@ -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 { + 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 1 ? itemsUnit : itemUnit; const listBody = this.getListBody( prefixCls, @@ -310,10 +329,7 @@ export default class TransferList extends React.Component {checkAllCheckbox} - - {(checkedKeys.length > 0 ? `${checkedKeys.length}/` : '') + filteredItems.length}{' '} - {unit} - + {this.getSelectAllLabel(checkedKeys.length, filteredItems.length)} {titleText}
diff --git a/package.json b/package.json index 37e830d34c..f7b4c3d905 100644 --- a/package.json +++ b/package.json @@ -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",