Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(modal): rewrite with hook and support strict mode #24238

Merged
merged 3 commits into from May 19, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.createRef<any>();
hengkx marked this conversation as resolved.
Show resolved Hide resolved
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);
}
};
}, [props.autoFocus]);

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;
143 changes: 72 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,102 @@ 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;
destroyAll: () => void;
}

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;
Modal.destroyAll = () => {};
hengkx marked this conversation as resolved.
Show resolved Hide resolved

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