diff --git a/core/modules/datetime/tests/src/Functional/DateTestBase.php b/core/modules/datetime/tests/src/Functional/DateTestBase.php
index 369e793..00564da 100644
--- a/core/modules/datetime/tests/src/Functional/DateTestBase.php
+++ b/core/modules/datetime/tests/src/Functional/DateTestBase.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Entity\Entity\EntityViewDisplay;
 use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
 use Drupal\entity_test\Entity\EntityTest;
+use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\Tests\BrowserTestBase;
@@ -31,6 +32,13 @@
   protected $displayOptions;
 
   /**
+   * An entity storage to use in this test class.
+   *
+   * @var \Drupal\Core\Entity\EntityStorageInterface;
+  */
+  protected $entityStorage;
+
+  /**
    * A field storage to use in this test class.
    *
    * @var \Drupal\field\Entity\FieldStorageConfig
@@ -96,6 +104,8 @@ protected function setUp() {
     ]);
     $this->drupalLogin($web_user);
 
+    $this->entityStorage = $this->container->get('entity_type.manager')->getStorage('entity_test');
+
     // Create a field with settings to validate.
     $this->createField();
 
diff --git a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php
index ab03ff5..6dcf311 100644
--- a/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php
+++ b/core/modules/datetime/tests/src/Functional/DateTimeFieldTest.php
@@ -11,6 +11,10 @@
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\node\Entity\Node;
+use Drupal\user\Entity\User;
+use Drupal\datetime\Plugin\Field\ConfigurableTimezoneInterface;
+use DOMDocument;
+use DOMXPath;
 
 /**
  * Tests Datetime field functionality.
@@ -24,7 +28,11 @@ class DateTimeFieldTest extends DateTestBase {
    *
    * @var array
    */
