118220: Add live-region service and component

This commit is contained in:
Andreas Awouters
2024-09-16 15:46:07 +02:00
parent 404ccd9b0e
commit 83a44ba924
11 changed files with 202 additions and 3 deletions

View File

@@ -380,3 +380,17 @@ vocabularies:
comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'
# Live Region configuration
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
# Live regions are perceivable regions of a web page that are typically updated as a
# result of an external event when user focus may be elsewhere.
#
# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful
# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages
# usually contain information about changes on the page that might not be in focus.
liveRegion:
# The duration after which messages disappear from the live region in milliseconds
messageTimeOutDurationMs: 30000
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
isVisible: false

View File

@@ -0,0 +1,3 @@
<div class="live-region" [ngClass]="{'visually-hidden': !isVisible }" aria-live="assertive" role="log" aria-relevant="additions" aria-atomic="true">
<div class="live-region-message" *ngFor="let message of (messages$ | async)">{{ message }}</div>
</div>

View File

@@ -0,0 +1,13 @@
.live-region {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding-left: 60px;
height: 90px;
line-height: 18px;
color: var(--bs-white);
background-color: var(--bs-dark);
opacity: 0.94;
z-index: var(--ds-live-region-z-index);
}

View File

@@ -0,0 +1,25 @@
import { Component, OnInit } from '@angular/core';
import { LiveRegionService } from './live-region.service';
import { Observable } from 'rxjs';
@Component({
selector: `ds-live-region`,
templateUrl: './live-region.component.html',
styleUrls: ['./live-region.component.scss'],
})
export class LiveRegionComponent implements OnInit {
protected isVisible: boolean;
protected messages$: Observable<string[]>;
constructor(
protected liveRegionService: LiveRegionService,
) {
}
ngOnInit() {
this.isVisible = this.liveRegionService.getLiveRegionVisibility();
this.messages$ = this.liveRegionService.getMessages$();
}
}

View File

@@ -0,0 +1,9 @@
import { Config } from '../../../config/config.interface';
/**
* Configuration interface used by the LiveRegionService
*/
export class LiveRegionConfig implements Config {
messageTimeOutDurationMs: number;
isVisible: boolean;
}

View File

@@ -0,0 +1,118 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
})
export class LiveRegionService {
/**
* The duration after which the messages disappear in milliseconds
* @protected
*/
protected messageTimeOutDurationMs: number = environment.liveRegion.messageTimeOutDurationMs;
/**
* Array containing the messages that should be shown in the live region
* @protected
*/
protected messages: string[] = [];
/**
* BehaviorSubject emitting the array with messages every time the array updates
* @protected
*/
protected messages$: BehaviorSubject<string[]> = new BehaviorSubject([]);
/**
* Whether the live region should be visible
* @protected
*/
protected liveRegionIsVisible: boolean = environment.liveRegion.isVisible;
/**
* Returns a copy of the array with the current live region messages
*/
getMessages() {
return [...this.messages];
}
/**
* Returns the BehaviorSubject emitting the array with messages every time the array updates
*/
getMessages$() {
return this.messages$;
}
/**
* Adds a message to the live-region messages array
* @param message
*/
addMessage(message: string) {
this.messages.push(message);
this.emitCurrentMessages();
// Clear the message once the timeOut has passed
setTimeout(() => this.pop(), this.messageTimeOutDurationMs);
}
/**
* Clears the live-region messages array
*/
clear() {
this.messages = [];
this.emitCurrentMessages();
}
/**
* Removes the longest living message from the array.
* @protected
*/
protected pop() {
if (this.messages.length > 0) {
this.messages.shift();
this.emitCurrentMessages();
}
}
/**
* Makes the messages$ BehaviorSubject emit the current messages array
* @protected
*/
protected emitCurrentMessages() {
this.messages$.next(this.getMessages());
}
/**
* Returns a boolean specifying whether the live region should be visible.
* Returns 'true' if the region should be visible and false otherwise.
*/
getLiveRegionVisibility(): boolean {
return this.liveRegionIsVisible;
}
/**
* Sets the visibility of the live region.
* Setting this to true will make the live region visible which is useful for debugging purposes.
* @param isVisible
*/
setLiveRegionVisibility(isVisible: boolean) {
this.liveRegionIsVisible = isVisible;
}
/**
* Gets the current message timeOut duration in milliseconds
*/
getMessageTimeOutMs(): number {
return this.messageTimeOutDurationMs;
}
/**
* Sets the message timeOut duration
* @param timeOutMs the message timeOut duration in milliseconds
*/
setMessageTimeOutMs(timeOutMs: number) {
this.messageTimeOutDurationMs = timeOutMs;
}
}

View File

@@ -284,6 +284,7 @@ import {
} from '../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
import { BitstreamListItemComponent } from './object-list/bitstream-list-item/bitstream-list-item.component';
import { NgxPaginationModule } from 'ngx-pagination';
import { LiveRegionComponent } from './live-region/live-region.component';
const MODULES = [
CommonModule,
@@ -465,7 +466,8 @@ const ENTRY_COMPONENTS = [
AdvancedClaimedTaskActionRatingComponent,
EpersonGroupListComponent,
EpersonSearchBoxComponent,
GroupSearchBoxComponent
GroupSearchBoxComponent,
LiveRegionComponent,
];
const PROVIDERS = [

View File

@@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface';
import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config';
import { DiscoverySortConfig } from './discovery-sort.config';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
interface AppConfig extends Config {
ui: UIServerConfig;
@@ -48,6 +49,7 @@ interface AppConfig extends Config {
markdown: MarkdownConfig;
vocabularies: FilterVocabularyConfig[];
comcolSelectionSort: DiscoverySortConfig;
liveRegion: LiveRegionConfig;
}
/**

View File

@@ -22,6 +22,7 @@ import { HomeConfig } from './homepage-config.interface';
import { MarkdownConfig } from './markdown-config.interface';
import { FilterVocabularyConfig } from './filter-vocabulary-config';
import { DiscoverySortConfig } from './discovery-sort.config';
import { LiveRegionConfig } from '../app/shared/live-region/live-region.config';
export class DefaultAppConfig implements AppConfig {
production = false;
@@ -432,4 +433,10 @@ export class DefaultAppConfig implements AppConfig {
sortField:'dc.title',
sortDirection:'ASC',
};
// Live Region configuration, used by the LiveRegionService
liveRegion: LiveRegionConfig = {
messageTimeOutDurationMs: 30000,
isVisible: false,
};
}

View File

@@ -313,5 +313,10 @@ export const environment: BuildConfig = {
vocabulary: 'srsc',
enabled: true
}
]
],
liveRegion: {
messageTimeOutDurationMs: 30000,
isVisible: false,
},
};

View File

@@ -13,6 +13,7 @@
--ds-login-logo-width:72px;
--ds-submission-header-z-index: 1001;
--ds-submission-footer-z-index: 999;
--ds-live-region-z-index: 1030;
--ds-main-z-index: 1;
--ds-nav-z-index: 10;