From 7659dcc4c5ed3ceeddda589304b2de093aa75fc7 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 1 Apr 2024 09:20:15 +0100
Subject: [PATCH] Issue #2827055 by yash.rode, srishtiiee, mrshowerman,
 rodrigoaguilera, herved, omkar.podey, MegaChriz, Lukas von Blarer,
 ravi.shankar, lauriii, c_archer, ifrik, froboy, Rudi Teschner, smustgrave,
 liquidcms, catch, quietone, mpdonadio, alexpott: Add option to show only
 start or end date in the DateTime Range custom formatter

---
 .../config/schema/datetime_range.schema.yml   |  24 +++
 .../datetime_range/datetime_range.module      |  38 ++++
 .../datetime_range.post_update.php            |  43 ++++
 .../src/DateTimeRangeConstantsInterface.php   |  20 ++
 .../datetime_range/src/DateTimeRangeTrait.php | 175 ++++++++++++++-
 .../DateRangeCustomFormatter.php              |  27 +--
 .../DateRangeDefaultFormatter.php             |  20 +-
 .../DateRangePlainFormatter.php               |  27 +--
 ...l.daterange-formatter-settings-2827055.php | 120 +++++++++++
 .../src/Functional/DateRangeFieldTest.php     | 201 +++++++++++++++++-
 .../DateRangeFormatterSettingsUpdateTest.php  |  57 +++++
 .../DateRangeFieldTest.php                    |  97 +++++++++
 12 files changed, 772 insertions(+), 77 deletions(-)
 create mode 100644 core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php
 create mode 100644 core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php
 create mode 100644 core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php
 create mode 100644 core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php

diff --git a/core/modules/datetime_range/config/schema/datetime_range.schema.yml b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
index fa17461be651..bb244553edc1 100644
--- a/core/modules/datetime_range/config/schema/datetime_range.schema.yml
+++ b/core/modules/datetime_range/config/schema/datetime_range.schema.yml
@@ -28,6 +28,14 @@ field.formatter.settings.daterange_default:
   type: field.formatter.settings.datetime_default
   label: 'Date range default display format settings'
   mapping:
+    from_to:
+      type: string
+      label: 'Display'
+      constraints:
+        Choice:
+          - both
+          - start_date
+          - end_date
     separator:
       type: label
       label: 'Separator'
@@ -37,6 +45,14 @@ field.formatter.settings.daterange_plain:
   type: field.formatter.settings.datetime_plain
   label: 'Date range plain display format settings'
   mapping:
+    from_to:
+      type: string
+      label: 'Display'
+      constraints:
+        Choice:
+          - both
+          - start_date
+          - end_date
     separator:
       type: label
       label: 'Separator'
@@ -46,6 +62,14 @@ field.formatter.settings.daterange_custom:
   type: field.formatter.settings.datetime_custom
   label: 'Date range custom display format settings'
   mapping:
+    from_to:
+      type: string
+      label: 'Display'
+      constraints:
+        Choice:
+          - both
+          - start_date
+          - end_date
     separator:
       type: label
       label: 'Separator'
diff --git a/core/modules/datetime_range/datetime_range.module b/core/modules/datetime_range/datetime_range.module
index c0be2a2089d8..0a505c30c75b 100644
--- a/core/modules/datetime_range/datetime_range.module
+++ b/core/modules/datetime_range/datetime_range.module
@@ -5,8 +5,13 @@
  * Field hooks to implement a datetime field that stores a start and end date.
  */
 
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\datetime_range\DateTimeRangeConstantsInterface;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeCustomFormatter;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeDefaultFormatter;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangePlainFormatter;
 
 /**
  * Implements hook_help().
@@ -27,3 +32,36 @@ function datetime_range_help($route_name, RouteMatchInterface $route_match) {
       return $output;
   }
 }
+
+/**
+ * Implements hook_ENTITY_TYPE_presave() for entity_view_display entities.
+ *
+ * @todo Remove this when datetime_range_post_update_from_to_configuration is removed.
+ */
+function datetime_range_entity_view_display_presave(EntityViewDisplayInterface $entity_view_display): void {
+  /** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */
+  $field_formatter_manager = \Drupal::service('plugin.manager.field.formatter');
+
+  foreach ($entity_view_display->getComponents() as $name => $component) {
+    if (empty($component['type'])) {
+      continue;
+    }
+
+    $plugin_definition = $field_formatter_manager->getDefinition($component['type'], FALSE);
+    $daterange_formatter_classes = [
+      DateRangeCustomFormatter::class,
+      DateRangeDefaultFormatter::class,
+      DateRangePlainFormatter::class,
+    ];
+
+    if (!in_array($plugin_definition['class'], $daterange_formatter_classes, FALSE)) {
+      continue;
+    }
+
+    if (!isset($component['settings']['from_to'])) {
+      // Existing daterange formatters don't have 'from_to'.
+      $component['settings']['from_to'] = DateTimeRangeConstantsInterface::BOTH;
+      $entity_view_display->setComponent($name, $component);
+    }
+  }
+}
diff --git a/core/modules/datetime_range/datetime_range.post_update.php b/core/modules/datetime_range/datetime_range.post_update.php
index 83df8ca9b1c8..9dfa5156eb26 100644
--- a/core/modules/datetime_range/datetime_range.post_update.php
+++ b/core/modules/datetime_range/datetime_range.post_update.php
@@ -5,6 +5,12 @@
  * Post-update functions for Datetime Range module.
  */
 
