From 7fe372c75a04a1e0785b1f33bd225a8144b8a88d Mon Sep 17 00:00:00 2001
From: Pierre Dureau <31905-pdureau@users.noreply.drupalcode.org>
Date: Thu, 8 Aug 2024 12:05:32 +0000
Subject: [PATCH] Issue #3461277 by pdureau, Christian.wiedemann:  Sources
 don't support required - #title missing

---
 .../src/ComponentConverter.php                | 17 ++++++++
 .../Commands/UiPatternsLegacyCommands.php     | 42 ++++++++++---------
 .../ui_patterns_legacy.services.yml           |  1 +
 src/ComponentPluginManager.php                | 29 +++++++++----
 src/Element/ComponentPropForm.php             | 21 ++++++++++
 .../UiPatterns/Source/AttributesWidget.php    | 11 ++---
 .../UiPatterns/Source/CheckboxWidget.php      | 10 ++---
 .../UiPatterns/Source/CheckboxesWidget.php    | 13 +++---
 .../UiPatterns/Source/EntityLinksSource.php   |  8 ++--
 .../UiPatterns/Source/ListTextareaWidget.php  | 13 +++---
 src/Plugin/UiPatterns/Source/MenuSource.php   | 10 ++---
 src/Plugin/UiPatterns/Source/NumberWidget.php | 15 +++----
 src/Plugin/UiPatterns/Source/PathSource.php   | 12 +++---
 src/Plugin/UiPatterns/Source/SelectWidget.php |  9 ++--
 .../UiPatterns/Source/TextfieldWidget.php     | 18 ++++----
 src/Plugin/UiPatterns/Source/TokenSource.php  | 16 +++----
 src/Plugin/UiPatterns/Source/UrlWidget.php    | 13 +++---
 .../UiPatterns/Source/WysiwygWidget.php       |  4 +-
 src/PropTypePluginBase.php                    |  3 ++
 src/SourcePluginBase.php                      | 19 ++++++++-
 .../src/Plugin/UiPatterns/Source/Foo.php      | 13 +++---
 21 files changed, 191 insertions(+), 106 deletions(-)

diff --git a/modules/ui_patterns_legacy/src/ComponentConverter.php b/modules/ui_patterns_legacy/src/ComponentConverter.php
index 05107f506..eb63c7578 100644
--- a/modules/ui_patterns_legacy/src/ComponentConverter.php
+++ b/modules/ui_patterns_legacy/src/ComponentConverter.php
@@ -69,6 +69,10 @@ class ComponentConverter {
         "type" => 'object',
         "properties" => $this->getPropsFromSettings($source['settings']),
       ];
+      $required_props = $this->getRequiredPropsFromSettings($source['settings']);
+      if (!empty($required_props)) {
+        $target['props']['required'] = $required_props;
+      }
     }
     $extractor = new StoryExtractor($this->componentPluginManager);
     $extractor->setExtension($this->extension);
@@ -164,6 +168,19 @@ class ComponentConverter {
     return $props;
   }
 
+  /**
+   * Get required props from UI Patterns 1.x settings.
+   */
+  private function getRequiredPropsFromSettings(array $settings): array {
+    $props = [];
+    foreach ($settings as $setting_id => $setting) {
+      if (\array_key_exists('required', $setting) && $setting["required"]) {
+        $props[] = $setting_id;
+      }
+    }
+    return $props;
+  }
+
   /**
    * A small helper to convert a property.
    */
diff --git a/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php b/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
index fd9d60fda..f8ac162aa 100644
--- a/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
+++ b/modules/ui_patterns_legacy/src/Drush/Commands/UiPatternsLegacyCommands.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 namespace Drupal\ui_patterns_legacy\Drush\Commands;
 
 use Drupal\Component\Plugin\Discovery\CachedDiscoveryInterface;
@@ -7,46 +9,51 @@ use Drupal\ui_patterns_legacy\ComponentConverter;
 use Drupal\ui_patterns_legacy\ComponentDiscovery;
 use Drupal\ui_patterns_legacy\ComponentWriter;
 use Drush\Attributes as CLI;
