diff --git a/core/includes/form.inc b/core/includes/form.inc
index 05f2a9e..9865e2f 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -240,6 +240,8 @@ function template_preprocess_fieldset(&$variables) {
*/
function template_preprocess_details(&$variables) {
$element = $variables['element'];
+ Element::setAttributes($element, ['id']);
+ RenderElement::setAttributes($element);
$variables['attributes'] = $element['#attributes'];
$variables['summary_attributes'] = new Attribute();
if (!empty($element['#title'])) {
@@ -251,7 +253,18 @@ function template_preprocess_details(&$variables) {
$variables['summary_attributes']['aria-pressed'] = $variables['summary_attributes']['aria-expanded'];
}
$variables['title'] = (!empty($element['#title'])) ? $element['#title'] : '';
- $variables['description'] = (!empty($element['#description'])) ? $element['#description'] : '';
+
+ if (!empty($element['#description'])) {
+ $variables['description_display'] = isset($element['#description_display']) ? $element['#description_display'] : 'before';
+ $description_id = $element['#attributes']['id'] . '--description';
+ $description_attributes['id'] = $description_id;
+ $variables['description']['attributes'] = new Attribute($description_attributes);
+ $variables['description']['content'] = $element['#description'];
+
+ // Add the description's id to the fieldset aria attributes.
+ $variables['attributes']['aria-describedby'] = $description_id;
+ }
+
$variables['children'] = (isset($element['#children'])) ? $element['#children'] : '';
$variables['value'] = (isset($element['#value'])) ? $element['#value'] : '';
$variables['required'] = !empty($element['#required']) ? $element['#required'] : NULL;
diff --git a/core/lib/Drupal/Core/Render/Element/Details.php b/core/lib/Drupal/Core/Render/Element/Details.php
index 92bbb32..fb3e74c 100644
--- a/core/lib/Drupal/Core/Render/Element/Details.php
+++ b/core/lib/Drupal/Core/Render/Element/Details.php
@@ -44,6 +44,7 @@ public function getInfo() {
return [
'#open' => FALSE,
'#value' => NULL,
+ '#description_display' => 'before',
'#process' => [
[$class, 'processGroup'],
[$class, 'processAjaxForm'],
diff --git a/core/modules/system/src/Tests/Form/ElementsDetailsTest.php b/core/modules/system/src/Tests/Form/ElementsDetailsTest.php
new file mode 100644
index 0000000..48c3bae
--- /dev/null
+++ b/core/modules/system/src/Tests/Form/ElementsDetailsTest.php
@@ -0,0 +1,54 @@
+drupalGet('form-test/group-details');
+
+ // Check #description default placement.
+ $field_id = 'edit-meta';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div/preceding-sibling::div');
+ $this->assertTrue(isset($elements[0]), 'Properly places by default the #description element before the form item within details group.');
+
+ // Check #description placement with #description_display='before'.
+ $field_id = 'edit-details-before';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-before"]/preceding-sibling::div[@id="' . $description_id . '"]');
+ $this->assertTrue(isset($elements[0]), 'Properly places the #description element before the form item within details group.');
+
+ // Check #description placement with #description_display='after'.
+ $field_id = 'edit-details-after';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-after"]/following-sibling::div[@id="' . $description_id . '"]');
+ $this->assertTrue(isset($elements[0]), 'Properly places the #description element after the form item within details group.');
+
+ // Check if the class is 'visually-hidden' on the form details description
+ // for the option with #description_display='invisible' and also check that
+ // the description is placed after the form element.
+ $field_id = 'edit-details-invisible';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-invisible"]/following-sibling::div[contains(@class, "visually-hidden")]');
+ $this->assertTrue(isset($elements[0]), 'Properly renders the #description element visually-hidden within details group.');
+ }
+
+}
diff --git a/core/modules/system/src/Tests/Form/ElementsLabelsTest.php b/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
index 2a57e5d..549e8e4 100644
--- a/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
+++ b/core/modules/system/src/Tests/Form/ElementsLabelsTest.php
@@ -102,6 +102,13 @@ public function testFormLabels() {
public function testFormDescriptions() {
$this->drupalGet('form_test/form-descriptions');
+ // Check #description placement with no #description_display, so default is
+ // 'after' the form item.
+ $field_id = 'edit-form-textfield-test-description-default';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[@id="' . $description_id . '"]');
+ $this->assertTrue(isset($elements[0]), t('Properly places by default the #description element after the form item.'));
+
// Check #description placement with #description_display='after'.
$field_id = 'edit-form-textfield-test-description-after';
$description_id = $field_id . '--description';
@@ -121,6 +128,37 @@ public function testFormDescriptions() {
$description_id = $field_id . '--description';
$elements = $this->xpath('//input[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]/following-sibling::div[contains(@class, "visually-hidden")]');
$this->assertTrue(isset($elements[0]), t('Properly renders the #description element visually-hidden.'));
+
+
+ $this->drupalGet('form-test/group-details');
+
+ // Check #description default placement in a 'details' element.
+ $field_id = 'edit-meta';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div/preceding-sibling::div');
+ $this->assertTrue(isset($elements[0]), 'Properly places by default the #description element before the form item within details group.');
+
+ // Check #description placement with #description_display='before' in a
+ // 'details' element.
+ $field_id = 'edit-details-before';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-before"]/preceding-sibling::div[@id="' . $description_id . '"]');
+ $this->assertTrue(isset($elements[0]), 'Properly places the #description element before the form item within details group.');
+
+ // Check #description placement with #description_display='after' in a
+ // 'details' element.
+ $field_id = 'edit-details-after';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-after"]/following-sibling::div[@id="' . $description_id . '"]');
+ $this->assertTrue(isset($elements[0]), 'Properly places the #description element after the form item within details group.');
+
+ // Check if the class is 'visually-hidden' on the form details description
+ // for the option with #description_display='invisible' and also check that
+ // the description is placed after the form element.
+ $field_id = 'edit-details-invisible';
+ $description_id = $field_id . '--description';
+ $elements = $this->xpath('//details[@id="' . $field_id . '" and @aria-describedby="' . $description_id . '"]//div[@id="edit-meta-invisible"]/following-sibling::div[contains(@class, "visually-hidden")]');
+ $this->assertTrue(isset($elements[0]), 'Properly renders the #description element visually-hidden within details group.');
}
/**
diff --git a/core/modules/system/templates/details.html.twig b/core/modules/system/templates/details.html.twig
index 5014deb..298f2ad 100644
--- a/core/modules/system/templates/details.html.twig
+++ b/core/modules/system/templates/details.html.twig
@@ -8,6 +8,13 @@
* - errors: (optional) Any errors for this details element, may not be set.
* - title: (optional) The title of the element, may not be set.
* - description: (optional) The description of the element, may not be set.
+ * - description_display: (optional) Description display setting. It can have
+ * these values:
+ * - before: The description is output before the element. This is the default
+ * value.
+ * - after: The description is output after the element.
+ * - invisible: The description is output after the element, hidden visually
+ * but available to screen readers.
* - children: (optional) The children of the element, may not be set.
* - value: (optional) The value of the element, may not be set.
*
@@ -16,6 +23,12 @@
* @ingroup themeable
*/
#}
+{%
+ set description_classes = [
+ 'description',
+ description_display == 'invisible' ? 'visually-hidden',
+ ]
+%}
{%
set summary_classes = [
@@ -33,7 +46,18 @@
{% endif %}
- {{ description }}
+ {% if description_display == 'before' and description.content %}
+
+ {{ description.content }}
+
+ {% endif %}
+
{{ children }}
{{ value }}
+
+ {% if description_display in ['after', 'invisible'] and description.content %}
+
+ {{ description.content }}
+
+ {% endif %}
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php
index 81120ad..bb8286b 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestDescriptionForm.php
@@ -23,6 +23,12 @@ public function getFormId() {
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['form_textfield_test_description_default'] = [
+ '#type' => 'textfield',
+ '#title' => 'Textfield test for description default after element',
+ '#description' => 'Textfield test for description default after element',
+ ];
+
$form['form_textfield_test_description_before'] = [
'#type' => 'textfield',
'#title' => 'Textfield test for description before element',
diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php
index 002b07d..304edf4 100644
--- a/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php
+++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestGroupDetailsForm.php
@@ -30,6 +30,7 @@ public function buildForm(array $form, FormStateInterface $form_state, $required
$form['meta'] = [
'#type' => 'details',
'#title' => 'Group element',
+ '#description' => 'Details test for default description position.',
'#open' => TRUE,
'#group' => 'details',
];
@@ -37,6 +38,55 @@ public function buildForm(array $form, FormStateInterface $form_state, $required
'#type' => 'textfield',
'#title' => 'Nest in details element',
];
+
+ $form['details_before'] = [
+ '#type' => 'details',
+ '#title' => 'Details test for description before element',
+ '#description' => 'Details test for description before element.',
+ '#description_display' => 'before',
+ ];
+ $form['meta_before'] = [
+ '#type' => 'container',
+ '#title' => 'Group element',
+ '#group' => 'details_before',
+ ];
+ $form['meta_before']['element'] = [
+ '#type' => 'textfield',
+ '#title' => 'Nest in container element',
+ ];
+
+ $form['details_after'] = [
+ '#type' => 'details',
+ '#title' => 'Details test for description after element',
+ '#description' => 'Details test for description after element.',
+ '#description_display' => 'after',
+ ];
+ $form['meta_after'] = [
+ '#type' => 'container',
+ '#title' => 'Group element',
+ '#group' => 'details_after',
+ ];
+ $form['meta_after']['element'] = [
+ '#type' => 'textfield',
+ '#title' => 'Nest in container element',
+ ];
+
+ $form['details_invisible'] = [
+ '#type' => 'details',
+ '#title' => 'Details test for visually-hidden description',
+ '#description' => 'Details test for visually-hidden description.',
+ '#description_display' => 'invisible',
+ ];
+ $form['meta_invisible'] = [
+ '#type' => 'container',
+ '#title' => 'Group element',
+ '#group' => 'details_invisible',
+ ];
+ $form['meta_invisible']['element'] = [
+ '#type' => 'textfield',
+ '#title' => 'Nest in container element',
+ ];
+
return $form;
}
diff --git a/core/themes/classy/templates/form/details.html.twig b/core/themes/classy/templates/form/details.html.twig
index 24366c3..2a9669b 100644
--- a/core/themes/classy/templates/form/details.html.twig
+++ b/core/themes/classy/templates/form/details.html.twig
@@ -8,12 +8,25 @@
* - errors: (optional) Any errors for this details element, may not be set.
* - title: (optional) The title of the element, may not be set.
* - description: (optional) The description of the element, may not be set.
+ * - description_display: (optional) Description display setting. It can have
+ * these values:
+ * - before: The description is output before the element. This is the default
+ * value.
+ * - after: The description is output after the element.
+ * - invisible: The description is output after the element, hidden visually
+ * but available to screen readers.
* - children: (optional) The children of the element, may not be set.
* - value: (optional) The value of the element, may not be set.
*
* @see template_preprocess_details()
*/
#}
+{%
+ set description_classes = [
+ 'description',
+ description_display == 'invisible' ? 'visually-hidden',
+ ]
+%}
{%- if title -%}
{%
@@ -30,14 +43,21 @@
{{ errors }}
{% endif %}
- {%- if description -%}
- {{ description }}
- {%- endif -%}
+ {% if description_display == 'before' and description.content %}
+
+ {{ description.content }}
+
+ {% endif %}
{%- if children -%}
{{ children }}
{%- endif -%}
{%- if value -%}
{{ value }}
{%- endif -%}
+ {% if description_display in ['after', 'invisible'] and description.content %}
+
+ {{ description.content }}
+
+ {% endif %}
diff --git a/core/themes/stable/templates/form/details.html.twig b/core/themes/stable/templates/form/details.html.twig
index ccd2796..3c3fba7 100644
--- a/core/themes/stable/templates/form/details.html.twig
+++ b/core/themes/stable/templates/form/details.html.twig
@@ -14,6 +14,12 @@
* @see template_preprocess_details()
*/
#}
+{%
+ set description_classes = [
+ 'description',
+ description_display == 'invisible' ? 'visually-hidden',
+ ]
+%}
{%
set summary_classes = [
@@ -31,7 +37,11 @@
{% endif %}
- {{ description }}
+ {% if description.content %}
+
+ {{ description.content }}
+
+ {% endif %}
{{ children }}
{{ value }}