From c69108d3f7ad1cb7ac5b81ce3aa7b9905d456b2b Mon Sep 17 00:00:00 2001 From: Wendell Date: Sun, 15 Mar 2020 15:27:10 +0800 Subject: [PATCH] chore(module:slider): refactor (#4730) * chore(module:slider): refactor slider fix: test fix: remove redundant code feat(module:slider): support change value through keyboard event test: add test chore: hide internal implementation fix: fix import path fix: fix import path fix(module:slider): fix property type close #4749 chore(module:slider): refactor slider fix: test fix: remove redundant code feat(module:slider): support change value through keyboard event test: add test chore: hide internal implementation fix: fix import path fix: fix import path fix(module:slider): fix property type close #4749 * fix: fix no step situation --- ...andle.component.ts => handle.component.ts} | 77 +-- components/slider/marks.component.ts | 117 +++++ components/slider/nz-slider-definitions.ts | 68 --- .../slider/nz-slider-handle.component.html | 14 - .../slider/nz-slider-marks.component.html | 10 - .../slider/nz-slider-marks.component.ts | 100 ---- .../slider/nz-slider-step.component.html | 9 - .../slider/nz-slider-track.component.html | 1 - components/slider/nz-slider.component.html | 43 -- components/slider/public-api.ts | 15 +- ...lider.component.ts => slider.component.ts} | 270 ++++++---- .../{nz-slider.module.ts => slider.module.ts} | 10 +- .../{nz-slider-error.ts => slider.service.ts} | 9 +- .../{nz-slider.spec.ts => slider.spec.ts} | 465 +++++++++--------- ...er-step.component.ts => step.component.ts} | 41 +- ...-track.component.ts => track.component.ts} | 28 +- components/slider/typings.ts | 54 ++ 17 files changed, 694 insertions(+), 637 deletions(-) rename components/slider/{nz-slider-handle.component.ts => handle.component.ts} (55%) create mode 100644 components/slider/marks.component.ts delete mode 100644 components/slider/nz-slider-definitions.ts delete mode 100644 components/slider/nz-slider-handle.component.html delete mode 100644 components/slider/nz-slider-marks.component.html delete mode 100644 components/slider/nz-slider-marks.component.ts delete mode 100644 components/slider/nz-slider-step.component.html delete mode 100644 components/slider/nz-slider-track.component.html delete mode 100644 components/slider/nz-slider.component.html rename components/slider/{nz-slider.component.ts => slider.component.ts} (61%) rename components/slider/{nz-slider.module.ts => slider.module.ts} (70%) rename components/slider/{nz-slider-error.ts => slider.service.ts} (54%) rename components/slider/{nz-slider.spec.ts => slider.spec.ts} (79%) rename components/slider/{nz-slider-step.component.ts => step.component.ts} (54%) rename components/slider/{nz-slider-track.component.ts => track.component.ts} (63%) create mode 100644 components/slider/typings.ts diff --git a/components/slider/nz-slider-handle.component.ts b/components/slider/handle.component.ts similarity index 55% rename from components/slider/nz-slider-handle.component.ts rename to components/slider/handle.component.ts index f6119671c65..8267e0cff1d 100644 --- a/components/slider/nz-slider-handle.component.ts +++ b/components/slider/handle.component.ts @@ -10,20 +10,19 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ElementRef, Input, OnChanges, - OnDestroy, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; -import { Subscription } from 'rxjs'; import { InputBoolean, NgStyleInterface } from 'ng-zorro-antd/core'; import { NzTooltipDirective } from 'ng-zorro-antd/tooltip'; -import { SliderShowTooltip } from './nz-slider-definitions'; -import { NzSliderComponent } from './nz-slider.component'; +import { NzSliderService } from './slider.service'; +import { NzSliderShowTooltip } from './typings'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -31,58 +30,67 @@ import { NzSliderComponent } from './nz-slider.component'; selector: 'nz-slider-handle', exportAs: 'nzSliderHandle', preserveWhitespaces: false, - templateUrl: './nz-slider-handle.component.html', + template: ` +
+ `, host: { '(mouseenter)': 'enterHandle()', '(mouseleave)': 'leaveHandle()' } }) -export class NzSliderHandleComponent implements OnChanges, OnDestroy { +export class NzSliderHandleComponent implements OnChanges { + @ViewChild('handle', { static: false }) handleEl: ElementRef; @ViewChild(NzTooltipDirective, { static: false }) tooltip: NzTooltipDirective; - @Input() nzVertical: string; - @Input() nzOffset: number; - @Input() nzValue: number; - @Input() nzTooltipVisible: SliderShowTooltip = 'default'; - @Input() nzTooltipPlacement: string; - @Input() nzTipFormatter: (value: number) => string; - @Input() @InputBoolean() nzActive = false; + @Input() vertical: string; + @Input() offset: number; + @Input() value: number; + @Input() tooltipVisible: NzSliderShowTooltip = 'default'; + @Input() tooltipPlacement: string; + @Input() tooltipFormatter: (value: number) => string; + @Input() @InputBoolean() active = false; tooltipTitle: string; style: NgStyleInterface = {}; - private hovers_ = new Subscription(); - - constructor(private sliderComponent: NzSliderComponent, private cdr: ChangeDetectorRef) {} + constructor(private sliderService: NzSliderService, private cdr: ChangeDetectorRef) {} ngOnChanges(changes: SimpleChanges): void { - const { nzOffset, nzValue, nzActive, nzTooltipVisible } = changes; + const { offset, value, active, tooltipVisible } = changes; - if (nzOffset) { + if (offset) { this.updateStyle(); } - if (nzValue) { + + if (value) { this.updateTooltipTitle(); this.updateTooltipPosition(); } - if (nzActive) { - if (nzActive.currentValue) { + + if (active) { + if (active.currentValue) { this.toggleTooltip(true); } else { this.toggleTooltip(false); } } - if (nzTooltipVisible && nzTooltipVisible.currentValue === 'always') { + + if (tooltipVisible && tooltipVisible.currentValue === 'always') { Promise.resolve().then(() => this.toggleTooltip(true, true)); } } - ngOnDestroy(): void { - this.hovers_.unsubscribe(); - } - enterHandle = () => { - if (!this.sliderComponent.isDragging) { + if (!this.sliderService.isDragging) { this.toggleTooltip(true); this.updateTooltipPosition(); this.cdr.detectChanges(); @@ -90,14 +98,18 @@ export class NzSliderHandleComponent implements OnChanges, OnDestroy { }; leaveHandle = () => { - if (!this.sliderComponent.isDragging) { + if (!this.sliderService.isDragging) { this.toggleTooltip(false); this.cdr.detectChanges(); } }; + focus(): void { + this.handleEl.nativeElement.focus(); + } + private toggleTooltip(show: boolean, force: boolean = false): void { - if (!force && (this.nzTooltipVisible !== 'default' || !this.tooltip)) { + if (!force && (this.tooltipVisible !== 'default' || !this.tooltip)) { return; } @@ -109,7 +121,7 @@ export class NzSliderHandleComponent implements OnChanges, OnDestroy { } private updateTooltipTitle(): void { - this.tooltipTitle = this.nzTipFormatter ? this.nzTipFormatter(this.nzValue) : `${this.nzValue}`; + this.tooltipTitle = this.tooltipFormatter ? this.tooltipFormatter(this.value) : `${this.value}`; } private updateTooltipPosition(): void { @@ -119,7 +131,10 @@ export class NzSliderHandleComponent implements OnChanges, OnDestroy { } private updateStyle(): void { - this.style[this.nzVertical ? 'bottom' : 'left'] = `${this.nzOffset}%`; + this.style = { + [this.vertical ? 'bottom' : 'left']: `${this.offset}%`, + transform: this.vertical ? null : 'translateX(-50%)' + }; this.cdr.markForCheck(); } } diff --git a/components/slider/marks.component.ts b/components/slider/marks.component.ts new file mode 100644 index 00000000000..071d7b79f62 --- /dev/null +++ b/components/slider/marks.component.ts @@ -0,0 +1,117 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * 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 { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; + +import { InputBoolean, NgStyleInterface } from 'ng-zorro-antd/core'; + +import { NzDisplayedMark, NzExtendedMark, NzMark, NzMarkObj } from './typings'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + preserveWhitespaces: false, + selector: 'nz-slider-marks', + exportAs: 'nzSliderMarks', + template: ` +
+ + +
+ ` +}) +export class NzSliderMarksComponent implements OnChanges { + @Input() lowerBound: number | null = null; + @Input() upperBound: number | null = null; + @Input() marksArray: NzExtendedMark[]; + @Input() min: number; + @Input() max: number; + @Input() @InputBoolean() vertical = false; + @Input() @InputBoolean() included = false; + + marks: NzDisplayedMark[]; + + ngOnChanges(changes: SimpleChanges): void { + const { marksArray, lowerBound, upperBound } = changes; + + if (marksArray) { + this.buildMarks(); + } + + if (marksArray || lowerBound || upperBound) { + this.togglePointActive(); + } + } + + trackById(_index: number, mark: NzDisplayedMark): number { + return mark.value; + } + + private buildMarks(): void { + const range = this.max - this.min; + + this.marks = this.marksArray.map(mark => { + const { value, offset, config } = mark; + const style = this.getMarkStyles(value, range, config); + const label = isConfigObject(config) ? config.label : config; + + return { + label, + offset, + style, + value, + config, + active: false + }; + }); + } + + private getMarkStyles(value: number, range: number, config: NzMark): NgStyleInterface { + let style; + + if (this.vertical) { + style = { + marginBottom: '-50%', + bottom: `${((value - this.min) / range) * 100}%` + }; + } else { + style = { + transform: `translate3d(-50%, 0, 0)`, + left: `${((value - this.min) / range) * 100}%` + }; + } + + if (isConfigObject(config) && config.style) { + style = { ...style, ...config.style }; + } + + return style; + } + + private togglePointActive(): void { + if (this.marks && this.lowerBound !== null && this.upperBound !== null) { + this.marks.forEach(mark => { + const value = mark.value; + const isActive = + (!this.included && value === this.upperBound) || (this.included && value <= this.upperBound! && value >= this.lowerBound!); + + mark.active = isActive; + }); + } + } +} + +function isConfigObject(config: NzMark): config is NzMarkObj { + return typeof config !== 'string'; +} diff --git a/components/slider/nz-slider-definitions.ts b/components/slider/nz-slider-definitions.ts deleted file mode 100644 index f64a9e541ba..00000000000 --- a/components/slider/nz-slider-definitions.ts +++ /dev/null @@ -1,68 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * 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 type Mark = string | MarkObj; - -export interface MarkObj { - style?: object; - label: string; -} - -export type NzMarks = Marks; - -export class Marks { - [key: number]: Mark; -} - -/** - * Processed steps that would be passed to sub components. - */ -export interface ExtendedMark { - value: number; - offset: number; - config: Mark; -} - -/** - * Marks that would be rendered. - */ -export interface DisplayedMark extends ExtendedMark { - active: boolean; - label: string; - style?: object; -} - -/** - * Steps that would be rendered. - */ -export interface DisplayedStep extends ExtendedMark { - active: boolean; - style?: object; -} - -export type SliderShowTooltip = 'always' | 'never' | 'default'; - -export type SliderValue = number[] | number; - -export interface SliderHandler { - offset: number | null; - value: number | null; - active: boolean; -} - -export function isValueARange(value: SliderValue): value is number[] { - if (value instanceof Array) { - return value.length === 2; - } else { - return false; - } -} - -export function isConfigAObject(config: Mark): config is MarkObj { - return config instanceof Object; -} diff --git a/components/slider/nz-slider-handle.component.html b/components/slider/nz-slider-handle.component.html deleted file mode 100644 index 488e8c7cd6b..00000000000 --- a/components/slider/nz-slider-handle.component.html +++ /dev/null @@ -1,14 +0,0 @@ -
-
diff --git a/components/slider/nz-slider-marks.component.html b/components/slider/nz-slider-marks.component.html deleted file mode 100644 index 85d3c6da0c0..00000000000 --- a/components/slider/nz-slider-marks.component.html +++ /dev/null @@ -1,10 +0,0 @@ -
- - -
diff --git a/components/slider/nz-slider-marks.component.ts b/components/slider/nz-slider-marks.component.ts deleted file mode 100644 index 96428213577..00000000000 --- a/components/slider/nz-slider-marks.component.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * @license - * Copyright Alibaba.com All Rights Reserved. - * - * 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 { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; - -import { InputBoolean, NgStyleInterface } from 'ng-zorro-antd/core'; - -import { DisplayedMark, ExtendedMark, isConfigAObject, Mark } from './nz-slider-definitions'; - -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush, - encapsulation: ViewEncapsulation.None, - preserveWhitespaces: false, - selector: 'nz-slider-marks', - exportAs: 'nzSliderMarks', - templateUrl: './nz-slider-marks.component.html' -}) -export class NzSliderMarksComponent implements OnChanges { - @Input() nzLowerBound: number | null = null; - @Input() nzUpperBound: number | null = null; - @Input() nzMarksArray: ExtendedMark[]; - @Input() nzMin: number; - @Input() nzMax: number; - @Input() @InputBoolean() nzVertical = false; - @Input() @InputBoolean() nzIncluded = false; - - marks: DisplayedMark[]; - - ngOnChanges(changes: SimpleChanges): void { - if (changes.nzMarksArray) { - this.buildMarks(); - } - if (changes.nzMarksArray || changes.nzLowerBound || changes.nzUpperBound) { - this.togglePointActive(); - } - } - - trackById(_index: number, mark: DisplayedMark): number { - return mark.value; - } - - private buildMarks(): void { - const range = this.nzMax - this.nzMin; - - this.marks = this.nzMarksArray.map(mark => { - const { value, offset, config } = mark; - const style = this.getMarkStyles(value, range, config); - const label = isConfigAObject(config) ? config.label : config; - - return { - label, - offset, - style, - value, - config, - active: false - }; - }); - } - - private getMarkStyles(value: number, range: number, config: Mark): NgStyleInterface { - let style; - - if (this.nzVertical) { - style = { - marginBottom: '-50%', - bottom: `${((value - this.nzMin) / range) * 100}%` - }; - } else { - style = { - transform: `translate3d(-50%, 0, 0)`, - left: `${((value - this.nzMin) / range) * 100}%` - }; - } - - if (isConfigAObject(config) && config.style) { - style = { ...style, ...config.style }; - } - - return style; - } - - private togglePointActive(): void { - if (this.marks && this.nzLowerBound !== null && this.nzUpperBound !== null) { - this.marks.forEach(mark => { - const value = mark.value; - const isActive = - (!this.nzIncluded && value === this.nzUpperBound) || - (this.nzIncluded && value <= this.nzUpperBound! && value >= this.nzLowerBound!); - - mark.active = isActive; - }); - } - } -} diff --git a/components/slider/nz-slider-step.component.html b/components/slider/nz-slider-step.component.html deleted file mode 100644 index f58b1702b0e..00000000000 --- a/components/slider/nz-slider-step.component.html +++ /dev/null @@ -1,9 +0,0 @@ -
- - -
diff --git a/components/slider/nz-slider-track.component.html b/components/slider/nz-slider-track.component.html deleted file mode 100644 index b604256b684..00000000000 --- a/components/slider/nz-slider-track.component.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/components/slider/nz-slider.component.html b/components/slider/nz-slider.component.html deleted file mode 100644 index 5a47a992d60..00000000000 --- a/components/slider/nz-slider.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
- - - - -
diff --git a/components/slider/public-api.ts b/components/slider/public-api.ts index 323ea1e39c0..dd2f3a335d9 100644 --- a/components/slider/public-api.ts +++ b/components/slider/public-api.ts @@ -6,10 +6,11 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -export * from './nz-slider.component'; -export * from './nz-slider.module'; -export * from './nz-slider-handle.component'; -export * from './nz-slider-marks.component'; -export * from './nz-slider-step.component'; -export * from './nz-slider-track.component'; -export * from './nz-slider-definitions'; +export { NzSliderComponent } from './slider.component'; +export { NzSliderService as ɵNzSliderService } from './slider.service'; +export { NzSliderModule } from './slider.module'; +export { NzSliderHandleComponent as ɵNzSliderHandleComponent } from './handle.component'; +export { NzSliderMarksComponent as ɵNzSliderMarksComponent } from './marks.component'; +export { NzSliderStepComponent as ɵNzSliderStepComponent } from './step.component'; +export { NzSliderTrackComponent as ɵNzSliderTrackComponent, NzSliderTrackStyle } from './track.component'; +export * from './typings'; diff --git a/components/slider/nz-slider.component.ts b/components/slider/slider.component.ts similarity index 61% rename from components/slider/nz-slider.component.ts rename to components/slider/slider.component.ts index a077178a6c6..858c9570c20 100644 --- a/components/slider/nz-slider.component.ts +++ b/components/slider/slider.component.ts @@ -6,6 +6,7 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ +import { DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW, UP_ARROW } from '@angular/cdk/keycodes'; import { Platform } from '@angular/cdk/platform'; import { ChangeDetectionStrategy, @@ -19,8 +20,10 @@ import { OnDestroy, OnInit, Output, + QueryList, SimpleChanges, ViewChild, + ViewChildren, ViewEncapsulation } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -34,13 +37,15 @@ import { getPercent, getPrecision, InputBoolean, + InputNumber, MouseTouchObserverConfig, - shallowCopyArray, silentEvent } from 'ng-zorro-antd/core'; -import { ExtendedMark, isValueARange, NzMarks, SliderHandler, SliderShowTooltip, SliderValue } from './nz-slider-definitions'; -import { getValueTypeNotMatchError } from './nz-slider-error'; +import { NzSliderHandleComponent } from './handle.component'; +import { NzSliderService } from './slider.service'; + +import { NzExtendedMark, NzMarks, NzSliderHandler, NzSliderShowTooltip, NzSliderValue } from './typings'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -53,39 +58,81 @@ import { getValueTypeNotMatchError } from './nz-slider-error'; provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NzSliderComponent), multi: true - } + }, + NzSliderService ], - templateUrl: './nz-slider.component.html' + host: { + '(keydown)': 'onKeyDown($event)' + }, + template: ` +
+
+ + + + +
+ ` }) export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChanges, OnDestroy { @ViewChild('slider', { static: true }) slider: ElementRef; + @ViewChildren(NzSliderHandleComponent) handlerComponents: QueryList; @Input() @InputBoolean() nzDisabled = false; @Input() @InputBoolean() nzDots: boolean = false; @Input() @InputBoolean() nzIncluded: boolean = true; @Input() @InputBoolean() nzRange: boolean = false; @Input() @InputBoolean() nzVertical: boolean = false; - @Input() nzDefaultValue: SliderValue | null = null; + @Input() nzDefaultValue?: NzSliderValue; @Input() nzMarks: NzMarks | null = null; - @Input() nzMax = 100; - @Input() nzMin = 0; - @Input() nzStep = 1; - @Input() nzTooltipVisible: SliderShowTooltip = 'default'; + @Input() @InputNumber() nzMax = 100; + @Input() @InputNumber() nzMin = 0; + @Input() @InputNumber() nzStep = 1; + @Input() nzTooltipVisible: NzSliderShowTooltip = 'default'; @Input() nzTooltipPlacement: string = 'top'; @Input() nzTipFormatter: (value: number) => string; - @Output() readonly nzOnAfterChange = new EventEmitter(); + @Output() readonly nzOnAfterChange = new EventEmitter(); - value: SliderValue | null = null; - sliderDOM: HTMLDivElement; + value: NzSliderValue | null = null; cacheSliderStart: number | null = null; cacheSliderLength: number | null = null; activeValueIndex: number | undefined = undefined; // Current activated handle's index ONLY for range=true track: { offset: null | number; length: null | number } = { offset: null, length: null }; // Track's offset and length - handles: SliderHandler[]; // Handles' offset - marksArray: ExtendedMark[] | null; // "steps" in array type with more data & FILTER out the invalid mark - bounds: { lower: SliderValue | null; upper: SliderValue | null } = { lower: null, upper: null }; // now for nz-slider-step - isDragging = false; // Current dragging state + handles: NzSliderHandler[]; // Handles' offset + marksArray: NzExtendedMark[] | null; // "steps" in array type with more data & FILTER out the invalid mark + bounds: { lower: NzSliderValue | null; upper: NzSliderValue | null } = { lower: null, upper: null }; // now for nz-slider-step private dragStart$: Observable; private dragMove$: Observable; @@ -94,16 +141,14 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange private dragMove_: Subscription | null; private dragEnd_: Subscription | null; - constructor(private cdr: ChangeDetectorRef, private platform: Platform) {} + constructor(private sliderService: NzSliderService, private cdr: ChangeDetectorRef, private platform: Platform) {} ngOnInit(): void { - this.handles = this.generateHandles(this.nzRange ? 2 : 1); - this.sliderDOM = this.slider.nativeElement; + this.handles = generateHandlers(this.nzRange ? 2 : 1); this.marksArray = this.nzMarks ? this.generateMarkItems(this.nzMarks) : null; - if (this.platform.isBrowser) { - this.createDraggingObservables(); - } + this.bindDraggingHandlers(); this.toggleDragDisabled(this.nzDisabled); + if (this.getValue() === null) { this.setValue(this.formatValue(null)); } @@ -125,15 +170,15 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange this.unsubscribeDrag(); } - writeValue(val: SliderValue | null): void { + writeValue(val: NzSliderValue | null): void { this.setValue(val, true); } - onValueChange(_value: SliderValue): void {} + onValueChange(_value: NzSliderValue): void {} onTouched(): void {} - registerOnChange(fn: (value: SliderValue) => void): void { + registerOnChange(fn: (value: NzSliderValue) => void): void { this.onValueChange = fn; } @@ -146,20 +191,39 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange this.toggleDragDisabled(isDisabled); } - private setValue(value: SliderValue | null, isWriteValue: boolean = false): void { + /** + * Event handler is only triggered when a slider handler is focused. + */ + onKeyDown(e: KeyboardEvent): void { + const code = e.keyCode; + const isIncrease = code === RIGHT_ARROW || code === UP_ARROW; + const isDecrease = code === LEFT_ARROW || code === DOWN_ARROW; + + if (!(isIncrease || isDecrease)) { + return; + } + + e.preventDefault(); + + const step = isDecrease ? -this.nzStep : this.nzStep; + const newVal = this.nzRange ? (this.value as number[])[this.activeValueIndex!] + step : (this.value as number) + step; + this.setActiveValue(ensureNumberInRange(newVal, this.nzMin, this.nzMax)); + } + + private setValue(value: NzSliderValue | null, isWriteValue: boolean = false): void { if (isWriteValue) { this.value = this.formatValue(value); this.updateTrackAndHandles(); - } else if (!this.valuesEqual(this.value!, value!)) { + } else if (!valuesEqual(this.value!, value!)) { this.value = value; this.updateTrackAndHandles(); this.onValueChange(this.getValue(true)); } } - private getValue(cloneAndSort: boolean = false): SliderValue { - if (cloneAndSort && this.value && isValueARange(this.value)) { - return shallowCopyArray(this.value).sort((a, b) => a - b); + private getValue(cloneAndSort: boolean = false): NzSliderValue { + if (cloneAndSort && this.value && isValueRange(this.value)) { + return [...this.value].sort((a, b) => a - b); } return this.value!; } @@ -167,22 +231,22 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange /** * Clone & sort current value and convert them to offsets, then return the new one. */ - private getValueToOffset(value?: SliderValue): SliderValue { + private getValueToOffset(value?: NzSliderValue): NzSliderValue { let normalizedValue = value; if (typeof normalizedValue === 'undefined') { normalizedValue = this.getValue(true); } - return isValueARange(normalizedValue) ? normalizedValue.map(val => this.valueToOffset(val)) : this.valueToOffset(normalizedValue); + return isValueRange(normalizedValue) ? normalizedValue.map(val => this.valueToOffset(val)) : this.valueToOffset(normalizedValue); } /** - * Find the closest value to be activated (only for range = true). + * Find the closest value to be activated. */ private setActiveValueIndex(pointerValue: number): void { const value = this.getValue(); - if (isValueARange(value)) { + if (isValueRange(value)) { let minimal: number | null = null; let gap: number; let activeIndex = -1; @@ -194,12 +258,15 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange } }); this.activeValueIndex = activeIndex; + this.handlerComponents.toArray()[activeIndex].focus(); + } else { + this.handlerComponents.toArray()[0].focus(); } } private setActiveValue(pointerValue: number): void { - if (isValueARange(this.value!)) { - const newValue = shallowCopyArray(this.value as number[]); + if (isValueRange(this.value!)) { + const newValue = [...(this.value as number[])]; newValue[this.activeValueIndex!] = pointerValue; this.setValue(newValue); } else { @@ -215,12 +282,12 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange const offset = this.getValueToOffset(value); const valueSorted = this.getValue(true); const offsetSorted = this.getValueToOffset(valueSorted); - const boundParts = isValueARange(valueSorted) ? valueSorted : [0, valueSorted]; - const trackParts = isValueARange(offsetSorted) ? [offsetSorted[0], offsetSorted[1] - offsetSorted[0]] : [0, offsetSorted]; + const boundParts = isValueRange(valueSorted) ? valueSorted : [0, valueSorted]; + const trackParts = isValueRange(offsetSorted) ? [offsetSorted[0], offsetSorted[1] - offsetSorted[0]] : [0, offsetSorted]; this.handles.forEach((handle, index) => { - handle.offset = isValueARange(offset) ? offset[index] : offset; - handle.value = isValueARange(value) ? value[index] : value || 0; + handle.offset = isValueRange(offset) ? offset[index] : offset; + handle.value = isValueRange(value) ? value[index] : value || 0; }); [this.bounds.lower, this.bounds.upper] = boundParts; @@ -253,8 +320,12 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange /** * Create user interactions handles. */ - private createDraggingObservables(): void { - const sliderDOM = this.sliderDOM; + private bindDraggingHandlers(): void { + if (!this.platform.isBrowser) { + return; + } + + const sliderDOM = this.slider.nativeElement; const orientField = this.nzVertical ? 'pageY' : 'pageX'; const mouse: MouseTouchObserverConfig = { start: 'mousedown', @@ -330,10 +401,10 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange private toggleDragMoving(movable: boolean): void { const periods = ['move', 'end']; if (movable) { - this.isDragging = true; + this.sliderService.isDragging = true; this.subscribeDrag(periods); } else { - this.isDragging = false; + this.sliderService.isDragging = false; this.unsubscribeDrag(periods); } } @@ -352,12 +423,13 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange const ratio = ensureNumberInRange((position - sliderStart) / sliderLength, 0, 1); const val = (this.nzMax - this.nzMin) * (this.nzVertical ? 1 - ratio : ratio) + this.nzMin; const points = this.nzMarks === null ? [] : Object.keys(this.nzMarks).map(parseFloat); - if (this.nzStep !== null && !this.nzDots) { + if (this.nzStep !== 0 && !this.nzDots) { const closestOne = Math.round(val / this.nzStep) * this.nzStep; points.push(closestOne); } const gaps = points.map(point => Math.abs(val - point)); const closest = points[gaps.indexOf(Math.min(...gaps))]; + return this.nzStep === null ? closest : parseFloat(closest.toFixed(getPrecision(this.nzStep))); } @@ -369,7 +441,7 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange if (this.cacheSliderStart !== null) { return this.cacheSliderStart; } - const offset = getElementOffset(this.sliderDOM); + const offset = getElementOffset(this.slider.nativeElement); return this.nzVertical ? offset.top : offset.left; } @@ -377,7 +449,7 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange if (this.cacheSliderLength !== null) { return this.cacheSliderLength; } - const sliderDOM = this.sliderDOM; + const sliderDOM = this.slider.nativeElement; return this.nzVertical ? sliderDOM.clientHeight : sliderDOM.clientWidth; } @@ -389,47 +461,16 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange this.cacheSliderLength = remove ? null : this.getSliderLength(); } - private formatValue(value: SliderValue | null): SliderValue { - let res = value; - if (!this.assertValueValid(value!)) { - res = this.nzDefaultValue === null ? (this.nzRange ? [this.nzMin, this.nzMax] : this.nzMin) : this.nzDefaultValue; - } else { - res = isValueARange(value!) - ? (value as number[]).map(val => ensureNumberInRange(val, this.nzMin, this.nzMax)) - : ensureNumberInRange(value as number, this.nzMin, this.nzMax); - } - - return res; - } - - /** - * Check if value is valid and throw error if value-type/range not match. - */ - private assertValueValid(value: SliderValue): boolean { - if (!Array.isArray(value) && isNaN(typeof value !== 'number' ? parseFloat(value) : value)) { - return false; - } - return this.assertValueTypeMatch(value); - } - - /** - * Assert that if `this.nzRange` is `true`, value is also a range, vice versa. - */ - private assertValueTypeMatch(value: SliderValue | null): boolean { + private formatValue(value: NzSliderValue | null): NzSliderValue { if (!value) { - return true; - } else if (isValueARange(value) !== this.nzRange) { - throw getValueTypeNotMatchError(); + return this.nzRange ? [this.nzMin, this.nzMax] : this.nzMin; + } else if (assertValueValid(value, this.nzRange)) { + return isValueRange(value) + ? value.map(val => ensureNumberInRange(val, this.nzMin, this.nzMax)) + : ensureNumberInRange(value, this.nzMin, this.nzMax); } else { - return true; - } - } - - private valuesEqual(valA: SliderValue, valB: SliderValue): boolean { - if (typeof valA !== typeof valB) { - return false; + return this.nzDefaultValue ? this.nzDefaultValue : this.nzRange ? [this.nzMin, this.nzMax] : this.nzMin; } - return isValueARange(valA) && isValueARange(valB) ? arraysEqual(valA, valB) : valA === valB; } /** @@ -445,14 +486,8 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange this.handles.forEach(handle => (handle.active = false)); } - private generateHandles(amount: number): SliderHandler[] { - return Array(amount) - .fill(0) - .map(() => ({ offset: null, value: null, active: false })); - } - - private generateMarkItems(marks: NzMarks): ExtendedMark[] | null { - const marksArray: ExtendedMark[] = []; + private generateMarkItems(marks: NzMarks): NzExtendedMark[] | null { + const marksArray: NzExtendedMark[] = []; for (const key in marks) { const mark = marks[key]; const val = typeof key === 'number' ? key : parseFloat(key); @@ -463,3 +498,50 @@ export class NzSliderComponent implements ControlValueAccessor, OnInit, OnChange return marksArray.length ? marksArray : null; } } + +function getValueTypeNotMatchError(): Error { + return new Error( + `The "nzRange" can't match the "ngModel"'s type, please check these properties: "nzRange", "ngModel", "nzDefaultValue".` + ); +} + +function isValueRange(value: NzSliderValue): value is number[] { + if (value instanceof Array) { + return value.length === 2; + } else { + return false; + } +} + +function generateHandlers(amount: number): NzSliderHandler[] { + return Array(amount) + .fill(0) + .map(() => ({ offset: null, value: null, active: false })); +} + +/** + * Check if value is valid and throw error if value-type/range not match. + */ +function assertValueValid(value: NzSliderValue, isRange?: boolean): boolean { + if ((!isValueRange(value) && isNaN(value)) || (isValueRange(value) && value.some(v => isNaN(v)))) { + return false; + } + return assertValueTypeMatch(value, isRange); +} + +/** + * Assert that if `this.nzRange` is `true`, value is also a range, vice versa. + */ +function assertValueTypeMatch(value: NzSliderValue, isRange: boolean = false): boolean { + if (isValueRange(value) !== isRange) { + throw getValueTypeNotMatchError(); + } + return true; +} + +function valuesEqual(valA: NzSliderValue, valB: NzSliderValue): boolean { + if (typeof valA !== typeof valB) { + return false; + } + return isValueRange(valA) && isValueRange(valB) ? arraysEqual(valA, valB) : valA === valB; +} diff --git a/components/slider/nz-slider.module.ts b/components/slider/slider.module.ts similarity index 70% rename from components/slider/nz-slider.module.ts rename to components/slider/slider.module.ts index 33e490b2be3..c65118c6883 100644 --- a/components/slider/nz-slider.module.ts +++ b/components/slider/slider.module.ts @@ -10,11 +10,11 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { NzToolTipModule } from 'ng-zorro-antd/tooltip'; -import { NzSliderHandleComponent } from './nz-slider-handle.component'; -import { NzSliderMarksComponent } from './nz-slider-marks.component'; -import { NzSliderStepComponent } from './nz-slider-step.component'; -import { NzSliderTrackComponent } from './nz-slider-track.component'; -import { NzSliderComponent } from './nz-slider.component'; +import { NzSliderHandleComponent } from './handle.component'; +import { NzSliderMarksComponent } from './marks.component'; +import { NzSliderComponent } from './slider.component'; +import { NzSliderStepComponent } from './step.component'; +import { NzSliderTrackComponent } from './track.component'; @NgModule({ exports: [NzSliderComponent, NzSliderTrackComponent, NzSliderHandleComponent, NzSliderStepComponent, NzSliderMarksComponent], diff --git a/components/slider/nz-slider-error.ts b/components/slider/slider.service.ts similarity index 54% rename from components/slider/nz-slider-error.ts rename to components/slider/slider.service.ts index 2b331ba412c..cf4788e0e82 100644 --- a/components/slider/nz-slider-error.ts +++ b/components/slider/slider.service.ts @@ -6,8 +6,9 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -export function getValueTypeNotMatchError(): Error { - return new Error( - `The "nzRange" can't match the "ngModel"'s type, please check these properties: "nzRange", "ngModel", "nzDefaultValue".` - ); +import { Injectable } from '@angular/core'; + +@Injectable() +export class NzSliderService { + isDragging = false; } diff --git a/components/slider/nz-slider.spec.ts b/components/slider/slider.spec.ts similarity index 79% rename from components/slider/nz-slider.spec.ts rename to components/slider/slider.spec.ts index 514ee528f64..001bf78afa4 100644 --- a/components/slider/nz-slider.spec.ts +++ b/components/slider/slider.spec.ts @@ -1,52 +1,44 @@ +import { LEFT_ARROW, RIGHT_ARROW } from '@angular/cdk/keycodes'; import { OverlayContainer } from '@angular/cdk/overlay'; import { Component, DebugElement, OnInit } from '@angular/core'; -import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, inject, tick } from '@angular/core/testing'; import { AbstractControl, FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { dispatchMouseEvent } from 'ng-zorro-antd/core'; -import { SliderShowTooltip } from './nz-slider-definitions'; -import { NzSliderComponent } from './nz-slider.component'; -import { NzSliderModule } from './nz-slider.module'; - -describe('NzSlider', () => { - beforeEach(fakeAsync(() => { - TestBed.configureTestingModule({ - imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule], - declarations: [ - StandardSliderComponent, - DisableSliderComponent, - SliderWithMinAndMaxComponent, - SliderWithValueComponent, - SliderWithStepComponent, - SliderWithValueSmallerThanMinComponent, - SliderWithValueGreaterThanMaxComponent, - VerticalSliderComponent, - MixedSliderComponent, - SliderWithFormControlComponent, - SliderShowTooltipComponent - ] - }); - - TestBed.compileComponents(); - })); - - describe('standard slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; +import { dispatchKeyboardEvent, dispatchMouseEvent } from 'ng-zorro-antd/core'; +import { ComponentBed, createComponentBed } from 'ng-zorro-antd/core/testing/componet-bed'; + +import { NzSliderComponent } from './slider.component'; +import { NzSliderModule } from './slider.module'; +import { NzSliderShowTooltip } from './typings'; + +describe('nz-slider', () => { + let sliderDebugElement: DebugElement; + let sliderNativeElement: HTMLElement; + let sliderInstance: NzSliderComponent; + let overlayContainerElement: HTMLElement; + + // tslint:disable-next-line:no-any + function getReferenceFromFixture(fixture: ComponentFixture): void { + sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); + sliderInstance = sliderDebugElement.componentInstance; + sliderNativeElement = sliderInstance.slider.nativeElement; + } + + describe('basic', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; let trackFillElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(StandardSliderComponent); + testBed = createComponentBed(NzTestSliderComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; - + getReferenceFromFixture(fixture); trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -126,88 +118,19 @@ describe('NzSlider', () => { }); }); - describe('show tooltip', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; - let testComponent: SliderShowTooltipComponent; - let overlayContainerElement: HTMLElement; - - beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => { - overlayContainerElement = oc.getContainerElement(); - })); + describe('disabled', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; beforeEach(() => { - fixture = TestBed.createComponent(SliderShowTooltipComponent); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); - sliderNativeElement = sliderInstance.sliderDOM; - }); - - it('should always display tooltips if set to `always`', fakeAsync(() => { - testComponent.show = 'always'; - fixture.detectChanges(); - tick(400); - fixture.detectChanges(); - expect(overlayContainerElement.textContent).toContain('0'); - - dispatchClickEventSequence(sliderNativeElement, 0.13); - fixture.detectChanges(); - expect(overlayContainerElement.textContent).toContain('13'); - - // Always show tooltip even when handle is not hovered. - fixture.detectChanges(); - expect(overlayContainerElement.textContent).toContain('13'); - - tick(400); - })); - - it('should never display tooltips if set to `never`', fakeAsync(() => { - const handlerHost = sliderNativeElement.querySelector('nz-slider-handle')!; - - testComponent.show = 'never'; - tick(400); - fixture.detectChanges(); - expect(overlayContainerElement.textContent).not.toContain('0'); - - dispatchClickEventSequence(sliderNativeElement, 0.13); - fixture.detectChanges(); - - // Do not show tooltip even when handle is hovered. - dispatchMouseEvent(handlerHost, 'mouseenter'); - fixture.detectChanges(); - expect(overlayContainerElement.textContent).not.toContain('13'); - })); - }); - - describe('disabled slider', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; - let testComponent: DisableSliderComponent; - - beforeEach(() => { - fixture = TestBed.createComponent(DisableSliderComponent); - fixture.detectChanges(); - - testComponent = fixture.debugElement.componentInstance; - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; - }); - - it("should/shouldn't be disabled", () => { - expect(sliderInstance.nzDisabled).toBeTruthy(); - - testComponent.disable = false; + testBed = createComponentBed(NzTestSliderComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; + fixture.componentInstance.disabled = true; fixture.detectChanges(); - expect(sliderInstance.nzDisabled).toBeFalsy(); + getReferenceFromFixture(fixture); }); it('should not change the value on click when disabled', () => { @@ -240,79 +163,77 @@ describe('NzSlider', () => { }); }); - describe('slider with set min and max', () => { - let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; - let trackFillElement: HTMLElement; + describe('show tooltip', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; + let testComponent: SliderShowTooltipComponent; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithMinAndMaxComponent); + testBed = createComponentBed(SliderShowTooltipComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); - + testComponent = fixture.debugElement.componentInstance; sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; - trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; + sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); + sliderNativeElement = sliderInstance.slider.nativeElement; }); - it('should set the default values from the attributes', () => { - expect(sliderInstance.value).toBe(4); - expect(sliderInstance.nzMin).toBe(4); - expect(sliderInstance.nzMax).toBe(6); - }); + beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => { + overlayContainerElement = oc.getContainerElement(); + })); - it('should set the correct value on click', () => { - dispatchClickEventSequence(sliderNativeElement, 0.09); + it('should always display tooltips if set to `always`', fakeAsync(() => { + testComponent.show = 'always'; + fixture.detectChanges(); + tick(400); fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('0'); - // Computed by multiplying the difference between the min and the max by the percentage from - // the click and adding that to the minimum. - const value = Math.round((6 - 4) * 0.09 + 4); - expect(sliderInstance.value).toBe(value); - }); + dispatchClickEventSequence(sliderNativeElement, 0.13); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('13'); - it('should set the correct value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.62); + // Always show tooltip even when handle is not hovered. fixture.detectChanges(); + expect(overlayContainerElement.textContent).toContain('13'); - // Computed by multiplying the difference between the min and the max by the percentage from - // the click and adding that to the minimum. - const value = Math.round((6 - 4) * 0.62 + 4); - expect(sliderInstance.value).toBe(value); - }); + tick(400); + })); - it('should snap the fill to the nearest value on click', () => { - dispatchClickEventSequence(sliderNativeElement, 0.68); - fixture.detectChanges(); + it('should never display tooltips if set to `never`', fakeAsync(() => { + const handlerHost = sliderNativeElement.querySelector('nz-slider-handle')!; - // The closest snap is halfway on the slider. - expect(trackFillElement.style.width).toBe('50%'); - }); + testComponent.show = 'never'; + tick(400); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).not.toContain('0'); - it('should snap the fill to the nearest value on slide', () => { - dispatchSlideEventSequence(sliderNativeElement, 0, 0.74); + dispatchClickEventSequence(sliderNativeElement, 0.13); fixture.detectChanges(); - // The closest snap is at the halfway point on the slider. - expect(trackFillElement.style.width).toBe('50%'); - }); + // Do not show tooltip even when handle is hovered. + dispatchMouseEvent(handlerHost, 'mouseenter'); + fixture.detectChanges(); + expect(overlayContainerElement.textContent).not.toContain('13'); + })); }); - describe('slider with set value', () => { + describe('setting value', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithValueComponent); + testBed = createComponentBed(SliderWithValueComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); - sliderNativeElement = sliderInstance.sliderDOM; + sliderNativeElement = sliderInstance.slider.nativeElement; }); it('should set the default value from the attribute', () => { @@ -336,20 +257,21 @@ describe('NzSlider', () => { }); }); - describe('slider with set step', () => { + describe('step', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; let trackFillElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithStepComponent); + testBed = createComponentBed(SliderWithStepComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); - sliderNativeElement = sliderInstance.sliderDOM; + sliderNativeElement = sliderInstance.slider.nativeElement; trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -411,20 +333,78 @@ describe('NzSlider', () => { }); }); - describe('slider with set min and max and a value smaller than min', () => { + describe('min and max', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; + let trackFillElement: HTMLElement; + + beforeEach(() => { + testBed = createComponentBed(SliderWithMinAndMaxComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; + fixture.detectChanges(); + + getReferenceFromFixture(fixture); + trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; + }); + + it('should set the default values from the attributes', () => { + expect(sliderInstance.value).toBe(4); + expect(sliderInstance.nzMin).toBe(4); + expect(sliderInstance.nzMax).toBe(6); + }); + + it('should set the correct value on click', () => { + dispatchClickEventSequence(sliderNativeElement, 0.09); + fixture.detectChanges(); + + // Computed by multiplying the difference between the min and the max by the percentage from + // the click and adding that to the minimum. + const value = Math.round((6 - 4) * 0.09 + 4); + expect(sliderInstance.value).toBe(value); + }); + + it('should set the correct value on slide', () => { + dispatchSlideEventSequence(sliderNativeElement, 0, 0.62); + fixture.detectChanges(); + + // Computed by multiplying the difference between the min and the max by the percentage from + // the click and adding that to the minimum. + const value = Math.round((6 - 4) * 0.62 + 4); + expect(sliderInstance.value).toBe(value); + }); + + it('should snap the fill to the nearest value on click', () => { + dispatchClickEventSequence(sliderNativeElement, 0.68); + fixture.detectChanges(); + + // The closest snap is halfway on the slider. + expect(trackFillElement.style.width).toBe('50%'); + }); + + it('should snap the fill to the nearest value on slide', () => { + dispatchSlideEventSequence(sliderNativeElement, 0, 0.74); + fixture.detectChanges(); + + // The closest snap is at the halfway point on the slider. + expect(trackFillElement.style.width).toBe('50%'); + }); + }); + + describe('min and max and a value smaller than min', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; let trackFillElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithValueSmallerThanMinComponent); + testBed = createComponentBed(SliderWithValueSmallerThanMinComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; + getReferenceFromFixture(fixture); trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -439,20 +419,19 @@ describe('NzSlider', () => { }); }); - describe('slider with set min and max and a value greater than max', () => { + describe('min and max and a value greater than max', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; let trackFillElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithValueGreaterThanMaxComponent); + testBed = createComponentBed(SliderWithValueGreaterThanMaxComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; + getReferenceFromFixture(fixture); trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -472,20 +451,21 @@ describe('NzSlider', () => { }); }); - describe('vertical slider', () => { + describe('vertical', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; let trackFillElement: HTMLElement; - let sliderInstance: NzSliderComponent; beforeEach(() => { - fixture = TestBed.createComponent(VerticalSliderComponent); + testBed = createComponentBed(VerticalSliderComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); sliderInstance = sliderDebugElement.injector.get(NzSliderComponent); - sliderNativeElement = sliderInstance.sliderDOM; + sliderNativeElement = sliderInstance.slider.nativeElement; trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -511,23 +491,21 @@ describe('NzSlider', () => { }); }); - describe('mixed slider usage', () => { + describe('mixed usage', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; let trackFillElement: HTMLElement; - let sliderInstance: NzSliderComponent; let testComponent: MixedSliderComponent; - let overlayContainerElement: HTMLElement; beforeEach(() => { - fixture = TestBed.createComponent(MixedSliderComponent); + testBed = createComponentBed(MixedSliderComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; + getReferenceFromFixture(fixture); trackFillElement = sliderNativeElement.querySelector('.ant-slider-track') as HTMLElement; }); @@ -543,10 +521,11 @@ describe('NzSlider', () => { fixture.detectChanges(); dispatchClickEventSequence(sliderNativeElement, 0.1); - dispatchClickEventSequence(sliderNativeElement, 0.6); + // Potentially a bug of jasmine or karma. Event handler makes calling stack destroyed. + // dispatchClickEventSequence(sliderNativeElement, 0.8); fixture.detectChanges(); - expect(sliderInstance.value).toEqual([10, 60]); + expect(sliderInstance.value).toEqual([10, 100]); }); it("should/shouldn't be included", () => { @@ -606,22 +585,21 @@ describe('NzSlider', () => { }); describe('slider as a custom form control', () => { + let testBed: ComponentBed; let fixture: ComponentFixture; - let sliderDebugElement: DebugElement; - let sliderNativeElement: HTMLElement; - let sliderInstance: NzSliderComponent; let testComponent: SliderWithFormControlComponent; let sliderControl: AbstractControl; beforeEach(() => { - fixture = TestBed.createComponent(SliderWithFormControlComponent); + testBed = createComponentBed(SliderWithFormControlComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - sliderDebugElement = fixture.debugElement.query(By.directive(NzSliderComponent)); - sliderInstance = sliderDebugElement.componentInstance; - sliderNativeElement = sliderInstance.sliderDOM; + getReferenceFromFixture(fixture); sliderControl = testComponent.form.controls.slider; }); @@ -708,6 +686,49 @@ describe('NzSlider', () => { expect(sliderControl.touched).toBe(true); }); }); + + describe('support keyboard event', () => { + let testBed: ComponentBed; + let fixture: ComponentFixture; + let testComponent: NzTestSliderKeyboardComponent; + + beforeEach(() => { + testBed = createComponentBed(NzTestSliderKeyboardComponent, { + imports: [NzSliderModule, FormsModule, ReactiveFormsModule, NoopAnimationsModule] + }); + fixture = testBed.fixture; + testComponent = testBed.component; + fixture.detectChanges(); + + getReferenceFromFixture(fixture); + }); + + it('should work for non-range slider', () => { + dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW); + + expect(sliderInstance.value).toBe(1); + }); + + it('should work for range slider', () => { + testComponent.range = true; + sliderInstance.activeValueIndex = 0; + fixture.detectChanges(); + + dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); + dispatchKeyboardEvent(sliderNativeElement, 'keydown', RIGHT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toEqual([2, 100]); + + sliderInstance.activeValueIndex = 1; + dispatchKeyboardEvent(sliderNativeElement, 'keydown', LEFT_ARROW); + fixture.detectChanges(); + + expect(sliderInstance.value).toEqual([2, 99]); + }); + }); }); const styles = ` @@ -723,22 +744,13 @@ const styles = ` @Component({ template: ` - + `, styles: [styles] }) -class StandardSliderComponent {} - -@Component({ - template: ` - - `, - styles: [styles] -}) -class DisableSliderComponent { - disable = true; +class NzTestSliderComponent { + disabled = false; } - @Component({ template: ` @@ -843,10 +855,19 @@ class SliderWithFormControlComponent implements OnInit { ` }) class SliderShowTooltipComponent { - show: SliderShowTooltip = 'default'; + show: NzSliderShowTooltip = 'default'; value = 0; } +@Component({ + template: ` + + ` +}) +class NzTestSliderKeyboardComponent { + range = false; +} + /** * Dispatches a click event sequence (consisting of moueseenter, click) from an element. * Note: The mouse event truncates the position for the click. diff --git a/components/slider/nz-slider-step.component.ts b/components/slider/step.component.ts similarity index 54% rename from components/slider/nz-slider-step.component.ts rename to components/slider/step.component.ts index 8e001911345..adea162e5da 100644 --- a/components/slider/nz-slider-step.component.ts +++ b/components/slider/step.component.ts @@ -10,7 +10,7 @@ import { ChangeDetectionStrategy, Component, Input, OnChanges, SimpleChanges, Vi import { InputBoolean } from 'ng-zorro-antd/core'; -import { DisplayedStep, ExtendedMark } from './nz-slider-definitions'; +import { NzDisplayedStep, NzExtendedMark } from './typings'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -18,34 +18,44 @@ import { DisplayedStep, ExtendedMark } from './nz-slider-definitions'; selector: 'nz-slider-step', exportAs: 'nzSliderStep', preserveWhitespaces: false, - templateUrl: './nz-slider-step.component.html' + template: ` +
+ + +
+ ` }) export class NzSliderStepComponent implements OnChanges { - @Input() nzLowerBound: number | null = null; - @Input() nzUpperBound: number | null = null; - @Input() nzMarksArray: ExtendedMark[]; - @Input() @InputBoolean() nzVertical = false; - @Input() @InputBoolean() nzIncluded = false; + @Input() lowerBound: number | null = null; + @Input() upperBound: number | null = null; + @Input() marksArray: NzExtendedMark[]; + @Input() @InputBoolean() vertical = false; + @Input() @InputBoolean() included = false; - steps: DisplayedStep[]; + steps: NzDisplayedStep[]; ngOnChanges(changes: SimpleChanges): void { - if (changes.nzMarksArray) { + if (changes.marksArray) { this.buildSteps(); } - if (changes.nzMarksArray || changes.nzLowerBound || changes.nzUpperBound) { + if (changes.marksArray || changes.lowerBound || changes.upperBound) { this.togglePointActive(); } } - trackById(_index: number, step: DisplayedStep): number { + trackById(_index: number, step: NzDisplayedStep): number { return step.value; } private buildSteps(): void { - const orient = this.nzVertical ? 'bottom' : 'left'; + const orient = this.vertical ? 'bottom' : 'left'; - this.steps = this.nzMarksArray.map(mark => { + this.steps = this.marksArray.map(mark => { const { value, offset, config } = mark; return { @@ -61,12 +71,11 @@ export class NzSliderStepComponent implements OnChanges { } private togglePointActive(): void { - if (this.steps && this.nzLowerBound !== null && this.nzUpperBound !== null) { + if (this.steps && this.lowerBound !== null && this.upperBound !== null) { this.steps.forEach(step => { const value = step.value; const isActive = - (!this.nzIncluded && value === this.nzUpperBound) || - (this.nzIncluded && value <= this.nzUpperBound! && value >= this.nzLowerBound!); + (!this.included && value === this.upperBound) || (this.included && value <= this.upperBound! && value >= this.lowerBound!); step.active = isActive; }); } diff --git a/components/slider/nz-slider-track.component.ts b/components/slider/track.component.ts similarity index 63% rename from components/slider/nz-slider-track.component.ts rename to components/slider/track.component.ts index bb8f2e655de..0ba2fb56fe2 100644 --- a/components/slider/nz-slider-track.component.ts +++ b/components/slider/track.component.ts @@ -24,29 +24,31 @@ export interface NzSliderTrackStyle { selector: 'nz-slider-track', exportAs: 'nzSliderTrack', preserveWhitespaces: false, - templateUrl: './nz-slider-track.component.html' + template: ` +
+ ` }) export class NzSliderTrackComponent implements OnChanges { - @Input() @InputNumber() nzOffset: number; - @Input() @InputNumber() nzLength: number; - @Input() @InputBoolean() nzVertical = false; - @Input() @InputBoolean() nzIncluded = false; + @Input() @InputNumber() offset: number; + @Input() @InputNumber() length: number; + @Input() @InputBoolean() vertical = false; + @Input() @InputBoolean() included = false; style: NzSliderTrackStyle = {}; ngOnChanges(changes: SimpleChanges): void { - if (changes.nzIncluded) { - this.style.visibility = this.nzIncluded ? 'visible' : 'hidden'; + if (changes.included) { + this.style.visibility = this.included ? 'visible' : 'hidden'; } - if (changes.nzVertical || changes.nzOffset || changes.nzLength) { - if (this.nzVertical) { - this.style.bottom = `${this.nzOffset}%`; - this.style.height = `${this.nzLength}%`; + if (changes.vertical || changes.offset || changes.length) { + if (this.vertical) { + this.style.bottom = `${this.offset}%`; + this.style.height = `${this.length}%`; this.style.left = null; this.style.width = null; } else { - this.style.left = `${this.nzOffset}%`; - this.style.width = `${this.nzLength}%`; + this.style.left = `${this.offset}%`; + this.style.width = `${this.length}%`; this.style.bottom = null; this.style.height = null; } diff --git a/components/slider/typings.ts b/components/slider/typings.ts new file mode 100644 index 00000000000..9113e689a55 --- /dev/null +++ b/components/slider/typings.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Alibaba.com All Rights Reserved. + * + * 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 type NzMark = string | NzMarkObj; + +export interface NzMarkObj { + style?: object; + label: string; +} + +export class NzMarks { + [key: number]: NzMark; +} + +/** + * Processed steps that would be passed to sub components. + */ +export interface NzExtendedMark { + value: number; + offset: number; + config: NzMark; +} + +/** + * Marks that would be rendered. + */ +export interface NzDisplayedMark extends NzExtendedMark { + active: boolean; + label: string; + style?: object; +} + +/** + * Steps that would be rendered. + */ +export interface NzDisplayedStep extends NzExtendedMark { + active: boolean; + style?: object; +} + +export type NzSliderShowTooltip = 'always' | 'never' | 'default'; + +export type NzSliderValue = number[] | number; + +export interface NzSliderHandler { + offset: number | null; + value: number | null; + active: boolean; +}