From 2d05fd465b9a5b746982035ede8c7f71cb82e33b Mon Sep 17 00:00:00 2001 From: Wenqi <1264578441@qq.com> Date: Mon, 17 Sep 2018 10:50:46 +0800 Subject: [PATCH] feat(module:skeleton):add skeleton component (#1829) * feat(module:skeleton): add skeleton component * fix(component:skeleton):sync demo to antd --- components/card/demo/loading.ts | 30 +++- components/components.less | 1 + components/list/demo/loadmore.ts | 41 ++--- components/ng-zorro-antd.module.ts | 5 +- components/skeleton/demo/active.md | 13 ++ components/skeleton/demo/active.ts | 9 ++ components/skeleton/demo/basic.md | 14 ++ components/skeleton/demo/basic.ts | 9 ++ components/skeleton/demo/children.md | 14 ++ components/skeleton/demo/children.ts | 36 +++++ components/skeleton/demo/complex.md | 14 ++ components/skeleton/demo/complex.ts | 9 ++ components/skeleton/demo/list.md | 14 ++ components/skeleton/demo/list.ts | 40 +++++ components/skeleton/doc/index.en-US.md | 46 ++++++ components/skeleton/doc/index.zh-CN.md | 47 ++++++ components/skeleton/index.ts | 1 + .../skeleton/nz-skeleton.component.html | 18 +++ components/skeleton/nz-skeleton.component.ts | 117 ++++++++++++++ components/skeleton/nz-skeleton.module.ts | 10 ++ components/skeleton/nz-skeleton.spec.ts | 150 ++++++++++++++++++ components/skeleton/nz-skeleton.type.ts | 19 +++ components/skeleton/public-api.ts | 3 + components/skeleton/style/index.less | 127 +++++++++++++++ components/style/themes/default.less | 4 + 25 files changed, 766 insertions(+), 25 deletions(-) create mode 100644 components/skeleton/demo/active.md create mode 100644 components/skeleton/demo/active.ts create mode 100644 components/skeleton/demo/basic.md create mode 100644 components/skeleton/demo/basic.ts create mode 100644 components/skeleton/demo/children.md create mode 100644 components/skeleton/demo/children.ts create mode 100644 components/skeleton/demo/complex.md create mode 100644 components/skeleton/demo/complex.ts create mode 100644 components/skeleton/demo/list.md create mode 100644 components/skeleton/demo/list.ts create mode 100644 components/skeleton/doc/index.en-US.md create mode 100644 components/skeleton/doc/index.zh-CN.md create mode 100644 components/skeleton/index.ts create mode 100644 components/skeleton/nz-skeleton.component.html create mode 100644 components/skeleton/nz-skeleton.component.ts create mode 100644 components/skeleton/nz-skeleton.module.ts create mode 100644 components/skeleton/nz-skeleton.spec.ts create mode 100644 components/skeleton/nz-skeleton.type.ts create mode 100644 components/skeleton/public-api.ts create mode 100644 components/skeleton/style/index.less diff --git a/components/card/demo/loading.ts b/components/card/demo/loading.ts index b3b994ce87b..79a317371f5 100644 --- a/components/card/demo/loading.ts +++ b/components/card/demo/loading.ts @@ -3,15 +3,33 @@ import { Component } from '@angular/core'; @Component({ selector: 'nz-demo-card-loading', template: ` - - Whatever content + + + - + + + + + + + + + + + + + + + + + ` }) export class NzDemoCardLoadingComponent { loading = true; - toggleLoading(): void { - this.loading = !this.loading; - } } diff --git a/components/components.less b/components/components.less index 03e688ed851..3cfb0b6caf9 100644 --- a/components/components.less +++ b/components/components.less @@ -33,6 +33,7 @@ @import "./radio/style/index.less"; @import "./rate/style/index.less"; @import "./select/style/index.less"; +@import "./skeleton/style/index.less"; @import "./slider/style/index.less"; @import "./spin/style/index.less"; @import "./steps/style/index.less"; diff --git a/components/list/demo/loadmore.ts b/components/list/demo/loadmore.ts index 1080b202e55..dbb19273057 100644 --- a/components/list/demo/loadmore.ts +++ b/components/list/demo/loadmore.ts @@ -3,6 +3,7 @@ import { HttpClient } from '@angular/common/http'; import { Component, OnInit } from '@angular/core'; import { NzMessageService } from 'ng-zorro-antd'; +const count = 5; const fakeDataUrl = 'https://randomuser.me/api/?results=5&inc=name,gender,email,nat&noinfo'; @Component({ @@ -10,29 +11,30 @@ const fakeDataUrl = 'https://randomuser.me/api/?results=5&inc=name,gender,email, template: ` - - edit - more - + + edit + more + - {{item.name.last}} + {{item.name.last}} - + + -
+
-
@@ -50,33 +52,36 @@ const fakeDataUrl = 'https://randomuser.me/api/?results=5&inc=name,gender,email, ` ] }) export class NzDemoListLoadmoreComponent implements OnInit { - loading = true; // bug + initLoading = true; // bug loadingMore = false; - showLoadingMore = true; data = []; + list = []; constructor(private http: HttpClient, private msg: NzMessageService) {} ngOnInit(): void { - this.getData((res: any) => { - this.data = res.results; - this.loading = false; - }); + this.getData((res: any) => { + this.data = res.results; + this.list = res.results; + this.initLoading = false; + }); } getData(callback: (res: any) => void): void { - this.http.get(fakeDataUrl).subscribe((res: any) => callback(res)); + this.http.get(fakeDataUrl).subscribe((res: any) => callback(res)); } onLoadMore(): void { this.loadingMore = true; + this.list = this.data.concat([...Array(count)].fill({}).map(() => ({ loading: true, name: {} }))); this.http.get(fakeDataUrl).subscribe((res: any) => { this.data = this.data.concat(res.results); + this.list = [...this.data]; this.loadingMore = false; }); } edit(item: any): void { - this.msg.success(item.email); + this.msg.success(item.email); } } diff --git a/components/ng-zorro-antd.module.ts b/components/ng-zorro-antd.module.ts index aaf57d4444f..1dfc0261bcc 100644 --- a/components/ng-zorro-antd.module.ts +++ b/components/ng-zorro-antd.module.ts @@ -37,6 +37,7 @@ import { NzProgressModule } from './progress/nz-progress.module'; import { NzRadioModule } from './radio/nz-radio.module'; import { NzRateModule } from './rate/nz-rate.module'; import { NzSelectModule } from './select/nz-select.module'; +import { NzSkeletonModule } from './skeleton/nz-skeleton.module'; import { NzSliderModule } from './slider/nz-slider.module'; import { NzSpinModule } from './spin/nz-spin.module'; import { NzStepsModule } from './steps/nz-steps.module'; @@ -96,6 +97,7 @@ export * from './auto-complete'; export * from './message'; export * from './time-picker'; export * from './tooltip'; +export * from './skeleton'; export * from './slider'; export * from './popover'; export * from './notification'; @@ -163,7 +165,8 @@ export * from './core/wave'; NzTreeModule, NzTreeSelectModule, NzTimePickerModule, - NzWaveModule + NzWaveModule, + NzSkeletonModule ] }) export class NgZorroAntdModule { diff --git a/components/skeleton/demo/active.md b/components/skeleton/demo/active.md new file mode 100644 index 00000000000..a7717ff8cc8 --- /dev/null +++ b/components/skeleton/demo/active.md @@ -0,0 +1,13 @@ +--- +order: 2 +title: + zh-CN: 动画效果 + en-US: Active Animation +--- + ## zh-CN + + 显示动画效果。 + + ## en-US + + Display active animation. \ No newline at end of file diff --git a/components/skeleton/demo/active.ts b/components/skeleton/demo/active.ts new file mode 100644 index 00000000000..cdf52a5d172 --- /dev/null +++ b/components/skeleton/demo/active.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-skeleton-active', + template: ` + + ` +}) +export class NzDemoSkeletonActiveComponent { } diff --git a/components/skeleton/demo/basic.md b/components/skeleton/demo/basic.md new file mode 100644 index 00000000000..06be1a9e12a --- /dev/null +++ b/components/skeleton/demo/basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +最简单的用法。 + +## en-US + +Basic usage. \ No newline at end of file diff --git a/components/skeleton/demo/basic.ts b/components/skeleton/demo/basic.ts new file mode 100644 index 00000000000..a8f24b1da7d --- /dev/null +++ b/components/skeleton/demo/basic.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-skeleton-basic', + template: ` + + ` +}) +export class NzDemoSkeletonBasicComponent { } diff --git a/components/skeleton/demo/children.md b/components/skeleton/demo/children.md new file mode 100644 index 00000000000..bd6dcb61dd0 --- /dev/null +++ b/components/skeleton/demo/children.md @@ -0,0 +1,14 @@ +--- +order: 3 +title: + zh-CN: 包含子组件 + en-US: Contains sub component +--- + + ## zh-CN + + 加载占位图包含子组件。 + + ## en-US + + Skeleton contains sub component. diff --git a/components/skeleton/demo/children.ts b/components/skeleton/demo/children.ts new file mode 100644 index 00000000000..d8fbca3f938 --- /dev/null +++ b/components/skeleton/demo/children.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-skeleton-children', + template: ` +
+ +

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.

