/// /// Copyright 2015-2026 Micro Focus or one of its affiliates. /// /// Licensed under the Apache License, Version 2.0 (the "License"); /// you may not use this file except in compliance with the License. /// You may obtain a copy of the License at /// /// http://www.apache.org/licenses/LICENSE-2.0 /// /// Unless required by applicable law or agreed to in writing, software /// distributed under the License is distributed on an "AS IS" BASIS, /// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. /// See the License for the specific language governing permissions and /// limitations under the License. /// import { animate, state, style, transition, trigger } from '@angular/animations'; import { NgTemplateOutlet, AsyncPipe } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostBinding, inject, Input, OnChanges, OnDestroy, QueryList, Renderer2, ViewChildren, } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { map, takeUntil, withLatestFrom } from 'rxjs/operators'; import { tick } from '../../common/index'; import { NotificationListDirection, NotificationRef, NotificationService, } from './notification.service'; @Component({ selector: 'ux-notification-list', templateUrl: './notification-list.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [ trigger('notificationState', [ state('in', style({ transform: 'translateY(0)', opacity: 0.9 })), transition(':enter', [style({ transform: 'translateY(-50px)', opacity: 0 }), animate(500)]), transition(':leave', [animate(500, style({ transform: 'translateY(50px)', opacity: 0 }))]), ]), ], imports: [NgTemplateOutlet, AsyncPipe], }) export class NotificationListComponent implements AfterViewInit, OnChanges, OnDestroy { private readonly _notificationService = inject(NotificationService); private readonly _changeDetectorRef = inject(ChangeDetectorRef); private readonly _renderer = inject(Renderer2); /** * Sets the order in which notifications are displayed: `above` - newer notifications will appear above older ones. `below` - newer notifications will appear below older ones. */ @Input() set direction(direction: NotificationListDirection) { this._notificationService.direction = direction; } /** Sets the position of the list of notifications within the browser window. */ @Input() @HostBinding('class') position: NotificationListPostion = 'bottom-right'; /** The list of notifications that have not been dismissed */ notifications$: Observable = this._notificationService.notifications$.pipe( map(() => this._notifications) ); /** Store the bottom position of the notification list */ @HostBinding('style.bottom.px') _bottom: number; /** Store a list of all element refs */ @ViewChildren('notification') _elements: QueryList; /** Filter out any hidden notifications */ private get _notifications(): NotificationRef[] { return this._notificationService.notifications.filter(notification => notification.visible); } /** Unsubscribe from all subscriptions on component destroy */ private readonly _onDestroy = new Subject(); ngAfterViewInit(): void { // whenever the notifications change we want to recalculate the positions and height this._elements.changes .pipe( takeUntil(this._onDestroy), tick(), map(changes => changes.toArray() as ElementRef[]), withLatestFrom(this.notifications$) ) .subscribe(([elements, notifications]) => { // Set the `top` style property of each element this.applyElementPositions(elements, notifications); this.updateListPosition(elements, notifications); this._changeDetectorRef.markForCheck(); }); } ngOnChanges(): void { if (this._elements) { this.updateListPosition(this._elements.toArray(), this._notifications); } } ngOnDestroy(): void { this._onDestroy.next(); this._onDestroy.complete(); } private applyElementPositions(elements: ElementRef[], notifications: NotificationRef[]): void { let top = 0; for (let i = 0; i < elements.length; i += 1) { const element = elements[i].nativeElement; const notification = notifications[i]; this._renderer.setStyle(element, 'top', `${top}px`); top = top + this.getNotificationHeightInPixels(notification, elements[i]); } } private updateListPosition(elements: ElementRef[], notifications: NotificationRef[]): void { if (this.position === 'bottom-left' || this.position === 'bottom-right') { this._bottom = notifications.reduce( (totalHeight, notification, index) => totalHeight + this.getNotificationHeightInPixels(notification, elements[index]), 0 ); } else { // In a top position, bottom should be unset. this._bottom = undefined; } } /** Get the height of the notification, including spacing if configured. */ private getNotificationHeightInPixels( notification: NotificationRef, elementRef: ElementRef ): number { if (notification.spacing === undefined) { return this.getElementOuterHeightInPixels(elementRef); } return elementRef.nativeElement.offsetHeight + notification.spacing; } /** Get the total height of the element including margins. */ private getElementOuterHeightInPixels(elementRef: ElementRef): number { const element = elementRef.nativeElement; const { marginTop, marginBottom } = getComputedStyle(element); return element.offsetHeight + parseInt(marginTop) + parseInt(marginBottom); } } export type NotificationListPostion = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';