Skip to content

Commit

Permalink
feat(Skeleton): add delay props
Browse files Browse the repository at this point in the history
  • Loading branch information
anlyyao committed Aug 14, 2024
1 parent c6bc1f7 commit ca3c1ba
Show file tree
Hide file tree
Showing 19 changed files with 360 additions and 220 deletions.
9 changes: 9 additions & 0 deletions src/_util/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ export function getCharacterLength(str: string, maxCharacter?: number) {
}
return len;
}

/**
* 兼容样式中支持 number/string 类型的传值 得出最后的结果。
* @param param number 或 string 类型的可用于样式上的值
* @returns 可使用的样式值。
*/
export function pxCompat(param: string | number) {
return typeof param === 'number' ? `${param}px` : param;
}
230 changes: 124 additions & 106 deletions src/skeleton/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -1,121 +1,139 @@
import React from 'react';
import React, { forwardRef, useState, useEffect } from 'react';
import classNames from 'classnames';
import isNumber from 'lodash/isNumber';
import isArray from 'lodash/isArray';
import { SkeletonRowCol, SkeletonRowColObj, TdSkeletonProps } from './type';
import { StyledProps, Styles } from '../common';
import useConfig from '../_util/useConfig';

export type SkeletonProps = TdSkeletonProps & StyledProps;

