diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index ee3e4893feca035c00a95603554569bb00c3d052..0f38132a975f62edcca9bced990b90310b077eb6 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -18,6 +18,7 @@ use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\HttpFoundation\FileBag; use Symfony\Component\HttpFoundation\InputBag; use Symfony\Component\HttpFoundation\RequestStack; @@ -277,6 +278,7 @@ public function buildForm($form_arg, FormStateInterface &$form_state) { } $form = $this->retrieveForm($form_id, $form_state); + $this->addAsteriskExplanation($form_id, $form); $this->prepareForm($form_id, $form, $form_state); // self::setCache() removes uncacheable $form_state keys (see properties @@ -638,6 +640,68 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) { } } + /** + * {@inheritdoc} + */ + public function addAsteriskExplanation(string $form_id, array &$form): void { + + $form_note_identifier = $form_id . '_required_fields_note'; + + foreach ($form as $form_key => $form_item) { + if (str_starts_with($form_key, '#')) { + // We'll skip over the special fields. + continue; + } + else { + // Check if type is primitive. + if (isset($form_item['#type']) && $this->isComplexElement($form_item['#type'])) { + $this->addAsteriskExplanation($form_id, $form_item); + if (isset($form_item[$form_note_identifier])) { + $form[$form_note_identifier] = $form_item[$form_note_identifier]; + unset($form_item[$form_note_identifier]); + return; + } + } + else { + if (isset($form_item['#required']) && (bool) $form_item['#required'] === TRUE) { + $form[$form_note_identifier] = [ + '#type' => 'container', + '#markup' => new TranslatableMarkup('<strong>@strong-markup: </strong><label>@label-markup *.</label>', [ + '@strong-markup' => new TranslatableMarkup('Note'), + '@label-markup' => new TranslatableMarkup('Required fields are marked with an asterisk'), + ]), + '#weight' => -1000, + ]; + + return; + } + } + } + } + } + + /** + * Checks to see if a specified type is used for grouping elements. + * + * @param string $type + * String representation of the type. + * + * @return bool + * Is the type in the array? + */ + public function isComplexElement($type) { + return in_array($type, [ + 'actions', + 'container', + 'details', + 'dropbutton', + 'fieldgroup', + 'fieldset', + 'form', + 'operations', + ]); + } + /** * Renders a form action URL. It's a #lazy_builder callback. * diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php index 1a1b2240367780e1fa5a60b54d8966429f687349..d7a6fb3fe13fc4708f38af646ebdc97085a0cf0a 100644 --- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php +++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php @@ -91,6 +91,19 @@ public function getForm($form_arg, mixed ...$args); */ public function buildForm($form_arg, FormStateInterface &$form_state); + /** + * Checks the form to see if any of the fields are required. + * + * If there is a required field + * it adds a text explaining what the asterisk means. + * + * @param string $form_id + * The unique string identifying the desired form. + * @param array $form + * An associative array containing the structure of the form. + */ + public function addAsteriskExplanation(string $form_id, array &$form): void; + /** * Constructs a new $form from the information in $form_state. * diff --git a/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php b/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php index a693d307238387cfba3c74ebc7dcfaf84c36cac1..1eb45e3d1e6edfa28a9c378a0f82ecf730109f9d 100644 --- a/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php +++ b/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php @@ -70,7 +70,7 @@ public function testFormsWithOverrides(): void { $this->assertSession()->titleEquals('Basic site settings | ' . $overridden_name); $this->assertSession()->elementTextContains('css', 'div[data-drupal-messages]', self::OVERRIDE_TEXT); // Ensure the configuration overrides message is at the top of the form. - $this->assertSession()->elementExists('css', 'div[data-drupal-messages] + details#edit-site-information'); + $this->assertSession()->elementExists('css', 'div[data-drupal-messages] + div#edit-system-site-information-settings-required-fields-note'); $this->assertSession()->elementContains('css', 'div[data-drupal-messages]', '<a href="#edit-site-name" title="\'Site name\' form element">Site name</a>'); $this->assertSession()->fieldValueEquals("site_name", 'Drupal'); $this->submitForm([ diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php index d7a364170f040c938fb689523ef5359d979e3780..a3502bc839b4937f009e30db497dba97e046c6e9 100644 --- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php @@ -19,6 +19,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -1002,6 +1003,26 @@ public static function providerTestFormTokenCacheability() { ]; } + /** + * @covers ::addAsteriskExplanation + */ + public function testAddAsteriskExplanation(): void { + $form_id = 'test_form_id'; + + // Tests without a required field. + $form = $form_id(); + $this->formBuilder->addAsteriskExplanation($form_id, $form); + $this->assertArrayNotHasKey('test_form_id_required_fields_note', $form); + + // Tests with a required field. + $form = $form_id(); + $form['test']['#required'] = TRUE; + $this->formBuilder->addAsteriskExplanation($form_id, $form); + $this->assertEquals('container', $form['test_form_id_required_fields_note']['#type']); + $this->assertEquals(-1000, $form['test_form_id_required_fields_note']['#weight']); + $this->assertInstanceOf(TranslatableMarkup::class, $form['test_form_id_required_fields_note']['#markup']); + } + } /**