+use Drush\Commands\AutowireTrait;
 use Drush\Commands\DrushCommands;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\DependencyInjection\Attribute\Autowire;
 use Symfony\Component\Finder\Finder;
 
 /**
  * Drush commands of UI Patterns Legacy module.
  */
 final class UiPatternsLegacyCommands extends DrushCommands {
+  use AutowireTrait;
 
   /**
    * {@inheritdoc}
    */
   public function __construct(
+    #[Autowire(service: 'ui_patterns_legacy.component_converter')]
     private readonly ComponentConverter $converter,
+    #[Autowire(service: 'ui_patterns_legacy.discovery')]
     private readonly ComponentDiscovery $discovery,
+    #[Autowire(service: 'ui_patterns_legacy.component_writer')]
     private readonly ComponentWriter $writer,
+    #[Autowire(service: 'plugin.manager.sdc')]
     private readonly CachedDiscoveryInterface $componentsManager,
   ) {
     parent::__construct();
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container): static {
-    return new static(
-      $container->get('ui_patterns_legacy.component_converter'),
-      $container->get('ui_patterns_legacy.discovery'),
-      $container->get('ui_patterns_legacy.component_writer'),
-      $container->get('plugin.manager.sdc')
-    );
-  }
-
   /**
    * Migrate components from UI Patterns 1.x to UI Patterns 2.x.
    */
   #[CLI\Command(name: 'ui-patterns:migrate', aliases: ['upm'])]
   #[CLI\Argument(name: 'extension', description: 'Module or theme machine name.')]
-  #[CLI\Usage(name: 'ui-patterns:migrate my_theme', description: 'Migrate components, replace dependencies and API calls.')]
+  #[CLI\Usage(name: 'ui-patterns:migrate my_theme', description: 'Convert components, replace dependencies and API calls.')]
   public function migrate(string $extension): void {
+    $this->convertComponents($extension);
+    $extension_path = $this->discovery->getExtensionPath($extension);
+    $this->replaceDeprecatedCalls($extension_path);
+    $this->changeInfoFile($extension, $extension_path);
+    $this->changeComposerFile($extension_path);
+  }
+
+  /**
+   * Convert components.
+   */
+  protected function convertComponents(string $extension): void {
     $legacy_definitions = $this->discovery->discover($extension);
     $extension_path = $this->discovery->getExtensionPath($extension);
     $this->converter->setExtension($extension);
@@ -72,13 +79,9 @@ final class UiPatternsLegacyCommands extends DrushCommands {
       }
       $errors = $this->converter->validate($component);
       foreach ($errors as $error) {
-        $this->io()->error($error);
+        $this->logger()->error($error);
       }
     }
-    $extension_path = $this->discovery->getExtensionPath($extension);
-    $this->replaceDeprecatedCalls($extension_path);
-    $this->changeInfoFile($extension, $extension_path);
-    $this->changeComposerFile($extension_path);
   }
 
   /**
@@ -90,6 +93,7 @@ final class UiPatternsLegacyCommands extends DrushCommands {
    */
   protected function replaceDeprecatedCalls(string $extension_path): void {
     $finder = new Finder();
+
     $patterns = ['*.php', '*.inc', '*.module', '*.theme'];
     $finder->files()->name($patterns)->in($extension_path);
     foreach ($finder as $file) {
diff --git a/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml b/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
index dd33f279a..6a8191089 100644
--- a/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
+++ b/modules/ui_patterns_legacy/ui_patterns_legacy.services.yml
@@ -13,6 +13,7 @@ services:
     class: Drupal\ui_patterns_legacy\ComponentConverter
     arguments:
       - "@renderer"
+      - "@plugin.manager.sdc"
   ui_patterns_legacy.component_writer:
     class: Drupal\ui_patterns_legacy\ComponentWriter
   ui_patterns_legacy.component_element_alter:
diff --git a/src/ComponentPluginManager.php b/src/ComponentPluginManager.php
index 768b7cc2f..17d0577e7 100644
--- a/src/ComponentPluginManager.php
+++ b/src/ComponentPluginManager.php
@@ -73,6 +73,7 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     // Adding custom logic.
     $fallback_prop_type_id = $this->propTypePluginManager->getFallbackPluginId("");
     $definition = $this->alterSlots($definition);
+    $definition = $this->annotateSlots($definition);
     $definition = $this->annotateProps($definition, $fallback_prop_type_id);
     return $definition;
   }
@@ -91,6 +92,21 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     return $definition;
   }
 
