-
Notifications
You must be signed in to change notification settings - Fork 66
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(Loading): refactoring completed and support LodingPlugin (#458)
* feat(Loading): refactoring completed and support LodingPlugin * test: update csr and ssr snap * chore: update snapshot * fix: fix cr --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
- Loading branch information
1 parent
71cd595
commit cb0c4bc
Showing
35 changed files
with
1,872 additions
and
368 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
Submodule _common
updated
31 files
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,2 @@ | ||
// 用于判断是否可使用 dom | ||
export const canUseDocument = !!(typeof window !== 'undefined' && window.document && window.document.createElement); |
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,68 @@ | ||
import React, { forwardRef, useEffect, useMemo, useImperativeHandle } from 'react'; | ||
import { createPortal } from 'react-dom'; | ||
import { AttachNode, AttachNodeReturnValue } from '../common'; | ||
import { canUseDocument } from '../_util/dom'; | ||
import useConfig from '../hooks/useConfig'; | ||
import useDefaultProps from '../hooks/useDefaultProps'; | ||
|
||
export interface PortalProps { | ||
/** | ||
* 指定挂载的 HTML 节点, false 为挂载在 body | ||
*/ | ||
attach?: React.ReactElement | AttachNode | boolean; | ||
/** | ||
* 触发元素 | ||
*/ | ||
triggerNode?: HTMLElement; | ||
children: React.ReactNode; | ||
} | ||
|
||
export function getAttach(attach: PortalProps['attach'], triggerNode?: HTMLElement): AttachNodeReturnValue { | ||
if (!canUseDocument) return null; | ||
|
||
let el: AttachNodeReturnValue; | ||
if (typeof attach === 'string') { | ||
el = document.querySelector(attach); | ||
} | ||
if (typeof attach === 'function') { | ||
el = attach(triggerNode); | ||
} | ||
if (typeof attach === 'object' && attach instanceof window.HTMLElement) { | ||
el = attach; | ||
} | ||
|
||
// fix el in iframe | ||
if (el && el.nodeType === 1) return el; | ||
|
||
return document.body; | ||
} | ||
|
||
const Portal = forwardRef<HTMLElement, PortalProps>((props, ref) => { | ||
const { attach, children, triggerNode } = useDefaultProps<PortalProps>(props, {}); | ||
|
||
const { classPrefix } = useConfig(); | ||
|
||
const container = useMemo(() => { | ||
if (!canUseDocument) return null; | ||
const el = document.createElement('div'); | ||
el.className = `${classPrefix}-portal-wrapper`; | ||
return el; | ||
}, [classPrefix]); | ||
|
||
useEffect(() => { | ||
const parentElement = getAttach(attach, triggerNode); | ||
parentElement?.appendChild?.(container); | ||
|
||
return () => { | ||
parentElement?.removeChild?.(container); | ||
}; | ||
}, [container, attach, triggerNode]); | ||
|
||
useImperativeHandle(ref, () => container); | ||
|
||
return canUseDocument ? createPortal(children, container) : null; | ||
}); | ||
|
||
Portal.displayName = 'Portal'; | ||
|
||
export default Portal; |
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 |
---|---|---|
@@ -1,144 +1,171 @@ | ||
import React, { useEffect, useMemo, useRef, useState } from 'react'; | ||
import React, { forwardRef, useEffect, useMemo, useRef, useState } from 'react'; | ||
import classNames from 'classnames'; | ||
import { TdLoadingProps } from './type'; | ||
import { loadingDefaultProps } from './defaultProps'; | ||
import { StyledProps } from '../common'; | ||
import useConfig from '../_util/useConfig'; | ||
import Spinner from './icon/Spinner'; | ||
import Gradient from './icon/Gradient'; | ||
import Portal from '../common/Portal'; | ||
import { useLockScroll } from '../hooks/useLockScroll'; | ||
import useDefaultProps from '../hooks/useDefaultProps'; | ||
import { usePrefixClass } from '../hooks/useClass'; | ||
|
||
export interface LoadingProps extends TdLoadingProps, StyledProps {} | ||
|
||
const Loading: React.FC<LoadingProps> = ({ | ||
children, // 子元素,同 content | ||
delay = 0, // 延迟显示加载效果的时间,用于防止请求速度过快引起的加载闪烁,单位:毫秒 | ||
duration = 800, // 加载动画执行完成一次的时间,单位:毫秒 | ||
indicator = true, // 是否显示加载指示符 | ||
inheritColor = false, // 是否继承父元素颜色 | ||
layout = 'horizontal', // 对齐方式 | ||
loading = true, // 是否处于加载状态 | ||
pause = false, // 是否暂停动画 | ||
// preventScrollThrough = true, // 防止滚动穿透,全屏加载模式有效 | ||
progress, // 加载进度 | ||
reverse, // 加载动画是否反向 | ||
size = '20px', // 尺寸,示例:40rpx/20px | ||
text, // 加载提示文案 | ||
theme = 'circular', // 加载组件类型 | ||
}) => { | ||
const { classPrefix } = useConfig(); | ||
|
||
const delayTimer = useRef(null); | ||
const Loading = forwardRef<HTMLDivElement, LoadingProps>((props) => { | ||
const { | ||
className, | ||
style, | ||
attach, | ||
content, | ||
children, | ||
delay, | ||
duration, | ||
fullscreen, | ||
indicator, | ||
inheritColor, | ||
layout, | ||
loading, | ||
pause, | ||
preventScrollThrough, | ||
reverse, | ||
size, | ||
text, | ||
theme, | ||
} = useDefaultProps<LoadingProps>(props, loadingDefaultProps); | ||
|
||
const loadingClass = usePrefixClass('loading'); | ||
const loadingRef = useRef<HTMLDivElement>(); | ||
|
||
const childNode = content || children; | ||
|
||
const centerClass = `${loadingClass}--center`; | ||
const fullClass = `${loadingClass}--full`; | ||
const relativeClass = `${loadingClass}__parent`; | ||
|
||
useLockScroll(loadingRef, loading && fullscreen && preventScrollThrough, loadingClass); | ||
|
||
// 当延时加载delay有值时,值会发生变化 | ||
const [reloading, setReloading] = useState(!delay && loading); | ||
|
||
const textContent = useMemo(() => { | ||
if (theme === 'error') { | ||
return '加载失败'; | ||
useEffect(() => { | ||
let timer: NodeJS.Timeout | null = null; | ||
if (delay && loading) { | ||
timer = setTimeout(() => { | ||
setReloading(loading); | ||
}, delay); | ||
} else { | ||
setReloading(loading); | ||
} | ||
return () => { | ||
if (timer) { | ||
clearTimeout(timer); | ||
} | ||
}; | ||
}, [delay, loading]); | ||
|
||
if (text) { | ||
return text; | ||
} | ||
const baseClasses = classNames(loadingClass, centerClass, { | ||
[`${loadingClass}--vertical`]: layout === 'vertical', | ||
[`${loadingClass}--fullscreen`]: fullscreen, | ||
[`${loadingClass}--full`]: !fullscreen && (!!attach || childNode), | ||
}); | ||
|
||
return null; | ||
}, [theme, text]); | ||
const rootStyle = useMemo<React.CSSProperties>( | ||
() => ({ | ||
color: inheritColor ? 'inherit' : undefined, | ||
fontSize: size || undefined, | ||
}), | ||
[inheritColor, size], | ||
); | ||
|
||
useEffect(() => { | ||
setReloading(!delay && loading); | ||
if (delayTimer.current) clearTimeout(delayTimer.current); | ||
if (!delay || !loading) return; | ||
|
||
// 延时加载 | ||
delayTimer.current = setTimeout(() => { | ||
setReloading(true); | ||
clearTimeout(delayTimer.current); | ||
delayTimer.current = null; | ||
}, delay); | ||
}, [delay, loading]); | ||
const textClass = classNames(`${loadingClass}__text`, { | ||
[`${loadingClass}__text--only`]: !indicator, | ||
}); | ||
|
||
const progressCss = useMemo(() => { | ||
if (!progress || progress <= 0) return -100; | ||
if (progress > 1) return 0; | ||
return (-1 + progress) * 100; | ||
}, [progress]); | ||
|
||
const sizeClass = useMemo(() => { | ||
const SIZE_CLASSNAMES = { | ||
small: `${classPrefix}-size-s`, | ||
medium: `${classPrefix}-size-m`, | ||
large: `${classPrefix}-size-l`, | ||
default: '', | ||
xs: `${classPrefix}-size-xs`, | ||
xl: `${classPrefix}-size-xl`, | ||
block: `${classPrefix}-size-full-width`, | ||
const dostLoading = () => ( | ||
<div | ||
className={`${loadingClass}__dots`} | ||
style={{ | ||
animationPlayState: pause ? 'paused' : '', | ||
animationDirection: reverse ? 'reverse' : '', | ||
animationDuration: `${duration}ms`, | ||
width: size, | ||
height: size, | ||
}} | ||
> | ||
{Array.from({ length: 3 }).map((val, i) => ( | ||
<div | ||
key={i} | ||
className={`${loadingClass}__dot`} | ||
style={{ | ||
animationDuration: `${duration / 1000}s`, | ||
animationDelay: `${(duration * i) / 3000}s`, | ||
}} | ||
></div> | ||
))} | ||
</div> | ||
); | ||
|
||
const renderContent = () => { | ||
if (!reloading) return null; | ||
|
||
const themeMap = { | ||
circular: <Gradient reverse={reverse} duration={duration} pause={pause} />, | ||
spinner: <Spinner reverse={reverse} duration={duration} pause={pause} />, | ||
dots: dostLoading(), | ||
}; | ||
|
||
if (size === 'large' || size === 'medium' || size === 'small') { | ||
console.log(SIZE_CLASSNAMES[size]); | ||
return SIZE_CLASSNAMES[size]; | ||
let renderIndicator = themeMap[theme]; | ||
|
||
if (indicator && typeof indicator !== 'boolean') { | ||
renderIndicator = indicator as JSX.Element; | ||
} | ||
return ''; | ||
}, [size, classPrefix]); | ||
return ( | ||
<> | ||
{indicator && renderIndicator} | ||
{text && <span className={textClass}>{text}</span>} | ||
</> | ||
); | ||
}; | ||
|
||
return ( | ||
<> | ||
<div | ||
className={classNames( | ||
[`${classPrefix}-loading`], | ||
{ | ||
[`${classPrefix}-loading--vertical`]: layout === 'vertical', | ||
[`${classPrefix}-loading--bar`]: theme === 'bar', | ||
}, | ||
sizeClass, | ||
if (childNode) { | ||
return ( | ||
<div className={classNames(relativeClass, className)} style={style}> | ||
{childNode} | ||
{reloading && ( | ||
<div ref={loadingRef} className={classNames(baseClasses)} style={{ ...rootStyle }}> | ||
{renderContent()} | ||
</div> | ||
)} | ||
style={inheritColor ? { color: 'inherit' } : {}} | ||
> | ||
{/* theme = 'bar' 时 */} | ||
{(theme === 'bar' && progress && ![0, 1].includes(progress) && ( | ||
<div className={`${classPrefix}-loading__bar`} style={{ transform: `translate3d(${progressCss}%, 0, 0)` }}> | ||
<div className={`${classPrefix}-loading__shadow`}></div> | ||
</div> | ||
); | ||
} | ||
|
||
if (attach) { | ||
return ( | ||
<Portal attach={attach}> | ||
{loading && ( | ||
<div | ||
ref={loadingRef} | ||
className={classNames(baseClasses, fullClass, className)} | ||
style={{ ...rootStyle, ...style }} | ||
> | ||
{renderContent()} | ||
</div> | ||
)) || | ||
null} | ||
{(theme !== 'bar' && ( | ||
<> | ||
{(indicator && reloading && ( | ||
<> | ||
{theme === 'circular' && <Gradient reverse={reverse} duration={duration} pause={pause} />} | ||
{theme === 'spinner' && <Spinner reverse={reverse} duration={duration} pause={pause} />} | ||
{theme === 'dots' && ( | ||
<div | ||
style={ | ||
pause | ||
? { animation: 'none' } | ||
: { | ||
animation: `t-dot-typing ${duration / 1000}s infinite linear`, | ||
animationDirection: `${reverse ? 'reverse' : 'normal'}`, | ||
} | ||
} | ||
className={`${classPrefix}-loading__dots`} | ||
/> | ||
)} | ||
</> | ||
)) || | ||
null} | ||
{(textContent && reloading && ( | ||
<span | ||
className={classNames(`${classPrefix}-loading__text`, { | ||
[`${classPrefix}-loading__text--error`]: theme === 'error', | ||
[`${classPrefix}-loading__text--only`]: !indicator || theme === 'error', | ||
})} | ||
> | ||
{textContent} | ||
</span> | ||
)) || | ||
null} | ||
</> | ||
)) || | ||
null} | ||
{children} | ||
)} | ||
</Portal> | ||
); | ||
} | ||
|
||
return ( | ||
loading && ( | ||
<div ref={loadingRef} className={classNames(baseClasses, className)} style={{ ...rootStyle, ...style }}> | ||
{renderContent()} | ||
</div> | ||
</> | ||
) | ||
); | ||
}; | ||
}); | ||
|
||
Loading.displayName = 'Loading'; | ||
|
||
export default Loading; |
Oops, something went wrong.