Skip to content

Commit

Permalink
feat(Loading): refactoring completed and support LodingPlugin (#458)
Browse files Browse the repository at this point in the history
* 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
anlyyao and github-actions[bot] authored Aug 16, 2024
1 parent 71cd595 commit cb0c4bc
Show file tree
Hide file tree
Showing 35 changed files with 1,872 additions and 368 deletions.
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

0 comments on commit cb0c4bc

Please sign in to comment.