+use Drupal\Core\Config\Entity\ConfigEntityUpdater;
+use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeCustomFormatter;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangeDefaultFormatter;
+use Drupal\datetime_range\Plugin\Field\FieldFormatter\DateRangePlainFormatter;
+
 /**
  * Implements hook_removed_post_updates().
  */
@@ -14,3 +20,40 @@ function datetime_range_removed_post_updates() {
     'datetime_range_post_update_views_string_plugin_id' => '9.0.0',
   ];
 }
+
+/**
+ * Adds 'from_to' in flagged entity view date range formatter.
+ *
+ * @see \datetime_range_entity_view_display_presave
+ */
+function datetime_range_post_update_from_to_configuration(array &$sandbox = NULL): void {
+  /** @var \Drupal\Core\Field\FormatterPluginManager $field_formatter_manager */
+  $field_formatter_manager = \Drupal::service('plugin.manager.field.formatter');
+  $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
+
+  $callback = function (EntityViewDisplayInterface $entity_view_display) use ($field_formatter_manager) {
+    foreach (array_values($entity_view_display->getComponents()) as $component) {
+      if (empty($component['type'])) {
+        continue;
+      }
+
+      $plugin_definition = $field_formatter_manager->getDefinition($component['type'], FALSE);
+      $daterange_formatter_classes = [
+        DateRangeCustomFormatter::class,
+        DateRangeDefaultFormatter::class,
+        DateRangePlainFormatter::class,
+      ];
+
+      if (!in_array($plugin_definition['class'], $daterange_formatter_classes, FALSE)) {
+        continue;
+      }
+
+      if (!isset($component['settings']['from_to'])) {
+        return TRUE;
+      }
+    }
+    return FALSE;
+  };
+
+  $config_entity_updater->update($sandbox, 'entity_view_display', $callback);
+}
diff --git a/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php b/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php
new file mode 100644
index 000000000000..9d97b6e4a798
--- /dev/null
+++ b/core/modules/datetime_range/src/DateTimeRangeConstantsInterface.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace Drupal\datetime_range;
+
+/**
+ * Declares constants used in the datetime range module.
+ *
+ * @todo Convert this to an enum in 11.0.
+ * @see https://www.drupal.org/project/drupal/issues/3425141.
+ */
+interface DateTimeRangeConstantsInterface {
+
+  /**
+   * Values for the 'from_to' formatter setting.
+   */
+  const BOTH = 'both';
+  const START_DATE = 'start_date';
+  const END_DATE = 'end_date';
+
+}
diff --git a/core/modules/datetime_range/src/DateTimeRangeTrait.php b/core/modules/datetime_range/src/DateTimeRangeTrait.php
index 3f05b82189b7..0563f7ff722b 100644
--- a/core/modules/datetime_range/src/DateTimeRangeTrait.php
+++ b/core/modules/datetime_range/src/DateTimeRangeTrait.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\datetime_range;
 
