Skip to content

Commit

Permalink
feat: add draft dialog component
Browse files Browse the repository at this point in the history
  • Loading branch information
mumiao committed Dec 9, 2020
1 parent 7a09921 commit 02aa023
Show file tree
Hide file tree
Showing 10 changed files with 516 additions and 6 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"loadsh": "^0.0.4",
"monaco-editor": "^0.21.2",
"rc-collapse": "^2.0.1",
"rc-dialog": "^8.4.5",
"rc-tree": "^3.10.0",
"react": "^16.13.1",
"react-dnd": "^9.3.4",
Expand Down
9 changes: 4 additions & 5 deletions src/components/button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ import { classNames, prefixClaName } from 'mo/common/className';

type BtnSizeType = 'normal' | 'large';
export interface IButton extends React.ComponentProps<'a'> {
/**
* Default size is normal
*/
disabled?: boolean;
size?: BtnSizeType;
}

export const defaultButtonClassName = 'btn';

export function Button(props: React.PropsWithChildren<IButton>) {
const { className, children, size = 'normal', ...others } = props;

const disabled = props.disabled ? 'disabled' : null;
const claNames = classNames(
prefixClaName(defaultButtonClassName),
size,
className
className,
disabled
);

return (
Expand Down
4 changes: 4 additions & 0 deletions src/components/button/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ $btn: 'btn';
padding: 8px;
}

&.disabled {
cursor: not-allowed;
}

&:hover {
opacity: 0.9;
}
Expand Down
82 changes: 82 additions & 0 deletions src/components/dialog/ActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import * as React from 'react';
import { Button, IButton } from 'mo/components/button';

export interface ActionButtonProps {
actionFn?: (...args: any[]) => any | PromiseLike<any>;
closeModal: Function;
autoFocus?: boolean;
buttonProps?: IButton;
}

const ActionButton: React.FC<ActionButtonProps> = props => {
const clickedRef = React.useRef<boolean>(false);
const ref = React.useRef<any>();

React.useEffect(() => {
let timeoutId: number;
if (props.autoFocus) {
const $this = ref.current as HTMLInputElement;
timeoutId = setTimeout(() => $this.focus());
}
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, []);

const handlePromiseOnOk = (returnValueOfOnOk?: PromiseLike<any>) => {
const { closeModal } = props;
if (!returnValueOfOnOk || !returnValueOfOnOk.then) {
return;
}
returnValueOfOnOk.then(
(...args: any[]) => {
// It's unnecessary to set loading=false, for the Modal will be unmounted after close.
closeModal(...args);
},
(e: Error) => {
// eslint-disable-next-line no-console
console.error(e);
clickedRef.current = false;
},
);
};

const onClick = () => {
const { actionFn, closeModal } = props;
if (clickedRef.current) {
return;
}
clickedRef.current = true;
if (!actionFn) {
closeModal();
return;
}
let returnValueOfOnOk;
if (actionFn.length) {
returnValueOfOnOk = actionFn(closeModal);
clickedRef.current = false;
} else {
returnValueOfOnOk = actionFn();
if (!returnValueOfOnOk) {
closeModal();
return;
}
}
handlePromiseOnOk(returnValueOfOnOk);
};

const { children, buttonProps } = props;
return (
<Button
onClick={onClick}
{...buttonProps}
ref={ref}
>
{children}
</Button>
);
};

export default ActionButton;
115 changes: 115 additions & 0 deletions src/components/dialog/ConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import * as React from 'react';
import classNames from 'classnames';
import Dialog, { IModalFuncProps } from './Modal';
import ActionButton from './ActionButton';

interface ConfirmDialogProps extends IModalFuncProps {
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,
prefixCls,
bodyStyle,
closable = false,
closeIcon,
modalRender,
focusTriggerAfterClose,
} = props;
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}
closable={closable}
closeIcon={closeIcon}
modalRender={modalRender}
focusTriggerAfterClose={focusTriggerAfterClose}
>
<div className={`${contentPrefixCls}-body-wrapper`}>
<div className={`${contentPrefixCls}-body`} style={bodyStyle}>
{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
actionFn={onOk}
closeModal={close}
autoFocus={autoFocusButton === 'ok'}
buttonProps={okButtonProps}
>
{okText}
</ActionButton>
</div>
</div>
</Dialog>
);
};

export default ConfirmDialog;
127 changes: 127 additions & 0 deletions src/components/dialog/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as React from 'react';
import Dialog from 'rc-dialog';
import { IDialogPropTypes } from 'rc-dialog/lib/IDialogPropTypes';

import { classNames, prefixClaName } from 'mo/common/className';

import { Button, IButton } from 'mo/components/button';

let mousePosition

const getClickPosition = (e: MouseEvent) => {
mousePosition = {
x: e.pageX,
y: e.pageY,
};
setTimeout(() => {
mousePosition = null;
}, 100);
};

// 只有点击事件支持从鼠标位置动画展开
if (typeof window !== 'undefined' && window.document?.documentElement) {
document.documentElement.addEventListener('click', getClickPosition, true);
}

export const destroyFns: Array<() => void> = [];

export interface IModalProps extends IDialogPropTypes {
/** 点击确定回调 */
onOk?: (e: React.MouseEvent<HTMLElement>) => void;
/** 点击模态框右上角叉、取消按钮、Props.maskClosable 值为 true 时的遮罩层或键盘按下 Esc 时的回调 */
onCancel?: (e: React.SyntheticEvent<Element, Event>) => void;
/** 垂直居中 */
centered?: boolean;
/** 确认按钮文字 */
okText?: React.ReactNode;
/** 取消按钮文字 */
cancelText?: React.ReactNode;
okButtonProps?: IButton;
cancelButtonProps?: IButton;
}

export interface IModalFuncProps extends IDialogPropTypes {
content?: React.ReactNode;
onOk?: (...args: any[]) => any;
onCancel?: (...args: any[]) => any;
okButtonProps?: IButton;
cancelButtonProps?: IButton;
centered?: boolean;
okText?: React.ReactNode;
cancelText?: React.ReactNode;
icon?: React.ReactNode;
okCancel?: boolean;
type?: string;
autoFocusButton?: null | 'ok' | 'cancel';
}

const Modal: React.FC<IModalProps> = (props) => {
const handleCancel = (e: React.SyntheticEvent<Element, Event>) => {
const { onCancel } = props;
onCancel?.(e);
};

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

const renderFooter = () => {
const { okText, cancelText } = props;
return (
<>
<Button onClick={handleCancel} {...props.cancelButtonProps}>
{cancelText}
</Button>
<Button onClick={handleOk} {...props.okButtonProps}>
{okText}
</Button>
</>
);
};

const {
footer,
visible,
wrapClassName,
centered,
getContainer,
closeIcon,
focusTriggerAfterClose = true,
...restProps
} = props;

const prefixCls = prefixClaName('modal');
const defaultFooter = renderFooter;

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

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

Modal.defaultProps = {
width: 520,
transitionName: 'zoom',
maskTransitionName: 'fade',
visible: false,
};

export default Modal;
Loading

0 comments on commit 02aa023

Please sign in to comment.