diff --git a/components/mention/mention.component.ts b/components/mention/mention.component.ts index 737fb79656..b894ecb0cf 100644 --- a/components/mention/mention.component.ts +++ b/components/mention/mention.component.ts @@ -15,6 +15,7 @@ import { import { TemplatePortal } from '@angular/cdk/portal'; import { DOCUMENT } from '@angular/common'; import { + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -23,6 +24,7 @@ import { EventEmitter, Inject, Input, + NgZone, OnChanges, OnDestroy, OnInit, @@ -35,7 +37,8 @@ import { ViewChildren, ViewContainerRef } from '@angular/core'; -import { fromEvent, merge, Subscription } from 'rxjs'; +import { fromEvent, merge, Observable, Subscription } from 'rxjs'; +import { startWith, switchMap } from 'rxjs/operators'; import { DEFAULT_MENTION_BOTTOM_POSITIONS, DEFAULT_MENTION_TOP_POSITIONS } from 'ng-zorro-antd/core/overlay'; import { BooleanInput, NzSafeAny } from 'ng-zorro-antd/core/types'; @@ -71,7 +74,6 @@ export type MentionPlacement = 'top' | 'bottom'; class="ant-mention-dropdown-item" *ngFor="let suggestion of filteredSuggestions; let i = index" [class.focus]="i === activeIndex" - (mousedown)="$event.preventDefault()" (click)="selectSuggestion(suggestion)" > @@ -90,7 +92,7 @@ export type MentionPlacement = 'top' | 'bottom'; changeDetection: ChangeDetectionStrategy.OnPush, providers: [NzMentionService] }) -export class NzMentionComponent implements OnDestroy, OnInit, OnChanges { +export class NzMentionComponent implements OnDestroy, OnInit, AfterViewInit, OnChanges { static ngAcceptInputType_nzLoading: BooleanInput; @Input() nzValueWith: (value: NzSafeAny) => string = value => value; @@ -141,6 +143,7 @@ export class NzMentionComponent implements OnDestroy, OnInit, OnChanges { } constructor( + private ngZone: NgZone, @Optional() @Inject(DOCUMENT) private ngDocument: NzSafeAny, private cdr: ChangeDetectorRef, private overlay: Overlay, @@ -167,6 +170,27 @@ export class NzMentionComponent implements OnDestroy, OnInit, OnChanges { } } + ngAfterViewInit(): void { + this.items.changes + .pipe( + startWith(this.items), + switchMap(() => { + const items = this.items.toArray(); + // Caretaker note: we explicitly should call `subscribe()` within the root zone. + // `runOutsideAngular(() => fromEvent(...))` will just create an observable within the root zone, + // but `addEventListener` is called when the `fromEvent` is subscribed. + return new Observable(subscriber => + this.ngZone.runOutsideAngular(() => + merge(...items.map(item => fromEvent(item.nativeElement, 'mousedown'))).subscribe(subscriber) + ) + ); + }) + ) + .subscribe(event => { + event.preventDefault(); + }); + } + ngOnDestroy(): void { this.closeDropdown(); } diff --git a/components/mention/nz-mention.spec.ts b/components/mention/nz-mention.spec.ts index c47195886d..44fb6fec94 100644 --- a/components/mention/nz-mention.spec.ts +++ b/components/mention/nz-mention.spec.ts @@ -2,7 +2,7 @@ import { Directionality } from '@angular/cdk/bidi'; import { DOWN_ARROW, ENTER, ESCAPE, RIGHT_ARROW, TAB, UP_ARROW } from '@angular/cdk/keycodes'; import { OverlayContainer } from '@angular/cdk/overlay'; import { ScrollDispatcher } from '@angular/cdk/scrolling'; -import { Component, NgZone, ViewChild } from '@angular/core'; +import { ApplicationRef, Component, NgZone, ViewChild } from '@angular/core'; import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { By } from '@angular/platform-browser'; @@ -157,6 +157,26 @@ describe('mention', () => { expect(textarea.value).toEqual('@angular '); })); + it('should prevent default on the mousedown event when an option is clicked and should not run change detection', fakeAsync(() => { + textarea.value = '@a'; + fixture.detectChanges(); + dispatchFakeEvent(textarea, 'click'); + fixture.detectChanges(); + flush(); + + const appRef = TestBed.inject(ApplicationRef); + const option = overlayContainerElement.querySelector('.ant-mention-dropdown-item') as HTMLElement; + const event = new MouseEvent('mousedown'); + + spyOn(appRef, 'tick'); + spyOn(event, 'preventDefault').and.callThrough(); + + option.dispatchEvent(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(appRef.tick).not.toHaveBeenCalled(); + })); + it('should support switch trigger', fakeAsync(() => { fixture.componentInstance.inputTrigger = true; fixture.detectChanges();