diff --git a/core/core.libraries.yml b/core/core.libraries.yml index 5da51e8ae5..70425b85cb 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -233,6 +233,14 @@ drupal.machine-name: - core/drupalSettings - core/drupal.form +drupal.message: + version: VERSION + js: + misc/message.js: {} + dependencies: + - core/drupal + - core/drupal.announce + 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 530457fd90..cd3bf525af 100644 --- a/core/lib/Drupal/Core/Render/Element/StatusMessages.php +++ b/core/lib/Drupal/Core/Render/Element/StatusMessages.php @@ -75,19 +75,17 @@ public static function generatePlaceholder(array $element) { public static function renderMessages($type) { $render = []; $messages = drupal_get_messages($type); - if ($messages) { - // Render the messages. - $render = [ - '#theme' => 'status_messages', - // @todo Improve when https://www.drupal.org/node/2278383 lands. - '#message_list' => $messages, - '#status_headings' => [ - 'status' => t('Status message'), - 'error' => t('Error message'), - 'warning' => t('Warning message'), - ], - ]; - } + // Render the messages. + $render = [ + '#theme' => 'status_messages', + // @todo Improve when https://www.drupal.org/node/2278383 lands. + '#message_list' => $messages, + '#status_headings' => [ + 'status' => t('Status message'), + 'error' => t('Error message'), + 'warning' => t('Warning message'), + ], + ]; return $render; } diff --git a/core/misc/announce.js b/core/misc/announce.js index acf850a641..cd1ee5bb1a 100644 --- a/core/misc/announce.js +++ b/core/misc/announce.js @@ -50,21 +50,23 @@ /** * 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'; + } } } @@ -81,8 +83,13 @@ // 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. * @@ -96,25 +103,24 @@ * * @param {string} text * A string to be read by the UA. - * @param {string} [priority='polite'] + * @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 */ 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); + } + else { + throw new Error(Drupal.t('"text" passed Drupal.announce must be a string.')); + } }; }(Drupal, Drupal.debounce)); diff --git a/core/misc/message.js b/core/misc/message.js new file mode 100644 index 0000000000..ed19df7209 --- /dev/null +++ b/core/misc/message.js @@ -0,0 +1,188 @@ +/** + * @file + * Message API. + */ +(function (Drupal) { + + 'use strict'; + + var defaultMessageWrapperSelector = '[data-drupal-messages]'; + + /** + * @typedef {object} Drupal.message~messageDefinition + */ + + /** + * Constructs a new instance of the Drupal.message object. + * + * This provides a uniform interface for adding and removing messages to a + * specific location on the page. + * + * @param {HTMLElement} messageWrapper + * The zone where to add messages. If no element is supplied, the default + * selector is used. + * + * @return {Drupal.message~messageDefinition} + * Object to add and remove messages. + */ + Drupal.message = function (messageWrapper) { + // If an argument is passed, verify that it's a DOM Node of some kind. + if (arguments.length === 1) { + if (!messageWrapper || messageWrapper.nodeType !== 1) { + throw new Error(Drupal.t('Drupal.message() expect an HTMLElement as parameter.')); + } + } + else { + messageWrapper = document.querySelector(defaultMessageWrapperSelector); + if (!messageWrapper) { + throw new Error(Drupal.t('There is no @element on the page.', {'@element': defaultMessageWrapperSelector})); + } + } + + /** + * Sequentially adds a message to the message area. + * + * @name Drupal.message~messageDefinition.add + * + * @param {string} message + * The message to display + * @param {string} [type=status] + * Message type, can be either 'status', 'error' or 'warning'. + * @param {object} [options] + * The context of the message, used for removing messages again. + * + * @return {string} + * Index of message. + */ + function messageAdd(message, type, options) { + if (typeof message !== 'string') { + throw new Error('Message must be a string.'); + } + type = type || 'status'; + options = options || {}; + // Send message to screen reader. + announce(message, type, options); + // Generate a unique key to allow message deletion. + options.index = Math.random().toFixed(15).replace('0.', ''); + messageWrapper.innerHTML += Drupal.theme('message', {text: message, type: type}, options); + + return options.index; + } + + /** + * Removes messages from the message area. + * + * @name Drupal.message~messageDefinition.remove + * + * @param {string|Array.} messages + * 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(messages) { + // If there is no argument or if the argument is an empty array, + // no message can be removed. + if (!messages || (Array.isArray(messages) && messages.length === 0)) { + return 0; + } + + var removeSelectors = (Array.isArray(messages) ? messages : [messages]) + .map(function (messageIndex) { + return '[data-drupal-message="' + messageIndex + '"]'; + }); + + var toRemove = messageWrapper.querySelectorAll(removeSelectors.join(',')); + var length = toRemove.length; + for (var i = 0; i < length; i++) { + messageWrapper.removeChild(toRemove[i]); + } + + return length; + } + + /** + * Removes all messages from the message area. + * + * @name Drupal.message~messageDefinition.clear + * + * @param {string} [type] + * The type of message to clear, default to all messages. + * + * @return {number} + * 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; + } + + return { + add: messageAdd, + remove: messageRemove, + clear: messageClear + }; + }; + + /** + * Helper to call Drupal.announce() with the right parameters. + * + * @param {string} message + * Displayed message. + * @param {string} type + * Message type, can be either 'status', 'error' or 'warning'. + * @param {object} options + * Additional data. + * @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(). + */ + function announce(message, type, options) { + // Check this. Might be too much. + if (!options.priority && type !== 'status') { + options.priority = 'assertive'; + } + // If screen reader message is not disabled announce screen reader specific + // text or fallback to the displayed message. + if (options.announce !== '') { + Drupal.announce(options.announce || message, options.priority); + } + } + + /** + * Theme function for a message. + * + * @param {object} message + * The message object. + * @param {string} message.text + * The message text. + * @param {string} message.type + * The message type. + * @param {object} [options] + * The message context. + * @param {string} options.index + * Index of the message, for reference. + * + * @return {string} + * A string representing a DOM fragment. + */ + Drupal.theme.message = function (message, options) { + return '
' + message.text + '
'; + }; + +}(Drupal)); diff --git a/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php b/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php index 69ec86fdc2..74816999eb 100644 --- a/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php +++ b/core/modules/big_pipe/src/Tests/BigPipePlaceholderTestCases.php @@ -110,11 +110,11 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf 'command' => 'insert', 'method' => 'replaceWith', 'selector' => '[data-big-pipe-placeholder-id="callback=Drupal%5CCore%5CRender%5CElement%5CStatusMessages%3A%3ArenderMessages&args[0]&token=_HAdUpwWmet0TOTe2PSiJuMntExoshbm1kh2wQzzzAA"]', - 'data' => "\n" . ' ' . "\n ", + 'data' => "\n" . '
' . "\n" . ' ' . "\n" . '
' . "\n", 'settings' => NULL, ], ]; - $status_messages->embeddedHtmlResponse = '' . "\n" . "\n" . ' ' . "\n \n"; + $status_messages->embeddedHtmlResponse = '
' . "\n" . ' ' . "\n" . '
' . "\n\n"; } diff --git a/core/modules/system/src/Tests/JsMessageTestCases.php b/core/modules/system/src/Tests/JsMessageTestCases.php new file mode 100644 index 0000000000..20d2e7630b --- /dev/null +++ b/core/modules/system/src/Tests/JsMessageTestCases.php @@ -0,0 +1,32 @@ + {% for type, messages in message_list %}
{% if type == 'error' %}
{% endif %} - {% if status_headings[type] %} -

{{ status_headings[type] }}

- {% endif %} - {% if messages|length > 1 %} -
    - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} -
- {% else %} - {{ messages|first }} - {% endif %} + {% 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 %}
{% endfor %} + diff --git a/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 new file mode 100644 index 0000000000..df9660d28b --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js/js_message_test.js @@ -0,0 +1,82 @@ +/** + * @file + * Testing behavior for JSMessageTest. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + var messageObjects = {}; + var messageIndexes = {multiple: []}; + + drupalSettings.testMessages.selectors.forEach(function (selector) { + messageObjects[selector] = Drupal.message(document.querySelector(selector)); + + messageIndexes[selector] = {}; + drupalSettings.testMessages.types.forEach(function (type) { + messageIndexes[selector][type] = []; + }); + }); + + var defaultMessageArea = messageObjects[drupalSettings.testMessages.selectors[0]]; + + /** + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Add click listeners that show and remove links with context and type. + */ + Drupal.behaviors.js_message_test = { + attach: function (context) { + + $('[data-drupal-messages-area]').once('messages-details').on('click', '[data-action]', function (e) { + var $target = $(e.currentTarget); + var type = $target.attr('data-type'); + var area = $target.closest('[data-drupal-messages-area]').attr('data-drupal-messages-area'); + var message = messageObjects[area]; + var action = $target.attr('data-action'); + + if (action === 'add') { + messageIndexes[area][type].push(message.add('Msg-' + type, type)); + } + else if (action === 'remove') { + message.remove(messageIndexes[area][type].pop()); + } + }); + + $('[data-action="add-multiple"]').once('add-multiple').on('click', function (e) { + var types = drupalSettings.testMessages.types; + // Add several of different types to make sure message type doesn't + // cause issues in the API. + for (var i = 0; i < types.length * 2; i++) { + messageIndexes.multiple.push(defaultMessageArea.add('Msg-' + i, types[i % types.length])); + } + }); + + $('[data-action="remove-multiple"]').once('remove-multiple').on('click', function (e) { + defaultMessageArea.remove(messageIndexes.multiple); + messageIndexes.multiple = []; + }); + + $('[data-action="add-multiple-error"]').once('add-multiple-error').on('click', function (e) { + // Use the same number of elements to facilitate things on the PHP side. + var total = drupalSettings.testMessages.types.length * 2; + for (var i = 0; i < total; i++) { + defaultMessageArea.add('Msg-' + i, 'error'); + } + defaultMessageArea.add('Msg-' + total, 'status'); + }); + + $('[data-action="clear-type"]').once('clear-type').on('click', function (e) { + defaultMessageArea.clear('error'); + }); + + $('[data-action="clear-all"]').once('clear-all').on('click', function (e) { + defaultMessageArea.clear(); + }); + + } + }; + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml new file mode 100644 index 0000000000..e8bc73b065 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.info.yml @@ -0,0 +1,6 @@ +name: 'JS Message test module' +type: module +description: 'Module for the JSMessageTest test.' +package: Testing +version: VERSION +core: 8.x diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml new file mode 100644 index 0000000000..57501a9ce0 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.libraries.yml @@ -0,0 +1,8 @@ +show_message: + version: VERSION + js: + js/js_message_test.js: {} + dependencies: + - core/drupalSettings + - core/drupal.message + - core/jquery.once diff --git a/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml b/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml new file mode 100644 index 0000000000..4c325e8461 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/js_message_test.routing.yml @@ -0,0 +1,7 @@ +js_message_test.links: + path: '/js_message_test_link' + defaults: + _controller: '\Drupal\js_message_test\Controller\JSMessageTestController::messageLinks' + _title: 'JsMessageLinks' + requirements: + _access: 'TRUE' diff --git a/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 new file mode 100644 index 0000000000..871ac8dbb3 --- /dev/null +++ b/core/modules/system/tests/modules/js_message_test/src/Controller/JSMessageTestController.php @@ -0,0 +1,128 @@ + 'details', + '#open' => TRUE, + '#title' => "Message area: $messagesSelector", + '#attributes' => [ + 'data-drupal-messages-area' => $messagesSelector, + ] + ]; + foreach (JsMessageTestCases::getTypes() as $type) { + $buttons[$messagesSelector]["add-$type"] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Add $type", + '#attributes' => [ + 'type' => 'button', + 'id' => "add-$messagesSelector-$type", + 'data-type' => $type, + 'data-action' => 'add', + ], + ]; + $buttons[$messagesSelector]["remove-$type"] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Remove $type", + '#attributes' => [ + 'type' => 'button', + 'id' => "remove-$messagesSelector-$type", + 'data-type' => $type, + 'data-action' => 'remove', + ], + ]; + } + } + // Add alternative message area. + $buttons[JsMessageTestCases::getMessagesSelectors()[1]]['messages-other-area'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'data-drupal-messages-other' => TRUE, + ], + ]; + $buttons['add-multiple'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Add multiple", + '#attributes' => [ + 'type' => 'button', + 'id' => 'add-multiple', + 'data-action' => 'add-multiple', + ], + ]; + $buttons['remove-multiple'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Remove multiple", + '#attributes' => [ + 'type' => 'button', + 'id' => 'remove-multiple', + 'data-action' => 'remove-multiple', + ], + ]; + $buttons['add-multiple-error'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Add multiple 'error' and one 'status'", + '#attributes' => [ + 'type' => 'button', + 'id' => 'add-multiple-error', + 'data-action' => 'add-multiple-error', + ], + ]; + $buttons['clear-type'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Clear 'error' type", + '#attributes' => [ + 'type' => 'button', + 'id' => 'clear-type', + 'data-action' => 'clear-type', + ], + ]; + $buttons['clear-all'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => "Clear all", + '#attributes' => [ + 'type' => 'button', + 'id' => 'clear-all', + 'data-action' => 'clear-all', + ], + ]; + + return $buttons + [ + '#attached' => [ + 'library' => [ + 'js_message_test/show_message', + ], + 'drupalSettings' => [ + 'testMessages' => [ + 'selectors' => JsMessageTestCases::getMessagesSelectors(), + 'types' => JsMessageTestCases::getTypes(), + ], + ], + ] + ]; + } + +} diff --git a/core/modules/system/tests/themes/test_messages/templates/status-messages.html.twig b/core/modules/system/tests/themes/test_messages/templates/status-messages.html.twig new file mode 100644 index 0000000000..6e94958b74 --- /dev/null +++ b/core/modules/system/tests/themes/test_messages/templates/status-messages.html.twig @@ -0,0 +1,42 @@ +{# +/** + * @file + * Test templates file with extra messages div. + */ +#} +{{ attach_library('classy/messages') }} +
+{% block messages %} +{% for type, messages in message_list %} + {% + set classes = [ + 'messages', + 'messages--' ~ type, + ] + %} +
+ {% if type == 'error' %} +
+ {% endif %} + {% 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 %} +
+ {# Remove type specific classes. #} + {% set attributes = attributes.removeClass(classes) %} +{% endfor %} +{% endblock messages %} +
+
diff --git a/core/modules/system/tests/themes/test_messages/test_messages.info.yml b/core/modules/system/tests/themes/test_messages/test_messages.info.yml new file mode 100644 index 0000000000..ea88438d98 --- /dev/null +++ b/core/modules/system/tests/themes/test_messages/test_messages.info.yml @@ -0,0 +1,6 @@ +name: 'Theme test messages' +type: theme +description: 'Test theme which provides another div for messages.' +version: VERSION +core: 8.x +base theme: classy diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php new file mode 100644 index 0000000000..415b167bf6 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/JsMessageTest.php @@ -0,0 +1,103 @@ +install(['test_messages']); + $theme_config = \Drupal::configFactory()->getEditable('system.theme'); + $theme_config->set('default', 'test_messages'); + $theme_config->save(); + } + + /** + * Test click on links to show messages and remove messages. + */ + public function testAddRemoveMessages() { + $web_assert = $this->assertSession(); + $this->drupalGet('js_message_test_link'); + + $current_messages = []; + foreach (JsMessageTestCases::getMessagesSelectors() as $messagesSelector) { + $web_assert->elementExists('css', $messagesSelector); + foreach (JsMessageTestCases::getTypes() as $type) { + $this->click('[id="add-' . $messagesSelector . '-' . $type . '"]'); + $selector = "$messagesSelector .messages.messages--$type"; + $msg_element = $web_assert->waitForElementVisible('css', $selector); + $this->assertNotEmpty($msg_element, "Message element visible: $selector"); + $web_assert->elementContains('css', $selector, "Msg-$type"); + $current_messages[$selector] = "Msg-$type"; + $this->assertCurrentMessages($current_messages); + } + // Remove messages 1 by 1 and confirm the messages are expected. + foreach (JsMessageTestCases::getTypes() as $type) { + $this->click('[id="remove-' . $messagesSelector . '-' . $type . '"]'); + $selector = "$messagesSelector .messages.messages--$type"; + // The message for this selector should not be on the page. + unset($current_messages[$selector]); + $this->assertCurrentMessages($current_messages); + } + } + + $current_messages = []; + $nb_messages = count(JsMessageTestCases::getTypes()) * 2; + for ($i = 0; $i < $nb_messages; $i++) { + $current_messages[] = "Msg-$i"; + } + // Test adding multiple messages at once. + // @see processMessages() + $this->click('[id="add-multiple"]'); + $this->assertCurrentMessages($current_messages); + $this->click('[id="remove-multiple"]'); + $this->assertCurrentMessages([]); + + // The last message is of a different type and shouldn't get cleared. + $last_message = 'Msg-' . count($current_messages); + $current_messages[] = $last_message; + $this->click('[id="add-multiple-error"]'); + $this->assertCurrentMessages($current_messages); + $this->click('[id="clear-type"]'); + $this->assertCurrentMessages([$last_message]); + $this->click('[id="clear-all"]'); + $this->assertCurrentMessages([]); + } + + /** + * Asserts that currently shown messages match expected messages. + * + * @param array $expected_messages + * Expected messages. + */ + protected function assertCurrentMessages(array $expected_messages) { + $expected_messages = array_values($expected_messages); + $current_messages = []; + if ($message_divs = $this->getSession()->getPage()->findAll('css', '.messages')) { + foreach ($message_divs as $message_div) { + /** @var \Behat\Mink\Element\NodeElement $message_div */ + $current_messages[] = $message_div->getText(); + } + } + $this->assertEquals($expected_messages, $current_messages); + } + +} diff --git a/core/themes/bartik/css/components/messages.css b/core/themes/bartik/css/components/messages.css index 15a550d0b9..bc9001167d 100644 --- a/core/themes/bartik/css/components/messages.css +++ b/core/themes/bartik/css/components/messages.css @@ -4,10 +4,15 @@ */ .messages__wrapper { - padding: 20px 0 5px 8px; + padding: 0 0 0 8px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - margin: 8px 0; +} +.messages__wrapper .messages:first-child { + margin-top: 28px; +} +.messages__wrapper .messages:last-child { + margin-bottom: 13px; } [dir="rtl"] .messages__wrapper { - padding: 20px 8px 5px 0; + padding: 0 8px 0 0; } diff --git a/core/themes/bartik/templates/status-messages.html.twig b/core/themes/bartik/templates/status-messages.html.twig index a4e1e9e240..aa1bedd773 100644 --- a/core/themes/bartik/templates/status-messages.html.twig +++ b/core/themes/bartik/templates/status-messages.html.twig @@ -23,7 +23,7 @@ {% block messages %} {% if message_list is not empty %} {{ attach_library('bartik/messages') }} -
+
{{ parent() }}
{% endif %} diff --git a/core/themes/classy/templates/misc/status-messages.html.twig b/core/themes/classy/templates/misc/status-messages.html.twig index bc8fd106c2..6985ee7594 100644 --- a/core/themes/classy/templates/misc/status-messages.html.twig +++ b/core/themes/classy/templates/misc/status-messages.html.twig @@ -22,6 +22,7 @@ */ #} {{ attach_library('classy/messages') }} +
{% block messages %} {% for type, messages in message_list %} {% @@ -54,3 +55,4 @@ {% set attributes = attributes.removeClass(classes) %} {% endfor %} {% endblock messages %} +