From 796b56dbc4786291cfd681eb68ef3fdc3f56d7c6 Mon Sep 17 00:00:00 2001 From: zombieJ Date: Wed, 22 Aug 2018 23:34:36 +0800 Subject: [PATCH] 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 --- .../__snapshots__/index.test.js.snap | 1 + .../__tests__/__snapshots__/demo.test.js.snap | 207 ++++--------- components/card/demo/loading.md | 32 +- components/index.tsx | 2 + .../__tests__/__snapshots__/demo.test.js.snap | 14 +- components/list/demo/loadmore.md | 46 +-- components/skeleton/Avatar.tsx | 37 +++ components/skeleton/Paragraph.tsx | 78 +++++ components/skeleton/Title.tsx | 28 ++ .../__tests__/__snapshots__/demo.test.js.snap | 204 +++++++++++++ .../__snapshots__/index.test.js.snap | 279 ++++++++++++++++++ components/skeleton/__tests__/demo.test.js | 3 + components/skeleton/__tests__/index.test.js | 50 ++++ components/skeleton/demo/active.md | 22 ++ components/skeleton/demo/basic.md | 22 ++ components/skeleton/demo/children.md | 58 ++++ components/skeleton/demo/list.md | 86 ++++++ components/skeleton/index.en-US.md | 44 +++ components/skeleton/index.tsx | 155 ++++++++++ components/skeleton/index.zh-CN.md | 46 +++ components/skeleton/style/index.less | 113 +++++++ components/skeleton/style/index.tsx | 2 + components/style/themes/default.less | 4 + tests/__snapshots__/index.test.js.snap | 1 + 24 files changed, 1342 insertions(+), 192 deletions(-) create mode 100644 components/skeleton/Avatar.tsx create mode 100644 components/skeleton/Paragraph.tsx create mode 100644 components/skeleton/Title.tsx create mode 100644 components/skeleton/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/skeleton/__tests__/__snapshots__/index.test.js.snap create mode 100644 components/skeleton/__tests__/demo.test.js create mode 100644 components/skeleton/__tests__/index.test.js create mode 100644 components/skeleton/demo/active.md create mode 100644 components/skeleton/demo/basic.md create mode 100644 components/skeleton/demo/children.md create mode 100644 components/skeleton/demo/list.md create mode 100644 components/skeleton/index.en-US.md create mode 100644 components/skeleton/index.tsx create mode 100644 components/skeleton/index.zh-CN.md create mode 100644 components/skeleton/style/index.less create mode 100644 components/skeleton/style/index.tsx diff --git a/components/__tests__/__snapshots__/index.test.js.snap b/components/__tests__/__snapshots__/index.test.js.snap index 53f98628bc..072e985c04 100644 --- a/components/__tests__/__snapshots__/index.test.js.snap +++ b/components/__tests__/__snapshots__/index.test.js.snap @@ -41,6 +41,7 @@ Array [ "Rate", "Row", "Select", + "Skeleton", "Slider", "Spin", "Steps", diff --git a/components/card/__tests__/__snapshots__/demo.test.js.snap b/components/card/__tests__/__snapshots__/demo.test.js.snap index 6d7c6f3147..fc7c064605 100644 --- a/components/card/__tests__/__snapshots__/demo.test.js.snap +++ b/components/card/__tests__/__snapshots__/demo.test.js.snap @@ -372,174 +372,83 @@ exports[`renders ./components/card/demo/inner.md correctly 1`] = ` exports[`renders ./components/card/demo/loading.md correctly 1`] = `
+ + +
-
-
-
- Card title -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
+
    -
    -
    -
    -
    -
    +
+
    +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
-
`; diff --git a/components/card/demo/loading.md b/components/card/demo/loading.md index 408d44d240..c3e39deb0a 100644 --- a/components/card/demo/loading.md +++ b/components/card/demo/loading.md @@ -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 (
- - Whatever content + + + , , ]} + > + + } + title="Card title" + description="This is the description" + /> + -
); } } -ReactDOM.render( - , - mountNode); +ReactDOM.render(, mountNode); ```` diff --git a/components/index.tsx b/components/index.tsx index bbb3c7bebf..9882a27327 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -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'; diff --git a/components/list/__tests__/__snapshots__/demo.test.js.snap b/components/list/__tests__/__snapshots__/demo.test.js.snap index 8a28d3822d..be4e0a8ab9 100644 --- a/components/list/__tests__/__snapshots__/demo.test.js.snap +++ b/components/list/__tests__/__snapshots__/demo.test.js.snap @@ -365,7 +365,7 @@ exports[`renders ./components/list/demo/infinite-virtualized-load.md correctly 1 exports[`renders ./components/list/demo/loadmore.md correctly 1`] = `
-
- -
`; diff --git a/components/list/demo/loadmore.md b/components/list/demo/loadmore.md index 3bea685955..c5a1884af2 100644 --- a/components/list/demo/loadmore.md +++ b/components/list/demo/loadmore.md @@ -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 ? (
- {loadingMore && } - {!loadingMore && } +
) : null; + return ( ( edit, more]}> - } - title={{item.name.last}} - description="Ant Design, a design language for background applications, is refined by Ant UED Team" - /> -
content
+ + } + title={{item.name.last}} + description="Ant Design, a design language for background applications, is refined by Ant UED Team" + /> +
content
+
)} /> diff --git a/components/skeleton/Avatar.tsx b/components/skeleton/Avatar.tsx new file mode 100644 index 0000000000..b31e313f1e --- /dev/null +++ b/components/skeleton/Avatar.tsx @@ -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 { + static defaultProps: Partial = { + 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 ( + + ); + } +} + +export default Title; diff --git a/components/skeleton/Paragraph.tsx b/components/skeleton/Paragraph.tsx new file mode 100644 index 0000000000..9d6982aa38 --- /dev/null +++ b/components/skeleton/Paragraph.tsx @@ -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; + rows?: number; +} + +interface SkeletonParagraphState { + prevProps: SkeletonParagraphProps; + widthList: Array; +} + +class Paragraph extends React.Component { + static defaultProps: Partial = { + prefixCls: 'ant-skeleton-paragraph', + }; + + static getDerivedStateFromProps( + props: SkeletonParagraphProps, + state: SkeletonParagraphState, + ): Partial { + const { prevProps } = state; + const { width, rows = 2 } = props; + + const newState: Partial = { + 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) => ( +
  • + )); + + return ( +
      + {rowList} +
    + ); + } +} + +polyfill(Paragraph); + +export default Paragraph; diff --git a/components/skeleton/Title.tsx b/components/skeleton/Title.tsx new file mode 100644 index 0000000000..507f4d697b --- /dev/null +++ b/components/skeleton/Title.tsx @@ -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 { + static defaultProps: Partial = { + prefixCls: 'ant-skeleton-title', + }; + + render() { + const { prefixCls, className, width, style } = this.props; + + return ( +

    + ); + } +} + +export default Title; diff --git a/components/skeleton/__tests__/__snapshots__/demo.test.js.snap b/components/skeleton/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 0000000000..1f4980b346 --- /dev/null +++ b/components/skeleton/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,204 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/skeleton/demo/active.md correctly 1`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +`; + +exports[`renders ./components/skeleton/demo/basic.md correctly 1`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +`; + +exports[`renders ./components/skeleton/demo/children.md correctly 1`] = ` +
    +
    +

    + Ant Design, a design language +

    +

    + 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. +

    +
    + +
    +`; + +exports[`renders ./components/skeleton/demo/list.md correctly 1`] = ` +
    + + + +
    +
    +
    +
    +
    +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +
    +
    +
    +
    +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +
    +
    +
    +
    +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +
    +
    +
    +
    +
    +
    +`; diff --git a/components/skeleton/__tests__/__snapshots__/index.test.js.snap b/components/skeleton/__tests__/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..bac82f863c --- /dev/null +++ b/components/skeleton/__tests__/__snapshots__/index.test.js.snap @@ -0,0 +1,279 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Skeleton avatar shape 1`] = ` +
    +
    + +
    +
    +

    +
      +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton avatar shape 2`] = ` +
    +
    + +
    +
    +

    +
      +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton avatar size 1`] = ` +
    +
    + +
    +
    +

    +
      +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton avatar size 2`] = ` +
    +
    + +
    +
    +

    +
      +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton avatar size 3`] = ` +
    +
    + +
    +
    +

    +
      +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton paragraph rows 1`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton paragraph width 1`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton paragraph width 2`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +`; + +exports[`Skeleton title width 1`] = ` +
    +
    +

    +
      +
    • +
    • +
    • +
    +

    +
    +`; diff --git a/components/skeleton/__tests__/demo.test.js b/components/skeleton/__tests__/demo.test.js new file mode 100644 index 0000000000..2fb54f3080 --- /dev/null +++ b/components/skeleton/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import demoTest from '../../../tests/shared/demoTest'; + +demoTest('skeleton'); diff --git a/components/skeleton/__tests__/index.test.js b/components/skeleton/__tests__/index.test.js new file mode 100644 index 0000000000..22697a6761 --- /dev/null +++ b/components/skeleton/__tests__/index.test.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Skeleton from '..'; + +describe('Skeleton', () => { + const genSkeleton = props => mount( + + Bamboo + + ); + + 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(); + }); + }); +}); diff --git a/components/skeleton/demo/active.md b/components/skeleton/demo/active.md new file mode 100644 index 0000000000..5249130cdd --- /dev/null +++ b/components/skeleton/demo/active.md @@ -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( + , +mountNode); +```` diff --git a/components/skeleton/demo/basic.md b/components/skeleton/demo/basic.md new file mode 100644 index 0000000000..189ed7ef4f --- /dev/null +++ b/components/skeleton/demo/basic.md @@ -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( + , +mountNode); +```` diff --git a/components/skeleton/demo/children.md b/components/skeleton/demo/children.md new file mode 100644 index 0000000000..1091081777 --- /dev/null +++ b/components/skeleton/demo/children.md @@ -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 ( +
    + +
    +

    Ant Design, a design language

    +

    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.

    +
    +
    + +
    + ); + } +} + +ReactDOM.render(, mountNode); +```` + + diff --git a/components/skeleton/demo/list.md b/components/skeleton/demo/list.md new file mode 100644 index 0000000000..5c14a55554 --- /dev/null +++ b/components/skeleton/demo/list.md @@ -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 }) => ( + + + {text} + +); + +class App extends React.Component { + state = { + loading: true, + } + + onChange = (checked) => { + this.setState({ loading: !checked }); + } + + render() { + const { loading } = this.state; + + return ( +
    + + + ( + , , ]} + extra={!loading && logo} + > + + } + title={{item.title}} + description={item.description} + /> + {item.content} + + + )} + /> +
    + ); + } +} + +ReactDOM.render(, mountNode); +```` + + diff --git a/components/skeleton/index.en-US.md b/components/skeleton/index.en-US.md new file mode 100644 index 0000000000..607db7d533 --- /dev/null +++ b/components/skeleton/index.en-US.md @@ -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 | - | diff --git a/components/skeleton/index.tsx b/components/skeleton/index.tsx new file mode 100644 index 0000000000..bf2064b5f4 --- /dev/null +++ b/components/skeleton/index.tsx @@ -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(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 { + static defaultProps: Partial = { + 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 = ( +
    + +
    + ); + } + + let contentNode; + if (hasTitle || hasParagraph) { + // Title + let $title; + if (hasTitle) { + const titleProps: SkeletonTitleProps = { + ...getTitleBasicProps(hasAvatar, hasParagraph), + ...getComponentProps(title), + }; + + $title = ( + + ); + } + + // 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; diff --git a/components/skeleton/index.zh-CN.md b/components/skeleton/index.zh-CN.md new file mode 100644 index 0000000000..f515513827 --- /dev/null +++ b/components/skeleton/index.zh-CN.md @@ -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> | - | diff --git a/components/skeleton/style/index.less b/components/skeleton/style/index.less new file mode 100644 index 0000000000..b2e0ec93e6 --- /dev/null +++ b/components/skeleton/style/index.less @@ -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%; + } +} diff --git a/components/skeleton/style/index.tsx b/components/skeleton/style/index.tsx new file mode 100644 index 0000000000..3a3ab0de59 --- /dev/null +++ b/components/skeleton/style/index.tsx @@ -0,0 +1,2 @@ +import '../../style/index.less'; +import './index.less'; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 81625aa2f0..c65b0b65a1 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -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; diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index 1f82a35b7c..d0acf60bcc 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -41,6 +41,7 @@ Array [ "Rate", "Row", "Select", + "Skeleton", "Slider", "Spin", "Steps",