 core/modules/link/config/schema/link.schema.yml    |  3 ++
 .../Plugin/Field/FieldFormatter/LinkFormatter.php  | 14 ++++++++-
 .../link/src/Plugin/Field/FieldType/LinkItem.php   | 34 ++++++++++++++++++++++
 .../src/Plugin/Field/FieldWidget/LinkWidget.php    |  9 ++++++
 .../src/Tests/d6/MigrateFieldInstanceTest.php      |  2 +-
 5 files changed, 60 insertions(+), 2 deletions(-)

diff --git a/core/modules/link/config/schema/link.schema.yml b/core/modules/link/config/schema/link.schema.yml
index 7983652..6bc7ddc 100644
--- a/core/modules/link/config/schema/link.schema.yml
+++ b/core/modules/link/config/schema/link.schema.yml
@@ -46,6 +46,9 @@ field.field_settings.link:
     title:
       type: integer
       label: 'Allow link text'
+    description:
+      type: integer
+      label: 'Allow link description'
     link_type:
       type: integer
       label: 'Allowed link type'
diff --git a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
index 8819fe3..df4b148 100644
--- a/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
+++ b/core/modules/link/src/Plugin/Field/FieldFormatter/LinkFormatter.php
@@ -185,6 +185,7 @@ public function viewElements(FieldItemListInterface $items) {
       // By default use the full URL as the link text.
       $url = $this->buildUrl($item);
       $link_title = $url->toString();
+      $link_description = '';
 
       // If the title field value is available, use it for the link text.
       if (empty($settings['url_only']) && !empty($item->title)) {
@@ -193,6 +194,13 @@ public function viewElements(FieldItemListInterface $items) {
         $link_title = \Drupal::token()->replace($item->title, array($entity->getEntityTypeId() => $entity), array('sanitize' => FALSE, 'clear' => TRUE));
       }
 
+      // If the description is available, use it for the link 'title' attribute.
+      if (empty($settings['url_only']) && !empty($item->description)) {
+        // Unsanitized token replacement here because $options['html'] is FALSE
+        // by default in _l().
+        $link_description = \Drupal::token()->replace($item->description, [$entity->getEntityTypeId() => $entity], ['sanitize' => FALSE, 'clear' => TRUE]);
+      }
+
       // Trim the link text to the desired length.
       if (!empty($settings['trim_length'])) {
         $link_title = Unicode::truncate($link_title, $settings['trim_length'], FALSE, TRUE);
@@ -215,8 +223,8 @@ public function viewElements(FieldItemListInterface $items) {
           '#type' => 'link',
           '#title' => $link_title,
           '#options' => $url->getOptions(),
+          '#url' => $url,
         );
-        $element[$delta]['#url'] = $url;
 
         if (!empty($item->_attributes)) {
           $element[$delta]['#options'] += array ('attributes' => array());
@@ -225,6 +233,10 @@ public function viewElements(FieldItemListInterface $items) {
           // formatter output and should not be rendered in the field template.
           unset($item->_attributes);
         }
+
+        if (!empty($link_description)) {
+          $element[$delta]['#options']['attributes']['title'] = $link_description;
+        }
       }
     }
 
diff --git a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
index 77d733f..68a498c 100644
--- a/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
+++ b/core/modules/link/src/Plugin/Field/FieldType/LinkItem.php
@@ -36,6 +36,7 @@ class LinkItem extends FieldItemBase implements LinkItemInterface {
   public static function defaultFieldSettings() {
     return array(
       'title' => DRUPAL_OPTIONAL,
+      'description' => DRUPAL_OPTIONAL,
       'link_type' => LinkItemInterface::LINK_GENERIC
     ) + parent::defaultFieldSettings();
   }
@@ -52,6 +53,9 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
     $properties['title'] = DataDefinition::create('string')
       ->setLabel(t('Link text'));
 
+    $properties['description'] = DataDefinition::create('string')
+      ->setLabel(t('Link description'));
+
     $properties['options'] = MapDataDefinition::create()
       ->setLabel(t('Options'));
 
@@ -74,6 +78,11 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
           'type' => 'varchar',
           'length' => 255,
         ),
+        'description' => array(
+          'description' => 'The link description.',
+          'type' => 'varchar',
+          'length' => 255,
+        ),
         'options' => array(
           'description' => 'Serialized array of options for the link.',
           'type' => 'blob',
@@ -112,6 +121,17 @@ public function fieldSettingsForm(array $form, FormStateInterface $form_state) {
       ),
     );
 
+    $element['description'] = array(
+      '#type' => 'radios',
+      '#title' => t('Allow link description'),
+      '#default_value' => $this->getSetting('description'),
+      '#options' => array(
+        DRUPAL_DISABLED => t('Disabled'),
+        DRUPAL_OPTIONAL => t('Optional'),
+        DRUPAL_REQUIRED => t('Required'),
+      ),
+    );
+
     return $element;
   }
 
@@ -137,6 +157,20 @@ public static function generateSampleValue(FieldDefinitionInterface $field_defin
         $values['title'] = mt_rand(0,1) ? $random->sentences(4) : '';
         break;
     }
+
+    switch ($field_definition->getSetting('description')) {
+      case DRUPAL_DISABLED:
+        $values['description'] = '';
+        break;
+      case DRUPAL_REQUIRED:
+        $values['description'] = $random->sentences(4);
+        break;
+      case DRUPAL_OPTIONAL:
+        // In case of optional description, randomize its generation.
+        $values['description'] = mt_rand(0,1) ? $random->sentences(4) : '';
+        break;
+    }
+
     $values['uri'] = 'http://www.' . $random->word($domain_length) . '.' . $tlds[mt_rand(0, (sizeof($tlds)-1))];
     return $values;
   }
diff --git a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
index f686ea8..f668ab1 100644
--- a/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
+++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
@@ -81,6 +81,15 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
       '#maxlength' => 255,
       '#access' => $this->getFieldSetting('title') != DRUPAL_DISABLED,
     );
+
+    $element['description'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('Link description'),
+      '#default_value' => isset($items[$delta]->description) ? $items[$delta]->description : NULL,
+      '#maxlength' => 255,
+      '#access' => $this->getFieldSetting('description') != DRUPAL_DISABLED,
+    );
+
     // Post-process the title field to make it conditionally required if URL is
     // non-empty. Omit the validation on the field edit form, since the field
     // settings cannot be saved otherwise.
diff --git a/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php b/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php
index dc4d2c8..3cfdb64 100644
--- a/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php
+++ b/core/modules/migrate_drupal/src/Tests/d6/MigrateFieldInstanceTest.php
@@ -144,7 +144,7 @@ public function testFieldInstanceSettings() {
     // Test a link field.
     $field = FieldConfig::load('node.story.field_test_link');
     $this->assertEqual($field->label(), 'Link Field');
-    $expected = array('title' => 2, 'link_type' => LinkItemInterface::LINK_GENERIC);
+    $expected = array('title' => 2, 'description' => DRUPAL_OPTIONAL, 'link_type' => LinkItemInterface::LINK_GENERIC);
     $this->assertEqual($field->getSettings(), $expected);
     $this->assertEqual('default link title', $entity->field_test_link->title, 'Field field_test_link default title is correct.');
     $this->assertEqual('http://drupal.org', $entity->field_test_link->url, 'Field field_test_link default title is correct.');
