New Component Skeleton (#11226)

* Add new component Skeleton
* Add related doc
* Add sample
* Add test case

ref: https://github.com/ant-design/ant-design/issues/10308
This commit is contained in:
zombieJ 2018-08-22 23:34:36 +08:00 committed by GitHub
parent cc2bd76595
commit 796b56dbc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1343 additions and 193 deletions

View File

@ -41,6 +41,7 @@ Array [
"Rate",
"Row",
"Select",
"Skeleton",
"Slider",
"Spin",
"Steps",

View File

@ -372,174 +372,83 @@ exports[`renders ./components/card/demo/inner.md correctly 1`] = `
exports[`renders ./components/card/demo/loading.md correctly 1`] = `
<div>
<div
class="ant-card ant-card-loading ant-card-bordered"
<span
class="ant-switch"
tabindex="0"
>
<span
class="ant-switch-inner"
/>
</span>
<div
class="ant-card ant-card-bordered"
style="width:300px;margin-top:16px"
>
<div
class="ant-card-head"
>
<div
class="ant-card-head-wrapper"
>
<div
class="ant-card-head-title"
>
Card title
</div>
</div>
</div>
<div
class="ant-card-body"
>
<div
class="ant-card-loading-content"
class="ant-skeleton ant-skeleton-with-avatar ant-skeleton-active"
>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
class="ant-skeleton-header"
>
<div
class="ant-col-22"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-lg ant-skeleton-avatar-circle"
/>
</div>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
class="ant-skeleton-content"
>
<div
class="ant-col-8"
style="padding-left:4px;padding-right:4px"
<h3
class="ant-skeleton-title"
style="width:50%"
/>
<ul
class="ant-skeleton-paragraph"
>
<div
class="ant-card-loading-block"
<li
style="width:100%"
/>
</div>
<div
class="ant-col-15"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
<li
style="width:100%"
/>
</div>
</div>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
>
<div
class="ant-col-6"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-18"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
</div>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
>
<div
class="ant-col-13"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-9"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
</div>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
>
<div
class="ant-col-4"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-3"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-16"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
</div>
<div
class="ant-row"
style="margin-left:-4px;margin-right:-4px"
>
<div
class="ant-col-8"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-6"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
<div
class="ant-col-8"
style="padding-left:4px;padding-right:4px"
>
<div
class="ant-card-loading-block"
/>
</div>
</ul>
</div>
</div>
</div>
<ul
class="ant-card-actions"
>
<li
style="width:33.333333333333336%"
>
<span>
<i
class="anticon anticon-setting"
/>
</span>
</li>
<li
style="width:33.333333333333336%"
>
<span>
<i
class="anticon anticon-edit"
/>
</span>
</li>
<li
style="width:33.333333333333336%"
>
<span>
<i
class="anticon anticon-ellipsis"
/>
</span>
</li>
</ul>
</div>
<button
class="ant-btn"
style="margin-top:16px"
type="button"
>
<span>
Toggle loading
</span>
</button>
</div>
`;

View File

@ -14,30 +14,42 @@ title:
Shows a loading indicator while the contents of the card is being fetched.
````jsx
import { Card, Button } from 'antd';
import { Skeleton, Switch, Card, Icon, Avatar } from 'antd';
class LoadingCard extends React.Component {
const { Meta } = Card;
class App extends React.Component {
state = {
loading: true,
}
handleClick = () => {
this.setState({ loading: !this.state.loading });
onChange = (checked) => {
this.setState({ loading: !checked });
}
render() {
const { loading } = this.state;
return (
<div>
<Card loading={this.state.loading} title="Card title">
Whatever content
<Switch checked={!loading} onChange={this.onChange} />
<Card
style={{ width: 300, marginTop: 16 }}
actions={[<Icon type="setting" />, <Icon type="edit" />, <Icon type="ellipsis" />]}
>
<Skeleton loading={loading} avatar active>
<Meta
avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />}
title="Card title"
description="This is the description"
/>
</Skeleton>
</Card>
<Button onClick={this.handleClick} style={{ marginTop: 16 }}>Toggle loading</Button>
</div>
);
}
}
ReactDOM.render(
<LoadingCard />,
mountNode);
ReactDOM.render(<App />, mountNode);
````

View File

@ -91,6 +91,8 @@ export { default as Row } from './row';
export { default as Select } from './select';
export { default as Skeleton } from './skeleton';
export { default as Slider } from './slider';
export { default as Spin } from './spin';

View File

@ -365,7 +365,7 @@ exports[`renders ./components/list/demo/infinite-virtualized-load.md correctly 1
exports[`renders ./components/list/demo/loadmore.md correctly 1`] = `
<div
class="ant-list demo-loadmore-list ant-list-split ant-list-loading ant-list-something-after-last-item"
class="ant-list demo-loadmore-list ant-list-split ant-list-loading"
>
<div
class="ant-spin-nested-loading"
@ -392,18 +392,6 @@ exports[`renders ./components/list/demo/loadmore.md correctly 1`] = `
/>
</div>
</div>
<div
style="text-align:center;margin-top:12px;height:32px;line-height:32px"
>
<button
class="ant-btn"
type="button"
>
<span>
loading more
</span>
</button>
</div>
</div>
`;

View File

@ -14,25 +14,27 @@ title:
Load more list with `loadMore` property.
````jsx
import { List, Avatar, Button, Spin } from 'antd';
import { List, Avatar, Button, Skeleton } from 'antd';
import reqwest from 'reqwest';
const fakeDataUrl = 'https://randomuser.me/api/?results=5&inc=name,gender,email,nat&noinfo';
const count = 3;
const fakeDataUrl = `https://randomuser.me/api/?results=${count}&inc=name,gender,email,nat&noinfo`;
class LoadMoreList extends React.Component {
state = {
loading: true,
loadingMore: false,
showLoadingMore: true,
initLoading: true,
loading: false,
data: [],
list: [],
}
componentDidMount() {
this.getData((res) => {
this.setState({
loading: false,
initLoading: false,
data: res.results,
list: res.results,
});
});
}
@ -51,13 +53,15 @@ class LoadMoreList extends React.Component {
onLoadMore = () => {
this.setState({
loadingMore: true,
loading: true,
list: this.state.data.concat([...new Array(count)].map(() => ({ loading: true, name: {} }))),
});
this.getData((res) => {
const data = this.state.data.concat(res.results);
this.setState({
data,
loadingMore: false,
list: data,
loading: false,
}, () => {
// Resetting window's offsetTop so as to display react-virtualized demo underfloor.
// In real scene, you can using public method of react-virtualized:
@ -68,28 +72,30 @@ class LoadMoreList extends React.Component {
}
render() {
const { loading, loadingMore, showLoadingMore, data } = this.state;
const loadMore = showLoadingMore ? (
const { initLoading, loading, list } = this.state;
const loadMore = !initLoading && !loading ? (
<div style={{ textAlign: 'center', marginTop: 12, height: 32, lineHeight: '32px' }}>
{loadingMore && <Spin />}
{!loadingMore && <Button onClick={this.onLoadMore}>loading more</Button>}
<Button onClick={this.onLoadMore}>loading more</Button>
</div>
) : null;
return (
<List
className="demo-loadmore-list"
loading={loading}
loading={initLoading}
itemLayout="horizontal"
loadMore={loadMore}
dataSource={data}
dataSource={list}
renderItem={item => (
<List.Item actions={[<a>edit</a>, <a>more</a>]}>
<List.Item.Meta
avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />}
title={<a href="https://ant.design">{item.name.last}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
<div>content</div>
<Skeleton avatar title={false} loading={item.loading} active>
<List.Item.Meta
avatar={<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />}
title={<a href="https://ant.design">{item.name.last}</a>}
description="Ant Design, a design language for background applications, is refined by Ant UED Team"
/>
<div>content</div>
</Skeleton>
</List.Item>
)}
/>

View File

@ -0,0 +1,37 @@
import * as React from 'react';
import classNames from 'classnames';
export interface SkeletonAvatarProps {
prefixCls?: string;
className?: string;
style?: object;
size?: 'large' | 'small' | 'default';
shape?: 'circle'| 'square';
}
class Title extends React.Component<SkeletonAvatarProps, any> {
static defaultProps: Partial<SkeletonAvatarProps> = {
prefixCls: 'ant-skeleton-avatar',
size: 'large',
};
render() {
const { prefixCls, className, style, size, shape } = this.props;
const sizeCls = classNames({
[`${prefixCls}-lg`]: size === 'large',
[`${prefixCls}-sm`]: size === 'small',
});
const shapeCls = classNames({
[`${prefixCls}-circle`]: shape === 'circle',
[`${prefixCls}-square`]: shape === 'square',
});
return (
<span className={classNames(prefixCls, className, sizeCls, shapeCls)} style={style} />
);
}
}
export default Title;

View File

@ -0,0 +1,78 @@
import * as React from 'react';
import classNames from 'classnames';
import { polyfill } from 'react-lifecycles-compat';
type widthUnit = number | string;
export interface SkeletonParagraphProps {
prefixCls?: string;
className?: string;
style?: object;
width?: widthUnit | Array<widthUnit>;
rows?: number;
}
interface SkeletonParagraphState {
prevProps: SkeletonParagraphProps;
widthList: Array<widthUnit>;
}
class Paragraph extends React.Component<SkeletonParagraphProps, SkeletonParagraphState> {
static defaultProps: Partial<SkeletonParagraphProps> = {
prefixCls: 'ant-skeleton-paragraph',
};
static getDerivedStateFromProps(
props: SkeletonParagraphProps,
state: SkeletonParagraphState,
): Partial<SkeletonParagraphState> {
const { prevProps } = state;
const { width, rows = 2 } = props;
const newState: Partial<SkeletonParagraphState> = {
prevProps: props,
};
if (rows !== prevProps.rows || width !== prevProps.width) {
// Parse width list
let widthList = [];
if (width && Array.isArray(width)) {
widthList = width;
} else if (width && !Array.isArray(width)) {
widthList = [];
widthList[rows - 1] = width;
}
newState.widthList = widthList;
}
return newState;
}
state: SkeletonParagraphState = {
prevProps: {},
widthList: [],
};
render() {
const { widthList } = this.state;
const { prefixCls, className, style, rows } = this.props;
const rowList = [...Array(rows)].map((_, index) => (
<li key={index} style={{ width: widthList[index] || '100%' }} />
));
return (
<ul
className={classNames(prefixCls, className)}
style={style}
>
{rowList}
</ul>
);
}
}
polyfill(Paragraph);
export default Paragraph;

View File

@ -0,0 +1,28 @@
import * as React from 'react';
import classNames from 'classnames';
export interface SkeletonTitleProps {
prefixCls?: string;
className?: string;
style?: object;
width?: number | string;
}
class Title extends React.Component<SkeletonTitleProps, any> {
static defaultProps: Partial<SkeletonTitleProps> = {
prefixCls: 'ant-skeleton-title',
};
render() {
const { prefixCls, className, width, style } = this.props;
return (
<h3
className={classNames(prefixCls, className)}
style={{ width, ...style }}
/>
);
}
}
export default Title;

View File

@ -0,0 +1,204 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders ./components/skeleton/demo/active.md correctly 1`] = `
<div
class="ant-skeleton ant-skeleton-active"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width:38%"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width:100%"
/>
<li
style="width:100%"
/>
<li
style="width:61%"
/>
</ul>
</div>
</div>
`;
exports[`renders ./components/skeleton/demo/basic.md correctly 1`] = `
<div
class="ant-skeleton"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width:38%"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width:100%"
/>
<li
style="width:100%"
/>
<li
style="width:61%"
/>
</ul>
</div>
</div>
`;
exports[`renders ./components/skeleton/demo/children.md correctly 1`] = `
<div
class="article"
>
<div>
<h4>
Ant Design, a design language
</h4>
<p>
We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.
</p>
</div>
<button
class="ant-btn"
type="button"
>
<span>
Show Skeleton
</span>
</button>
</div>
`;
exports[`renders ./components/skeleton/demo/list.md correctly 1`] = `
<div>
<span
class="ant-switch"
tabindex="0"
>
<span
class="ant-switch-inner"
/>
</span>
<div
class="ant-list ant-list-vertical ant-list-lg ant-list-split"
>
<div
class="ant-spin-nested-loading"
>
<div
class="ant-spin-container"
>
<div
class="ant-list-item"
>
<div
class="ant-list-item-content ant-list-item-content-single"
>
<div
class="ant-skeleton ant-skeleton-active"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width:38%"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width:100%"
/>
<li
style="width:100%"
/>
<li
style="width:61%"
/>
</ul>
</div>
</div>
</div>
</div>
<div
class="ant-list-item"
>
<div
class="ant-list-item-content ant-list-item-content-single"
>
<div
class="ant-skeleton ant-skeleton-active"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width:38%"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width:100%"
/>
<li
style="width:100%"
/>
<li
style="width:61%"
/>
</ul>
</div>
</div>
</div>
</div>
<div
class="ant-list-item"
>
<div
class="ant-list-item-content ant-list-item-content-single"
>
<div
class="ant-skeleton ant-skeleton-active"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width:38%"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width:100%"
/>
<li
style="width:100%"
/>
<li
style="width:61%"
/>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -0,0 +1,279 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Skeleton avatar shape 1`] = `
<div
class="ant-skeleton ant-skeleton-with-avatar"
>
<div
class="ant-skeleton-header"
>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-lg ant-skeleton-avatar-circle"
/>
</div>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 50%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton avatar shape 2`] = `
<div
class="ant-skeleton ant-skeleton-with-avatar"
>
<div
class="ant-skeleton-header"
>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-lg ant-skeleton-avatar-square"
/>
</div>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 50%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton avatar size 1`] = `
<div
class="ant-skeleton ant-skeleton-with-avatar"
>
<div
class="ant-skeleton-header"
>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-sm ant-skeleton-avatar-circle"
/>
</div>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 50%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton avatar size 2`] = `
<div
class="ant-skeleton ant-skeleton-with-avatar"
>
<div
class="ant-skeleton-header"
>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-circle"
/>
</div>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 50%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton avatar size 3`] = `
<div
class="ant-skeleton ant-skeleton-with-avatar"
>
<div
class="ant-skeleton-header"
>
<span
class="ant-skeleton-avatar ant-skeleton-avatar-lg ant-skeleton-avatar-circle"
/>
</div>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 50%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton paragraph rows 1`] = `
<div
class="ant-skeleton"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 38%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
<li
style="width: 61%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton paragraph width 1`] = `
<div
class="ant-skeleton"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 38%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
<li
style="width: 93%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton paragraph width 2`] = `
<div
class="ant-skeleton"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 38%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 28%;"
/>
<li
style="width: 93%;"
/>
<li
style="width: 100%;"
/>
</ul>
</div>
</div>
`;
exports[`Skeleton title width 1`] = `
<div
class="ant-skeleton"
>
<div
class="ant-skeleton-content"
>
<h3
class="ant-skeleton-title"
style="width: 93%;"
/>
<ul
class="ant-skeleton-paragraph"
>
<li
style="width: 100%;"
/>
<li
style="width: 100%;"
/>
<li
style="width: 61%;"
/>
</ul>
</div>
</div>
`;

View File

@ -0,0 +1,3 @@
import demoTest from '../../../tests/shared/demoTest';
demoTest('skeleton');

View File

@ -0,0 +1,50 @@
import React from 'react';
import { mount } from 'enzyme';
import Skeleton from '..';
describe('Skeleton', () => {
const genSkeleton = props => mount(
<Skeleton loading {...props}>
Bamboo
</Skeleton>
);
describe('avatar', () => {
it('size', () => {
const wrapperSmall = genSkeleton({ avatar: { size: 'small' } });
expect(wrapperSmall.render()).toMatchSnapshot();
const wrapperDefault = genSkeleton({ avatar: { size: 'default' } });
expect(wrapperDefault.render()).toMatchSnapshot();
const wrapperLarge = genSkeleton({ avatar: { size: 'large' } });
expect(wrapperLarge.render()).toMatchSnapshot();
});
it('shape', () => {
const wrapperCircle = genSkeleton({ avatar: { shape: 'circle' } });
expect(wrapperCircle.render()).toMatchSnapshot();
const wrapperSquare = genSkeleton({ avatar: { shape: 'square' } });
expect(wrapperSquare.render()).toMatchSnapshot();
});
});
describe('title', () => {
it('width', () => {
const wrapper = genSkeleton({ title: { width: '93%' } });
expect(wrapper.render()).toMatchSnapshot();
});
});
describe('paragraph', () => {
it('rows', () => {
const wrapper = genSkeleton({ paragraph: { rows: 5 } });
expect(wrapper.render()).toMatchSnapshot();
});
it('width', () => {
const wrapperPure = genSkeleton({ paragraph: { width: '93%' } });
expect(wrapperPure.render()).toMatchSnapshot();
const wrapperList = genSkeleton({ paragraph: { width: ['28%', '93%'] } });
expect(wrapperList.render()).toMatchSnapshot();
});
});
});

View File

@ -0,0 +1,22 @@
---
order: 1
title:
zh-CN: 动画效果
en-US: Active Animation
---
## zh-CN
显示动画效果。
## en-US
Display active animation.
````jsx
import { Skeleton } from 'antd';
ReactDOM.render(
<Skeleton active />,
mountNode);
````

View File

@ -0,0 +1,22 @@
---
order: 0
title:
zh-CN: 基本
en-US: Basic
---
## zh-CN
最简单的用法。
## en-US
Basic usage.
````jsx
import { Skeleton } from 'antd';
ReactDOM.render(
<Skeleton />,
mountNode);
````

View File

@ -0,0 +1,58 @@
---
order: 2
title:
zh-CN: 包含子组件
en-US: Contains sub component
---
## zh-CN
加载占位图包含子组件。
## en-US
Skeleton contains sub component.
````jsx
import { Skeleton, Button } from 'antd';
class Demo extends React.Component {
state = {
loading: false,
};
showSkeleton = () => {
this.setState({ loading: true });
setTimeout(() => {
this.setState({ loading: false });
}, 3000);
};
render() {
return (
<div className="article">
<Skeleton loading={this.state.loading}>
<div>
<h4>Ant Design, a design language</h4>
<p>We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.</p>
</div>
</Skeleton>
<Button onClick={this.showSkeleton} disabled={this.state.loading}>
Show Skeleton
</Button>
</div>
);
}
}
ReactDOM.render(<Demo />, mountNode);
````
<style>
.article h4 {
margin-bottom: 16px;
}
.article button {
margin-top: 16px;
}
</style>

View File

@ -0,0 +1,86 @@
---
order: 3
title:
zh-CN: 列表
en-US: List
---
## zh-CN
在列表组件中使用加载占位符。
## en-US
Use skeleton in list component.
````jsx
import { Skeleton, Switch, List, Avatar, Icon } from 'antd';
const listData = [];
for (let i = 0; i < 3; i++) {
listData.push({
href: 'http://ant.design',
title: `ant design part ${i}`,
avatar: 'https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png',
description: 'Ant Design, a design language for background applications, is refined by Ant UED Team.',
content: 'We supply a series of design principles, practical patterns and high quality design resources (Sketch and Axure), to help people create their product prototypes beautifully and efficiently.',
});
}
const IconText = ({ type, text }) => (
<span>
<Icon type={type} style={{ marginRight: 8 }} />
{text}
</span>
);
class App extends React.Component {
state = {
loading: true,
}
onChange = (checked) => {
this.setState({ loading: !checked });
}
render() {
const { loading } = this.state;
return (
<div>
<Switch checked={!loading} onChange={this.onChange} />
<List
itemLayout="vertical"
size="large"
dataSource={listData}
renderItem={item => (
<List.Item
key={item.title}
actions={!loading && [<IconText type="star-o" text="156" />, <IconText type="like-o" text="156" />, <IconText type="message" text="2" />]}
extra={!loading && <img width={272} alt="logo" src="https://gw.alipayobjects.com/zos/rmsportal/mqaQswcyDLcXyDKnZfES.png" />}
>
<Skeleton loading={loading} active>
<List.Item.Meta
avatar={<Avatar src={item.avatar} />}
title={<a href={item.href}>{item.title}</a>}
description={item.description}
/>
{item.content}
</Skeleton>
</List.Item>
)}
/>
</div>
);
}
}
ReactDOM.render(<App />, mountNode);
````
<style>
.skeleton-demo {
border: 1px solid #f4f4f4;
}
</style>

View File

@ -0,0 +1,44 @@
---
category: Components
type: Data Entry
title: Skeleton
cols: 1
---
Provide a placeholder at the place which need waiting for loading.
## When To Use
- When resource needs long time to load, like low network speed.
- The component contains much information. Such as List or Card.
## API
### Skeleton
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| active | Show animation effect | boolean | false |
| avatar | Show avatar placeholder | boolean \| [SkeletonAvatarProps](#SkeletonAvatarProps) | false |
| loading | Display the skeleton when `true` | boolean | - |
| paragraph | Show paragraph placeholder | boolean \| [SkeletonParagraphProps](#SkeletonParagraphProps) | true |
| title | Show title placeholder | boolean \| [SkeletonTitleProps](#SkeletonTitleProps) | true |
### SkeletonAvatarProps
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| size | Set the size of avatar | Enum{ 'large', 'small', 'default' } | - |
| shape | Set the shape of avatar | Enum{ 'circle', 'square' } | - |
### SkeletonTitleProps
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| width | Set the width of title | number \| string | - |
### SkeletonParagraphProps
| Property | Description | Type | Default |
| --- | --- | --- | --- |
| rows | Set the row count of paragraph | number | - |
| width | Set the width of paragraph. When width is an Array, it can set the width of each row. Otherwise only set the last row width | number \| string \| Array<number \| string> | - |

View File

@ -0,0 +1,155 @@
import * as React from 'react';
import classNames from 'classnames';
import Avatar, { SkeletonAvatarProps } from './Avatar';
import Title, { SkeletonTitleProps } from './Title';
import Paragraph, { SkeletonParagraphProps } from './Paragraph';
export interface SkeletonProps {
active?: boolean;
loading?: boolean;
prefixCls?: string;
className?: string;
children?: React.ReactNode;
avatar?: SkeletonAvatarProps | boolean;
title?: SkeletonTitleProps | boolean;
paragraph?: SkeletonParagraphProps | boolean;
}
function getComponentProps<T>(prop: T | boolean | undefined): T | {} {
if (prop && typeof prop === 'object') {
return prop;
}
return {};
}
function getAvatarBasicProps(hasTitle: boolean, hasParagraph: boolean): SkeletonAvatarProps {
if (hasTitle && !hasParagraph) {
return { shape: 'square' };
}
return { shape: 'circle' };
}
function getTitleBasicProps(hasAvatar: boolean, hasParagraph: boolean): SkeletonTitleProps {
if (!hasAvatar && hasParagraph) {
return { width: '38%' };
}
if (hasAvatar && hasParagraph) {
return { width: '50%' };
}
return { width: '100%' };
}
function getParagraphBasicProps(hasAvatar: boolean, hasTitle: boolean): SkeletonParagraphProps {
const basicProps: SkeletonParagraphProps = {};
// Width
if (hasAvatar && hasTitle) {
basicProps.width = '100%';
} else {
basicProps.width = '61%';
}
// Rows
if (!hasAvatar && hasTitle) {
basicProps.rows = 3;
} else {
basicProps.rows = 2;
}
return basicProps;
}
class Skeleton extends React.Component<SkeletonProps, any> {
static defaultProps: Partial<SkeletonProps> = {
prefixCls: 'ant-skeleton',
avatar: false,
title: true,
paragraph: true,
};
render() {
const {
loading, prefixCls, className, children,
avatar, title, paragraph, active,
} = this.props;
if (loading || !('loading' in this.props)) {
const hasAvatar = !!avatar;
const hasTitle = !!title;
const hasParagraph = !!paragraph;
// Avatar
let avatarNode;
if (hasAvatar) {
const avatarProps: SkeletonAvatarProps = {
...getAvatarBasicProps(hasTitle, hasParagraph),
...getComponentProps(avatar),
};
avatarNode = (
<div className={`${prefixCls}-header`}>
<Avatar {...avatarProps} />
</div>
);
}
let contentNode;
if (hasTitle || hasParagraph) {
// Title
let $title;
if (hasTitle) {
const titleProps: SkeletonTitleProps = {
...getTitleBasicProps(hasAvatar, hasParagraph),
...getComponentProps(title),
};
$title = (
<Title {...titleProps} />
);
}
// Paragraph
let paragraphNode;
if (hasParagraph) {
const paragraphProps: SkeletonParagraphProps = {
...getParagraphBasicProps(hasAvatar, hasTitle),
...getComponentProps(paragraph),
};
paragraphNode = (
<Paragraph {...paragraphProps} />
);
}
contentNode = (
<div className={`${prefixCls}-content`}>
{$title}
{paragraphNode}
</div>
);
}
const cls = classNames(
prefixCls,
className, {
[`${prefixCls}-with-avatar`]: hasAvatar,
[`${prefixCls}-active`]: active,
},
);
return (
<div className={cls}>
{avatarNode}
{contentNode}
</div>
);
}
return children;
}
}
export default Skeleton;

View File

@ -0,0 +1,46 @@
---
category: Components
subtitle: 加载占位图
type: Data Entry
title: Skeleton
cols: 1
---
在需要等待加载内容的位置提供一个占位图。
## 何时使用
- 网络较慢,需要长时间等待加载处理的情况下。
- 图文信息内容较多的列表/卡片中。
## API
### Skeleton
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| active | 是否展示动画效果 | boolean | false |
| avatar | 是否显示头像占位图 | boolean \| [SkeletonAvatarProps](#SkeletonAvatarProps) | false |
| loading | 为 `true` 时,显示占位图。反之则直接展示子组件 | boolean | - |
| paragraph | 是否显示段落占位图 | boolean \| [SkeletonParagraphProps](#SkeletonParagraphProps) | true |
| title | 是否显示标题占位图 | boolean \| [SkeletonTitleProps](#SkeletonTitleProps) | true |
### SkeletonAvatarProps
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| size | 设置头像占位图的大小 | Enum{ 'large', 'small', 'default' } | - |
| shape | 指定头像的形状 | Enum{ 'circle', 'square' } | - |
### SkeletonTitleProps
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| width | 设置标题占位图的宽度 | number \| string | - |
### SkeletonParagraphProps
| 属性 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| rows | 设置段落占位图的行数 | number | - |
| width | 设置标题占位图的宽度,若为数组时则为对应的每行宽度,反之则是最后一行的宽度 | number \| string \| Array<number \| string> | - |

View File

@ -0,0 +1,113 @@
@import "../../style/themes/default";
@import "../../style/mixins/index";
@skeleton-prefix-cls: ~"@{ant-prefix}-skeleton";
@skeleton-avatar-prefix-cls: ~"@{skeleton-prefix-cls}-avatar";
@skeleton-title-prefix-cls: ~"@{skeleton-prefix-cls}-title";
@skeleton-paragraph-prefix-cls: ~"@{skeleton-prefix-cls}-paragraph";
@skeleton-to-color: shade(@skeleton-color, 5%);
.@{skeleton-prefix-cls} {
display: table;
width: 100%;
&-header {
display: table-cell;
vertical-align: top;
padding-right: 16px;
// Avatar
.@{skeleton-avatar-prefix-cls} {
display: inline-block;
vertical-align: top;
background: @skeleton-color;
.avatar-size(@avatar-size-base);
&-lg {
.avatar-size(@avatar-size-lg);
}
&-sm {
.avatar-size(@avatar-size-sm);
}
}
}
&-content {
display: table-cell;
vertical-align: top;
width: 100%;
// Title
.@{skeleton-title-prefix-cls} {
margin-top: 16px;
height: 16px;
width: 100%;
background: @skeleton-color;
+ .@{skeleton-paragraph-prefix-cls} {
margin-top: 24px;
}
}
// paragraph
.@{skeleton-paragraph-prefix-cls} {
> li {
height: 16px;
background: @skeleton-color;
+ li {
margin-top: 16px;
}
}
}
}
&-with-avatar &-content {
// Title
.@{skeleton-title-prefix-cls} {
margin-top: 12px;
+ .@{skeleton-paragraph-prefix-cls} {
margin-top: 28px;
}
}
}
// With active animation
&.@{skeleton-prefix-cls}-active {
& .@{skeleton-prefix-cls}-content {
.@{skeleton-title-prefix-cls},
.@{skeleton-paragraph-prefix-cls} > li {
.skeleton-color();
}
}
}
}
.avatar-size(@size) {
width: @size;
height: @size;
line-height: @size;
&.@{skeleton-avatar-prefix-cls}-circle {
border-radius: 50%;
}
}
.skeleton-color() {
background: linear-gradient(90deg, @skeleton-color 25%, @skeleton-to-color 37%, @skeleton-color 63%);
animation: ~"@{skeleton-prefix-cls}-loading" 1.4s ease infinite;
background-size: 400% 100%;
}
@keyframes ~"@{skeleton-prefix-cls}-loading" {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0 50%;
}
}

View File

@ -0,0 +1,2 @@
import '../../style/index.less';
import './index.less';

View File

@ -484,6 +484,10 @@
@collapse-content-padding: @padding-md;
@collapse-content-bg: @component-background;
// Skeleton
// ---
@skeleton-color: #f2f2f2;
// Message
// ---
@message-notice-content-padding: 10px 16px;

View File

@ -41,6 +41,7 @@ Array [
"Rate",
"Row",
"Select",
"Skeleton",
"Slider",
"Spin",
"Steps",