--- /dev/null
+/**
+ * @file
+ * Adds an HTML element and method to trigger audio UAs to read system messages.
+ *
+ * Use {@link Drupal.announce} to indicate to screen reader users that an
+ * element on the page has changed state. For instance, if clicking a link
+ * loads 10 more items into a list, one might announce the change like this.
+ *
+ * @example
+ * $('#search-list')
+ * .on('itemInsert', function (event, data) {
+ * // Insert the new items.
+ * $(data.container.el).append(data.items.el);
+ * // Announce the change to the page contents.
+ * Drupal.announce(Drupal.t('@count items added to @container',
+ * {'@count': data.items.length, '@container': data.container.title}
+ * ));
+ * });
+ */
+
+(function (Drupal, debounce) {
+ let liveElement;
+ const announcements = [];
+
+ /**
+ * Builds a div element with the aria-live attribute and add it to the DOM.
+ *
+ * @type {Drupal~behavior}
+ *
+ * @prop {Drupal~behaviorAttach} attach
+ * Attaches the behavior for drupalAnnouce.
+ */
+ Drupal.behaviors.drupalAnnounce = {
+ attach(context) {
+ // Create only one aria-live element.
+ if (!liveElement) {
+ liveElement = document.createElement('div');
+ liveElement.id = 'drupal-live-announce';
+ liveElement.className = 'visually-hidden';
+ liveElement.setAttribute('aria-live', 'polite');
+ liveElement.setAttribute('aria-busy', 'false');
+ document.body.appendChild(liveElement);
+ }
+ },
+ };
+
+ /**
+ * Concatenates announcements to a single string; appends to the live region.
+ */
+ function announce() {
+ const text = [];
+ let priority = 'polite';
+ let announcement;
+
+ // Create an array of announcement strings to be joined and appended to the
+ // aria live region.
+ const il = announcements.length;
+ for (let i = 0; i < il; i++) {
+ announcement = announcements.pop();
+ text.unshift(announcement.text);
+ // If any of the announcements has a priority of assertive then the group
+ // of joined announcements will have this priority.
+ if (announcement.priority === 'assertive') {
+ priority = 'assertive';
+ }
+ }
+
+ if (text.length) {
+ // Clear the liveElement so that repeated strings will be read.
+ liveElement.innerHTML = '';
+ // Set the busy state to true until the node changes are complete.
+ liveElement.setAttribute('aria-busy', 'true');
+ // Set the priority to assertive, or default to polite.
+ liveElement.setAttribute('aria-live', priority);
+ // Print the text to the live region. Text should be run through
+ // Drupal.t() before being passed to Drupal.announce().
+ liveElement.innerHTML = text.join('\n');
+ // The live text area is updated. Allow the AT to announce the text.
+ liveElement.setAttribute('aria-busy', 'false');
+ }
+ }
+
+ /**
+ * Triggers audio UAs to read the supplied text.
+ *
+ * The aria-live region will only read the text that currently populates its
+ * text node. Replacing text quickly in rapid calls to announce results in
+ * only the text from the most recent call to {@link Drupal.announce} being
+ * read. By wrapping the call to announce in a debounce function, we allow for
+ * time for multiple calls to {@link Drupal.announce} to queue up their
+ * messages. These messages are then joined and append to the aria-live region
+ * as one text node.
+ *
+ * @param {string} text
+ * A string to be read by the UA.
+ * @param {string} [priority='polite']
+ * A string to indicate the priority of the message. Can be either
+ * 'polite' or 'assertive'.
+ *
+ * @return {function}
+ * The return of the call to debounce.
+ *
+ * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops
+ */
+ Drupal.announce = function (text, priority) {
+ // Save the text and priority into a closure variable. Multiple simultaneous
+ // announcements will be concatenated and read in sequence.
+ announcements.push({
+ text,
+ priority,
+ });
+ // Immediately invoke the function that debounce returns. 200 ms is right at
+ // the cusp where humans notice a pause, so we will wait
+ // at most this much time before the set of queued announcements is read.
+ return (debounce(announce, 200)());
+ };
+}(Drupal, Drupal.debounce));