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/book/config/schema/book.schema.yml b/core/modules/book/config/schema/book.schema.yml index 97d2f93..845a970 100644 --- a/core/modules/book/config/schema/book.schema.yml +++ b/core/modules/book/config/schema/book.schema.yml @@ -32,3 +32,6 @@ block.settings.book_navigation: block_mode: type: string label: 'Block display mode' + top_level_title: + type: boolean + label: 'Use the top-level page title as block title' diff --git a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php index e55fe36..bf5bad7 100644 --- a/core/modules/book/src/Plugin/Block/BookNavigationBlock.php +++ b/core/modules/book/src/Plugin/Block/BookNavigationBlock.php @@ -105,7 +105,15 @@ public function blockForm($form, FormStateInterface $form_state) { '#options' => $options, '#default_value' => $this->configuration['block_mode'], '#description' => $this->t("If Show block on all pages is selected, the block will contain the automatically generated menus for all of the site's books. If Show block only on book pages is selected, the block will contain only the one menu corresponding to the current page's book. In this case, if the current page is not in a book, no block will be displayed. The Page specific visibility settings or other visibility settings can be used in addition to selectively display this block."), - ]; + ]; + $form['top_level_title'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Use top-level page title as block title'), + '#default_value' => $this->configuration['top_level_title'], + '#states' => [ + 'visible' => [':input[name="settings[book_block_mode]"]' => ['value' => 'book pages']], + ] + ]; return $form; } @@ -115,6 +123,7 @@ public function blockForm($form, FormStateInterface $form_state) { */ public function blockSubmit($form, FormStateInterface $form_state) { $this->configuration['block_mode'] = $form_state->getValue('book_block_mode'); + $this->configuration['top_level_title'] = $form_state->getValue('top_level_title'); } /** @@ -171,6 +180,9 @@ public function build() { $data = array_shift($tree); $below = $this->bookManager->bookTreeOutput($data['below']); if (!empty($below)) { + if ($this->configuration['top_level_title']) { + $below['#title'] = $data['link']['title']; + } return $below; } } diff --git a/core/modules/book/tests/src/Functional/BookTest.php b/core/modules/book/tests/src/Functional/BookTest.php index 66edec9..a9e96b7 100644 --- a/core/modules/book/tests/src/Functional/BookTest.php +++ b/core/modules/book/tests/src/Functional/BookTest.php @@ -439,6 +439,58 @@ public function testBookNavigationBlock() { } /** + * Tests the top-level page title setting of the book navigation block. + */ + function testBookNavigationBlockWithTopLevelPageTitle() { + $this->drupalLogin($this->adminUser); + + // Enable the block. + $block = $this->drupalPlaceBlock('book_navigation', ['block_mode' => 'book pages', 'top_level_title' => TRUE]); + + // Give anonymous users the permission 'node test view'. + $edit = []; + $edit[RoleInterface::ANONYMOUS_ID . '[node test view]'] = TRUE; + $this->drupalPostForm('admin/people/permissions/' . RoleInterface::ANONYMOUS_ID, $edit, t('Save permissions')); + $this->assertText(t('The changes have been saved.'), "Permission 'node test view' successfully assigned to anonymous users."); + + /* + * Create a book with three nested pages: + * Book + * |- Node 0 + * |- Node 1 + * |- Node 2 + */ + $this->drupalLogin($this->bookAuthor); + $book = $this->createBookNode('new'); + $pid = NULL; + for ($i = 0 ; $i < 3 ; ++$i) { + $node = $this->createBookNode($book->id(), $pid); + $pid = $node->id(); + } + $this->drupalLogout(); + + // Change the book top-level title. + $book->title = 'Top-level node title'; + $book->save(); + + // Check that the block title is the top-level page title on the book + // summary. + $this->drupalGet('node/' . $book->id()); + $xpath = $this->buildXPathQuery('//div[@id=:block-id]/h2', [ + ':block-id' => 'block-' . str_replace('_', '-', strtolower($block->id())), + ]); + $this->assertFieldByXPath($xpath, $book->title->value, t('Block title is the same as book top-level page title.')); + + // Check that the block title is the top-level page title on a deep book + // page. + $this->drupalGet('node/' . $node->id()); + $xpath = $this->buildXPathQuery('//div[@id=:block-id]/h2', [ + ':block-id' => 'block-' . str_replace('_', '-', strtolower($block->id())), + ]); + $this->assertFieldByXPath($xpath, $book->title->value, t('Block title is the same as book top-level page title.')); + } + + /** * Tests BookManager::getTableOfContents(). */ public function testGetTableOfContents() { 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 %}