Issue #1959306 by jessebeach: Drupal.announce does not handle multiple messages at the same time; Improve the utility so that simultaneously calls are read.

8.0.x
webchick 2013-04-19 20:43:48 -07:00
parent ea38078c81
commit 7981d5d07b
5 changed files with 121 additions and 60 deletions

106
core/misc/announce.js Normal file
View File

@ -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));

View File

@ -258,66 +258,6 @@ Drupal.t = function (str, args, options) {
return str;
};
/**
* 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.
*/

View File

@ -110,6 +110,7 @@ function contextual_library_info() {
array('system', 'jquery.once'),
array('system', 'backbone'),
array('system', 'drupal.tabbingmanager'),
array('system', 'drupal.announce'),
),
);

View File

@ -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'),
),

View File

@ -1252,6 +1252,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',