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..cc9ea5c 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: {}