Skip to content
Merged
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type NotificationOptions = {
button?: ButtonOptions;
link?: LinkOptions;
hasTimer?: boolean;
onClose?: (() => void) | null;
};

export type InlineNotificationOptions = {
Expand All @@ -39,7 +40,9 @@ export interface NotificationElement extends HTMLElement {
timeoutID: ReturnType<typeof setTimeout> | null;
elapsed: number;
startedAt: number;
remaining: number;
anchorElement: HTMLElement;
onClose?: Function | null;
Comment thread
manuelsanchez2 marked this conversation as resolved.
Outdated
}

export type NotificationService = {
Expand Down
54 changes: 29 additions & 25 deletions src/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,15 @@ export class Notification {
button,
link,
hasTimer,
onClose,
}: NotificationOptions): void {
if (group) {
let stack = this.notificationStacks.get(position);
if (stack) {
const notificationToRemove = [...stack]
.reverse()
.find(
item =>
item.group === group &&
!item.classList.contains('z-notification--leaving'),
);
if (notificationToRemove) {
this.removeNotification(notificationToRemove);
}
}
const notificationsToRemove = this.notificationStacks
.get(position)
?.filter(item => item.group === group);
notificationsToRemove?.forEach(notification => {
this.removeNotification(notification, { shouldReflow: false });
});
}

const notification = this.createNotification({
Expand All @@ -74,6 +68,7 @@ export class Notification {
button,
link,
hasTimer,
onClose,
});

this.insertNotification(notification);
Expand Down Expand Up @@ -170,16 +165,19 @@ export class Notification {
element: HTMLElement,
group: string | null,
position: NotificationPosition,
onClose: Function | null,
): NotificationElement {
const el = document.createElement('div') as unknown as NotificationElement;
el.setAttribute('popover', 'manual');
el.group = group;
el.hasTimer = false;
el.onClose = onClose;
el.isPaused = false;
el.position = position;
el.timeoutID = null;
el.elapsed = 0;
el.startedAt = 0;
el.remaining = this.notificationTimeout;
el.anchorElement = element;
return el;
}
Expand All @@ -194,8 +192,9 @@ export class Notification {
button,
link,
hasTimer,
onClose = null,
}: NotificationOptions): NotificationElement {
const notification = this.createNotificationElement(element, group, position);
const notification = this.createNotificationElement(element, group, position, onClose);
notification.className = `z-notification z-notification--${position} z-notification--${status}`;

const buttonClass = 'z-notification__action-btn';
Expand Down Expand Up @@ -228,6 +227,7 @@ export class Notification {
);
closeButton.onclick = () => {
this.setFocus(notification.anchorElement);
notification.remaining = 0;
this.removeNotification(notification);
};
}
Expand All @@ -252,12 +252,8 @@ export class Notification {
addNotificationToStack(notification: NotificationElement): void {
const stack = this.getStack(notification.position);
stack.push(notification);

const activeNotifications = stack.filter(
item => !item.classList.contains('z-notification--leaving'),
);
if (activeNotifications.length > MAX_NOTIFICATIONS_PER_POSITION) {
this.removeNotification(activeNotifications[0], { shouldReflow: false });
if (stack.length > MAX_NOTIFICATIONS_PER_POSITION) {
this.removeNotification(stack[0], { shouldReflow: false });
}
}

Expand Down Expand Up @@ -375,6 +371,7 @@ export class Notification {
notification.startedAt = Date.now();
notification.timeoutID = setTimeout(() => {
if (!notification.isPaused) {
notification.remaining = 0;
this.setFocus(notification.anchorElement);
this.removeNotification(notification);
}
Expand All @@ -400,13 +397,13 @@ export class Notification {
const resume = () => {
notification.isPaused = false;
notification.startedAt = Date.now();
const remaining = this.notificationTimeout - notification.elapsed;
if (remaining <= 0) {
notification.remaining = this.notificationTimeout - notification.elapsed;
if (notification.remaining <= 0) {
this.setFocus(notification.anchorElement);
this.removeNotification(notification);
return;
}
this.startTimeout(notification, remaining);
this.startTimeout(notification, notification.remaining);
if (ring) {
ring.style.animationPlayState = 'running';
}
Expand All @@ -425,8 +422,13 @@ export class Notification {
if (notification.timeoutID) {
clearTimeout(notification.timeoutID);
}

this.dispatchEvent('notification-removed', notification.anchorElement);
// when a grouped notification is removed, it can have some time remaining
// this happens, when the next notification of the same group appears.
// onClose won't get called then.
if (notification.remaining <= 0 && notification.onClose) {
console.log('Notification: close callback', notification, notification.onClose);
notification.onClose();
}
this.finishRemovingNotification(notification, { shouldReflow });
}

Expand Down Expand Up @@ -507,6 +509,7 @@ const notification: NotificationService = {
button,
link,
hasTimer,
onClose,
}: NotificationOptions): void {
this.notification.show({
group,
Expand All @@ -518,6 +521,7 @@ const notification: NotificationService = {
button,
link,
hasTimer,
onClose,
});
},
debug(): void {
Expand Down
96 changes: 96 additions & 0 deletions src/tests/notification.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,4 +674,100 @@
expect(screen.getByText('Foo notification')).not.toBeNull();
expect(screen.getByText('Share notification')).not.toBeNull();
});

it('dispatches notification-removed event when timer expires', async () => {
const trigger = document.createElement('button');
trigger.textContent = 'Trigger';
document.body.append(trigger);

const eventHandler = vi.fn();
document.addEventListener('notification-removed', eventHandler);

notification.show({
element: trigger,
message: 'Dispatch event after timer ended',
status: 'info',
hasTimer: true,
});

await vi.advanceTimersByTimeAsync(notification.notificationTimeout);

expect(eventHandler).toHaveBeenCalledTimes(1);

Check failure on line 695 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches notification-removed event when timer expires

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:695:24

Check failure on line 695 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches notification-removed event when timer expires

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:695:24
expect(eventHandler.mock.calls[0][0].detail.originator).toBe(trigger);

document.removeEventListener('notification-removed', eventHandler);
});

it('dispatches notification-removed event when closed manually', async () => {
const trigger = document.createElement('button');
trigger.textContent = 'Trigger';
document.body.append(trigger);

const user = userEvent.setup({
advanceTimers: vi.advanceTimersByTime,
});

const eventHandler = vi.fn();
document.addEventListener('notification-removed', eventHandler);

notification.show({
element: trigger,
message: 'Dispatch Event manual with close button',
status: 'info',
hasTimer: true,
});

await user.click(screen.getByRole('button', { name: 'Meldung schließen', hidden: true }));

expect(eventHandler).toHaveBeenCalledTimes(1);

Check failure on line 722 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches notification-removed event when closed manually

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:722:24

Check failure on line 722 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches notification-removed event when closed manually

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:722:24
expect(eventHandler.mock.calls[0][0].detail.originator).toBe(trigger);

document.removeEventListener('notification-removed', eventHandler);
});

it('dispatches events only for notifications whose timer expires, not for replaced ones', async () => {
const trigger1 = document.createElement('button');
const trigger2 = document.createElement('button');
trigger1.textContent = 'Trigger 1';
trigger2.textContent = 'Trigger 2';
document.body.append(trigger1, trigger2);

const eventHandler = vi.fn();
document.addEventListener('notification-removed', eventHandler);

// First notification with timer
notification.show({
element: trigger1,
group: 'test-group',
message: 'First notification',
status: 'info',
hasTimer: true,
});

// Advance time partially (not enough to trigger timeout)
await vi.advanceTimersByTimeAsync(1000);

// Second notification replaces the first one (same group)
notification.show({
element: trigger2,
group: 'test-group',
message: 'Second notification',
status: 'info',
hasTimer: true,
});

// First notification was replaced, no event should have been dispatched yet
expect(eventHandler).not.toHaveBeenCalled();
expect(screen.queryByText('First notification')).toBeNull();
expect(screen.getByText('Second notification')).not.toBeNull();

// Wait for second notification's timer to expire
await vi.advanceTimersByTimeAsync(notification.notificationTimeout);

// Only one event should be dispatched (from second notification)
expect(eventHandler).toHaveBeenCalledTimes(1);

Check failure on line 768 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches events only for notifications whose timer expires, not for replaced ones

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:768:24

Check failure on line 768 in src/tests/notification.test.ts

View workflow job for this annotation

GitHub Actions / test

src/tests/notification.test.ts > notification accessibility behavior > dispatches events only for notifications whose timer expires, not for replaced ones

AssertionError: expected "vi.fn()" to be called 1 times, but got 0 times ❯ src/tests/notification.test.ts:768:24
expect(eventHandler.mock.calls[0][0].detail.originator).toBe(trigger2);

document.removeEventListener('notification-removed', eventHandler);
});
});
Loading