Skip to content

Commit

Permalink
119176: Announce notification content in live region
Browse files Browse the repository at this point in the history
  • Loading branch information
AAwouters committed Oct 17, 2024
1 parent 8d93f22 commit 5bb6f6d
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ export interface INotificationOptions {
timeOut: number;
clickToClose: boolean;
animate: NotificationAnimationsType | string;
announceContentInLiveRegion: boolean;
}

export class NotificationOptions implements INotificationOptions {
public timeOut: number;
public clickToClose: boolean;
public animate: any;
public announceContentInLiveRegion: boolean;

constructor(timeOut = 5000,
clickToClose = true,
animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale) {

constructor(
timeOut = 5000,
clickToClose = true,
animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale,
announceContentInLiveRegion: boolean = true,
) {
this.timeOut = timeOut;
this.clickToClose = clickToClose;
this.animate = animate;
this.announceContentInLiveRegion = announceContentInLiveRegion;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, inject, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
import { BrowserModule, By } from '@angular/platform-browser';
import { ChangeDetectorRef } from '@angular/core';

Expand All @@ -15,14 +15,20 @@ import uniqueId from 'lodash/uniqueId';
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
import { NotificationsServiceStub } from '../../testing/notifications-service.stub';
import { cold } from 'jasmine-marbles';
import { LiveRegionService } from '../../live-region/live-region.service';
import { LiveRegionServiceStub } from '../../live-region/live-region.service.stub';
import { NotificationOptions } from '../models/notification-options.model';

export const bools = { f: false, t: true };

describe('NotificationsBoardComponent', () => {
let comp: NotificationsBoardComponent;
let fixture: ComponentFixture<NotificationsBoardComponent>;
let liveRegionService: LiveRegionServiceStub;

beforeEach(waitForAsync(() => {
liveRegionService = new LiveRegionServiceStub();

TestBed.configureTestingModule({
imports: [
BrowserModule,
Expand All @@ -36,7 +42,9 @@ describe('NotificationsBoardComponent', () => {
declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component
providers: [
{ provide: NotificationsService, useClass: NotificationsServiceStub },
ChangeDetectorRef]
{ provide: LiveRegionService, useValue: liveRegionService },
ChangeDetectorRef,
]
}).compileComponents(); // compile template and css
}));

Expand Down Expand Up @@ -106,5 +114,42 @@ describe('NotificationsBoardComponent', () => {
});
});

describe('add', () => {
beforeEach(() => {
liveRegionService.addMessage.calls.reset();
});

it('should announce content to the live region', fakeAsync(() => {
const notification = new Notification('id', NotificationType.Info, 'title', 'content');
comp.add(notification);

flush();

expect(liveRegionService.addMessage).toHaveBeenCalledWith('content');
}));

it('should not announce anything if there is no content', fakeAsync(() => {
const notification = new Notification('id', NotificationType.Info, 'title');
comp.add(notification);

flush();

expect(liveRegionService.addMessage).not.toHaveBeenCalled();
}));

it('should not announce the content if disabled', fakeAsync(() => {
const options = new NotificationOptions();
options.announceContentInLiveRegion = false;

const notification = new Notification('id', NotificationType.Info, 'title', 'content');
notification.options = options;
comp.add(notification);

flush();

expect(liveRegionService.addMessage).not.toHaveBeenCalled();
}));
});

})
;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@angular/core';

import { select, Store } from '@ngrx/store';
import { BehaviorSubject, Subscription } from 'rxjs';
import { BehaviorSubject, Subscription, of as observableOf } from 'rxjs';
import difference from 'lodash/difference';

import { NotificationsService } from '../notifications.service';
Expand All @@ -18,6 +18,9 @@ import { notificationsStateSelector } from '../selectors';
import { INotification } from '../models/notification.model';
import { NotificationsState } from '../notifications.reducers';
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
import { LiveRegionService } from '../../live-region/live-region.service';
import { hasNoValue, isNotEmptyOperator } from '../../empty.util';
import { take } from 'rxjs/operators';

@Component({
selector: 'ds-notifications-board',
Expand Down Expand Up @@ -49,9 +52,12 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
*/
public isPaused$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

constructor(private service: NotificationsService,
private store: Store<AppState>,
private cdr: ChangeDetectorRef) {
constructor(
private service: NotificationsService,
private store: Store<AppState>,
private cdr: ChangeDetectorRef,
protected liveRegionService: LiveRegionService,
) {
}

ngOnInit(): void {
Expand Down Expand Up @@ -85,6 +91,7 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
this.notifications.splice(this.notifications.length - 1, 1);
}
this.notifications.splice(0, 0, item);
this.addContentToLiveRegion(item);
} else {
// Remove the notification from the store
// This notification was in the store, but not in this.notifications
Expand All @@ -93,29 +100,44 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
}
}

/**
* Adds the content of the notification (if any) to the live region, so it can be announced by screen readers.
*/
private addContentToLiveRegion(item: INotification) {
let content = item.content;

if (!item.options.announceContentInLiveRegion || hasNoValue(content)) {
return;
}

if (typeof content === 'string') {
content = observableOf(content);
}

content.pipe(
isNotEmptyOperator(),
take(1),
).subscribe(contentStr => this.liveRegionService.addMessage(contentStr));
}

/**
* Whether to block the provided item because a duplicate notification with the exact same information already
* exists within the notifications array.
* @param item The item to check
* @return true if the notifications array already contains a notification with the exact same information as the
* provided item. false otherwise.
* @private
*/
private block(item: INotification): boolean {
const toCheck = item.html ? this.checkHtml : this.checkStandard;

this.notifications.forEach((notification) => {
if (toCheck(notification, item)) {
return true;
}
});

if (this.notifications.length > 0) {
this.notifications.forEach((notification) => {
if (toCheck(notification, item)) {
return true;
}
});
}

let comp: INotification;
if (this.notifications.length > 0) {
comp = this.notifications[0];
} else {
return false;
}
return toCheck(comp, item);
return false;
}

private checkStandard(checker: INotification, item: INotification): boolean {
Expand Down

0 comments on commit 5bb6f6d

Please sign in to comment.