diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 56030cb..8f80ba7 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -213,6 +213,14 @@ drupal.machine-name: - core/drupal - core/drupalSettings +drupal.message: + version: VERSION + js: + misc/message.js: {} + dependencies: + - core/drupal + - core/drupal.debounce + drupal.progress: version: VERSION js: diff --git a/core/lib/Drupal/Core/Render/Element/StatusMessages.php b/core/lib/Drupal/Core/Render/Element/StatusMessages.php index febf018..00f6430 100644 --- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php @@ -104,6 +104,7 @@ public static function renderMessages(array $element, array $context) { // Render the messages. $messages = [ + '#attached' => ['library' => ['core/drupal.message']], '#theme' => 'status_messages', // @todo Improve when https://www.drupal.org/node/2278383 lands. '#message_list' => drupal_get_messages($context['display']), diff --git a/core/misc/announce.js b/core/misc/announce.js index 9aafbbc..4a7f970 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -26,7 +26,7 @@ * to the DOM. */ Drupal.behaviors.drupalAnnounce = { - attach: function (context) { + attach: function () { // Create only one aria-live element. if (!liveElement) { liveElement = document.createElement('div'); @@ -42,39 +42,45 @@ /** * Concatenates announcements to a single string; appends to the live region. */ - function announce() { + function processAnnounce() { var text = []; var priority = 'polite'; var announcement; - // Create an array of announcement strings to be joined and appended to the - // aria live region. - var il = announcements.length; - for (var 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 (announcements.length) { + // Create an array of announcement strings to be joined and appended to the + // aria live region. + var il = announcements.length; + for (var 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'); + 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'); + } } } + // 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. + var debouncedProcessAnnounce = debounce(processAnnounce, 200); + /** * Triggers audio UAs to read the supplied text. * @@ -86,24 +92,23 @@ * These messages are then joined and append to the aria-live region as one * text node. * - * @param String text + * @param {String} text * A string to be read by the UA. - * @param String priority + * @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)()); + if (typeof text === 'string') { + // 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 + }); + debouncedProcessAnnounce(announcements); + } }; }(Drupal, Drupal.debounce)); diff --git a/core/misc/message.js b/core/misc/message.js new file mode 100644 index 0000000..44fb4f2 --- /dev/null +++ b/core/misc/message.js @@ -0,0 +1,82 @@ +/** + * @file + * Message API. + */ +(function (Drupal, debounce) { + + "use strict"; + + var messages = []; + var messagesElement; + + /** + * Builds a div element with the aria-live attribute and attaches it to the DOM. + */ + Drupal.behaviors.drupalMessage = { + attach: function () { + if (!messagesElement) { + messagesElement = document.querySelector('[data-drupal-messages]'); + if (!messagesElement) { + throw new Error(Drupal.t("There is no element with a [data-drupal-messages] attribute")); + } + } + } + }; + + /** + * Displays all queued messages in one pass instead of one after the other. + * @see debouncedProcessMessages + */ + function processMessages () { + var text = []; + var message; + if (messages.length) { + for (var n = 0, nl = messages.length; n < nl; n++) { + message = messages.pop(); + text.unshift(Drupal.theme('message', message)); + } + if (text.length) { + messagesElement.innerHTML += text.join(''); + } + } + } + + // Debounce the function in case Drupal.message() is used in a loop. + var debouncedProcessMessages = debounce(processMessages, 100); + + /** + * Displays a message on the page. + * + * @param {String} message + * The message to display + * @param {String} type + * Message type, can be either 'status', 'error' or 'warning'. + * Default to 'status' + */ + Drupal.message = function (message, type) { + if (typeof message === 'string') { + // Save the text and priority into a closure variable. Multiple simultaneous + // announcements will be concatenated and read in sequence. + messages.push({ + text: message, + type: type || 'status' + }); + debouncedProcessMessages(messages); + } + }; + + /** + * Theme function for a message. + * + * @param {Object} message + * An object with the following keys: + * - {String} text: The message text + * - {String} type: The message type + * @return {String} + * A string representing a DOM fragment. + */ + Drupal.theme.message = function (message) { + return '
' + message.text + '
'; + }; + +}(Drupal, Drupal.debounce)); diff --git a/core/modules/system/templates/status-messages.html.twig b/core/modules/system/templates/status-messages.html.twig index e479b87..b57a85b 100644 --- a/core/modules/system/templates/status-messages.html.twig +++ b/core/modules/system/templates/status-messages.html.twig @@ -25,25 +25,27 @@ * @ingroup themeable */ #} -{% for type, messages in message_list %} -
- {% if type == 'error' %} -
- {% endif %} - {% if status_headings[type] %} -

{{ status_headings[type] }}

+
+ {% for type, messages in message_list %} +
+ {% if type == 'error' %} +
{% endif %} - {% if messages|length > 1 %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% else %} - {{ messages|first }} + {% if status_headings[type] %} +

{{ status_headings[type] }}

+ {% endif %} + {% if messages|length > 1 %} +
    + {% for message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+ {% else %} + {{ messages|first }} + {% endif %} + {% if type == 'error' %} +
{% endif %} - {% if type == 'error' %} -
- {% endif %} -
-{% endfor %} +
+ {% endfor %} +
\ No newline at end of file diff --git a/core/themes/classy/templates/misc/status-messages.html.twig b/core/themes/classy/templates/misc/status-messages.html.twig index a58ed8f..136757c 100644 --- a/core/themes/classy/templates/misc/status-messages.html.twig +++ b/core/themes/classy/templates/misc/status-messages.html.twig @@ -23,6 +23,7 @@ * @see template_preprocess_status_messages() */ #} +
{% block messages %} {% for type, messages in message_list %} {% @@ -55,3 +56,4 @@ {{ attributes.removeClass(classes) }} {% endfor %} {% endblock messages %} +