diff --git a/core/modules/views/src/Tests/Plugin/StyleTableTest.php b/core/modules/views/src/Tests/Plugin/StyleTableTest.php
index 0c423a9..614e1a1 100644
--- a/core/modules/views/src/Tests/Plugin/StyleTableTest.php
+++ b/core/modules/views/src/Tests/Plugin/StyleTableTest.php
@@ -136,4 +136,58 @@ public function testNumericFieldVisible() {
     $this->assertTrue(count($result), 'Ensure that the baby\'s age is shown');
   }
 
+  /**
+   * Tests grouping by a field.
+   */
+  public function testGrouping() {
+    /** @var \Drupal\views\ViewEntityInterface $view */
+    $view = \Drupal::entityManager()->getStorage('view')->load('test_table');
+    // Get a reference to the display configuration so we can alter some
+    // specific style options.
+    $display = &$view->getDisplay('default');
+    // Set job as the grouping field.
+    $display['display_options']['style']['options']['grouping'][0] = array(
+      'field' => 'job',
+      'rendered' => TRUE,
+      'rendered_strip' => FALSE,
+    );
+    // Clear the caption text, the rendered job field will be used as a caption.
+    $display['display_options']['style']['options']['caption'] = '';
+    $display['display_options']['style']['options']['summary'] = '';
+    $display['display_options']['style']['options']['description'] = '';
+    $view->save();
+
+    // Add a record containing unsafe markup to be sure it's filtered out.
+    $unsafe_markup = '<script>alert("Rapper");</script>';
+    $unsafe_markup_data = array(
+      'name' => 'Marshall',
+      'age' => 42,
+      'job' => $unsafe_markup,
+      'created' => gmmktime(0, 0, 0, 2, 15, 2001),
+      'status' => 1,
+    );
+    $database = $this->container->get('database');
+    $database->insert('views_test_data')
+      ->fields(array_keys($unsafe_markup_data))
+      ->values($unsafe_markup_data)
+      ->execute();
+
+    $this->drupalGet('test-table');
+    $expected_captions = array(
+      'Job: Speaker',
+      'Job: Songwriter',
+      'Job: Drummer',
+      'Job: Singer',
+      'Job: ' . $unsafe_markup,
+    );
+
+    // Ensure that we don't find the caption containing unsafe markup.
+    $this->assertNoRaw($unsafe_markup, "Didn't find caption containing unsafe markup.");
+
+    // Ensure that all expected captions are found.
+    foreach ($expected_captions as $raw_caption) {
+      $this->assertEscaped($raw_caption);
+    }
+  }
+
 }
diff --git a/core/modules/views/views.theme.inc b/core/modules/views/views.theme.inc
index 50b1a6c..30dcffa 100644
--- a/core/modules/views/views.theme.inc
+++ b/core/modules/views/views.theme.inc
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\Xss;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
+use Drupal\views\Render\ViewsRenderPipelineSafeString;
 
 /**
  * Prepares variables for view templates.
@@ -660,9 +661,13 @@ function template_preprocess_views_view_table(&$variables) {
     $variables['caption_needed'] = FALSE;
   }
 
+  if (!empty($variables['title'])) {
+    $variables['title'] = ViewsRenderPipelineSafeString::create($variables['title']);
+  }
+
   $variables['summary'] = $handler->options['summary'];
   $variables['description'] = $handler->options['description'];
-  $variables['caption_needed'] |= !empty($variables['summary']) || !empty($variables['description']);
+  $variables['caption_needed'] |= !empty($variables['summary']) || !empty($variables['description']) || !empty($variables['title']);
 
   $variables['responsive'] = FALSE;
   // If the table has headers and it should react responsively to columns hidden
