Skip to content

Commit 5bb6f6d

Browse files
committed
119176: Announce notification content in live region
1 parent 8d93f22 commit 5bb6f6d

3 files changed

Lines changed: 97 additions & 24 deletions

File tree

src/app/shared/notifications/models/notification-options.model.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,25 @@ export interface INotificationOptions {
44
timeOut: number;
55
clickToClose: boolean;
66
animate: NotificationAnimationsType | string;
7+
announceContentInLiveRegion: boolean;
78
}
89

910
export class NotificationOptions implements INotificationOptions {
1011
public timeOut: number;
1112
public clickToClose: boolean;
1213
public animate: any;
14+
public announceContentInLiveRegion: boolean;
1315

14-
constructor(timeOut = 5000,
15-
clickToClose = true,
16-
animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale) {
1716

17+
constructor(
18+
timeOut = 5000,
19+
clickToClose = true,
20+
animate: NotificationAnimationsType | string = NotificationAnimationsType.Scale,
21+
announceContentInLiveRegion: boolean = true,
22+
) {
1823
this.timeOut = timeOut;
1924
this.clickToClose = clickToClose;
2025
this.animate = animate;
26+
this.announceContentInLiveRegion = announceContentInLiveRegion;
2127
}
2228
}

src/app/shared/notifications/notifications-board/notifications-board.component.spec.ts

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
1+
import { ComponentFixture, inject, TestBed, waitForAsync, fakeAsync, flush } from '@angular/core/testing';
22
import { BrowserModule, By } from '@angular/platform-browser';
33
import { ChangeDetectorRef } from '@angular/core';
44

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

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

2124
describe('NotificationsBoardComponent', () => {
2225
let comp: NotificationsBoardComponent;
2326
let fixture: ComponentFixture<NotificationsBoardComponent>;
27+
let liveRegionService: LiveRegionServiceStub;
2428

2529
beforeEach(waitForAsync(() => {
30+
liveRegionService = new LiveRegionServiceStub();
31+
2632
TestBed.configureTestingModule({
2733
imports: [
2834
BrowserModule,
@@ -36,7 +42,9 @@ describe('NotificationsBoardComponent', () => {
3642
declarations: [NotificationsBoardComponent, NotificationComponent], // declare the test component
3743
providers: [
3844
{ provide: NotificationsService, useClass: NotificationsServiceStub },
39-
ChangeDetectorRef]
45+
{ provide: LiveRegionService, useValue: liveRegionService },
46+
ChangeDetectorRef,
47+
]
4048
}).compileComponents(); // compile template and css
4149
}));
4250

@@ -106,5 +114,42 @@ describe('NotificationsBoardComponent', () => {
106114
});
107115
});
108116

117+
describe('add', () => {
118+
beforeEach(() => {
119+
liveRegionService.addMessage.calls.reset();
120+
});
121+
122+
it('should announce content to the live region', fakeAsync(() => {
123+
const notification = new Notification('id', NotificationType.Info, 'title', 'content');
124+
comp.add(notification);
125+
126+
flush();
127+
128+
expect(liveRegionService.addMessage).toHaveBeenCalledWith('content');
129+
}));
130+
131+
it('should not announce anything if there is no content', fakeAsync(() => {
132+
const notification = new Notification('id', NotificationType.Info, 'title');
133+
comp.add(notification);
134+
135+
flush();
136+
137+
expect(liveRegionService.addMessage).not.toHaveBeenCalled();
138+
}));
139+
140+
it('should not announce the content if disabled', fakeAsync(() => {
141+
const options = new NotificationOptions();
142+
options.announceContentInLiveRegion = false;
143+
144+
const notification = new Notification('id', NotificationType.Info, 'title', 'content');
145+
notification.options = options;
146+
comp.add(notification);
147+
148+
flush();
149+
150+
expect(liveRegionService.addMessage).not.toHaveBeenCalled();
151+
}));
152+
});
153+
109154
})
110155
;

src/app/shared/notifications/notifications-board/notifications-board.component.ts

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
} from '@angular/core';
1010

1111
import { select, Store } from '@ngrx/store';
12-
import { BehaviorSubject, Subscription } from 'rxjs';
12+
import { BehaviorSubject, Subscription, of as observableOf } from 'rxjs';
1313
import difference from 'lodash/difference';
1414

1515
import { NotificationsService } from '../notifications.service';
@@ -18,6 +18,9 @@ import { notificationsStateSelector } from '../selectors';
1818
import { INotification } from '../models/notification.model';
1919
import { NotificationsState } from '../notifications.reducers';
2020
import { INotificationBoardOptions } from '../../../../config/notifications-config.interfaces';
21+
import { LiveRegionService } from '../../live-region/live-region.service';
22+
import { hasNoValue, isNotEmptyOperator } from '../../empty.util';
23+
import { take } from 'rxjs/operators';
2124

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

52-
constructor(private service: NotificationsService,
53-
private store: Store<AppState>,
54-
private cdr: ChangeDetectorRef) {
55+
constructor(
56+
private service: NotificationsService,
57+
private store: Store<AppState>,
58+
private cdr: ChangeDetectorRef,
59+
protected liveRegionService: LiveRegionService,
60+
) {
5561
}
5662

5763
ngOnInit(): void {
@@ -85,6 +91,7 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
8591
this.notifications.splice(this.notifications.length - 1, 1);
8692
}
8793
this.notifications.splice(0, 0, item);
94+
this.addContentToLiveRegion(item);
8895
} else {
8996
// Remove the notification from the store
9097
// This notification was in the store, but not in this.notifications
@@ -93,29 +100,44 @@ export class NotificationsBoardComponent implements OnInit, OnDestroy {
93100
}
94101
}
95102

103+
/**
104+
* Adds the content of the notification (if any) to the live region, so it can be announced by screen readers.
105+
*/
106+
private addContentToLiveRegion(item: INotification) {
107+
let content = item.content;
108+
109+
if (!item.options.announceContentInLiveRegion || hasNoValue(content)) {
110+
return;
111+
}
112+
113+
if (typeof content === 'string') {
114+
content = observableOf(content);
115+
}
116+
117+
content.pipe(
118+
isNotEmptyOperator(),
119+
take(1),
120+
).subscribe(contentStr => this.liveRegionService.addMessage(contentStr));
121+
}
122+
123+
/**
124+
* Whether to block the provided item because a duplicate notification with the exact same information already
125+
* exists within the notifications array.
126+
* @param item The item to check
127+
* @return true if the notifications array already contains a notification with the exact same information as the
128+
* provided item. false otherwise.
129+
* @private
130+
*/
96131
private block(item: INotification): boolean {
97132
const toCheck = item.html ? this.checkHtml : this.checkStandard;
133+
98134
this.notifications.forEach((notification) => {
99135
if (toCheck(notification, item)) {
100136
return true;
101137
}
102138
});
103139

104-
if (this.notifications.length > 0) {
105-
this.notifications.forEach((notification) => {
106-
if (toCheck(notification, item)) {
107-
return true;
108-
}
109-
});
110-
}
111-
112-
let comp: INotification;
113-
if (this.notifications.length > 0) {
114-
comp = this.notifications[0];
115-
} else {
116-
return false;
117-
}
118-
return toCheck(comp, item);
140+
return false;
119141
}
120142

121143
private checkStandard(checker: INotification, item: INotification): boolean {

0 commit comments

Comments
 (0)