azure-devops-ui
Version:
React components for building web UI in Azure DevOps
135 lines (134 loc) • 5.13 kB
JavaScript
/**
* Maximum number of messages to have in the containers that announce() uses.
*/
const MaxAnnounceChildren = 1;
/**
* Maximum number of containers for announce() to have per assertiveness level.
*/
const MaxAnnounceContainers = 10;
/**
* Default number of milliseconds to wait before announcing the start of an operation.
*/
const DefaultAnnounceDelay = 1000;
/**
* ID of the container for the announce() containers.
*/
const ParentContainerId = "utils-accessibility-announce";
let nextId = 0;
/**
* Gets the parent container for all the announce containers.
*/
function getAnnounceContainer() {
let container = document.getElementById(ParentContainerId);
if (!container) {
container = document.createElement("div");
container.id = ParentContainerId;
container.classList.add("visually-hidden");
document.body.appendChild(container);
}
return container;
}
/**
* Causes screen readers to read the given message.
* @param message
* @param assertive if true, the screen reader will read the announcement immediately, instead of waiting for "the next graceful opportunity"
*/
export function announce(message, assertive = false, pause = 100) {
if (!message) {
return;
}
const assertiveness = assertive ? "assertive" : "polite";
const parentContainer = getAnnounceContainer();
const containerList = parentContainer.getElementsByClassName(assertiveness);
let container = (containerList.length > 0 ? containerList[containerList.length - 1] : null);
if (!container || container.childElementCount >= MaxAnnounceChildren) {
container = document.createElement("div");
container.id = ParentContainerId + nextId++;
container.setAttribute("aria-live", assertiveness);
container.classList.add(assertiveness);
container.setAttribute("aria-relevant", "additions");
parentContainer.appendChild(container);
// getElementsByClassName() returns a live list so the new container is already in this list
if (containerList.length > MaxAnnounceContainers) {
// remove old containers
parentContainer.removeChild(containerList[0]);
}
window.setTimeout(() => {
// live regions get announced on update not create, so wait a bit and then update
announce(message, assertive);
}, pause);
}
else {
const child = document.createElement("p");
child.textContent = message;
container.appendChild(child);
// toggling the visibility like this seems to help Edge
container.style.visibility = "hidden";
container.style.visibility = "visible";
}
}
/**
* Class for announcing, through a screen reader, when a single operation begins and ends. Supports
* a delay before the starting announcement so that quick operations don't trigger announcements.
*
* To use, create a ProgressAnnouncer, and call completed()
*/
export class ProgressAnnouncer {
constructor(options) {
this._startAnnounced = false;
this._completed = false;
this._options = options;
this._start();
}
/**
* Create a ProgressAnnouncer for a promise that will announce promise start and completion/rejection.
* @param promise
* @param options
*/
static forPromise(promise, options) {
const announcer = new ProgressAnnouncer(options);
promise.then(() => {
announcer.announceCompleted();
}, () => {
announcer.announceError();
});
return announcer;
}
/**
* Call this method when the operation has completed. This will cause the end message to be
* announced if the start message was announced.
*/
announceCompleted() {
if (!this._completed) {
this._completed = true;
if (this._startAnnounced) {
announce(this._options.announceEndMessage);
}
}
}
/**
* Call this method if the operation completes with an error. This will cause the error message
* to be announced regardless of whether or not the start message was announced.
*/
announceError() {
if (!this._completed) {
this._completed = true;
announce(this._options.announceErrorMessage);
}
}
/**
* Call this method to stop any announcements from being made
*/
cancel() {
this._completed = true;
}
_start() {
// this._announceDelay = Utils_Core.delay(this, this._options.announceStartDelay !== undefined ? this._options.announceStartDelay : DefaultAnnounceDelay, () => {
window.setTimeout(() => {
if (!this._completed) {
announce(this._options.announceStartMessage);
}
this._startAnnounced = true;
}, this._options.announceStartDelay !== undefined ? this._options.announceStartDelay : DefaultAnnounceDelay);
}
}