-
Notifications
You must be signed in to change notification settings - Fork 48
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: Guide 组件重构完成,fixed 和 normal 两种 mode 进行组件拆分,并新增单元测试
fix:修复了一些旧问题,父类构造函数 options 传递问题,按钮文案配置不生效的问题
- Loading branch information
1 parent
bbe9391
commit c732e68
Showing
8 changed files
with
644 additions
and
276 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.