diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index fa39d87..9ce0ac2 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -58,9 +58,6 @@ function block_theme() {
     'block' => array(
       'render element' => 'elements',
     ),
-    'block_list' => array(
-      'render element' => 'form',
-    ),
   );
 }
 
diff --git a/core/modules/block/block.routing.yml b/core/modules/block/block.routing.yml
index 2f26a38..396bf0d 100644
--- a/core/modules/block/block.routing.yml
+++ b/core/modules/block/block.routing.yml
@@ -42,6 +42,15 @@ block.admin_display_theme:
     _access_theme: 'TRUE'
     _permission: 'administer blocks'
 
+block.admin_library:
+  path: 'admin/structure/block/library/{theme}'
+  defaults:
+    _controller: '\Drupal\block\Controller\BlockLibraryController::listBlocks'
+    _title: 'Place block'
+  requirements:
+    _access_theme: 'TRUE'
+    _permission: 'administer blocks'
+
 block.admin_add:
   path: '/admin/structure/block/add/{plugin_id}/{theme}'
   defaults:
diff --git a/core/modules/block/css/block.admin.css b/core/modules/block/css/block.admin.css
index d9f32a2..2b9eb45 100644
--- a/core/modules/block/css/block.admin.css
+++ b/core/modules/block/css/block.admin.css
@@ -1,3 +1,13 @@
+/* Block listing page */
+.region-title .button {
+  margin-left: 1em; /* LTR */
+}
+[dir="rtl"] .region-title .button {
+  margin-left: 0;
+  margin-right: 1em;
+}
+
+/* Block demo mode */
 .block-region {
   background-color: #ff6;
   margin-top: 4px;
@@ -22,87 +32,10 @@ a.block-demo-backlink:hover {
   text-decoration: underline;
 }
 
-.layout-region {
-  box-sizing: border-box;
-}
-.block-list-secondary {
-  border: 1px solid #bfbfbf;
-  border-bottom-width: 0;
-}
-.block-list {
-  padding: 0 0.75em;
-  margin: 0;
-}
-.block-list li {
-  list-style: none;
-  padding: 0.1em 0;
-}
-.block-list a:before {
-  content: '+ ';
-}
-.block-list-secondary .form-type-search {
-  padding: 0 1em;
-}
+/* Configure block form - Block description */
 .block-form .form-item-settings-admin-label label {
   display: inline;
 }
 .block-form .form-item-settings-admin-label label:after {
   content: ':';
 }
-
-/* Wide screens */
-@media
-screen and (min-width: 780px),
-(orientation: landscape) and (min-device-height: 780px) {
-
-  .block-list-primary {
-    float: left; /* LTR */
-    width: 75%;
-    padding-right: 2em;
-  }
-  [dir="rtl"] .block-list-primary {
-    float: right;
-    padding-left: 2em;
-    padding-right: 0;
-  }
-
-  .block-list-secondary {
-    float: right; /* LTR */
-    width: 25%;
-  }
-  [dir="rtl"] .block-list-secondary {
-    float: left;
-  }
-
-  /* @todo File an issue to add a standard class to all text-like inputs */
-  .block-list-secondary .form-autocomplete,
-  .block-list-secondary .form-text,
-  .block-list-secondary .form-tel,
-  .block-list-secondary .form-email,
-  .block-list-secondary .form-url,
-  .block-list-secondary .form-search,
-  .block-list-secondary .form-number,
-  .block-list-secondary .form-color,
-  .block-list-secondary textarea {
-    box-sizing:         border-box;
-    width: 100%;
-    max-width: 100%;
-  }
-}
-
-/**
- * The vertical toolbar mode gets triggered for narrow screens, which throws off
- * the intent of media queries written for the viewport width. When the vertical
- * toolbar is on, we need to suppress layout for the original media width + the
- * toolbar width (240px). In this case, 240px + 780px.
- */
-@media
-screen and (max-width: 1020px) {
-
-  .toolbar-vertical.toolbar-tray-open .block-list-primary,
-  .toolbar-vertical.toolbar-tray-open .block-list-secondary {
-    float: none;
-    width: auto;
-    padding-right: 0;
-  }
-}
diff --git a/core/modules/block/js/block.admin.js b/core/modules/block/js/block.admin.js
index 23109f4..0485063 100644
--- a/core/modules/block/js/block.admin.js
+++ b/core/modules/block/js/block.admin.js
@@ -19,66 +19,38 @@
   Drupal.behaviors.blockFilterByText = {
     attach: function (context, settings) {
       var $input = $('input.block-filter-text').once('block-filter-text');
-      var $element = $($input.attr('data-element'));
-      var $blocks;
-      var $details;
+      var $table = $($input.attr('data-element'));
+      var $filter_rows;
 
-      /**
-       * Hides the `<details>` element for a category if it has no visible blocks.
-       *
-       * @param {number} index
-       * @param {HTMLElement} element
-       */
-      function hideCategoryDetails(index, element) {
-        var $catDetails = $(element);
-        $catDetails.toggle($catDetails.find('li:visible').length > 0);
-      }
-
-      /**
-       * Filters the block list.
-       *
-       * @param {jQuery.Event} e
-       */
       function filterBlockList(e) {
         var query = $(e.target).val().toLowerCase();
 
         /**
          * Shows or hides the block entry based on the query.
          *
-         * @param {number} index
-         * @param {HTMLElement} block
+         * @param {number} index The index of the block.
+         * @param {HTMLElement} label The label of the block.
          */
-        function showBlockEntry(index, block) {
-          var $block = $(block);
-          var $sources = $block.find('.block-filter-text-source');
-          var textMatch = $sources.text().toLowerCase().indexOf(query) !== -1;
-          $block.toggle(textMatch);
+        function toggleBlockEntry(index, label) {
+          var $label = $(label);
+          var $row = $label.parent().parent();
+          var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
+          $row.toggle(textMatch);
         }
 
         // Filter if the length of the query is at least 2 characters.
         if (query.length >= 2) {
-          $blocks.each(showBlockEntry);
-
-          // Note that we first open all <details> to be able to use ':visible'.
-          // Mark the <details> elements that were closed before filtering, so
-          // they can be reclosed when filtering is removed.
-          $details.not('[open]').attr('data-drupal-block-state', 'forced-open');
-          // Hide the category <details> if they don't have any visible rows.
-          $details.attr('open', 'open').each(hideCategoryDetails);
+          $filter_rows.each(toggleBlockEntry);
         }
         else {
-          $blocks.show();
-          $details.show();
-          // Return <details> elements that had been closed before filtering
-          // to a closed state.
-          $details.filter('[data-drupal-block-state="forced-open"]').removeAttr('open data-drupal-block-state');
+          $filter_rows.each(function (index) {
+            $(this).parent().parent().show();
+          });
         }
       }
 
-      if ($element.length) {
-        $details = $element.find('details');
-        $blocks = $details.find('li');
-
+      if ($table.length) {
+        $filter_rows = $table.find('div.block-filter-text-source');
         $input.on('keyup', filterBlockList);
       }
     }
diff --git a/core/modules/block/src/BlockForm.php b/core/modules/block/src/BlockForm.php
index eac4d51..8c13bbb 100644
--- a/core/modules/block/src/BlockForm.php
+++ b/core/modules/block/src/BlockForm.php
@@ -170,11 +170,13 @@ public function form(array $form, FormStateInterface $form_state) {
     }
 
     // Region settings.
+    $entity_region = $entity->getRegion();
+    $region = $entity->isNew() ? $this->getRequest()->query->get('region', $entity_region) : $entity_region;
     $form['region'] = array(
       '#type' => 'select',
       '#title' => $this->t('Region'),
       '#description' => $this->t('Select the region where this block should be displayed.'),
-      '#default_value' => $entity->getRegion(),
+      '#default_value' => $region,
       '#empty_value' => BlockInterface::BLOCK_REGION_NONE,
       '#options' => system_region_list($theme, REGIONS_VISIBLE),
       '#prefix' => '<div id="edit-block-region-wrapper">',
diff --git a/core/modules/block/src/BlockListBuilder.php b/core/modules/block/src/BlockListBuilder.php
index 32d5581..43d7a1c 100644
--- a/core/modules/block/src/BlockListBuilder.php
+++ b/core/modules/block/src/BlockListBuilder.php
@@ -8,7 +8,6 @@
 namespace Drupal\block;
 
 use Drupal\Component\Utility\Html;
-use Drupal\Core\Block\BlockManagerInterface;
 use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
@@ -45,13 +44,6 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
   protected $request;
 
   /**
-   * The block manager.
-   *
-   * @var \Drupal\Core\Block\BlockManagerInterface
-   */
-  protected $blockManager;
-
-  /**
    * The theme manager.
    *
    * @var \Drupal\Core\Theme\ThemeManagerInterface
@@ -77,17 +69,14 @@ class BlockListBuilder extends ConfigEntityListBuilder implements FormInterface
    *   The entity type definition.
    * @param \Drupal\Core\Entity\EntityStorageInterface $storage
    *   The entity storage class.
-   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
-   *   The block manager.
    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
    *   The theme manager.
    * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
    *   The form builder.
    */
-  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, BlockManagerInterface $block_manager, ThemeManagerInterface $theme_manager, FormBuilderInterface $form_builder) {
+  public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, ThemeManagerInterface $theme_manager, FormBuilderInterface $form_builder) {
     parent::__construct($entity_type, $storage);
 
-    $this->blockManager = $block_manager;
     $this->themeManager = $theme_manager;
     $this->formBuilder = $form_builder;
   }
@@ -99,7 +88,6 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
     return new static(
       $entity_type,
       $container->get('entity.manager')->getStorage($entity_type->id()),
-      $container->get('plugin.manager.block'),
       $container->get('theme.manager'),
       $container->get('form_builder')
     );
@@ -135,7 +123,6 @@ public function getFormId() {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['#theme'] = array('block_list');
     $form['#attached']['library'][] = 'core/drupal.tableheader';
     $form['#attached']['library'][] = 'block/drupal.block';
     $form['#attached']['library'][] = 'block/drupal.block.admin';
@@ -143,7 +130,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
     // Build the form tree.
     $form['blocks'] = $this->buildBlocksForm();
-    $form['place_blocks'] = $this->buildPlaceBlocksForm();
 
     $form['actions'] = array(
       '#tree' => FALSE,
@@ -206,7 +192,7 @@ protected function buildBlocksForm() {
 
     // Loop over each region and build blocks.
     $regions = $this->systemRegionList($this->getThemeName(), REGIONS_VISIBLE);
-    $block_regions_with_disabled = $regions + array(BlockInterface::BLOCK_REGION_NONE => BlockInterface::BLOCK_REGION_NONE);
+    $block_regions_with_disabled = $regions + array(BlockInterface::BLOCK_REGION_NONE => $this->t('Disabled', array(), array('context' => 'Plural')));
     foreach ($block_regions_with_disabled as $region => $title) {
       $form['#tabledrag'][] = array(
         'action' => 'match',
@@ -229,10 +215,20 @@ protected function buildBlocksForm() {
         ),
       );
       $form['region-' . $region]['title'] = array(
-        '#markup' => $region != BlockInterface::BLOCK_REGION_NONE ? $title : $this->t('Disabled', array(), array('context' => 'Plural')),
+        '#prefix' => $region != BlockInterface::BLOCK_REGION_NONE ? $title : $block_regions_with_disabled[$region],
+        '#type' => 'link',
+        '#title' => $this->t('Place block <span class="visually-hidden">in the %region region</span>', ['%region' => $block_regions_with_disabled[$region]]),
+        '#url' => Url::fromRoute('block.admin_library', ['theme' => $this->getThemeName()], ['query' => ['region' => $region]]),
         '#wrapper_attributes' => array(
           'colspan' => 5,
         ),
+        '#attributes' => [
+          'class' => ['use-ajax', 'button', 'button--small'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'width' => 700,
+          ]),
+        ],
       );
 
       $form['region-' . $region . '-message'] = array(
@@ -313,78 +309,6 @@ protected function buildBlocksForm() {
   }
 
   /**
-   * Builds the "Place Blocks" portion of the form.
-   *
-   * @return array
-   */
-  protected function buildPlaceBlocksForm() {
-    $form['title'] = array(
-      '#type' => 'container',
-      '#markup' => '<h3>' . $this->t('Place blocks') . '</h3>',
-      '#attributes' => array(
-        'class' => array(
-          'entity-meta__header',
-        ),
-      ),
-    );
-
-    $form['filter'] = array(
-      '#type' => 'search',
-      '#title' => $this->t('Filter'),
-      '#title_display' => 'invisible',
-      '#size' => 30,
-      '#placeholder' => $this->t('Filter by block name'),
-      '#attributes' => array(
-        'class' => array('block-filter-text'),
-        'data-element' => '.entity-meta',
-        'title' => $this->t('Enter a part of the block name to filter by.'),
-      ),
-    );
-
-    $form['list']['#type'] = 'container';
-    $form['list']['#attributes']['class'][] = 'entity-meta';
-
-    // Only add blocks which work without any available context.
-    $definitions = $this->blockManager->getDefinitionsForContexts();
-    $sorted_definitions = $this->blockManager->getSortedDefinitions($definitions);
-    foreach ($sorted_definitions as $plugin_id => $plugin_definition) {
-      $category = SafeMarkup::checkPlain($plugin_definition['category']);
-      $category_key = 'category-' . $category;
-      if (!isset($form['list'][$category_key])) {
-        $form['list'][$category_key] = array(
-          '#type' => 'details',
-          '#title' => $category,
-          '#open' => TRUE,
-          'content' => array(
-            '#theme' => 'links',
-            '#links' => array(),
-            '#attributes' => array(
-              'class' => array(
-                'block-list',
-              ),
-            ),
-          ),
-        );
-      }
-      $form['list'][$category_key]['content']['#links'][$plugin_id] = array(
-        'title' => $plugin_definition['admin_label'],
-        'url' => Url::fromRoute('block.admin_add', [
-          'plugin_id' => $plugin_id,
-          'theme' => $this->theme
-        ]),
-        'attributes' => array(
-          'class' => array('use-ajax', 'block-filter-text-source'),
-          'data-dialog-type' => 'modal',
-          'data-dialog-options' => Json::encode(array(
-            'width' => 700,
-          )),
-        ),
-      );
-    }
-    return $form;
-  }
-
-  /**
    * Gets the name of the theme used for this block listing.
    *
    * @return string
diff --git a/core/modules/block/src/Controller/BlockLibraryController.php b/core/modules/block/src/Controller/BlockLibraryController.php
new file mode 100644
index 0000000..2a0148f
--- /dev/null
+++ b/core/modules/block/src/Controller/BlockLibraryController.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\block\Controller\BlockLibraryController.
+ */
+
+namespace Drupal\block\Controller;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Block\BlockManagerInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Menu\LocalActionManagerInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Provides a list of block plugins to be added to the layout.
+ */
+class BlockLibraryController extends ControllerBase {
+
+  /**
+   * The block manager.
+   *
+   * @var \Drupal\Core\Block\BlockManagerInterface
+   */
+  protected $blockManager;
+
+  /**
+   * The route match.
+   *
+   * @var \Drupal\Core\Routing\RouteMatchInterface
+   */
+  protected $routeMatch;
+
+  /**
+   * The local action manager.
+   *
+   * @var \Drupal\Core\Menu\LocalActionManagerInterface
+   */
+  protected $localActionManager;
+
+  /**
+   * Constructs a BlockLibraryController object.
+   *
+   * @param \Drupal\Core\Block\BlockManagerInterface $block_manager
+   *   The block manager.
+   * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
+   *   The current route match.
+   * @param \Drupal\Core\Menu\LocalActionManagerInterface $local_action_manager
+   *   The local action manager.
+   */
+  public function __construct(BlockManagerInterface $block_manager, RouteMatchInterface $route_match, LocalActionManagerInterface $local_action_manager) {
+    $this->blockManager = $block_manager;
+    $this->routeMatch = $route_match;
+    $this->localActionManager = $local_action_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('plugin.manager.block'),
+      $container->get('current_route_match'),
+      $container->get('plugin.manager.menu.local_action')
+    );
+  }
+
+  /**
+   * Shows a list of blocks that can be added to a theme's layout.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The current request.
+   * @param string $theme
+   *   Theme key of the block list.
+   *
+   * @return array
+   *   A render array as expected by the renderer.
+   */
+  public function listBlocks(Request $request, $theme) {
+    // Since modals do not render any other part of the page, we need to render
+    // them manually as part of this listing.
+    if ($request->query->get(MainContentViewSubscriber::WRAPPER_FORMAT) === 'drupal_modal') {
+      $build['local_actions'] = $this->buildLocalActions();
+    }
+
+    $headers = [
+      ['data' => $this->t('Block')],
+      ['data' => $this->t('Category')],
+      ['data' => $this->t('Operations')],
+    ];
+
+    // Only add blocks which work without any available context.
+    $definitions = $this->blockManager->getDefinitionsForContexts();
+    // Order by category, and then by admin label.
+    $definitions = $this->blockManager->getSortedDefinitions($definitions);
+
+    $region = $request->query->get('region');
+    $rows = [];
+    foreach ($definitions as $plugin_id => $plugin_definition) {
+      $row = [];
+      $row['title']['data'] = [
+        '#markup' => $plugin_definition['admin_label'],
+        '#prefix' => '<div class="block-filter-text-source">',
+        '#suffix' => '</div>',
+      ];
+      $row['category']['data'] = SafeMarkup::checkPlain($plugin_definition['category']);
+      $links['add'] = [
+        'title' => $this->t('Place block'),
+        'url' => Url::fromRoute('block.admin_add', ['plugin_id' => $plugin_id, 'theme' => $theme]),
+        'attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => Json::encode([
+            'width' => 700,
+          ]),
+        ],
+      ];
+      if ($region) {
+        $links['add']['query']['region'] = $region;
+      }
+      $row['operations']['data'] = [
+        '#type' => 'operations',
+        '#links' => $links,
+      ];
+      $rows[] = $row;
+    }
+
+    $build['#attached']['library'][] = 'block/drupal.block.admin';
+
+    $build['filter'] = [
+      '#type' => 'search',
+      '#title' => $this->t('Filter'),
+      '#title_display' => 'invisible',
+      '#size' => 30,
+      '#placeholder' => $this->t('Filter by block name'),
+      '#attributes' => [
+        'class' => ['block-filter-text'],
+        'data-element' => '.block-add-table',
+        'title' => $this->t('Enter a part of the block name to filter by.'),
+      ],
+    ];
+
+    $build['blocks'] = [
+      '#type' => 'table',
+      '#header' => $headers,
+      '#rows' => $rows,
+      '#empty' => $this->t('No blocks available.'),
+      '#attributes' => [
+        'class' => ['block-add-table'],
+      ],
+    ];
+
+    return $build;
+  }
+
+  /**
+   * Builds the local actions for this listing.
+   *
+   * @return array
+   *   An array of local actions for this listing.
+   */
+  protected function buildLocalActions() {
+    $build = $this->localActionManager->getActionsForRoute($this->routeMatch->getRouteName());
+    // Without this workaround, the action links will be rendered as <li> with
+    // no wrapping <ul> element.
+    if (!empty($build)) {
+      $build['#prefix'] = '<ul class="action-links">';
+      $build['#suffix'] = '</ul>';
+    }
+    return $build;
+  }
+
+}
diff --git a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php
index cbc0a73..c91bc7c 100644
--- a/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php
+++ b/core/modules/block/src/Plugin/Derivative/ThemeLocalTask.php
@@ -69,7 +69,7 @@ public function getDerivativeDefinitions($base_plugin_definition) {
       }
       // Default task!
       if ($default_theme == $theme_name) {
-        $this->derivatives[$theme_name]['route_name'] = 'block.admin_display';
+        $this->derivatives[$theme_name]['route_name'] = $base_plugin_definition['parent_id'];
         // Emulate default logic because without the base plugin id we can't
         // change the base_route.
         $this->derivatives[$theme_name]['weight'] = -10;
diff --git a/core/modules/block/src/Tests/BlockLanguageCacheTest.php b/core/modules/block/src/Tests/BlockLanguageCacheTest.php
index 45f6803..543e65e 100644
--- a/core/modules/block/src/Tests/BlockLanguageCacheTest.php
+++ b/core/modules/block/src/Tests/BlockLanguageCacheTest.php
@@ -62,6 +62,7 @@ public function testBlockLinks() {
     // Create the block cache for all languages.
     foreach ($this->langcodes as $langcode) {
       $this->drupalGet('admin/structure/block', array('language' => $langcode));
+      $this->clickLink('Place block');
     }
 
     // Create a menu in the default language.
@@ -73,6 +74,7 @@ public function testBlockLinks() {
     // Check that the block is listed for all languages.
     foreach ($this->langcodes as $langcode) {
       $this->drupalGet('admin/structure/block', array('language' => $langcode));
+      $this->clickLink('Place block');
       $this->assertText($edit['label']);
     }
   }
diff --git a/core/modules/block/src/Tests/BlockTitleXSSTest.php b/core/modules/block/src/Tests/BlockTitleXSSTest.php
deleted file mode 100644
index 3790d56..0000000
--- a/core/modules/block/src/Tests/BlockTitleXSSTest.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * @file
- * Contains \Drupal\block\Tests\BlockTitleXSSTest.
- */
-
-namespace Drupal\block\Tests;
-
-use Drupal\simpletest\WebTestBase;
-
-/**
- * Tests block XSS in title.
- *
- * @group block
- */
-class BlockTitleXSSTest extends WebTestBase {
-
-  /**
-   * Modules to install.
-   *
-   * @var array
-   */
-  public static $modules = array('block', 'block_test');
-
-  protected function setUp() {
-    parent::setUp();
-
-    $this->drupalPlaceBlock('test_xss_title', array('label' => '<script>alert("XSS label");</script>'));
-  }
-
-  /**
-   * Test XSS in title.
-   */
-  function testXSSInTitle() {
-    \Drupal::state()->set('block_test.content', $this->randomMachineName());
-    $this->drupalGet('');
-    $this->assertNoRaw('<script>alert("XSS label");</script>', 'The block title was properly sanitized when rendered.');
-
-    $this->drupalLogin($this->drupalCreateUser(array('administer blocks', 'access administration pages')));
-    $default_theme = $this->config('system.theme')->get('default');
-    $this->drupalGet('admin/structure/block/list/' . $default_theme);
-    $this->assertNoRaw("<script>alert('XSS subject');</script>", 'The block title was properly sanitized in Block Plugin UI Admin page.');
-  }
-
-}
diff --git a/core/modules/block/src/Tests/BlockUiTest.php b/core/modules/block/src/Tests/BlockUiTest.php
index 4dc388f..1f26739 100644
--- a/core/modules/block/src/Tests/BlockUiTest.php
+++ b/core/modules/block/src/Tests/BlockUiTest.php
@@ -139,14 +139,15 @@ function testBlockAdminUiPage() {
    */
   public function testCandidateBlockList() {
     $arguments = array(
-      ':ul_class' => 'block-list',
-      ':li_class' => 'test-block-instantiation',
+      ':title' => 'Display message',
+      ':category' => 'Block test',
       ':href' => 'admin/structure/block/add/test_block_instantiation/classy',
-      ':text' => 'Display message',
     );
+    $pattern = '//tr[.//td/div[text()=:title] and .//td[text()=:category] and .//td//a[contains(@href, :href)]]';
 
     $this->drupalGet('admin/structure/block');
-    $elements = $this->xpath('//details[@id="edit-category-block-test"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $this->clickLink('Place block');
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The test block appears in the category for its module.');
 
     // Trigger the custom category addition in block_test_block_alter().
@@ -154,7 +155,9 @@ public function testCandidateBlockList() {
     $this->container->get('plugin.manager.block')->clearCachedDefinitions();
 
     $this->drupalGet('admin/structure/block');
-    $elements = $this->xpath('//details[@id="edit-category-custom-category"]//ul[contains(@class, :ul_class)]/li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $this->clickLink('Place block');
+    $arguments[':category'] = 'Custom category';
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The test block appears in a custom category controlled by block_test_block_alter().');
   }
 
diff --git a/core/modules/block/src/Tests/BlockXssTest.php b/core/modules/block/src/Tests/BlockXssTest.php
index f24811a..0baa67f 100644
--- a/core/modules/block/src/Tests/BlockXssTest.php
+++ b/core/modules/block/src/Tests/BlockXssTest.php
@@ -26,7 +26,34 @@ class BlockXssTest extends WebTestBase {
    *
    * @var array
    */
-  public static $modules = ['block', 'block_content', 'menu_ui', 'views'];
+  public static $modules = ['block', 'block_content', 'block_test', 'menu_ui', 'views'];
+
+  /**
+   * Test XSS in title.
+   */
+  public function testXssInTitle() {
+    $this->drupalPlaceBlock('test_xss_title', ['label' => '<script>alert("XSS label");</script>']);
+
+    \Drupal::state()->set('block_test.content', $this->randomMachineName());
+    $this->drupalGet('');
+    $this->assertNoRaw('<script>alert("XSS label");</script>', 'The block title was properly sanitized when rendered.');
+
+    $this->drupalLogin($this->drupalCreateUser(['administer blocks', 'access administration pages']));
+    $default_theme = $this->config('system.theme')->get('default');
+    $this->drupalGet('admin/structure/block/list/' . $default_theme);
+    $this->assertNoRaw("<script>alert('XSS subject');</script>", 'The block title was properly sanitized in Block Plugin UI Admin page.');
+  }
+
+  /**
+   * Tests XSS in category.
+   */
+  public function testXssInCategory() {
+    $this->drupalPlaceBlock('test_xss_title');
+    $this->drupalLogin($this->drupalCreateUser(['administer blocks', 'access administration pages']));
+    $this->drupalGet(Url::fromRoute('block.admin_display'));
+    $this->clickLink('Place block');
+    $this->assertNoRaw("<script>alert('XSS category');</script>");
+  }
 
   /**
    * Tests various modules that provide blocks for XSS.
@@ -51,8 +78,9 @@ protected function doViewTest() {
     $view->save();
 
     $this->drupalGet(Url::fromRoute('block.admin_display'));
-    $this->clickLink('<script>alert("view");</script>');
-    $this->assertRaw('&lt;script&gt;alert(&quot;view&quot;);&lt;/script&gt;');
+    $this->clickLink('Place block');
+    // The block admin label is automatically XSS admin filtered.
+    $this->assertRaw('alert("view");');
     $this->assertNoRaw('<script>alert("view");</script>');
   }
 
@@ -66,8 +94,9 @@ protected function doMenuTest() {
     ])->save();
 
     $this->drupalGet(Url::fromRoute('block.admin_display'));
-    $this->clickLink('<script>alert("menu");</script>');
-    $this->assertRaw('&lt;script&gt;alert(&quot;menu&quot;);&lt;/script&gt;');
+    $this->clickLink('Place block');
+    // The block admin label is automatically XSS admin filtered.
+    $this->assertRaw('alert("menu");');
     $this->assertNoRaw('<script>alert("menu");</script>');
   }
 
@@ -86,8 +115,9 @@ protected function doBlockContentTest() {
     ])->save();
 
     $this->drupalGet(Url::fromRoute('block.admin_display'));
-    $this->clickLink('<script>alert("block_content");</script>');
-    $this->assertRaw('&lt;script&gt;alert(&quot;block_content&quot;);&lt;/script&gt;');
+    $this->clickLink('Place block');
+    // The block admin label is automatically XSS admin filtered.
+    $this->assertRaw('alert("block_content");');
     $this->assertNoRaw('<script>alert("block_content");</script>');
   }
 
diff --git a/core/modules/block/src/Tests/Views/DisplayBlockTest.php b/core/modules/block/src/Tests/Views/DisplayBlockTest.php
index 3f96ab1..d9f9196 100644
--- a/core/modules/block/src/Tests/Views/DisplayBlockTest.php
+++ b/core/modules/block/src/Tests/Views/DisplayBlockTest.php
@@ -8,8 +8,6 @@
 namespace Drupal\block\Tests\Views;
 
 use Drupal\Component\Serialization\Json;
-use Drupal\Component\Utility\Html;
-use Drupal\Component\Utility\SafeMarkup;
 use Drupal\views\Views;
 use Drupal\views\Tests\ViewTestBase;
 use Drupal\views\Tests\ViewTestData;
@@ -60,19 +58,20 @@ public function testBlockCategory() {
     $edit['block[style][row_plugin]'] = 'fields';
     $this->drupalPostForm('admin/structure/views/add', $edit, t('Save and edit'));
 
+    $pattern = '//tr[.//td[text()=:category] and .//td//a[contains(@href, :href)]]';
+
     // Test that the block was given a default category corresponding to its
     // base table.
     $arguments = array(
-      ':id' => 'edit-category-lists-views',
-      ':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-1',
       ':href' => \Drupal::Url('block.admin_add', array(
         'plugin_id' => 'views_block:' . $edit['id'] . '-block_1',
         'theme' => 'classy',
       )),
-      ':text' => $edit['label'],
+      ':category' => t('Lists (Views)'),
     );
     $this->drupalGet('admin/structure/block');
-    $elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $this->clickLink('Place block');
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The test block appears in the category for its base table.');
 
     // Duplicate the block before changing the category.
@@ -81,10 +80,9 @@ public function testBlockCategory() {
 
     // Change the block category to a random string.
     $this->drupalGet('admin/structure/views/view/' . $edit['id'] . '/edit/block_1');
-    $label = t('Lists (Views)');
-    $link = $this->xpath('//a[@id="views-block-1-block-category" and normalize-space(text())=:label]', array(':label' => $label));
+    $link = $this->xpath('//a[@id="views-block-1-block-category" and normalize-space(text())=:category]', $arguments);
     $this->assertTrue(!empty($link));
-    $this->clickLink($label);
+    $this->clickLink(t('Lists (Views)'));
     $category = $this->randomString();
     $this->drupalPostForm(NULL, array('block_category' => $category), t('Apply'));
 
@@ -95,34 +93,30 @@ public function testBlockCategory() {
     $this->drupalPostForm(NULL, array(), t('Save'));
 
     // Test that the blocks are listed under the correct categories.
-    $category_id = Html::getUniqueId('edit-category-' . SafeMarkup::checkPlain($category));
-    $arguments[':id'] = $category_id;
+    $arguments[':category'] = $category;
     $this->drupalGet('admin/structure/block');
-    $elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $this->clickLink('Place block');
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The test block appears in the custom category.');
 
     $arguments = array(
-      ':id' => 'edit-category-lists-views',
-      ':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-2',
       ':href' => \Drupal::Url('block.admin_add', array(
         'plugin_id' => 'views_block:' . $edit['id'] . '-block_2',
         'theme' => 'classy',
       )),
-      ':text' => $edit['label'],
+      ':category' => t('Lists (Views)'),
     );
-    $elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The first duplicated test block remains in the original category.');
 
     $arguments = array(
-      ':id' => $category_id,
-      ':li_class' => 'views-block' . Html::getClass($edit['id']) . '-block-3',
       ':href' => \Drupal::Url('block.admin_add', array(
         'plugin_id' => 'views_block:' . $edit['id'] . '-block_3',
         'theme' => 'classy',
       )),
-      ':text' => $edit['label'],
+      ':category' => $category,
     );
-    $elements = $this->xpath('//details[@id=:id]//li[contains(@class, :li_class)]/a[contains(@href, :href) and text()=:text]', $arguments);
+    $elements = $this->xpath($pattern, $arguments);
     $this->assertTrue(!empty($elements), 'The second duplicated test block appears in the custom category.');
   }
 
diff --git a/core/modules/block_content/block_content.links.action.yml b/core/modules/block_content/block_content.links.action.yml
index d94ca3f..4772a6f 100644
--- a/core/modules/block_content/block_content.links.action.yml
+++ b/core/modules/block_content/block_content.links.action.yml
@@ -8,7 +8,6 @@ block_content_add_action:
   route_name: block_content.add_page
   title: 'Add custom block'
   appears_on:
-    - block.admin_display
-    - block.admin_display_theme
+    - block.admin_library
     - entity.block_content.collection
   class: \Drupal\block_content\Plugin\Menu\LocalAction\BlockContentAddLocalAction
diff --git a/core/modules/block_content/src/Tests/BlockContentTypeTest.php b/core/modules/block_content/src/Tests/BlockContentTypeTest.php
index 4270538..681c6ce 100644
--- a/core/modules/block_content/src/Tests/BlockContentTypeTest.php
+++ b/core/modules/block_content/src/Tests/BlockContentTypeTest.php
@@ -189,6 +189,7 @@ public function testsBlockContentAddTypes() {
         // block configure form.
         $path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme";
         $this->drupalGet($path);
+        $this->clickLink('Place block');
         $this->clickLink(t('Add custom block'));
         // The seven theme has markup inside the link, we cannot use clickLink().
         if ($default_theme == 'seven') {
diff --git a/core/modules/menu_ui/src/Tests/MenuTest.php b/core/modules/menu_ui/src/Tests/MenuTest.php
index 3a1bac5..3c00e92 100644
--- a/core/modules/menu_ui/src/Tests/MenuTest.php
+++ b/core/modules/menu_ui/src/Tests/MenuTest.php
@@ -219,6 +219,7 @@ function addCustomMenu() {
 
     // Confirm that the custom menu block is available.
     $this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
+    $this->clickLink('Place block');
     $this->assertText($label);
 
     // Enable the block.
@@ -532,6 +533,7 @@ function testSystemMenuRename() {
     // Make sure menu shows up with new name in block addition.
     $default_theme = $this->config('system.theme')->get('default');
     $this->drupalget('admin/structure/block/list/' . $default_theme);
+    $this->clickLink('Place block');
     $this->assertText($edit['label']);
   }
 
diff --git a/core/modules/search/src/Tests/SearchBlockTest.php b/core/modules/search/src/Tests/SearchBlockTest.php
index bf18056..2348ae0 100644
--- a/core/modules/search/src/Tests/SearchBlockTest.php
+++ b/core/modules/search/src/Tests/SearchBlockTest.php
@@ -36,6 +36,7 @@ public function testSearchFormBlock() {
 
     // Test availability of the search block in the admin "Place blocks" list.
     $this->drupalGet('admin/structure/block');
+    $this->clickLink('Place block');
     $this->assertLinkByHref('/admin/structure/block/add/search_form_block/classy', 0,
       'Did not find the search block in block candidate list.');
 
diff --git a/core/modules/simpletest/src/WebTestBase.php b/core/modules/simpletest/src/WebTestBase.php
index e17d703..ca20171 100644
--- a/core/modules/simpletest/src/WebTestBase.php
+++ b/core/modules/simpletest/src/WebTestBase.php
@@ -2342,7 +2342,7 @@ protected function handleForm(&$post, &$edit, &$upload, $submit, $form) {
    */
   protected function clickLink($label, $index = 0) {
     $url_before = $this->getUrl();
-    $urls = $this->xpath('//a[normalize-space()=:label]', array(':label' => $label));
+    $urls = $this->xpath('//a[starts-with(normalize-space(), :label)]', array(':label' => $label));
     if (isset($urls[$index])) {
       $url_target = $this->getAbsoluteUrl($urls[$index]['href']);
       $this->pass(SafeMarkup::format('Clicked link %label (@url_target) from @url_before', array('%label' => $label, '@url_target' => $url_target, '@url_before' => $url_before)), 'Browser');
diff --git a/core/modules/views/src/Tests/Wizard/BasicTest.php b/core/modules/views/src/Tests/Wizard/BasicTest.php
index d55b941..2ba0dac 100644
--- a/core/modules/views/src/Tests/Wizard/BasicTest.php
+++ b/core/modules/views/src/Tests/Wizard/BasicTest.php
@@ -133,6 +133,7 @@ function testViewsWizardAndListing() {
 
     // Confirm that the block is available in the block administration UI.
     $this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
+    $this->clickLink('Place block');
     $this->assertText($view3['label']);
 
     // Place the block.
diff --git a/core/modules/views/src/Tests/Wizard/ItemsPerPageTest.php b/core/modules/views/src/Tests/Wizard/ItemsPerPageTest.php
index 8e8edfc..7fd06d0 100644
--- a/core/modules/views/src/Tests/Wizard/ItemsPerPageTest.php
+++ b/core/modules/views/src/Tests/Wizard/ItemsPerPageTest.php
@@ -71,6 +71,7 @@ function testItemsPerPage() {
 
     // Confirm that the block is listed in the block administration UI.
     $this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
+    $this->clickLink('Place block');
     $this->assertText($view['label']);
 
     // Place the block, visit a page that displays the block, and check that the
diff --git a/core/modules/views_ui/src/Tests/OverrideDisplaysTest.php b/core/modules/views_ui/src/Tests/OverrideDisplaysTest.php
index f1815a8..15b6c88 100644
--- a/core/modules/views_ui/src/Tests/OverrideDisplaysTest.php
+++ b/core/modules/views_ui/src/Tests/OverrideDisplaysTest.php
@@ -50,6 +50,7 @@ function testOverrideDisplays() {
 
     // Confirm that the view block is available in the block administration UI.
     $this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
+    $this->clickLink('Place block');
     $this->assertText($view['label']);
 
     // Place the block.
@@ -109,6 +110,7 @@ function testWizardMixedDefaultOverriddenDisplays() {
 
     // Confirm that the block is available in the block administration UI.
     $this->drupalGet('admin/structure/block/list/' . $this->config('system.theme')->get('default'));
+    $this->clickLink('Place block');
     $this->assertText($view['label']);
 
     // Put the block into the first sidebar region, and make sure it will not
