diff --git a/projects/ngneat/dialog/src/lib/dialog-ref.ts b/projects/ngneat/dialog/src/lib/dialog-ref.ts index ac5ac94..3f661a8 100644 --- a/projects/ngneat/dialog/src/lib/dialog-ref.ts +++ b/projects/ngneat/dialog/src/lib/dialog-ref.ts @@ -24,7 +24,10 @@ export abstract class DialogRef< abstract resetDrag(offset?: DragOffset): void; } +type DialogAnimationState = 'void' | 'enter' | 'exit'; + export class InternalDialogRef extends DialogRef { + _state: DialogAnimationState = 'void'; public backdropClick$: Subject; beforeCloseGuards: GuardFN[] = []; @@ -40,7 +43,11 @@ export class InternalDialogRef extends DialogRef { close(result?: unknown): void { this.canClose(result) .pipe(filter(Boolean)) - .subscribe({ next: () => this.onClose(result) }); + .subscribe({ + next: () => { + this.onClose(result); + }, + }); } beforeClose(guard: GuardFN) { @@ -69,4 +76,8 @@ export class InternalDialogRef extends DialogRef { asDialogRef(): DialogRef { return this; } + + _getAnimationState(): DialogAnimationState { + return this._state; + } } diff --git a/projects/ngneat/dialog/src/lib/dialog.component.scss b/projects/ngneat/dialog/src/lib/dialog.component.scss index b20eb1b..6b383f3 100644 --- a/projects/ngneat/dialog/src/lib/dialog.component.scss +++ b/projects/ngneat/dialog/src/lib/dialog.component.scss @@ -3,16 +3,6 @@ flex-direction: column; overflow: hidden; position: relative; - @keyframes dialog-open { - 0% { - transform: translateX(50px); - } - 100% { - transform: none; - } - } - - animation: dialog-open 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); border-radius: var(--dialog-content-border-radius, 4px); box-sizing: border-box; @@ -49,18 +39,6 @@ &.ngneat-dialog-backdrop-visible { background: var(--dialog-backdrop-bg, rgba(0, 0, 0, 0.32)); } - - animation: dialog-open-backdrop 0.3s; - - @keyframes dialog-open-backdrop { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } - } } .ngneat-drag-marker { diff --git a/projects/ngneat/dialog/src/lib/dialog.component.ts b/projects/ngneat/dialog/src/lib/dialog.component.ts index 5b60492..dfba425 100644 --- a/projects/ngneat/dialog/src/lib/dialog.component.ts +++ b/projects/ngneat/dialog/src/lib/dialog.component.ts @@ -1,5 +1,14 @@ import { CommonModule, DOCUMENT } from '@angular/common'; -import { Component, ElementRef, inject, Inject, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + Component, + ElementRef, + EventEmitter, + inject, + OnDestroy, + OnInit, + ViewChild, + ViewEncapsulation, +} from '@angular/core'; import { fromEvent, merge, Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { InternalDialogRef } from './dialog-ref'; @@ -7,11 +16,60 @@ import { DialogService } from './dialog.service'; import { coerceCssPixelValue } from './dialog.utils'; import { DialogDraggableDirective, DragOffset } from './draggable.directive'; import { DIALOG_CONFIG, NODES_TO_INSERT } from './providers'; +import { DialogConfig } from '@ngneat/dialog'; +import { animate, animateChild, group, keyframes, query, state, style, transition, trigger } from '@angular/animations'; + +export const _defaultParams = { + params: { enterAnimationDuration: '150ms', exitAnimationDuration: '75ms' }, +}; @Component({ selector: 'ngneat-dialog', standalone: true, imports: [DialogDraggableDirective, CommonModule], + animations: [ + trigger('dialogContent', [ + // Note: The `enter` animation transitions to `transform: none`, because for some reason + // specifying the transform explicitly, causes IE both to blur the dialog content and + // decimate the animation performance. Leaving it as `none` solves both issues. + state('void, exit', style({ opacity: 0, transform: 'scale(0.7)' })), + state('enter', style({ transform: 'none' })), + transition( + '* => enter', + group([ + animate( + '0.4s cubic-bezier(0.25, 0.8, 0.25, 1)', + keyframes([style({ opacity: 0, transform: 'translateX(50px)' }), style({ opacity: 1, transform: 'none' })]) + ), + query('@*', animateChild(), { optional: true }), + ]), + _defaultParams + ), + transition( + '* => void, * => exit', + group([ + animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })), + query('@*', animateChild(), { optional: true }), + ]), + _defaultParams + ), + ]), + trigger('dialogContainer', [ + transition( + '* => void, * => exit', + group([ + animate('0.4s cubic-bezier(0.4, 0.0, 0.2, 1)', style({ opacity: 0 })), + query('@*', animateChild(), { optional: true }), + ]), + _defaultParams + ), + ]), + ], + host: { + '[@dialogContainer]': `this.dialogRef._getAnimationState()`, + '(@dialogContainer.start)': '_onAnimationStart($event)', + '(@dialogContainer.done)': '_onAnimationDone($event)', + }, template: `
(); config = inject(DIALOG_CONFIG); dialogRef = inject(InternalDialogRef); @@ -96,6 +158,34 @@ export class DialogComponent implements OnInit, OnDestroy { } ngOnInit() { + const dialogElement = this.dialogElement.nativeElement; + this.evaluateConfigBasedFields(); + + // `dialogElement` is resolved at this point + // And here is where dialog finally will be placed + this.nodes.forEach((node) => dialogElement.appendChild(node)); + this.dialogRef._state = 'enter'; + } + + reset(offset?: DragOffset): void { + if (this.config.draggable) { + this.draggable.reset(offset); + } + } + + closeDialog() { + this.dialogRef.close(); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + + this.dialogRef = null; + this.nodes = null; + } + + private evaluateConfigBasedFields(): void { const backdrop = this.config.backdrop ? this.backdrop.nativeElement : this.document.body; const dialogElement = this.dialogElement.nativeElement; @@ -133,21 +223,19 @@ export class DialogComponent implements OnInit, OnDestroy { } } - reset(offset?: DragOffset): void { - if (this.config.draggable) { - this.draggable.reset(offset); + _onAnimationStart(event): any { + if (event.toState === 'enter') { + this._animationStateChanged.next({ state: 'opening', totalTime: event.totalTime }); + } else if (event.toState === 'exit' || event.toState === 'void') { + this._animationStateChanged.next({ state: 'closing', totalTime: event.totalTime }); } } - closeDialog() { - this.dialogRef.close(); - } - - ngOnDestroy() { - this.destroy$.next(); - this.destroy$.complete(); - - this.dialogRef = null; - this.nodes = null; + _onAnimationDone(event) { + if (event.toState === 'enter') { + // this._openAnimationDone(totalTime); + } else if (event.toState === 'exit') { + this._animationStateChanged.next({ state: 'closed', totalTime: event.totalTime }); + } } } diff --git a/projects/ngneat/dialog/src/lib/dialog.service.ts b/projects/ngneat/dialog/src/lib/dialog.service.ts index fbd706f..75dd38d 100644 --- a/projects/ngneat/dialog/src/lib/dialog.service.ts +++ b/projects/ngneat/dialog/src/lib/dialog.service.ts @@ -10,13 +10,14 @@ import { Type, ViewRef, } from '@angular/core'; -import { BehaviorSubject, startWith, Subject } from 'rxjs'; +import { BehaviorSubject, Subject, take } from 'rxjs'; import { DialogRef, InternalDialogRef } from './dialog-ref'; import { DialogComponent } from './dialog.component'; import { DragOffset } from './draggable.directive'; import { DIALOG_CONFIG, DIALOG_DOCUMENT_REF, GLOBAL_DIALOG_CONFIG, NODES_TO_INSERT } from './providers'; import { AttachOptions, DialogConfig, ExtractData, ExtractResult, GlobalDialogConfig, OpenParams } from './types'; -import { map } from 'rxjs/operators'; +import { provideAnimations } from '@angular/platform-browser/animations'; +import { filter } from 'rxjs/operators'; const OVERFLOW_HIDDEN_CLASS = 'ngneat-dialog-hidden'; @@ -141,33 +142,43 @@ export class DialogService { }; const onClose = (result: unknown) => { - this.globalConfig.onClose?.(); - this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id); - this.hasOpenDialogSub.next(this.hasOpenDialogs()); - - container.removeChild(dialog.location.nativeElement); - this.appRef.detachView(dialog.hostView); - this.appRef.detachView(view); - - dialog.destroy(); - view.destroy(); - - dialogRef.backdropClick$.complete(); - - dialogRef.mutate({ - ref: null, - onClose: null, - afterClosed$: null, - backdropClick$: null, - beforeCloseGuards: null, - onReset: null, - }); - - hooks.after.next(result); - hooks.after.complete(); - if (this.dialogs.length === 0) { - this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS); - } + dialog.instance._animationStateChanged + .pipe( + filter((event) => event.state === 'closed'), + take(1) + ) + .subscribe((event) => { + this.globalConfig.onClose?.(); + + this.dialogs = this.dialogs.filter(({ id }) => dialogRef.id !== id); + this.hasOpenDialogSub.next(this.hasOpenDialogs()); + + container.removeChild(dialog.location.nativeElement); + this.appRef.detachView(dialog.hostView); + this.appRef.detachView(view); + + dialog.destroy(); + view.destroy(); + + dialogRef.backdropClick$.complete(); + + dialogRef.mutate({ + ref: null, + onClose: null, + afterClosed$: null, + backdropClick$: null, + beforeCloseGuards: null, + onReset: null, + }); + + hooks.after.next(result); + hooks.after.complete(); + if (this.dialogs.length === 0) { + this.document.body.classList.remove(OVERFLOW_HIDDEN_CLASS); + } + }); + + dialogRef._state = 'exit'; }; const onReset = (offset?: DragOffset) => { @@ -209,6 +220,7 @@ export class DialogService { provide: DIALOG_CONFIG, useValue: config, }, + provideAnimations(), ], parent: this.injector, }),