diff --git a/components/modal/modal-confirm-container.component.ts b/components/modal/modal-confirm-container.component.ts index dc4b1cb1bb8..ad84f2462d1 100644 --- a/components/modal/modal-confirm-container.component.ts +++ b/components/modal/modal-confirm-container.component.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -import { FocusTrapFactory } from '@angular/cdk/a11y'; +import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; import { OverlayRef } from '@angular/cdk/overlay'; import { CdkPortalOutlet } from '@angular/cdk/portal'; import { DOCUMENT } from '@angular/common'; @@ -17,7 +17,6 @@ import { ElementRef, EventEmitter, Inject, - NgZone, Optional, Output, Renderer2, @@ -117,17 +116,16 @@ export class NzModalConfirmContainerComponent extends BaseModalContainer { constructor( private i18n: NzI18nService, elementRef: ElementRef, - focusTrapFactory: FocusTrapFactory, + focusTrapFactory: ConfigurableFocusTrapFactory, cdr: ChangeDetectorRef, render: Renderer2, - zone: NgZone, overlayRef: OverlayRef, nzConfigService: NzConfigService, public config: ModalOptions, @Optional() @Inject(DOCUMENT) document: NzSafeAny, @Optional() @Inject(ANIMATION_MODULE_TYPE) animationType: string ) { - super(elementRef, focusTrapFactory, cdr, render, zone, overlayRef, nzConfigService, config, document, animationType); + super(elementRef, focusTrapFactory, cdr, render, overlayRef, nzConfigService, config, document, animationType); this.i18n.localeChange.pipe(takeUntil(this.destroy$)).subscribe(() => { this.locale = this.i18n.getLocaleData('Modal'); }); diff --git a/components/modal/modal-container.component.ts b/components/modal/modal-container.component.ts index 196124b8da3..da37175c0ec 100644 --- a/components/modal/modal-container.component.ts +++ b/components/modal/modal-container.component.ts @@ -6,21 +6,11 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ -import { FocusTrapFactory } from '@angular/cdk/a11y'; +import { ConfigurableFocusTrapFactory } from '@angular/cdk/a11y'; import { OverlayRef } from '@angular/cdk/overlay'; import { CdkPortalOutlet } from '@angular/cdk/portal'; import { DOCUMENT } from '@angular/common'; -import { - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - Inject, - NgZone, - Optional, - Renderer2, - ViewChild -} from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Inject, Optional, Renderer2, ViewChild } from '@angular/core'; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations'; import { NzConfigService } from 'ng-zorro-antd/core/config'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; @@ -79,16 +69,15 @@ export class NzModalContainerComponent extends BaseModalContainer { @ViewChild('modalElement', { static: true }) modalElementRef!: ElementRef; constructor( elementRef: ElementRef, - focusTrapFactory: FocusTrapFactory, + focusTrapFactory: ConfigurableFocusTrapFactory, cdr: ChangeDetectorRef, render: Renderer2, - zone: NgZone, overlayRef: OverlayRef, nzConfigService: NzConfigService, public config: ModalOptions, @Optional() @Inject(DOCUMENT) document: NzSafeAny, @Optional() @Inject(ANIMATION_MODULE_TYPE) animationType: string ) { - super(elementRef, focusTrapFactory, cdr, render, zone, overlayRef, nzConfigService, config, document, animationType); + super(elementRef, focusTrapFactory, cdr, render, overlayRef, nzConfigService, config, document, animationType); } } diff --git a/components/modal/modal-container.ts b/components/modal/modal-container.ts index 04680f79fd9..63cc066ab55 100644 --- a/components/modal/modal-container.ts +++ b/components/modal/modal-container.ts @@ -7,10 +7,10 @@ */ import { AnimationEvent } from '@angular/animations'; -import { FocusTrap, FocusTrapFactory } from '@angular/cdk/a11y'; +import { ConfigurableFocusTrapFactory, FocusTrap } from '@angular/cdk/a11y'; import { OverlayRef } from '@angular/cdk/overlay'; import { BasePortalOutlet, CdkPortalOutlet, ComponentPortal, TemplatePortal } from '@angular/cdk/portal'; -import { ChangeDetectorRef, ComponentRef, ElementRef, EmbeddedViewRef, EventEmitter, NgZone, OnDestroy, Renderer2 } from '@angular/core'; +import { ChangeDetectorRef, ComponentRef, ElementRef, EmbeddedViewRef, EventEmitter, OnDestroy, Renderer2 } from '@angular/core'; import { NzConfigService } from 'ng-zorro-antd/core/config'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; import { getElementOffset } from 'ng-zorro-antd/core/util'; @@ -34,7 +34,6 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { containerClick = new EventEmitter(); cancelTriggered = new EventEmitter(); okTriggered = new EventEmitter(); - onDestroy = new EventEmitter(); state: 'void' | 'enter' | 'exit' = 'enter'; document: Document; @@ -60,10 +59,9 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { constructor( protected elementRef: ElementRef, - protected focusTrapFactory: FocusTrapFactory, + protected focusTrapFactory: ConfigurableFocusTrapFactory, public cdr: ChangeDetectorRef, protected render: Renderer2, - protected zone: NgZone, protected overlayRef: OverlayRef, protected nzConfigService: NzConfigService, public config: ModalOptions, @@ -139,6 +137,10 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { } private savePreviouslyFocusedElement(): void { + if (!this.focusTrap) { + this.focusTrap = this.focusTrapFactory.create(this.elementRef.nativeElement); + } + if (this.document) { this.elementFocusedBeforeModalWasOpened = this.document.activeElement as HTMLElement; if (this.elementRef.nativeElement.focus) { @@ -150,10 +152,6 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { private trapFocus(): void { const element = this.elementRef.nativeElement; - if (!this.focusTrap) { - this.focusTrap = this.focusTrapFactory.create(element); - } - if (this.config.nzAutofocus) { this.focusTrap.focusInitialElementWhenReady().then(); } else { @@ -186,56 +184,60 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { if (this.animationDisabled()) { return; } - this.zone.runOutsideAngular(() => { - // Make sure to set the `TransformOrigin` style before set the modelElement's class names - this.setModalTransformOrigin(); - const modalElement = this.modalElementRef.nativeElement; - const backdropElement = this.overlayRef.backdropElement; - this.render.addClass(modalElement, ZOOM_CLASS_NAME_MAP.enter); - this.render.addClass(modalElement, ZOOM_CLASS_NAME_MAP.enterActive); - this.render.addClass(backdropElement, FADE_CLASS_NAME_MAP.enter); - this.render.addClass(backdropElement, FADE_CLASS_NAME_MAP.enterActive); - }); + // Make sure to set the `TransformOrigin` style before set the modelElement's class names + this.setModalTransformOrigin(); + const modalElement = this.modalElementRef.nativeElement; + const backdropElement = this.overlayRef.backdropElement; + modalElement.classList.add(ZOOM_CLASS_NAME_MAP.enter); + modalElement.classList.add(ZOOM_CLASS_NAME_MAP.enterActive); + if (backdropElement) { + backdropElement.classList.add(FADE_CLASS_NAME_MAP.enter); + backdropElement.classList.add(FADE_CLASS_NAME_MAP.enterActive); + } } private setExitAnimationClass(): void { - this.zone.runOutsideAngular(() => { - const modalElement = this.modalElementRef.nativeElement; - const backdropElement = this.overlayRef.backdropElement; + const modalElement = this.modalElementRef.nativeElement; + + modalElement.classList.add(ZOOM_CLASS_NAME_MAP.leave); + modalElement.classList.add(ZOOM_CLASS_NAME_MAP.leaveActive); + + this.setMaskExitAnimationClass(); + } - if (this.animationDisabled()) { + private setMaskExitAnimationClass(force: boolean = false): void { + const backdropElement = this.overlayRef.backdropElement; + if (backdropElement) { + if (this.animationDisabled() || force) { // https://github.com/angular/components/issues/18645 - this.render.removeClass(backdropElement, MODAL_MASK_CLASS_NAME); + backdropElement.classList.remove(MODAL_MASK_CLASS_NAME); return; } - - this.render.addClass(modalElement, ZOOM_CLASS_NAME_MAP.leave); - this.render.addClass(modalElement, ZOOM_CLASS_NAME_MAP.leaveActive); - this.render.addClass(backdropElement, FADE_CLASS_NAME_MAP.leave); - this.render.addClass(backdropElement, FADE_CLASS_NAME_MAP.leaveActive); - }); + backdropElement.classList.add(FADE_CLASS_NAME_MAP.leave); + backdropElement.classList.add(FADE_CLASS_NAME_MAP.leaveActive); + } } private cleanAnimationClass(): void { if (this.animationDisabled()) { return; } - this.zone.runOutsideAngular(() => { - const backdropElement = this.overlayRef.backdropElement; - const modalElement = this.modalElementRef.nativeElement; - this.render.removeClass(modalElement, ZOOM_CLASS_NAME_MAP.enter); - this.render.removeClass(modalElement, ZOOM_CLASS_NAME_MAP.enterActive); - this.render.removeClass(modalElement, ZOOM_CLASS_NAME_MAP.leave); - this.render.removeClass(modalElement, ZOOM_CLASS_NAME_MAP.leaveActive); - this.render.removeClass(backdropElement, FADE_CLASS_NAME_MAP.enter); - this.render.removeClass(backdropElement, FADE_CLASS_NAME_MAP.enterActive); - }); + const backdropElement = this.overlayRef.backdropElement; + const modalElement = this.modalElementRef.nativeElement; + if (backdropElement) { + backdropElement.classList.remove(FADE_CLASS_NAME_MAP.enter); + backdropElement.classList.remove(FADE_CLASS_NAME_MAP.enterActive); + } + modalElement.classList.remove(ZOOM_CLASS_NAME_MAP.enter); + modalElement.classList.remove(ZOOM_CLASS_NAME_MAP.enterActive); + modalElement.classList.remove(ZOOM_CLASS_NAME_MAP.leave); + modalElement.classList.remove(ZOOM_CLASS_NAME_MAP.leaveActive); } - private bindBackdropStyle(): void { - this.zone.runOutsideAngular(() => { + bindBackdropStyle(): void { + const backdropElement = this.overlayRef.backdropElement; + if (backdropElement) { if (this.oldMaskStyle) { - const backdropElement = this.overlayRef.backdropElement; const styles = this.oldMaskStyle as { [key: string]: string }; Object.keys(styles).forEach(key => { this.render.removeStyle(backdropElement, key); @@ -244,14 +246,13 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { } if (typeof this.config.nzMaskStyle === 'object' && Object.keys(this.config.nzMaskStyle).length) { - const backdropElement = this.overlayRef.backdropElement; const styles: { [key: string]: string } = { ...this.config.nzMaskStyle }; Object.keys(styles).forEach(key => { this.render.setStyle(backdropElement, key, styles[key]); }); this.oldMaskStyle = styles; } - }); + } } /** @@ -286,17 +287,16 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { updateMaskClassname(): void { const backdropElement = this.overlayRef.backdropElement; - if (this.showMask) { - this.render.addClass(backdropElement, MODAL_MASK_CLASS_NAME); - } else { - this.render.removeClass(backdropElement, MODAL_MASK_CLASS_NAME); + if (backdropElement) { + if (this.showMask) { + backdropElement.classList.add(MODAL_MASK_CLASS_NAME); + } else { + backdropElement.classList.remove(MODAL_MASK_CLASS_NAME); + } } } onAnimationDone(event: AnimationEvent): void { - if (event.toState === 'void') { - return; - } if (event.toState === 'enter') { this.setContainer(); this.trapFocus(); @@ -324,7 +324,7 @@ export class BaseModalContainer extends BasePortalOutlet implements OnDestroy { } ngOnDestroy(): void { - this.onDestroy.emit(); + this.setMaskExitAnimationClass(true); this.destroy$.next(); this.destroy$.complete(); } diff --git a/components/modal/modal-ref.ts b/components/modal/modal-ref.ts index d7fed8cb6d1..4338bf51cd0 100644 --- a/components/modal/modal-ref.ts +++ b/components/modal/modal-ref.ts @@ -10,7 +10,7 @@ import { OverlayRef } from '@angular/cdk/overlay'; import { EventEmitter } from '@angular/core'; import { NzSafeAny } from 'ng-zorro-antd/core/types'; import { isPromise } from 'ng-zorro-antd/core/util'; -import { merge, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; import { filter, take } from 'rxjs/operators'; import { BaseModalContainer } from './modal-container'; @@ -51,17 +51,14 @@ export class NzModalRef implements NzModalLegacyAP } }); - merge( - containerInstance.onDestroy, - containerInstance.animationStateChanged.pipe( + containerInstance.animationStateChanged + .pipe( filter(event => event.phaseName === 'done' && event.toState === 'exit'), take(1) ) - ) - .pipe(take(1)) .subscribe(() => { clearTimeout(this.closeTimeout); - this.finishDialogClose(); + this._finishDialogClose(); }); containerInstance.containerClick.pipe(take(1)).subscribe(() => { @@ -143,7 +140,7 @@ export class NzModalRef implements NzModalLegacyAP .subscribe(event => { this.overlayRef.detachBackdrop(); this.closeTimeout = setTimeout(() => { - this.finishDialogClose(); + this._finishDialogClose(); }, event.totalTime + 100); }); @@ -153,6 +150,7 @@ export class NzModalRef implements NzModalLegacyAP updateConfig(config: ModalOptions): void { Object.assign(this.config, config); + this.containerInstance.bindBackdropStyle(); this.containerInstance.cdr.markForCheck(); } @@ -199,7 +197,7 @@ export class NzModalRef implements NzModalLegacyAP } } - private finishDialogClose(): void { + _finishDialogClose(): void { this.state = NzModalState.CLOSED; this.overlayRef.dispose(); } diff --git a/components/modal/modal.component.ts b/components/modal/modal.component.ts index e9e2a1c6edf..b035f4fedb3 100644 --- a/components/modal/modal.component.ts +++ b/components/modal/modal.component.ts @@ -15,6 +15,7 @@ import { EventEmitter, Input, OnChanges, + OnDestroy, Output, SimpleChanges, TemplateRef, @@ -41,7 +42,7 @@ import { getConfigFromComponent } from './utils'; template: ` `, changeDetection: ChangeDetectionStrategy.OnPush }) -export class NzModalComponent implements OnChanges, NzModalLegacyAPI { +export class NzModalComponent implements OnChanges, NzModalLegacyAPI, OnDestroy { static ngAcceptInputType_nzMask: BooleanInput; static ngAcceptInputType_nzMaskClosable: BooleanInput; static ngAcceptInputType_nzCloseOnNavigation: BooleanInput; @@ -206,4 +207,8 @@ export class NzModalComponent implements OnChanges } } } + + ngOnDestroy(): void { + this.modalRef?._finishDialogClose(); + } } diff --git a/components/modal/modal.spec.ts b/components/modal/modal.spec.ts index 5f2e1040a26..8d8a96bc5a7 100644 --- a/components/modal/modal.spec.ts +++ b/components/modal/modal.spec.ts @@ -1,5 +1,6 @@ import { ESCAPE } from '@angular/cdk/keycodes'; import { OverlayContainer } from '@angular/cdk/overlay'; +import { ComponentPortal, TemplatePortal } from '@angular/cdk/portal'; import { Location } from '@angular/common'; import { SpyLocation } from '@angular/common/testing'; import { @@ -145,6 +146,27 @@ describe('NzModal', () => { modalRef.close(); }); + it('should be thrown when attaching repeatedly', () => { + const modalRefComponent = modalService.create({ + nzContent: TestWithModalContentComponent, + nzComponentParams: { + value: 'Modal' + } + }); + + expect(() => { + modalRefComponent.containerInstance.attachComponentPortal(new ComponentPortal(TestWithModalContentComponent)); + }).toThrowError('Attempting to attach modal content after content is already attached'); + + const modalRefTemplate = modalService.create({ + nzContent: fixture.componentInstance.templateRef + }); + + expect(() => { + modalRefTemplate.containerInstance.attachTemplatePortal(new TemplatePortal(fixture.componentInstance.templateRef, null!)); + }).toThrowError('Attempting to attach modal content after content is already attached'); + }); + it('should open modal with HTML string', () => { const modalRef = modalService.create({ nzContent: `Hello Modal` @@ -401,9 +423,18 @@ describe('NzModal', () => { fixture.detectChanges(); - expect(modalRef.getBackdropElement()?.classList).not.toContain('ant-modal-mask'); + expect(modalRef.getBackdropElement()?.classList).not.toContain('ant-modal-mask', 'should use global config'); configService.set('modal', { nzMask: true }); + fixture.detectChanges(); + + expect(modalRef.getBackdropElement()?.classList).toContain('ant-modal-mask', 'should add class when global config changed'); + + configService.set('modal', { nzMask: false }); + fixture.detectChanges(); + expect(modalRef.getBackdropElement()?.classList).not.toContain('ant-modal-mask', 'should remove class when global config changed'); + + configService.set('modal', { nzMask: true }); // reset modalRef.close(); fixture.detectChanges(); flush(); @@ -629,6 +660,16 @@ describe('NzModal', () => { expect(modalRef.getBackdropElement()!.style.color).toBe('rgb(0, 0, 0)'); + modalRef.updateConfig({ + nzMaskStyle: { + color: 'rgb(255, 0, 0)' + } + }); + + fixture.detectChanges(); + flushMicrotasks(); + + expect(modalRef.getBackdropElement()!.style.color).toBe('rgb(255, 0, 0)'); modalRef.close(); fixture.detectChanges(); flush();