Skip to content

Commit 83a44ba

Browse files
committed
118220: Add live-region service and component
1 parent 404ccd9 commit 83a44ba

11 files changed

Lines changed: 202 additions & 3 deletions

config/config.example.yml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,4 +379,18 @@ vocabularies:
379379
# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query.
380380
comcolSelectionSort:
381381
sortField: 'dc.title'
382-
sortDirection: 'ASC'
382+
sortDirection: 'ASC'
383+
384+
# Live Region configuration
385+
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
386+
# Live regions are perceivable regions of a web page that are typically updated as a
387+
# result of an external event when user focus may be elsewhere.
388+
#
389+
# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful
390+
# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages
391+
# usually contain information about changes on the page that might not be in focus.
392+
liveRegion:
393+
# The duration after which messages disappear from the live region in milliseconds
394+
messageTimeOutDurationMs: 30000
395+
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
396+
isVisible: false
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<div class="live-region" [ngClass]="{'visually-hidden': !isVisible }" aria-live="assertive" role="log" aria-relevant="additions" aria-atomic="true">
2+
<div class="live-region-message" *ngFor="let message of (messages$ | async)">{{ message }}</div>
3+
</div>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
.live-region {
2+
position: fixed;
3+
bottom: 0;
4+
left: 0;
5+
right: 0;
6+
padding-left: 60px;
7+
height: 90px;
8+
line-height: 18px;
9+
color: var(--bs-white);
10+
background-color: var(--bs-dark);
11+
opacity: 0.94;
12+
z-index: var(--ds-live-region-z-index);
13+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { LiveRegionService } from './live-region.service';
3+
import { Observable } from 'rxjs';
4+
5+
@Component({
6+
selector: `ds-live-region`,
7+
templateUrl: './live-region.component.html',
8+
styleUrls: ['./live-region.component.scss'],
9+
})
10+
export class LiveRegionComponent implements OnInit {
11+
12+
protected isVisible: boolean;
13+
14+
protected messages$: Observable<string[]>;
15+
16+
constructor(
17+
protected liveRegionService: LiveRegionService,
18+
) {
19+
}
20+
21+
ngOnInit() {
22+
this.isVisible = this.liveRegionService.getLiveRegionVisibility();
23+
this.messages$ = this.liveRegionService.getMessages$();
24+
}
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { Config } from '../../../config/config.interface';
2+
3+
/**
4+
* Configuration interface used by the LiveRegionService
5+
*/
6+
export class LiveRegionConfig implements Config {
7+
messageTimeOutDurationMs: number;
8+
isVisible: boolean;
9+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Injectable } from '@angular/core';
2+
import { BehaviorSubject } from 'rxjs';
3+
import { environment } from '../../../environments/environment';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class LiveRegionService {
9+
10+
/**
11+
* The duration after which the messages disappear in milliseconds
12+
* @protected
13+
*/
14+
protected messageTimeOutDurationMs: number = environment.liveRegion.messageTimeOutDurationMs;
15+
16+
/**
17+
* Array containing the messages that should be shown in the live region
18+
* @protected
19+
*/
20+
protected messages: string[] = [];
21+
22+
/**
23+
* BehaviorSubject emitting the array with messages every time the array updates
24+
* @protected
25+
*/
26+
protected messages$: BehaviorSubject<string[]> = new BehaviorSubject([]);
27+
28+
/**
29+
* Whether the live region should be visible
30+
* @protected
31+
*/
32+
protected liveRegionIsVisible: boolean = environment.liveRegion.isVisible;
33+
34+
/**
35+
* Returns a copy of the array with the current live region messages
36+
*/
37+
getMessages() {
38+
return [...this.messages];
39+
}
40+
41+
/**
42+
* Returns the BehaviorSubject emitting the array with messages every time the array updates
43+
*/
44+
getMessages$() {
45+
return this.messages$;
46+
}
47+
48+
/**
49+
* Adds a message to the live-region messages array
50+
* @param message
51+
*/
52+
addMessage(message: string) {
53+
this.messages.push(message);
54+
this.emitCurrentMessages();
55+
56+
// Clear the message once the timeOut has passed
57+
setTimeout(() => this.pop(), this.messageTimeOutDurationMs);
58+
}
59+
60+
/**
61+
* Clears the live-region messages array
62+
*/
63+
clear() {
64+
this.messages = [];
65+
this.emitCurrentMessages();
66+
}
67+
68+
/**
69+
* Removes the longest living message from the array.
70+
* @protected
71+
*/
72+
protected pop() {
73+
if (this.messages.length > 0) {
74+
this.messages.shift();
75+
this.emitCurrentMessages();
76+
}
77+
}
78+
79+
/**
80+
* Makes the messages$ BehaviorSubject emit the current messages array
81+
* @protected
82+
*/
83+
protected emitCurrentMessages() {
84+
this.messages$.next(this.getMessages());
85+
}
86+
87+
/**
88+
* Returns a boolean specifying whether the live region should be visible.
89+
* Returns 'true' if the region should be visible and false otherwise.
90+
*/
91+
getLiveRegionVisibility(): boolean {
92+
return this.liveRegionIsVisible;
93+
}
94+
95+
/**
96+
* Sets the visibility of the live region.
97+
* Setting this to true will make the live region visible which is useful for debugging purposes.
98+
* @param isVisible
99+
*/
100+
setLiveRegionVisibility(isVisible: boolean) {
101+
this.liveRegionIsVisible = isVisible;
102+
}
103+
104+
/**
105+
* Gets the current message timeOut duration in milliseconds
106+
*/
107+
getMessageTimeOutMs(): number {
108+
return this.messageTimeOutDurationMs;
109+
}
110+
111+
/**
112+
* Sets the message timeOut duration
113+
* @param timeOutMs the message timeOut duration in milliseconds
114+
*/
115+
setMessageTimeOutMs(timeOutMs: number) {
116+
this.messageTimeOutDurationMs = timeOutMs;
117+
}
118+
}

src/app/shared/shared.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ import {
284284
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
285285
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
286286
import { NgxPaginationModule } from 'ngx-pagination';
287+
import { LiveRegionComponent } from './live-region/live-region.component';
287288

288289
const MODULES = [
289290
CommonModule,
@@ -465,7 +466,8 @@ const ENTRY_COMPONENTS = [
465466
AdvancedClaimedTaskActionRatingComponent,
466467
EpersonGroupListComponent,
467468
EpersonSearchBoxComponent,
468-
GroupSearchBoxComponent
469+
GroupSearchBoxComponent,
470+
LiveRegionComponent,
469471
];
470472

471473
const PROVIDERS = [

src/config/app-config.interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface';
2222
import { MarkdownConfig } from './markdown-config.interface';
2323
import { FilterVocabularyConfig } from './filter-vocabulary-config';
2424
import { DiscoverySortConfig } from './discovery-sort.config';
25+
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
2526

2627
interface AppConfig extends Config {
2728
ui: UIServerConfig;
@@ -48,6 +49,7 @@ interface AppConfig extends Config {
4849
markdown: MarkdownConfig;
4950
vocabularies: FilterVocabularyConfig[];
5051
comcolSelectionSort: DiscoverySortConfig;
52+
liveRegion: LiveRegionConfig;
5153
}
5254

5355
/**

src/config/default-app-config.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface';
2222
import { MarkdownConfig } from './markdown-config.interface';
2323
import { FilterVocabularyConfig } from './filter-vocabulary-config';
2424
import { DiscoverySortConfig } from './discovery-sort.config';
25+
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
2526

2627
export class DefaultAppConfig implements AppConfig {
2728
production = false;
@@ -432,4 +433,10 @@ export class DefaultAppConfig implements AppConfig {
432433
sortField:'dc.title',
433434
sortDirection:'ASC',
434435
};
436+
437+
// Live Region configuration, used by the LiveRegionService
438+
liveRegion: LiveRegionConfig = {
439+
messageTimeOutDurationMs: 30000,
440+
isVisible: false,
441+
};
435442
}

src/environments/environment.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,5 +313,10 @@ export const environment: BuildConfig = {
313313
vocabulary: 'srsc',
314314
enabled: true
315315
}
316-
]
316+
],
317+
318+
liveRegion: {
319+
messageTimeOutDurationMs: 30000,
320+
isVisible: false,
321+
},
317322
};

0 commit comments

Comments
 (0)