feat: UserNotificationService supports click-to-navigate toasts
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

This commit is contained in:
sdarbinyan
2026-08-15 03:47:46 +04:00
parent 55e4938a57
commit 5ed4936898
4 changed files with 48 additions and 5 deletions

View File

@@ -1,9 +1,14 @@
@if (notifications().length > 0) {
<aside class="floating-notifications" aria-live="polite" aria-atomic="true">
@for (note of notifications(); track note.id) {
<article class="floating-note" [class]="'floating-note floating-note-' + note.type">
<article
class="floating-note"
[class]="'floating-note floating-note-' + note.type"
[class.floating-note-clickable]="!!note.route"
(click)="note.route && navigate(note)"
>
<p>{{ note.message }}</p>
<button type="button" (click)="dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
<button type="button" (click)="$event.stopPropagation(); dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
</article>
}
</aside>

View File

@@ -1,5 +1,6 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { UserNotificationService } from '../../services/user-notification.service';
import { Router } from '@angular/router';
import { UserNotification, UserNotificationService } from '../../services/user-notification.service';
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
@Component({
@@ -12,10 +13,18 @@ import { TranslatePipe } from '../../../../../i18n/translate.pipe';
})
export class FloatingNotificationsComponent {
private readonly notificationsService = inject(UserNotificationService);
private readonly router = inject(Router);
readonly notifications = this.notificationsService.notifications;
dismiss(id: string): void {
this.notificationsService.dismiss(id);
}
navigate(note: UserNotification): void {
if (note.route) {
void this.router.navigate(note.route);
}
this.dismiss(note.id);
}
}

View File

@@ -0,0 +1,26 @@
import { TestBed } from '@angular/core/testing';
import { UserNotificationService } from './user-notification.service';
describe('UserNotificationService', () => {
let service: UserNotificationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UserNotificationService);
});
it('stores the route on the notification when provided', () => {
service.show('New order #1042', 'info', 4000, ['en', 'backoffice', 'orders', 'ord_1']);
const [note] = service.notifications();
expect(note.message).toBe('New order #1042');
expect(note.route).toEqual(['en', 'backoffice', 'orders', 'ord_1']);
});
it('leaves route undefined when not provided', () => {
service.show('Saved');
const [note] = service.notifications();
expect(note.route).toBeUndefined();
});
});

View File

@@ -6,6 +6,8 @@ export interface UserNotification {
id: string;
message: string;
type: UserNotificationType;
/** Route to navigate to when the notification is clicked. Absent means not clickable. */
route?: string[];
}
@Injectable({ providedIn: 'root' })
@@ -14,11 +16,12 @@ export class UserNotificationService {
readonly notifications = this.state.asReadonly();
show(message: string, type: UserNotificationType = 'info', durationMs: number = 2500): void {
show(message: string, type: UserNotificationType = 'info', durationMs: number = 2500, route?: string[]): void {
const next: UserNotification = {
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
message,
type
type,
...(route ? { route } : {}),
};
this.state.update(items => [next, ...items].slice(0, 4));