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

feat(Loading): refactoring completed and support LodingPlugin #458

Merged
merged 4 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion site/mobile/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import App from './App';
import '../style/mobile/index.less';

import '../../src/_common/style/mobile/_reset.less';
import '../../src/_common/style/mobile/index.less';
// import '../../src/_common/style/mobile/index.less';

ReactDOM.render(
<React.StrictMode>
Expand Down
2 changes: 1 addition & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ export default {
{
title: 'Loading 加载中',
name: 'loading',
component: () => import('tdesign-mobile-react/loading/_example/index.jsx'),
component: () => import('tdesign-mobile-react/loading/_example/index.tsx'),
},
{
title: 'Swiper 轮播',
Expand Down
2 changes: 2 additions & 0 deletions src/_util/dom.ts
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);
68 changes: 68 additions & 0 deletions src/common/Portal.tsx
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;
2 changes: 1 addition & 1 deletion src/hooks/useLockScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ let totalLockCount = 0;
// 移植自vant:https://github.com/youzan/vant/blob/HEAD/src/composables/use-lock-scroll.ts
export function useLockScroll(rootRef: RefObject<HTMLElement>, shouldLock: boolean, componentName: string) {
const touch = useTouch();
const BODY_LOCK_CLASS = `${componentName}-overflow-hidden`;
const BODY_LOCK_CLASS = `${componentName}--lock`;

const onTouchMove = useCallback(
(event: TouchEvent) => {
Expand Down
263 changes: 145 additions & 118 deletions src/loading/Loading.tsx
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;
Loading
Loading