diff --git a/core/core.services.yml b/core/core.services.yml index 1aadb75..9402752 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -338,7 +338,7 @@ services: arguments: ['@request_stack', '@url_generator'] form_error_handler: class: Drupal\Core\Form\FormErrorHandler - arguments: ['@string_translation', '@link_generator'] + arguments: ['@string_translation', '@link_generator', '@renderer'] form_cache: class: Drupal\Core\Form\FormCache arguments: ['@app.root', '@keyvalue.expirable', '@module_handler', '@current_user', '@csrf_token', '@logger.channel.form', '@request_stack', '@page_cache_request_policy'] diff --git a/core/lib/Drupal/Core/Form/FormErrorHandler.php b/core/lib/Drupal/Core/Form/FormErrorHandler.php index df736b0..41b0e87 100644 --- a/core/lib/Drupal/Core/Form/FormErrorHandler.php +++ b/core/lib/Drupal/Core/Form/FormErrorHandler.php @@ -7,9 +7,9 @@ namespace Drupal\Core\Form; -use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Render\Element; use Drupal\Core\Routing\LinkGeneratorTrait; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\Core\Url; @@ -24,16 +24,26 @@ class FormErrorHandler implements FormErrorHandlerInterface { use LinkGeneratorTrait; /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs a new FormErrorHandler. * * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The string translation service. * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator * The link generation service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The Renderer service. */ - public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator) { + public function __construct(TranslationInterface $string_translation, LinkGeneratorInterface $link_generator, RendererInterface $renderer) { $this->stringTranslation = $string_translation; $this->linkGenerator = $link_generator; + $this->renderer = $renderer; } /** @@ -82,9 +92,7 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st unset($errors[$name]); } elseif ($is_visible_element && $has_title && $has_id) { - // We need to pass this through SafeMarkup::escape() so - // drupal_set_message() does not encode the links. - $error_links[] = SafeMarkup::escape($this->l($title, Url::fromRoute('', [], ['fragment' => $form_element['#id'], 'external' => TRUE]))); + $error_links[] = $this->l($title, Url::fromRoute('', [], ['fragment' => $form_element['#id'], 'external' => TRUE])); unset($errors[$name]); } } @@ -95,9 +103,17 @@ protected function displayErrorMessages(array $form, FormStateInterface $form_st } if (!empty($error_links)) { - $message = $this->formatPlural(count($error_links), '1 error has been found: !errors', '@count errors have been found: !errors', [ - '!errors' => SafeMarkup::set(implode(', ', $error_links)), - ]); + $render_array = [ + [ + '#markup' => $this->formatPlural(count($error_links), '1 error has been found: ', '@count errors have been found: '), + ], + [ + '#theme' => 'item_list', + '#items' => $error_links, + '#context' => ['list_style' => 'comma-list'], + ], + ]; + $message = $this->renderer->renderPlain($render_array); $this->drupalSetMessage($message, 'error'); } } diff --git a/core/modules/datetime/src/Tests/DateTimeFieldTest.php b/core/modules/datetime/src/Tests/DateTimeFieldTest.php index d5d14a4..69f3725 100644 --- a/core/modules/datetime/src/Tests/DateTimeFieldTest.php +++ b/core/modules/datetime/src/Tests/DateTimeFieldTest.php @@ -589,13 +589,13 @@ function testDatelistWidget() { protected function datelistDataProvider() { return [ // Year only selected, validation error on Month, Day, Hour, Minute. - [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], '4 errors have been found: Month, Day, Hour, Minute'], + [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], '4 errors have been found: MonthDayHourMinute'], // Year and Month selected, validation error on Day, Hour, Minute. - [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: Day, Hour, Minute'], + [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''], '3 errors have been found: DayHourMinute'], // Year, Month and Day selected, validation error on Hour, Minute. - [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: Hour, Minute'], + [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''], '2 errors have been found: HourMinute'], // Year, Month, Day and Hour selected, validation error on Minute only. - [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], '1 error has been found: Minute'], + [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''], '1 error has been found: Minute'], ]; } diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php index 0956896..cdd56cc 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceAdminTest.php @@ -275,7 +275,7 @@ public function testFieldAdminHandler() { $this->drupalPostForm('node/add/' . $this->type, $edit, t('Save')); // Assert that entity reference autocomplete field is validated. - $this->assertText(t('1 error has been found: Test Entity Reference Field'), 'Node save failed when required entity reference field was not correctly filled.'); + $this->assertText(t('1 error has been found: Test Entity Reference Field'), 'Node save failed when required entity reference field was not correctly filled.'); $this->assertText(t('There are no entities matching "@entity"', ['@entity' => 'Test'])); $edit = array( @@ -286,7 +286,7 @@ public function testFieldAdminHandler() { // Assert the results multiple times to avoid sorting problem of nodes with // the same title. - $this->assertText(t('1 error has been found: Test Entity Reference Field')); + $this->assertText(t('1 error has been found: Test Entity Reference Field')); $this->assertText(t('Multiple entities match this reference;')); $this->assertText(t("@node1", ['@node1' => $node1->getTitle() . ' (' . $node1->id() . ')'])); $this->assertText(t("@node2", ['@node2' => $node2->getTitle() . ' (' . $node2->id() . ')'])); diff --git a/core/modules/file/src/Tests/FileFieldValidateTest.php b/core/modules/file/src/Tests/FileFieldValidateTest.php index 46e34ee..ab1d2a5 100644 --- a/core/modules/file/src/Tests/FileFieldValidateTest.php +++ b/core/modules/file/src/Tests/FileFieldValidateTest.php @@ -35,7 +35,7 @@ function testRequired() { $edit = array(); $edit['title[0][value]'] = $this->randomMachineName(); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); - $this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required file field was empty.'); + $this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required file field was empty.'); $this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', [':class' => ' messages--error '])), 'There is one link in the error message.'); // Create a new node with the uploaded file. @@ -57,7 +57,7 @@ function testRequired() { $edit = array(); $edit['title[0][value]'] = $this->randomMachineName(); $this->drupalPostForm('node/add/' . $type_name, $edit, t('Save and publish')); - $this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required multiple value file field was empty.'); + $this->assertText('1 error has been found: ' . $field->label(), 'Node save failed when required multiple value file field was empty.'); $this->assertIdentical(1, count($this->xpath('//div[contains(concat(" ", normalize-space(@class), " "), :class)]//a', [':class' => ' messages--error '])), 'There is one link in the error message.'); // Create a new node with the uploaded file into the multivalue field. diff --git a/core/modules/shortcut/src/Tests/ShortcutSetsTest.php b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php index 9334ea1..26a3171 100644 --- a/core/modules/shortcut/src/Tests/ShortcutSetsTest.php +++ b/core/modules/shortcut/src/Tests/ShortcutSetsTest.php @@ -132,7 +132,8 @@ function testShortcutSetSwitchCreate() { function testShortcutSetSwitchNoSetName() { $edit = array('set' => 'new'); $this->drupalPostForm('user/' . $this->adminUser->id() . '/shortcuts', $edit, t('Change set')); - $this->assertRaw('1 error has been found: Label'); + $this->assertRaw('1 error has been found:'); + $this->assertRaw('Label'); $current_set = shortcut_current_displayed_set($this->adminUser); $this->assertEqual($current_set->id(), $this->set->id(), 'Attempting to switch to a new shortcut set without providing a set name does not succeed.'); $this->assertFieldByXPath("//input[@name='label' and contains(concat(' ', normalize-space(@class), ' '), ' error ')]", NULL, 'The new set label field has the error class'); diff --git a/core/modules/system/css/components/item-list.theme.css b/core/modules/system/css/components/item-list.theme.css index 9a1088e..1623907 100644 --- a/core/modules/system/css/components/item-list.theme.css +++ b/core/modules/system/css/components/item-list.theme.css @@ -2,25 +2,12 @@ * @file * Visual styles for item list. */ - -.item-list .title { - font-weight: bold; -} -.item-list ul { - margin: 0 0 0.75em 0; - padding: 0; -} -.item-list ul li { - margin: 0 0 0.25em 1.5em; /* LTR */ - padding: 0; -} -[dir="rtl"] .item-list ul li { - margin: 0 1.5em 0.25em 0; -} -ul.item-list__comma-list { +ul.item-list__comma-list, +.item-list ul.item-list__comma-list { display: inline; } -ul.item-list__comma-list li { +ul.item-list__comma-list li, +.item-list ul.item-list__comma-list li { display: inline; list-style-type: none; } diff --git a/core/modules/system/src/Tests/Form/ValidationTest.php b/core/modules/system/src/Tests/Form/ValidationTest.php index 246fb68..1d130f9 100644 --- a/core/modules/system/src/Tests/Form/ValidationTest.php +++ b/core/modules/system/src/Tests/Form/ValidationTest.php @@ -292,7 +292,10 @@ protected function assertErrorMessages($messages) { $error_links[] = \Drupal::l($message['title'], Url::fromRoute('', [], ['fragment' => 'edit-' . str_replace('_', '-', $message['key']), 'external' => TRUE])); } $top_message = \Drupal::translation()->formatPlural(count($error_links), '1 error has been found:', '@count errors have been found:'); - $this->assertRaw($top_message . ' ' . implode(', ', $error_links)); + $this->assertRaw($top_message); + foreach ($error_links as $error_link) { + $this->assertRaw($error_link); + } $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.'); } diff --git a/core/modules/user/src/Tests/UserBlocksTest.php b/core/modules/user/src/Tests/UserBlocksTest.php index e92640b..9ac5ae9 100644 --- a/core/modules/user/src/Tests/UserBlocksTest.php +++ b/core/modules/user/src/Tests/UserBlocksTest.php @@ -48,7 +48,8 @@ function testUserLoginBlock() { $edit['name'] = $this->randomMachineName(); $edit['pass'] = $this->randomMachineName(); $this->drupalPostForm('node', $edit, t('Log in')); - $this->assertRaw('1 error has been found: Username'); + $this->assertRaw('1 error has been found:'); + $this->assertRaw('Username'); $this->assertText(t('Sorry, unrecognized username or password.')); // Create a user with some permission that anonymous users lack. diff --git a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php index 53d7e76..4d2219d 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormErrorHandlerTest.php @@ -25,8 +25,9 @@ public function testDisplayErrorMessages() { $link_generator->expects($this->any()) ->method('generate') ->willReturnArgument(0); + $renderer = $this->getMock('\Drupal\Core\Render\RendererInterface'); $form_error_handler = $this->getMockBuilder('Drupal\Core\Form\FormErrorHandler') - ->setConstructorArgs([$this->getStringTranslationStub(), $link_generator]) + ->setConstructorArgs([$this->getStringTranslationStub(), $link_generator, $renderer]) ->setMethods(['drupalSetMessage']) ->getMock(); @@ -41,7 +42,14 @@ public function testDisplayErrorMessages() { ->with('this missing element is invalid', 'error'); $form_error_handler->expects($this->at(3)) ->method('drupalSetMessage') - ->with('3 errors have been found: Test 1, Test 2 & a half, Test 3', 'error'); + ->with('3 errors have been found: Test 1Test 2 & a halfTest 3', 'error'); + + $renderer->expects($this->any()) + ->method('renderPlain') + ->will($this->returnCallback(function ($render_array) { + return $render_array[0]['#markup'] . '' . implode(array_map('htmlspecialchars', $render_array[1]['#items']), '') . ''; + })); + $form = [ '#parents' => [], @@ -103,7 +111,7 @@ public function testDisplayErrorMessages() { */ public function testSetElementErrorsFromFormState() { $form_error_handler = $this->getMockBuilder('Drupal\Core\Form\FormErrorHandler') - ->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface')]) + ->setConstructorArgs([$this->getStringTranslationStub(), $this->getMock('Drupal\Core\Utility\LinkGeneratorInterface'), $this->getMock('\Drupal\Core\Render\RendererInterface')]) ->setMethods(['drupalSetMessage']) ->getMock(); diff --git a/core/themes/bartik/css/components/item-list.css b/core/themes/bartik/css/components/item-list.css index 698bd56..eeb4e77 100644 --- a/core/themes/bartik/css/components/item-list.css +++ b/core/themes/bartik/css/components/item-list.css @@ -10,10 +10,3 @@ [dir="rtl"] .item-list ul li { padding: 0.2em 0 0 0.5em; } -.item-list .item-list__comma-list, -.item-list .item-list__comma-list li, -[dir="rtl"] .item-list .item-list__comma-list, -[dir="rtl"] .item-list .item-list__comma-list li { - margin: 0; - padding: 0; -} diff --git a/core/themes/classy/classy.libraries.yml b/core/themes/classy/classy.libraries.yml index a5bde77..8276033 100644 --- a/core/themes/classy/classy.libraries.yml +++ b/core/themes/classy/classy.libraries.yml @@ -3,6 +3,9 @@ base: css: theme: css/layout.css: {} + # We can not attach this in the item-list.html.twig template since it is + # often used during RendererInterface::renderPlain(). + css/dataset/item.list.css: {} book-navigation: version: VERSION diff --git a/core/themes/classy/css/dataset/item.list.css b/core/themes/classy/css/dataset/item.list.css new file mode 100644 index 0000000..bce0456 --- /dev/null +++ b/core/themes/classy/css/dataset/item.list.css @@ -0,0 +1,38 @@ +/** + * @file + * Styling for item lists. + */ +.item-list .title { + font-weight: bold; +} +.item-list ul { + margin: 0 0 0.75em 0; + padding: 0; +} +.item-list ul li { + margin: 0 0 0.25em 1.5em; /* LTR */ + padding: 0; +} +[dir="rtl"] .item-list ul li { + margin: 0 1.5em 0.25em 0; +} +div.item-list__comma-list { + display: inline; +} +/** + * Comma separated lists inside lists. + */ +.item-list ul.item-list__comma-list { + display: inline; +} +.item-list ul.item-list__comma-list li { + display: inline; + list-style-type: none; +} +.item-list .item-list__comma-list, +.item-list .item-list__comma-list li, +[dir="rtl"] .item-list .item-list__comma-list, +[dir="rtl"] .item-list .item-list__comma-list li { + margin: 0; + padding: 0; +} diff --git a/core/themes/classy/templates/dataset/item-list.html.twig b/core/themes/classy/templates/dataset/item-list.html.twig index 7ba8be4..abecd47 100644 --- a/core/themes/classy/templates/dataset/item-list.html.twig +++ b/core/themes/classy/templates/dataset/item-list.html.twig @@ -18,11 +18,14 @@ * @see template_preprocess_item_list() */ #} -{% if context.list_style %} - {% set attributes = attributes.addClass('item-list__' ~ context.list_style) %} -{% endif %} +{%- if context.list_style -%} + {%- set attributes = attributes.addClass('item-list__' ~ context.list_style) -%} + {%- set wrapper_class = 'item-list item-list__' ~ context.list_style -%} +{%- else -%} + {%- set wrapper_class = 'item-list' -%} +{%- endif -%} {%- if items or empty -%} -
+
{%- if title is not empty -%}

{{ title }}

{%- endif -%}