-  protected $defaultSettings = ['timezone_override' => ''];
+  protected $defaultSettings = [
+    'timezone_default' => ConfigurableTimezoneInterface::TIMEZONE_USER,
+    'timezone_override' => '',
+    'timezone_per_date' => FALSE,
+  ];
 
   /**
    * {@inheritdoc}
@@ -288,16 +296,6 @@ public function testDatetimeField() {
     $output = $this->renderTestEntity($id);
     $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
 
-    // Verify that the 'timezone_override' setting works.
-    $this->displayOptions['type'] = 'datetime_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 = $date->format($this->displayOptions['settings']['date_format'], ['timezone' => 'America/New_York']);
-    $output = $this->renderTestEntity($id);
-    $this->assertContains($expected, $output, SafeMarkup::format('Formatted date field using datetime_custom format displayed as %expected.', ['%expected' => $expected]));
-
     // Verify that the 'datetime_time_ago' formatter works for intervals in the
     // past.  First update the test entity so that the date difference always
     // has the same interval.  Since the database always stores UTC, and the
@@ -345,6 +343,330 @@ public function testDatetimeField() {
     ]);
     $output = $this->renderTestEntity($id);
     $this->assertContains((string) $expected, $output, SafeMarkup::format('Formatted date field using datetime_time_ago format displayed as %expected.', ['%expected' => $expected]));
+
+  }
+
+  /**
+   * Test combinations of timezone configurations.
+   */
+  public function testFormatterTimezoneSettings() {
+    // Using different timezones wherever possible helps to highlight leakage.
+    // Giving timezones labels that describes the context in which they are
+    // stored makes it easier to diagnose test failures.
+    $timezones = [
+      'site' => 'Pacific/Kwajalein',
+      'user' => 'America/Phoenix',
+      'input' => 'Africa/Lagos',
+      'override'=> 'Asia/Kolkata',
+      'php' => 'Pacific/Funafuti',
+      'storage' => DATETIME_STORAGE_TIMEZONE,
+    ];
+
+    // Setup test environment.
+    // Reset php's default timezone to guarantee it's different to others here.
+    date_default_timezone_set($timezones['php']);
+    $this->config('system.date')
+      ->set('timezone.user.configurable', 1)
+      ->set('timezone.default', $timezones['site'])
+      ->save();
+    $this->setLoggedInUserTimezone($timezones['user']);
+
+    // Prepare the date that will be stored and rendered by the formatter.
+    $date = new DrupalDateTime("2012-10-15 17:25:00", 'UTC');
+    $date->setTimezone(new \DateTimezone(DATETIME_STORAGE_TIMEZONE));
+    $field_name = $this->fieldStorage->getName();
+    $fields = [
+      $field_name => [
+        'value' => $date->format(DATETIME_DATETIME_STORAGE_FORMAT),
+      ]
+    ];
+
+    // Scenario 1: Stored preference not enabled.
+    // Test the formatter when per-date time zone storage has not been enabled.
+    $this->fieldStorage->setSetting('datetime_type', 'datetime');
+    $this->fieldStorage->setSetting('timezone_storage', FALSE);
+    $this->fieldStorage->save();
+    $entity = $this->entityStorage->create($fields);
+    $entity->save();
+    $this->formatterSettingsTest('not enabled', $entity->id(), $timezones);
+
+    // Scenario 2: Stored preference unspecified.
+    // Test the formatter on the same entity, after enabling per-date storage,
+    // in which case existing entities have NULL as their stored time zone.
+    $this->fieldStorage->setSetting('datetime_type', 'datetime');
+    $this->fieldStorage->setSetting('timezone_storage', TRUE);
+    $this->fieldStorage->save();
+    $this->formatterSettingsTest('no value', $entity->id(), $timezones);
+
+    // Scenario 3: Stored preference specified.
+    // Test the formatter on a new entity, which will now have a preferred
+    // time zone stored in its field.
+    $fields[$field_name]['timezone'] = $timezones['input'];
+    $entity = $this->entityStorage->create($fields);
+    $entity->save();
+    $this->formatterSettingsTest('has value', $entity->id(), $timezones);
+
+  }
+
+  /**
+   * Tests formatter output for different sets of settings.
+   */
+  protected function formatterSettingsTest($timezone_storage, $id, $timezones) {
+    // All the possible formatter timezone settings.
+    $settings = [
+      'per_date' => [
+        'Formatter using preferred time zone for each date' => TRUE,
+        'Formatter not using preferred time zone for each date' => FALSE,
+      ],
+      'default' => [
+        'user' => ConfigurableTimezoneInterface::TIMEZONE_USER,
+        'site' => ConfigurableTimezoneInterface::TIMEZONE_SITE,
+        'override' =>ConfigurableTimezoneInterface::TIMEZONE_FIXED,
+      ],
+    ];
+
+    // Gather information about the field.
+    $field_name = $this->fieldStorage->getName();
+    $entity = $this->entityStorage->load($id);
+    $fieldValues = $entity->get($field_name)->getValue()[0];
+    $storedTimezone = $fieldValues['timezone'];
+    $storedDateInStorageFormat = $fieldValues['value'];
+
+    foreach ($settings['per_date'] as $per_date_label => $per_date) {
+      foreach ($settings['default'] as $default_label => $default) {
+        // Prepare the expectations. The timezone used should always be that set
+        // in the 'default' setting, unless the 'Use stored preference' setting
+        // is set AND a timezone is stored in the field for the date value being
+        // rendered.
+        $expected_timezone_label = array_search($default, $settings['default']);
+        if ($timezone_storage === 'has value') {
+          $this->assertEqual($timezones['input'], $storedTimezone, "Verifying the test is correctly setup with the right time zone stored.");
+          if ($per_date) {
+            $expected_timezone_label = 'input';
+          }
+        }
+
+        // Setup the formatter using the settings for the current scenario.
+        // Have the time zone displayed as part of the output.
+        $output_format = 'm/d/Y g:i:s A e';
+        $this->displayOptions['type'] = 'datetime_custom';
+        $this->displayOptions['settings'] = [
+          'date_format' => $output_format,
+          'timezone_default' => $default,
+          'timezone_override' => $timezones['override'],
+          'timezone_per_date' => $per_date,
+        ];
+        entity_get_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'full')
+          ->setComponent($field_name, $this->displayOptions)
+          ->save();
+
+        // Try to get the rendered field.
+        $output = $this->renderTestEntity($id);
+        $dom = new DOMDocument();
+        @$dom->loadHTML($output);
+        $xpath = new DOMXPath($dom);
+        $fieldXpath = $xpath->query("//div[contains(@class, 'field--type-datetime')]");
+        $fieldFound = ($fieldXpath->length === 1);
+        $this->assertTrue($fieldFound, "Looking for the div.field--type-datetime element");
+
+        // Test the rendered value
+        if ($fieldFound) {
+          // Extract a date from the rendered value
+          $actualText = trim($fieldXpath->item(0)->textContent);
+          $actualDate = DrupalDateTime::createFromFormat($output_format, $actualText);
+
+          // Prepare debug messages.
+          $scenario_message = "Per-date timezones in the field: $timezone_storage.";
+          $stored_message = new FormattableMarkup("The stored time zone is '@stored_timezone_label' ('@stored_timezone').", [
+            '@stored_timezone' => $storedTimezone,
+            '@stored_timezone_label' => array_search($storedTimezone, $timezones),
+          ]);
+          $settings_message = new FormattableMarkup("@per_date and 'Default timezone' set to '@default_label' (@default_timezone).", [
+            '@per_date' => $per_date_label,
+            '@default' => $default_label,
+            '@default_timezone' => $timezones[$default_label],
+          ]);
+          $base_message = new FormattableMarkup("Found text: '@actualText'. \n@scenario_message \n@stored_message \n@settings_message", [
+            '@actualText' => $actualText,
+            '@scenario_message' => $scenario_message,
+            '@stored_message' => $stored_message,
+            '@settings_message' => $settings_message,
+          ]);
+
+          // Test that the rendered date matches the stored date.
+          $actualDateInStorageFormat = $actualDate->format(DATETIME_DATETIME_STORAGE_FORMAT, ['timezone' => DATETIME_STORAGE_TIMEZONE]);
+          $message = new FormattableMarkup("Rendered date can be interpreted as '@actual',  expected @expected. \n@base_message.", [
+            '@actual' => $actualDateInStorageFormat,
+            '@expected' => $storedDateInStorageFormat,
+            '@base_message' => $base_message,
+          ]);
+          $this->assertEqual($storedDateInStorageFormat, $actualDateInStorageFormat, $message);
+
+          // Test that the rendered date used the expected timezone.
+          // Lookup the logical source of the timezone shown in the rendered date.
+          $actualTimeZone = $actualDate->getTimezone()->getName();
+          if (!$actualTimeSetting = array_search($actualTimeZone, $timezones)) {
+            $actualTimeSetting = $actualTimeZone;
+          }
+          // Compare that with the source expected to be in effect.
+          $message = new FormattableMarkup("Time formatted using '@actual' timezone, expected to use '@expected' timezone. \n @scenario_message @stored_message @settings_message.", [
+            '@actual' => $actualTimeSetting,
+            '@expected' => $expected_timezone_label,
+            '@base_message' => $base_message,
+          ]);
+          $this->assertIdentical($expected_timezone_label, $actualTimeSetting, $message);
+        }
+      }
+    }
+  }
+
+  /**
+   * Tests widget with different settings.
+   */
+  public function testWidgetTimezoneSettings() {
+    // Using different timezones wherever possible helps to highlight leakage.
+    // Giving timezones labels that describes the context in which they are
+    // stored makes it easier to diagnose test failures.
+    $timezones = [
+      'site' => 'Pacific/Kwajalein',
+      'user' => 'America/Phoenix',
+      'input' => 'Africa/Lagos',
+      'override'=> 'Asia/Kolkata',
+      'php' => 'Pacific/Funafuti',
+      'storage' => DATETIME_STORAGE_TIMEZONE,
+    ];
+
+    // The test date that should be saved by the widget
+    $date = new DrupalDateTime('2012-10-15 17:25:00', DATETIME_STORAGE_TIMEZONE);
+
+    // Store the test date formatted according to the different timezones, for
+    // use in error reporting and debugging.
+    $formattedDates = [];
+    foreach ($timezones as $label => $timezone) {
+      $formattedDates[$label] = $date->format(DATETIME_DATETIME_STORAGE_FORMAT, $timezone);
+    }
+
+    // Setup test environment.
+    // Reset php's default timezone to guarantee it's different to others here.
+    date_default_timezone_set($timezones['php']);
+    $this->config('system.date')
+      ->set('timezone.user.configurable', 1)
+      ->set('timezone.default', $timezones['site'])
+      ->save();
+    $this->setLoggedInUserTimezone($timezones['user']);
+
+    $scenarios = [
+      'per_date' => [
+        // The field is not configured to have per-date timezone storage.
+        'no storage' => FALSE,
+        // The field allows per-date timezones, but the widget does not.
+        'not allowed' => FALSE,
+        // The widget allows, but the editor leaves at the default timezone.
+        'default' => TRUE,
+        // The editor selects a timezone other than the default.
+        'input' => TRUE,
+      ],
+      'default' => [
+        'user' => ConfigurableTimezoneInterface::TIMEZONE_USER,
+        'site' => ConfigurableTimezoneInterface::TIMEZONE_SITE,
+        'override' =>ConfigurableTimezoneInterface::TIMEZONE_FIXED,
+      ],
+    ];
+    $this->setLoggedInUserTimezone($timezones['user']);
+    $field_name = $this->fieldStorage->getName();
+
+    foreach ($scenarios['per_date'] as $per_date_label => $per_date) {
+      foreach ($scenarios['default'] as $default_label => $default) {
+        // Determine the timezone the user will intend for input. We assume the
+        // user behaves according to the site builder's intention. Therefore the
+        // timezone used should always be that set in the 'default' setting,
+        // unless we are testing the scenario where the user can and does
+        // select a per-date timezone other than the default.
+        $timezone_intended_label = $default_label;
+        if ($per_date_label === 'input') {
+          $timezone_intended_label = 'input';
+        }
+        $timezone_intended = $timezones[$timezone_intended_label];
+        // Determine what we expect to be stored in the field's timezone column.
+        // We expect nothing to be stored if the widget doesn't allow the editor
+        // to select a per-date time zone.
+        $timezone_expected_label = NULL;
+        $timezone_expected = NULL;
+        if ($per_date) {
+          $timezone_expected_label = $timezone_intended_label;
+          $timezone_expected = $timezones[$timezone_expected_label];
+        }
+
+        // Set up the field.
+        $this->fieldStorage->setSetting('datetime_type', 'datetime');
+        if ($per_date_label === 'no storage') {
+          $this->fieldStorage->setSetting('timezone_storage', FALSE);
+        }
+        else {
+          $this->fieldStorage->setSetting('timezone_storage', TRUE);
+        }
+        $this->fieldStorage->save();
+
+        // Setup the widget.
+        entity_get_form_display($this->field->getTargetEntityTypeId(), $this->field->getTargetBundle(), 'default')
+          ->setComponent($field_name, [
+            'type' => 'datetime_default',
+            'settings' => [
+              'timezone_default' => $default,
+              'timezone_override' => $timezones['override'],
+              'timezone_per_date' => $per_date,
+            ],
+          ])
+          ->save();
+
+        // Prepare the field values.
+        $date_format = DateFormat::load('html_date')->getPattern();
+        $time_format = DateFormat::load('html_time')->getPattern();
+        $field_name = $this->fieldStorage->getName();
+        $edit = [
+          "{$field_name}[0][value][date]" => $date->format($date_format, ['timezone' => $timezone_intended]),
+          "{$field_name}[0][value][time]" => $date->format($time_format, ['timezone' => $timezone_intended]),
+        ];
+        // Only set time zone input if testing the scenario where a time zone
+        // select is exposed and the user selects other than the default.
+        if ($per_date_label === 'input') {
+          $edit += [
+            "{$field_name}[0][value][timezone]" => $timezone_intended,
+          ];
+        }
+
+        // Try to save the date through a widget on an entity.
+        $this->drupalPostForm('entity_test/add', $edit, t('Save'));
+        preg_match('|entity_test/manage/(\d+)|', $this->getUrl(), $match);
+        $id = $match[1];
+        $this->assertText(t('entity_test @id has been created.', ['@id' => $id]));
+
+        // Test what is stored in the field
+        if ($id) {
+          $entity = $this->entityStorage->load($id);
+          $fieldValues = $entity->get($field_name)->getValue()[0];
+          $messageBase = new FormattableMarkup("For scenario per-date '@per_date' with default '@default': ", [
+            '@per_date' => $per_date_label,
+            '@default' => $default_label,
+          ]);
+          $this->assertEqual($date->format(DATETIME_DATETIME_STORAGE_FORMAT, ['timezone' => DATETIME_STORAGE_TIMEZONE]), $fieldValues['value'], $messageBase . "checking correct date is stored.");
+          $message = new FormattableMarkup("the time zone stored is expected to be '@expected', actually is '@actual'.", [
+            '@expected' => $timezone_expected_label,
+            '@actual' => array_search($fieldValues['timezone'], $timezones),
+          ]);
+          $this->assertEqual($timezone_expected, $fieldValues['timezone'], $messageBase . $message);
+        }
+      }
+    }
+  }
+
+  /**
+   * Sets the timezone for the currently logged in user.
+   */
+  protected function setLoggedInUserTimezone($timezone) {
+    $user = User::load($this->loggedInUser->id());
+    $user->set('timezone', $timezone)->save();
+    $this->setCurrentUser($user);
   }
 
   /**
@@ -727,6 +1049,7 @@ public function testInvalidField() {
     $this->drupalGet('entity_test/add');
     $this->assertFieldByName("{$field_name}[0][value][date]", '', 'Date element found.');
     $this->assertFieldByName("{$field_name}[0][value][time]", '', 'Time element found.');
+    $this->assertNoFieldByName("{$field_name}[0][timezone]", '', 'No timezone field appears for dates that do not collect timezone information.');
 
     // Submit invalid dates and ensure they is not accepted.
     $date_value = '';
diff --git a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php
index e28667f..5bd38ba 100644
--- a/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php
+++ b/core/modules/datetime/tests/src/Kernel/DateTimeItemTest.php
@@ -46,7 +46,10 @@ protected function setUp() {
       'field_name' => 'field_datetime',
       'type' => 'datetime',
       'entity_type' => 'entity_test',
-      'settings' => ['datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME],
+      'settings' => [
+        'datetime_type' => DateTimeItem::DATETIME_TYPE_DATETIME,
+        'timezone_storage' => FALSE,
+      ],
     ]);
     $this->fieldStorage->save();
     $this->field = FieldConfig::create([
@@ -221,4 +224,28 @@ public function testSetValueProperty() {
     $this->assertEqual($entity->field_datetime[0]->value, $value, '"Value" property can be set directly.');
   }
 
+  /**
+   * Tests DateTimeItem with per-date time zone storage.
+   */
+  public function testTimezoneDate() {
+    /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+    $field_storage = FieldStorageConfig::load('entity_test.field_datetime');
+    $field_storage->setSetting('timezone_storage', TRUE);
+    $field_storage->save();
+
+    // Use a non-UTC timezone.
+    $timezone = 'America/Yellowknife';
+
+    $entity = EntityTest::create();
+    $value = '2014-01-01T20:00:00Z';
+
+    $entity->set('field_datetime', ['value' => $value, 'timezone' => $timezone]);
+    $entity->save();
+
+    // Load the entity.
+    $id = $entity->id();
+    $entity = EntityTest::load($id);
+    $this->assertEqual($timezone, $entity->field_datetime[0]->timezone, '"timezone" property can be set.');
+  }
+
 }
