diff --git a/CHANGELOG.md b/CHANGELOG.md index e4df4cc..937fe84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [3.0.2-beta.1](https://github.com/ngxpert/hot-toast/compare/v3.0.1...v3.0.2-beta.1) (2024-10-05) + + +### Bug Fixes + +* add exit animation once enter animation is done ([48c7600](https://github.com/ngxpert/hot-toast/commit/48c7600e6c1fe496bc74a16b900e7e35365bbf92)), closes [#21](https://github.com/ngxpert/hot-toast/issues/21) + ## [3.0.1](https://github.com/ngxpert/hot-toast/compare/v3.0.0...v3.0.1) (2024-08-25) diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts index 743b2b8..45e9414 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-container/hot-toast-container.component.ts @@ -192,14 +192,17 @@ export class HotToastContainerComponent { const comp = this.hotToastComponentList.find((item) => item.toast.id === id); if (comp) { comp.close(); + this.cdr.markForCheck(); } } else { this.hotToastComponentList.forEach((comp) => comp.close()); + this.cdr.markForCheck(); } } beforeClosed(toast: Toast) { toast.visible = false; + this.cdr.markForCheck(); } afterClosed(closeToast: HotToastClose) { @@ -243,5 +246,6 @@ export class HotToastContainerComponent { private updateToasts(toast: Toast, options?: UpdateToastOptions) { this.toasts = this.toasts.map((t) => ({ ...t, ...(t.id === toast.id && { ...toast, ...options }) })); + this.cdr.markForCheck(); } } diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html index 691a5ec..24954d2 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast-group-item/hot-toast-group-item.component.html @@ -9,7 +9,7 @@
; + private _toast: Toast; + @Input() + set toast(value: Toast) { + this._toast = value; + const top = value.position.includes('top'); + const enterAnimation = `hotToastEnterAnimation${ + top ? 'Negative' : 'Positive' + } ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; + + this.toastBarBaseStylesSignal.set({ ...value.style, animation: enterAnimation }); + } + get toast() { + return this._toast; + } @Input() offset = 0; @Input() defaultConfig: ToastConfig; @Input() toastRef: CreateHotToastRef; @@ -55,6 +74,7 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI isManualClose = false; context: Record; toastComponentInjector: Injector; + toastBarBaseStylesSignal = signal({}); private unlisteners: VoidFunction[] = []; protected softClosed = false; @@ -63,6 +83,7 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI protected injector: Injector, protected renderer: Renderer2, protected ngZone: NgZone, + private cdr: ChangeDetectorRef ) {} get toastBarBaseHeight() { @@ -111,20 +132,6 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI }; } - get toastBarBaseStyles() { - const enterAnimation = `hotToastEnterAnimation${ - this.top ? 'Negative' : 'Positive' - } ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; - - const exitAnimation = `hotToastExitAnimation${ - this.top ? 'Negative' : 'Positive' - } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; - - const animation = this.toast.autoClose ? `${enterAnimation}, ${exitAnimation}` : enterAnimation; - - return { ...this.toast.style, animation }; - } - get isIconString() { return typeof this.toast.icon === 'string'; } @@ -171,16 +178,8 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI parent: this.toast.injector || this.injector, }); } - } - ngAfterViewInit() { const nativeElement = this.toastBarBase.nativeElement; - // Caretaker note: accessing `offsetHeight` triggers the whole layout update. - // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. - requestAnimationFrame(() => { - this.height.emit(nativeElement.offsetHeight); - }); - // Caretaker note: `animationstart` and `animationend` events are event tasks that trigger change detection. // We'd want to trigger the change detection only if it's an exit animation. this.ngZone.runOutsideAngular(() => { @@ -190,16 +189,39 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI // with callback that capture `this`. this.renderer.listen(nativeElement, 'animationstart', (event: AnimationEvent) => { if (this.isExitAnimation(event)) { - this.ngZone.run(() => this.beforeClosed.emit()); + this.ngZone.run(() => { + this.renderer.setStyle(nativeElement, 'pointer-events', 'none'); + this.renderer.setStyle(nativeElement.parentElement, 'pointer-events', 'none'); + this.beforeClosed.emit(); + }); } }), this.renderer.listen(nativeElement, 'animationend', (event: AnimationEvent) => { + if (this.isEnterAnimation(event)) { + this.ngZone.run(() => { + if (this.toast.autoClose) { + const exitAnimation = `hotToastExitAnimation${ + this.top ? 'Negative' : 'Positive' + } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; + this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); + } + }); + } if (this.isExitAnimation(event)) { this.ngZone.run(() => this.afterClosed.emit({ dismissedByAction: this.isManualClose, id: this.toast.id })); } }) ); }); + } + + ngAfterViewInit() { + const nativeElement = this.toastBarBase.nativeElement; + // Caretaker note: accessing `offsetHeight` triggers the whole layout update. + // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. + requestAnimationFrame(() => { + this.height.emit(nativeElement.offsetHeight); + }); this.setToastAttributes(); } @@ -227,14 +249,12 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI close() { this.isManualClose = true; + this.cdr.markForCheck(); const exitAnimation = `hotToastExitAnimation${ this.top ? 'Negative' : 'Positive' } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; - - const nativeElement = this.toastBarBase.nativeElement; - - animate(this.renderer, nativeElement, exitAnimation); + this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } handleMouseEnter() { @@ -255,6 +275,10 @@ export class HotToastGroupItemComponent implements OnChanges, OnInit, AfterViewI return ev.animationName.includes('hotToastExitAnimation'); } + private isEnterAnimation(ev: AnimationEvent) { + return ev.animationName.includes('hotToastEnterAnimation'); + } + private setToastAttributes() { const toastAttributes: Record = this.toast.attributes; for (const [key, value] of Object.entries(toastAttributes)) { diff --git a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html index de4e389..df8911b 100644 --- a/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html +++ b/projects/ngxpert/hot-toast/src/lib/components/hot-toast/hot-toast.component.html @@ -14,7 +14,7 @@
; + private _toast: Toast; + @Input() + set toast(value: Toast) { + this._toast = value; + const top = value.position.includes('top'); + const enterAnimation = `hotToastEnterAnimation${ + top ? 'Negative' : 'Positive' + } ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; + + this.toastBarBaseStylesSignal.set({ ...value.style, animation: enterAnimation }); + } + get toast() { + return this._toast; + } @Input() offset = 0; @Input() defaultConfig: ToastConfig; @Input() toastRef: CreateHotToastRef; @@ -77,6 +91,7 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh context: Record; toastComponentInjector: Injector; isExpanded = false; + toastBarBaseStylesSignal = signal({}); private unlisteners: VoidFunction[] = []; private softClosed = false; @@ -135,20 +150,6 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh }; } - get toastBarBaseStyles() { - const enterAnimation = `hotToastEnterAnimation${ - this.top ? 'Negative' : 'Positive' - } ${ENTER_ANIMATION_DURATION}ms cubic-bezier(0.21, 1.02, 0.73, 1) forwards`; - - const exitAnimation = `hotToastExitAnimation${ - this.top ? 'Negative' : 'Positive' - } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; - - const animation = this.toast.autoClose ? `${enterAnimation}, ${exitAnimation}` : enterAnimation; - - return { ...this.toast.style, animation }; - } - get isIconString() { return typeof this.toast.icon === 'string'; } @@ -214,16 +215,8 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh parent: this.toast.injector || this.injector, }); } - } - ngAfterViewInit() { const nativeElement = this.toastBarBase.nativeElement; - // Caretaker note: accessing `offsetHeight` triggers the whole layout update. - // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. - requestAnimationFrame(() => { - this.height.emit(nativeElement.offsetHeight); - }); - // Caretaker note: `animationstart` and `animationend` events are event tasks that trigger change detection. // We'd want to trigger the change detection only if it's an exit animation. this.ngZone.runOutsideAngular(() => { @@ -233,16 +226,39 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh // with callback that capture `this`. this.renderer.listen(nativeElement, 'animationstart', (event: AnimationEvent) => { if (this.isExitAnimation(event)) { - this.ngZone.run(() => this.beforeClosed.emit()); + this.ngZone.run(() => { + this.renderer.setStyle(nativeElement, 'pointer-events', 'none'); + this.renderer.setStyle(nativeElement.parentElement, 'pointer-events', 'none'); + this.beforeClosed.emit(); + }); } }), this.renderer.listen(nativeElement, 'animationend', (event: AnimationEvent) => { + if (this.isEnterAnimation(event)) { + this.ngZone.run(() => { + if (this.toast.autoClose) { + const exitAnimation = `hotToastExitAnimation${ + this.top ? 'Negative' : 'Positive' + } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1) var(--hot-toast-exit-animation-delay) var(--hot-toast-exit-animation-state)`; + this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); + } + }); + } if (this.isExitAnimation(event)) { this.ngZone.run(() => this.afterClosed.emit({ dismissedByAction: this.isManualClose, id: this.toast.id })); } }) ); }); + } + + ngAfterViewInit() { + const nativeElement = this.toastBarBase.nativeElement; + // Caretaker note: accessing `offsetHeight` triggers the whole layout update. + // Macro tasks (like `setTimeout`) might be executed within the current rendering frame and cause a frame drop. + requestAnimationFrame(() => { + this.height.emit(nativeElement.offsetHeight); + }); this.setToastAttributes(); } @@ -275,14 +291,13 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh close() { this.isManualClose = true; + this.cdr.markForCheck(); const exitAnimation = `hotToastExitAnimation${ this.top ? 'Negative' : 'Positive' } ${EXIT_ANIMATION_DURATION}ms forwards cubic-bezier(0.06, 0.71, 0.55, 1)`; - const nativeElement = this.toastBarBase.nativeElement; - - animate(this.renderer, nativeElement, exitAnimation); + this.toastBarBaseStylesSignal.set({ ...this.toast.style, animation: exitAnimation }); } handleMouseEnter() { @@ -303,6 +318,10 @@ export class HotToastComponent implements OnInit, AfterViewInit, OnDestroy, OnCh return ev.animationName.includes('hotToastExitAnimation'); } + private isEnterAnimation(ev: AnimationEvent) { + return ev.animationName.includes('hotToastEnterAnimation'); + } + private setToastAttributes() { const toastAttributes: Record = this.toast.attributes; for (const [key, value] of Object.entries(toastAttributes)) {