Skip to content

Commit

Permalink
refactor: Guide 组件重构完成,fixed 和 normal 两种 mode 进行组件拆分,并新增单元测试
Browse files Browse the repository at this point in the history
fix:修复了一些旧问题,父类构造函数 options 传递问题,按钮文案配置不生效的问题
  • Loading branch information
chenchen32 committed Sep 23, 2021
1 parent bbe9391 commit c732e68
Show file tree
Hide file tree
Showing 8 changed files with 644 additions and 276 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const refactoredComp = [
'Dropdown',
'Input',
'ImageLoader',
'Guide',
];

const getRefactoredCompMatch = name => {
Expand Down
80 changes: 80 additions & 0 deletions source/components/Guide/Guide.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React from 'react';
import PropTypes from 'prop-types';
import GuideFixed from './GuideFixed';
import GuideNormal from './GuideNormal';
import ConfigConsumer from '../Config/Consumer';
import { LocaleProperties } from '../Locale';

export interface GuideProps {
allowClose?: boolean;
className?: string;
counter?: boolean;
keyboardControl?: boolean;
mask?: boolean;
mode?: string;
onClose?: () => void;
steps?: any[];
style?: React.CSSProperties;
visible?: boolean;
prefixCls?: string;
doneBtnText?: string;
prevBtnText?: string;
nextBtnText?: string;
skipBtnText?: string;
}

const Guide = (props: GuideProps) => {
return (
<ConfigConsumer componentName="Guide">
{(Locale: LocaleProperties['Guide']) => {
const btnTextProps = {
prevBtnText: props.prevBtnText || Locale.prevBtnText,
nextBtnText: props.nextBtnText || Locale.nextBtnText,
doneBtnText: props.doneBtnText || Locale.doneBtnText,
skipBtnText: props.skipBtnText || Locale.skipBtnText,
};
const childrenProps = {
...props,
...btnTextProps,
};
const { mode } = props;
if (mode === 'fixed') {
return <GuideFixed {...childrenProps} />;
} else {
return <GuideNormal {...childrenProps} />;
}
}}
</ConfigConsumer>
);
};

Guide.propTypes = {
prefixCls: PropTypes.string,
className: PropTypes.string,
style: PropTypes.object,
mode: PropTypes.string,
steps: PropTypes.array.isRequired,
visible: PropTypes.bool,
counter: PropTypes.bool,
mask: PropTypes.bool,
allowClose: PropTypes.bool,
keyboardControl: PropTypes.bool,
onClose: PropTypes.func,
doneBtnText: PropTypes.string,
prevBtnText: PropTypes.string,
nextBtnText: PropTypes.string,
skipBtnText: PropTypes.string,
};

Guide.defaultProps = {
prefixCls: 'fishd-guide',
mode: 'normal',
allowClose: false,
keyboardControl: false,
visible: false,
counter: true,
mask: true,
steps: [],
};

export default Guide;
152 changes: 152 additions & 0 deletions source/components/Guide/GuideFixed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import * as React from 'react';
import classNames from 'classnames';
import Modal from '../Modal';
import Button from '../Button';
import useUpdateEffect from '../../hooks/useUpdateEffect';
import { GuideProps } from './Guide';
import { ESC_KEY_CODE, LEFT_KEY_CODE, RIGHT_KEY_CODE } from './src/common/constants';

const GuideFixed = (props: GuideProps) => {
const totalCountRef = React.useRef<number>(props.steps.length);
const totalCount = totalCountRef.current;
const [visible, setVisible] = React.useState<boolean>(props.visible);
const [currentIndex, setCurrentIndex] = React.useState<number>(0);

const handleClose = () => {
Promise.resolve()
.then(() => {
setVisible(false);
})
.then(() => {
setCurrentIndex(0);
});

props.onClose?.();
};

const handleNext = () => {
if (currentIndex >= totalCount - 1) {
handleClose();
} else {
const nextIndex = currentIndex + 1;
setCurrentIndex(nextIndex);
}
};

const handlePrev = () => {
let nextIndex;

if (currentIndex <= 0) {
nextIndex = 0;
} else {
nextIndex = currentIndex - 1;
}

setCurrentIndex(nextIndex);
};

const onKeyUp = event => {
if (!props.keyboardControl || !visible) {
return;
}

if (event.keyCode === ESC_KEY_CODE) {
handleClose();
return;
}

if (event.keyCode === RIGHT_KEY_CODE) {
handleNext();
} else if (event.keyCode === LEFT_KEY_CODE) {
handlePrev();
}
};

React.useEffect(() => {
window.addEventListener('keyup', onKeyUp, false);
return () => {
window.removeEventListener('keyup', onKeyUp);
};
}, [onKeyUp]);

useUpdateEffect(() => {
if (!visible && props.visible) {
setVisible(true);
}
}, [props]);

const renderTitle = curStep => {
if (!curStep.title) {
return null;
}

if (curStep.subtitle) {
return (
<React.Fragment>
{curStep.title}
<div className={`${props.prefixCls}-fixed-subtitle`}>{curStep.subtitle}</div>
</React.Fragment>
);
} else {
return curStep.title;
}
};

const {
prefixCls,
allowClose,
mask,
className,
style,
steps,
skipBtnText,
prevBtnText,
nextBtnText,
doneBtnText,
} = props;
const rootCls = classNames(`${prefixCls}-fixed`, {
[className]: className,
});
const isFirstStep = currentIndex <= 0;
const isLastStep = currentIndex >= totalCount - 1;

return (
<Modal
className={rootCls}
style={{
...style,
}}
mask={mask}
maskClosable={allowClose}
title={renderTitle(steps[currentIndex])}
visible={visible}
width={800}
footer={
<React.Fragment>
<div key="skip" className={`${prefixCls}-skip-btn skip`} onClick={handleClose}>
{skipBtnText}
</div>
{isFirstStep ? null : (
<Button key="prev" className={`${prefixCls}-prev-btn`} onClick={handlePrev}>
{prevBtnText}
</Button>
)}
<Button
key="next"
className={isLastStep ? `${prefixCls}-done-btn` : `${prefixCls}-next-btn`}
type="primary"
onClick={handleNext}
>
{isLastStep ? doneBtnText : nextBtnText}
{` (${currentIndex + 1}/${steps.length})`}
</Button>
</React.Fragment>
}
onCancel={handleClose}
>
{steps[currentIndex].content}
</Modal>
);
};

export default GuideFixed;
85 changes: 85 additions & 0 deletions source/components/Guide/GuideNormal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import * as React from 'react';
import Driver from './src/index';
import useUpdateEffect from '../../hooks/useUpdateEffect';
import { GuideProps } from './Guide';

const GuideNormal = (props: GuideProps) => {
const [visible, setVisible] = React.useState<boolean>(props.visible);

const handleCloseRef = React.useRef<() => void>();
handleCloseRef.current = () => {
setVisible(false);

props.onClose?.();
};

const driver: Driver = React.useMemo(() => {
let opt = {
counter: props.counter,
allowClose: props.allowClose,
keyboardControl: props.keyboardControl,
prevBtnText: props.prevBtnText,
nextBtnText: props.nextBtnText,
skipBtnText: props.skipBtnText,
doneBtnText: props.doneBtnText,
onReset: () => {
const handleClose = handleCloseRef.current;
handleClose?.();
},
};

if (!props.mask) {
opt['opacity'] = 0;
}

return new Driver(opt);
}, []);

const init = () => {
let { steps } = props;

if (!(steps && steps.length)) {
return;
}

setTimeout(() => {
if (steps.length == 1) {
driver.highlight(steps[0]);
} else {
driver.defineSteps(props.steps);
driver.start();
}
}, 300);
};

React.useEffect(() => {
if (!visible) {
return;
}

init();
}, []);

/*
重构前,visible 字段没有被设计成受控属性
外部想要重新打开 Guide 组件,是通过重新 setState visible 为 true,
Guide 组件内部 componentWillReceiveProps 判断组件内部 visible 为 false
且 nextProps.visible 为 true 时,进行重新初始化
这里存在隐患,Guide 组件内部 visible 变为 false 时,
因为 visible 不受控,外部 visible 还是 true,
此时除 visible 外其他的 props 改变,
componentWillReceiveProps 内也会执行重新初始化的逻辑,弹出 Guide 组件
重构成 hooks 之后,暂时和重构前保持一致的逻辑
*/
useUpdateEffect(() => {
if (!visible && props.visible) {
setVisible(true);
init();
}
}, [props]);

return null;
};

export default GuideNormal;
Loading

0 comments on commit c732e68

Please sign in to comment.