Skip to content

Commit

Permalink
refactor(modal): rewrite with hook and support strict mode (#24238)
Browse files Browse the repository at this point in the history
* refactor(modal): rewrite with hook and support strict mode

* Update ActionButton.tsx

* improve code
  • Loading branch information
hengkx committed May 19, 2020
1 parent 6ad1b18 commit 50fe770
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 126 deletions.
93 changes: 43 additions & 50 deletions components/modal/ActionButton.tsx
@@ -1,5 +1,4 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Button from '../button';
import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/button';

Expand All @@ -11,59 +10,53 @@ export interface ActionButtonProps {
buttonProps?: ButtonProps;
}

export interface ActionButtonState {
loading: ButtonProps['loading'];
}

export default class ActionButton extends React.Component<ActionButtonProps, ActionButtonState> {
timeoutId: number;

clicked: boolean;
const ActionButton: React.FC<ActionButtonProps> = props => {
const clickedRef = React.useRef<boolean>(false);
const ref = React.useRef<any>();
const [loading, setLoading] = React.useState<ButtonProps['loading']>(false);

state = {
loading: false,
};

componentDidMount() {
if (this.props.autoFocus) {
const $this = ReactDOM.findDOMNode(this) as HTMLInputElement;
this.timeoutId = setTimeout(() => $this.focus());
React.useEffect(() => {
let timeoutId: number;
if (props.autoFocus) {
const $this = ref.current as HTMLInputElement;
timeoutId = setTimeout(() => $this.focus());
}
}

componentWillUnmount() {
clearTimeout(this.timeoutId);
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);

handlePromiseOnOk(returnValueOfOnOk?: PromiseLike<any>) {
const { closeModal } = this.props;
const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => {
const { closeModal } = props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) {
return;
}
this.setState({ loading: true });
setLoading(true);
returnValueOfOnOk.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
// this.setState({ loading: false });
// setState({ loading: false });
closeModal(...args);
},
(e: Error) => {
// Emit error when catch promise reject
// eslint-disable-next-line no-console
console.error(e);
// See: https://github.com/ant-design/ant-design/issues/6183
this.setState({ loading: false });
this.clicked = false;
setLoading(false);
clickedRef.current = false;
},
);
}
};

onClick = () => {
const { actionFn, closeModal } = this.props;
if (this.clicked) {
const onClick = () => {
const { actionFn, closeModal } = props;
if (clickedRef.current) {
return;
}
this.clicked = true;
clickedRef.current = true;
if (!actionFn) {
closeModal();
return;
Expand All @@ -72,29 +65,29 @@ export default class ActionButton extends React.Component<ActionButtonProps, Act
if (actionFn.length) {
returnValueOfOnOk = actionFn(closeModal);
// https://github.com/ant-design/ant-design/issues/23358
this.clicked = false;
clickedRef.current = false;
} else {
returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) {
closeModal();
return;
}
}
this.handlePromiseOnOk(returnValueOfOnOk);
handlePromiseOnOk(returnValueOfOnOk);
};

render() {
const { type, children, buttonProps } = this.props;
const { loading } = this.state;
return (
<Button
{...convertLegacyProps(type)}
onClick={this.onClick}
loading={loading}
{...buttonProps}
>
{children}
</Button>
);
}
}
const { type, children, buttonProps } = props;
return (
<Button
{...convertLegacyProps(type)}
onClick={onClick}
loading={loading}
{...buttonProps}
ref={ref}
>
{children}
</Button>
);
};

export default ActionButton;
141 changes: 70 additions & 71 deletions components/modal/Modal.tsx
Expand Up @@ -9,7 +9,7 @@ import { getConfirmLocale } from './locale';
import Button from '../button';
import { LegacyButtonType, ButtonProps, convertLegacyProps } from '../button/button';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import { ConfigConsumer, ConfigConsumerProps } from '../config-provider';
import { ConfigContext } from '../config-provider';

let mousePosition: { x: number; y: number } | null;
export const destroyFns: Array<() => void> = [];
Expand Down Expand Up @@ -119,101 +119,100 @@ export interface ModalLocale {
justOkText: string;
}

export default class Modal extends React.Component<ModalProps, {}> {
static destroyAll: () => void;

static useModal = useModal;
interface ModalInterface extends React.FC<ModalProps> {
useModal: typeof useModal;
}

static defaultProps = {
width: 520,
transitionName: 'zoom',
maskTransitionName: 'fade',
confirmLoading: false,
visible: false,
okType: 'primary' as LegacyButtonType,
};
const Modal: ModalInterface = props => {
const { getPopupContainer: getContextPopupContainer, getPrefixCls, direction } = React.useContext(
ConfigContext,
);

handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onCancel } = this.props;
const handleCancel = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onCancel } = props;
if (onCancel) {
onCancel(e);
}
};

handleOk = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onOk } = this.props;
const handleOk = (e: React.MouseEvent<HTMLButtonElement>) => {
const { onOk } = props;
if (onOk) {
onOk(e);
}
};

renderFooter = (locale: ModalLocale) => {
const { okText, okType, cancelText, confirmLoading } = this.props;
const renderFooter = (locale: ModalLocale) => {
const { okText, okType, cancelText, confirmLoading } = props;
return (
<>
<Button onClick={this.handleCancel} {...this.props.cancelButtonProps}>
<Button onClick={handleCancel} {...props.cancelButtonProps}>
{cancelText || locale.cancelText}
</Button>
<Button
{...convertLegacyProps(okType)}
loading={confirmLoading}
onClick={this.handleOk}
{...this.props.okButtonProps}
onClick={handleOk}
{...props.okButtonProps}
>
{okText || locale.okText}
</Button>
</>
);
};

renderModal = ({
getPopupContainer: getContextPopupContainer,
getPrefixCls,
direction,
}: ConfigConsumerProps) => {
const {
prefixCls: customizePrefixCls,
footer,
visible,
wrapClassName,
centered,
getContainer,
closeIcon,
...restProps
} = this.props;

const prefixCls = getPrefixCls('modal', customizePrefixCls);
const defaultFooter = (
<LocaleReceiver componentName="Modal" defaultLocale={getConfirmLocale()}>
{this.renderFooter}
</LocaleReceiver>
);
const {
prefixCls: customizePrefixCls,
footer,
visible,
wrapClassName,
centered,
getContainer,
closeIcon,
...restProps
} = props;

const prefixCls = getPrefixCls('modal', customizePrefixCls);
const defaultFooter = (
<LocaleReceiver componentName="Modal" defaultLocale={getConfirmLocale()}>
{renderFooter}
</LocaleReceiver>
);

const closeIconToRender = (
<span className={`${prefixCls}-close-x`}>
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
</span>
);

const wrapClassNameExtended = classNames(wrapClassName, {
[`${prefixCls}-centered`]: !!centered,
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
return (
<Dialog
{...restProps}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={handleCancel}
closeIcon={closeIconToRender}
/>
);
};

const closeIconToRender = (
<span className={`${prefixCls}-close-x`}>
{closeIcon || <CloseOutlined className={`${prefixCls}-close-icon`} />}
</span>
);
const wrapClassNameExtended = classNames(wrapClassName, {
[`${prefixCls}-centered`]: !!centered,
[`${prefixCls}-wrap-rtl`]: direction === 'rtl',
});
return (
<Dialog
{...restProps}
getContainer={getContainer === undefined ? getContextPopupContainer : getContainer}
prefixCls={prefixCls}
wrapClassName={wrapClassNameExtended}
footer={footer === undefined ? defaultFooter : footer}
visible={visible}
mousePosition={mousePosition}
onClose={this.handleCancel}
closeIcon={closeIconToRender}
/>
);
};
Modal.useModal = useModal;

render() {
return <ConfigConsumer>{this.renderModal}</ConfigConsumer>;
}
}
Modal.defaultProps = {
width: 520,
transitionName: 'zoom',
maskTransitionName: 'fade',
confirmLoading: false,
visible: false,
okType: 'primary' as LegacyButtonType,
};

export default Modal;
8 changes: 4 additions & 4 deletions components/modal/__tests__/Modal.test.js
Expand Up @@ -52,15 +52,15 @@ describe('Modal', () => {

it('onCancel should be called', () => {
const onCancel = jest.fn();
const wrapper = mount(<Modal onCancel={onCancel} />).instance();
wrapper.handleCancel();
const wrapper = mount(<Modal visible onCancel={onCancel} />);
wrapper.find('.ant-btn').first().simulate('click');
expect(onCancel).toHaveBeenCalled();
});

it('onOk should be called', () => {
const onOk = jest.fn();
const wrapper = mount(<Modal onOk={onOk} />).instance();
wrapper.handleOk();
const wrapper = mount(<Modal visible onOk={onOk} />);
wrapper.find('.ant-btn').last().simulate('click');
expect(onOk).toHaveBeenCalled();
});

Expand Down
2 changes: 1 addition & 1 deletion components/modal/index.tsx
Expand Up @@ -15,7 +15,7 @@ function modalWarn(props: ModalFuncProps) {
return confirm(withWarn(props));
}

type Modal = typeof OriginModal & ModalStaticFunctions;
type Modal = typeof OriginModal & ModalStaticFunctions & { destroyAll: () => void };
const Modal = OriginModal as Modal;

Modal.info = function infoFn(props: ModalFuncProps) {
Expand Down

0 comments on commit 50fe770

Please sign in to comment.