Skip to content

Commit

Permalink
fix(module:cascader): fix column is not dropped in hover mode (#3916)
Browse files Browse the repository at this point in the history
* fix: fix column is not dropped in hover mode

* fix: improve code coverage
  • Loading branch information
Wendell authored and simplejason committed Aug 26, 2019
1 parent a3bd531 commit 906849b
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 89 deletions.
2 changes: 1 addition & 1 deletion components/affix/nz-affix.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<div #fixedEl>
<ng-content></ng-content>
</div>
</div>
2 changes: 1 addition & 1 deletion components/cascader/demo/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const otherOptions = [
@Component({
selector: 'nz-demo-cascader-basic',
template: `
<nz-cascader [nzOptions]="nzOptions" [(ngModel)]="values" (ngModelChange)="onChanges($event)"> </nz-cascader>
<nz-cascader [nzOptions]="nzOptions" [(ngModel)]="values" (ngModelChange)="onChanges($event)"></nz-cascader>
&nbsp;
<a href="javascript:;" (click)="changeNzOptions()" class="change-options">
Change Options
Expand Down
5 changes: 2 additions & 3 deletions components/cascader/doc/index.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -54,8 +54,7 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
| `(ngModelChange)` | Emit on values change | `EventEmitter<any[]>` | - |
| `(nzClear)` | Emit on clear values | `EventEmitter<void>` | - |
| `(nzVisibleChange)` | Emit on popup menu visible or hide | `EventEmitter<boolean>` | - |
| `(nzSelect)` | Emit on select | `EventEmitter<{option: any, index: number}>` | - |
| `(nzSelectionChange)` | Emit on selection change | `EventEmitter<any[]>` | - |
| `(nzSelectionChange)` | Emit on values change | `EventEmitter<CascaderOption[]>` | - |

When `nzShowSearch` is an object it should implements `NzShowSearchOptions`

Expand Down
6 changes: 2 additions & 4 deletions components/cascader/doc/index.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand All @@ -53,10 +53,8 @@ import { NzCascaderModule } from 'ng-zorro-antd/cascader';
| `[nzSize]` | 输入框大小,可选 `large` `default` `small` | `'large' \| 'small' \| 'default'` | `'default'` |
| `[nzValueProperty]` | 选项的实际值的属性名 | `string` | `'value'` |
| `(ngModelChange)` | 值发生变化时触发 | `EventEmitter<any[]>` | - |
| `(nzClear)` | 清空值时触发 | `EventEmitter<void>` | - |
| `(nzVisibleChange)` | 菜单浮层的显示/隐藏 | `EventEmitter<boolean>` | - |
| `(nzSelect)` | 选中菜单选项时触发 | `EventEmitter<{option: any, index: number}>` | - |
| `(nzSelectionChange)` | 选中菜单选项时触发 | `EventEmitter<any[]>` |- |
| `(nzSelectionChange)` | 值发生变化时触发 | `EventEmitter<CascaderOption[]>` |- |

`nzShowSearch` 为对象时需遵守 `NzShowSearchOptions` 接口:

Expand Down
2 changes: 1 addition & 1 deletion components/cascader/nz-cascader.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
113 changes: 69 additions & 44 deletions components/cascader/nz-cascader.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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',
Expand Down Expand Up @@ -114,7 +116,7 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit,
@Input() nzNotFoundContent: string | TemplateRef<void>;
@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
Expand All @@ -132,11 +134,16 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit,
this.cascaderService.withOptions(options);
}

@Output() readonly nzVisibleChange = new EventEmitter<boolean>();

@Output() readonly nzSelectionChange = new EventEmitter<CascaderOption[]>();

/**
* @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<void>();
@Output() readonly nzVisibleChange = new EventEmitter<boolean>(); // Not exposed, only for test
@Output() readonly nzChange = new EventEmitter(); // Not exposed, only for test

el: HTMLElement;
dropDownPosition = 'bottom';
Expand All @@ -150,6 +157,8 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit,
dropdownWidthStyle: string;
isFocused = false;

locale: NzCascaderI18nInterface;

private $destroy = new Subject<void>();
private inputString = '';
private isOpening = false;
Expand Down Expand Up @@ -199,6 +208,7 @@ export class NzCascaderComponent implements NzCascaderComponentAsSource, OnInit,

constructor(
public cascaderService: NzCascaderService,
private i18nService: NzI18nService,
private cdr: ChangeDetectorRef,
elementRef: ElementRef,
renderer: Renderer2,
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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];
Expand Down Expand Up @@ -511,35 +547,19 @@ 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);
this.delaySelectTimer = null;
}
}

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 {
Expand Down Expand Up @@ -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();
}
}
42 changes: 24 additions & 18 deletions components/cascader/nz-cascader.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.
*/
Expand Down
Loading

0 comments on commit 906849b

Please sign in to comment.