const Skeleton: React.FC<SkeletonProps> = (props) => {
const { animation, loading = true, rowCol, theme = 'text' } = props;
const { classPrefix } = useConfig();
const name = `${classPrefix}-skeleton`;

const completeContent = props.content || props.default || props.children;
const rootClasses = classNames(`${name}`, `${name}--${theme}`);
const defaultRowcols: SkeletonRowCol = [1, 1, 1, { width: '70%' }];
const rowCols: SkeletonRowCol = [];
if (theme === 'avatar-text') {
rowCols.push(...defaultRowcols);
} else if (rowCol) {
rowCols.push(...rowCol);
} else {
rowCols.push(...defaultRowcols);
}
import { skeletonDefaultProps } from './defaultProps';
import { pxCompat } from '../_util/helper';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface SkeletonProps extends TdSkeletonProps, StyledProps {}

const ThemeMap: Record<TdSkeletonProps['theme'], SkeletonRowCol> = {
avatar: [{ type: 'circle', size: '48px' }],
image: [{ type: 'rect', size: '72px' }],
text: [
[
{ width: '24%', height: '16px', marginRight: '16px' },
{ width: '76%', height: '16px' },
],
1,
],
paragraph: [1, 1, 1, { width: '55%' }],
};

const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>((props) => {
const skeletonClass = usePrefixClass('skeleton');
const { className, style, children, animation, delay, loading, rowCol, theme } = useDefaultProps<SkeletonProps>(
props,
skeletonDefaultProps,
);

const renderCols = (_cols: Number | SkeletonRowColObj | Array<SkeletonRowColObj>) => {
let cols: Array<SkeletonRowColObj> = [];
if (isArray(_cols)) {
cols = _cols;
} else if (isNumber(_cols)) {
cols = new Array(_cols).fill({ type: 'text' });
} else {
cols = [_cols as SkeletonRowColObj];
}

const getColItemClass = (obj: SkeletonRowColObj) =>
classNames(`${skeletonClass}__col`, `${skeletonClass}--type-${obj.type || 'text'}`, {
[`${skeletonClass}--animation-${animation}`]: animation,
});

const rowClass = `${name}__row`;
const colClass = classNames(`${name}__col`, `${name}--type-text`, {
[`${name}--animation-${animation}`]: animation,
});

const getColItemStyle = (obj: SkeletonRowColObj): Styles => {
const styleName = [
'width',
'height',
'marginRight',
'marginLeft',
'margin',
'size',
'background',
'backgroundColor',
'borderRadius',
];

return styleName.reduce((style, currentStyle) => {
const accStyle = style;
if (currentStyle in obj) {
const px = isNumber(obj[currentStyle]) ? `${obj[currentStyle]}px` : obj[currentStyle];
if (currentStyle === 'size') {
[accStyle.width, accStyle.height] = [px, px];
const getColItemStyle = (obj: SkeletonRowColObj): Styles => {
const styleName = [
'width',
'height',
'marginRight',
'marginLeft',
'margin',
'size',
'background',
'backgroundColor',
'borderRadius',
];
const style: Styles = {};
styleName.forEach((name) => {
if (name in obj) {
const px = pxCompat(obj[name]);
if (name === 'size') {
[style.width, style.height] = [px, px];
} else {
style[name] = px;
}
}
accStyle[currentStyle] = px;
}
return accStyle;
}, {} as Styles);
});
return style;
};

return cols.map((obj, index) => (
<div key={index} className={getColItemClass(obj)} style={getColItemStyle(obj)}>
{parseTNode(obj.content)}
</div>
));
};

const parsedRowcols = rowCols.map((item) => {
if (isNumber(item)) {
return [
{
type: 'text',
style: {},
},
];
}
if (Array.isArray(item)) {
return item.map((col) => ({
...col,
style: getColItemStyle(col),
}));
const renderRowCol = (_rowCol?: SkeletonRowCol) => {
const renderedRowCol: SkeletonRowCol = _rowCol || rowCol;

return renderedRowCol.map<React.ReactNode>((item, index) => (
<div key={index} className={`${skeletonClass}__row`}>
{renderCols(item)}
</div>
));
};

const [isLoading, setIsLoading] = useState(loading);

useEffect(() => {
if (delay > 0 && !loading) {
const timeout = setTimeout(() => {
setIsLoading(loading);
}, delay);
return () => clearTimeout(timeout);
}

const nItem = item as SkeletonRowColObj;
return [
{
...nItem,
style: getColItemStyle(nItem),
},
];
});
setIsLoading(loading);
}, [delay, loading]);

if (!isLoading) {
return <>{parseTNode(children)}</>;
}

const childrenContent: React.ReactNode[] = [];

// 保持优先级: rowCol > theme,增加默认值兜底
if (rowCol) {
childrenContent.push(renderRowCol(rowCol));
} else if (props.theme) {
childrenContent.push(renderRowCol(ThemeMap[theme]));
} else if (!theme && !rowCol) {
// 什么都不传时,传入默认 rowCol
childrenContent.push(
renderRowCol([
[
{ width: '24%', height: '16px', marginRight: '16px' },
{ width: '76%', height: '16px' },
],
1,
]),
);
}

return (
<>
<div className={rootClasses}>
{!loading && completeContent}

{loading && theme === 'avatar-text' && (
<>
{/* avater-text */}
<div className={`${name}__col ${name}--type-circle`}></div>
<div className={`${name}__paragraph`}>
{parsedRowcols.map((item, index) => (
<div className={rowClass} key={index}>
{item.map((subItem, subIndex) => (
<div key={subIndex} className={colClass} style={subItem.style}></div>
))}
</div>
))}
</div>
</>
)}

{loading && theme === 'text' && (
<>
{/* text */}
{parsedRowcols.map((item, index) => (
<div className={rowClass} key={index}>
{item.map((subItem, subIndex) => (
<div key={subIndex} className={colClass} style={subItem.style}></div>
))}
</div>
))}
</>
)}
</div>
</>
<div className={className} style={style}>
{childrenContent}
</div>
);
};
});

Skeleton.displayName = 'Skeleton';

export default Skeleton;
30 changes: 30 additions & 0 deletions src/skeleton/_example/animation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import { Skeleton } from 'tdesign-mobile-react';

const animations = [
{
title: '渐变加载效果',
value: 'gradient',
loading: true,
},
{
title: '闪烁加载效果',
value: 'flashed',
loading: true,
},
] as const;

export default function AnimationSkeleton() {
return (
<div>
{animations.map((animation, index) => (
<div key={index}>
<div className="demo-section__desc">{animation.title}</div>
<div className="demo-section__content">
<Skeleton theme="paragraph" animation={animation.value} loading={animation.loading} />
</div>
</div>
))}
</div>
);
}
7 changes: 0 additions & 7 deletions src/skeleton/_example/avatar-text.jsx

This file was deleted.

6 changes: 0 additions & 6 deletions src/skeleton/_example/base.jsx

This file was deleted.

23 changes: 23 additions & 0 deletions src/skeleton/_example/cell-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Skeleton } from 'tdesign-mobile-react';
import type { SkeletonProps } from 'tdesign-mobile-react';

const rowColsAvater: SkeletonProps['rowCol'] = [{ size: '48px', type: 'circle' }];
const rowColsImage: SkeletonProps['rowCol'] = [{ size: '48px', type: 'rect' }];
const rowColsContent: SkeletonProps['rowCol'] = [{ width: '50%' }, { width: '100%' }];

export default function CellGroupSkeleton() {
return (
<>
<div className="cell-group">
<Skeleton className="cell-group-avatar" rowCol={rowColsAvater} loading />
<Skeleton className="cell-group-content" rowCol={rowColsContent} loading />
</div>

<div className="cell-group">
<Skeleton className="cell-group-avatar" rowCol={rowColsImage} loading />
<Skeleton className="cell-group-content" rowCol={rowColsContent} loading />
</div>
</>
);
}
6 changes: 0 additions & 6 deletions src/skeleton/_example/flashed.jsx

This file was deleted.

6 changes: 0 additions & 6 deletions src/skeleton/_example/gradient.jsx

This file was deleted.

27 changes: 27 additions & 0 deletions src/skeleton/_example/grid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React from 'react';
import { Skeleton } from 'tdesign-mobile-react';

const grid = [
[
{ width: '48px', height: '48px', borderRadius: '6px' },
{ width: '48px', height: '48px', borderRadius: '6px' },
{ width: '48px', height: '48px', borderRadius: '6px' },
{ width: '48px', height: '48px', borderRadius: '6px' },
{ width: '48px', height: '48px', borderRadius: '6px' },
],
[
{ width: '48px', height: '16px', borderRadius: '3px' },
{ width: '48px', height: '16px', borderRadius: '3px' },
{ width: '48px', height: '16px', borderRadius: '3px' },
{ width: '48px', height: '16px', borderRadius: '3px' },
{ width: '48px', height: '16px', borderRadius: '3px' },
],
];

export default function GridSkeleton() {
return (
<>
<Skeleton rowCol={grid} loading={true} />
</>
);
}
16 changes: 16 additions & 0 deletions src/skeleton/_example/image-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Skeleton } from 'tdesign-mobile-react';
import './style/index.less';

const rowCols = [{ size: '163.5px', borderRadius: '12px' }, 1, { width: '61%' }];

export default function ImageGroupSkeleton() {
return (
<>
<div className="image-group">
<Skeleton rowCol={rowCols} loading={true} />
<Skeleton rowCol={rowCols} loading={true} />
</div>
</>
);
}
Loading

0 comments on commit ca3c1ba

Please sign in to comment.