From 906849bfcc26bfebe166fa7ce37886592d9a5153 Mon Sep 17 00:00:00 2001 From: Wendell Date: Mon, 26 Aug 2019 11:40:15 +0800 Subject: [PATCH] fix(module:cascader): fix column is not dropped in hover mode (#3916) * fix: fix column is not dropped in hover mode * fix: improve code coverage --- components/affix/nz-affix.component.html | 2 +- components/cascader/demo/basic.ts | 2 +- components/cascader/doc/index.en-US.md | 5 +- components/cascader/doc/index.zh-CN.md | 6 +- .../cascader/nz-cascader.component.html | 2 +- components/cascader/nz-cascader.component.ts | 113 +++++++++++------- components/cascader/nz-cascader.service.ts | 42 ++++--- components/cascader/nz-cascader.spec.ts | 104 ++++++++++++++-- components/i18n/nz-i18n.interface.ts | 25 ++-- 9 files changed, 212 insertions(+), 89 deletions(-) diff --git a/components/affix/nz-affix.component.html b/components/affix/nz-affix.component.html index fccd5c8c4db..d09600028e0 100644 --- a/components/affix/nz-affix.component.html +++ b/components/affix/nz-affix.component.html @@ -1,3 +1,3 @@
-
\ No newline at end of file + diff --git a/components/cascader/demo/basic.ts b/components/cascader/demo/basic.ts index 0a1d26456f8..020a11aa4d0 100644 --- a/components/cascader/demo/basic.ts +++ b/components/cascader/demo/basic.ts @@ -83,7 +83,7 @@ const otherOptions = [ @Component({ selector: 'nz-demo-cascader-basic', template: ` - +   Change Options diff --git a/components/cascader/doc/index.en-US.md b/components/cascader/doc/index.en-US.md index d95ac66cacd..575548236cf 100755 --- a/components/cascader/doc/index.en-US.md +++ b/components/cascader/doc/index.en-US.md @@ -33,7 +33,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader'; | `[ngModel]` | selected value | `any[]` | - | | `[nzAllowClear]` | whether allow clear | `boolean` | `true` | | `[nzAutoFocus]` | whether auto focus the input box | `boolean` | `false` | -| `[nzChangeOn]` | change value on each selection if this function return `true` | `function(option: any, index: number) => boolean` | - | +| `[nzChangeOn]` | change value on each selection if this function return `true` | `(option: any, index: number) => boolean` | - | | `[nzChangeOnSelect]` | change value on each selection if set to true, see above demo for details | `boolean` | `false` | | `[nzColumnClassName]` | additional className of column in the popup overlay | `string` | - | | `[nzDisabled]` | whether disabled select | `boolean` | `false` | @@ -54,8 +54,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader'; | `(ngModelChange)` | Emit on values change | `EventEmitter` | - | | `(nzClear)` | Emit on clear values | `EventEmitter` | - | | `(nzVisibleChange)` | Emit on popup menu visible or hide | `EventEmitter` | - | -| `(nzSelect)` | Emit on select | `EventEmitter<{option: any, index: number}>` | - | -| `(nzSelectionChange)` | Emit on selection change | `EventEmitter` | - | +| `(nzSelectionChange)` | Emit on values change | `EventEmitter` | - | When `nzShowSearch` is an object it should implements `NzShowSearchOptions`: diff --git a/components/cascader/doc/index.zh-CN.md b/components/cascader/doc/index.zh-CN.md index d6af9e4aaa2..fed99ae2a53 100755 --- a/components/cascader/doc/index.zh-CN.md +++ b/components/cascader/doc/index.zh-CN.md @@ -34,7 +34,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader'; | `[ngModel]` | 指定选中项 | `any[]` | - | | `[nzAllowClear]` | 是否支持清除 | `boolean` | `true` | | `[nzAutoFocus]` | 是否自动聚焦,当存在输入框时 | `boolean` | `false` | -| `[nzChangeOn]` | 点击父级菜单选项时,可通过该函数判断是否允许值的变化 | `function(option: any, index: number) => boolean` | - | +| `[nzChangeOn]` | 点击父级菜单选项时,可通过该函数判断是否允许值的变化 | `(option: any, index: number) => boolean` | - | | `[nzChangeOnSelect]` | 当此项为 true 时,点选每级菜单选项值都会发生变化,具体见上面的演示 | `boolean` | `false` | | `[nzColumnClassName]` | 自定义浮层列类名 | `string` | - | | `[nzDisabled]` | 禁用 | `boolean` | `false` | @@ -53,10 +53,8 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader'; | `[nzSize]` | 输入框大小,可选 `large` `default` `small` | `'large' \| 'small' \| 'default'` | `'default'` | | `[nzValueProperty]` | 选项的实际值的属性名 | `string` | `'value'` | | `(ngModelChange)` | 值发生变化时触发 | `EventEmitter` | - | -| `(nzClear)` | 清空值时触发 | `EventEmitter` | - | | `(nzVisibleChange)` | 菜单浮层的显示/隐藏 | `EventEmitter` | - | -| `(nzSelect)` | 选中菜单选项时触发 | `EventEmitter<{option: any, index: number}>` | - | -| `(nzSelectionChange)` | 选中菜单选项时触发 | `EventEmitter` |- | +| `(nzSelectionChange)` | 值发生变化时触发 | `EventEmitter` |- | `nzShowSearch` 为对象时需遵守 `NzShowSearchOptions` 接口: diff --git a/components/cascader/nz-cascader.component.html b/components/cascader/nz-cascader.component.html index 4dce63f7fe0..6e42c613c31 100644 --- a/components/cascader/nz-cascader.component.html +++ b/components/cascader/nz-cascader.component.html @@ -11,7 +11,7 @@ [class.ant-cascader-input-lg]="nzSize === 'large'" [class.ant-cascader-input-sm]="nzSize === 'small'" [attr.autoComplete]="'off'" - [attr.placeholder]="showPlaceholder ? nzPlaceHolder : null" + [attr.placeholder]="showPlaceholder ? (nzPlaceHolder || locale.placeholder ) : null" [attr.autofocus]="nzAutoFocus ? 'autofocus' : null" [readonly]="!nzShowSearch" [disabled]="nzDisabled" diff --git a/components/cascader/nz-cascader.component.ts b/components/cascader/nz-cascader.component.ts index 6e6bf674cf4..98fd279d075 100644 --- a/components/cascader/nz-cascader.component.ts +++ b/components/cascader/nz-cascader.component.ts @@ -31,17 +31,19 @@ import { } from '@angular/core'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { startWith, takeUntil } from 'rxjs/operators'; import { slideMotion, toArray, + warnDeprecation, DEFAULT_DROPDOWN_POSITIONS, InputBoolean, NgClassType, NzNoAnimationDirective } from 'ng-zorro-antd/core'; +import { NzCascaderI18nInterface, NzI18nService } from 'ng-zorro-antd/i18n'; import { CascaderOption, CascaderSearchOption, @@ -59,7 +61,7 @@ const defaultDisplayRender = (labels: string[]) => labels.join(' / '); @Component({ changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - selector: 'nz-cascader,[nz-cascader]', + selector: 'nz-cascader, [nz-cascader]', exportAs: 'nzCascader', preserveWhitespaces: false, templateUrl: './nz-cascader.component.html', @@ -114,7 +116,7 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, @Input() nzNotFoundContent: string | TemplateRef; @Input() nzSize: NzCascaderSize = 'default'; @Input() nzShowSearch: boolean | NzShowSearchOptions; - @Input() nzPlaceHolder = 'Please select'; // TODO: i18n? + @Input() nzPlaceHolder: string; @Input() nzMenuClassName: string; @Input() nzMenuStyle: { [key: string]: string }; @Input() nzMouseEnterDelay: number = 150; // ms @@ -132,11 +134,16 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, this.cascaderService.withOptions(options); } + @Output() readonly nzVisibleChange = new EventEmitter(); + @Output() readonly nzSelectionChange = new EventEmitter(); + + /** + * @deprecated 9.0.0. This api is a duplication of `ngModelChange`. + */ @Output() readonly nzSelect = new EventEmitter<{ option: CascaderOption; index: number } | null>(); + @Output() readonly nzClear = new EventEmitter(); - @Output() readonly nzVisibleChange = new EventEmitter(); // Not exposed, only for test - @Output() readonly nzChange = new EventEmitter(); // Not exposed, only for test el: HTMLElement; dropDownPosition = 'bottom'; @@ -150,6 +157,8 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, dropdownWidthStyle: string; isFocused = false; + locale: NzCascaderI18nInterface; + private $destroy = new Subject(); private inputString = ''; private isOpening = false; @@ -199,6 +208,7 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, constructor( public cascaderService: NzCascaderService, + private i18nService: NzI18nService, private cdr: ChangeDetectorRef, elementRef: ElementRef, renderer: Renderer2, @@ -229,6 +239,7 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, if (!data) { this.onChange([]); this.nzSelect.emit(null); + this.nzSelectionChange.emit([]); } else { const { option, index } = data; const shouldClose = option.isLeaf; @@ -246,6 +257,19 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, this.inputString = ''; this.dropdownWidthStyle = ''; }); + + this.i18nService.localeChange + .pipe( + startWith(), + takeUntil(this.$destroy) + ) + .subscribe(() => { + this.setLocale(); + }); + + if (this.nzSelect.observers.length > 0) { + warnDeprecation(`nzSelect is deprecated and will be removed in 9.0.0. Please use 'nzSelectionChange' instead.`); + } } ngOnDestroy(): void { @@ -409,38 +433,44 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, @HostListener('mouseenter') onTriggerMouseEnter(): void { - if (this.nzDisabled) { + if (this.nzDisabled || !this.isActionTrigger('hover')) { return; } - if (this.isActionTrigger('hover')) { - this.delaySetMenuVisible(true, this.nzMouseEnterDelay, true); - } + + this.delaySetMenuVisible(true, this.nzMouseEnterDelay, true); } @HostListener('mouseleave', ['$event']) onTriggerMouseLeave(event: MouseEvent): void { - if (this.nzDisabled) { + if (this.nzDisabled || !this.menuVisible || this.isOpening || !this.isActionTrigger('hover')) { + event.preventDefault(); return; } - if (!this.menuVisible || this.isOpening) { - event.preventDefault(); + const mouseTarget = event.relatedTarget as HTMLElement; + const hostEl = this.el; + const menuEl = this.menu && (this.menu.nativeElement as HTMLElement); + if (hostEl.contains(mouseTarget) || (menuEl && menuEl.contains(mouseTarget))) { return; } - if (this.isActionTrigger('hover')) { - const mouseTarget = event.relatedTarget as HTMLElement; - const hostEl = this.el; - const menuEl = this.menu && (this.menu.nativeElement as HTMLElement); - if (hostEl.contains(mouseTarget) || (menuEl && menuEl.contains(mouseTarget))) { - return; + this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); + } + + onOptionMouseEnter(option: CascaderOption, columnIndex: number, event: Event): void { + event.preventDefault(); + if (this.nzExpandTrigger === 'hover') { + if (!option.isLeaf) { + this.delaySetOptionActivated(option, columnIndex, false); + } else { + this.cascaderService.setOptionDeactivatedSinceColumn(columnIndex); } - this.delaySetMenuVisible(false, this.nzMouseLeaveDelay); } } - private isActionTrigger(action: 'click' | 'hover'): boolean { - return typeof this.nzTriggerAction === 'string' - ? this.nzTriggerAction === action - : this.nzTriggerAction.indexOf(action) !== -1; + onOptionMouseLeave(option: CascaderOption, _columnIndex: number, event: Event): void { + event.preventDefault(); + if (this.nzExpandTrigger === 'hover' && !option.isLeaf) { + this.clearDelaySelectTimer(); + } } onOptionClick(option: CascaderOption, columnIndex: number, event: Event): void { @@ -456,6 +486,12 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, : this.cascaderService.setOptionActivated(option, columnIndex, true); } + private isActionTrigger(action: 'click' | 'hover'): boolean { + return typeof this.nzTriggerAction === 'string' + ? this.nzTriggerAction === action + : this.nzTriggerAction.indexOf(action) !== -1; + } + private onEnter(): void { const columnIndex = Math.max(this.cascaderService.activatedOptions.length - 1, 0); const option = this.cascaderService.activatedOptions[columnIndex]; @@ -511,20 +547,6 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, } } - onOptionMouseEnter(option: CascaderOption, columnIndex: number, event: Event): void { - event.preventDefault(); - if (this.nzExpandTrigger === 'hover' && !option.isLeaf) { - this.delaySelectOption(option, columnIndex, true); - } - } - - onOptionMouseLeave(option: CascaderOption, columnIndex: number, event: Event): void { - event.preventDefault(); - if (this.nzExpandTrigger === 'hover' && !option.isLeaf) { - this.delaySelectOption(option, columnIndex, false); - } - } - private clearDelaySelectTimer(): void { if (this.delaySelectTimer) { clearTimeout(this.delaySelectTimer); @@ -532,14 +554,12 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, } } - private delaySelectOption(option: CascaderOption, index: number, doSelect: boolean): void { + private delaySetOptionActivated(option: CascaderOption, columnIndex: number, performSelect: boolean): void { this.clearDelaySelectTimer(); - if (doSelect) { - this.delaySelectTimer = setTimeout(() => { - this.cascaderService.setOptionActivated(option, index); - this.delaySelectTimer = null; - }, 150); - } + this.delaySelectTimer = setTimeout(() => { + this.cascaderService.setOptionActivated(option, columnIndex, performSelect); + this.delaySelectTimer = null; + }, 150); } private toggleSearchingMode(toSearching: boolean): void { @@ -609,4 +629,9 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit, this.labelRenderText = defaultDisplayRender.call(this, labels, selectedOptions); } } + + private setLocale(): void { + this.locale = this.i18nService.getLocaleData('global'); + this.cdr.markForCheck(); + } } diff --git a/components/cascader/nz-cascader.service.ts b/components/cascader/nz-cascader.service.ts index fcbf0a838e8..7e1d17545fd 100644 --- a/components/cascader/nz-cascader.service.ts +++ b/components/cascader/nz-cascader.service.ts @@ -162,13 +162,13 @@ export class NzCascaderService implements OnDestroy { * Try to set a option as activated. * @param option Cascader option * @param columnIndex Of which column this option is in - * @param select Select + * @param performSelect Select * @param loadingChildren Try to load children asynchronously. */ setOptionActivated( option: CascaderOption, columnIndex: number, - select: boolean = false, + performSelect: boolean = false, loadingChildren: boolean = true ): void { if (option.disabled) { @@ -193,15 +193,35 @@ export class NzCascaderService implements OnDestroy { } // Actually perform selection to make an options not only activated but also selected. - if (select) { + if (performSelect) { this.setOptionSelected(option, columnIndex); } this.$redraw.next(); } + setOptionSelected(option: CascaderOption, index: number): void { + const changeOn = this.cascaderComponent.nzChangeOn; + const shouldPerformSelection = (o: CascaderOption, i: number): boolean => { + return typeof changeOn === 'function' ? changeOn(o, i) : false; + }; + + if (option.isLeaf || this.cascaderComponent.nzChangeOnSelect || shouldPerformSelection(option, index)) { + this.selectedOptions = [...this.activatedOptions]; + this.prepareEmitValue(); + this.$redraw.next(); + this.$optionSelected.next({ option, index }); + } + } + + setOptionDeactivatedSinceColumn(column: number): void { + this.dropBehindActivatedOptions(column - 1); + this.dropBehindColumns(column); + this.$redraw.next(); + } + /** - * Set a searching option as activated, finishing up things. + * Set a searching option as selected, finishing up things. * @param option */ setSearchOptionSelected(option: CascaderSearchOption): void { @@ -305,20 +325,6 @@ export class NzCascaderService implements OnDestroy { } } - setOptionSelected(option: CascaderOption, index: number): void { - const changeOn = this.cascaderComponent.nzChangeOn; - const shouldPerformSelection = (o: CascaderOption, i: number): boolean => { - return typeof changeOn === 'function' ? changeOn(o, i) : false; - }; - - if (option.isLeaf || this.cascaderComponent.nzChangeOnSelect || shouldPerformSelection(option, index)) { - this.selectedOptions = [...this.activatedOptions]; - this.prepareEmitValue(); - this.$redraw.next(); - this.$optionSelected.next({ option, index }); - } - } - /** * Clear selected options. */ diff --git a/components/cascader/nz-cascader.spec.ts b/components/cascader/nz-cascader.spec.ts index b0d855e6510..67e8f61d579 100644 --- a/components/cascader/nz-cascader.spec.ts +++ b/components/cascader/nz-cascader.spec.ts @@ -56,6 +56,7 @@ describe('cascader', () => { overlayContainerElement = oc.getContainerElement(); })(); })); + afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => { currentOverlayContainer.ngOnDestroy(); overlayContainer.ngOnDestroy(); @@ -71,12 +72,14 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.className).toContain('ant-cascader ant-cascader-picker'); }); + it('should have input', () => { fixture.detectChanges(); const input: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-input'); expect(input).toBeDefined(); expect(input.getAttribute('placeholder')).toBe('please select'); }); + it('should input change event stopPropagation', () => { fixture.detectChanges(); const input: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-input'); @@ -86,12 +89,14 @@ describe('cascader', () => { fixture.detectChanges(); expect(fakeInputChangeEvent.stopPropagation).toHaveBeenCalled(); }); + it('should have EMPTY label', () => { fixture.detectChanges(); const label: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-picker-label'); expect(label).toBeDefined(); expect(label.innerText).toBe(''); }); + it('should placeholder work', () => { const placeholder = 'placeholder test'; testComponent.nzPlaceHolder = placeholder; @@ -99,13 +104,7 @@ describe('cascader', () => { const input: HTMLElement = cascader.nativeElement.querySelector('.ant-cascader-input'); expect(input.getAttribute('placeholder')).toBe(placeholder); }); - // This API is redundant and should be removed. - // it('should prefixCls work', () => { - // testComponent.nzPrefixCls = 'new-cascader'; - // fixture.detectChanges(); - // expect(testComponent.cascader.nzPrefixCls).toBe('new-cascader'); - // expect(cascader.nativeElement.className).toContain('new-cascader new-cascader-picker'); - // }); + it('should size work', () => { testComponent.nzSize = 'small'; fixture.detectChanges(); @@ -115,6 +114,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(input.classList).toContain('ant-input-lg'); }); + it('should value and label property work', fakeAsync(() => { testComponent.nzOptions = ID_NAME_LIST; testComponent.nzValueProperty = 'id'; @@ -131,6 +131,7 @@ describe('cascader', () => { ); expect(testComponent.cascader.getSubmitValue().join(',')).toBe('1,2,3'); })); + it('should no value and label property work', fakeAsync(() => { testComponent.nzValueProperty = null; testComponent.nzLabelProperty = null; @@ -146,6 +147,7 @@ describe('cascader', () => { ); expect(testComponent.cascader.getSubmitValue().join(',')).toBe('zhejiang,hangzhou,xihu'); })); + it('should showArrow work', () => { testComponent.nzShowArrow = true; fixture.detectChanges(); @@ -155,6 +157,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('.ant-cascader-picker-arrow')).toBeNull(); }); + it('should allowClear work', () => { fixture.detectChanges(); testComponent.values = ['zhejiang', 'hangzhou', 'xihu']; @@ -164,6 +167,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('.ant-cascader-picker-clear')).toBeNull(); }); + it('should open work', () => { fixture.detectChanges(); expect(cascader.nativeElement.classList).not.toContain('ant-cascader-picker-open'); @@ -173,6 +177,7 @@ describe('cascader', () => { expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); expect(testComponent.cascader.nzOptions).toBe(options1); }); + it('should click toggle open', fakeAsync(() => { fixture.detectChanges(); expect(testComponent.nzDisabled).toBe(false); @@ -193,6 +198,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); + it('should mouse hover toggle open', fakeAsync(() => { fixture.detectChanges(); testComponent.nzTriggerAction = 'hover'; @@ -276,6 +282,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); + it('should mouse hover toggle open immediately', fakeAsync(() => { fixture.detectChanges(); testComponent.nzTriggerAction = ['hover']; @@ -296,6 +303,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(2); })); + it('should clear timer on option mouseenter and mouseleave', fakeAsync(() => { const mouseenter = createMouseEvent('mouseenter'); const mouseleave = createMouseEvent('mouseleave'); @@ -331,6 +339,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(optionEl.classList).toContain('ant-cascader-menu-item-active'); })); + it('should disabled work', fakeAsync(() => { fixture.detectChanges(); expect(cascader.nativeElement.classList).not.toContain('ant-cascader-picker-disabled'); @@ -351,6 +360,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); + it('should disabled state work', fakeAsync(() => { fixture.detectChanges(); expect(cascader.nativeElement.classList).not.toContain('ant-cascader-picker-disabled'); @@ -365,6 +375,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); + it('should disabled mouse hover open', fakeAsync(() => { testComponent.nzTriggerAction = 'hover'; testComponent.nzDisabled = true; @@ -393,6 +404,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(true); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(1); })); + it('should mouse leave not work when menu not open', fakeAsync(() => { testComponent.nzTriggerAction = ['hover']; fixture.detectChanges(); @@ -404,6 +416,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.onVisibleChange).toHaveBeenCalledTimes(0); })); + it('should clear value work', fakeAsync(() => { fixture.detectChanges(); testComponent.nzAllowClear = true; @@ -416,6 +429,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.values!.length).toBe(0); })); + it('should clear value work 2', fakeAsync(() => { fixture.detectChanges(); testComponent.values = ['zhejiang', 'hangzhou', 'xihu']; @@ -427,6 +441,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.values!.length).toBe(0); })); + it('should autofocus work', () => { testComponent.nzShowInput = true; testComponent.nzAutoFocus = true; @@ -436,6 +451,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('input').getAttribute('autofocus')).toBe(null); }); + it('should input focus and blur work', fakeAsync(() => { const fakeInputFocusEvent = createFakeEvent('focus', false, true); const fakeInputBlurEvent = createFakeEvent('blur', false, true); @@ -457,6 +473,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.classList).toContain('ant-cascader-focused'); })); + it('should focus and blur function work', () => { testComponent.nzShowInput = true; cascader.nativeElement.click(); @@ -469,6 +486,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('input') === document.activeElement).toBe(false); }); + it('should focus and blur function work 2', () => { testComponent.nzShowInput = false; cascader.nativeElement.click(); @@ -481,6 +499,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement === document.activeElement).toBe(false); }); + it('should menu class work', fakeAsync(() => { fixture.detectChanges(); cascader.nativeElement.click(); @@ -491,6 +510,7 @@ describe('cascader', () => { expect(overlayContainerElement.querySelector('.ant-cascader-menus')!.classList).toContain('menu-classA'); expect(overlayContainerElement.querySelector('.ant-cascader-menu')!.classList).toContain('column-classA'); })); + it('should menu style work', fakeAsync(() => { fixture.detectChanges(); cascader.nativeElement.click(); @@ -501,6 +521,7 @@ describe('cascader', () => { const targetElement = overlayContainerElement.querySelector('.menu-classA') as HTMLElement; expect(targetElement.style.height).toBe('120px'); })); + it('should show input false work', fakeAsync(() => { testComponent.nzShowInput = false; fixture.detectChanges(); @@ -515,6 +536,7 @@ describe('cascader', () => { expect(cascader.nativeElement.querySelector('.ant-cascader-picker-clear')).toBeNull(); expect(cascader.nativeElement.querySelector('.ant-cascader-picker-label')).toBeNull(); })); + it('should input value work', fakeAsync(() => { fixture.detectChanges(); expect(cascader.nativeElement.classList).not.toContain('ant-cascader-picker-with-value'); @@ -522,6 +544,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(cascader.nativeElement.classList).toContain('ant-cascader-picker-with-value'); })); + it('should create label work', fakeAsync(() => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('.ant-cascader-picker-label').innerText).toBe(''); @@ -533,6 +556,7 @@ describe('cascader', () => { 'Zhejiang / Hangzhou / West Lake' ); })); + it('should label template work', fakeAsync(() => { fixture.detectChanges(); expect(cascader.nativeElement.querySelector('.ant-cascader-picker-label').innerText).toBe(''); @@ -555,6 +579,7 @@ describe('cascader', () => { 'Zhejiang | Hangzhou | West Lake' ); })); + it('should write value work', fakeAsync(() => { const control = testComponent.cascader; testComponent.nzOptions = options1; @@ -620,6 +645,7 @@ describe('cascader', () => { expect(values4[2]).toBe('xihu'); expect(control.labelRenderText).toBe('ZJ / HZ / XH'); })); + it('should write value work on setting `nzOptions` asyn', fakeAsync(() => { const control = testComponent.cascader; testComponent.nzOptions = null; @@ -648,6 +674,7 @@ describe('cascader', () => { expect(control.getSubmitValue()[0]).toBe('zhejiang'); expect(control.labelRenderText).toBe('Zhejiang'); })); + it('should write value work on setting `nzOptions` asyn (match)', fakeAsync(() => { const control = testComponent.cascader; testComponent.nzOptions = null; @@ -665,6 +692,7 @@ describe('cascader', () => { expect(values![2]).toBe('xihu'); expect(control.labelRenderText).toBe('Zhejiang / Hangzhou / West Lake'); })); + it('should write value work on setting `nzOptions` asyn (not match)', fakeAsync(() => { const control = testComponent.cascader; testComponent.nzOptions = null; @@ -682,6 +710,7 @@ describe('cascader', () => { expect(values![2]).toBe('xihu2'); expect(control.labelRenderText).toBe('zhejiang2 / hangzhou2 / xihu2'); })); + it('should click option to expand', () => { fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列:未显示菜单 @@ -698,6 +727,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); // 3列 }); + it('should click option to change column count', () => { fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列:未显示菜单 @@ -724,6 +754,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(2); // 2列 }); + it('should click option to change column count 2', fakeAsync(() => { testComponent.values = ['zhejiang', 'hangzhou', 'xihu']; fixture.detectChanges(); @@ -771,6 +802,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(testComponent.values!.join(',')).toBe('zhejiang,ningbo'); })); + it('should click option to change column count 3', () => { testComponent.nzOptions = options3; fixture.detectChanges(); @@ -799,6 +831,7 @@ describe('cascader', () => { ) as HTMLElement; expect(itemEl21.innerText.trim()).toBe('Nanjing'); }); + it('should click disabled option false to expand', fakeAsync(() => { testComponent.nzOptions = options2; fixture.detectChanges(); @@ -822,6 +855,7 @@ describe('cascader', () => { expect(optionEl1.classList).toContain('ant-cascader-menu-item-active'); expect(optionEl2.classList).not.toContain('ant-cascader-menu-item-active'); })); + it('should click leaf option to close menu', fakeAsync(() => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); @@ -848,6 +882,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); })); + it('should open menu when press DOWN_ARROW', fakeAsync(() => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(false); @@ -857,6 +892,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(true); })); + it('should open menu when press UP_ARROW', fakeAsync(() => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(false); @@ -866,6 +902,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(true); })); + it('should close menu when press ESC', fakeAsync(() => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); @@ -877,6 +914,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(false); })); + it('should navigate up when press UP_ARROW', fakeAsync(() => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); @@ -897,6 +935,7 @@ describe('cascader', () => { expect(itemEl2.classList).toContain('ant-cascader-menu-item-active'); expect(itemEl1.classList).not.toContain('ant-cascader-menu-item-active'); })); + it('should navigate down when press DOWN_ARROW', fakeAsync(() => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); @@ -909,6 +948,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); })); + it('should navigate right when press RIGHT_ARROW', fakeAsync(() => { fixture.detectChanges(); testComponent.cascader.setMenuVisible(true); @@ -940,6 +980,7 @@ describe('cascader', () => { ) as HTMLElement; // The first option in the third column expect(itemEl3.classList).toContain('ant-cascader-menu-item-active'); })); + it('should navigate left when press LEFT_ARROW', fakeAsync(() => { fixture.detectChanges(); testComponent.values = ['zhejiang', 'hangzhou', 'xihu']; @@ -977,6 +1018,7 @@ describe('cascader', () => { expect(itemEl2.classList).not.toContain('ant-cascader-menu-item-active'); expect(itemEl3.classList).not.toContain('ant-cascader-menu-item-active'); })); + it('should select option when press ENTER', fakeAsync(() => { fixture.detectChanges(); expect(testComponent.values).toBeNull(); @@ -1012,6 +1054,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.menuVisible).toBe(false); })); + it('should key nav disabled option correct', fakeAsync(() => { testComponent.nzOptions = options2; fixture.detectChanges(); @@ -1074,6 +1117,7 @@ describe('cascader', () => { expect(optionEl13.classList).not.toContain('ant-cascader-menu-item-active'); expect(optionEl14.classList).not.toContain('ant-cascader-menu-item-active'); })); + it('should ignore keyboardEvent on some key', fakeAsync(() => { const A = 65; const Z = 90; @@ -1093,6 +1137,7 @@ describe('cascader', () => { expect(testComponent.cascader.menuVisible).toBe(false); }); })); + it('should expand option on hover', fakeAsync(() => { testComponent.nzExpandTrigger = 'hover'; fixture.detectChanges(); @@ -1156,7 +1201,7 @@ describe('cascader', () => { expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); // 3列 expect(itemEl1.classList).toContain('ant-cascader-menu-item-active'); expect(itemEl2.classList).toContain('ant-cascader-menu-item-active'); - expect(itemEl3.classList).not.toContain('ant-cascader-menu-item-active'); // not select because it is leaf + expect(itemEl3.classList).not.toContain('ant-cascader-menu-item-active'); expect(testComponent.values).toBeNull(); // not select yet itemEl3.click(); @@ -1173,6 +1218,7 @@ describe('cascader', () => { expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 expect(testComponent.cascader.menuVisible).toBe(false); })); + it('should not expand disabled option on hover', fakeAsync(() => { testComponent.nzExpandTrigger = 'hover'; testComponent.nzOptions = options2; @@ -1204,6 +1250,31 @@ describe('cascader', () => { expect(itemEl2.classList).not.toContain('ant-cascader-menu-item-active'); expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(1); // 1列 })); + + // fix #3914 + it('should drop selected items and columns if a leaf node is hovered', fakeAsync(() => { + testComponent.nzExpandTrigger = 'hover'; + fixture.detectChanges(); + + testComponent.values = ['zhejiang', 'hangzhou', 'xihu']; + testComponent.cascader.setMenuVisible(true); // Open cascader dropdown. + + fixture.detectChanges(); + tick(500); + fixture.detectChanges(); + expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(3); + + const c2i2 = overlayContainerElement.querySelector( + '.ant-cascader-menu:nth-child(2) .ant-cascader-menu-item:nth-child(2)' + ) as HTMLElement; + dispatchMouseEvent(c2i2, 'mouseenter'); + + fixture.detectChanges(); + tick(500); + fixture.detectChanges(); + expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(2); + })); + it('should change on select work', fakeAsync(() => { testComponent.nzChangeOnSelect = true; fixture.detectChanges(); @@ -1268,6 +1339,7 @@ describe('cascader', () => { expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 expect(testComponent.cascader.menuVisible).toBe(false); })); + it('should not change on hover work', fakeAsync(() => { testComponent.nzChangeOnSelect = true; testComponent.nzExpandTrigger = 'hover'; @@ -1339,6 +1411,7 @@ describe('cascader', () => { expect(overlayContainerElement.querySelectorAll('.ant-cascader-menu').length).toBe(0); // 0列 expect(testComponent.cascader.menuVisible).toBe(false); })); + it('should change on function work', fakeAsync(() => { testComponent.nzChangeOn = testComponent.fakeChangeOn; fixture.detectChanges(); @@ -1371,6 +1444,7 @@ describe('cascader', () => { expect(testComponent.values!.length).toBe(1); expect(testComponent.values![0]).toBe('zhejiang'); })); + it('should position change correct', () => { const fakeTopEvent = { connectionPair: { @@ -1399,6 +1473,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(testComponent.cascader.dropDownPosition).toBe('bottom'); }); + it('should support search', fakeAsync(() => { fixture.detectChanges(); testComponent.nzShowSearch = true; @@ -1475,6 +1550,7 @@ describe('cascader', () => { expect(testComponent.cascader.inputValue).toBe(''); expect(testComponent.values!.join(',')).toBe('zhejiang,hangzhou,xihu'); })); + it('should support custom filter', fakeAsync(() => { testComponent.nzShowSearch = { filter(inputValue: string, path: CascaderOption[]): boolean { @@ -1499,6 +1575,7 @@ describe('cascader', () => { expect(testComponent.cascader.inputValue).toBe(''); expect(testComponent.values!.join(',')).toBe('zhejiang,hangzhou,xihu'); })); + it('should support custom sorter', fakeAsync(() => { testComponent.nzShowSearch = { sorter(a: CascaderOption[], b: CascaderOption[], _inputValue: string): number { @@ -1525,6 +1602,7 @@ describe('cascader', () => { expect(testComponent.cascader.inputValue).toBe(''); expect(testComponent.values!.join(',')).toBe('jiangsu,nanjing,zhonghuamen'); })); + it('should forbid disabled search options to be clicked', fakeAsync(() => { testComponent.nzOptions = options4; fixture.detectChanges(); @@ -1544,6 +1622,7 @@ describe('cascader', () => { expect(testComponent.cascader.inputValue).toBe('o'); // expect(testComponent.values).toBe(null); })); + it('should pass disabled property to children when searching', () => { testComponent.nzOptions = options4; fixture.detectChanges(); @@ -1554,6 +1633,7 @@ describe('cascader', () => { expect(testComponent.cascader.cascaderService.columns[0][1].disabled).toBe(undefined); expect(testComponent.cascader.cascaderService.columns[0][2].disabled).toBe(true); }); + it('should support arrow in search mode', done => { testComponent.nzOptions = options2; fixture.detectChanges(); @@ -1580,6 +1660,7 @@ describe('cascader', () => { done(); }); }); + it('should not preventDefault left/right arrow in search mode', () => { fixture.detectChanges(); testComponent.nzShowSearch = true; @@ -1596,6 +1677,7 @@ describe('cascader', () => { fixture.detectChanges(); expect(itemEl1.classList).not.toContain('ant-cascader-menu-item-active'); }); + it('should support search a root node have no children ', fakeAsync(() => { fixture.detectChanges(); testComponent.nzShowSearch = true; @@ -1615,6 +1697,7 @@ describe('cascader', () => { expect(itemEl1.innerText.trim()).toBe('暂无数据'); flush(); })); + it('should re-prepare search results when nzOptions change', () => { fixture.detectChanges(); testComponent.nzShowSearch = true; @@ -1641,6 +1724,7 @@ describe('cascader', () => { let fixture: ComponentFixture; let cascader: DebugElement; let testComponent: NzDemoCascaderLoadDataComponent; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [FormsModule, ReactiveFormsModule, NoopAnimationsModule, NzCascaderModule], @@ -1653,6 +1737,7 @@ describe('cascader', () => { overlayContainerElement = oc.getContainerElement(); })(); })); + afterEach(inject([OverlayContainer], (currentOverlayContainer: OverlayContainer) => { currentOverlayContainer.ngOnDestroy(); overlayContainer.ngOnDestroy(); @@ -2028,6 +2113,7 @@ const options5: any[] = []; // tslint:disable-line:no-any [nzChangeOnSelect]="nzChangeOnSelect" (ngModelChange)="onValueChanges($event)" (nzVisibleChange)="onVisibleChange($event)" + (nzSelect)="onSelect($event)" > @@ -2083,6 +2169,8 @@ export class NzDemoCascaderDefaultComponent { clearSelection(): void { this.cascader.clearSelection(); } + + onSelect(_d: { option: CascaderOption; index: number }): void {} } @Component({ diff --git a/components/i18n/nz-i18n.interface.ts b/components/i18n/nz-i18n.interface.ts index 4897df14baa..38be071f17d 100644 --- a/components/i18n/nz-i18n.interface.ts +++ b/components/i18n/nz-i18n.interface.ts @@ -21,18 +21,13 @@ export interface NzPaginationI18nInterface { next_3: string; } -export interface NzDatePickerI18nInterface { - lang: NzDatePickerLangI18nInterface; - timePickerLocale: NzTimePickerI18nInterface; -} - -export interface NzDatePickerLangI18nInterface extends NzCalendarI18nInterface { +export interface NzGlobalI18nInterface { placeholder: string; - rangePlaceholder: string[]; } -export interface NzTimePickerI18nInterface { - placeholder: string; +export interface NzDatePickerI18nInterface { + lang: NzDatePickerLangI18nInterface; + timePickerLocale: NzTimePickerI18nInterface; } export interface NzCalendarI18nInterface { @@ -64,12 +59,24 @@ export interface NzCalendarI18nInterface { nextCentury: string; } +export interface NzDatePickerLangI18nInterface extends NzCalendarI18nInterface { + placeholder: string; + rangePlaceholder: string[]; +} + +export interface NzTimePickerI18nInterface { + placeholder: string; +} + +export type NzCascaderI18nInterface = NzGlobalI18nInterface; + export interface NzI18nInterface { locale: string; Pagination: NzPaginationI18nInterface; DatePicker: NzDatePickerI18nInterface; TimePicker: NzTimePickerI18nInterface; Calendar: NzCalendarI18nInterface; + global: NzGlobalI18nInterface; Table: { filterTitle: string; filterConfirm: string;