From 080e153a9bfbbda094a0f12a9d3aec7ff5b6d975 Mon Sep 17 00:00:00 2001 From: docwilmot Date: Sat, 6 Mar 2021 23:28:17 -0500 Subject: [PATCH 1/8] Issue #4985: Add an aria announce method --- core/includes/common.inc | 1 + core/misc/announce.js | 77 +++++++++++++++++++++++++++++++ core/misc/tabledrag.js | 6 +++ core/modules/system/system.module | 9 ++++ 4 files changed, 93 insertions(+) create mode 100644 core/misc/announce.js diff --git a/core/includes/common.inc b/core/includes/common.inc index 9d2e2b21498..978a429be92 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5483,6 +5483,7 @@ function backdrop_add_tabledrag($table_id, $action, $relationship, $group, $subg // to ensure that table drag behaviors are registered before any module // uses it. backdrop_add_library('system', 'jquery.cookie'); + backdrop_add_library('system', 'announce'); backdrop_add_js('core/misc/tabledrag.js', array('weight' => -1)); $js_added = TRUE; } diff --git a/core/misc/announce.js b/core/misc/announce.js new file mode 100644 index 00000000000..b102c7696b4 --- /dev/null +++ b/core/misc/announce.js @@ -0,0 +1,77 @@ +/** + * Adds an HTML element and method to trigger audio UAs to read system messages. + * + * Use Backdrop.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. + * Backdrop.announce(Backdrop.t('@count items added to @container', + * {'@count': data.items.length, '@container': data.container.title} + * )); + * }); + */ +(function (document, Backdrop) { + +var liveElement; + +/** + * Builds a div element with the aria-live attribute and attaches it + * to the DOM. + */ +Backdrop.behaviors.backdropAnnounce = { + attach: function (settings, context) { + liveElement = document.createElement('div'); + liveElement.id = 'backdrop-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 Backdrop.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. + * Backdrop.announce(Backdrop.t('@count items added to @container', + * {'@count': data.items.length, '@container': data.container.title} + * )); + * }); + * + * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops + */ +Backdrop.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 = Backdrop.checkPlain(text); + // The live text area is updated. Allow the AT to announce the text. + liveElement.setAttribute('aria-busy', 'false'); + } +}; + +}(document, Backdrop)); + diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index 3d55f69cdc8..eb989e39e9d 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -345,6 +345,7 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true); } + var dragDirection = ''; var keyChange = false; var groupHeight; switch (event.keyCode) { @@ -352,6 +353,7 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { case 63234: // Safari left arrow. keyChange = true; self.rowObject.indent(-1 * self.rtl); + dragDirection = 'left'; break; case 38: // Up arrow. case 63232: // Safari up arrow. @@ -388,6 +390,7 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject.indent(0); window.scrollBy(0, -parseInt(item.offsetHeight, 10)); } + dragDirection = 'up'; handle.trigger('focus'); // Regain focus after the DOM manipulation. } break; @@ -395,6 +398,7 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { case 63235: // Safari right arrow. keyChange = true; self.rowObject.indent(self.rtl); + dragDirection = 'right'; break; case 40: // Down arrow. case 63233: // Safari down arrow. @@ -430,12 +434,14 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject.indent(0); window.scrollBy(0, parseInt(item.offsetHeight, 10)); } + dragDirection = 'down'; handle.trigger('focus'); // Regain focus after the DOM manipulation. } break; } if (self.rowObject && self.rowObject.changed === true) { + Backdrop.announce(Backdrop.t('row dragged ' + dragDirection)); $(item).addClass('drag'); if (self.oldRowElement) { $(self.oldRowElement).removeClass('drag-previous'); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index e776902c6ed..47caea7b0b4 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2030,6 +2030,15 @@ function system_library_info() { ), ); + // Announce. + $libraries['announce'] = array( + 'title' => 'Announce', + 'version' => '1.0.0', + 'js' => array( + 'core/misc/announce.js' => array(), + ), + ); + return $libraries; } From 3a1e9d6d6a322fe0fdea84bbd11d548e639f4652 Mon Sep 17 00:00:00 2001 From: docwilmot Date: Sun, 7 Mar 2021 09:17:05 -0500 Subject: [PATCH 2/8] Prevent duplicate liveElement --- core/misc/announce.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/core/misc/announce.js b/core/misc/announce.js index b102c7696b4..e6fa0822b4b 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -24,12 +24,14 @@ var liveElement; */ Backdrop.behaviors.backdropAnnounce = { attach: function (settings, context) { - liveElement = document.createElement('div'); - liveElement.id = 'backdrop-live-announce'; - liveElement.className = 'element-invisible'; - liveElement.setAttribute('aria-live', 'polite'); - liveElement.setAttribute('aria-busy', 'false'); - document.body.appendChild(liveElement); + if (!liveElement) { + liveElement = document.createElement('div'); + liveElement.id = 'backdrop-live-announce'; + liveElement.className = 'element-invisible'; + liveElement.setAttribute('aria-live', 'polite'); + liveElement.setAttribute('aria-busy', 'false'); + document.body.appendChild(liveElement); + } } }; From 81fdbf2177940f59a9daf89c4ba0311f831fb845 Mon Sep 17 00:00:00 2001 From: docwilmot Date: Mon, 8 Mar 2021 17:16:40 -0500 Subject: [PATCH 3/8] Add debounce --- core/misc/announce.js | 99 ++++++++++++++++++------------------------- core/misc/backdrop.js | 30 +++++++++++++ 2 files changed, 72 insertions(+), 57 deletions(-) diff --git a/core/misc/announce.js b/core/misc/announce.js index e6fa0822b4b..cac4607aaf7 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -14,66 +14,51 @@ * )); * }); */ -(function (document, Backdrop) { +(function (Backdrop, debounce) { + var liveElement; + var announcements = []; + Backdrop.behaviors.backdropAnnounce = { + attach: function attach(context) { + if (!liveElement) { + liveElement = document.createElement('div'); + liveElement.id = 'backdrop-live-announce'; + liveElement.className = 'visually-hidden'; + liveElement.setAttribute('aria-live', 'polite'); + liveElement.setAttribute('aria-busy', 'false'); + document.body.appendChild(liveElement); + } + } + }; -var liveElement; + function announce() { + var text = []; + var priority = 'polite'; + var announcement; + var il = announcements.length; -/** - * Builds a div element with the aria-live attribute and attaches it - * to the DOM. - */ -Backdrop.behaviors.backdropAnnounce = { - attach: function (settings, context) { - if (!liveElement) { - liveElement = document.createElement('div'); - liveElement.id = 'backdrop-live-announce'; - liveElement.className = 'element-invisible'; - liveElement.setAttribute('aria-live', 'polite'); - liveElement.setAttribute('aria-busy', 'false'); - document.body.appendChild(liveElement); + for (var i = 0; i < il; i++) { + announcement = announcements.pop(); + text.unshift(announcement.text); + + if (announcement.priority === 'assertive') { + priority = 'assertive'; + } } - } -}; -/** - * 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 Backdrop.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. - * Backdrop.announce(Backdrop.t('@count items added to @container', - * {'@count': data.items.length, '@container': data.container.title} - * )); - * }); - * - * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops - */ -Backdrop.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 = Backdrop.checkPlain(text); - // The live text area is updated. Allow the AT to announce the text. - liveElement.setAttribute('aria-busy', 'false'); + if (text.length) { + liveElement.innerHTML = ''; + liveElement.setAttribute('aria-busy', 'true'); + liveElement.setAttribute('aria-live', priority); + liveElement.innerHTML = text.join('\n'); + liveElement.setAttribute('aria-busy', 'false'); + } } -}; - -}(document, Backdrop)); + Backdrop.announce = function (text, priority) { + announcements.push({ + text: text, + priority: priority + }); + return debounce(announce, 300)(); + }; +})(Backdrop, Backdrop.debounce); diff --git a/core/misc/backdrop.js b/core/misc/backdrop.js index 495a20d69f4..327679d808c 100644 --- a/core/misc/backdrop.js +++ b/core/misc/backdrop.js @@ -827,4 +827,34 @@ Backdrop.optimizedResize = (function() { } }()); +Backdrop.debounce = function (func, wait, immediate) { + var timeout; + var result; + return function () { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var context = this; + + var later = function later() { + timeout = null; + + if (!immediate) { + result = func.apply(context, args); + } + }; + + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + + if (callNow) { + result = func.apply(context, args); + } + + return result; + }; +}; + })(jQuery); From 23da76c10fcd95e5e453536d46297385daaf667c Mon Sep 17 00:00:00 2001 From: docwilmot Date: Mon, 8 Mar 2021 17:16:57 -0500 Subject: [PATCH 4/8] Remove tabledrag changes --- core/includes/common.inc | 1 - core/misc/tabledrag.js | 6 ------ 2 files changed, 7 deletions(-) diff --git a/core/includes/common.inc b/core/includes/common.inc index 978a429be92..9d2e2b21498 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -5483,7 +5483,6 @@ function backdrop_add_tabledrag($table_id, $action, $relationship, $group, $subg // to ensure that table drag behaviors are registered before any module // uses it. backdrop_add_library('system', 'jquery.cookie'); - backdrop_add_library('system', 'announce'); backdrop_add_js('core/misc/tabledrag.js', array('weight' => -1)); $js_added = TRUE; } diff --git a/core/misc/tabledrag.js b/core/misc/tabledrag.js index eb989e39e9d..3d55f69cdc8 100644 --- a/core/misc/tabledrag.js +++ b/core/misc/tabledrag.js @@ -345,7 +345,6 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject = new self.row(item, 'keyboard', self.indentEnabled, self.maxDepth, true); } - var dragDirection = ''; var keyChange = false; var groupHeight; switch (event.keyCode) { @@ -353,7 +352,6 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { case 63234: // Safari left arrow. keyChange = true; self.rowObject.indent(-1 * self.rtl); - dragDirection = 'left'; break; case 38: // Up arrow. case 63232: // Safari up arrow. @@ -390,7 +388,6 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject.indent(0); window.scrollBy(0, -parseInt(item.offsetHeight, 10)); } - dragDirection = 'up'; handle.trigger('focus'); // Regain focus after the DOM manipulation. } break; @@ -398,7 +395,6 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { case 63235: // Safari right arrow. keyChange = true; self.rowObject.indent(self.rtl); - dragDirection = 'right'; break; case 40: // Down arrow. case 63233: // Safari down arrow. @@ -434,14 +430,12 @@ Backdrop.tableDrag.prototype.makeDraggable = function (item) { self.rowObject.indent(0); window.scrollBy(0, parseInt(item.offsetHeight, 10)); } - dragDirection = 'down'; handle.trigger('focus'); // Regain focus after the DOM manipulation. } break; } if (self.rowObject && self.rowObject.changed === true) { - Backdrop.announce(Backdrop.t('row dragged ' + dragDirection)); $(item).addClass('drag'); if (self.oldRowElement) { $(self.oldRowElement).removeClass('drag-previous'); From 556fddc02e154df45b2e1cd103e48a2e4d76e757 Mon Sep 17 00:00:00 2001 From: docwilmot Date: Wed, 10 Mar 2021 11:52:42 -0500 Subject: [PATCH 5/8] Add weight, version, fix namespacing --- core/misc/announce.js | 8 ++++---- core/modules/system/system.module | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/misc/announce.js b/core/misc/announce.js index cac4607aaf7..b077001977b 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -14,7 +14,7 @@ * )); * }); */ -(function (Backdrop, debounce) { +(function (Backdrop) { var liveElement; var announcements = []; Backdrop.behaviors.backdropAnnounce = { @@ -22,7 +22,7 @@ if (!liveElement) { liveElement = document.createElement('div'); liveElement.id = 'backdrop-live-announce'; - liveElement.className = 'visually-hidden'; + liveElement.className = 'element-invisible'; liveElement.setAttribute('aria-live', 'polite'); liveElement.setAttribute('aria-busy', 'false'); document.body.appendChild(liveElement); @@ -59,6 +59,6 @@ text: text, priority: priority }); - return debounce(announce, 300)(); + return Backdrop.debounce(announce, 300)(); }; -})(Backdrop, Backdrop.debounce); +})(Backdrop); diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 47caea7b0b4..5709a35fe87 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -2033,9 +2033,9 @@ function system_library_info() { // Announce. $libraries['announce'] = array( 'title' => 'Announce', - 'version' => '1.0.0', + 'version' => BACKDROP_VERSION, 'js' => array( - 'core/misc/announce.js' => array(), + 'core/misc/announce.js' => array('group' => JS_DEFAULT), ), ); From 262bba725d01a95be60e6e874c7b786a1a6c945a Mon Sep 17 00:00:00 2001 From: docwilmot Date: Wed, 10 Mar 2021 17:53:39 -0500 Subject: [PATCH 6/8] Rename library --- core/modules/system/system.module | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 5709a35fe87..540ffaa762a 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -1284,6 +1284,16 @@ function system_library_info() { ), // CSS is added conditionally below. ); + + // Announce. + $libraries['backdrop.announce'] = array( + 'title' => 'Announce', + 'version' => BACKDROP_VERSION, + 'js' => array( + 'core/misc/announce.js' => array('group' => JS_DEFAULT), + ), + ); + $libraries['backdrop.menu-toggle'] = array( 'title' => 'Backdrop menu toggle', 'version' => BACKDROP_VERSION, @@ -2030,15 +2040,6 @@ function system_library_info() { ), ); - // Announce. - $libraries['announce'] = array( - 'title' => 'Announce', - 'version' => BACKDROP_VERSION, - 'js' => array( - 'core/misc/announce.js' => array('group' => JS_DEFAULT), - ), - ); - return $libraries; } From c87e9686acbcee9f6d33c424de413be791c81163 Mon Sep 17 00:00:00 2001 From: Nate Lampton Date: Wed, 24 Mar 2021 15:42:37 -0700 Subject: [PATCH 7/8] Adding docblock --- core/misc/announce.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/core/misc/announce.js b/core/misc/announce.js index b077001977b..fe86d23b450 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -54,7 +54,30 @@ } } - Backdrop.announce = function (text, priority) { + /** + * 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 Backdrop.announce() being + * read. By wrapping the call to announce in a debounce function, we allow for + * time for multiple calls to Backdrop.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'. + * + * @return function + * The return of the call to debounce. + * + * @see http://www.w3.org/WAI/PF/aria-practices/#liveprops + * @since 1.18.2 Method added. + */ + Backdrop.announce = function (text, priority) { announcements.push({ text: text, priority: priority From 381c569a6e8c78fe4395a9f827501ae856683bc2 Mon Sep 17 00:00:00 2001 From: Nate Lampton Date: Wed, 24 Mar 2021 15:42:44 -0700 Subject: [PATCH 8/8] Adding docblock --- core/misc/backdrop.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/misc/backdrop.js b/core/misc/backdrop.js index 327679d808c..b5591c6ad89 100644 --- a/core/misc/backdrop.js +++ b/core/misc/backdrop.js @@ -827,6 +827,30 @@ Backdrop.optimizedResize = (function() { } }()); +/** + * Limits the invocations of a function in a given time frame. + * + * This can be useful to respond to an event that fires very frequently, + * such as a "keyup" event while a user is typing in a field. + * + * A common use of debouncing in other systems is window resizing, + * however Backdrop provides the Backdrop.optimizedResize() method, + * which should be used for that purpose. + * + * @param function func + * The function to be invoked. + * @param number wait + * The time period within which the callback function should only be + * invoked once. For example if the wait period is 250ms, then the callback + * will only be called at most 4 times per second. + * @param bool immediate + * Whether we wait at the beginning or end to execute the function. + * + * @return function + * The debounced function. + * + * @since 1.18.2 Method added. + */ Backdrop.debounce = function (func, wait, immediate) { var timeout; var result;