+
+ +
+ `, + styles : [ + ` + .article h4 { + margin-bottom: 16px; + } + .article button { + margin-top: 16px; + } + ` + ] +}) +export class NzDemoSkeletonChildrenComponent { + loading = false; + + showSkeleton(): void { + this.loading = true; + setTimeout(() => { + this.loading = false; + }, 3000); + } +} diff --git a/components/skeleton/demo/complex.md b/components/skeleton/demo/complex.md new file mode 100644 index 00000000000..6cef3a944dd --- /dev/null +++ b/components/skeleton/demo/complex.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh-CN: 复杂的组合 + en-US: Complex combination +--- + +## zh-CN + +更复杂的组合。 + +## en-US + +Complex combination with avatar and multiple paragraphs. \ No newline at end of file diff --git a/components/skeleton/demo/complex.ts b/components/skeleton/demo/complex.ts new file mode 100644 index 00000000000..e84122136f3 --- /dev/null +++ b/components/skeleton/demo/complex.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-skeleton-complex', + template: ` + + ` +}) +export class NzDemoSkeletonComplexComponent { } diff --git a/components/skeleton/demo/list.md b/components/skeleton/demo/list.md new file mode 100644 index 00000000000..5767637cf79 --- /dev/null +++ b/components/skeleton/demo/list.md @@ -0,0 +1,14 @@ +--- +order: 4 +title: + zh-CN: 列表样例 + en-US: List Sample +--- + +## zh-CN + +在列表组件中使用加载占位符。 + +## en-US + +Use skeleton in list component. \ No newline at end of file diff --git a/components/skeleton/demo/list.ts b/components/skeleton/demo/list.ts new file mode 100644 index 00000000000..0d4171d1417 --- /dev/null +++ b/components/skeleton/demo/list.ts @@ -0,0 +1,40 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-skeleton-list', + template: ` + + + + + + 156 + 156 + 2 + + {{item.title}} + + + logo + + + + + + ` +}) +export class NzDemoSkeletonListComponent { + loading = true; + listData = new Array(3).fill({}).map((i, index) => { + return { + href: 'http://ng.ant.design', + title: `ant design part ${index}`, + 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.' + }; + }); +} diff --git a/components/skeleton/doc/index.en-US.md b/components/skeleton/doc/index.en-US.md new file mode 100644 index 00000000000..5e42e544966 --- /dev/null +++ b/components/skeleton/doc/index.en-US.md @@ -0,0 +1,46 @@ +--- +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 need long time loading, like low network speed. +- The component contains much information, such as List or Card. + +## API + +### nz-skeleton + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| `[nzActive]` | Show animation effect | boolean | false | +| `[nzAvatar]` | Show avatar placeholder | boolean | NzSkeletonAvatar | false | +| `[nzLoading]` | Display the skeleton when `true` | boolean | - | +| `[nzParagraph]` | Show paragraph placeholder | boolean | NzSkeletonParagraph | true | +| `[nzTitle]` | Show title placeholder | boolean | NzSkeletonTitle | true | + + +### NzSkeletonAvatar + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| `size` | Set the size of avatar | Enum{ 'large', 'small', 'default' } | - | +| `shape` | Set the shape of avatar | Enum{ 'circle', 'square' } | - | + +### NzSkeletonTitle + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| `width` | Set the width of title | number | string | - | + +### NzSkeletonParagraph + +| 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 | - | \ No newline at end of file diff --git a/components/skeleton/doc/index.zh-CN.md b/components/skeleton/doc/index.zh-CN.md new file mode 100644 index 00000000000..24cb031f712 --- /dev/null +++ b/components/skeleton/doc/index.zh-CN.md @@ -0,0 +1,47 @@ +--- +category: Components +subtitle: 加载占位图 +type: Data Entry +title: Skeleton +cols: 1 +--- + +在需要等待加载内容的位置提供一个占位图。 + +## 何时使用 + +- 网络较慢,需要长时间等待加载处理的情况下。 +- 图文信息内容较多的列表/卡片中。 + + +## API + +### nz-skeleton + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `[nzActive]` | 是否展示动画效果 | boolean | false | +| `[nzAvatar]` | 是否显示头像占位图 | boolean | NzSkeletonAvatar | false | +| `[nzLoading]` | 为 `true` 时,显示占位图。反之则直接展示子组件 | boolean | - | +| `[nzParagraph]` | 是否显示段落占位图 | boolean | NzSkeletonParagraph | true | +| `[nzTitle]` | 是否显示标题占位图 | boolean | NzSkeletonTitle | true | + +### NzSkeletonAvatar + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `size` | 设置头像占位图的大小 | Enum{ 'large', 'small', 'default' } | - | +| `shape` | 指定头像的形状 | Enum{ 'circle', 'square' } | - | + +### NzSkeletonTitle + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `width` | 设置标题占位图的宽度 | number | string | - | + +### NzSkeletonParagraph + +| 属性 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| `rows` | 设置段落占位图的行数 | number | - | +| `width` | 设置标题占位图的宽度,若为数组时则为对应的每行宽度,反之则是最后一行的宽度 | number | string | Array | - | \ No newline at end of file diff --git a/components/skeleton/index.ts b/components/skeleton/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/components/skeleton/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/components/skeleton/nz-skeleton.component.html b/components/skeleton/nz-skeleton.component.html new file mode 100644 index 00000000000..07d08595ba4 --- /dev/null +++ b/components/skeleton/nz-skeleton.component.html @@ -0,0 +1,18 @@ + +
+ + +
+
+

+
    +
  • +
  • +
+
+
+ + + \ No newline at end of file diff --git a/components/skeleton/nz-skeleton.component.ts b/components/skeleton/nz-skeleton.component.ts new file mode 100644 index 00000000000..a5a9aa707f5 --- /dev/null +++ b/components/skeleton/nz-skeleton.component.ts @@ -0,0 +1,117 @@ +import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { AvatarShape, AvatarSize, NzSkeletonAvatar, NzSkeletonParagraph, NzSkeletonTitle } from './nz-skeleton.type'; + +@Component({ + selector: 'nz-skeleton', + templateUrl: './nz-skeleton.component.html', + host: { + '[class.ant-skeleton]': 'true', + '[class.ant-skeleton-with-avatar]': '!!nzAvatar', + '[class.ant-skeleton-active]': 'nzActive' + } +}) +export class NzSkeletonComponent implements OnInit, OnChanges { + title: NzSkeletonTitle; + avatar: NzSkeletonAvatar; + paragraph: NzSkeletonParagraph; + avatarClassMap; + rowsList: number[] = []; + widthList: Array = []; + + @Input() nzActive = false; + @Input() nzLoading = true; + @Input() nzTitle: NzSkeletonTitle | boolean = true; + @Input() nzAvatar: NzSkeletonAvatar | boolean = false; + @Input() nzParagraph: NzSkeletonParagraph | boolean = true; + + private getTitleProps(): NzSkeletonTitle { + const hasAvatar: boolean = !!this.nzAvatar; + const hasParagraph: boolean = !!this.nzParagraph; + let width: string; + if (!hasAvatar && hasParagraph) { + width = '38%'; + } else if (hasAvatar && hasParagraph) { + width = '50%'; + } + return { width, ...this.getProps(this.nzTitle) }; + } + + private getAvatarProps(): NzSkeletonAvatar { + const shape: AvatarShape = (!!this.nzTitle && !this.nzParagraph) ? 'square' : 'circle'; + const size: AvatarSize = 'large'; + return { shape, size, ...this.getProps(this.nzAvatar) }; + } + + private getParagraphProps(): NzSkeletonParagraph { + const hasAvatar: boolean = !!this.nzAvatar; + const hasTitle: boolean = !!this.nzTitle; + const basicProps: NzSkeletonParagraph = {}; + // Width + if (!hasAvatar || !hasTitle) { + basicProps.width = '61%'; + } + // Rows + if (!hasAvatar && hasTitle) { + basicProps.rows = 3; + } else { + basicProps.rows = 2; + } + return { ...basicProps, ...this.getProps(this.nzParagraph) }; + } + + private getProps(prop: T | boolean | undefined): T | {} { + if (prop && typeof prop === 'object') { + return prop; + } + return {}; + } + + toCSSUnit(value: number | string = ''): string { + if (typeof value === 'number') { + return `${value}px`; + } else if (typeof value === 'string') { + return value; + } + } + + private getWidthList(): Array { + const { width, rows } = this.paragraph; + let widthList = []; + if (width && Array.isArray(width)) { + widthList = width; + } else if (width && !Array.isArray(width)) { + widthList = []; + widthList[rows - 1] = width; + } + return widthList; + } + + updateClassMap(): void { + this.avatarClassMap = { + [ `ant-skeleton-avatar-lg` ] : this.avatar.size === 'large', + [ `ant-skeleton-avatar-sm ` ] : this.avatar.size === 'small', + [ `ant-skeleton-avatar-circle` ] : this.avatar.shape === 'circle', + [ `ant-skeleton-avatar-square ` ]: this.avatar.shape === 'square' + }; + } + + updateProps(): void { + this.title = this.getTitleProps(); + this.avatar = this.getAvatarProps(); + this.paragraph = this.getParagraphProps(); + this.rowsList = [...Array(this.paragraph.rows)]; + this.widthList = this.getWidthList(); + } + + ngOnInit(): void { + this.updateProps(); + this.updateClassMap(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.nzTitle || changes.nzAvatar || changes.nzParagraph) { + this.updateProps(); + this.updateClassMap(); + } + } +} diff --git a/components/skeleton/nz-skeleton.module.ts b/components/skeleton/nz-skeleton.module.ts new file mode 100644 index 00000000000..9001b775550 --- /dev/null +++ b/components/skeleton/nz-skeleton.module.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NzSkeletonComponent } from './nz-skeleton.component'; + +@NgModule({ + declarations: [ NzSkeletonComponent ], + imports: [ CommonModule ], + exports: [ NzSkeletonComponent ] +}) +export class NzSkeletonModule {} diff --git a/components/skeleton/nz-skeleton.spec.ts b/components/skeleton/nz-skeleton.spec.ts new file mode 100644 index 00000000000..77f30513300 --- /dev/null +++ b/components/skeleton/nz-skeleton.spec.ts @@ -0,0 +1,150 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NzSkeletonModule } from './nz-skeleton.module'; + +describe('skeleton', () => { + let fixture: ComponentFixture; + let testComp: NzTestSkeletonComponent; + let dl: DebugElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NzSkeletonModule], + declarations: [NzTestSkeletonComponent] + }).compileComponents(); + fixture = TestBed.createComponent(NzTestSkeletonComponent); + testComp = fixture.componentInstance; + dl = fixture.debugElement; + fixture.detectChanges(); + }); + + describe('#nzActive', () => { + it('should active work', () => { + expect(dl.nativeElement.querySelector('.ant-skeleton-active')).toBeFalsy(); + testComp.nzActive = true; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-active')).toBeTruthy(); + }); + }); + + describe('#nzTitle', () => { + it('should basic width prop work', () => { + expect(dl.nativeElement.querySelector('.ant-skeleton-title')).toBeFalsy(); + testComp.nzTitle = true; + testComp.nzAvatar = false; + testComp.nzParagraph = true; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe('38%'); + testComp.nzAvatar = true; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe('50%'); + testComp.nzParagraph = false; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe(''); + }); + it('should customize width props work', () => { + testComp.nzTitle = true; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe(''); + testComp.nzTitle = { width: '50%' }; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe('50%'); + testComp.nzTitle = { width: 200 }; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-title').style.width).toBe('200px'); + }); + }); + + describe('#nzAvatar', () => { + it('should basic avatar props work', () => { + testComp.nzTitle = true; + testComp.nzAvatar = true; + testComp.nzParagraph = false; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-avatar-square')).toBeTruthy(); + expect(dl.nativeElement.querySelector('.ant-skeleton-with-avatar')).toBeTruthy(); + testComp.nzParagraph = true; + fixture.detectChanges(); + expect(dl.nativeElement.querySelector('.ant-skeleton-avatar-circle')).toBeTruthy(); + }); + for (const type of ['square', 'circle']) { + it(`should customize shape ${type} work`, () => { + testComp.nzAvatar = { shape: type }; + fixture.detectChanges(); + expect(dl.query(By.css(`.ant-skeleton-avatar-${type}`)) !== null).toBe(true); + }); + } + for (const type of [{ size: 'large', cls: 'lg' }, { size: 'small', cls: 'sm' }]) { + it(`should customize size ${type.size} work`, () => { + testComp.nzAvatar = { size: type.size }; + fixture.detectChanges(); + expect(dl.query(By.css(`.ant-skeleton-avatar-${type.cls}`)) !== null).toBe(true); + }); + } + }); + + describe('#nzParagraph', () => { + it('should basic rows and width work', () => { + testComp.nzTitle = true; + testComp.nzAvatar = true; + testComp.nzParagraph = true; + fixture.detectChanges(); + let paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs.length).toBe(2); + expect(paragraphs[0].style.width).toBe(''); + expect(paragraphs[1].style.width).toBe(''); + testComp.nzAvatar = false; + fixture.detectChanges(); + paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs.length).toBe(3); + expect(paragraphs[1].style.width).toBe(''); + expect(paragraphs[2].style.width).toBe('61%'); + }); + it('should width type is string or number work', () => { + testComp.nzParagraph = { width: 100 }; + fixture.detectChanges(); + let paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs[0].style.width).toBe(''); + expect(paragraphs[1].style.width).toBe('100px'); + expect(paragraphs[2]).toBeFalsy(); + testComp.nzParagraph = { width: 100, rows: 3 }; + fixture.detectChanges(); + paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs[1].style.width).toBe(''); + expect(paragraphs[2].style.width).toBe('100px'); + }); + it('should define width type is Array work', () => { + testComp.nzParagraph = { width: [200, '100px', '90%'] }; + fixture.detectChanges(); + let paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs[0].style.width).toBe('200px'); + expect(paragraphs[1].style.width).toBe('100px'); + expect(paragraphs[2]).toBeFalsy(); + testComp.nzParagraph = { width: [200, '100px', '90%'], rows: 4 }; + fixture.detectChanges(); + paragraphs = dl.nativeElement.querySelectorAll('.ant-skeleton-paragraph > li'); + expect(paragraphs[2].style.width).toBe('90%'); + expect(paragraphs[3].style.width).toBe(''); + }); + }); +}); + +@Component({ + selector: 'nz-test-skeleton', + template: ` + + + ` +}) +export class NzTestSkeletonComponent { + nzActive; + nzAvatar; + nzTitle; + nzParagraph; +} diff --git a/components/skeleton/nz-skeleton.type.ts b/components/skeleton/nz-skeleton.type.ts new file mode 100644 index 00000000000..f9465d99074 --- /dev/null +++ b/components/skeleton/nz-skeleton.type.ts @@ -0,0 +1,19 @@ +export type ParagraphWidth = number | string | Array; + +export type AvatarShape = 'square' | 'circle'; + +export type AvatarSize = 'small' | 'large' | 'default'; + +export interface NzSkeletonAvatar { + size?: AvatarSize; + shape?: AvatarShape; +} + +export interface NzSkeletonTitle { + width?: number | string; +} + +export interface NzSkeletonParagraph { + rows?: number; + width?: ParagraphWidth; +} diff --git a/components/skeleton/public-api.ts b/components/skeleton/public-api.ts new file mode 100644 index 00000000000..00b217d6fc9 --- /dev/null +++ b/components/skeleton/public-api.ts @@ -0,0 +1,3 @@ +export * from './nz-skeleton.component'; +export * from './nz-skeleton.module'; +export * from './nz-skeleton.type'; diff --git a/components/skeleton/style/index.less b/components/skeleton/style/index.less new file mode 100644 index 00000000000..710d2c82acc --- /dev/null +++ b/components/skeleton/style/index.less @@ -0,0 +1,127 @@ +@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; + list-style: none; + width: 100%; + + &:nth-child(3) { + width: 94%; + } + + &:nth-child(4) { + width: 96%; + } + + + 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(); + } + } + + .@{skeleton-avatar-prefix-cls} { + .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%; + } +} \ No newline at end of file diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 81625aa2f0b..25c00ebb39a 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;