From cd9e1a34e40eea7b358ae537c2130279b4be7069 Mon Sep 17 00:00:00 2001 From: hullis Date: Sat, 30 Apr 2022 18:54:30 +0800 Subject: [PATCH 1/7] feat(module:segmented): implement new component --- .github/CODEOWNERS | 1 + components/components.less | 1 + components/core/animation/thumb.ts | 28 +++ components/core/config/config.ts | 5 + components/segmented/demo/basic.md | 15 ++ components/segmented/demo/basic.ts | 24 ++ components/segmented/demo/block.md | 14 ++ components/segmented/demo/block.ts | 20 ++ components/segmented/demo/custom.md | 14 ++ components/segmented/demo/custom.ts | 47 ++++ components/segmented/demo/disabled.md | 14 ++ components/segmented/demo/disabled.ts | 28 +++ components/segmented/demo/dynamic.md | 14 ++ components/segmented/demo/dynamic.ts | 31 +++ components/segmented/demo/icon.md | 14 ++ components/segmented/demo/icon.ts | 13 ++ components/segmented/demo/module | 11 + components/segmented/demo/size.md | 15 ++ components/segmented/demo/size.ts | 20 ++ components/segmented/demo/value.md | 14 ++ components/segmented/demo/value.ts | 36 +++ components/segmented/doc/index.en-US.md | 23 ++ components/segmented/doc/index.zh-CN.md | 26 +++ components/segmented/index.ts | 6 + components/segmented/ng-package.json | 5 + components/segmented/public-api.ts | 8 + components/segmented/segmented.component.ts | 209 ++++++++++++++++++ components/segmented/segmented.module.ts | 22 ++ components/segmented/style/entry.less | 0 components/segmented/style/index.less | 118 ++++++++++ components/segmented/style/mixins.less | 24 ++ components/segmented/style/rtl.less | 15 ++ components/segmented/types.ts | 30 +++ components/style/themes/default.less | 8 + .../online-ide/files/ng-zorro-antd.module.ts | 2 + .../template/ng-zorro-antd.module.template.ts | 2 + 36 files changed, 877 insertions(+) create mode 100644 components/core/animation/thumb.ts create mode 100644 components/segmented/demo/basic.md create mode 100644 components/segmented/demo/basic.ts create mode 100644 components/segmented/demo/block.md create mode 100644 components/segmented/demo/block.ts create mode 100644 components/segmented/demo/custom.md create mode 100644 components/segmented/demo/custom.ts create mode 100644 components/segmented/demo/disabled.md create mode 100644 components/segmented/demo/disabled.ts create mode 100644 components/segmented/demo/dynamic.md create mode 100644 components/segmented/demo/dynamic.ts create mode 100644 components/segmented/demo/icon.md create mode 100644 components/segmented/demo/icon.ts create mode 100644 components/segmented/demo/module create mode 100644 components/segmented/demo/size.md create mode 100644 components/segmented/demo/size.ts create mode 100644 components/segmented/demo/value.md create mode 100644 components/segmented/demo/value.ts create mode 100644 components/segmented/doc/index.en-US.md create mode 100644 components/segmented/doc/index.zh-CN.md create mode 100644 components/segmented/index.ts create mode 100644 components/segmented/ng-package.json create mode 100644 components/segmented/public-api.ts create mode 100644 components/segmented/segmented.component.ts create mode 100644 components/segmented/segmented.module.ts create mode 100644 components/segmented/style/entry.less create mode 100644 components/segmented/style/index.less create mode 100644 components/segmented/style/mixins.less create mode 100644 components/segmented/style/rtl.less create mode 100644 components/segmented/types.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 33f59655c3..c1d108d109 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -37,6 +37,7 @@ components/statistic/** @hullis components/timeline/** @hullis components/tooltip/** @hullis components/code-editor/** @hullis +components/segmented/** @hullis components/calendar/** @wenqi73 components/date-picker/** @wenqi73 components/skeleton/** @wenqi73 diff --git a/components/components.less b/components/components.less index 31333cb7ff..20a10ffc60 100644 --- a/components/components.less +++ b/components/components.less @@ -37,6 +37,7 @@ @import "./radio/style/entry.less"; @import "./rate/style/entry.less"; @import "./select/style/entry.less"; +@import "./segmented/style/index.less"; @import "./skeleton/style/entry.less"; @import "./slider/style/entry.less"; @import "./spin/style/entry.less"; diff --git a/components/core/animation/thumb.ts b/components/core/animation/thumb.ts new file mode 100644 index 0000000000..a70ad04a94 --- /dev/null +++ b/components/core/animation/thumb.ts @@ -0,0 +1,28 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { animate, AnimationTriggerMetadata, state, style, transition, trigger } from '@angular/animations'; + +import { AnimationCurves } from './animation-consts'; + +/** + * a move and resize transition in the horizontal direction + */ +export interface ThumbAnimationProps { + transform: number; + width: number; +} + +export const thumbMotion: AnimationTriggerMetadata = trigger('thumbMotion', [ + state('from', style({ transform: 'translateX({{ transform }}px)', width: '{{ width }}px' }), { + params: { transform: 0, width: 0 } + }), + + state('to', style({ transform: 'translateX({{ transform }}px)', width: '{{ width }}px' }), { + params: { transform: 100, width: 0 } + }), + + transition('from => to', animate(`300ms ${AnimationCurves.EASE_IN_OUT}`)) +]); diff --git a/components/core/config/config.ts b/components/core/config/config.ts index 532a4100d7..e7d8823ec6 100644 --- a/components/core/config/config.ts +++ b/components/core/config/config.ts @@ -56,6 +56,7 @@ export interface NzConfig { pagination?: PaginationConfig; progress?: ProgressConfig; rate?: RateConfig; + segmented?: SegmentedConfig; space?: SpaceConfig; spin?: SpinConfig; switch?: SwitchConfig; @@ -252,6 +253,10 @@ export interface RateConfig { nzAllowHalf?: boolean; } +export interface SegmentedConfig { + nzSize?: NzSizeLDSType; +} + export interface SpaceConfig { nzSize?: 'small' | 'middle' | 'large' | number; } diff --git a/components/segmented/demo/basic.md b/components/segmented/demo/basic.md new file mode 100644 index 0000000000..264e850398 --- /dev/null +++ b/components/segmented/demo/basic.md @@ -0,0 +1,15 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic Usage +--- + +## zh-CN + +最简单的用法。 + +## en-US + +Basic Usage. + diff --git a/components/segmented/demo/basic.ts b/components/segmented/demo/basic.ts new file mode 100644 index 0000000000..c68cca5b26 --- /dev/null +++ b/components/segmented/demo/basic.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-basic', + template: ``, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .code-box-demo .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedBasicComponent { + options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']; + + handleIndexChange(e: number): void { + console.log(e); + } +} diff --git a/components/segmented/demo/block.md b/components/segmented/demo/block.md new file mode 100644 index 0000000000..a1b2aef415 --- /dev/null +++ b/components/segmented/demo/block.md @@ -0,0 +1,14 @@ +--- +order: 10 +title: + zh-CN: Block 分段选择器 + en-US: Block Segmented +--- + +## zh-CN + +`nzBlock` 属性使其适合父元素宽度。 + +## en-US + +`nzBlock` property will make the `Segmented` fit to its parent width. diff --git a/components/segmented/demo/block.ts b/components/segmented/demo/block.ts new file mode 100644 index 0000000000..9e77d6682c --- /dev/null +++ b/components/segmented/demo/block.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-block', + template: ``, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .code-box-demo .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedBlockComponent { + options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']; +} diff --git a/components/segmented/demo/custom.md b/components/segmented/demo/custom.md new file mode 100644 index 0000000000..d25d382428 --- /dev/null +++ b/components/segmented/demo/custom.md @@ -0,0 +1,14 @@ +--- +order: 1 +title: + zh-CN: 自定义渲染 + en-US: Custom Render +--- + +## zh-CN + +使用 ReactNode 自定义渲染每一个 Segmented Item。 + +## en-US + +Custom each Segmented Item by ReactNode. diff --git a/components/segmented/demo/custom.ts b/components/segmented/demo/custom.ts new file mode 100644 index 0000000000..3917ab4447 --- /dev/null +++ b/components/segmented/demo/custom.ts @@ -0,0 +1,47 @@ +import { Component, TemplateRef, ViewChild } from '@angular/core'; + +import { NzSegmentedOption } from 'ng-zorro-antd/segmented'; + +@Component({ + selector: 'nz-demo-segmented-custom', + template: ` + + + + +
User 1
+
+ + +
User 2
+
+ + +
User 3
+
+
+
`, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedCustomComponent { + @ViewChild('temp', { static: true, read: TemplateRef }) templateRef!: TemplateRef<{ + $implicit: NzSegmentedOption; + index: number; + }>; + + options = [ + { label: 'user1', value: 'user1', useTemplate: true }, + { label: 'user2', value: 'user2', useTemplate: true }, + { label: 'user3', value: 'user3', useTemplate: true } + ]; +} diff --git a/components/segmented/demo/disabled.md b/components/segmented/demo/disabled.md new file mode 100644 index 0000000000..969a8f2e38 --- /dev/null +++ b/components/segmented/demo/disabled.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 不可用 + en-US: Disabled +--- + +## zh-CN + +Segmented 不可用。 + +## en-US + +Disabled Segmented. diff --git a/components/segmented/demo/disabled.ts b/components/segmented/demo/disabled.ts new file mode 100644 index 0000000000..6cca551121 --- /dev/null +++ b/components/segmented/demo/disabled.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-disabled', + template: ` +
+ `, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedDisabledComponent { + options = [ + 'Daily', + { label: 'Weekly', value: 'Weekly', disabled: true }, + 'Monthly', + { label: 'Quarterly', value: 'Quarterly', disabled: true }, + 'Yearly' + ]; +} diff --git a/components/segmented/demo/dynamic.md b/components/segmented/demo/dynamic.md new file mode 100644 index 0000000000..279e0dbdbb --- /dev/null +++ b/components/segmented/demo/dynamic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 动态数据 + en-US: Dynamic +--- + +## zh-CN + +动态加载数据。 + +## en-US + +Load `options` dynamically. diff --git a/components/segmented/demo/dynamic.ts b/components/segmented/demo/dynamic.ts new file mode 100644 index 0000000000..56ddfc8573 --- /dev/null +++ b/components/segmented/demo/dynamic.ts @@ -0,0 +1,31 @@ +import { Component } from '@angular/core'; + +const defaultOptions = ['Daily', 'Weekly', 'Monthly']; + +@Component({ + selector: 'nz-demo-segmented-dynamic', + template: ` +
+ `, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedDynamicComponent { + options = [...defaultOptions]; + + moreLoaded = false; + + handleLoadMore(): void { + this.moreLoaded = true; + this.options = [...defaultOptions, 'Quarterly', 'Yearly']; + } +} diff --git a/components/segmented/demo/icon.md b/components/segmented/demo/icon.md new file mode 100644 index 0000000000..13b0e2f887 --- /dev/null +++ b/components/segmented/demo/icon.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 设置图标 + en-US: With Icon +--- + +## zh-CN + +给 Segmented Item 设置 Icon。 + +## en-US + +Set `icon` for Segmented Item. diff --git a/components/segmented/demo/icon.ts b/components/segmented/demo/icon.ts new file mode 100644 index 0000000000..06fa72b428 --- /dev/null +++ b/components/segmented/demo/icon.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-icon', + template: ` `, + styles: [``] +}) +export class NzDemoSegmentedIconComponent { + options = [ + { label: 'List', value: 'List', icon: 'bars' }, + { label: 'Kanban', value: 'Kanban', icon: 'appstore' } + ]; +} diff --git a/components/segmented/demo/module b/components/segmented/demo/module new file mode 100644 index 0000000000..a1b340255c --- /dev/null +++ b/components/segmented/demo/module @@ -0,0 +1,11 @@ +import { NzIconModule } from 'ng-zorro-antd/icon'; +import { NzButtonModule } from 'ng-zorro-antd/button'; +import { NzSegmentedModule } from 'ng-zorro-antd/segmented'; +import { NzAvatarModule } from 'ng-zorro-antd/avatar'; + +export const moduleList = [ + NzAvatarModule, + NzIconModule, + NzButtonModule, + NzSegmentedModule, +]; diff --git a/components/segmented/demo/size.md b/components/segmented/demo/size.md new file mode 100644 index 0000000000..5d613e1d9f --- /dev/null +++ b/components/segmented/demo/size.md @@ -0,0 +1,15 @@ +--- +order: 1 +title: + zh-CN: 三种大小 + en-US: Three sizes of Segmented +--- + +## zh-CN + +我们为 Segmented 组件定义了三种尺寸(大、默认、小),高度分别为 `40px`、`32px` 和 `24px`。 + +## en-US + +There are three sizes of an Segmented: `large` (40px), `default` (32px) and `small` (24px). + diff --git a/components/segmented/demo/size.ts b/components/segmented/demo/size.ts new file mode 100644 index 0000000000..f42dc8d9fb --- /dev/null +++ b/components/segmented/demo/size.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-size', + template: ` +
+ +
+ `, + styles: [ + ` + .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedSizeComponent { + options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']; +} diff --git a/components/segmented/demo/value.md b/components/segmented/demo/value.md new file mode 100644 index 0000000000..bc0bf33bfe --- /dev/null +++ b/components/segmented/demo/value.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: ngModel + en-US: ngModel +--- + +## zh-CN + +通过 ngModel 指定选中的 index + +## en-US + +Set selected option via ngModel. diff --git a/components/segmented/demo/value.ts b/components/segmented/demo/value.ts new file mode 100644 index 0000000000..137c4220f3 --- /dev/null +++ b/components/segmented/demo/value.ts @@ -0,0 +1,36 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-segmented-value', + template: `

+ Selected index: {{ selectedIndex }}`, + styles: [ + ` + .code-box-demo { + overflow-x: auto; + } + + .ant-segmented { + margin-bottom: 10px; + } + ` + ] +}) +export class NzDemoSegmentedValueComponent { + selectedIndex = 1; + options = ['Daily', 'Weekly', 'Monthly', 'Quarterly', 'Yearly']; + + handleModelChange(index: number): void { + console.log(index); + } +} diff --git a/components/segmented/doc/index.en-US.md b/components/segmented/doc/index.en-US.md new file mode 100644 index 0000000000..bcd9f4be88 --- /dev/null +++ b/components/segmented/doc/index.en-US.md @@ -0,0 +1,23 @@ +--- +category: Components +type: Data Display +title: Segmented +cover: https://gw.alipayobjects.com/zos/bmw-prod/a3ff040f-24ba-43e0-92e9-c845df1612ad.svg +--- + +## When To Use + +- When displaying multiple options and user can select a single option; +- When switching the selected option, the content of the associated area changes. + +## API + +| Property | Description | Type | Default | Global Config | +| --- | --- | --- | --- | --- | +| `[nzBlock]` | Option to fit width to its parent\'s width | `boolean` | false | | +| `[nzDisabled]` | Disable all segments | `boolean` | false | | +| `[nzOptions]` | Set children optional | `string[] \| number[] \| Array<{ label: string; value: string \| number; icon: string; disabled?: boolean; useTemplate?: boolean }>` | - | | +| `[nzSize]` | The size of the Segmented | `large \| default \| small` | - | ✅ | +| `[ngModel]` | Index of the currently selected option | `number` | - | | +| `(nzValueChange)` | Emits when index of the currently selected option changes | `EventEmitter` | - | | +| `(ngModelChange)` | Emits when index of the currently selected option changes | `EventEmitter` | - | | diff --git a/components/segmented/doc/index.zh-CN.md b/components/segmented/doc/index.zh-CN.md new file mode 100644 index 0000000000..86909f9227 --- /dev/null +++ b/components/segmented/doc/index.zh-CN.md @@ -0,0 +1,26 @@ +--- +category: Components +subtitle: 分段控制器 +type: 数据展示 +title: Segmented +cover: https://gw.alipayobjects.com/zos/bmw-prod/a3ff040f-24ba-43e0-92e9-c845df1612ad.svg +--- + +## 何时使用 + +- 用于展示多个选项并允许用户选择其中单个选项; +- 当切换选中选项时,关联区域的内容会发生变化。 + +## API + +### Segmented + +| 参数 | 说明 | 类型 | 默认值 | 全局配置 | +| --- | --- | --- | --- | --- | +| `[nzBlock]` | 将宽度调整为父元素宽度的选项 | `boolean` | false | | +| `[nzDisabled]` | 是否禁用 | `boolean` | false | | +| `[nzOptions]` | 数据化配置选项内容 | `string[] \| number[] \| Array<{ label: string; value: string \| number; icon: string; disabled?: boolean; useTemplate?: boolean }>` | - | | +| `[nzSize]` | 控件尺寸 | `large \| default \| small` | - | ✅ | +| `[ngModel]` | 当前选中项目的 index | `number` | - | | +| `(nzValueChange)` | 当前选中项目变化时触发回调 | `EventEmitter` | - | | +| `(ngModelChange)` | 当前选中项目变化时触发回调 | `EventEmitter` | - | | \ No newline at end of file diff --git a/components/segmented/index.ts b/components/segmented/index.ts new file mode 100644 index 0000000000..97717c1c83 --- /dev/null +++ b/components/segmented/index.ts @@ -0,0 +1,6 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export * from './public-api'; diff --git a/components/segmented/ng-package.json b/components/segmented/ng-package.json new file mode 100644 index 0000000000..789c95e496 --- /dev/null +++ b/components/segmented/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/components/segmented/public-api.ts b/components/segmented/public-api.ts new file mode 100644 index 0000000000..a9d1d6137a --- /dev/null +++ b/components/segmented/public-api.ts @@ -0,0 +1,8 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export { NzSegmentedModule } from './segmented.module'; +export { NzSegmentedComponent } from './segmented.component'; +export * from './types'; diff --git a/components/segmented/segmented.component.ts b/components/segmented/segmented.component.ts new file mode 100644 index 0000000000..41313c2cf4 --- /dev/null +++ b/components/segmented/segmented.component.ts @@ -0,0 +1,209 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { Direction, Directionality } from '@angular/cdk/bidi'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + OnChanges, + Optional, + Output, + QueryList, + SimpleChanges, + TemplateRef, + ViewChildren, + ViewEncapsulation +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +import { ThumbAnimationProps, thumbMotion } from 'ng-zorro-antd/core/animation/thumb'; +import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; +import { BooleanInput, NzSafeAny, NzSizeLDSType, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; +import { InputBoolean } from 'ng-zorro-antd/core/util'; + +import { normalizeOptions, NzNormalizedOptions, NzSegmentedOption, NzSegmentedOptions } from './types'; + +const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'segmented'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + selector: 'nz-segmented', + exportAs: 'nzSegmented', + template: ` + +
+ + `, + host: { + class: 'ant-segmented', + '[class.ant-segmented-disabled]': '!!nzDisabled', + '[class.ant-segmented-rtl]': `dir === 'rtl'`, + '[class.ant-segmented-lg]': `nzSize === 'large'`, + '[class.ant-segmented-sm]': `nzSize === 'small'`, + '[class.ant-segmented-block]': `!!nzBlock` + }, + providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzSegmentedComponent), multi: true }], + animations: [thumbMotion] +}) +export class NzSegmentedComponent implements OnChanges, ControlValueAccessor { + static ngAcceptInputType_nzDisabled: BooleanInput; + static ngAcceptInputType_nzBlock: BooleanInput; + + readonly _nzModuleName: NzConfigKey = NZ_CONFIG_MODULE_NAME; + + @ViewChildren('itemLabels', { read: ElementRef }) listOfOptions!: QueryList; + + @Input() + @InputBoolean() + nzBlock: boolean = false; + + @Input() + @InputBoolean() + nzDisabled: boolean = false; + + @Input() nzOptions: NzSegmentedOptions = []; + + @Input() @WithConfig() nzSize: NzSizeLDSType = 'default'; + + @Input() nzLabelTemplate: TemplateRef<{ $implicit: NzSegmentedOption; index: number }> | null = null; + + @Output() readonly nzValueChange = new EventEmitter(); + + public dir: Direction = 'ltr'; + + public selectedIndex = 0; + public transitionedToIndex = -1; + public animationState: null | { value: string; params: ThumbAnimationProps } = null; + + public normalizedOptions: NzNormalizedOptions = []; + + private destroy$ = new Subject(); + + onChange: OnChangeType = () => {}; + + onTouched: OnTouchedType = () => {}; + + constructor( + public nzConfigService: NzConfigService, + private cdr: ChangeDetectorRef, + @Optional() private directionality: Directionality + ) { + this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe(direction => { + this.dir = direction; + this.cdr.detectChanges(); + }); + } + + ngOnChanges(changes: SimpleChanges): void { + const { nzOptions } = changes; + if (nzOptions) { + this.normalizedOptions = normalizeOptions(nzOptions.currentValue); + } + } + + handleOptionClick(index: number): void { + if (this.nzDisabled) { + return; + } + + this.changeSelectedIndex(index); + + this.onChange(index); + this.nzValueChange.emit(index); + } + + handleThumbAnimationDone(e: NzSafeAny): void { + if (e.fromState === 'from') { + this.selectedIndex = this.transitionedToIndex; + this.transitionedToIndex = -1; + this.animationState = null; + this.cdr.detectChanges(); + } + } + + writeValue(value: number): void { + this.changeSelectedIndex(value); + this.cdr.markForCheck(); + } + + registerOnChange(fn: OnChangeType): void { + this.onChange = fn; + } + + registerOnTouched(fn: OnTouchedType): void { + this.onTouched = fn; + } + + private changeSelectedIndex(index: number): void { + if (!this.listOfOptions || this.selectedIndex === -1 || this.selectedIndex === index) { + return; + } + + this.animationState = { + value: 'from', + params: getThumbAnimationProps(this.listOfOptions.get(this.selectedIndex)!.nativeElement!) + }; + this.selectedIndex = -1; + this.cdr.detectChanges(); + + this.animationState = { + value: 'to', + params: getThumbAnimationProps(this.listOfOptions.get(index)!.nativeElement!) + }; + this.cdr.detectChanges(); + + this.transitionedToIndex = index; + } +} + +function getThumbAnimationProps(element: HTMLElement): ThumbAnimationProps { + return { + transform: element.offsetLeft, + width: element.clientWidth + }; +} diff --git a/components/segmented/segmented.module.ts b/components/segmented/segmented.module.ts new file mode 100644 index 0000000000..a7c9480655 --- /dev/null +++ b/components/segmented/segmented.module.ts @@ -0,0 +1,22 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +import { BidiModule } from '@angular/cdk/bidi'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; + +import { NzOutletModule } from 'ng-zorro-antd/core/outlet'; +import { NzI18nModule } from 'ng-zorro-antd/i18n'; +import { NzIconModule } from 'ng-zorro-antd/icon'; + +import { NzSegmentedComponent } from './segmented.component'; + +@NgModule({ + exports: [NzSegmentedComponent], + declarations: [NzSegmentedComponent], + imports: [BidiModule, CommonModule, FormsModule, NzI18nModule, NzIconModule, NzOutletModule] +}) +export class NzSegmentedModule {} diff --git a/components/segmented/style/entry.less b/components/segmented/style/entry.less new file mode 100644 index 0000000000..e69de29bb2 diff --git a/components/segmented/style/index.less b/components/segmented/style/index.less new file mode 100644 index 0000000000..c3d83b726a --- /dev/null +++ b/components/segmented/style/index.less @@ -0,0 +1,118 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; +@import './mixins.less'; + +@segmented-prefix-cls: ~'@{ant-prefix}-segmented'; + +.@{segmented-prefix-cls} { + .reset-component(); + + position: relative; + display: inline-flex; + align-items: stretch; + justify-items: flex-start; + color: @segmented-label-color; + background-color: @segmented-bg; + border-radius: 2px; + box-shadow: 0 0 0 2px @segmented-bg; + transition: all 0.3s @ease-in-out; + + // hover/focus styles + &:not(&-disabled) { + &:hover, + &:focus { + background-color: @segmented-hover-bg; + box-shadow: 0 0 0 2px @segmented-hover-bg; + } + } + + // block styles + &&-block { + display: flex; + } + + &&-block &-item { + flex: 1; + min-width: 0; + } + + // item styles + &-item { + position: relative; + text-align: center; + cursor: pointer; + transition: color 0.3s @ease-in-out; + + &-selected { + .segmented-item-selected(); + color: @segmented-label-hover-color; + } + + &:hover, + &:focus { + color: @segmented-label-hover-color; + } + + &-label { + min-height: @input-height-base; + padding: @input-padding-vertical-base @input-padding-horizontal-base; + line-height: @input-height-base - @input-padding-vertical-base * 2; + .segmented-text-ellipsis(); + } + + // syntactic sugar to add `icon` for Segmented Item + &-icon { + margin-right: 6px; + } + + &-input { + position: absolute; + top: 0; + left: 0; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + } + } + + // size styles + &&-lg &-item-label { + min-height: @input-height-lg; + padding: @input-padding-vertical-lg @input-padding-horizontal-lg; + font-size: @font-size-lg; + line-height: @input-height-lg - @input-padding-vertical-lg * 2; + } + + &&-sm &-item-label { + min-height: @input-height-sm; + padding: @input-padding-vertical-sm @input-padding-horizontal-sm; + line-height: @input-height-sm - @input-padding-vertical-sm * 2; + } + + // disabled styles + &-disabled &-item, + &-item-disabled { + .segmented-disabled-item(); + } + + // thumb styles + &-thumb { + .segmented-item-selected(); + + position: absolute; + top: 0; + left: 0; + width: 0; + height: 100%; + padding: 4px 0; + } + + // transition effect when `enter-active` + &-thumb-motion-enter-active { + transition: transform 0.3s @ease-in-out, width 0.3s @ease-in-out; + will-change: transform, width; + } +} + +@import './rtl'; diff --git a/components/segmented/style/mixins.less b/components/segmented/style/mixins.less new file mode 100644 index 0000000000..0c6839af74 --- /dev/null +++ b/components/segmented/style/mixins.less @@ -0,0 +1,24 @@ +// mixins +.segmented-disabled-item { + &, + &:hover, + &:focus { + color: @disabled-color; + cursor: not-allowed; + } +} + +.segmented-item-selected { + background-color: @segmented-selected-bg; + border-radius: @border-radius-base; + box-shadow: 0 2px 8px -2px fade(@black, 5%), 0 1px 4px -1px fade(@black, 7%), + 0 0 1px 0 fade(@black, 8%); +} + +.segmented-text-ellipsis { + overflow: hidden; + // handle text ellipsis + white-space: nowrap; + text-overflow: ellipsis; + word-break: keep-all; +} diff --git a/components/segmented/style/rtl.less b/components/segmented/style/rtl.less new file mode 100644 index 0000000000..c459bf035e --- /dev/null +++ b/components/segmented/style/rtl.less @@ -0,0 +1,15 @@ +@import '../../style/themes/index'; +@import '../../style/mixins/index'; + +@segmented-prefix-cls: ~'@{ant-prefix}-segmented'; + +.@{segmented-prefix-cls} { + &&-rtl { + direction: rtl; + } + + &&-rtl &-item-icon { + margin-right: 0; + margin-left: 6px; + } +} diff --git a/components/segmented/types.ts b/components/segmented/types.ts new file mode 100644 index 0000000000..bb8ec51703 --- /dev/null +++ b/components/segmented/types.ts @@ -0,0 +1,30 @@ +/** + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE + */ + +export interface NzSegmentedOption { + label: string; + value: string | number; + useTemplate?: boolean; + icon?: string; + disabled?: boolean; + className?: string; +} + +export type NzSegmentedOptions = Array; + +export type NzNormalizedOptions = NzSegmentedOption[]; + +export function normalizeOptions(unnormalized: NzSegmentedOptions): NzNormalizedOptions { + return unnormalized.map(item => { + if (typeof item === 'string' || typeof item === 'number') { + return { + label: `${item}`, + value: item + } as NzSegmentedOption; + } + + return item as NzSegmentedOption; + }); +} diff --git a/components/style/themes/default.less b/components/style/themes/default.less index a268427e0d..ec963ca704 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -1056,3 +1056,11 @@ @image-preview-operation-size: 18px; @image-preview-operation-color: @text-color-dark; @image-preview-operation-disabled-color: fade(@image-preview-operation-color, 25%); + +// Segmented +// --- +@segmented-bg: fade(@black, 4%); +@segmented-hover-bg: fade(@black, 6%); +@segmented-selected-bg: @white; +@segmented-label-color: fade(@black, 65%); +@segmented-label-hover-color: #262626; diff --git a/scripts/site/_site/doc/app/online-ide/files/ng-zorro-antd.module.ts b/scripts/site/_site/doc/app/online-ide/files/ng-zorro-antd.module.ts index 91818fe654..f0d010a7e0 100644 --- a/scripts/site/_site/doc/app/online-ide/files/ng-zorro-antd.module.ts +++ b/scripts/site/_site/doc/app/online-ide/files/ng-zorro-antd.module.ts @@ -48,6 +48,7 @@ import { NzProgressModule } from 'ng-zorro-antd/progress'; import { NzRadioModule } from 'ng-zorro-antd/radio'; import { NzRateModule } from 'ng-zorro-antd/rate'; import { NzResultModule } from 'ng-zorro-antd/result'; +import { NzSegmentedModule } from 'ng-zorro-antd/segmented'; import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzSkeletonModule } from 'ng-zorro-antd/skeleton'; import { NzSliderModule } from 'ng-zorro-antd/slider'; @@ -117,6 +118,7 @@ import { NzPipesModule } from 'ng-zorro-antd/pipes'; NzRadioModule, NzRateModule, NzResultModule, + NzSegmentedModule, NzSelectModule, NzSkeletonModule, NzSliderModule, diff --git a/scripts/site/template/ng-zorro-antd.module.template.ts b/scripts/site/template/ng-zorro-antd.module.template.ts index b867f2025d..abbde9a351 100644 --- a/scripts/site/template/ng-zorro-antd.module.template.ts +++ b/scripts/site/template/ng-zorro-antd.module.template.ts @@ -46,6 +46,7 @@ import { NzProgressModule } from 'ng-zorro-antd/progress'; import { NzRadioModule } from 'ng-zorro-antd/radio'; import { NzRateModule } from 'ng-zorro-antd/rate'; import { NzResultModule } from 'ng-zorro-antd/result'; +import { NzSegmentedModule } from 'ng-zorro-antd/segmented'; import { NzSelectModule } from 'ng-zorro-antd/select'; import { NzSkeletonModule } from 'ng-zorro-antd/skeleton'; import { NzSliderModule } from 'ng-zorro-antd/slider'; @@ -111,6 +112,7 @@ import { NzUploadModule } from 'ng-zorro-antd/upload'; NzRadioModule, NzRateModule, NzResultModule, + NzSegmentedModule, NzSelectModule, NzSkeletonModule, NzSliderModule, From c89d2382d3aae97f5f587076f26455f31af91ab1 Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 11:07:42 +0800 Subject: [PATCH 2/7] test: add test for segmented component --- components/segmented/demo/basic.md | 2 +- components/segmented/segmented.component.ts | 16 +-- components/segmented/segmented.spec.ts | 135 ++++++++++++++++++++ 3 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 components/segmented/segmented.spec.ts diff --git a/components/segmented/demo/basic.md b/components/segmented/demo/basic.md index 264e850398..f1e1e09a72 100644 --- a/components/segmented/demo/basic.md +++ b/components/segmented/demo/basic.md @@ -2,7 +2,7 @@ order: 0 title: zh-CN: 基本 - en-US: Basic Usage + en-US: Basic usage --- ## zh-CN diff --git a/components/segmented/segmented.component.ts b/components/segmented/segmented.component.ts index 41313c2cf4..f85ea1c680 100644 --- a/components/segmented/segmented.component.ts +++ b/components/segmented/segmented.component.ts @@ -55,10 +55,9 @@ const NZ_CONFIG_MODULE_NAME: NzConfigKey = 'segmented'; 'ant-segmented-item-selected': i === selectedIndex, 'ant-segmented-item-disabled': item.disabled }" - (click)="!item.disabled && handleOptionClick(i)" > - -
+ +
@@ -166,9 +165,11 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor { } } - writeValue(value: number): void { - this.changeSelectedIndex(value); - this.cdr.markForCheck(); + writeValue(value: number | null): void { + if (typeof value === 'number' && value > -1) { + this.changeSelectedIndex(value); + this.cdr.markForCheck(); + } } registerOnChange(fn: OnChangeType): void { @@ -195,9 +196,8 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor { value: 'to', params: getThumbAnimationProps(this.listOfOptions.get(index)!.nativeElement!) }; - this.cdr.detectChanges(); - this.transitionedToIndex = index; + this.cdr.detectChanges(); } } diff --git a/components/segmented/segmented.spec.ts b/components/segmented/segmented.spec.ts new file mode 100644 index 0000000000..4b2f2c3468 --- /dev/null +++ b/components/segmented/segmented.spec.ts @@ -0,0 +1,135 @@ +import { Component, DebugElement } from '@angular/core'; +import { ComponentFixture, fakeAsync, tick } from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; + +import { dispatchMouseEvent } from 'ng-zorro-antd/core/testing'; +import { ComponentBed, createComponentBed } from 'ng-zorro-antd/core/testing/component-bed'; + +import { NzSegmentedComponent } from './segmented.component'; +import { NzSegmentedModule } from './segmented.module'; +import { NzSegmentedOptions } from './types'; + +describe('nz-segmented', () => { + describe('basic', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; + let testComponent: NzSegmentedTestComponent; + let segmentedComponent: DebugElement; + + function getSegmentedOptionByIndex(index: number): HTMLElement { + return segmentedComponent.nativeElement.querySelectorAll('.ant-segmented-item')[index]; + } + + // function getSegmentedLabelByIndex(index: number): HTMLElement { + // return segmentedComponent.nativeElement + // .querySelectorAll('.ant-segmented-item') + // [index].querySelector('.ant-segmented-item-label'); + // } + + beforeEach(() => { + testBed = createComponentBed(NzSegmentedTestComponent, { + imports: [NzSegmentedModule, FormsModule] + }); + + fixture = testBed.fixture; + testComponent = testBed.component; + segmentedComponent = fixture.debugElement.query(By.directive(NzSegmentedComponent)); + + fixture.detectChanges(); + }); + + it('should support block mode', () => { + expect((segmentedComponent.nativeElement as HTMLElement).classList.contains('ant-segmented-block')).toBeFalse(); + testComponent.block = true; + fixture.detectChanges(); + expect((segmentedComponent.nativeElement as HTMLElement).classList.contains('ant-segmented-block')).toBeTrue(); + }); + + it('should emit when index changes', fakeAsync(() => { + spyOn(testComponent, 'handleIndexChange'); + + const theFirstElement = getSegmentedOptionByIndex(0); + expect(theFirstElement.classList.contains('ant-segmented-item-selected')).toBeTrue(); + + const theThirdElement = getSegmentedOptionByIndex(2); + dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click'); + fixture.detectChanges(); + tick(400); + fixture.detectChanges(); + expect(testComponent.index).toBe(2); + expect(theFirstElement.classList.contains('ant-segmented-item-selected')).toBeFalse(); + expect(theThirdElement.classList.contains('ant-segmented-item-selected')).toBeTrue(); + expect(testComponent.handleIndexChange).toHaveBeenCalledWith(2); + expect(testComponent.handleIndexChange).toHaveBeenCalledTimes(2); + + testComponent.index = 1; + fixture.detectChanges(); + tick(400); + fixture.detectChanges(); + const theSecondElement = getSegmentedOptionByIndex(1); + expect(segmentedComponent.componentInstance.selectedIndex).toBe(1); + expect(testComponent.handleIndexChange).toHaveBeenCalledTimes(2); + expect(theSecondElement.classList.contains('ant-segmented-item-selected')).toBeTrue(); + })); + + it('should support disabled mode', fakeAsync(() => { + testComponent.disabled = true; + fixture.detectChanges(); + + const theThirdElement = getSegmentedOptionByIndex(2); + dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click'); + fixture.detectChanges(); + tick(400); + fixture.detectChanges(); + expect(testComponent.index).toBe(0); + + testComponent.disabled = false; + fixture.detectChanges(); + dispatchMouseEvent(theThirdElement.querySelector('.ant-segmented-item-label')!, 'click'); + fixture.detectChanges(); + tick(400); + fixture.detectChanges(); + expect(testComponent.index).toBe(2); + + testComponent.options = [ + 'Daily', + { label: 'Weekly', value: 'Weekly', disabled: true }, + 'Monthly', + { label: 'Quarterly', value: 'Quarterly', disabled: true }, + 'Yearly' + ]; + fixture.detectChanges(); + + const theSecondElement = getSegmentedOptionByIndex(1); + dispatchMouseEvent(theSecondElement.querySelector('.ant-segmented-item-label')!, 'click'); + fixture.detectChanges(); + tick(400); + fixture.detectChanges(); + expect(testComponent.index).toBe(2); + })); + }); +}); + +@Component({ + template: `` +}) +export class NzSegmentedTestComponent { + size = 'default'; + options: NzSegmentedOptions = [1, 2, 3]; + index = 0; + block = false; + disabled = false; + + handleIndexChange(_e: number): void { + // empty + } +} From 2e16f04c64949e12577e9def8de496f9b59bd792 Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 11:14:19 +0800 Subject: [PATCH 3/7] chore: code update --- components/segmented/segmented.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/segmented/segmented.component.ts b/components/segmented/segmented.component.ts index f85ea1c680..1babccb627 100644 --- a/components/segmented/segmented.component.ts +++ b/components/segmented/segmented.component.ts @@ -128,9 +128,9 @@ export class NzSegmentedComponent implements OnChanges, ControlValueAccessor { onTouched: OnTouchedType = () => {}; constructor( - public nzConfigService: NzConfigService, - private cdr: ChangeDetectorRef, - @Optional() private directionality: Directionality + public readonly nzConfigService: NzConfigService, + private readonly cdr: ChangeDetectorRef, + @Optional() private readonly directionality: Directionality ) { this.directionality.change?.pipe(takeUntil(this.destroy$)).subscribe(direction => { this.dir = direction; From 1f93c8aafa7eae0ad5c458b5397f085785554aca Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 11:20:41 +0800 Subject: [PATCH 4/7] fix: fix doc --- components/segmented/demo/custom.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/segmented/demo/custom.md b/components/segmented/demo/custom.md index d25d382428..f6d22c2ace 100644 --- a/components/segmented/demo/custom.md +++ b/components/segmented/demo/custom.md @@ -7,8 +7,8 @@ title: ## zh-CN -使用 ReactNode 自定义渲染每一个 Segmented Item。 +使用 nzLabelTemplate 自定义渲染每一个 Segmented Item。 ## en-US -Custom each Segmented Item by ReactNode. +Custom each Segmented Item by nzLabelTemplate. From 8bb7e1d1faaeccfc14d65fe7d3a1e04fdd478cf1 Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 11:25:21 +0800 Subject: [PATCH 5/7] feat: add dark mode theme --- components/style/themes/dark.less | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/components/style/themes/dark.less b/components/style/themes/dark.less index 59a9b69f20..4cfda904be 100644 --- a/components/style/themes/dark.less +++ b/components/style/themes/dark.less @@ -447,3 +447,11 @@ // Mentions // --- @mentions-dropdown-bg: @popover-background; + +// Segmented +// --- +@segmented-bg: fade(@black, 25%); +@segmented-hover-bg: fade(@black, 45%); +@segmented-selected-bg: #333333; +@segmented-label-color: fade(@white, 65%); +@segmented-label-hover-color: fade(@white, 85%); From b20cf47afc71ceeb42f36b5748b507f59354847e Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 20:18:46 +0800 Subject: [PATCH 6/7] fix: fix export --- components/core/animation/public-api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/components/core/animation/public-api.ts b/components/core/animation/public-api.ts index 2f5c9a6232..480faca714 100644 --- a/components/core/animation/public-api.ts +++ b/components/core/animation/public-api.ts @@ -11,3 +11,4 @@ export * from './move'; export * from './notification'; export * from './slide'; export * from './zoom'; +export * from './thumb'; From 6c7e6b05934395e05fa102da2358878cfff6e028 Mon Sep 17 00:00:00 2001 From: hullis Date: Sun, 1 May 2022 22:19:00 +0800 Subject: [PATCH 7/7] fix: fix import --- components/segmented/segmented.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/segmented/segmented.component.ts b/components/segmented/segmented.component.ts index 1babccb627..35b1920412 100644 --- a/components/segmented/segmented.component.ts +++ b/components/segmented/segmented.component.ts @@ -25,7 +25,7 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; -import { ThumbAnimationProps, thumbMotion } from 'ng-zorro-antd/core/animation/thumb'; +import { ThumbAnimationProps, thumbMotion } from 'ng-zorro-antd/core/animation'; import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config'; import { BooleanInput, NzSafeAny, NzSizeLDSType, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types'; import { InputBoolean } from 'ng-zorro-antd/core/util';