diff --git a/core/lib/Drupal/Core/Render/Element/Details.php b/core/lib/Drupal/Core/Render/Element/Details.php index 92bbb32..28a6f83 100644 --- a/core/lib/Drupal/Core/Render/Element/Details.php +++ b/core/lib/Drupal/Core/Render/Element/Details.php @@ -89,6 +89,19 @@ public static function preRenderDetails($element) { } } + // Indicate that there is a child element with an error. + if (!empty($element['#children_errors'])) { + $element['#attributes']['class'][] = 'details--child-error'; + $element['#title'] = \Drupal::translation()->formatPlural( + count($element['#children_errors']), + '@title (contains an error)', + '@title (contains @count errors)', + [ + '@title' => $element['#title'] + ] + ); + } + return $element; } diff --git a/core/lib/Drupal/Core/Render/Element/RenderElement.php b/core/lib/Drupal/Core/Render/Element/RenderElement.php index 243a9af..ebf30d7 100644 --- a/core/lib/Drupal/Core/Render/Element/RenderElement.php +++ b/core/lib/Drupal/Core/Render/Element/RenderElement.php @@ -204,6 +204,12 @@ public static function preRenderGroup($element) { } } + // Set attributes to indicate that there is a child element with an error. + if (!empty($element['#children_errors'])) { + $element['#attributes']['class'][] = 'form-wrapper--child-error'; + $element['#attributes']['data-child-error-count'] = count($element['#children_errors']); + } + return $element; } diff --git a/core/misc/vertical-tabs.es6.js b/core/misc/vertical-tabs.es6.js index 9c351d7..00eef0b 100644 --- a/core/misc/vertical-tabs.es6.js +++ b/core/misc/vertical-tabs.es6.js @@ -56,8 +56,9 @@ $details.each(function () { const $that = $(this); const vertical_tab = new Drupal.verticalTab({ - title: $that.find('> summary').text(), + title: $that.find('> summary').html(), details: $that, + child_error_count: $that.data('child-error-count'), }); tab_list.append(vertical_tab.item); $that @@ -239,10 +240,15 @@ const tab = {}; tab.item = $('
  • ') .append(tab.link = $('') - .append(tab.title = $('').text(settings.title)) + .append(tab.title = $('').html(settings.title)) .append(tab.summary = $(''), ), ); + + if (settings.child_error_count) { + tab.item.addClass('vertical-tab__menu-item--child-error'); + } + return tab; }; }(jQuery, Drupal, drupalSettings)); diff --git a/core/misc/vertical-tabs.js b/core/misc/vertical-tabs.js index b5ea23b..8940047 100644 --- a/core/misc/vertical-tabs.js +++ b/core/misc/vertical-tabs.js @@ -31,8 +31,9 @@ $details.each(function () { var $that = $(this); var vertical_tab = new Drupal.verticalTab({ - title: $that.find('> summary').text(), - details: $that + title: $that.find('> summary').html(), + details: $that, + child_error_count: $that.data('child-error-count') }); tab_list.append(vertical_tab.item); $that.removeClass('collapsed').attr('open', true).addClass('vertical-tabs__pane').data('verticalTab', vertical_tab); @@ -130,7 +131,12 @@ Drupal.theme.verticalTab = function (settings) { var tab = {}; - tab.item = $('
  • ').append(tab.link = $('').append(tab.title = $('').text(settings.title)).append(tab.summary = $(''))); + tab.item = $('
  • ').append(tab.link = $('').append(tab.title = $('').html(settings.title)).append(tab.summary = $(''))); + + if (settings.child_error_count) { + tab.item.addClass('vertical-tab__menu-item--child-error'); + } + return tab; }; })(jQuery, Drupal, drupalSettings); \ No newline at end of file diff --git a/core/modules/node/tests/src/Functional/NodeEditFormTest.php b/core/modules/node/tests/src/Functional/NodeEditFormTest.php index ce032e5..c65602c 100644 --- a/core/modules/node/tests/src/Functional/NodeEditFormTest.php +++ b/core/modules/node/tests/src/Functional/NodeEditFormTest.php @@ -55,6 +55,7 @@ protected function setUp() { */ public function testNodeEdit() { $this->drupalLogin($this->webUser); + $web_assert = $this->assertSession(); $title_key = 'title[0][value]'; $body_key = 'body[0][value]'; @@ -123,13 +124,16 @@ public function testNodeEdit() { // This invalid date will trigger an error. $edit['created[0][value][date]'] = $this->randomMachineName(8); // Get the current amount of open details elements. - $open_details_elements = count($this->cssSelect('details[open="open"]')); + $open_details_count = count($this->cssSelect('details[open="open"]')); $this->drupalPostForm(NULL, $edit, t('Save')); - // The node author details must be open. - $this->assertRaw('
    '); + // The node author details must be open and have child error attributes. + $this->assertRaw('
    '); + $open_details = $this->cssSelect('details[open="open"]'); // Only one extra details element should now be open. - $open_details_elements++; - $this->assertEqual(count($this->cssSelect('details[open="open"]')), $open_details_elements, 'Exactly one extra open <details> element found.'); + $open_details_count++; + $this->assertEqual(count($open_details), $open_details_count, 'Exactly one extra open <details> element found.'); + // The details summary should indicate the amount of child element errors. + $web_assert->elementTextContains('css', 'details.node-form-author .child-error-indicator', '(contains 3 errors)'); // Edit the same node, save it and verify it's unpublished after unchecking // the 'Published' boolean_checkbox and clicking 'Save'. diff --git a/core/modules/system/tests/modules/form_test/form_test.routing.yml b/core/modules/system/tests/modules/form_test/form_test.routing.yml index 2250a0b..46d14e8 100644 --- a/core/modules/system/tests/modules/form_test/form_test.routing.yml +++ b/core/modules/system/tests/modules/form_test/form_test.routing.yml @@ -459,6 +459,14 @@ form_test.group_vertical_tabs: requirements: _access: 'TRUE' +form_test.child_error_vertical_tabs: + path: '/form-test/child-error-vertical-tabs' + defaults: + _form: '\Drupal\form_test\Form\FormTestChildErrorVerticalTabsForm' + _title: 'Child element error vertical tabs testing' + requirements: + _access: 'TRUE' + form_test.two_instances: path: '/form-test/two-instances-of-same-form' defaults: diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestChildErrorVerticalTabsForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestChildErrorVerticalTabsForm.php new file mode 100644 index 0000000..7de7d01 --- /dev/null +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestChildErrorVerticalTabsForm.php @@ -0,0 +1,51 @@ + 'vertical_tabs', + ]; + $form['meta'] = [ + '#type' => 'details', + '#title' => 'First group element', + '#group' => 'vertical_tabs', + ]; + $form['meta']['element'] = [ + '#type' => 'textfield', + '#title' => 'First nested element in details element', + '#required' => TRUE, + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => 'Submit', + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + } + +} diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Core/Render/Element/VerticalTabsTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Core/Render/Element/VerticalTabsTest.php new file mode 100644 index 0000000..4677015 --- /dev/null +++ b/core/tests/Drupal/FunctionalJavascriptTests/Core/Render/Element/VerticalTabsTest.php @@ -0,0 +1,32 @@ +assertSession(); + + $this->drupalGet('form-test/child-error-vertical-tabs'); + + $this->submitForm([], 'Submit'); + + $web_assert->elementTextContains('css', '.vertical-tabs__menu-item-title .child-error-indicator', '(contains an error)'); + } + +} diff --git a/core/themes/bartik/bartik.libraries.yml b/core/themes/bartik/bartik.libraries.yml index 3bbecd9..bace5d0 100644 --- a/core/themes/bartik/bartik.libraries.yml +++ b/core/themes/bartik/bartik.libraries.yml @@ -10,6 +10,7 @@ global-styling: css/components/captions.css: {} css/components/comments.css: {} css/components/contextual.css: {} + css/components/details.css: {} css/components/demo-block.css: {} # @see https://www.drupal.org/node/2389735 css/components/dropbutton.component.css: {} diff --git a/core/themes/bartik/css/components/details.css b/core/themes/bartik/css/components/details.css new file mode 100644 index 0000000..2277e9d --- /dev/null +++ b/core/themes/bartik/css/components/details.css @@ -0,0 +1,33 @@ +/** + * @file + * Collapsible details. + * + * @see collapse.js + */ + +/* Errors */ +.details--child-error:not([open]) summary { + background-color: #f1f1f1; +} +.details--child-error:not([open]) summary:before { + content: ''; + display: inline-block; + height: 20px; + width: 14px; + background: url(../../../../misc/icons/e32700/error.svg) no-repeat center; + background-size: contain; + float: right; /* LTR */ +} +[dir="rtl"] .details--child-error:not([open]) summary:before { + float: left; +} +.details--child-error:not([open]) { + border: 2px solid red; + box-shadow: inset 7px 0 0 red; /* LTR */ + padding-left: 7px; /* LTR */ +} +[dir="rtl"] .details--child-error:not([open]) { + box-shadow: inset -7px 0 0 red; + padding-left: 0; + padding-right: 7px; +} diff --git a/core/themes/bartik/css/components/vertical-tabs.component.css b/core/themes/bartik/css/components/vertical-tabs.component.css index ce4d6cd..95cd421 100644 --- a/core/themes/bartik/css/components/vertical-tabs.component.css +++ b/core/themes/bartik/css/components/vertical-tabs.component.css @@ -14,3 +14,40 @@ /* This is required to win specificity over [dir="rtl"] .region-content ul */ padding: 0; } +.vertical-tab__menu-item--child-error.is-selected { + padding-top: 1px; +} +.vertical-tab__menu-item--child-error:not(.is-selected) { + border-top: 1px solid red; + background-color: #f1f1f1; + box-shadow: inset 7px 0 0 red; /* LTR */ + border-radius: 2px 0 0 2px; /* LTR */ + padding-left: 7px; /* LTR */ +} +.vertical-tab__menu-item--child-error:not(.is-selected) a { + padding-left: 7px; /* LTR */ + border-bottom: 1px solid red; + color: #e32700; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) { + box-shadow: inset -7px 0 0 red; + border-radius: 0 2px 2px 0; + padding-left: 0; + padding-right: 7px; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) a { + padding-left: 7px; + padding-right: 7px; +} +.vertical-tab__menu-item--child-error:not(.is-selected) .vertical-tabs__menu-item-title:before { + content: ''; + display: inline-block; + height: 20px; + width: 14px; + background: url(../../../../misc/icons/e32700/error.svg) no-repeat center; + background-size: contain; + float: right; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) .vertical-tabs__menu-item-title:before { + float: left; +} diff --git a/core/themes/seven/css/components/details.css b/core/themes/seven/css/components/details.css new file mode 100644 index 0000000..07d33be --- /dev/null +++ b/core/themes/seven/css/components/details.css @@ -0,0 +1,33 @@ +/** + * @file + * Collapsible details. + * + * @see collapse.js + */ + +/* Errors */ +.details--child-error:not([open]) summary:before { + content: ''; + display: inline-block; + height: 16px; + width: 14px; + background: url(../../../../misc/icons/e32700/error.svg) no-repeat center; + background-size: contain; + float: right; /* LTR */ +} +[dir="rtl"] .details--child-error:not([open]) summary:before { + float: left; +} +.details--child-error:not([open]) { + border: 1px solid #e62600; + box-shadow: inset 7px 0 0 #e62600; /* LTR */ + border-radius: 2px 0 0 2px; /* LTR */ + padding-left: 7px; /* LTR */ + color: #e62600; +} +[dir="rtl"] .details--child-error:not([open]) { + box-shadow: inset -7px 0 0 #e62600; + border-radius: 0 2px 2px 0; + padding-left: 0; + padding-right: 7px; +} diff --git a/core/themes/seven/css/components/entity-meta.css b/core/themes/seven/css/components/entity-meta.css index 701e8dc..a0f6151 100644 --- a/core/themes/seven/css/components/entity-meta.css +++ b/core/themes/seven/css/components/entity-meta.css @@ -57,3 +57,21 @@ .entity-meta details .summary { display: none; /* Hide JS summaries. @todo Rethink summaries. */ } +.entity-meta .details--child-error:not([open]) { + padding-left: 6px; +} +.entity-meta .details--child-error:not([open]) > * { + position: relative; + left: -7px; + margin-right: -12px; +} +[dir="rtl"] .entity-meta details.error:not([open]) { + padding-left: 0; + padding-right: 7px; +} +[dir="rtl"] .entity-meta details.error:not([open]) > * { + right: -8px; + left: auto; + margin-left: -12px; + margin-right: 0; +} diff --git a/core/themes/seven/css/components/vertical-tabs.css b/core/themes/seven/css/components/vertical-tabs.css index 34b5a52..cefae6d 100644 --- a/core/themes/seven/css/components/vertical-tabs.css +++ b/core/themes/seven/css/components/vertical-tabs.css @@ -37,6 +37,44 @@ .vertical-tabs__menu-item.last { border-bottom: none; } +.vertical-tab__menu-item--child-error.is-selected { + padding-top: 1px; +} +.vertical-tab__menu-item--child-error:not(.is-selected) { + border: 1px solid #e62600; + border-right: 0; + border-bottom-color: #ccc; + box-shadow: inset 7px 0 0 #e62600; /* LTR */ + border-radius: 2px 0 0 2px; /* LTR */ + padding-left: 7px; /* LTR */ +} +.vertical-tab__menu-item--child-error:not(.is-selected) a { + padding-left: 7px; /* LTR */ + padding-right: 10px; + border-bottom: 1px solid #e62600; + color: #e62600; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) { + box-shadow: inset -7px 0 0 #e62600; + border-radius: 0 2px 2px 0; + padding-left: 0; + padding-right: 7px; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) a { + padding-right: 7px; +} +.vertical-tab__menu-item--child-error:not(.is-selected) .vertical-tabs__menu-item-title:before { + content: ''; + display: inline-block; + height: 14px; + width: 14px; + background: url(../../../../misc/icons/e32700/error.svg) no-repeat center; + background-size: contain; + float: right; +} +[dir="rtl"] .vertical-tab__menu-item--child-error:not(.is-selected) .vertical-tabs__menu-item-title:before { + float: left; +} [dir="rtl"] .vertical-tabs__menu-item.is-selected { border-left: 1px solid #fcfcfa; border-right: none; diff --git a/core/themes/seven/seven.libraries.yml b/core/themes/seven/seven.libraries.yml index 21ac5bc..61e3154 100644 --- a/core/themes/seven/seven.libraries.yml +++ b/core/themes/seven/seven.libraries.yml @@ -12,6 +12,7 @@ global-styling: css/components/buttons.css: {} css/components/colors.css: {} css/components/messages.css: {} + css/components/details.css: {} css/components/dropbutton.component.css: {} css/components/entity-meta.css: {} css/components/field-ui.css: {}