diff --git a/core/modules/datetime/config/schema/datetime.schema.yml b/core/modules/datetime/config/schema/datetime.schema.yml
index 406a2fd..57f70ac 100644
--- a/core/modules/datetime/config/schema/datetime.schema.yml
+++ b/core/modules/datetime/config/schema/datetime.schema.yml
@@ -1,5 +1,7 @@
 # Schema for the configuration files of the Datetime module.
 
+# Datetime field type.
+
 field.storage_settings.datetime:
   type: mapping
   label: 'Datetime settings'
@@ -83,3 +85,72 @@ field.widget.settings.datetime_datelist:
 field.widget.settings.datetime_default:
   type: mapping
   label: 'Datetime default display format settings'
+
+# Daterange field type.
+
+field.storage_settings.daterange:
+  type: field.storage_settings.datetime
+  label: 'Date range settings'
+
+field.field_settings.daterange:
+  type: field.field_settings.datetime
+  label: 'Date range settings'
+
+field.value.daterange:
+  type: mapping
+  label: 'Default value'
+  mapping:
+    default_date_type:
+      type: string
+      label: 'Default start date type'
+    default_date:
+      type: string
+      label: 'Default start date value'
+    default_end_date_type:
+      type: string
+      label: 'Default end date type'
+    default_end_date:
+      type: string
+      label: 'Default end date value'
+
+field.formatter.settings.daterange_default:
+  type: field.formatter.settings.datetime_default
+  label: 'Date range default display format settings'
+  mapping:
+    separator:
+      type: string
+      label: 'Separator'
+
+field.formatter.settings.daterange_plain:
+  type: field.formatter.settings.datetime_plain
+  label: 'Date range plain display format settings'
+  mapping:
+    separator:
+      type: string
+      label: 'Separator'
+
+field.formatter.settings.daterange_custom:
+  type: field.formatter.settings.datetime_custom
+  label: 'Date range custom display format settings'
+  mapping:
+    separator:
+      type: string
+      label: 'Separator'
+
+field.widget.settings.daterange_datelist:
+  type: mapping
+  label: 'Date range select list display format settings'
+  mapping:
+    increment:
+      type: integer
+      label: 'Time increments'
+    date_order:
+      type: string
+      label: 'Date part order'
+    time_type:
+      type: string
+      label: 'Time type'
+
+field.widget.settings.daterange_default:
+  type: mapping
+  label: 'Date range default display format settings'
diff --git a/core/modules/datetime/datetime.views.inc b/core/modules/datetime/datetime.views.inc
index d3b0d18..564592b 100644
--- a/core/modules/datetime/datetime.views.inc
+++ b/core/modules/datetime/datetime.views.inc
@@ -11,6 +11,16 @@
  * Implements hook_field_views_data().
  */
 function datetime_field_views_data(FieldStorageConfigInterface $field_storage) {
+  // The 'datetime' argument, filter, and sort handlers do not yet work for
+  // 'daterange' fields, so return an empty array for them, causing
+  // views_views_data() to fall back to the generic Views integration provided
+  // by views_field_default_views_data().
+  // @todo Fix the handlers and remove this in
+  //   https://www.drupal.org/node/2786577.
+  if ($field_storage->getType() !== 'datetime') {
+    return [];
+  }
+
   // @todo This code only covers configurable fields, handle base table fields
   //   in https://www.drupal.org/node/2489476.
   $data = views_field_default_views_data($field_storage);
diff --git a/core/modules/datetime/src/DateTimeComputed.php b/core/modules/datetime/src/DateTimeComputed.php
index 6939994..6b33161 100644
--- a/core/modules/datetime/src/DateTimeComputed.php
+++ b/core/modules/datetime/src/DateTimeComputed.php
@@ -40,6 +40,7 @@ public function getValue($langcode = NULL) {
       return $this->date;
     }
 
+    /** @var \Drupal\Core\Field\FieldItemInterface $item */
     $item = $this->getParent();
     $value = $item->{($this->definition->getSetting('date source'))};
 
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
new file mode 100644
index 0000000..44553db
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeCustomFormatter.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Custom' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as plain text, with fully
+ * configurable a date format using the PHP date syntax and separator.
+ *
+* @FieldFormatter(
+ *   id = "daterange_custom",
+ *   label = @Translation("Custom"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeCustomFormatter extends DateTimeCustomFormatter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'separator' => '-',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if (!empty($item->start_date) && !empty($item->end_date)) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($start_date->format('U') !== $end_date->format('U')) {
+          $elements[$delta] = [
+            'start_date' => $this->buildDate($start_date),
+            'separator' => ['#plain_text' => ' ' . $separator . ' '],
+            'end_date' => $this->buildDate($end_date),
+          ];
+        }
+        else {
+          $elements[$delta] = $this->buildDate($start_date);
+        }
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    if ($separator = $this->getSetting('separator')) {
+      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+    }
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
new file mode 100644
index 0000000..1616bb8
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangeDefaultFormatter.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Default' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range using <time> elements, with
+ * configurable date formats (from the list of configured formats) and
+ * separator.
+ *
+ * @FieldFormatter(
+ *   id = "daterange_default",
+ *   label = @Translation("Default"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDefaultFormatter extends DateTimeDefaultFormatter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'separator' => '-',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if (!empty($item->start_date) && !empty($item->end_date)) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($start_date->format('U') !== $end_date->format('U')) {
+          $elements[$delta] = [
+            'start_date' => $this->buildDate($start_date),
+            'separator' => ['#plain_text' => ' ' . $separator . ' '],
+            'end_date' => $this->buildDate($end_date),
+          ];
+        }
+        else {
+          $elements[$delta] = $this->buildDate($start_date);
+        }
+
+        if (!empty($item->_attributes)) {
+          $elements[$delta]['#attributes'] += $item->_attributes;
+          // Unset field item attributes since they have been included in the
+          // formatter output and should not be rendered in the field template.
+          unset($item->_attributes);
+        }
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    if ($separator = $this->getSetting('separator')) {
+      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+    }
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
new file mode 100644
index 0000000..622135c
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateRangePlainFormatter.php
@@ -0,0 +1,93 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'Plain' formatter for 'daterange' fields.
+ *
+ * This formatter renders the data range as a plain text string, with a
+ * configurable separator using an ISO-like date format string.
+ *
+ * @FieldFormatter(
+ *   id = "daterange_plain",
+ *   label = @Translation("Plain"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangePlainFormatter extends DateTimePlainFormatter {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'separator' => '-',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewElements(FieldItemListInterface $items, $langcode) {
+    $elements = [];
+    $separator = $this->getSetting('separator');
+
+    foreach ($items as $delta => $item) {
+      if (!empty($item->start_date) && !empty($item->end_date)) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item->start_date;
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item->end_date;
+
+        if ($start_date->format('U') !== $end_date->format('U')) {
+          $elements[$delta] = [
+            'start_date' => $this->buildDate($start_date),
+            'separator' => ['#plain_text' => ' ' . $separator . ' '],
+            'end_date' => $this->buildDate($end_date),
+          ];
+        }
+        else {
+          $elements[$delta] = $this->buildDate($start_date);
+        }
+      }
+    }
+
+    return $elements;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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'),
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = parent::settingsSummary();
+
+    if ($separator = $this->getSetting('separator')) {
+      $summary[] = $this->t('Separator: %separator', ['%separator' => $separator]);
+    }
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
index 678ddc8..9d96c54 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeCustomFormatter.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 
 /**
  * Plugin implementation of the 'Custom' formatter for 'datetime' fields.
@@ -35,27 +36,10 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = array();
 
     foreach ($items as $delta => $item) {
-      $output = '';
       if (!empty($item->date)) {
         /** @var \Drupal\Core\Datetime\DrupalDateTime $date */
-        $date = $item->date;
-
-        if ($this->getFieldSetting('datetime_type') == 'date') {
-          // A date without time will pick up the current time, use the default.
-          datetime_date_default_time($date);
-        }
-        $this->setTimeZone($date);
-
-        $output = $this->formatDate($date);
+        $elements[$delta] = $this->buildDate($item->date);
       }
-      $elements[$delta] = [
-        '#markup' => $output,
-        '#cache' => [
-          'contexts' => [
-            'timezone',
-          ],
-        ],
-      ];
     }
 
     return $elements;
@@ -66,13 +50,31 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    */
   protected function formatDate($date) {
     $format = $this->getSetting('date_format');
-    $timezone = $this->getSetting('timezone_override');
+    $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName();
     return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
   }
 
   /**
    * {@inheritdoc}
    */
+  protected function buildDate($date) {
+    if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // A date without time will pick up the current time, use the default.
+      datetime_date_default_time($date);
+    }
+    $this->setTimeZone($date);
+
+    $build = [
+      '#plain_text' => $this->formatDate($date),
+    ];
+    $build['#cache']['contexts'][] = 'timezone';
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
 
@@ -94,7 +96,7 @@ public function settingsSummary() {
 
     $date = new DrupalDateTime();
     $this->setTimeZone($date);
-    $summary[] = $date->format($this->getSetting('date_format'), $this->getFormatSettings());
+    $summary[] = $this->t('Format: @display', ['@display' => $this->formatDate($date)]);
 
     return $summary;
   }
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php
index 03c92ae..59783aa 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeDefaultFormatter.php
@@ -5,6 +5,7 @@
 use Drupal\Core\Datetime\DrupalDateTime;
 use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 
 /**
  * Plugin implementation of the 'Default' formatter for 'datetime' fields.
@@ -35,45 +36,17 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = array();
 
     foreach ($items as $delta => $item) {
-      $output = '';
-      $iso_date = '';
-
-      if ($item->date) {
+      if (!empty($item->date)) {
         /** @var \Drupal\Core\Datetime\DrupalDateTime $date */
-        $date = $item->date;
-
-        if ($this->getFieldSetting('datetime_type') == 'date') {
-          // A date without time will pick up the current time, use the default.
-          datetime_date_default_time($date);
+        // Display the date using theme datetime.
+        $elements[$delta] = $this->buildDate($item->date);
+
+        if (!empty($item->_attributes)) {
+          $elements[$delta]['#attributes'] += $item->_attributes;
+          // Unset field item attributes since they have been included in the
+          // formatter output and should not be rendered in the field template.
+          unset($item->_attributes);
         }
-
-        // Create the ISO date in Universal Time.
-        $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z';
-
-        $this->setTimeZone($date);
-
-        $output = $this->formatDate($date);
-      }
-
-      // Display the date using theme datetime.
-      $elements[$delta] = array(
-        '#cache' => [
-          'contexts' => [
-            'timezone',
-          ],
-        ],
-        '#theme' => 'time',
-        '#text' => $output,
-        '#html' => FALSE,
-        '#attributes' => array(
-          'datetime' => $iso_date,
-        ),
-      );
-      if (!empty($item->_attributes)) {
-        $elements[$delta]['#attributes'] += $item->_attributes;
-        // Unset field item attributes since they have been included in the
-        // formatter output and should not be rendered in the field template.
-        unset($item->_attributes);
       }
     }
 
@@ -93,6 +66,33 @@ protected function formatDate($date) {
   /**
    * {@inheritdoc}
    */
+  protected function buildDate($date) {
+    if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // A date without time will pick up the current time, use the default.
+      datetime_date_default_time($date);
+    }
+
+    // Create the ISO date in Universal Time.
+    $iso_date = $date->format("Y-m-d\TH:i:s") . 'Z';
+
+    $this->setTimeZone($date);
+
+    $build = [
+      '#theme' => 'time',
+      '#text' => $this->formatDate($date),
+      '#html' => FALSE,
+      '#attributes' => [
+        'datetime' => $iso_date,
+      ],
+    ];
+    $build['#cache']['contexts'][] = 'timezone';
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
   public function settingsForm(array $form, FormStateInterface $form_state) {
     $form = parent::settingsForm($form, $form_state);
 
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
index be9df38..8d04e44 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimeFormatterBase.php
@@ -129,6 +129,17 @@ public function settingsSummary() {
   abstract protected function formatDate($date);
 
   /**
+   * Creates a render array from a data object.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   A date object.
+   *
+   * @return array
+   *   A render array.
+   */
+  abstract protected function buildDate($date);
+
+  /**
    * Sets the proper time zone on a DrupalDateTime object for the current user.
    *
    * A DrupalDateTime object loaded from the database will have the UTC time
diff --git a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
index 683d75c..145b0b0 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldFormatter/DateTimePlainFormatter.php
@@ -25,27 +25,10 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = array();
 
     foreach ($items as $delta => $item) {
-      $output = '';
       if (!empty($item->date)) {
         /** @var \Drupal\Core\Datetime\DrupalDateTime $date */
-        $date = $item->date;
-
-        if ($this->getFieldSetting('datetime_type') == 'date') {
-          // A date without time will pick up the current time, use the default.
-          datetime_date_default_time($date);
-        }
-        $this->setTimeZone($date);
-
-        $output = $this->formatDate($date);
+        $elements[$delta] = $this->buildDate($item->date);
       }
-      $elements[$delta] = [
-        '#cache' => [
-          'contexts' => [
-            'timezone',
-          ],
-        ],
-        '#markup' => $output,
-      ];
     }
 
     return $elements;
@@ -56,8 +39,26 @@ public function viewElements(FieldItemListInterface $items, $langcode) {
    */
   protected function formatDate($date) {
     $format = $this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
-    $timezone = $this->getSetting('timezone_override');
+    $timezone = $this->getSetting('timezone_override') ?: $date->getTimezone()->getName();
     return $this->dateFormatter->format($date->getTimestamp(), 'custom', $format, $timezone != '' ? $timezone : NULL);
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildDate($date) {
+    if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // A date without time will pick up the current time, use the default.
+      datetime_date_default_time($date);
+    }
+    $this->setTimeZone($date);
+
+    $build = [
+      '#plain_text' => $this->formatDate($date),
+    ];
+    $build['#cache']['contexts'][] = 'timezone';
+
+    return $build;
+  }
+
 }
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
new file mode 100644
index 0000000..f04efb1
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeFieldItemList.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldType;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Represents a configurable entity daterange field.
+ */
+class DateRangeFieldItemList extends DateTimeFieldItemList {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesForm(array &$form, FormStateInterface $form_state) {
+    if (empty($this->getFieldDefinition()->getDefaultValueCallback())) {
+      $default_value = $this->getFieldDefinition()->getDefaultValueLiteral();
+
+      $element = parent::defaultValuesForm($form, $form_state);
+
+      $element['default_date_type']['#title'] = $this->t('Default start date');
+      $element['default_date_type']['#description'] = $this->t('Set a default value for the start date.');
+
+      $element['default_end_date_type'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Default end date'),
+        '#description' => $this->t('Set a default value for the end date.'),
+        '#default_value' => isset($default_value[0]['default_end_date_type']) ? $default_value[0]['default_end_date_type'] : '',
+        '#options' => [
+          static::DEFAULT_VALUE_NOW => $this->t('Current date'),
+          static::DEFAULT_VALUE_CUSTOM => $this->t('Relative date'),
+        ],
+        '#empty_value' => '',
+      ];
+
+      $element['default_end_date'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Relative default value'),
+        '#description' => $this->t("Describe a time by reference to the current day, like '+90 days' (90 days from the day the field is created) or '+1 Saturday' (the next Saturday). See <a href=\"http://php.net/manual/function.strtotime.php\">strtotime</a> for more details."),
+        '#default_value' => (isset($default_value[0]['default_end_date_type']) && $default_value[0]['default_end_date_type'] == static::DEFAULT_VALUE_CUSTOM) ? $default_value[0]['default_end_date'] : '',
+        '#states' => [
+          'visible' => [
+            ':input[id="edit-default-value-input-default-end-date-type"]' => ['value' => static::DEFAULT_VALUE_CUSTOM],
+          ],
+        ],
+      ];
+
+      return $element;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesFormValidate(array $element, array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+      $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_date']));
+      if (!$is_strtotime) {
+        $form_state->setErrorByName('default_value_input][default_date', $this->t('The relative start date value entered is invalid.'));
+      }
+    }
+
+    if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_CUSTOM) {
+      $is_strtotime = @strtotime($form_state->getValue(['default_value_input', 'default_end_date']));
+      if (!$is_strtotime) {
+        $form_state->setErrorByName('default_value_input][default_end_date', $this->t('The relative end date value entered is invalid.'));
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultValuesFormSubmit(array $element, array &$form, FormStateInterface $form_state) {
+    if ($form_state->getValue(['default_value_input', 'default_date_type']) || $form_state->getValue(['default_value_input', 'default_end_date_type'])) {
+      if ($form_state->getValue(['default_value_input', 'default_date_type']) == static::DEFAULT_VALUE_NOW) {
+        $form_state->setValueForElement($element['default_date'], static::DEFAULT_VALUE_NOW);
+      }
+      if ($form_state->getValue(['default_value_input', 'default_end_date_type']) == static::DEFAULT_VALUE_NOW) {
+        $form_state->setValueForElement($element['default_end_date'], static::DEFAULT_VALUE_NOW);
+      }
+      return [$form_state->getValue('default_value_input')];
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function processDefaultValue($default_value, FieldableEntityInterface $entity, FieldDefinitionInterface $definition) {
+    // Explicitly call the base class so tht we can get the default value types.
+    $default_value = FieldItemList::processDefaultValue($default_value, $entity, $definition);
+
+    // Allow either the start or end date to have a default, but not the other.
+    if (!empty($default_value[0]['default_date_type']) || !empty($default_value[0]['default_end_date_type'])) {
+      // A default value should be in the format and timezone used for date
+      // storage. All-day ranges are stored the same as date+time ranges.  We
+      // only provide a default value for the first item, as do all fields.
+      // Otherwise, there is no way to clear out unwanted values on multiple
+      // value fields.
+      $storage_format = $definition->getSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATE ? DATETIME_DATE_STORAGE_FORMAT : DATETIME_DATETIME_STORAGE_FORMAT;
+      $default_values = [[]];
+
+      if (!empty($default_value[0]['default_date_type'])) {
+        $start_date = new DrupalDateTime($default_value[0]['default_date'], DATETIME_STORAGE_TIMEZONE);
+        $start_value = $start_date->format($storage_format);
+        $default_values[0]['value'] = $start_value;
+        $default_values[0]['start_date'] = $start_date;
+      }
+
+      if (!empty($default_value[0]['default_end_date_type'])) {
+        $end_date = new DrupalDateTime($default_value[0]['default_end_date'], DATETIME_STORAGE_TIMEZONE);
+        $end_value = $end_date->format($storage_format);
+        $default_values[0]['end_value'] = $end_value;
+        $default_values[0]['end_date'] = $end_date;
+      }
+
+      $default_value = $default_values;
+    }
+
+    return $default_value;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
new file mode 100644
index 0000000..3ab96ad
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldType/DateRangeItem.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldType;
+
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\TypedData\DataDefinition;
+use Drupal\datetime\DateTimeComputed;
+
+/**
+ * Plugin implementation of the 'daterange' field type.
+ *
+ * @FieldType(
+ *   id = "daterange",
+ *   label = @Translation("Date range"),
+ *   description = @Translation("Create and store date ranges."),
+ *   default_widget = "daterange_default",
+ *   default_formatter = "daterange_default",
+ *   list_class = "\Drupal\datetime\Plugin\Field\FieldType\DateRangeFieldItemList"
+ * )
+ */
+class DateRangeItem extends DateTimeItem {
+
+  /**
+   * Value for the 'datetime_type' setting: store a date and time.
+   */
+  const DATETIME_TYPE_ALLDAY = 'allday';
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties['value'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('Start date value'))
+      ->setRequired(TRUE);
+
+    $properties['start_date'] = DataDefinition::create('any')
+      ->setLabel(t('Computed start date'))
+      ->setDescription(t('The computed start DateTime object.'))
+      ->setComputed(TRUE)
+      ->setClass(DateTimeComputed::class)
+      ->setSetting('date source', 'value');
+
+    $properties['end_value'] = DataDefinition::create('datetime_iso8601')
+      ->setLabel(t('End date value'))
+      ->setRequired(TRUE);
+
+    $properties['end_date'] = DataDefinition::create('any')
+      ->setLabel(t('Computed end date'))
+      ->setDescription(t('The computed end DateTime object.'))
+      ->setComputed(TRUE)
+      ->setClass(DateTimeComputed::class)
+      ->setSetting('date source', 'end_value');
+
+    return $properties;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function schema(FieldStorageDefinitionInterface $field_definition) {
+    $schema = parent::schema($field_definition);
+
+    $schema['columns']['value']['description'] = 'The start date value.';
+
+    $schema['columns']['end_value'] = [
+      'description' => 'The end date value.',
+    ] + $schema['columns']['value'];
+
+    $schema['indexes']['end_value'] = ['end_value'];
+
+    return $schema;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) {
+    $element = parent::storageSettingsForm($form, $form_state, $has_data);
+
+    $element['datetime_type']['#options'][static::DATETIME_TYPE_ALLDAY] = $this->t('All Day');
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function generateSampleValue(FieldDefinitionInterface $field_definition) {
+    $type = $field_definition->getSetting('datetime_type');
+
+    // Just pick a date in the past year. No guidance is provided by this Field
+    // type.
+    $start = REQUEST_TIME - mt_rand(0, 86400 * 365) - 86400;
+    $end = $start + 86400;
+    if ($type == static::DATETIME_TYPE_DATETIME) {
+      $values['value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $start);
+      $values['end_value'] = gmdate(DATETIME_DATETIME_STORAGE_FORMAT, $end);
+    }
+    else {
+      $values['value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $start);
+      $values['end_value'] = gmdate(DATETIME_DATE_STORAGE_FORMAT, $end);
+    }
+    return $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $start_value = $this->get('value')->getValue();
+    $end_value = $this->get('end_value')->getValue();
+    return ($start_value === NULL || $start_value === '') && ($end_value === NULL || $end_value === '');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onChange($property_name, $notify = TRUE) {
+    // Enforce that the computed date is recalculated.
+    if ($property_name == 'value') {
+      $this->start_date = NULL;
+    }
+    elseif ($property_name == 'end_value') {
+      $this->end_date = NULL;
+    }
+    parent::onChange($property_name, $notify);
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
new file mode 100644
index 0000000..65e8edd
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDatelistWidget.php
@@ -0,0 +1,157 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Plugin implementation of the 'daterange_datelist' widget.
+ *
+ * @FieldWidget(
+ *   id = "daterange_datelist",
+ *   label = @Translation("Select list"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDatelistWidget extends DateRangeWidgetBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+      'increment' => '15',
+      'date_order' => 'YMD',
+      'time_type' => '24',
+    ] + parent::defaultSettings();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    $date_order = $this->getSetting('date_order');
+
+    if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+      $time_type = $this->getSetting('time_type');
+      $increment = $this->getSetting('increment');
+    }
+    else {
+      $time_type = '';
+      $increment = '';
+    }
+
+    // Set up the date part order array.
+    switch ($date_order) {
+      default:
+      case 'YMD':
+        $date_part_order = ['year', 'month', 'day'];
+        break;
+
+      case 'MDY':
+        $date_part_order = ['month', 'day', 'year'];
+        break;
+
+      case 'DMY':
+        $date_part_order = ['day', 'month', 'year'];
+        break;
+    }
+    switch ($time_type) {
+      case '24':
+        $date_part_order = array_merge($date_part_order, ['hour', 'minute']);
+        break;
+
+      case '12':
+        $date_part_order = array_merge($date_part_order, ['hour', 'minute', 'ampm']);
+        break;
+
+      case 'none':
+        break;
+    }
+
+    $element['value'] = [
+      '#type' => 'datelist',
+      '#date_increment' => $increment,
+      '#date_part_order' => $date_part_order,
+    ] + $element['value'];
+
+    $element['end_value'] = [
+      '#type' => 'datelist',
+      '#date_increment' => $increment,
+      '#date_part_order' => $date_part_order,
+    ] + $element['end_value'];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  function settingsForm(array $form, FormStateInterface $form_state) {
+    $element = parent::settingsForm($form, $form_state);
+
+    $element['date_order'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Date part order'),
+      '#default_value' => $this->getSetting('date_order'),
+      '#options' => ['MDY' => $this->t('Month/Day/Year'), 'DMY' => $this->t('Day/Month/Year'), 'YMD' => $this->t('Year/Month/Day')],
+    ];
+
+    if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+      $element['time_type'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Time type'),
+        '#default_value' => $this->getSetting('time_type'),
+        '#options' => ['24' => $this->t('24 hour time'), '12' => $this->t('12 hour time')],
+      ];
+
+      $element['increment'] = [
+        '#type' => 'select',
+        '#title' => $this->t('Time increments'),
+        '#default_value' => $this->getSetting('increment'),
+        '#options' => [
+          1 => $this->t('1 minute'),
+          5 => $this->t('5 minute'),
+          10 => $this->t('10 minute'),
+          15 => $this->t('15 minute'),
+          30 => $this->t('30 minute'),
+        ],
+      ];
+    }
+    else {
+      $element['time_type'] = [
+        '#type' => 'hidden',
+        '#value' => 'none',
+      ];
+
+      $element['increment'] = [
+        '#type' => 'hidden',
+        '#value' => $this->getSetting('increment'),
+      ];
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = [];
+
+    $summary[] = $this->t('Date part order: @order', ['@order' => $this->getSetting('date_order')]);
+    if ($this->getFieldSetting('datetime_type') == DateRangeItem::DATETIME_TYPE_DATETIME) {
+      $summary[] = $this->t('Time type: @time_type', ['@time_type' => $this->getSetting('time_type')]);
+      $summary[] = $this->t('Time increments: @increment', ['@increment' => $this->getSetting('increment')]);
+    }
+
+    return $summary;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
new file mode 100644
index 0000000..bd311d2
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeDefaultWidget.php
@@ -0,0 +1,101 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'daterange_default' widget.
+ *
+ * @FieldWidget(
+ *   id = "daterange_default",
+ *   label = @Translation("Date and time range"),
+ *   field_types = {
+ *     "daterange"
+ *   }
+ * )
+ */
+class DateRangeDefaultWidget extends DateRangeWidgetBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The date format storage.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface
+   */
+  protected $dateStorage;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityStorageInterface $date_storage) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+
+    $this->dateStorage = $date_storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('entity_type.manager')->getStorage('date_format')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+
+    // Identify the type of date and time elements to use.
+    switch ($this->getFieldSetting('datetime_type')) {
+      case DateRangeItem::DATETIME_TYPE_DATE:
+      case DateRangeItem::DATETIME_TYPE_ALLDAY:
+        $date_type = 'date';
+        $time_type = 'none';
+        $date_format = $this->dateStorage->load('html_date')->getPattern();
+        $time_format = '';
+        break;
+
+      default:
+        $date_type = 'date';
+        $time_type = 'time';
+        $date_format = $this->dateStorage->load('html_date')->getPattern();
+        $time_format = $this->dateStorage->load('html_time')->getPattern();
+        break;
+    }
+
+    $element['value'] += [
+      '#date_date_format' => $date_format,
+      '#date_date_element' => $date_type,
+      '#date_date_callbacks' => [],
+      '#date_time_format' => $time_format,
+      '#date_time_element' => $time_type,
+      '#date_time_callbacks' => [],
+    ];
+
+    $element['end_value'] += [
+      '#date_date_format' => $date_format,
+      '#date_date_element' => $date_type,
+      '#date_date_callbacks' => [],
+      '#date_time_format' => $time_format,
+      '#date_time_element' => $time_type,
+      '#date_time_callbacks' => [],
+    ];
+
+    return $element;
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
new file mode 100644
index 0000000..ce0964d
--- /dev/null
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateRangeWidgetBase.php
@@ -0,0 +1,139 @@
+<?php
+
+namespace Drupal\datetime\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\datetime\Plugin\Field\FieldType\DateRangeItem;
+
+/**
+ * Base class for the 'daterange_*' widgets.
+ */
+class DateRangeWidgetBase extends DateTimeWidgetBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    $element = parent::formElement($items, $delta, $element, $form, $form_state);
+    $element['#element_validate'][] = [$this, 'validateStartEnd'];
+    $element['value']['#title'] = $this->t('Start');
+
+    $element['end_value'] = [
+      '#title' => $this->t('End'),
+    ] + $element['value'];
+
+    if ($items[$delta]->start_date) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+      $start_date = $items[$delta]->start_date;
+      $element['value']['#default_value'] = $this->createDefaultValue($start_date, $element['value']['#date_timezone']);
+    }
+
+    if ($items[$delta]->end_date) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+      $end_date = $items[$delta]->end_date;
+      $element['end_value']['#default_value'] = $this->createDefaultValue($end_date, $element['end_value']['#date_timezone']);
+    }
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    // The widget form element type has transformed the value to a
+    // DrupalDateTime object at this point. We need to convert it back to the
+    // storage timezone and format.
+    foreach ($values as &$item) {
+      if (!empty($item['value']) && $item['value'] instanceof DrupalDateTime) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $start_date */
+        $start_date = $item['value'];
+        switch ($this->getFieldSetting('datetime_type')) {
+          case DateRangeItem::DATETIME_TYPE_DATE:
+            // If this is a date-only field, set it to the default time so the
+            // timezone conversion can be reversed.
+            datetime_date_default_time($start_date);
+            $format = DATETIME_DATE_STORAGE_FORMAT;
+            break;
+
+          case DateRangeItem::DATETIME_TYPE_ALLDAY:
+            // All day fields start at midnight on the starting date, but are
+            // stored like datetime fields, so we need to adjust the time.
+            // This function is called twice, so to prevent a double conversion
+            // we need to explicitly set the timezone.
+            $start_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+            $start_date->setTime(0, 0, 0);
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+
+          default:
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+        }
+        // Adjust the date for storage.
+        $start_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+        $item['value'] = $start_date->format($format);
+      }
+
+      if (!empty($item['end_value']) && $item['end_value'] instanceof DrupalDateTime) {
+        /** @var \Drupal\Core\Datetime\DrupalDateTime $end_date */
+        $end_date = $item['end_value'];
+        switch ($this->getFieldSetting('datetime_type')) {
+          case DateRangeItem::DATETIME_TYPE_DATE:
+            // If this is a date-only field, set it to the default time so the
+            // timezone conversion can be reversed.
+            datetime_date_default_time($end_date);
+            $format = DATETIME_DATE_STORAGE_FORMAT;
+            break;
+
+          case DateRangeItem::DATETIME_TYPE_ALLDAY:
+            // All day fields end at midnight on the end date, but are
+            // stored like datetime fields, so we need to adjust the time.
+            // This function is called twice, so to prevent a double conversion
+            // we need to explicitly set the timezone.
+            $end_date->setTimeZone(timezone_open(drupal_get_user_timezone()));
+            $end_date->setTime(23, 59, 59);
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+
+          default:
+            $format = DATETIME_DATETIME_STORAGE_FORMAT;
+            break;
+        }
+        // Adjust the date for storage.
+        $end_date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+        $item['end_value'] = $end_date->format($format);
+      }
+    }
+
+    return $values;
+  }
+
+  /**
+   * #element_validate callback to ensure that the start date <= the end date.
+   *
+   * @param array $element
+   *   An associative array containing the properties and children of the
+   *   generic form element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array $complete_form
+   *   The complete form structure.
+   */
+  public function validateStartEnd(&$element, FormStateInterface $form_state, &$complete_form) {
+    $start_date = $element['value']['#value']['object'];
+    $end_date = $element['end_value']['#value']['object'];
+
+    if ($start_date instanceof DrupalDateTime && $end_date instanceof DrupalDateTime) {
+      if ($start_date->format('U') !== $end_date->format('U')) {
+        $interval = $start_date->diff($end_date);
+        if ($interval->invert === 1) {
+          $form_state->setError($element, $this->t('The @title end date cannot be before the start date', ['@title' => $element['#title']]));
+        }
+      }
+    }
+  }
+
+}
diff --git a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
index aa6175a..054d3e0 100644
--- a/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
+++ b/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php
@@ -41,16 +41,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
     }
 
     if ($items[$delta]->date) {
+      /** @var \Drupal\Core\Datetime\DrupalDateTime $date */
       $date = $items[$delta]->date;
-      // The date was created and verified during field_load(), so it is safe to
-      // use without further inspection.
-      if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
-        // A date without time will pick up the current time, use the default
-        // time.
-        datetime_date_default_time($date);
-      }
-      $date->setTimezone(new \DateTimeZone($element['value']['#date_timezone']));
-      $element['value']['#default_value'] = $date;
+      $element['value']['#default_value'] = $this->createDefaultValue($date, $element['value']['#date_timezone']);
     }
 
     return $element;
@@ -86,4 +79,30 @@ public function massageFormValues(array $values, array $form, FormStateInterface
     return $values;
   }
 
+  /**
+   * Creates a date object for use as a default value.
+   *
+   * This will take a default value, apply the proper timezone for display in
+   * a widget, and set the default time for date-only fields.
+   *
+   * @param \Drupal\Core\Datetime\DrupalDateTime $date
+   *   The UTC default date.
+   * @param string $timezone
+   *   The timezone to apply.
+   *
+   * @return \Drupal\Core\Datetime\DrupalDateTime
+   *   A date object for use as a default value in a field widget.
+   */
+  protected function createDefaultValue($date, $timezone) {
+    // The date was created and verified during field_load(), so it is safe to
+    // use without further inspection.
+    if ($this->getFieldSetting('datetime_type') == DateTimeItem::DATETIME_TYPE_DATE) {
+      // A date without time will pick up the current time, use the default
+      // time.
+      datetime_date_default_time($date);
+    }
+    $date->setTimezone(new \DateTimeZone($timezone));
+    return $date;
+  }
+
 }
diff --git a/core/modules/datetime/src/Tests/DateRangeFieldTest.php b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
new file mode 100644
index 0000000..38d00ce
--- /dev/null
+++ b/core/modules/datetime/src/Tests/DateRangeFieldTest.php
@@ -0,0 +1,1227 @@
+<?php
+
+namespace Drupal\datetime\Tests;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Datetime\Entity\DateFormat;
+use Drupal\Core\Entity\Entity\EntityViewDisplay;
+use Drupal\datetime\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\simpletest\WebTestBase;
+
+/**
+ * Tests Daterange field functionality.
+ *
+ * @group datetime
+ */
+class DateRangeFieldTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = ['node', 'entity_test', 'datetime', 'field_ui'];
+
+  /**
+   * The default display settings to use for the formatters.
+   */
+  protected $defaultSettings;
+
+  /**
+   * An array of display options to pass to entity_get_display()
+   *
+   * @var array
+   */
+  protected $displayOptions;
+
+  /**
+   * A field storage to use in this test class.
+   *
+   * @var \Drupal\field\Entity\FieldStorageConfig
+   */
+  protected $fieldStorage;
+
+  /**
+   * The field used in this test class.
+   *
+   * @var \Drupal\field\Entity\FieldConfig
+   */
+  protected $field;
+
+  /**
+   * An array of timezone extremes to test.
+   *
+   * @var string[]
+   */
+  protected static $timezones = [
+    // UTC-12, no DST.
+    'Pacific/Kwajalein',
+    // UTC-11, no DST
+    'Pacific/Midway',
+    // UTC-7, no DST.
+    'America/Phoenix',
+    // UTC.
+    'UTC',
+    // UTC+5:30, no DST.
+    'Asia/Kolkata',
+    // UTC+12, no DST
+    'Pacific/Funafuti',
+    // UTC+13, no DST.
+    'Pacific/Tongatapu',
+  ];
+
+  /**
+   * The date formatter service.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface
+   */
+  protected $dateFormatter;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $web_user = $this->drupalCreateUser([
+      'access content',
+      'view test entity',
+      'administer entity_test content',
+      'administer entity_test form display',
+      'administer content types',
+      'administer node fields',
+    ]);
+    $this->drupalLogin($web_user);
+
+    // Create a field with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $this->fieldStorage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => 'entity_test',
+      'type' => 'daterange',
+      'settings' => ['datetime_type' => DateRangeItem::DATETIME_TYPE_DATE],
+    ]);
+    $this->fieldStorage->save();
+    $this->field = FieldConfig::create([
+      'field_storage' => $this->fieldStorage,
+      'bundle' => 'entity_test',
+      'required' => TRUE,
+    ]);
+    $this->field->save();
+
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_default',
+      ])
+      ->save();
+
+    $this->defaultSettings = [
+      'separator' => '-',
+      'timezone_override' => '',
+    ];
+
+    $this->displayOptions = [
+      'type' => 'daterange_default',
+      'label' => 'hidden',
+      'settings' => ['format_type' => 'medium'] + $this->defaultSettings,
+    ];
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $this->dateFormatter = \Drupal::service('date.formatter');
+  }
+
+  /**
+   * Tests date field functionality.
+   */
+  public function testDateRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Loop through defined timezones to test that date-only fields work at the
+    // extremes.
+    foreach (static::$timezones as $timezone) {
+
+      $this->setSiteTimezone($timezone);
+
+      // Ensure field is set to a date-only field.
+      $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE);
+      $this->fieldStorage->save();
+
+      // Display creation form.
+      $this->drupalGet('entity_test/add');
+      $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+      $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+      $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+      $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+      $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.');
+
+      // 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();
+      $time_format = DateFormat::load('html_time')->getPattern();
+
+      $edit = array(
+        "{$field_name}[0][value][date]" => $start_date->format($date_format),
+        "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+      );
+      $this->drupalPostForm(NULL, $edit, t('Save'));
+      preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+      $id = $match[1];
+      $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+      $this->assertRaw($start_date->format($date_format));
+      $this->assertNoRaw($start_date->format($time_format));
+      $this->assertRaw($end_date->format($date_format));
+      $this->assertNoRaw($end_date->format($time_format));
+
+      // Verify the date doesn't change when entity is edited through the form.
+      $entity = EntityTest::load($id);
+      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+      $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value);
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $this->drupalGet('entity_test/manage/' . $id . '/edit');
+      $this->drupalPostForm(NULL, [], t('Save'));
+      $entity = EntityTest::load($id);
+      $this->assertEqual('2012-12-31', $entity->{$field_name}->value);
+      $this->assertEqual('2013-06-06', $entity->{$field_name}->end_value);
+
+      // Formats that display a time component for date-only fields will display
+      // the default time, so that is applied before calculating the expected
+      // value.
+      datetime_date_default_time($start_date);
+      datetime_date_default_time($end_date);
+
+      // Reset display options since these get changed below.
+      $this->displayOptions =[
+        'type' => 'daterange_default',
+        'label' => 'hidden',
+        'settings' => [
+          'format_type' => 'long',
+          'separator' => 'THESEPARATOR',
+        ] + $this->defaultSettings,
+      ];
+
+      // Verify that the default formatter works.
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+
+      $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+      $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+      $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long', '', DATETIME_STORAGE_TIMEZONE);
+      $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', DATETIME_STORAGE_TIMEZONE);
+      $this->renderTestEntity($id);
+      $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+        '%value' => 'long',
+        '%expected' => $start_expected,
+        '%expected_iso' => $start_expected_iso
+      ]));
+      $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', [
+        '%value' => 'long',
+        '%expected' => $end_expected,
+        '%expected_iso' => $end_expected_iso
+      ]));
+      $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+      // Verify that the plain formatter works.
+      $this->displayOptions['type'] = 'daterange_plain';
+      $this->displayOptions['settings'] = $this->defaultSettings;
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+      $expected = $start_date->format(DATETIME_DATE_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATE_STORAGE_FORMAT);
+      $this->renderTestEntity($id);
+      $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+      // Verify that the custom formatter works.
+      $this->displayOptions['type'] = 'daterange_custom';
+      $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+      entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+        ->setComponent($field_name, $this->displayOptions)
+        ->save();
+      $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+      $this->renderTestEntity($id);
+      $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+    }
+  }
+
+  /**
+   * Tests date and time field.
+   */
+  public function testDatetimeRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure the field to a datetime field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+    $this->fieldStorage->save();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+    $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+    $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.');
+
+    // 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');
+
+    // Update the timezone to the system default.
+    $start_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+    $end_date->setTimezone(timezone_open(drupal_get_user_timezone()));
+
+    // Submit a valid date and ensure it is accepted.
+    $date_format = DateFormat::load('html_date')->getPattern();
+    $time_format = DateFormat::load('html_time')->getPattern();
+
+    $edit = array(
+      "{$field_name}[0][value][date]" => $start_date->format($date_format),
+      "{$field_name}[0][value][time]" => $start_date->format($time_format),
+      "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+      "{$field_name}[0][end_value][time]" => $end_date->format($time_format),
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+    $this->assertRaw($start_date->format($date_format));
+    $this->assertRaw($start_date->format($time_format));
+    $this->assertRaw($end_date->format($date_format));
+    $this->assertRaw($end_date->format($time_format));
+
+    // Verify that the default formatter works.
+    $this->displayOptions['settings'] = [
+        'format_type' => 'long',
+        'separator' => 'THESEPARATOR',
+      ] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+    $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long');
+    $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $this->renderTestEntity($id);
+    $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+    $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+    $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+    // Verify that the plain formatter works.
+    $this->displayOptions['type'] = 'daterange_plain';
+    $this->displayOptions['settings'] = $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'datetime_custom' formatter works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'timezone_override' setting works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+  }
+
+  /**
+   * Tests all-day field.
+   */
+  public function testAlldayRangeField() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure field is set to a all-day field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY);
+    $this->fieldStorage->save();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+    $this->assertFieldByXPath('//*[@id="edit-' . $field_name . '-wrapper"]/h4[contains(@class, "js-form-required")]', TRUE, 'Required markup found');
+    $this->assertNoFieldByName("{$field_name}[0][value][time]", '', 'Start time element not found.');
+    $this->assertNoFieldByName("{$field_name}[0][end_value][time]", '', 'End time element not found.');
+
+    // Build up dates in the proper timezone.
+    $value = '2012-12-31 00:00:00';
+    $start_date = new DrupalDateTime($value, timezone_open(drupal_get_user_timezone()));
+    $end_value = '2013-06-06 23:59:59';
+    $end_date = new DrupalDateTime($end_value, timezone_open(drupal_get_user_timezone()));
+
+    // Submit a valid date and ensure it is accepted.
+    $date_format = DateFormat::load('html_date')->getPattern();
+    $time_format = DateFormat::load('html_time')->getPattern();
+
+    $edit = array(
+      "{$field_name}[0][value][date]" => $start_date->format($date_format),
+      "{$field_name}[0][end_value][date]" => $end_date->format($date_format),
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', array('@id' => $id)));
+    $this->assertRaw($start_date->format($date_format));
+    $this->assertNoRaw($start_date->format($time_format));
+    $this->assertRaw($end_date->format($date_format));
+    $this->assertNoRaw($end_date->format($time_format));
+
+    // Verify that the default formatter works.
+    $this->displayOptions['settings'] = [
+        'format_type' => 'long',
+        'separator' => 'THESEPARATOR',
+      ] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+
+    $start_expected = $this->dateFormatter->format($start_date->getTimestamp(), 'long');
+    $start_expected_iso = $this->dateFormatter->format($start_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $end_expected = $this->dateFormatter->format($end_date->getTimestamp(), 'long');
+    $end_expected_iso = $this->dateFormatter->format($end_date->getTimestamp(), 'custom', 'Y-m-d\TH:i:s\Z', 'UTC');
+    $this->renderTestEntity($id);
+    $this->assertFieldByXPath('//time[@datetime="' . $start_expected_iso . '"]', $start_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $start_expected, '%expected_iso' => $start_expected_iso]));
+    $this->assertFieldByXPath('//time[@datetime="' . $end_expected_iso . '"]', $end_expected, new FormattableMarkup('Formatted date field using %value format displayed as %expected with %expected_iso attribute.', ['%value' => 'long', '%expected' => $end_expected, '%expected_iso' => $end_expected_iso]));
+    $this->assertText(' THESEPARATOR ', 'Found proper separator');
+
+    // Verify that the plain formatter works.
+    $this->displayOptions['type'] = 'daterange_plain';
+    $this->displayOptions['settings'] = $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format(DATETIME_DATETIME_STORAGE_FORMAT) . ' - ' . $end_date->format(DATETIME_DATETIME_STORAGE_FORMAT);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using plain format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the custom formatter works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = array('date_format' => 'm/d/Y') + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format']) . ' - ' . $end_date->format($this->displayOptions['settings']['date_format']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+
+    // Verify that the 'timezone_override' setting works.
+    $this->displayOptions['type'] = 'daterange_custom';
+    $this->displayOptions['settings'] = ['date_format' => 'm/d/Y g:i:s A', 'timezone_override' => 'America/New_York'] + $this->defaultSettings;
+    entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+      ->setComponent($field_name, $this->displayOptions)
+      ->save();
+    $expected = $start_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $expected .= ' - ' . $end_date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
+    $this->renderTestEntity($id);
+    $this->assertText($expected, new FormattableMarkup('Formatted date field using daterange_custom format displayed as %expected.', array('%expected' => $expected)));
+  }
+
+  /**
+   * Tests Date Range List Widget functionality.
+   */
+  public function testDatelistWidget() {
+    $field_name = $this->fieldStorage->getName();
+
+    // Ensure field is set to a date only field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATE);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'date_order' => 'YMD',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Assert that Hour and Minute Elements do not appear on Date Only
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+    // Go to the form display page to assert that increment option does not appear on Date Only
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+    $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+    // Change the field is set to an all day field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_ALLDAY);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'date_order' => 'YMD',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Assert that Hour and Minute Elements do not appear on Date Only
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element not found on Date Only.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-minute\"]", NULL, 'Minute element not found on Date Only.');
+
+    // Go to the form display page to assert that increment option does not appear on Date Only
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $xpathIncr = "//select[starts-with(@id, \"edit-fields-$field_name-settings-edit-form-settings-increment\")]";
+    $this->assertNoFieldByXPath($xpathIncr, NULL, 'Increment element not found for Date Only.');
+
+    // Change the field to a datetime field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+    $this->fieldStorage->save();
+
+    // Change the widget to a datelist widget.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 1,
+          'date_order' => 'YMD',
+          'time_type' => '12',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Go to the form display page to assert that increment option does appear on Date Time
+    $fieldEditUrl = 'entity_test/structure/entity_test/form-display';
+    $this->drupalGet($fieldEditUrl);
+
+    // Click on the widget settings button to open the widget settings form.
+    $this->drupalPostAjaxForm(NULL, [], $field_name . "_settings_edit");
+    $this->assertFieldByXPath($xpathIncr, NULL, 'Increment element found for Date and time.');
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    foreach (['value','end-value'] as $column) {
+      foreach (['year', 'month', 'day', 'hour', 'minute', 'ampm'] as $element) {
+        $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-$column-$element\"]", NULL, $element . ' element found.');
+        $this->assertOptionSelected("edit-$field_name-0-$column-$element", '', 'No ' . $element . ' selected.');
+      }
+    }
+
+    // Submit a valid date and ensure it is accepted.
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 5, 'minute' => 15];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+    $edit = [];
+    // Add the ampm indicator since we are testing 12 hour time.
+    $start_date_value['ampm'] = 'am';
+    $end_date_value['ampm'] = 'pm';
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][end_value][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '5', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-ampm", 'am', 'Correct ampm selected.');
+
+    $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-ampm", 'pm', 'Correct ampm selected.');
+
+    // Test the widget using increment other than 1 and 24 hour mode.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 15,
+          'date_order' => 'YMD',
+          'time_type' => '24',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+
+    // Other elements are unaffected by the changed settings.
+    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-value-hour\"]", NULL, 'Hour element found.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '', 'No hour selected.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-value-ampm\"]", NULL, 'AMPM element not found.');
+    $this->assertFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-hour\"]", NULL, 'Hour element found.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '', 'No hour selected.');
+    $this->assertNoFieldByXPath("//*[@id=\"edit-$field_name-0-end-value-ampm\"]", NULL, 'AMPM element not found.');
+
+    // Submit a valid date and ensure it is accepted.
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 17, 'minute' => 15];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][end_value][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    $this->assertOptionSelected("edit-$field_name-0-value-year", '2012', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-month", '12', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-day", '31', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-hour", '17', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '15', 'Correct minute selected.');
+
+    $this->assertOptionSelected("edit-$field_name-0-end-value-year", '2013', 'Correct year selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-month", '1', 'Correct month selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-day", '15', 'Correct day selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-hour", '3', 'Correct hour selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '30', 'Correct minute selected.');
+
+    // Test the widget for partial completion of fields.
+    entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+      ->setComponent($field_name, [
+        'type' => 'daterange_datelist',
+        'settings' => [
+          'increment' => 1,
+          'date_order' => 'YMD',
+          'time_type' => '24',
+        ],
+      ])
+      ->save();
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Test the widget for validation notifications.
+    foreach ($this->datelistDataProvider() as $data) {
+      list($start_date_value, $end_date_value, $expected) = $data;
+
+      // Display creation form.
+      $this->drupalGet('entity_test/add');
+
+      // Submit a partial date and ensure and error message is provided.
+      $edit = [];
+      foreach ($start_date_value as $part => $value) {
+        $edit["{$field_name}[0][value][$part]"] = $value;
+      }
+      foreach ($end_date_value as $part => $value) {
+        $edit["{$field_name}[0][end_value][$part]"] = $value;
+      }
+
+      $this->drupalPostForm(NULL, $edit, t('Save'));
+      $this->assertResponse(200);
+      foreach ($expected as $expected_text) {
+        $this->assertText(t($expected_text));
+      }
+    }
+
+    // Test the widget for complete input with zeros as part of selections.
+    $this->drupalGet('entity_test/add');
+
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 30];
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][end_value][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertResponse(200);
+    preg_match('|entity_test/manage/(\d+)|', $this->url, $match);
+    $id = $match[1];
+    $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+    // Test the widget to ensure zeros are not deselected on validation.
+    $this->drupalGet('entity_test/add');
+
+    $start_date_value = ['year' => 2012, 'month' => 12, 'day' => 31, 'hour' => 0, 'minute' => 0];
+    $end_date_value = ['year' => 2013, 'month' => 1, 'day' => 15, 'hour' => 3, 'minute' => 0];
+    $edit = [];
+    foreach ($start_date_value as $part => $value) {
+      $edit["{$field_name}[0][value][$part]"] = $value;
+    }
+    foreach ($end_date_value as $part => $value) {
+      $edit["{$field_name}[0][end_value][$part]"] = $value;
+    }
+
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertResponse(200);
+    $this->assertOptionSelected("edit-$field_name-0-value-minute", '0', 'Correct minute selected.');
+    $this->assertOptionSelected("edit-$field_name-0-end-value-minute", '0', 'Correct minute selected.');
+  }
+
+  /**
+   * The data provider for testing the validation of the datelist widget.
+   *
+   * @return array
+   *   An array of datelist input permutations to test.
+   */
+  protected function datelistDataProvider() {
+    return [
+      // Year only selected, validation error on Month, Day, Hour, Minute.
+      [['year' => 2012, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''],
+       ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for month.',
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year and Month selected, validation error on Day, Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '', 'hour' => '', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month and Day selected, validation error on Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month, Day and Hour selected, validation error on Minute only.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => ''],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => '30'], [
+        'A value must be selected for minute.',
+      ]],
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '', 'day' => '', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for month.',
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year and Month selected, validation error on Day, Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for day.',
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month and Day selected, validation error on Hour, Minute.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '', 'minute' => ''], [
+        'A value must be selected for hour.',
+        'A value must be selected for minute.',
+      ]],
+      // Year, Month, Day and Hour selected, validation error on Minute only.
+      [['year' => 2012, 'month' => '12', 'day' => '31', 'hour' => '0', 'minute' => '0'],
+        ['year' => 2013, 'month' => '1', 'day' => '15', 'hour' => '3', 'minute' => ''], [
+        'A value must be selected for minute.',
+      ]],
+    ];
+  }
+
+  /**
+   * Test default value functionality.
+   */
+  public function testDefaultValue() {
+    // Create a test content type.
+    $this->drupalCreateContentType(['type' => 'date_content']);
+
+    // Create a field storage with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $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();
+
+    // Set now as default_value.
+    $field_edit = [
+      'default_value_input[default_date_type]' => 'now',
+      'default_value_input[default_end_date_type]' => 'now',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-date-type', 'now', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_date]', '', 'The relative start default value is empty in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'now', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative end default value is empty in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertEqual($config_entity['default_value'][0], [
+      'default_date_type' => 'now',
+      'default_date' => 'now',
+      'default_end_date_type' => 'now',
+      'default_end_date' => 'now'
+    ], 'Default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is today.
+    $new_node = Node::create(['type' => 'date_content']);
+    $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+    // Set an invalid relative default_value to test validation.
+    $field_edit = [
+      'default_value_input[default_date_type]' => 'relative',
+      'default_value_input[default_date]' => 'invalid date',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => '+1 day',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+    $this->assertText('The relative start date value entered is invalid.');
+
+    $field_edit = [
+      'default_value_input[default_date_type]' => 'relative',
+      'default_value_input[default_date]' => '+1 day',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => 'invalid date',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+    $this->assertText('The relative end date value entered is invalid.');
+
+    // Set a relative default_value.
+    $field_edit = [
+      'default_value_input[default_date_type]' => 'relative',
+      'default_value_input[default_date]' => '+45 days',
+      'default_value_input[default_end_date_type]' => 'relative',
+      'default_value_input[default_end_date]' => '+90 days',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-date-type', 'relative', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_date]', '+45 days', 'The relative default start value is displayed in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', 'relative', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '+90 days', 'The relative default end value is displayed in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertEqual($config_entity['default_value'][0], [
+      'default_date_type' => 'relative',
+      'default_date' => '+45 days',
+      'default_end_date_type' => 'relative',
+      'default_end_date' => '+90 days',
+    ], 'Default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is +90 days.
+    $new_node = Node::create(['type' => 'date_content']);
+    $expected_start_date = new DrupalDateTime('+45 days', DATETIME_STORAGE_TIMEZONE);
+    $expected_end_date = new DrupalDateTime('+90 days', DATETIME_STORAGE_TIMEZONE);
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->value, $expected_start_date->format(DATETIME_DATE_STORAGE_FORMAT));
+    $this->assertEqual($new_node->get($field_name)->offsetGet(0)->end_value, $expected_end_date->format(DATETIME_DATE_STORAGE_FORMAT));
+
+    // Remove default value.
+    $field_edit = [
+      'default_value_input[default_date_type]' => '',
+      'default_value_input[default_end_date_type]' => '',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Check that default value is selected in default value form.
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name);
+    $this->assertOptionSelected('edit-default-value-input-default-date-type', '', 'The default start value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_date]', '', 'The relative default start value is empty in instance settings page');
+    $this->assertOptionSelected('edit-default-value-input-default-end-date-type', '', 'The default end value is selected in instance settings page');
+    $this->assertFieldByName('default_value_input[default_end_date]', '', 'The relative default end value is empty in instance settings page');
+
+    // Check if default_date has been stored successfully.
+    $config_entity = $this->config('field.field.node.date_content.' . $field_name)->get();
+    $this->assertTrue(empty($config_entity['default_value']), 'Empty default value has been stored successfully');
+
+    // Clear field cache in order to avoid stale cache values.
+    \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
+
+    // Create a new node to check that datetime field default value is not set.
+    $new_node = Node::create(['type' => 'date_content']);
+    $this->assertNull($new_node->get($field_name)->value, 'Default value is not set');
+
+    // Set now as default_value for start date only.
+    entity_get_form_display('node', 'date_content', 'default')
+      ->setComponent($field_name, [
+        'type' => 'datetime_default',
+      ])
+      ->save();
+
+    $expected_date = new DrupalDateTime('now', DATETIME_STORAGE_TIMEZONE);
+
+    $field_edit = [
+      'default_value_input[default_date_type]' => 'now',
+      'default_value_input[default_end_date_type]' => '',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Make sure only the start value is populated on node add page.
+    $this->drupalGet('node/add/date_content');
+    $this->assertFieldByName("{$field_name}[0][value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'Start date element populated.');
+    $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element empty.');
+
+    // Set now as default_value for end date only.
+    $field_edit = [
+      'default_value_input[default_date_type]' => '',
+      'default_value_input[default_end_date_type]' => 'now',
+    ];
+    $this->drupalPostForm('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name, $field_edit, t('Save settings'));
+
+    // Make sure only the start value is populated on node add page.
+    $this->drupalGet('node/add/date_content');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element empty.');
+    $this->assertFieldByName("{$field_name}[0][end_value][date]", $expected_date->format(DATETIME_DATE_STORAGE_FORMAT), 'End date element populated.');
+  }
+
+  /**
+   * Test that invalid values are caught and marked as invalid.
+   */
+  public function testInvalidField() {
+    // Change the field to a datetime field.
+    $this->fieldStorage->setSetting('datetime_type', DateRangeItem::DATETIME_TYPE_DATETIME);
+    $this->fieldStorage->save();
+    $field_name = $this->fieldStorage->getName();
+
+    // Display creation form.
+    $this->drupalGet('entity_test/add');
+    $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Start date element found.');
+    $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Start time element found.');
+    $this->assertFieldByName("{$field_name}[0][end_value][date]", '', 'End date element found.');
+    $this->assertFieldByName("{$field_name}[0][end_value][time]", '', 'End time element found.');
+
+    // Submit invalid start dates and ensure they is not accepted.
+    $date_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty start date value has been caught.');
+
+    $date_value = 'aaaa-12-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start year value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-75-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start month value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-12-99';
+    $edit = [
+      "{$field_name}[0][value][date]" => $date_value,
+      "{$field_name}[0][value][time]" => '00:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start day value %date has been caught.', ['%date' => $date_value]));
+
+    // Submit invalid start times and ensure they is not accepted.
+    $time_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty start time value has been caught.');
+
+    $time_value = '49:00:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start hour value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:99:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start minute value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:15:99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => $time_value,
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid start second value %time has been caught.', ['%time' => $time_value]));
+
+    // Submit invalid end dates and ensure they is not accepted.
+    $date_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => $date_value,
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty end date value has been caught.');
+
+    $date_value = 'aaaa-12-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => $date_value,
+      "{$field_name}[0][end_value][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end year value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-75-01';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => $date_value,
+      "{$field_name}[0][end_value][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end month value %date has been caught.', ['%date' => $date_value]));
+
+    $date_value = '2012-12-99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => $date_value,
+      "{$field_name}[0][end_value][time]" => '00:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end day value %date has been caught.', ['%date' => $date_value]));
+
+    // Submit invalid start times and ensure they is not accepted.
+    $time_value = '';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', 'Empty end time value has been caught.');
+
+    $time_value = '49:00:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end hour value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:99:00';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end minute value %time has been caught.', ['%time' => $time_value]));
+
+    $time_value = '12:15:99';
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => $time_value,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText('date is invalid', new FormattableMarkup('Invalid end second value %time has been caught.', ['%time' => $time_value]));
+
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2010-12-01',
+      "{$field_name}[0][end_value][time]" => '12:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End date before start date has been caught.');
+
+    $edit = [
+      "{$field_name}[0][value][date]" => '2012-12-01',
+      "{$field_name}[0][value][time]" => '12:00:00',
+      "{$field_name}[0][end_value][date]" => '2012-12-01',
+      "{$field_name}[0][end_value][time]" => '11:00:00',
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(new FormattableMarkup('The @title end date cannot be before the start date', ['@title' => $field_name]), 'End time before start time has been caught.');
+  }
+
+  /**
+   * Tests that 'Date' field storage setting form is disabled if field has data.
+   */
+  public function testDateStorageSettings() {
+    // Create a test content type.
+    $this->drupalCreateContentType(['type' => 'date_content']);
+
+    // Create a field storage with settings to validate.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $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,
+      'field_name' => $field_name,
+      'bundle' => 'date_content',
+    ]);
+    $field->save();
+
+    entity_get_form_display('node', 'date_content', 'default')
+      ->setComponent($field_name, [
+        'type' => 'datetime_default',
+      ])
+      ->save();
+    $edit = [
+      'title[0][value]' => $this->randomString(),
+      'body[0][value]' => $this->randomString(),
+      $field_name . '[0][value][date]' => '2016-04-01',
+      $field_name . '[0][end_value][date]' => '2016-04-02',
+    ];
+    $this->drupalPostForm('node/add/date_content', $edit, t('Save'));
+    $this->drupalGet('admin/structure/types/manage/date_content/fields/node.date_content.' . $field_name . '/storage');
+    $result = $this->xpath("//*[@id='edit-settings-datetime-type' and contains(@disabled, 'disabled')]");
+    $this->assertEqual(count($result), 1, "Changing datetime setting is disabled.");
+    $this->assertText('There is data for this field in the database. The field settings can no longer be changed.');
+  }
+
+
+  /**
+   * Renders a entity_test and sets the output in the internal browser.
+   *
+   * @param int $id
+   *   The entity_test ID to render.
+   * @param string $view_mode
+   *   (optional) The view mode to use for rendering. Defaults to 'full'.
+   * @param bool $reset
+   *   (optional) Whether to reset the entity_test controller cache. Defaults to
+   *   TRUE to simplify testing.
+   */
+  protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
+    if ($reset) {
+      \Drupal::service('entity_type.manager')->getStorage('entity_test')->resetCache([$id]);
+    }
+    $entity = EntityTest::load($id);
+    $display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
+    $build = $display->build($entity);
+    $output = \Drupal::service('renderer')->renderRoot($build);
+    $this->setRawContent($output);
+    $this->verbose($output);
+  }
+
+  /**
+   * Sets the site timezone to a given timezone.
+   *
+   * @param string $timezone
+   *   The timezone identifier to set.
+   */
+  protected function setSiteTimezone($timezone) {
+    // Set an explicit site timezone, and disallow per-user timezones.
+    $this->config('system.date')
+      ->set('timezone.user.configurable', 0)
+      // A timezone with an offset greater than UTC+12 is used.
+      ->set('timezone.default', $timezone)
+      ->save();
+  }
+
+}
