diff -u b/core/misc/message.js b/core/misc/message.js --- b/core/misc/message.js +++ b/core/misc/message.js @@ -7,6 +7,11 @@ 'use strict'; var defaultMessageWrapperSelector = '[data-drupal-messages]'; + var messagesTypes = { + status: Drupal.t('Status message'), + error: Drupal.t('Error message'), + warning: Drupal.t('Warning message') + }; /** * @typedef {object} Drupal.message~messageDefinition @@ -50,6 +55,15 @@ * Message type, can be either 'status', 'error' or 'warning'. * @param {object} [options] * The context of the message, used for removing messages again. + * @param {string} [options.index] + * The message index, it can be a simple value: `'filevalidationerror'` + * or several values separated by a space: `'mymodule formvalidation'` + * which can be used as a sort of tag for message deletion. + * @param {string} [options.announce] + * Screen-reader version of the message if necessary. To prevent a message + * being sent to Drupal.announce() this should be `''`. + * @param {string} [options.priority] + * Priority of the message for Drupal.announce(). * * @return {string} * Index of message. @@ -64,47 +78,87 @@ announce(message, type, options); // Use the provided index for the message or generate a unique key to // allow message deletion. - options.index = options.index || Math.random().toFixed(15).replace('0.', ''); - messageWrapper.innerHTML += Drupal.theme('message', {text: message, type: type}, options); + options.index = type + ' ' + (options.index || Math.random().toFixed(15).replace('0.', '')); + messageWrapper.appendChild(Drupal.theme('message', {text: message, type: type}, options)); return options.index; } /** - * Removes messages from the message area. + * Select a set of messages based on type or index. * - * @name Drupal.message~messageDefinition.remove + * @name Drupal.message~messageDefinition.select * * @param {string|Array.} index - * Index of the message to remove, as returned by - * {@link Drupal.message~messageDefinition.add}, or an - * array of indexes. + * The message index or type to delete from the area. * - * @return {number} - * Number of removed messages. + * @return {NodeList|Array} + * Elements found. */ - function messageRemove(index) { - // If there is no argument or if the argument is an empty array, - // no message can be removed. + function messageSelect(index) { + var selectors; + + // When there are nothing to select, return an empty list. if (!index || (Array.isArray(index) && index.length === 0)) { - return 0; + return []; } - var removeSelectors = (Array.isArray(index) ? index : [index]) - .map(function (messageIndex) { - return '[data-drupal-message="' + messageIndex + '"]'; - }); + var whitespace = /\s+/; + var indexes = Array.isArray(index) ? index : [index]; + selectors = indexes.map(function (currentIndex) { + // If the index has spaces, add several data matches, the same way + // class names work. + return currentIndex.trim() + .split(whitespace) + .map(function (i) { + return '[data-drupal-message~="' + i + '"]'; + }) + .join(''); + }); - var toRemove = messageWrapper.querySelectorAll(removeSelectors.join(',')); - var length = toRemove.length; - for (var i = 0; i < length; i++) { - messageWrapper.removeChild(toRemove[i]); + return messageWrapper.querySelectorAll(selectors.join(',')); + } + + /** + * Helper to remove elements. + * + * @param {NodeList|Array.} elements + * DOM Nodes to be removed. + * + * @return {number} + * Number of removed nodes. + */ + function removeElements(elements) { + if (!elements || !elements.length) { + return 0; } + var length = elements.length; + for (var i = 0; i < length; i++) { + messageWrapper.removeChild(elements[i]); + } return length; } /** + * Removes messages from the message area. + * + * @name Drupal.message~messageDefinition.remove + * + * @param {string|Array.} index + * Index of the message to remove, as returned by + * {@link Drupal.message~messageDefinition.add}, or an + * array of indexes. + * + * @return {number} + * Number of removed messages. + */ + function messageRemove(options) { + var messages = messageSelect(options); + return removeElements(messages); + } + + /** * Removes all messages from the message area. * * @name Drupal.message~messageDefinition.clear @@ -116,22 +170,13 @@ * Number of removed messages. */ function messageClear(type) { - var messageSelector = '[data-drupal-message]'; - if (type) { - messageSelector += '[class~="messages--' + type + '"]'; - } - - var toRemove = messageWrapper.querySelectorAll(messageSelector); - var length = toRemove.length; - for (var i = 0; i < length; i++) { - messageWrapper.removeChild(toRemove[i]); - } - - return length; + var messages = messageWrapper.querySelectorAll('[data-drupal-message]'); + return removeElements(messages); } return { add: messageAdd, + select: messageSelect, remove: messageRemove, clear: messageClear }; @@ -156,13 +201,11 @@ if (!options.priority && (type === 'warning' || type === 'error')) { options.priority = 'assertive'; } - // If screen reader message is not disabled announce screen reader specific // text or fallback to the displayed message. - Drupal.announce((typeof options.announce === 'string' && options.announce !== '') ? - options.announce : - message, options.priority); - + if (options.announce !== '') { + Drupal.announce(options.announce || message, options.priority); + } } /** @@ -174,17 +217,37 @@ * The message text. * @param {string} message.type * The message type. - * @param {object} [options] + * @param {object} options * The message context. * @param {string} options.index * Index of the message, for reference. * - * @return {string} - * A string representing a DOM fragment. + * @return {HTMLElement} + * A DOM Node. */ Drupal.theme.message = function (message, options) { - return '
' + message.text + '
'; + var messageText = message.text; + var messageWraper = document.createElement('div'); + messageWraper.setAttribute('class', 'messages messages--' + message.type); + messageWraper.setAttribute('role', 'contentinfo'); + messageWraper.setAttribute('data-drupal-message', options.index); + if (message.type in messagesTypes) { + messageWraper.setAttribute('aria-label', messagesTypes[message.type]); + messageText = '

' + messagesTypes[message.type] + '

\n' + message.text; + } + + // Alerts have a different HTML structure + if (message.type === 'error') { + var ariaAlert = document.createElement('div'); + ariaAlert.setAttribute('role', 'alert'); + ariaAlert.innerHTML = messageText; + messageWraper.appendChild(ariaAlert); + } + else { + messageWraper.innerHTML = messageText; + } + + return messageWraper; }; }(Drupal)); diff -u b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js --- b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js +++ b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js @@ -68,8 +68,8 @@ defaultMessageArea.add('Msg-' + total, 'status'); }); - $('[data-action="clear-type"]').once('clear-type').on('click', function (e) { - defaultMessageArea.clear('error'); + $('[data-action="remove-type"]').once('remove-type').on('click', function (e) { + defaultMessageArea.remove('error'); }); $('[data-action="clear-all"]').once('clear-all').on('click', function (e) { diff -u b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php --- b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php +++ b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php @@ -89,14 +89,14 @@ 'data-action' => 'add-multiple-error', ], ]; - $buttons['clear-type'] = [ + $buttons['remove-type'] = [ '#type' => 'html_tag', '#tag' => 'button', - '#value' => "Clear 'error' type", + '#value' => "Remove 'error' type", '#attributes' => [ 'type' => 'button', - 'id' => 'clear-type', - 'data-action' => 'clear-type', + 'id' => 'remove-type', + 'data-action' => 'remove-type', ], ]; $buttons['clear-all'] = [ diff -u b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php --- b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php @@ -76,7 +76,7 @@ $current_messages[] = $last_message; $this->click('[id="add-multiple-error"]'); $this->assertCurrentMessages($current_messages); - $this->click('[id="clear-type"]'); + $this->click('[id="remove-type"]'); $this->assertCurrentMessages([$last_message]); $this->click('[id="clear-all"]'); $this->assertCurrentMessages([]);