UNPKG

azure-devops-ui

Version:

React components for building web UI in Azure DevOps

135 lines (134 loc) 5.13 kB
/** * 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); } }