From 8b4eac0f3a44739e4a08518460ed14cd8769e48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?"J.=20Rene=CC=81e=20Beach"?= Date: Fri, 19 Apr 2013 14:38:26 -0400 Subject: [PATCH] Issue #1959306 by jessebeach, Wim Leers, nod_, mgifford: Drupal.announce does not handle multiple messages at the same time; Improve the utility so that simultaneously calls are read. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: J. ReneĢe Beach --- core/misc/announce.js | 106 +++++++++++++++++++++++++++++ core/misc/drupal.js | 60 ---------------- core/modules/contextual/contextual.module | 1 + core/modules/overlay/overlay.module | 1 + core/modules/system/system.module | 13 ++++ 5 files changed, 121 insertions(+), 60 deletions(-) create mode 100644 core/misc/announce.js diff --git a/core/misc/announce.js b/core/misc/announce.js new file mode 100644 index 0000000..ed02038 --- /dev/null +++ b/core/misc/announce.js @@ -0,0 +1,106 @@ +/** + * Adds an HTML element and method to trigger audio UAs to read system messages. + * + * Use 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. + * $('#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) { + + var liveElement; + var announcements = []; + + /** + * Builds a div element with the aria-live attribute and attaches it + * to the DOM. + */ + Drupal.behaviors.drupalAnnounce = { + attach: function (context) { + // Create only one aria-live element. + if (!liveElement) { + liveElement = document.createElement('div'); + liveElement.id = 'drupal-live-announce'; + liveElement.className = 'element-invisible'; + 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 () { + var text = []; + var priority = 'polite'; + var announcement; + + // Create an array of announcement strings to be joined and appended to the + // aria live region. + for (var i = 0, il = announcements.length; 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 Drupal.announce() being read. + * By wrapping the call to announce in a debounce function, we allow for + * time for multiple calls to 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 + * A string to indicate the priority of the message. Can be either + * 'polite' or 'assertive'. Polite is the default. + * + * @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: text, + priority: 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)); diff --git a/core/misc/drupal.js b/core/misc/drupal.js index b646e20..627e264 100644 --- a/core/misc/drupal.js +++ b/core/misc/drupal.js @@ -259,66 +259,6 @@ Drupal.t = function (str, args, options) { }; /** - * Adds an HTML element and method to trigger audio UAs to read system messages. - */ -var liveElement; - -/** - * Builds a div element with the aria-live attribute and attaches it - * to the DOM. - */ -Drupal.behaviors.drupalAnnounce = { - attach: function (settings, context) { - liveElement = document.createElement('div'); - liveElement.id = 'drupal-live-announce'; - liveElement.className = 'element-invisible'; - liveElement.setAttribute('aria-live', 'polite'); - liveElement.setAttribute('aria-busy', 'false'); - document.body.appendChild(liveElement); - } -}; - -/** - * Triggers audio UAs to read the supplied text. - * - * @param {String} text - * - A string to be read by the UA. - * - * @param {String} priority - * - A string to indicate the priority of the message. Can be either - * 'polite' or 'assertive'. Polite is the default. - * - * Use 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. - * $('#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} - * )); - * }); - * - * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops - */ -Drupal.announce = function (text, priority) { - if (typeof text === 'string') { - // 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 === 'assertive') ? 'assertive' : 'polite'); - // Print the text to the live region. - liveElement.innerHTML = Drupal.checkPlain(text); - // The live text area is updated. Allow the AT to announce the text. - liveElement.setAttribute('aria-busy', 'false'); - } -}; - -/** * Returns the URL to a Drupal page. */ Drupal.url = function (path) { diff --git a/core/modules/contextual/contextual.module b/core/modules/contextual/contextual.module index f859409..3ceda05 100644 --- a/core/modules/contextual/contextual.module +++ b/core/modules/contextual/contextual.module @@ -110,6 +110,7 @@ function contextual_library_info() { array('system', 'jquery.once'), array('system', 'backbone'), array('system', 'drupal.tabbingmanager'), + array('system', 'drupal.announce'), ), ); diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module index 83ba145..75f3706 100644 --- a/core/modules/overlay/overlay.module +++ b/core/modules/overlay/overlay.module @@ -227,6 +227,7 @@ function overlay_library_info() { array('system', 'drupalSettings'), array('system', 'drupal.displace'), array('system', 'drupal.tabbingmanager'), + array('system', 'drupal.announce'), array('system', 'jquery.ui.core'), array('system', 'jquery.bbq'), ), diff --git a/core/modules/system/system.module b/core/modules/system/system.module index bcc6bee..4110f26 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1250,6 +1250,19 @@ function system_library_info() { ), ); + // Drupal's Screen Reader change announcement utility. + $libraries['drupal.announce'] = array( + 'title' => 'Drupal announce', + 'version' => VERSION, + 'js' => array( + 'core/misc/announce.js' => array('group' => JS_LIBRARY), + ), + 'dependencies' => array( + array('system', 'drupal'), + array('system', 'drupal.debounce'), + ), + ); + // Drupal's batch API. $libraries['drupal.batch'] = array( 'title' => 'Drupal batch API', -- 1.7.10.4