+use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Field\FieldItemListInterface;
 
 /**
@@ -9,6 +10,19 @@
  */
 trait DateTimeRangeTrait {
 
+  /**
+   * Get the default settings for a date and time range display.
+   *
+   * @return array
+   *   An array containing default settings.
+   */
+  protected static function dateTimeRangeDefaultSettings(): array {
+    return [
+      'from_to' => DateTimeRangeConstantsInterface::BOTH,
+      'separator' => '-',
+    ];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -24,11 +38,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         $end_date = $item->end_date;
 
         if ($start_date->getTimestamp() !== $end_date->getTimestamp()) {
-          $elements[$delta] = [
-            'start_date' => $this->buildDateWithIsoAttribute($start_date),
-            'separator' => ['#plain_text' => ' ' . $separator . ' '],
-            'end_date' => $this->buildDateWithIsoAttribute($end_date),
-          ];
+          $elements[$delta] = $this->renderStartEndWithIsoAttribute($start_date, $separator, $end_date);
         }
         else {
           $elements[$delta] = $this->buildDateWithIsoAttribute($start_date);
@@ -46,4 +56,159 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     return $elements;
   }
 
+  /**
+   * Configuration form for date time range.
+   *
+   * @param array $form
+   *   The form array.
+   *
+   * @return array
+   *   Modified form array.
+   */
+  protected function dateTimeRangeSettingsForm(array $form): array {
+    $form['from_to'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Display'),
+      '#options' => $this->getFromToOptions(),
+      '#default_value' => $this->getSetting('from_to'),
+    ];
+
+    $field_name = $this->fieldDefinition->getName();
+    $form['separator'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Date separator'),
+      '#description' => $this->t('The string to separate the start and end dates'),
+      '#default_value' => $this->getSetting('separator'),
+      '#states' => [
+        'visible' => [
+          'select[name="fields[' . $field_name . '][settings_edit_form][settings][from_to]"]' => ['value' => DateTimeRangeConstantsInterface::BOTH],
+        ],
+      ],
+    ];
+
+    return $form;
+  }
+
+  /**
+   * Gets the date time range settings summary.
+   *
+   * @return array
+   *   An array of summary messages.
+   */
+  protected function dateTimeRangeSettingsSummary(): array {
+    $summary = [];
+    if ($from_to = $this->getSetting('from_to')) {
+      $from_to_options = $this->getFromToOptions();
+      if (isset($from_to_options[$from_to])) {
+        $summary[] = $from_to_options[$from_to];
+      }
+    }
+
+    if (($separator = $this->getSetting('separator')) && $this->getSetting('from_to') === DateTimeRangeConstantsInterface::BOTH) {
+      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+    }
+
+    return $summary;
+  }
+
+  /**
+   * Returns a list of possible values for the 'from_to' setting.
+   *
+   * @return array
+   *   A list of 'from_to' options.
+   */
+  protected function getFromToOptions(): array {
+    return [
+      DateTimeRangeConstantsInterface::BOTH => $this->t('Display both start and end dates'),
+      DateTimeRangeConstantsInterface::START_DATE => $this->t('Display start date only'),
+      DateTimeRangeConstantsInterface::END_DATE => $this->t('Display end date only'),
+    ];
+  }
+
+  /**
+   * Gets whether the start date should be displayed.
+   *
+   * @return bool
+   *   True if the start date should be displayed. False otherwise.
+   */
+  protected function startDateIsDisplayed(): bool {
+    switch ($this->getSetting('from_to')) {
+      case DateTimeRangeConstantsInterface::BOTH:
+      case DateTimeRangeConstantsInterface::START_DATE:
+        return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Gets whether the end date should be displayed.
+   *
+   * @return bool
+   *   True if the end date should be displayed. False otherwise.
+   */
+  protected function endDateIsDisplayed(): bool {
+    switch ($this->getSetting('from_to')) {
+      case DateTimeRangeConstantsInterface::BOTH:
+      case DateTimeRangeConstantsInterface::END_DATE:
+        return TRUE;
+    }
+
+    return FALSE;
+  }
+
+  /**
+   * Creates a render array given start/end dates.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date to be rendered.
+   * @param string $separator
+   *   The separator string.
+   * @param \Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date to be rendered.
+   *
+   * @return array
+   *   A renderable array for a single date time range.
+   */
+  protected function renderStartEnd(DrupalDateTime $start_date, string $separator, DrupalDateTime $end_date): array {
+    $element = [];
+    if ($this->startDateIsDisplayed()) {
+      $element[DateTimeRangeConstantsInterface::START_DATE] = $this->buildDate($start_date);
+    }
+    if ($this->startDateIsDisplayed() && $this->endDateIsDisplayed()) {
+      $element['separator'] = ['#plain_text' => ' ' . $separator . ' '];
+    }
+    if ($this->endDateIsDisplayed()) {
+      $element[DateTimeRangeConstantsInterface::END_DATE] = $this->buildDate($end_date);
+    }
+    return $element;
+  }
+
+  /**
+   * Creates a render array with ISO attributes given start/end dates.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $start_date
+   *   The start date to be rendered.
+   * @param string $separator
+   *   The separator string.
+   * @param \Drupal\Core\Datetime\DrupalDateTime $end_date
+   *   The end date to be rendered.
+   *
+   * @return array
+   *   A renderable array for a single date time range.
+   */
+  protected function renderStartEndWithIsoAttribute(DrupalDateTime $start_date, string $separator, DrupalDateTime $end_date): array {
+    $element = [];
+    if ($this->startDateIsDisplayed()) {
+      $element[DateTimeRangeConstantsInterface::START_DATE] = $this->buildDateWithIsoAttribute($start_date);
+    }
+    if ($this->startDateIsDisplayed() && $this->endDateIsDisplayed()) {
+      $element['separator'] = ['#plain_text' => ' ' . $separator . ' '];
+    }
+    if ($this->endDateIsDisplayed()) {
+      $element[DateTimeRangeConstantsInterface::END_DATE] = $this->buildDateWithIsoAttribute($end_date);
+    }
+    return $element;
+  }
+
 }
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
index b9facc709661..1cf94249be68 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -30,9 +30,7 @@ class DateRangeCustomFormatter extends DateTimeCustomFormatter {
    * {@inheritdoc}
    */
   public static function defaultSettings() {
-    return [
-      'separator' => '-',
-    ] + parent::defaultSettings();
+    return static::dateTimeRangeDefaultSettings() + parent::defaultSettings();
   }
 
   /**
@@ -53,11 +51,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         $end_date = $item->end_date;
 
         if ($start_date->getTimestamp() !== $end_date->getTimestamp()) {
-          $elements[$delta] = [
-            'start_date' => $this->buildDate($start_date),
-            'separator' => ['#plain_text' => ' ' . $separator . ' '],
-            'end_date' => $this->buildDate($end_date),
-          ];
+          $elements[$delta] = $this->renderStartEnd($start_date, $separator, $end_date);
         }
         else {
           $elements[$delta] = $this->buildDate($start_date);
@@ -73,14 +67,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
-
-    $form['separator'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Date separator'),
-      '#description' => $this->t('The string to separate the start and end dates'),
-      '#default_value' => $this->getSetting('separator'),
-    ];
-
+    $form = $this->dateTimeRangeSettingsForm($form);
     return $form;
   }
 
@@ -88,13 +75,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function settingsSummary() {
-    $summary = parent::settingsSummary();
-
-    if ($separator = $this->getSetting('separator')) {
-      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
-    }
-
-    return $summary;
+    return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary());
   }
 
 }
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
index 76655da33a57..401fca6395de 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -30,9 +30,7 @@ class DateRangeDefaultFormatter extends DateTimeDefaultFormatter {
    * {@inheritdoc}
    */
   public static function defaultSettings() {
-    return [
-      'separator' => '-',
-    ] + parent::defaultSettings();
+    return static::dateTimeRangeDefaultSettings() + parent::defaultSettings();
   }
 
   /**
@@ -40,13 +38,7 @@ public static function defaultSettings() {
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
-
-    $form['separator'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Date separator'),
-      '#description' => $this->t('The string to separate the start and end dates'),
-      '#default_value' => $this->getSetting('separator'),
-    ];
+    $form = $this->dateTimeRangeSettingsForm($form);
 
     return $form;
   }
@@ -55,13 +47,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function settingsSummary() {
-    $summary = parent::settingsSummary();
-
-    if ($separator = $this->getSetting('separator')) {
-      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
-    }
-
-    return $summary;
+    return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary());
   }
 
 }
diff --git a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
index 76a3799a2d46..960bd5e8ca7d 100644
--- a/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
+++ b/core/modules/datetime_range/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -30,9 +30,7 @@ class DateRangePlainFormatter extends DateTimePlainFormatter {
    * {@inheritdoc}
    */
   public static function defaultSettings() {
-    return [
-      'separator' => '-',
-    ] + parent::defaultSettings();
+    return static::dateTimeRangeDefaultSettings() + parent::defaultSettings();
   }
 
   /**
@@ -50,11 +48,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
         $end_date = $item->end_date;
 
         if ($start_date->getTimestamp() !== $end_date->getTimestamp()) {
-          $elements[$delta] = [
-            'start_date' => $this->buildDate($start_date),
-            'separator' => ['#plain_text' => ' ' . $separator . ' '],
-            'end_date' => $this->buildDate($end_date),
-          ];
+          $elements[$delta] = $this->renderStartEnd($start_date, $separator, $end_date);
         }
         else {
           $elements[$delta] = $this->buildDate($start_date);
@@ -77,14 +71,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
-
-    $form['separator'] = [
-      '#type' => 'textfield',
-      '#title' => $this->t('Date separator'),
-      '#description' => $this->t('The string to separate the start and end dates'),
-      '#default_value' => $this->getSetting('separator'),
-    ];
-
+    $form = $this->dateTimeRangeSettingsForm($form);
     return $form;
   }
 
@@ -92,13 +79,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function settingsSummary() {
-    $summary = parent::settingsSummary();
-
-    if ($separator = $this->getSetting('separator')) {
-      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
-    }
-
-    return $summary;
+    return array_merge(parent::settingsSummary(), $this->dateTimeRangeSettingsSummary());
   }
 
 }
diff --git a/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php b/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php
new file mode 100644
index 000000000000..f8552a5018bd
--- /dev/null
+++ b/core/modules/datetime_range/tests/fixtures/update/drupal.daterange-formatter-settings-2827055.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * @file
+ * Provides database changes for testing the daterange formatter upgrade path.
+ *
+ * @see \Drupal\Tests\datetime_range\Functional\DateRangeFormatterSettingsUpdateTest
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\field\Entity\FieldStorageConfig;
+
+$connection = Database::getConnection();
+
+// Add all datetime_range_removed_post_updates() as existing updates.
+require_once __DIR__ . '/../../../../datetime_range/datetime_range.post_update.php';
+$existing_updates = $connection->select('key_value')
+  ->fields('key_value', ['value'])
+  ->condition('collection', 'post_update')
+  ->condition('name', 'existing_updates')
+  ->execute()
+  ->fetchField();
+$existing_updates = unserialize($existing_updates);
+$existing_updates = array_merge(
+  $existing_updates,
+  array_keys(datetime_range_removed_post_updates())
+);
+$connection->update('key_value')
+  ->fields(['value' => serialize($existing_updates)])
+  ->condition('collection', 'post_update')
+  ->condition('name', 'existing_updates')
+  ->execute();
+
+// Add a new timestamp field 'field_datetime_range'.
+$connection->insert('config')
+  ->fields(['collection', 'name', 'data'])->values([
+    'collection' => '',
+    'name' => 'field.storage.node.field_datetime_range',
+    'data' => $field_storage = 'a:16:{s:4:"uuid";s:36:"a01264e6-2821-4b94-bc79-ba2b346795bb";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"module";a:2:{i:0;s:14:"datetime_range";i:1;s:4:"node";}}s:2:"id";s:25:"node.field_datetime_range";s:10:"field_name";s:20:"field_datetime_range";s:11:"entity_type";s:4:"node";s:4:"type";s:9:"daterange";s:8:"settings";a:1:{s:13:"datetime_type";s:8:"datetime";}s:6:"module";s:14:"datetime_range";s:6:"locked";b:0;s:11:"cardinality";i:1;s:12:"translatable";b:1;s:7:"indexes";a:0:{}s:22:"persist_with_no_fields";b:0;s:14:"custom_storage";b:0;}',
+  ])->values([
+    'collection' => '',
+    'name' => 'field.field.node.page.field_datetime_range',
+    'data' => 'a:16:{s:4:"uuid";s:36:"678b9e68-cff5-4b2e-9111-43e5d9d6c826";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:39:"field.storage.node.field_datetime_range";i:1;s:14:"node.type.page";}s:6:"module";a:1:{i:0;s:14:"datetime_range";}}s:2:"id";s:30:"node.page.field_datetime_range";s:10:"field_name";s:20:"field_datetime_range";s:11:"entity_type";s:4:"node";s:6:"bundle";s:4:"page";s:5:"label";s:14:"datetime range";s:11:"description";s:0:"";s:8:"required";b:0;s:12:"translatable";b:0;s:13:"default_value";a:0:{}s:22:"default_value_callback";s:0:"";s:8:"settings";a:0:{}s:10:"field_type";s:9:"daterange";}',
+  ])->execute();
+
+$connection->insert('key_value')
+  ->fields(['collection', 'name', 'value'])
+  ->values([
+    'collection' => 'config.entity.key_store.field_config',
+    'name' => 'uuid:678b9e68-cff5-4b2e-9111-43e5d9d6c826',
+    'value' => 'a:1:{i:0;s:42:"field.field.node.page.field_datetime_range";}',
+  ])
+  ->values([
+    'collection' => 'config.entity.key_store.field_storage_config',
+    'name' => 'uuid:a01264e6-2821-4b94-bc79-ba2b346795bb',
+    'value' => 'a:1:{i:0;s:39:"field.storage.node.field_datetime_range";}',
+  ])
+  ->values([
+    'collection' => 'entity.storage_schema.sql',
+    'name' => 'node.field_schema_data.field_datetime_range',
+    'value' => 'a:2:{s:26:"node__field_datetime_range";a:4:{s:11:"description";s:49:"Data storage for node field field_datetime_range.";s:6:"fields";a:8:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:26:"field_datetime_range_value";a:4:{s:11:"description";s:21:"The start date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}s:30:"field_datetime_range_end_value";a:4:{s:11:"description";s:19:"The end date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}}s:11:"primary key";a:4:{i:0;s:9:"entity_id";i:1;s:7:"deleted";i:2;s:5:"delta";i:3;s:8:"langcode";}s:7:"indexes";a:4:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}s:26:"field_datetime_range_value";a:1:{i:0;s:26:"field_datetime_range_value";}s:30:"field_datetime_range_end_value";a:1:{i:0;s:30:"field_datetime_range_end_value";}}}s:35:"node_revision__field_datetime_range";a:4:{s:11:"description";s:61:"Revision archive storage for node field field_datetime_range.";s:6:"fields";a:8:{s:6:"bundle";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:128;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:88:"The field instance bundle to which this row belongs, used when deleting a field instance";}s:7:"deleted";a:5:{s:4:"type";s:3:"int";s:4:"size";s:4:"tiny";s:8:"not null";b:1;s:7:"default";i:0;s:11:"description";s:60:"A boolean indicating whether this data item has been deleted";}s:9:"entity_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:38:"The entity id this data is attached to";}s:11:"revision_id";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:47:"The entity revision id this data is attached to";}s:8:"langcode";a:5:{s:4:"type";s:13:"varchar_ascii";s:6:"length";i:32;s:8:"not null";b:1;s:7:"default";s:0:"";s:11:"description";s:37:"The language code for this data item.";}s:5:"delta";a:4:{s:4:"type";s:3:"int";s:8:"unsigned";b:1;s:8:"not null";b:1;s:11:"description";s:67:"The sequence number for this data item, used for multi-value fields";}s:26:"field_datetime_range_value";a:4:{s:11:"description";s:21:"The start date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}s:30:"field_datetime_range_end_value";a:4:{s:11:"description";s:19:"The end date value.";s:4:"type";s:7:"varchar";s:6:"length";i:20;s:8:"not null";b:1;}}s:11:"primary key";a:5:{i:0;s:9:"entity_id";i:1;s:11:"revision_id";i:2;s:7:"deleted";i:3;s:5:"delta";i:4;s:8:"langcode";}s:7:"indexes";a:4:{s:6:"bundle";a:1:{i:0;s:6:"bundle";}s:11:"revision_id";a:1:{i:0;s:11:"revision_id";}s:26:"field_datetime_range_value";a:1:{i:0;s:26:"field_datetime_range_value";}s:30:"field_datetime_range_end_value";a:1:{i:0;s:30:"field_datetime_range_end_value";}}}}',
+  ])
+  ->execute();
+
+$data = $connection->select('key_value')
+  ->fields('key_value', ['value'])
+  ->condition('collection', 'entity.definitions.installed')
+  ->condition('name', 'node.field_storage_definitions')
+  ->execute()
+  ->fetchField();
+$data = unserialize($data);
+$data['field_datetime_range'] = new FieldStorageConfig(unserialize($field_storage));
+$connection->update('key_value')
+  ->fields(['value' => serialize($data)])
+  ->condition('collection', 'entity.definitions.installed')
+  ->condition('name', 'node.field_storage_definitions')
+  ->execute();
+
+$data = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.entity_view_display.node.page.default')
+  ->execute()
+  ->fetchField();
+$data = unserialize($data);
+$data['content']['field_datetime_range'] = [
+  'type' => 'daterange_default',
+  'label' => 'above',
+  'settings' => [
+    'timezone_override' => '',
+    'format_type' => 'medium',
+    'separator' => '-',
+  ],
+  'third_party_settings' => [],
+  'weight' => 102,
+  'region' => 'content',
+];
+$connection->update('config')
+  ->fields([
+    'data' => serialize($data),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.entity_view_display.node.page.default')
+  ->execute();
+
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['datetime_range'] = 0;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php
index f5de3a28ba1b..fcec7f3e1054 100644
--- a/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php
+++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFieldTest.php
@@ -7,12 +7,13 @@
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Datetime\Entity\DateFormat;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
-use Drupal\Tests\datetime\Functional\DateTestBase;
+use Drupal\datetime_range\DateTimeRangeConstantsInterface;
 use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\node\Entity\Node;
+use Drupal\Tests\datetime\Functional\DateTestBase;
 
 /**
  * Tests Daterange field functionality.
@@ -39,7 +40,11 @@ class DateRangeFieldTest extends DateTestBase {
    *
    * @var array
    */
-  protected $defaultSettings = ['timezone_override' => '', 'separator' => '-'];
+  protected $defaultSettings = [
+    'timezone_override' => '',
+    'separator' => '-',
+    'from_to' => DateTimeRangeConstantsInterface::BOTH,
+  ];
 
   /**
    * {@inheritdoc}
@@ -185,7 +190,7 @@ public function testDateRangeField() {
       $output = $this->renderTestEntity($id);
       $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected in $timezone.");
 
-      // Test formatters when start date and end date are the same
+      // Test formatters when start date and end date are the same.
       $this->drupalGet('entity_test/add');
       $value = '2012-12-31 00:00:00';
       $start_date = new DrupalDateTime($value, 'UTC');
@@ -363,7 +368,7 @@ public function testDatetimeRangeField() {
     $output = $this->renderTestEntity($id);
     $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected.");
 
-    // Test formatters when start date and end date are the same
+    // Test formatters when start date and end date are the same.
     $this->drupalGet('entity_test/add');
     $value = '2012-12-31 00:00:00';
     $start_date = new DrupalDateTime($value, 'UTC');
@@ -536,7 +541,7 @@ public function testAlldayRangeField() {
     $output = $this->renderTestEntity($id);
     $this->assertStringContainsString($expected, $output, "Formatted date field using daterange_custom format displayed as $expected.");
 
-    // Test formatters when start date and end date are the same
+    // Test formatters when start date and end date are the same.
     $this->drupalGet('entity_test/add');
 
     $value = '2012-12-31 00:00:00';
@@ -825,7 +830,7 @@ public function testDatelistWidget() {
     \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
 
     // Test the widget for validation notifications.
-    foreach ($this->datelistDataProvider() as $data) {
+    foreach (static::datelistDataProvider() as $data) {
       [$start_date_value, $end_date_value, $expected] = $data;
 
       // Display creation form.
@@ -891,7 +896,7 @@ public function testDatelistWidget() {
    * @return array
    *   An array of datelist input permutations to test.
    */
-  protected function datelistDataProvider() {
+  protected static function datelistDataProvider() {
     return [
       // Year only selected, validation error on Month, Day, Hour, Minute.
       [
@@ -1007,7 +1012,13 @@ public function testDefaultValue() {
 
     // Check if default_date has been stored successfully.
     $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
-    $this->assertEquals(['default_date_type' => 'now', 'default_date' => 'now', 'default_end_date_type' => 'now', 'default_end_date' => 'now'], $config_entity['default_value'][0], 'Default value has been stored successfully');
+    $this->assertEquals([
+      'default_date_type' => 'now',
+      'default_date' => 'now',
+      'default_end_date_type' => 'now',
+      'default_end_date' => 'now',
+    ],
+    $config_entity['default_value'][0], 'Default value has been stored successfully');
 
     // Clear field cache in order to avoid stale cache values.
     \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
@@ -1063,7 +1074,13 @@ public function testDefaultValue() {
 
     // Check if default_date has been stored successfully.
     $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
-    $this->assertEquals(['default_date_type' => 'relative', 'default_date' => '+45 days', 'default_end_date_type' => 'relative', 'default_end_date' => '+90 days'], $config_entity['default_value'][0], 'Default value has been stored successfully');
+    $this->assertEquals([
+      'default_date_type' => 'relative',
+      'default_date' => '+45 days',
+      'default_end_date_type' => 'relative',
+      'default_end_date' => '+90 days',
+    ],
+    $config_entity['default_value'][0], 'Default value has been stored successfully');
 
     // Clear field cache in order to avoid stale cache values.
     \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
@@ -1398,4 +1415,170 @@ public function testDateStorageSettings() {
     $this->assertSession()->elementsCount('xpath', "//*[@name='field_storage[subform][settings][datetime_type]' and contains(@disabled, 'disabled')]", 1);
   }
 
+  /**
+   * Tests displaying dates with the 'from_to' setting.
+   *
+   * @dataProvider fromToSettingDataProvider
+   */
+  public function testFromToSetting(array $expected, string $datetime_type, string $field_formatter_type, array $display_settings = []): void {
+    $field_name = $this->fieldStorage->getName();
+
+    // Create a test content type.
+    $this->drupalCreateContentType(['type' => 'date_content']);
+
+    // Ensure the field to a datetime field.
+    $this->fieldStorage->setSetting('datetime_type', $datetime_type);
+    $this->fieldStorage->save();
+
+    // Build up dates in the UTC timezone.
+    $value = '2012-12-31 00:00:00';
+    $start_date = new DrupalDateTime($value, 'UTC');
+    $end_value = '2013-06-06 00:00:00';
+    $end_date = new DrupalDateTime($end_value, 'UTC');
+
+    // Submit a valid date and ensure it is accepted.
+    $date_format = DateFormat::load('html_date')->getPattern();
+
+    $edit = [
+      "{$field_name}[0][value][date]" => $start_date->format($date_format),
+      "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+    ];
+
+    // Supply time as well when field is a datetime field.
+    if ($datetime_type === DateRangeItem::DATETIME_TYPE_DATETIME) {
+      $time_format = DateFormat::load('html_time')->getPattern();
+      $edit["{$field_name}[0][value][time]"] = $start_date->format($time_format);
+      $edit["{$field_name}[0][end_value][time]"] = $end_date->format($time_format);
+    }
+
+    $this->drupalGet('entity_test/add');
+    $this->submitForm($edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
+    $id = $match[1];
+    $this->assertSession()->pageTextContains(t('entity_test @id has been created.', ['@id' => $id]));
+
+    // Now set display options.
+    $this->displayOptions = [
+      'type' => $field_formatter_type,
+      'label' => 'hidden',
+      'settings' => $display_settings + [
+        'format_type' => 'short',
+        'separator' => 'THE_SEPARATOR',
+      ] + $this->defaultSettings,
+    ];
+
+    \Drupal::service('entity_display.repository')->getViewDisplay(
+      $this->field->getTargetEntityTypeId(),
+      $this->field->getTargetBundle(),
+      'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $output = $this->renderTestEntity($id);
+    foreach ($expected as $content => $is_expected) {
+      if ($is_expected) {
+        $this->assertStringContainsString($content, $output);
+      }
+      else {
+        $this->assertStringNotContainsString($content, $output);
+      }
+    }
+  }
+
+  /**
+   * The data provider for testing the 'from_to' setting.
+   *
+   * @return array
+   *   An array of date settings to test the behavior of the 'from_to' setting.
+   */
+  public static function fromToSettingDataProvider(): array {
+    $datetime_types = [
+      DateRangeItem::DATETIME_TYPE_DATE => [
+        'daterange_default' => [
+          DateTimeRangeConstantsInterface::START_DATE => '12/31/2012',
+          DateTimeRangeConstantsInterface::END_DATE => '06/06/2013',
+        ],
+        'daterange_plain' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06',
+        ],
+        'daterange_custom' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06',
+        ],
+      ],
+      DateRangeItem::DATETIME_TYPE_DATETIME => [
+        'daterange_default' => [
+          DateTimeRangeConstantsInterface::START_DATE => '12/31/2012 - 00:00',
+          DateTimeRangeConstantsInterface::END_DATE => '06/06/2013 - 00:00',
+        ],
+        'daterange_plain' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31T00:00:00',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06T00:00:00',
+        ],
+        'daterange_custom' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31T00:00:00',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06T00:00:00',
+        ],
+      ],
+      DateRangeItem::DATETIME_TYPE_ALLDAY => [
+        'daterange_default' => [
+          DateTimeRangeConstantsInterface::START_DATE => '12/31/2012',
+          DateTimeRangeConstantsInterface::END_DATE => '06/06/2013',
+        ],
+        'daterange_plain' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06',
+        ],
+        'daterange_custom' => [
+          DateTimeRangeConstantsInterface::START_DATE => '2012-12-31',
+          DateTimeRangeConstantsInterface::END_DATE => '2013-06-06',
+        ],
+      ],
+    ];
+
+    $return = [];
+    $separator = ' THE_SEPARATOR ';
+    foreach ($datetime_types as $datetime_type => $field_formatters) {
+      foreach ($field_formatters as $field_formatter_type => $dates) {
+        // Both start and end date.
+        $return[$datetime_type . '-' . $field_formatter_type . '-both'] = [
+          'expected' => [
+            $dates[DateTimeRangeConstantsInterface::START_DATE] => TRUE,
+            $separator => TRUE,
+            $dates[DateTimeRangeConstantsInterface::END_DATE] => TRUE,
+          ],
+          'datetime_type' => $datetime_type,
+          'field_formatter_type' => $field_formatter_type,
+        ];
+
+        // Only start date.
+        $return[$datetime_type . '-' . $field_formatter_type . '-start_date'] = [
+          'expected' => [
+            $dates[DateTimeRangeConstantsInterface::START_DATE] => TRUE,
+            $separator => FALSE,
+            $dates[DateTimeRangeConstantsInterface::END_DATE] => FALSE,
+          ],
+          'datetime_type' => $datetime_type,
+          'field_formatter_type' => $field_formatter_type,
+          ['from_to' => DateTimeRangeConstantsInterface::START_DATE],
+        ];
+
+        // Only end date.
+        $return[$datetime_type . '-' . $field_formatter_type . '-end_date'] = [
+          'expected' => [
+            $dates[DateTimeRangeConstantsInterface::START_DATE] => FALSE,
+            $separator => FALSE,
+            $dates[DateTimeRangeConstantsInterface::END_DATE] => TRUE,
+          ],
+          'datetime_type' => $datetime_type,
+          'field_formatter_type' => $field_formatter_type,
+          ['from_to' => DateTimeRangeConstantsInterface::END_DATE],
+        ];
+      }
+    }
+
+    return $return;
+  }
+
 }
diff --git a/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php b/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php
new file mode 100644
index 000000000000..841eb93bc28c
--- /dev/null
+++ b/core/modules/datetime_range/tests/src/Functional/DateRangeFormatterSettingsUpdateTest.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\datetime_range\Functional;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests the update path for daterange formatter settings.
+ *
+ * @group datetime
+ */
+class DateRangeFormatterSettingsUpdateTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'node',
+    'datetime',
+    'datetime_range',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles(): void {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz',
+      __DIR__ . '/../../fixtures/update/drupal.daterange-formatter-settings-2827055.php',
+    ];
+  }
+
+  /**
+   * Tests update path for the 'from_to' formatter setting.
+   *
+   * @covers \datetime_range_post_update_from_to_configuration
+   */
+  public function testPostUpdateDateRangeFormatter(): void {
+    $config_factory = \Drupal::configFactory();
+    // Check that 'from_to' is missing before update.
+    $settings = $config_factory->get('core.entity_view_display.node.page.default')->get('content.field_datetime_range.settings');
+    $this->assertArrayNotHasKey('from_to', $settings);
+
+    $this->runUpdates();
+
+    $settings = $config_factory->get('core.entity_view_display.node.page.default')->get('content.field_datetime_range.settings');
+    $this->assertArrayHasKey('from_to', $settings);
+  }
+
+}
diff --git a/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php
new file mode 100644
index 000000000000..db42c26ec593
--- /dev/null
+++ b/core/modules/datetime_range/tests/src/FunctionalJavascript/DateRangeFieldTest.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\datetime_range\FunctionalJavascript;
+
+use Drupal\datetime_range\DateTimeRangeConstantsInterface;
+use Drupal\datetime_range\Plugin\Field\FieldType\DateRangeItem;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests Daterange field.
+ *
+ * @group datetime
+ */
+class DateRangeFieldTest extends WebDriverTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['node', 'entity_test', 'field_ui', 'datetime', 'datetime_range'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->drupalLogin($this->drupalCreateUser([
+      'view test entity',
+      'administer entity_test content',
+      'administer content types',
+      'administer node fields',
+      'administer node display',
+      'bypass node access',
+      'administer entity_test fields',
+    ]));
+  }
+
+  /**
+   * Tests the conditional visibility of the 'Date separator' field.
+   */
+  public function testFromToSeparatorState(): void {
+    $field_name = $this->randomMachineName();
+    $this->drupalCreateContentType(['type' => 'date_content']);
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'node',
+      'type' => 'daterange',
+      'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE],
+    ]);
+    $field_storage->save();
+
+    $field = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => 'date_content',
+    ]);
+    $field->save();
+    \Drupal::service('entity_display.repository')->getViewDisplay('node', 'date_content')
+      ->setComponent($field_name, [
+        'type' => 'daterange_default',
+        'label' => 'hidden',
+        'settings' => [
+          'format_type' => 'short',
+          'separator' => 'THE_SEPARATOR',
+        ],
+      ])
+      ->save();
+    $this->drupalGet("admin/structure/types/manage/date_content/display");
+
+    $page = $this->getSession()->getPage();
+    $page->pressButton("{$field_name}_settings_edit");
+    $this->assertSession()->waitForElement('css', '.ajax-new-content');
+
+    $from_to_locator = 'fields[' . $field_name . '][settings_edit_form][settings][from_to]';
+    $separator = $page->findField('Date separator');
+
+    // Assert that date separator field is visible if 'from_to' is set to
+    // BOTH.
+    $this->assertSession()->fieldValueEquals($from_to_locator, DateTimeRangeConstantsInterface::BOTH);
+    $this->assertTrue($separator->isVisible());
+    // Assert that the date separator is not visible if 'from_to' is set to
+    // START_DATE or END_DATE.
+    $page->selectFieldOption($from_to_locator, DateTimeRangeConstantsInterface::START_DATE);
+    $this->assertFalse($separator->isVisible());
+    $page->selectFieldOption($from_to_locator, DateTimeRangeConstantsInterface::END_DATE);
+    $this->assertFalse($separator->isVisible());
+  }
+
+}
-- 
GitLab