From b83554bac17f0fa7e57b2b914dd17d4b1e7d9b19 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 10:56:30 +0100
Subject: [PATCH 1/8] Apply patch from comment 32

---
 core/lib/Drupal/Core/Form/FormBuilder.php     | 65 +++++++++++++++++++
 .../Drupal/Core/Form/FormBuilderInterface.php | 15 +++++
 2 files changed, 80 insertions(+)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index ee3e4893feca..3b7114270394 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,69 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function addAsteriskExplanation($form_id, array &$form) {
+
+    $form_note_identifier = $form_id . '_required_fields_note';
+
+    foreach ($form as $form_key => $form_item) {
+      if (strpos($form_key, '#') === 0) {
+        // 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' => 'markup',
+              '#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' => INF,
+            ];
+
+            return;
+          }
+        }
+      }
+    }
+    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 1a1b22403677..c875c03103d1 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -91,6 +91,21 @@ 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.
+   *
+   * @return array
+   *   The rendered form.
+   */
+  public function addAsteriskExplanation($form_id, array &$form);
+
   /**
    * Constructs a new $form from the information in $form_state.
    *
-- 
GitLab


From 4bdc18e777cad63ecb0bd2142899f3d56c9c72a4 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:01:03 +0100
Subject: [PATCH 2/8] Move explanation to the beginning of the form

---
 core/lib/Drupal/Core/Form/FormBuilder.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 3b7114270394..d2385c79e490 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -670,7 +670,7 @@ public function addAsteriskExplanation($form_id, array &$form) {
                 '@strong-markup' => new TranslatableMarkup('Note'),
                 '@label-markup' => new TranslatableMarkup('Required fields are marked with an asterisk'),
               ]),
-              '#weight' => INF,
+              '#weight' => -INF,
             ];
 
             return;
-- 
GitLab


From b2212f4299eaad678e6ef7b7c65443acbcfd259c Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:02:49 +0100
Subject: [PATCH 3/8] Fix return type

The return value is never used
---
 core/lib/Drupal/Core/Form/FormBuilder.php          | 5 ++---
 core/lib/Drupal/Core/Form/FormBuilderInterface.php | 5 +----
 2 files changed, 3 insertions(+), 7 deletions(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index d2385c79e490..f73e62e95902 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -643,12 +643,12 @@ public function processForm($form_id, &$form, FormStateInterface &$form_state) {
   /**
    * {@inheritdoc}
    */
-  public function addAsteriskExplanation($form_id, array &$form) {
+  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 (strpos($form_key, '#') === 0) {
+      if (str_starts_with($form_key, '#')) {
         // We'll skip over the special fields.
         continue;
       }
@@ -678,7 +678,6 @@ public function addAsteriskExplanation($form_id, array &$form) {
         }
       }
     }
-    return [];
   }
 
   /**
diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index c875c03103d1..52e44600fb65 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -100,11 +100,8 @@ public function buildForm($form_arg, FormStateInterface &$form_state);
    *   The unique string identifying the desired form.
    * @param array $form
    *   An associative array containing the structure of the form.
-   *
-   * @return array
-   *   The rendered form.
    */
-  public function addAsteriskExplanation($form_id, array &$form);
+  public function addAsteriskExplanation(string $form_id, array &$form): void;
 
   /**
    * Constructs a new $form from the information in $form_state.
-- 
GitLab


From 9e55f2a6b07af6981b4bc0d2bf568d935a0de233 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:11:38 +0100
Subject: [PATCH 4/8] Add test

---
 .../Tests/Core/Form/FormBuilderTest.php       | 21 +++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index d7a364170f04..3ef88d25a2d9 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
+   */
+  function testAddAsteriskExplanation() {
+    $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('markup', $form['test_form_id_required_fields_note']['#type']);
+    $this->assertEquals(-INF, $form['test_form_id_required_fields_note']['#weight']);
+    $this->assertInstanceOf(TranslatableMarkup::class, $form['test_form_id_required_fields_note']['#markup']);
+  }
+
 }
 
 /**
-- 
GitLab


From 6cd80ab3265ebeebb33ffb9135b738397607b28e Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:19:09 +0100
Subject: [PATCH 5/8] Lint

---
 core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
index 3ef88d25a2d9..268a8f9becd4 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -1006,7 +1006,7 @@ public static function providerTestFormTokenCacheability() {
   /**
    * @covers ::addAsteriskExplanation
    */
-  function testAddAsteriskExplanation() {
+  public function testAddAsteriskExplanation(): void {
     $form_id = 'test_form_id';
 
     // Tests without a required field.
-- 
GitLab


From 39590754bf605a9f7ee345eed3a6e519ffafdb26 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:44:58 +0100
Subject: [PATCH 6/8] Weight can't be a float

---
 core/lib/Drupal/Core/Form/FormBuilder.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index f73e62e95902..1ec6510d0951 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -670,7 +670,7 @@ public function addAsteriskExplanation(string $form_id, array &$form): void {
                 '@strong-markup' => new TranslatableMarkup('Note'),
                 '@label-markup' => new TranslatableMarkup('Required fields are marked with an asterisk'),
               ]),
-              '#weight' => -INF,
+              '#weight' => -1000,
             ];
 
             return;
-- 
GitLab


From f9df8a5d31302a3b7caefe7269280b4304d88017 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Fri, 14 Mar 2025 11:50:38 +0100
Subject: [PATCH 7/8] Fix related test

---
 core/lib/Drupal/Core/Form/FormBuilder.php                     | 2 +-
 .../config/tests/src/Functional/ConfigFormOverrideTest.php    | 2 +-
 core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php         | 4 ++--
 3 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php
index 1ec6510d0951..0f38132a975f 100644
--- a/core/lib/Drupal/Core/Form/FormBuilder.php
+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -665,7 +665,7 @@ public function addAsteriskExplanation(string $form_id, array &$form): void {
         else {
           if (isset($form_item['#required']) && (bool) $form_item['#required'] === TRUE) {
             $form[$form_note_identifier] = [
-              '#type' => 'markup',
+              '#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'),
diff --git a/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php b/core/modules/config/tests/src/Functional/ConfigFormOverrideTest.php
index a693d3072383..1eb45e3d1e6e 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 268a8f9becd4..a3502bc839b4 100644
--- a/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
+++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
@@ -1018,8 +1018,8 @@ public function testAddAsteriskExplanation(): void {
     $form = $form_id();
     $form['test']['#required'] = TRUE;
     $this->formBuilder->addAsteriskExplanation($form_id, $form);
-    $this->assertEquals('markup', $form['test_form_id_required_fields_note']['#type']);
-    $this->assertEquals(-INF, $form['test_form_id_required_fields_note']['#weight']);
+    $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']);
   }
 
-- 
GitLab


From 33b821e03091d579eba9707effca50d17489a1f7 Mon Sep 17 00:00:00 2001
From: Pierre Rudloff <contact@rudloff.pro>
Date: Mon, 24 Mar 2025 20:55:25 +0100
Subject: [PATCH 8/8] Comment is too long

---
 core/lib/Drupal/Core/Form/FormBuilderInterface.php | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/core/lib/Drupal/Core/Form/FormBuilderInterface.php b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
index 52e44600fb65..d7a6fb3fe13f 100644
--- a/core/lib/Drupal/Core/Form/FormBuilderInterface.php
+++ b/core/lib/Drupal/Core/Form/FormBuilderInterface.php
@@ -94,7 +94,8 @@ 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.
+   * 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.
-- 
GitLab