+  /**
+   * Annotate each slot in a component definition.
+   */
+  protected function annotateSlots(array $definition): array {
+    if (empty($definition['slots'])) {
+      return $definition;
+    }
+    $slot_prop_type = $this->propTypePluginManager->createInstance('slot', []);
+    foreach ($definition['slots'] as $slot_id => $slot) {
+      $slot['ui_patterns']['type_definition'] = $slot_prop_type;
+      $definition['slots'][$slot_id] = $slot;
+    }
+    return $definition;
+  }
+
   /**
    * Annotate each prop in a component definition.
    *
@@ -98,6 +114,12 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
    * We add a 'ui_patterns' object in each prop schema of the definition.
    */
   protected function annotateProps(array $definition, string $fallback_prop_type_id): array {
+    // In JSON schema, 'required' is out of the prop definition.
+    if (isset($definition['props']['required'])) {
+      foreach ($definition['props']['required'] as $prop_id) {
+        $definition['props']['properties'][$prop_id]['ui_patterns']['required'] = TRUE;
+      }
+    }
     if (!isset($definition['props']['properties'])) {
       return $definition;
     }
@@ -107,13 +129,6 @@ class ComponentPluginManager extends SdcPluginManager implements CategorizingPlu
     foreach ($definition['props']['properties'] as $prop_id => $prop) {
       $definition['props']['properties'][$prop_id] = $this->annotateProp($prop_id, $prop, $fallback_prop_type_id);
     }
-    $slot_prop_type = $this->propTypePluginManager->createInstance('slot', []);
-    if (!empty($definition['slots'])) {
-      foreach ($definition['slots'] as $slot_id => $slot) {
-        $slot['ui_patterns']['type_definition'] = $slot_prop_type;
-        $definition['slots'][$slot_id] = $slot;
-      }
-    }
     return $definition;
   }
 
diff --git a/src/Element/ComponentPropForm.php b/src/Element/ComponentPropForm.php
index 767510be8..e21838ff3 100644
--- a/src/Element/ComponentPropForm.php
+++ b/src/Element/ComponentPropForm.php
@@ -93,6 +93,27 @@ class ComponentPropForm extends ComponentFormBase {
       'source' => $source_form,
     ];
     $element['#attributes']['style'] = 'position: relative;';
+    $element = static::addRequired($element, $prop_id);
+    return $element;
+  }
+
+  /**
+   * Add required visual clue to the fieldset.
+   *
+   * The proper required control is managed by SourcePluginBase::addRequired()
+   * so the visual clue is present whether or not the control is done by the
+   * source plugin. This is feature, not a bug.
+   */
+  protected static function addRequired(array $element, string $prop_id): array {
+    $component = static::getComponent($element);
+    if (!isset($component->metadata->schema["required"])) {
+      return $element;
+    }
+    $required_props = $component->metadata->schema["required"];
+    if (!in_array($prop_id, $required_props)) {
+      return $element;
+    }
+    $element["#required"] = TRUE;
     return $element;
   }
 
diff --git a/src/Plugin/UiPatterns/Source/AttributesWidget.php b/src/Plugin/UiPatterns/Source/AttributesWidget.php
index d78ad4aab..6e0650d3b 100644
--- a/src/Plugin/UiPatterns/Source/AttributesWidget.php
+++ b/src/Plugin/UiPatterns/Source/AttributesWidget.php
@@ -39,11 +39,11 @@ class AttributesWidget extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
+    $form = parent::settingsForm($form, $form_state);
     // Attributes are associative arrays, but this source plugin is storing
     // them as string in config.
     // It would be better to use something else than a textfield one day.
-    $build['value'] = [
+    $form['value'] = [
       '#type' => 'textfield',
       '#default_value' => $this->getSetting('value'),
     ];
@@ -52,9 +52,10 @@ class AttributesWidget extends SourcePluginBase {
     // See https://www.w3.org/TR/2011/WD-html5-20110525/syntax.html#attributes-0
     $forbidden_characters = "<>&/`";
     $pattern = "^(([^" . $forbidden_characters . "]+)=[\"'].+[\"']\s*)*?$";
-    $build['value']['#pattern'] = $pattern;
-    $build['value']['#description'] = $this->t("Values must be present and quoted.");
-    return $build;
+    $form['value']['#pattern'] = $pattern;
+    $form['value']['#description'] = $this->t("Values must be present and quoted.");
+    $this->addRequired($form['value']);
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/CheckboxWidget.php b/src/Plugin/UiPatterns/Source/CheckboxWidget.php
index 42f6bceb4..84fe1e472 100644
--- a/src/Plugin/UiPatterns/Source/CheckboxWidget.php
+++ b/src/Plugin/UiPatterns/Source/CheckboxWidget.php
@@ -32,12 +32,12 @@ class CheckboxWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'checkbox',
-        '#default_value' => $this->getSetting('value'),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'checkbox',
+      '#default_value' => $this->getSetting('value'),
     ];
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
index 6fb930be5..8c5527466 100644
--- a/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
+++ b/src/Plugin/UiPatterns/Source/CheckboxesWidget.php
@@ -34,13 +34,14 @@ class CheckboxesWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'checkboxes',
-        '#default_value' => $this->getSetting('value') ?? [],
-        "#options" => $this->getOptions(),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'checkboxes',
+      '#default_value' => $this->getSetting('value') ?? [],
+      "#options" => $this->getOptions(),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/EntityLinksSource.php b/src/Plugin/UiPatterns/Source/EntityLinksSource.php
index 242c3ceff..c28dffeab 100644
--- a/src/Plugin/UiPatterns/Source/EntityLinksSource.php
+++ b/src/Plugin/UiPatterns/Source/EntityLinksSource.php
@@ -109,12 +109,12 @@ class EntityLinksSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
+    $form = parent::settingsForm($form, $form_state);
     $entity_links = $this->getEntityLinkTemplates();
     $link_templates_options = array_keys($entity_links);
     $link_templates_options = array_combine($link_templates_options, $link_templates_options);
     asort($link_templates_options);
-    $build["template"] = [
+    $form["template"] = [
       '#type' => 'select',
       '#title' => $this->t("Select"),
       '#options' => $link_templates_options,
@@ -122,12 +122,12 @@ class EntityLinksSource extends SourcePluginBase {
       "#empty_option" => $this->t("- Select -"),
       '#empty_value' => '',
     ];
-    $build['absolute'] = [
+    $form['absolute'] = [
       '#type' => 'checkbox',
       '#title' => $this->t('Absolute URL'),
       '#default_value' => $this->isAbsoluteUrl(),
     ];
-    return $build;
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/ListTextareaWidget.php b/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
index 84ac17dfa..a18381ed6 100644
--- a/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
+++ b/src/Plugin/UiPatterns/Source/ListTextareaWidget.php
@@ -32,17 +32,18 @@ class ListTextareaWidget extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $items = $this->getSetting('value');
     if (is_array($items)) {
       $items = implode("\r", $items);
     }
-    return [
-      'value' => [
-        '#type' => 'textarea',
-        '#default_value' => $items,
-        "#description" => $this->t("One item by line"),
-      ],
+    $form['value'] = [
+      '#type' => 'textarea',
+      '#default_value' => $items,
+      "#description" => $this->t("One item by line"),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/MenuSource.php b/src/Plugin/UiPatterns/Source/MenuSource.php
index 3ddee4947..2dfd73e5b 100644
--- a/src/Plugin/UiPatterns/Source/MenuSource.php
+++ b/src/Plugin/UiPatterns/Source/MenuSource.php
@@ -100,8 +100,8 @@ class MenuSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build["menu"] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form["menu"] = [
       '#type' => 'select',
       '#title' => $this->t("Menu"),
       '#options' => $this->getMenuList(),
@@ -109,14 +109,14 @@ class MenuSource extends SourcePluginBase {
     ];
     $options = range(0, $this->menuLinkTree->maxDepth());
     unset($options[0]);
-    $build['level'] = [
+    $form['level'] = [
       '#type' => 'select',
       '#title' => $this->t('Initial visibility level'),
       '#default_value' => $this->getSetting('level'),
       '#options' => $options,
     ];
     $options[0] = $this->t('Unlimited');
-    $build['depth'] = [
+    $form['depth'] = [
       '#type' => 'select',
       '#title' => $this->t('Number of levels to display'),
       '#default_value' => $this->getSetting('depth'),
@@ -125,7 +125,7 @@ class MenuSource extends SourcePluginBase {
         'This maximum number includes the initial level and the final display is dependant of the component template.'
       ),
     ];
-    return $build;
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/NumberWidget.php b/src/Plugin/UiPatterns/Source/NumberWidget.php
index 2d8ce1499..cb81b7876 100644
--- a/src/Plugin/UiPatterns/Source/NumberWidget.php
+++ b/src/Plugin/UiPatterns/Source/NumberWidget.php
@@ -37,29 +37,30 @@ class NumberWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'number',
       '#default_value' => $this->getSetting('value'),
       '#step' => 0.01,
     ];
     if ($this->propDefinition["type"] === "integer") {
-      $build['value']['#step'] = 1;
+      $form['value']['#step'] = 1;
     }
     // Because of SDC's ComponentMetadata::parseSchemaInfo() which is adding
     // "object" type to all props to "allows deferring rendering in Twig to the
     // render pipeline". Remove it as soon as this weird mechanism is removed
     // from SDC.
     if (in_array("integer", $this->propDefinition["type"])) {
-      $build['value']['#step'] = 1;
+      $form['value']['#step'] = 1;
     }
     if (isset($this->propDefinition["minimum"])) {
-      $build['value']['#min'] = $this->propDefinition["minimum"];
+      $form['value']['#min'] = $this->propDefinition["minimum"];
     }
     if (isset($this->propDefinition["maximum"])) {
-      $build['value']['#max'] = $this->propDefinition["maximum"];
+      $form['value']['#max'] = $this->propDefinition["maximum"];
     }
-    return $build;
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/PathSource.php b/src/Plugin/UiPatterns/Source/PathSource.php
index e818f026b..9724cb52e 100644
--- a/src/Plugin/UiPatterns/Source/PathSource.php
+++ b/src/Plugin/UiPatterns/Source/PathSource.php
@@ -48,15 +48,15 @@ class PathSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $value = $this->getSetting('value');
     $value = $this->getUrlFromRoute($value);
-    return [
-      'value' => [
-        '#type' => 'path',
-        '#default_value' => $value,
-        '#description' => $this->t("Enter an internal path"),
-      ],
+    $form['value'] = [
+      '#type' => 'path',
+      '#default_value' => $value,
+      '#description' => $this->t("Enter an internal path"),
     ];
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/SelectWidget.php b/src/Plugin/UiPatterns/Source/SelectWidget.php
index e80c6f145..5ab0bf362 100644
--- a/src/Plugin/UiPatterns/Source/SelectWidget.php
+++ b/src/Plugin/UiPatterns/Source/SelectWidget.php
@@ -25,17 +25,18 @@ class SelectWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'select',
       '#default_value' => $this->getSetting('value'),
       "#options" => $this->getOptions(),
       "#empty_option" => $this->t("- Select -"),
     ];
+    $this->addRequired($form['value']);
     // With Firefox, autocomplete may override #default_value.
     // https://drupal.stackexchange.com/questions/257732/default-value-not-working-in-select-field
-    $build['value']['#attributes']['autocomplete'] = 'off';
-    return $build;
+    $form['value']['#attributes']['autocomplete'] = 'off';
+    return $form;
   }
 
   /**
diff --git a/src/Plugin/UiPatterns/Source/TextfieldWidget.php b/src/Plugin/UiPatterns/Source/TextfieldWidget.php
index a20a9f36a..427e4810d 100644
--- a/src/Plugin/UiPatterns/Source/TextfieldWidget.php
+++ b/src/Plugin/UiPatterns/Source/TextfieldWidget.php
@@ -25,28 +25,28 @@ class TextfieldWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    $build = [];
-    $build['value'] = [
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
       '#type' => 'textfield',
       '#default_value' => $this->getSetting('value'),
     ];
+    $this->addRequired($form['value']);
     $description = [];
     if (isset($this->propDefinition["pattern"])) {
-      $build['value']['#pattern'] = $this->propDefinition["pattern"];
+      $form['value']['#pattern'] = $this->propDefinition["pattern"];
       $description[] = $this->t("Constraint: @pattern", ["@pattern" => $this->propDefinition["pattern"]]);
     }
     if (isset($this->propDefinition["maxLength"])) {
-      $build['value']['#maxlength'] = $this->propDefinition["maxLength"];
-      $build['value']['#size'] = $this->propDefinition["maxLength"];
+      $form['value']['#maxlength'] = $this->propDefinition["maxLength"];
+      $form['value']['#size'] = $this->propDefinition["maxLength"];
       $description[] = $this->t("Max length: @length", ["@length" => $this->propDefinition["maxLength"]]);
     }
     if (!isset($this->propDefinition["pattern"]) && isset($this->propDefinition["minLength"])) {
-      // @todo Cover also the use case pattern + minLength.
-      $build['value']['#pattern'] = "^.{" . $this->propDefinition["minLength"] . ",}$";
+      $form['value']['#pattern'] = "^.{" . $this->propDefinition["minLength"] . ",}$";
       $description[] = $this->t("Min length: @length", ["@length" => $this->propDefinition["minLength"]]);
     }
-    $build['value']["#description"] = implode("; ", $description);
-    return $build;
+    $form['value']["#description"] = implode("; ", $description);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/TokenSource.php b/src/Plugin/UiPatterns/Source/TokenSource.php
index 25edb3ad0..46b6f1800 100644
--- a/src/Plugin/UiPatterns/Source/TokenSource.php
+++ b/src/Plugin/UiPatterns/Source/TokenSource.php
@@ -68,15 +68,15 @@ class TokenSource extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'textfield',
-        '#default_value' => $this->getSetting('value'),
-        // Tokens always start with a [ and end with a ].
-        '#pattern' => '^\[.+\]$',
-      ],
-      'context_mapping' => $this->addContextAssignmentElement($this, $this->gatheredContexts),
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'textfield',
+      '#default_value' => $this->getSetting('value'),
+      // Tokens always start with a [ and end with a ].
+      '#pattern' => '^\[.+\]$',
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/UrlWidget.php b/src/Plugin/UiPatterns/Source/UrlWidget.php
index 8fdd58e30..8bfefd637 100644
--- a/src/Plugin/UiPatterns/Source/UrlWidget.php
+++ b/src/Plugin/UiPatterns/Source/UrlWidget.php
@@ -25,13 +25,14 @@ class UrlWidget extends SourcePluginPropValue {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      'value' => [
-        '#type' => 'url',
-        '#default_value' => $this->getSetting('value'),
-        '#description' => $this->t("Enter an external URL"),
-      ],
+    $form = parent::settingsForm($form, $form_state);
+    $form['value'] = [
+      '#type' => 'url',
+      '#default_value' => $this->getSetting('value'),
+      '#description' => $this->t("Enter an external URL"),
     ];
+    $this->addRequired($form['value']);
+    return $form;
   }
 
 }
diff --git a/src/Plugin/UiPatterns/Source/WysiwygWidget.php b/src/Plugin/UiPatterns/Source/WysiwygWidget.php
index 8bf60a769..8e7978145 100644
--- a/src/Plugin/UiPatterns/Source/WysiwygWidget.php
+++ b/src/Plugin/UiPatterns/Source/WysiwygWidget.php
@@ -87,6 +87,7 @@ class WysiwygWidget extends SourcePluginBase implements TrustedCallbackInterface
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
+    $form = parent::settingsForm($form, $form_state);
     $value = $this->getSetting('value');
     $element = [
       '#type' => 'text_format',
@@ -101,7 +102,8 @@ class WysiwygWidget extends SourcePluginBase implements TrustedCallbackInterface
     else {
       $element['#format'] = filter_fallback_format();
     }
-    return ['value' => $element];
+    $form['value'] = $element;
+    return $form;
   }
 
   /**
diff --git a/src/PropTypePluginBase.php b/src/PropTypePluginBase.php
index 80279f4b1..aa4ffe514 100644
--- a/src/PropTypePluginBase.php
+++ b/src/PropTypePluginBase.php
@@ -56,6 +56,9 @@ abstract class PropTypePluginBase extends PluginBase implements PropTypeInterfac
     if (isset($definition['default']) && is_array($definition['default'])) {
       $summary[] = $this->t("Default: @default", ["@default" => implode(", ", $definition['default'])]);
     }
+    if (isset($definition['ui_patterns']['required']) && $definition['ui_patterns']['required']) {
+      $summary[] = $this->t("Required");
+    }
     return $summary;
   }
 
diff --git a/src/SourcePluginBase.php b/src/SourcePluginBase.php
index cde9fb796..19c74ca95 100644
--- a/src/SourcePluginBase.php
+++ b/src/SourcePluginBase.php
@@ -142,7 +142,7 @@ abstract class SourcePluginBase extends PluginBase implements
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [];
+    return $form;
   }
 
   /**
@@ -401,4 +401,21 @@ abstract class SourcePluginBase extends PluginBase implements
     }
   }
 
+  /**
+   * Add required property.
+   *
+   * @param array $form_element
+   *   The form element.
+   *
+   * @see \Drupal\ui_patterns\Element\ComponentPropForm::addRequired()
+   */
+  protected function addRequired(array &$form_element): void {
+    if (isset($this->propDefinition["ui_patterns"]["required"])) {
+      $form_element['#required'] = TRUE;
+      // ComponentPropForm is carrying the visual clue
+      // We set here the custom error message.
+      $form_element['#required_error'] = $this->t('@name field is required.', ['@name' => (!empty($form_element['#title'])) ? $form_element['#title'] : $this->propDefinition["title"]]);
+    }
+  }
+
 }
diff --git a/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php b/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
index 8b42eb6f1..7bee5c157 100644
--- a/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
+++ b/tests/modules/ui_patterns_test/src/Plugin/UiPatterns/Source/Foo.php
@@ -24,15 +24,14 @@ final class Foo extends SourcePluginBase {
    * {@inheritdoc}
    */
   public function settingsForm(array $form, FormStateInterface $form_state): array {
-    return [
-      "value" => [
-        '#type' => 'textfield',
-        '#attributes' => [
-          'placeholder' => $this->t('Test: FOO'),
-        ],
-        '#default_value' => $this->getSetting('value'),
+    $form["value"] = [
+      '#type' => 'textfield',
+      '#attributes' => [
+        'placeholder' => $this->t('Test: FOO'),
       ],
+      '#default_value' => $this->getSetting('value'),
     ];
+    return $form;
   }
 
   /**
-- 
GitLab