diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
index a914ec6649..3f142dfed4 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityType.php
@@ -173,7 +173,6 @@ public function getPropertiesToExport($id = NULL) {
       if (!isset($definition['mapping'])) {
         return NULL;
       }
-      @trigger_error(sprintf('Entity type "%s" is using config schema as a fallback for a missing `config_export` definition is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. See https://www.drupal.org/node/2949023.', $this->id()), E_USER_DEPRECATED);
       $this->mergedConfigExport = array_combine(array_keys($definition['mapping']), array_keys($definition['mapping']));
     }
     return $this->mergedConfigExport;
diff --git a/core/modules/config/tests/config_test/config_test.module b/core/modules/config/tests/config_test/config_test.module
index 7eb00f3fa1..5008575425 100644
--- a/core/modules/config/tests/config_test/config_test.module
+++ b/core/modules/config/tests/config_test/config_test.module
@@ -36,7 +36,6 @@ function config_test_entity_type_alter(array &$entity_types) {
   $config_test_no_status->set('id', 'config_test_no_status');
   $config_test_no_status->set('entity_keys', $keys);
   $config_test_no_status->set('config_prefix', 'no_status');
-  $config_test_no_status->set('mergedConfigExport', ['id' => 'id', 'label' => 'label']);
   if (\Drupal::service('state')->get('config_test.lookup_keys', FALSE)) {
     $entity_types['config_test']->set('lookup_keys', ['uuid', 'style']);
   }
diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigQueryTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigQueryTest.php
index 6b9e8007db..ca3b354984 100644
--- a/core/modules/config/tests/config_test/src/Entity/ConfigQueryTest.php
+++ b/core/modules/config/tests/config_test/src/Entity/ConfigQueryTest.php
@@ -16,12 +16,6 @@
  *     }
  *   },
  *   config_prefix = "query",
- *   config_export = {
- *     "id",
- *     "label",
- *     "array",
- *     "number",
- *   },
  *   entity_keys = {
  *     "id" = "id",
  *     "label" = "label"
diff --git a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
index f8ceea4219..d1a7249bed 100644
--- a/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
+++ b/core/modules/config/tests/config_test/src/Entity/ConfigTest.php
@@ -28,15 +28,6 @@
  *     "label" = "label",
  *     "status" = "status"
  *   },
- *   config_export = {
- *     "id",
- *     "label",
- *     "weight",
- *     "style",
- *     "size",
- *     "size_value",
- *     "protected_property",
- *   },
  *   links = {
  *     "edit-form" = "/admin/structure/config_test/manage/{config_test}",
  *     "delete-form" = "/admin/structure/config_test/manage/{config_test}/delete",
diff --git a/core/modules/language/src/Entity/ContentLanguageSettings.php b/core/modules/language/src/Entity/ContentLanguageSettings.php
index d89b1508f9..0012e79994 100644
--- a/core/modules/language/src/Entity/ContentLanguageSettings.php
+++ b/core/modules/language/src/Entity/ContentLanguageSettings.php
@@ -26,13 +26,6 @@
  *   entity_keys = {
  *     "id" = "id"
  *   },
- *   config_export = {
- *     "id",
- *     "target_entity_type_id",
- *     "target_bundle",
- *     "default_langcode",
- *     "language_alterable",
- *   },
  *   list_cache_tags = { "rendered" }
  * )
  */
diff --git a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
index 8b445bd773..276e680b43 100644
--- a/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
+++ b/core/modules/layout_builder/src/EventSubscriber/BlockComponentRenderArray.php
@@ -90,14 +90,6 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
     if ($access->isAllowed()) {
       $event->addCacheableDependency($block);
 
-      $content = $block->build();
-      $is_content_empty = Element::isEmpty($content);
-      $is_placeholder_ready = $event->inPreview() && $block instanceof PreviewFallbackInterface;
-      // If the content is empty and no placeholder is available, return.
-      if ($is_content_empty && !$is_placeholder_ready) {
-        return;
-      }
-
       $build = [
         // @todo Move this to BlockBase in https://www.drupal.org/node/2931040.
         '#theme' => 'block',
@@ -106,9 +98,9 @@ public function onBuildRender(SectionComponentBuildRenderArrayEvent $event) {
         '#base_plugin_id' => $block->getBaseId(),
         '#derivative_plugin_id' => $block->getDerivativeId(),
         '#weight' => $event->getComponent()->getWeight(),
-        'content' => $content,
+        'content' => $block->build(),
       ];
-      if ($is_content_empty && $is_placeholder_ready) {
+      if ($event->inPreview() && Element::isEmpty($build['content']) && $block instanceof PreviewFallbackInterface) {
         $build['content']['#markup'] = $block->getPreviewFallbackString();
       }
       $event->setBuild($build);
diff --git a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
index 98277a133d..35a4a26cdf 100644
--- a/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
@@ -361,11 +361,6 @@ public function testPluginDependencies() {
     $page->fillField('id', 'myothermenu');
     $page->pressButton('Save');
 
-    $page->clickLink('Add link');
-    $page->fillField('title[0][value]', 'My link');
-    $page->fillField('link[0][uri]', '/');
-    $page->pressButton('Save');
-
     $this->drupalPostForm('admin/structure/types/manage/bundle_with_section_field/display', ['layout[enabled]' => TRUE], 'Save');
     $assert_session->linkExists('Manage layout');
     $this->clickLink('Manage layout');
diff --git a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
index 2ac7994d74..30820846cd 100644
--- a/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/BlockComponentRenderArrayTest.php
@@ -11,7 +11,6 @@
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Plugin\Context\ContextHandlerInterface;
-use Drupal\Core\Render\PreviewFallbackInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\layout_builder\Access\LayoutPreviewAccessAllowed;
 use Drupal\layout_builder\Event\SectionComponentBuildRenderArrayEvent;
@@ -253,98 +252,6 @@ public function testOnBuildRenderInPreview($refinable_dependent_access) {
     $this->assertEquals($expected_cache, $result);
   }
 
-  /**
-   * @covers ::onBuildRender
-   */
-  public function testOnBuildRenderInPreviewEmptyBuild() {
-    $block = $this->prophesize(BlockPluginInterface::class)->willImplement(PreviewFallbackInterface::class);
-
-    $block->access($this->account->reveal(), TRUE)->shouldNotBeCalled();
-    $block->getCacheContexts()->willReturn([]);
-    $block->getCacheTags()->willReturn(['test']);
-    $block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
-    $block->getConfiguration()->willReturn([]);
-    $block->getPluginId()->willReturn('block_plugin_id');
-    $block->getBaseId()->willReturn('block_plugin_id');
-    $block->getDerivativeId()->willReturn(NULL);
-    $placeholder_string = 'The placeholder string';
-    $block->getPreviewFallbackString()->willReturn($placeholder_string);
-
-    $block_content = [];
-    $block->build()->willReturn($block_content);
-    $this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
-
-    $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
-    $event = new SectionComponentBuildRenderArrayEvent($component, [], TRUE);
-
-    $subscriber = new BlockComponentRenderArray($this->account->reveal());
-
-    $expected_build = [
-      '#theme' => 'block',
-      '#weight' => 0,
-      '#configuration' => [],
-      '#plugin_id' => 'block_plugin_id',
-      '#base_plugin_id' => 'block_plugin_id',
-      '#derivative_plugin_id' => NULL,
-      'content' => $block_content,
-    ];
-    $expected_build['content']['#markup'] = $placeholder_string;
-
-    $expected_cache = $expected_build + [
-      '#cache' => [
-        'contexts' => [],
-        'tags' => ['test'],
-        'max-age' => 0,
-      ],
-    ];
-
-    $subscriber->onBuildRender($event);
-    $result = $event->getBuild();
-    $this->assertEquals($expected_build, $result);
-    $event->getCacheableMetadata()->applyTo($result);
-    $this->assertEquals($expected_cache, $result);
-  }
-
-  /**
-   * @covers ::onBuildRender
-   */
-  public function testOnBuildRenderEmptyBuild() {
-    $block = $this->prophesize(BlockPluginInterface::class);
-    $access_result = AccessResult::allowed();
-    $block->access($this->account->reveal(), TRUE)->willReturn($access_result)->shouldBeCalled();
-    $block->getCacheContexts()->willReturn([]);
-    $block->getCacheTags()->willReturn(['test']);
-    $block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
-    $block->getConfiguration()->willReturn([]);
-    $block->getPluginId()->willReturn('block_plugin_id');
-    $block->getBaseId()->willReturn('block_plugin_id');
-    $block->getDerivativeId()->willReturn(NULL);
-
-    $block->build()->willReturn([]);
-    $this->blockManager->createInstance('some_block_id', ['id' => 'some_block_id'])->willReturn($block->reveal());
-
-    $component = new SectionComponent('some-uuid', 'some-region', ['id' => 'some_block_id']);
-    $event = new SectionComponentBuildRenderArrayEvent($component, [], FALSE);
-
-    $subscriber = new BlockComponentRenderArray($this->account->reveal());
-
-    $expected_build = [];
-
-    $expected_cache = $expected_build + [
-      '#cache' => [
-        'contexts' => [],
-        'tags' => ['test'],
-        'max-age' => -1,
-      ],
-    ];
-
-    $subscriber->onBuildRender($event);
-    $result = $event->getBuild();
-    $this->assertEquals($expected_build, $result);
-    $event->getCacheableMetadata()->applyTo($result);
-    $this->assertEquals($expected_cache, $result);
-  }
-
   /**
    * @covers ::onBuildRender
    */
diff --git a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php
index ad94e14e18..079a220a45 100644
--- a/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php
+++ b/core/modules/layout_builder/tests/src/Unit/SectionRenderTest.php
@@ -231,7 +231,6 @@ public function testToRenderArrayEmpty() {
    * @covers ::toRenderArray
    */
   public function testContextAwareBlock() {
-    $block_content = ['#markup' => 'The block content.'];
     $render_array = [
       '#theme' => 'block',
       '#weight' => 0,
@@ -239,7 +238,7 @@ public function testContextAwareBlock() {
       '#plugin_id' => 'block_plugin_id',
       '#base_plugin_id' => 'block_plugin_id',
       '#derivative_plugin_id' => NULL,
-      'content' => $block_content,
+      'content' => [],
       '#cache' => [
         'contexts' => [],
         'tags' => [],
@@ -252,7 +251,7 @@ public function testContextAwareBlock() {
 
     $access_result = AccessResult::allowed();
     $block->access($this->account->reveal(), TRUE)->willReturn($access_result);
-    $block->build()->willReturn($block_content);
+    $block->build()->willReturn([]);
     $block->getCacheContexts()->willReturn([]);
     $block->getCacheTags()->willReturn([]);
     $block->getCacheMaxAge()->willReturn(Cache::PERMANENT);
diff --git a/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml
index 32096a6f9c..afff3dba78 100644
--- a/core/modules/media_library/config/install/views.view.media_library.yml
+++ b/core/modules/media_library/config/install/views.view.media_library.yml
@@ -380,6 +380,48 @@ display:
       display_extenders: {  }
       use_ajax: true
       css_class: 'media-library-view js-media-library-view'
+      arguments:
+        bundle:
+          id: bundle
+          table: media_field_data
+          field: bundle
+          relationship: none
+          group_type: group
+          admin_label: ''
+          default_action: ignore
+          exception:
+            value: all
+            title_enable: false
+            title: All
+          title_enable: false
+          title: ''
+          default_argument_type: fixed
+          default_argument_options:
+            argument: ''
+          default_argument_skip_url: false
+          summary_options:
+            base_path: ''
+            count: true
+            items_per_page: 25
+            override: false
+          summary:
+            sort_order: asc
+            number_of_records: 0
+            format: default_summary
+          specify_validation: false
+          validate:
+            type: none
+            fail: 'not found'
+          validate_options: {  }
+          glossary: false
+          limit: 0
+          case: none
+          path_case: none
+          transform_dash: false
+          break_phrase: false
+          entity_type: media
+          entity_field: bundle
+          plugin_id: string
     cache_metadata:
       max-age: 0
       contexts:
@@ -529,11 +571,58 @@ display:
       defaults:
         fields: false
         access: false
+        filters: false
+        filter_groups: false
       display_description: ''
       access:
         type: perm
         options:
           perm: 'view media'
+      filters:
+        name:
+          id: name
+          table: media_field_data
+          field: name
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: contains
+          value: ''
+          group: 1
+          exposed: true
+          expose:
+            operator_id: name_op
+            label: Name
+            description: ''
+            use_operator: false
+            operator: name_op
+            identifier: name
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          entity_type: media
+          entity_field: name
+          plugin_id: string
+      filter_groups:
+        operator: AND
+        groups:
+          1: AND
     cache_metadata:
       max-age: -1
       contexts:
diff --git a/core/modules/media_library/config/schema/media_library.schema.yml b/core/modules/media_library/config/schema/media_library.schema.yml
new file mode 100644
index 0000000000..ce46c91e74
--- /dev/null
+++ b/core/modules/media_library/config/schema/media_library.schema.yml
@@ -0,0 +1,10 @@
+field.widget.settings.media_library_widget:
+  type: mapping
+  label: 'Media library widget settings'
+  mapping:
+    media_types:
+      type: sequence
+      label: 'Media types tab order'
+      sequence:
+        type: integer
+        label: 'Weight'
diff --git a/core/modules/media_library/css/media_library.module.css b/core/modules/media_library/css/media_library.module.css
index 89b1580063..4dbed468d6 100644
--- a/core/modules/media_library/css/media_library.module.css
+++ b/core/modules/media_library/css/media_library.module.css
@@ -2,6 +2,11 @@
 * @file media_library.module.css
 */
 
+.media-library-wrapper {
+  display: flex;
+  margin: -1em;
+}
+
 .media-library-views-form > .form-actions {
   flex-basis: 100%;
 }
@@ -69,6 +74,15 @@
   pointer-events: none;
 }
 
+.media-library-widget-modal .ui-dialog-buttonpane {
+  display: flex;
+  align-items: center;
+}
+
+.media-library-widget-modal .ui-dialog-buttonpane .form-actions {
+  flex: 1;
+}
+
 @media screen and (max-width: 600px) {
   .media-library-view .form-actions {
     flex-basis: 100%;
diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 4f04445dd5..1c835b13c7 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -5,6 +5,63 @@
  * @see https://www.drupal.org/project/drupal/issues/2980769
  */
 
+.media-library-menu {
+  background-color: #e6e5e1;
+  display: block;
+  padding: 0;
+  margin: 0;
+  width: 600px;
+  max-width: 20%;
+  border-bottom: 1px solid #ccc;
+  line-height: 1;
+}
+
+.media-library-menu li {
+  display: block;
+  list-style: none;
+  padding: 0;
+}
+
+.media-library-menu a {
+  box-sizing: border-box;
+  display: block;
+  padding: 10px 15px 15px;
+  position: relative;
+  border-bottom: 1px solid #b3b2ad;
+  background-color: #f2f2f0;
+  text-shadow: 0 1px hsla(0, 0%, 100%, 0.6);
+  text-decoration: none;
+}
+
+.media-library-menu a:active,
+.media-library-menu a:hover,
+.media-library-menu a:focus {
+  background: #fcfcfa;
+  text-shadow: none;
+}
+
+.media-library-menu a:focus,
+.media-library-menu a:active {
+  z-index: 2;
+  outline: none;
+}
+
+.media-library-menu a.active {
+  background-color: #fff;
+  color: #000;
+  z-index: 1;
+  border-right: 1px solid #fcfcfa;
+  box-shadow: 0 5px 5px -5px hsla(0, 0%, 0%, 0.3);
+  border-bottom: 1px solid #b3b2ad;
+  margin-right: -1px;
+}
+
+.media-library-content {
+  padding: 1em;
+  border-left: 1px solid #b3b2ad;
+  width: 100%;
+}
+
 .media-library-views-form__header .form-item {
   margin-right: 8px;
 }
diff --git a/core/modules/media_library/js/media_library.widget.es6.js b/core/modules/media_library/js/media_library.widget.es6.js
index a784cb3884..0242569202 100644
--- a/core/modules/media_library/js/media_library.widget.es6.js
+++ b/core/modules/media_library/js/media_library.widget.es6.js
@@ -1,7 +1,58 @@
 /**
  * @file media_library.widget.js
  */
-(($, Drupal) => {
+(($, Drupal, window) => {
+  /**
+   * Store the media library selection.
+   *
+   * When a user interacts with the media library we want the selection to
+   * persist as long as the media library modal is opened. We temporarily store
+   * the selected items while the user filters the media library view or
+   * navigates to different views pages and tabs.
+   */
+  Drupal.mediaLibrarySelection = {
+    selection: [],
+    /**
+     * Add a media item ID to the selection.
+     *
+     * @param {number} id
+     *   The ID of the item we want to add.
+     */
+    add(id) {
+      const position = this.selection.indexOf(id);
+      if (position === -1) {
+        this.selection.push(id);
+      }
+    },
+    /**
+     * Remove a media item ID from the selection.
+     *
+     * @param {number} id
+     *   The ID of the item we want to remove.
+     */
+    remove(id) {
+      const position = this.selection.indexOf(id);
+      if (position !== -1) {
+        this.selection.splice(position, 1);
+      }
+    },
+    /**
+     * Get the selected media items IDs.
+     *
+     * @return {Array}
+     *   An array of selected media item IDs.
+     */
+    get() {
+      return this.selection;
+    },
+    /**
+     * Reset the selected media items IDs.
+     */
+    reset() {
+      this.selection = [];
+    },
+  };
+
   /**
    * Allows users to re-order their selection with drag+drop.
    */
@@ -81,40 +132,152 @@
   };
 
   /**
-   * Prevent users from selecting more items than allowed in the view.
+   * Update the select button and number of selected items in the button pane.
    */
-  Drupal.behaviors.MediaLibraryWidgetRemaining = {
+  function updateButtonPane() {
+    const $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane');
+    if (!$buttonPane.length) {
+      return;
+    }
+
+    const count = Drupal.mediaLibrarySelection.get().length;
+    const $toggleElements = $buttonPane.find(
+      '.media-library-select, .media-library-selected-count',
+    );
+
+    if (count === 0) {
+      // Hide the select button and selection count when nothing is
+      // selected.
+      $toggleElements.hide();
+    } else {
+      // Add the selection count and show the select button.
+      const $wrapper = $buttonPane.find('.media-library-selected-count');
+      if ($wrapper.length) {
+        $wrapper.replaceWith(Drupal.theme('mediaLibrarySelectionCount', count));
+      } else {
+        $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount', count));
+      }
+      $toggleElements.fadeIn('fast');
+    }
+  }
+
+  /**
+   * Change the selection when a media item is checked/unchecked.
+   */
+  Drupal.behaviors.MediaLibraryModalChangeSelection = {
     attach(context, settings) {
-      const $view = $('.js-media-library-view', context).once(
-        'media-library-remaining',
+      const $mediaItems = $(
+        '.js-media-library-item input[type="checkbox"]',
+        context,
       );
-      $view
-        .find('.js-media-library-item input[type="checkbox"]')
-        .on('change', () => {
-          if (
-            settings.media_library &&
-            settings.media_library.selection_remaining
-          ) {
-            const $checkboxes = $view.find(
-              '.js-media-library-item input[type="checkbox"]',
-            );
-            if (
-              $checkboxes.filter(':checked').length ===
-              settings.media_library.selection_remaining
-            ) {
-              $checkboxes
-                .not(':checked')
-                .prop('disabled', true)
-                .closest('.js-media-library-item')
-                .addClass('media-library-item--disabled');
-            } else {
-              $checkboxes
-                .prop('disabled', false)
-                .closest('.js-media-library-item')
-                .removeClass('media-library-item--disabled');
-            }
-          }
+
+      if (!$mediaItems.length) {
+        return;
+      }
+
+      function disableItems($items) {
+        $items
+          .prop('disabled', true)
+          .closest('.js-media-library-item')
+          .addClass('media-library-item--disabled');
+      }
+
+      function enableItems($items) {
+        $items
+          .prop('disabled', false)
+          .closest('.js-media-library-item')
+          .removeClass('media-library-item--disabled');
+      }
+
+      $mediaItems.once('media-item-change').on('change', e => {
+        const $form = $(e.currentTarget).parents('.media-library-views-form');
+
+        // Update the selection.
+        if ($(e.currentTarget).is(':checked')) {
+          Drupal.mediaLibrarySelection.add(e.currentTarget.value);
+        } else {
+          Drupal.mediaLibrarySelection.remove(e.currentTarget.value);
+        }
+
+        // Set the selection in the hidden form element.
+        $form
+          .find('input#media-library-modal-selection')
+          .val(Drupal.mediaLibrarySelection.get().join());
+
+        // Once the selection is update, update the button pane.
+        updateButtonPane();
+
+        // Prevent users from selecting more items than allowed in the view.
+        if (
+          Drupal.mediaLibrarySelection.get().length ===
+          settings.media_library.selection_remaining
+        ) {
+          disableItems($mediaItems.not(':checked'));
+          enableItems($mediaItems.filter(':checked'));
+        } else {
+          enableItems($mediaItems);
+        }
+      });
+    },
+  };
+
+  /**
+   * Apply the current selection when loading the media library view.
+   */
+  Drupal.behaviors.MediaLibraryModalApplySelection = {
+    attach(context) {
+      const $form = $('.media-library-views-form', context);
+
+      if (!$form.length) {
+        return;
+      }
+
+      if ($form.find('.js-media-library-item').length) {
+        // Select the items in the view.
+        Drupal.mediaLibrarySelection.get().forEach(value => {
+          $form
+            .find(`input[type="checkbox"][value="${value}"]`)
+            .prop('checked', true)
+            .trigger('change');
+        });
+      }
+
+      // Hide selection button if nothing is selected. We can't use the
+      // context here because the dialog copies the select button.
+      $(window)
+        .once('media-library-toggle-buttons')
+        .on('dialog:aftercreate', updateButtonPane);
+    },
+  };
+
+  /**
+   * Clear the current selection.
+   */
+  Drupal.behaviors.MediaLibraryModalClearSelection = {
+    attach() {
+      $(window)
+        .once('media-library-clear-selection')
+        .on('dialog:afterclose', () => {
+          Drupal.mediaLibrarySelection.reset();
         });
     },
   };
-})(jQuery, Drupal);
+
+  /**
+   * Theme function for the selection count.
+   *
+   * @param {number} count
+   *   The number of selected items.
+   *
+   * @return {string}
+   *   The corresponding HTML.
+   */
+  Drupal.theme.mediaLibrarySelectionCount = function(count) {
+    const selectItemsText = Drupal.formatPlural(
+      count,
+      '1 item selected',
+      '@count items selected',
+    );
+    return `<div class="media-library-selected-count">${selectItemsText}</div>`;
+  };
+})(jQuery, Drupal, window);
diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js
index ad6fbd76e5..d85ff3cae4 100644
--- a/core/modules/media_library/js/media_library.widget.js
+++ b/core/modules/media_library/js/media_library.widget.js
@@ -5,7 +5,29 @@
 * @preserve
 **/
 
-(function ($, Drupal) {
+(function ($, Drupal, window) {
+  Drupal.mediaLibrarySelection = {
+    selection: [],
+    add: function add(id) {
+      var position = this.selection.indexOf(id);
+      if (position === -1) {
+        this.selection.push(id);
+      }
+    },
+    remove: function remove(id) {
+      var position = this.selection.indexOf(id);
+      if (position !== -1) {
+        this.selection.splice(position, 1);
+      }
+    },
+    get: function get() {
+      return this.selection;
+    },
+    reset: function reset() {
+      this.selection = [];
+    }
+  };
+
   Drupal.behaviors.MediaLibraryWidgetSortable = {
     attach: function attach(context) {
       $('.js-media-library-selection', context).once('media-library-sortable').sortable({
@@ -49,19 +71,95 @@
     }
   };
 
-  Drupal.behaviors.MediaLibraryWidgetRemaining = {
+  function updateButtonPane() {
+    var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane');
+    if (!$buttonPane.length) {
+      return;
+    }
+
+    var count = Drupal.mediaLibrarySelection.get().length;
+    var $toggleElements = $buttonPane.find('.media-library-select, .media-library-selected-count');
+
+    if (count === 0) {
+      $toggleElements.hide();
+    } else {
+      var $wrapper = $buttonPane.find('.media-library-selected-count');
+      if ($wrapper.length) {
+        $wrapper.replaceWith(Drupal.theme('mediaLibrarySelectionCount', count));
+      } else {
+        $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount', count));
+      }
+      $toggleElements.fadeIn('fast');
+    }
+  }
+
+  Drupal.behaviors.MediaLibraryModalChangeSelection = {
     attach: function attach(context, settings) {
-      var $view = $('.js-media-library-view', context).once('media-library-remaining');
-      $view.find('.js-media-library-item input[type="checkbox"]').on('change', function () {
-        if (settings.media_library && settings.media_library.selection_remaining) {
-          var $checkboxes = $view.find('.js-media-library-item input[type="checkbox"]');
-          if ($checkboxes.filter(':checked').length === settings.media_library.selection_remaining) {
-            $checkboxes.not(':checked').prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled');
-          } else {
-            $checkboxes.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled');
-          }
+      var $mediaItems = $('.js-media-library-item input[type="checkbox"]', context);
+
+      if (!$mediaItems.length) {
+        return;
+      }
+
+      function disableItems($items) {
+        $items.prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled');
+      }
+
+      function enableItems($items) {
+        $items.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled');
+      }
+
+      $mediaItems.once('media-item-change').on('change', function (e) {
+        var $form = $(e.currentTarget).parents('.media-library-views-form');
+
+        if ($(e.currentTarget).is(':checked')) {
+          Drupal.mediaLibrarySelection.add(e.currentTarget.value);
+        } else {
+          Drupal.mediaLibrarySelection.remove(e.currentTarget.value);
+        }
+
+        $form.find('input#media-library-modal-selection').val(Drupal.mediaLibrarySelection.get().join());
+
+        updateButtonPane();
+
+        if (Drupal.mediaLibrarySelection.get().length === settings.media_library.selection_remaining) {
+          disableItems($mediaItems.not(':checked'));
+          enableItems($mediaItems.filter(':checked'));
+        } else {
+          enableItems($mediaItems);
         }
       });
     }
   };
-})(jQuery, Drupal);
\ No newline at end of file
+
+  Drupal.behaviors.MediaLibraryModalApplySelection = {
+    attach: function attach(context) {
+      var $form = $('.media-library-views-form', context);
+
+      if (!$form.length) {
+        return;
+      }
+
+      if ($form.find('.js-media-library-item').length) {
+        Drupal.mediaLibrarySelection.get().forEach(function (value) {
+          $form.find('input[type="checkbox"][value="' + value + '"]').prop('checked', true).trigger('change');
+        });
+      }
+
+      $(window).once('media-library-toggle-buttons').on('dialog:aftercreate', updateButtonPane);
+    }
+  };
+
+  Drupal.behaviors.MediaLibraryModalClearSelection = {
+    attach: function attach() {
+      $(window).once('media-library-clear-selection').on('dialog:afterclose', function () {
+        Drupal.mediaLibrarySelection.reset();
+      });
+    }
+  };
+
+  Drupal.theme.mediaLibrarySelectionCount = function (count) {
+    var selectItemsText = Drupal.formatPlural(count, '1 item selected', '@count items selected');
+    return '<div class="media-library-selected-count">' + selectItemsText + '</div>';
+  };
+})(jQuery, Drupal, window);
\ No newline at end of file
diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module
index 977da24ae5..ae853827ed 100644
--- a/core/modules/media_library/media_library.module
+++ b/core/modules/media_library/media_library.module
@@ -13,10 +13,9 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Template\Attribute;
 use Drupal\Core\Url;
+use Drupal\media_library\MediaLibraryState;
 use Drupal\views\Form\ViewsForm;
 use Drupal\views\Plugin\views\cache\CachePluginBase;
-use Drupal\views\Plugin\views\query\QueryPluginBase;
-use Drupal\views\Plugin\views\query\Sql;
 use Drupal\views\ViewExecutable;
 
 /**
@@ -81,11 +80,8 @@ function media_library_views_post_render(ViewExecutable $view, &$output, CachePl
   if ($view->id() === 'media_library') {
     $output['#attached']['library'][] = 'media_library/view';
     if ($view->current_display === 'widget') {
-      $query = array_intersect_key(\Drupal::request()->query->all(), array_flip([
-        'media_library_widget_id',
-        'media_library_allowed_types',
-        'media_library_remaining',
-      ]));
+      $state = MediaLibraryState::fromRequest();
+      $query = $state->all();
       // If the current query contains any parameters we use to contextually
       // filter the view, ensure they persist across AJAX rebuilds.
       // The ajax_path is shared for all AJAX views on the page, but our query
@@ -186,49 +182,6 @@ function _media_library_views_form_media_library_after_build(array $form, FormSt
   return $form;
 }
 
-/**
- * Implements hook_views_query_alter().
- *
- * Alters the widget view's query to only show media that can be selected,
- * based on what types are allowed in the field settings.
- *
- * @todo Remove in https://www.drupal.org/node/2983454
- */
-function media_library_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
-  if ($query instanceof Sql && $view->id() === 'media_library' && $view->current_display === 'widget') {
-    $types = _media_library_get_allowed_types();
-    if ($types) {
-      $entity_type = \Drupal::entityTypeManager()->getDefinition('media');
-      $group = $query->setWhereGroup();
-      $query->addWhere($group, $entity_type->getDataTable() . '.' . $entity_type->getKey('bundle'), $types, 'in');
-    }
-  }
-}
-
-/**
- * Implements hook_form_FORM_ID_alter().
- *
- * Limits the types available in the exposed filter to avoid users trying to
- * filter by a type that is un-selectable.
- *
- * @see media_library_views_query_alter()
- *
- * @todo Remove in https://www.drupal.org/node/2983454
- */
-function media_library_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state) {
-  if (isset($form['#id']) && $form['#id'] === 'views-exposed-form-media-library-widget') {
-    $types = _media_library_get_allowed_types();
-    if ($types && isset($form['type']['#options'])) {
-      $keys = array_flip($types);
-      // Ensure that the default value (by default "All") persists.
-      if (isset($form['type']['#default_value'])) {
-        $keys[$form['type']['#default_value']] = TRUE;
-      }
-      $form['type']['#options'] = array_intersect_key($form['type']['#options'], $keys);
-    }
-  }
-}
-
 /**
  * Implements hook_field_ui_preconfigured_options_alter().
  */
@@ -259,17 +212,3 @@ function media_library_local_tasks_alter(&$local_tasks) {
     }
   }
 }
-
-/**
- * Determines what types are allowed based on the current request.
- *
- * @return array
- *   An array of allowed types.
- */
-function _media_library_get_allowed_types() {
-  $types = \Drupal::request()->query->get('media_library_allowed_types');
-  if ($types && is_array($types)) {
-    return array_filter($types, 'is_string');
-  }
-  return [];
-}
diff --git a/core/modules/media_library/media_library.routing.yml b/core/modules/media_library/media_library.routing.yml
index 1724760acb..787b236078 100644
--- a/core/modules/media_library/media_library.routing.yml
+++ b/core/modules/media_library/media_library.routing.yml
@@ -4,3 +4,9 @@ media_library.upload:
     _form: '\Drupal\media_library\Form\MediaLibraryUploadForm'
   requirements:
     _custom_access: '\Drupal\media_library\Form\MediaLibraryUploadForm::access'
+media_library.modal:
+  path: '/media-library'
+  defaults:
+    _controller: 'media_library.modal:build'
+  requirements:
+    _custom_access: 'media_library.modal:access'
diff --git a/core/modules/media_library/media_library.services.yml b/core/modules/media_library/media_library.services.yml
new file mode 100644
index 0000000000..2c50cda166
--- /dev/null
+++ b/core/modules/media_library/media_library.services.yml
@@ -0,0 +1,4 @@
+services:
+  media_library.modal:
+    class: Drupal\media_library\MediaLibraryModal
+    arguments: ['@logger.factory']
diff --git a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
index e897a2178c..5f8c047f70 100644
--- a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
+++ b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
@@ -18,6 +18,7 @@
 use Drupal\file\Plugin\Field\FieldType\FileItem;
 use Drupal\media\MediaInterface;
 use Drupal\media\MediaTypeInterface;
+use Drupal\media_library\MediaLibraryState;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
@@ -314,24 +315,31 @@ public function selectType(array &$form, FormStateInterface $form_state) {
    *   A command to send the selection to the current field widget.
    *
    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
-   *   If the "media_library_widget_id" query parameter is not present.
+   *   If the "media_library_state_id" query parameter is not present.
    */
   public function updateWidget(array &$form, FormStateInterface $form_state) {
     if ($form_state->getErrors()) {
       return $form;
     }
-    $widget_id = $this->getRequest()->query->get('media_library_widget_id');
-    if (!$widget_id || !is_string($widget_id)) {
-      throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.');
+
+    $state = MediaLibraryState::fromRequest();
+    $field_id = $state->getFieldId();
+    $field_type = $state->getFieldType();
+    if (!$field_id || !$field_type) {
+      throw new BadRequestHttpException('The media library state ID is required and must be a string.');
     }
+
     $mids = array_map(function (MediaInterface $media) {
       return $media->id();
     }, $this->media);
+
     // Pass the selection to the field widget based on the current widget ID.
-    return (new AjaxResponse())
-      ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)]))
-      ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']))
-      ->addCommand(new CloseDialogCommand());
+    if ($field_type === 'entity_reference') {
+      return (new AjaxResponse())
+        ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$field_id\"]", 'val', [implode(',', $mids)]))
+        ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$field_id\"]", 'trigger', ['mousedown']))
+        ->addCommand(new CloseDialogCommand());
+    }
   }
 
   /**
@@ -480,13 +488,16 @@ public function access(array $allowed_types = NULL) {
    *   A list of media types that are valid for this form.
    */
   protected function getTypes(array $allowed_types = NULL) {
+    $state = MediaLibraryState::fromRequest();
     // Cache results if possible.
     if (!isset($this->types)) {
       $media_type_storage = $this->entityTypeManager->getStorage('media_type');
       if (!$allowed_types) {
-        $allowed_types = _media_library_get_allowed_types() ?: NULL;
+        $types = $state->getAllowedTypes();
+      }
+      else {
+        $types = $media_type_storage->loadMultiple($allowed_types);
       }
-      $types = $media_type_storage->loadMultiple($allowed_types);
       $types = $this->filterTypesWithFileSource($types);
       $types = $this->filterTypesWithCreateAccess($types);
       $this->types = $types;
diff --git a/core/modules/media_library/src/MediaLibraryModal.php b/core/modules/media_library/src/MediaLibraryModal.php
new file mode 100644
index 0000000000..8e15adbfd0
--- /dev/null
+++ b/core/modules/media_library/src/MediaLibraryModal.php
@@ -0,0 +1,174 @@
+<?php
+
+namespace Drupal\media_library;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Logger\LoggerChannelFactoryInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Drupal\media\MediaTypeInterface;
+use Drupal\views\Views;
+
+/**
+ * Service which renders the media library modal.
+ */
+class MediaLibraryModal {
+
+  /**
+   * The logger service.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a MediaLibraryModal instance.
+   *
+   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
+   *   The logger factory service.
+   */
+  public function __construct(LoggerChannelFactoryInterface $logger_factory) {
+    $this->logger = $logger_factory->get('media_library');
+  }
+
+  /**
+   * Get media library dialog options.
+   *
+   * @return array
+   *   The media library dialog options.
+   */
+  public static function dialogOptions() {
+    return [
+      'dialogClass' => 'media-library-widget-modal',
+      'title' => t('Media library'),
+      'height' => '75%',
+      'width' => '75%',
+    ];
+  }
+
+  /**
+   * Build the media library modal.
+   *
+   * @return array
+   *   The render array for the media library.
+   */
+  public function build() {
+    $state = MediaLibraryState::fromRequest();
+    return [
+      'wrapper' => [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#attributes' => [
+          'class' => ['media-library-wrapper'],
+        ],
+        'menu' => $this->getMediaTypeMenu($state),
+        'content' => [
+          '#type' => 'html_tag',
+          '#tag' => 'div',
+          '#attributes' => [
+            'class' => ['media-library-content'],
+          ],
+          'view' => $this->getMediaLibraryView($state->getSelectedType()),
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Check access to the media library.
+   *
+   * @param \Drupal\Core\Session\AccountInterface $account
+   *   Run access checks for this account.
+   *
+   * @return \Drupal\Core\Access\AccessResult
+   *   The access result.
+   */
+  public function access(AccountInterface $account) {
+    // Deny access if the view or display are removed.
+    $view = Views::getView('media_library');
+    if (!$view) {
+      $this->logger->error('The media library view does not exist.');
+      return AccessResult::forbidden('The media library view does not exist.');
+    }
+    if (!$view->storage->getDisplay('widget')) {
+      $this->logger->error('The media library view does not exist.');
+      return AccessResult::forbidden('The media library widget display does not exist.');
+    }
+
+    return AccessResult::allowedIfHasPermission($account, 'view media');
+  }
+
+  /**
+   * Get the media type menu for the media library.
+   *
+   * @param \Drupal\media_library\MediaLibraryState $state
+   *   The media library configuration.
+   *
+   * @return array
+   *   The render array for the media type menu.
+   */
+  protected function getMediaTypeMenu(MediaLibraryState $state) {
+    $selected_type = $state->getSelectedType();
+    $allowed_types = $state->getAllowedTypes();
+
+    $dialog_options = Json::encode(static::dialogOptions());
+
+    // Add the menu for each type if we have more than 1 media type enabled for
+    // the field.
+    if (count($allowed_types) === 1) {
+      return [];
+    }
+
+    $menu = [
+      '#theme' => 'links',
+      '#links' => [],
+      '#attributes' => [
+        'class' => ['media-library-menu'],
+      ]
+    ];
+
+    $query = $state->all();
+    foreach ($allowed_types as $allowed_type_id => $allowed_type) {
+      $query['media_library_selected_type'] = $allowed_type_id;
+      $menu['#links'][$allowed_type_id] = [
+        'title' => $allowed_type->label(),
+        'url' => Url::fromRoute('media_library.modal', [], [
+          'query' => $query,
+        ]),
+        'attributes' => [
+          'class' => ['use-ajax'],
+          'data-dialog-type' => 'modal',
+          'data-dialog-options' => $dialog_options,
+        ],
+      ];
+      if ($selected_type->id() === $allowed_type_id) {
+        $menu['#links'][$allowed_type_id]['attributes']['class'][] = 'active';
+      }
+    }
+
+    return $menu;
+  }
+
+  /**
+   * Get the media library view.
+   *
+   * @param \Drupal\media\MediaTypeInterface $media_type
+   *   The selected media type.
+   *
+   * @return array
+   *   The render array for the media library view.
+   */
+  protected function getMediaLibraryView(MediaTypeInterface $media_type) {
+    $view = Views::getView('media_library');
+    $display_id = 'widget';
+
+    $media_type_id = $media_type->id();
+    $view->setDisplay($display_id);
+    $view->preExecute([$media_type_id]);
+    $view->execute($display_id);
+
+    return $view->buildRenderable($display_id, [$media_type_id], FALSE);
+  }
+
+}
diff --git a/core/modules/media_library/src/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php
new file mode 100644
index 0000000000..8b16098915
--- /dev/null
+++ b/core/modules/media_library/src/MediaLibraryState.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\media_library;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\media\Entity\MediaType;
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A value object for the media library state.
+ *
+ * @internal
+ */
+class MediaLibraryState extends ParameterBag {
+
+  use StringTranslationTrait;
+
+  /**
+   * Returns the ID of the field that opened the modal.
+   *
+   * @return string
+   *   The field ID.
+   */
+  public function getFieldId() {
+    return $this->get('media_library_field_id');
+  }
+
+  /**
+   * Returns the field type of the field that opened the modal.
+   *
+   * @return string
+   *   The field type.
+   */
+  public function getFieldType() {
+    return $this->get('media_library_field_type');
+  }
+
+  /**
+   * Returns the selected media type.
+   *
+   * @return \Drupal\media\Entity\MediaType
+   *   The media type.
+   */
+  public function getSelectedType() {
+    return MediaType::load($this->get('media_library_selected_type'));
+  }
+
+  /**
+   * Returns the media types which can be selected.
+   *
+   * @return \Drupal\media\Entity\MediaType[]
+   *   The media types.
+   */
+  public function getAllowedTypes() {
+    // When no media types are passed, we load all media types since that is the
+    // default behaviour of the entity reference field target bundles.
+    $media_types = $this->get('media_library_allowed_types') ?: NULL;
+    return MediaType::loadMultiple($media_types);
+  }
+
+  /**
+   * Determines if additional media items can be selected.
+   *
+   * @return bool
+   *   TRUE if additional items can be selected, otherwise FALSE.
+   */
+  public function hasSlotsAvailable() {
+    return $this->getAvailableSlots() !== 0;
+  }
+
+  /**
+   * Returns the number of additional media items that can be selected.
+   *
+   * @return int
+   *   The number of additional media items that can be selected.
+   */
+  public function getAvailableSlots() {
+    return $this->getInt('media_library_remaining');
+  }
+
+  /**
+   * Create a new media library URL with state parameters.
+   *
+   * @param string $field_id
+   *   The field ID.
+   * @param string $field_type
+   *   The field type.
+   * @param string $selected_type_id
+   *   The selected media type ID.
+   * @param string[] $allowed_media_type_ids
+   *   The allowed media type IDs.
+   * @param int $remaining
+   *   The number of remaining items the user is allowed to select or add in the
+   *   library.
+   *
+   * @return \Drupal\Core\Url
+   *   A new Url object for the media library.
+   */
+  public static function createUrl($field_id, $field_type, $selected_type_id, array $allowed_media_type_ids, $remaining) {
+    $query = [
+      'media_library_field_id' => $field_id,
+      'media_library_field_type' => $field_type,
+      'media_library_selected_type' => $selected_type_id,
+      'media_library_allowed_types' => $allowed_media_type_ids,
+      'media_library_remaining' => $remaining,
+    ];
+    return Url::fromRoute('media_library.modal', [], [
+      'query' => $query,
+    ]);
+  }
+
+  /**
+   * Get the media library state from a request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   (optional) The request. If not given, the current request will be used.
+   *
+   * @return \Drupal\media_library\MediaLibraryState
+   *   A selection object.
+   */
+  public static function fromRequest(Request $request = NULL) {
+    $request = $request ?: \Drupal::request();
+    return new static($request->query->all());
+  }
+
+}
diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
index 22ac3ecb7a..f963290d1e 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -12,9 +12,12 @@
 use Drupal\Core\Field\WidgetBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\Url;
 use Drupal\media\Entity\Media;
 use Drupal\media_library\Form\MediaLibraryUploadForm;
+use Drupal\media_library\MediaLibraryModal;
+use Drupal\media_library\MediaLibraryState;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\ConstraintViolationInterface;
 
@@ -98,6 +101,135 @@ public static function isApplicable(FieldDefinitionInterface $field_definition)
     return $field_definition->getSetting('target_type') === 'media';
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function defaultSettings() {
+    return [
+        'media_types' => [],
+      ] + parent::defaultSettings();
+  }
+
+  /**
+   * Get the enabled media types sorted by weight.
+   *
+   * @return \Drupal\media\MediaTypeInterface[]
+   *   The media types sorted by weight.
+   */
+  protected function getEnabledMediaTypeIdsSorted() {
+    $media_types_setting = $this->getSetting('media_types');
+    $configured_media_type_ids = array_keys($this->getFieldSetting('handler_settings')['target_bundles']);
+
+    if (empty($media_types_setting)) {
+      return $configured_media_type_ids;
+    }
+
+    asort($media_types_setting);
+    $sorted_media_type_ids = array_keys($media_types_setting);
+
+    // There could have been added or removed media types in the field storage.
+    // We need to make sure new media types are added to the list and remove
+    // media types that are no longer available for the field.
+    $new_media_type_ids = array_diff($configured_media_type_ids, $sorted_media_type_ids);
+    // Add new media type IDs to the list.
+    $sorted_media_type_ids = array_merge($sorted_media_type_ids, $new_media_type_ids);
+    // Remove media types that are no longer available.
+    $sorted_media_type_ids = array_intersect($sorted_media_type_ids, $configured_media_type_ids);
+
+    return $sorted_media_type_ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $media_type_ids = $this->getEnabledMediaTypeIdsSorted();
+    if (count($media_type_ids) !== 1) {
+      $form['media_types'] = [
+        '#type' => 'table',
+        '#header' => [
+          $this->t('Tab order'),
+          $this->t('Weight'),
+        ],
+        '#tabledrag' => [
+          [
+            'action' => 'order',
+            'relationship' => 'sibling',
+            'group' => 'weight',
+          ],
+        ],
+      ];
+
+      $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($media_type_ids);
+      $delta = 0;
+      foreach ($media_types as $media_type_id => $media_type) {
+        $label = $media_type->label();
+        $form['media_types'][$media_type_id] = [
+          'label' => ['#markup' => $label],
+          'weight' => [
+            '#type' => 'weight',
+            '#title' => t('Weight for @title', ['@title' => $label]),
+            '#title_display' => 'invisible',
+            '#default_value' => $delta,
+            '#attributes' => ['class' => ['weight']],
+          ],
+          '#weight' => $delta,
+          '#attributes' => ['class' => ['draggable']],
+          '#process' => [[static::class, 'processMediaTypeParents']],
+        ];
+        $delta++;
+      }
+    }
+    return $form;
+  }
+
+  /**
+   * Process callback to optimize the way the media type weights are stored.
+   *
+   * The tabledrag functionality needs a specific weight field, but we don't
+   * this extra weight field in our settings. To remove this, we need to change
+   * the #parents array of the weight field. We also need to change this from
+   * the wrapper element, since the form builder handles input before processing
+   * an element.
+   *
+   * @param array $element
+   *   The media type weight element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The changed element.
+   *
+   * @see \Drupal\Core\Form\FormBuilder::doBuildForm()
+   */
+  public static function processMediaTypeParents(array $element, FormStateInterface $form_state) {
+    foreach (Element::children($element) as $key) {
+      if (!isset($element[$key]['#tree'])) {
+        $element[$key]['#tree'] = $element['#tree'];
+      }
+      if (!isset($element[$key]['#parents'])) {
+        $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? $element['#parents'] : [$key];
+      }
+    }
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = [];
+    $media_type_labels = [];
+    $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($this->getEnabledMediaTypeIdsSorted());
+    if ($media_types !== 1) {
+      foreach ($media_types as $media_type) {
+        $media_type_labels[] = $media_type->label();
+      }
+      $summary[] = t('Tab order: @order', ['@order' => implode(', ', $media_type_labels)]);
+    }
+    return $summary;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -121,7 +253,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
     $view_builder = $this->entityTypeManager->getViewBuilder('media');
     $field_name = $this->fieldDefinition->getName();
     $parents = $form['#parents'];
-    $id_suffix = '-' . implode('-', $parents);
+    $id_suffix = $parents ? '-' . implode('-', $parents) : '';
     $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix;
     $limit_validation_errors = [array_merge($parents, [$field_name])];
 
@@ -232,17 +364,14 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
       $element['#description'] .= '<br />' . $cardinality_message;
     }
 
-    $query = [
-      'media_library_widget_id' => $field_name . $id_suffix,
-      'media_library_allowed_types' => $element['#target_bundles'],
-      'media_library_remaining' => $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining,
-    ];
-    $dialog_options = Json::encode([
-      'dialogClass' => 'media-library-widget-modal',
-      'height' => '75%',
-      'width' => '75%',
-      'title' => $this->t('Media library'),
-    ]);
+    // Create a new media library URL with the correct state parameters.
+    $media_type_ids = $this->getEnabledMediaTypeIdsSorted();
+    $primary_media_type_id = array_slice($media_type_ids, 0, 1, TRUE);
+    $selected_type = reset($primary_media_type_id);
+    $remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;
+    $url = MediaLibraryState::createUrl($field_name . $id_suffix, 'entity_reference', $selected_type, $media_type_ids, $remaining);
+
+    $dialog_options = Json::encode(MediaLibraryModal::dialogOptions());
 
     // Add a button that will load the Media library in a modal using AJAX.
     $element['media_library_open_button'] = [
@@ -250,9 +379,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen
       '#title' => $this->t('Browse media'),
       '#name' => $field_name . '-media-library-open-button' . $id_suffix,
       // @todo Make the view configurable in https://www.drupal.org/project/drupal/issues/2971209
-      '#url' => Url::fromRoute('view.media_library.widget', [], [
-        'query' => $query,
-      ]),
+      '#url' => $url,
       '#attributes' => [
         'class' => ['button', 'use-ajax', 'media-library-open-button'],
         'data-dialog-type' => 'modal',
diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
index 72a4b50bbf..10379f5dcd 100644
--- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\media_library\MediaLibraryState;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\Render\ViewsRenderPipelineMarkup;
 use Drupal\views\ResultRow;
@@ -45,11 +46,6 @@ public function render(ResultRow $values) {
    *   The current state of the form.
    */
   public function viewsForm(array &$form, FormStateInterface $form_state) {
-    // Only add the bulk form options and buttons if there are results.
-    if (empty($this->view->result)) {
-      return;
-    }
-
     // Render checkboxes for all rows.
     $form[$this->options['id']]['#tree'] = TRUE;
     foreach ($this->view->result as $row_index => $row) {
@@ -64,6 +60,17 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
       ];
     }
 
+    // The selection is persistent across different pages in the media library
+    // and populated via javascript.
+    $selection_field_id = $this->options['id'] . '_selection';
+    $form[$selection_field_id] = [
+      '#type' => 'hidden',
+      '#attributes' => [
+        // This is used to identify the hidden field in the form via javascript.
+        'id' => ['media-library-modal-selection'],
+      ],
+    ];
+
     // @todo Remove in https://www.drupal.org/project/drupal/issues/2504115
     // Currently the default URL for all AJAX form elements is the current URL,
     // not the form action. This causes bugs when this form is rendered from an
@@ -80,7 +87,10 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
     ];
 
     $form['actions']['submit']['#value'] = $this->t('Select media');
-    $form['actions']['submit']['#field_id'] = $this->options['id'];
+    $form['actions']['submit']['#field_id'] = $selection_field_id;
+    $form['actions']['submit']['#attributes'] = [
+      'class' => ['media-library-select'],
+    ];
   }
 
   /**
@@ -95,17 +105,27 @@ public function viewsForm(array &$form, FormStateInterface $form_state) {
    *   A command to send the selection to the current field widget.
    */
   public static function updateWidget(array &$form, FormStateInterface $form_state) {
-    $widget_id = \Drupal::request()->query->get('media_library_widget_id');
-    if (!$widget_id || !is_string($widget_id)) {
-      throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.');
-    }
     $field_id = $form_state->getTriggeringElement()['#field_id'];
-    $selected = array_values(array_filter($form_state->getValue($field_id, [])));
-    // Pass the selection to the field widget based on the current widget ID.
-    return (new AjaxResponse())
-      ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $selected)]))
-      ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']))
-      ->addCommand(new CloseDialogCommand());
+    $selected = array_filter(explode(',', $form_state->getValue($field_id, [])));
+
+    $response = new AjaxResponse();
+    $response->addCommand(new CloseDialogCommand());
+
+    $state = MediaLibraryState::fromRequest();
+    $field_id = $state->getFieldId();
+    $field_type = $state->getFieldType();
+    if (!$field_id || !$field_type) {
+      throw new BadRequestHttpException('The media library field ID and field type are required and must be a string.');
+    }
+    $ids = implode(',', $selected);
+
+    if ($field_type === 'entity_reference') {
+      $response
+        ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$field_id\"]", 'val', [$ids]))
+        ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$field_id\"]", 'trigger', ['mousedown']));
+    }
+
+    return $response;
   }
 
   /**
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
index e18981cd8d..f7fd27b327 100644
--- a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
@@ -3,6 +3,7 @@ status: true
 dependencies:
   config:
     - field.field.node.basic_page.field_twin_media
+    - field.field.node.basic_page.field_single_media_type
     - field.field.node.basic_page.field_unlimited_media
     - field.field.node.basic_page.field_noadd_media
     - node.type.basic_page
@@ -25,6 +26,12 @@ content:
     settings: {  }
     third_party_settings: {  }
     region: content
+  field_single_media_type:
+    type: media_library_widget
+    weight: 124
+    settings: {  }
+    third_party_settings: {  }
+    region: content
   field_unlimited_media:
     type: media_library_widget
     weight: 121
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
index a66daea429..17fb52793f 100644
--- a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
@@ -22,6 +22,15 @@ content:
       link: false
     third_party_settings: {  }
     region: content
+  field_single_media_type:
+    type: entity_reference_entity_view
+    weight: 101
+    label: above
+    settings:
+      view_mode: default
+      link: false
+    third_party_settings: {  }
+    region: content
   field_unlimited_media:
     type: entity_reference_entity_view
     weight: 101
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_single_media_type.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_single_media_type.yml
new file mode 100644
index 0000000000..57a850d71e
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_single_media_type.yml
@@ -0,0 +1,28 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.field_single_media_type
+    - media.type.type_one
+    - media.type.type_two
+    - node.type.basic_page
+id: node.basic_page.field_single_media_type
+field_name: field_single_media_type
+entity_type: node
+bundle: basic_page
+label: 'Single media type'
+description: ''
+required: false
+translatable: false
+default_value: {  }
+default_value_callback: ''
+settings:
+  handler: 'default:media'
+  handler_settings:
+    target_bundles:
+      type_three: type_three
+    sort:
+      field: _none
+    auto_create: false
+    auto_create_bundle: file
+field_type: entity_reference
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_single_media_type.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_single_media_type.yml
new file mode 100644
index 0000000000..81adca162b
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_single_media_type.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+    - node
+id: node.field_single_media_type
+field_name: field_single_media_type
+entity_type: node
+type: entity_reference
+settings:
+  target_type: media
+module: core
+locked: false
+cardinality: 2
+translatable: true
+indexes: {  }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index da4ebeb16a..9866810781 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -20,7 +20,7 @@ class MediaLibraryTest extends WebDriverTestBase {
   /**
    * {@inheritdoc}
    */
-  protected static $modules = ['block', 'media_library_test'];
+  protected static $modules = ['block', 'media_library_test', 'field_ui'];
 
   /**
    * {@inheritdoc}
@@ -65,6 +65,7 @@ protected function setUp() {
       'create media',
       'delete any media',
       'view media',
+      'administer node form display',
     ]);
     $this->drupalLogin($user);
     $this->drupalPlaceBlock('local_tasks_block');
@@ -145,120 +146,298 @@ public function testWidget() {
     // Visit a node create page.
     $this->drupalGet('node/add/basic_page');
 
-    // Verify that both media widget instances are present.
+    // Verify that media widget instances are present.
     $assert_session->pageTextContains('Unlimited media');
     $assert_session->pageTextContains('Twin media');
+    $assert_session->pageTextContains('Single media type');
 
-    // Add to the unlimited cardinality field.
+    // Assert generic media library elements.
     $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
     $unlimited_button->click();
     $assert_session->assertWaitOnAjaxRequest();
-    // Assert that only type_one media items exist, since this field only
-    // accepts items of that type.
     $assert_session->pageTextContains('Media library');
+    $this->assertFalse($assert_session->elementExists('css', '.media-library-select-all')->isVisible());
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert that the media type menu is available when more than 1 type is
+    // configured for the field.
+    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '.media-library-menu');
+    $assert_session->elementTextContains('css', '.media-library-menu', 'Type One');
+    $assert_session->elementTextNotContains('css', '.media-library-menu', 'Type Two');
+    $assert_session->elementTextContains('css', '.media-library-menu', 'Type Three');
+    $assert_session->elementTextNotContains('css', '.media-library-menu', 'Type Four');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert that the media type menu is not available when only 1 type is
+    // configured for the field.
+    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_single_media_type"]');
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementNotExists('css', '.media-library-menu');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert menu link reordering.
+    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
+    $twin_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $links = $page->findAll('css', '.media-library-menu a');
+    $link_titles = [];
+    foreach ($links as $link) {
+      $link_titles[] = $link->getText();
+    }
+    $expected_link_titles = ['Type One', 'Type Two', 'Type Three', 'Type Four'];
+    $this->assertSame($link_titles, $expected_link_titles);
+    $this->drupalGet('admin/structure/types/manage/basic_page/form-display');
+    $page->find('css', 'input[name="field_twin_media_settings_edit"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->find('css', '.tabledrag-toggle-weight')->click();
+    $edit = [
+      'fields[field_twin_media][settings_edit_form][settings][media_types][type_one]' => 0,
+      'fields[field_twin_media][settings_edit_form][settings][media_types][type_three]' => 1,
+      'fields[field_twin_media][settings_edit_form][settings][media_types][type_four]' => 2,
+      'fields[field_twin_media][settings_edit_form][settings][media_types][type_two]' => 3,
+    ];
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $page->find('css', '.tabledrag-toggle-weight')->click();
+    $this->drupalGet('node/add/basic_page');
+    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
+    $twin_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $links = $page->findAll('css', '.media-library-menu a');
+    $link_titles = [];
+    foreach ($links as $link) {
+      $link_titles[] = $link->getText();
+    }
+    $expected_link_titles = ['Type One', 'Type Three', 'Type Four', 'Type Two'];
+    $this->assertSame($link_titles, $expected_link_titles);
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert media is only visible on the tab for the related media type.
+    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Dog');
     $assert_session->pageTextContains('Bear');
     $assert_session->pageTextNotContains('Turtle');
-    // Ensure that the "Select all" checkbox is not visible.
-    $this->assertFalse($assert_session->elementExists('css', '.media-library-select-all')->isVisible());
-    // Use an exposed filter.
+    $page->find('css', '.media-library-menu .type-three > a')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextNotContains('Dog');
+    $assert_session->pageTextNotContains('Bear');
+    $assert_session->pageTextNotContains('Turtle');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert the exposed name filter of the view.
+    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
     $session = $this->getSession();
     $session->getPage()->fillField('Name', 'Dog');
     $session->getPage()->pressButton('Apply Filters');
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Dog');
     $assert_session->pageTextNotContains('Bear');
-    // Clear the exposed filter.
     $session->getPage()->fillField('Name', '');
     $session->getPage()->pressButton('Apply Filters');
     $assert_session->assertWaitOnAjaxRequest();
-    // Select the first three media items (should be Dog/Cat/Bear).
-    $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
-    $checkboxes = $page->findAll('css', $checkbox_selector);
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextContains('Bear');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert the selection is persistent in the media library modal, the number
+    // of selected items is displayed correctly and the select button is only
+    // shown when there are selected items.
+    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
+    $twin_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // The select button and number of selected items are invisible when nothing
+    // is selected.
+    $select_button = $assert_session->elementExists('css', '.ui-dialog-buttonpane .media-library-select');
+    $this->assertFalse($select_button->isVisible());
+    $assert_session->elementNotExists('css', '.media-library-selected-count');
+    // Select a media item, assert the select button is shown and the hidden
+    // selection field contains the ID of the selected item.
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
     $checkboxes[0]->click();
+    $this->assertTrue($select_button->isVisible());
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
+    // Assert the number of selected items is displayed correctly.
+    $assert_session->elementExists('css', '.media-library-selected-count');
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
+    // Select another item and assert the number of selected items is updated.
     $checkboxes[1]->click();
-    $checkboxes[2]->click();
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '2 items selected');
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4,3');
+    // Assert unselected items are disabled when the maximum allowed items are
+    // selected (cardinality for this field is 2).
+    $this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
+    // Assert the selected items are updated when deselecting an item.
+    $checkboxes[0]->click();
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '3');
+    // Assert deselected items are available again.
+    $this->assertFalse($checkboxes[2]->hasAttribute('disabled'));
+    $this->assertFalse($checkboxes[3]->hasAttribute('disabled'));
+    // The selection should be persisted when navigating to other media types in
+    // the modal.
+    $page->find('css', '.media-library-menu .type-three > a')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->find('css', '.media-library-menu .type-one > a')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $selected_checkboxes = [];
+    foreach ($checkboxes as $checkbox) {
+      if ($checkbox->isChecked()) {
+        $selected_checkboxes[] = $checkbox->getValue();
+      }
+    }
+    $this->assertCount(1, $selected_checkboxes);
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', implode(',', $selected_checkboxes));
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
+    // Add to selection from another type.
+    $page->find('css', '.media-library-menu .type-two > a')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $checkboxes[0]->click();
+    // Assert the selection is updated correctly.
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '2 items selected');
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '3,8');
+    // Assert unselected items are disabled when the maximum allowed items are
+    // selected (cardinality for this field is 2).
+    $this->assertFalse($checkboxes[0]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[1]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
+    // Assert the checkboxes are also disabled on other pages.
+    $page->find('css', '.media-library-menu .type-one > a')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertTrue($checkboxes[0]->hasAttribute('disabled'));
+    $this->assertFalse($checkboxes[1]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[2]->hasAttribute('disabled'));
+    $this->assertTrue($checkboxes[3]->hasAttribute('disabled'));
+    // Select the items.
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
+
     // Ensure that the selection completed successfully.
     $assert_session->pageTextNotContains('Media library');
-    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Dog');
     $assert_session->pageTextContains('Cat');
-    $assert_session->pageTextContains('Bear');
-    // Remove "Dog" (happens to be the first remove button on the page).
+    $assert_session->pageTextContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
+
+    // Remove "Cat" (happens to be the first remove button on the page).
     $assert_session->elementExists('css', '.media-library-item__remove')->click();
     $assert_session->assertWaitOnAjaxRequest();
-    $assert_session->pageTextNotContains('Dog');
-    $assert_session->pageTextContains('Cat');
-    $assert_session->pageTextContains('Bear');
+    $assert_session->pageTextNotContains('Cat');
+    $assert_session->pageTextContains('Turtle');
 
-    // Open another Media library on the same page.
+    // Open the media library again and select another item.
     $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
     $twin_button->click();
     $assert_session->assertWaitOnAjaxRequest();
-    // This field allows both media types.
-    $assert_session->pageTextContains('Media library');
-    $assert_session->pageTextContains('Dog');
-    $assert_session->pageTextContains('Turtle');
-    // Attempt to select three items - the cardinality of this field is two so
-    // the third selection should be disabled.
-    $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
-    $checkboxes = $page->findAll('css', $checkbox_selector);
-    $this->assertFalse($checkboxes[5]->hasAttribute('disabled'));
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
     $checkboxes[0]->click();
-    $checkboxes[7]->click();
-    $this->assertTrue($checkboxes[5]->hasAttribute('disabled'));
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
-    // Ensure that the selection completed successfully, and we have only two
-    // media items of two different types.
-    $assert_session->pageTextNotContains('Media library');
-    $assert_session->pageTextContains('Horse');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Cat');
     $assert_session->pageTextContains('Turtle');
     $assert_session->pageTextNotContains('Snake');
 
+    // Assert we are not allowed to add more items to the field.
+    $assert_session->elementNotExists('css', '.media-library-open-button[href*="field_twin_media"]');
+
+    // Assert the selection is cleared when the modal is closed.
+    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // Nothing is selected yet.
+    $this->assertFalse($checkboxes[0]->isChecked());
+    $this->assertFalse($checkboxes[1]->isChecked());
+    $this->assertFalse($checkboxes[2]->isChecked());
+    $this->assertFalse($checkboxes[3]->isChecked());
+    // Select the first 2 items.
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $checkboxes[0]->click();
+    $checkboxes[1]->click();
+    $this->assertTrue($checkboxes[0]->isChecked());
+    $this->assertTrue($checkboxes[1]->isChecked());
+    $this->assertFalse($checkboxes[2]->isChecked());
+    $this->assertFalse($checkboxes[3]->isChecked());
+    // Close the dialog, reopen it and assert not is selected again.
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $unlimited_button->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $this->assertFalse($checkboxes[0]->isChecked());
+    $this->assertFalse($checkboxes[1]->isChecked());
+    $this->assertFalse($checkboxes[2]->isChecked());
+    $this->assertFalse($checkboxes[3]->isChecked());
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
     // Finally, save the form.
     $assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
     $this->submitForm([
       'title[0][value]' => 'My page',
-      'field_unlimited_media[selection][0][weight]' => '2',
+      'field_twin_media[selection][0][weight]' => '2',
     ], 'Save');
     $assert_session->pageTextContains('Basic Page My page has been created');
     // We removed this item earlier.
-    $assert_session->pageTextNotContains('Dog');
-    // This item should not have been selected due to cardinality constraints.
+    $assert_session->pageTextNotContains('Cat');
+    // This item was never selected.
     $assert_session->pageTextNotContains('Snake');
-    // "Cat" should come after "Bear", since we changed the weight.
-    $assert_session->elementExists('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child:contains("Cat")');
+    // "Dog" should come after "Turtle", since we changed the weight.
+    $assert_session->elementExists('css', '.field--name-field-twin-media > .field__items > .field__item:last-child:contains("Turtle")');
     // Make sure everything that was selected shows up.
-    $assert_session->pageTextContains('Cat');
-    $assert_session->pageTextContains('Bear');
-    $assert_session->pageTextContains('Horse');
+    $assert_session->pageTextContains('Dog');
     $assert_session->pageTextContains('Turtle');
 
     // Re-edit the content and make a new selection.
     $this->drupalGet('node/1/edit');
-    $assert_session->pageTextNotContains('Dog');
-    $assert_session->pageTextContains('Cat');
-    $assert_session->pageTextContains('Bear');
-    $assert_session->pageTextContains('Horse');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Cat');
+    $assert_session->pageTextNotContains('Bear');
+    $assert_session->pageTextNotContains('Horse');
     $assert_session->pageTextContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
     $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
     $unlimited_button->click();
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Media library');
-    // Select the first media items (should be Dog, again).
+    // Select all media items of type one (should also contain Dog, again).
     $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
     $checkboxes = $page->findAll('css', $checkbox_selector);
     $checkboxes[0]->click();
+    $checkboxes[1]->click();
+    $checkboxes[2]->click();
+    $checkboxes[3]->click();
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
-    // "Dog" and the existing selection should still exist.
     $assert_session->pageTextContains('Dog');
     $assert_session->pageTextContains('Cat');
     $assert_session->pageTextContains('Bear');
     $assert_session->pageTextContains('Horse');
     $assert_session->pageTextContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
+    $this->submitForm([], 'Save');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextContains('Cat');
+    $assert_session->pageTextContains('Bear');
+    $assert_session->pageTextContains('Horse');
+    $assert_session->pageTextContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
   }
 
   /**
@@ -276,6 +455,8 @@ public function testWidgetAnonymous() {
     // Verify that unprivileged users can't access the widget view.
     $this->drupalGet('admin/content/media-widget');
     $assert_session->responseContains('Access denied');
+    $this->drupalGet('media-library');
+    $assert_session->responseContains('Access denied');
 
     // Allow the anonymous user to create pages and view media.
     $this->grantPermissions($role, [
diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal6.php b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
index af32d9662d..6c00957399 100644
--- a/core/modules/migrate_drupal/tests/fixtures/drupal6.php
+++ b/core/modules/migrate_drupal/tests/fixtures/drupal6.php
@@ -46123,7 +46123,7 @@
   'name' => 'search',
   'type' => 'module',
   'owner' => '',
-  'status' => '1',
+  'status' => '0',
   'throttle' => '0',
   'bootstrap' => '0',
   'schema_version' => '-1',
diff --git a/core/modules/migrate_drupal/tests/fixtures/drupal7.php b/core/modules/migrate_drupal/tests/fixtures/drupal7.php
index 57e831b5b5..68171683d8 100644
--- a/core/modules/migrate_drupal/tests/fixtures/drupal7.php
+++ b/core/modules/migrate_drupal/tests/fixtures/drupal7.php
@@ -51205,7 +51205,7 @@
 ))
 ->values(array(
   'name' => 'search_active_modules',
-  'value' => 'a:2:{s:4:"node";s:4:"node";s:4:"user";i:0;}',
+  'value' => 'a:2:{s:4:"node";s:4:"node";s:4:"user";s:4:"user";}',
 ))
 ->values(array(
   'name' => 'search_and_or_limit',
diff --git a/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php b/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php
index bf1943aa1d..0b4dfd752f 100644
--- a/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php
+++ b/core/modules/responsive_image/src/Entity/ResponsiveImageStyle.php
@@ -34,13 +34,6 @@
  *     "id" = "id",
  *     "label" = "label"
  *   },
- *   config_export = {
- *     "id",
- *     "label",
- *     "image_style_mappings",
- *     "breakpoint_group",
- *     "fallback_image_style",
- *   },
  *   links = {
  *     "edit-form" = "/admin/config/media/responsive-image-style/{responsive_image_style}",
  *     "duplicate-form" = "/admin/config/media/responsive-image-style/{responsive_image_style}/duplicate",
diff --git a/core/modules/search/migrations/d7_search_page.yml b/core/modules/search/migrations/d7_search_page.yml
deleted file mode 100644
index 033bd409d6..0000000000
--- a/core/modules/search/migrations/d7_search_page.yml
+++ /dev/null
@@ -1,47 +0,0 @@
-id: d7_search_page
-label: Search page configuration
-migration_tags:
-  - Drupal 7
-  - Configuration
-source:
-  plugin: d7_search_page
-  variables:
-    - node_rank_comments
-    - node_rank_promote
-    - node_rank_relevance
-    - node_rank_sticky
-    - node_rank_views
-  constants:
-    suffix: _search
-process:
-  module: module
-  module_exists:
-    -
-      plugin: skip_on_empty
-      method: row
-      source: module_exists
-  status:
-    -
-      plugin: static_map
-      source: status
-      map:
-        node: true
-        user: true
-      default_value: false
-  id:
-    -
-      plugin: concat
-      source:
-        - module
-        - 'constants/suffix'
-  plugin:
-    -
-      plugin: concat
-      source:
-        - module
-        - 'constants/suffix'
-  path: module
-  'configuration/rankings':
-    plugin: search_configuration_rankings
-destination:
-  plugin: entity:search_page
diff --git a/core/modules/search/migrations/search_page.yml b/core/modules/search/migrations/search_page.yml
index 2847a191ff..cf465003ab 100644
--- a/core/modules/search/migrations/search_page.yml
+++ b/core/modules/search/migrations/search_page.yml
@@ -2,9 +2,10 @@ id: search_page
 label: Search page configuration
 migration_tags:
   - Drupal 6
+  - Drupal 7
   - Configuration
 source:
-  plugin: d6_search_page
+  plugin: variable
   variables:
     - node_rank_comments
     - node_rank_promote
@@ -16,8 +17,8 @@ source:
     id: node_search
     path: node
     plugin: node_search
+  source_module: search
 process:
-  module: module
   id: 'constants/id'
   path: 'constants/path'
   plugin: 'constants/plugin'
diff --git a/core/modules/search/src/Plugin/migrate/destination/EntitySearchPage.php b/core/modules/search/src/Plugin/migrate/destination/EntitySearchPage.php
index 2d9c118c3d..ddd8da7956 100644
--- a/core/modules/search/src/Plugin/migrate/destination/EntitySearchPage.php
+++ b/core/modules/search/src/Plugin/migrate/destination/EntitySearchPage.php
@@ -2,93 +2,17 @@
 
 namespace Drupal\search\Plugin\migrate\destination;
 
-use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityStorageInterface;
-use Drupal\Core\Extension\ModuleHandlerInterface;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\migrate\MigrateException;
-use Drupal\migrate\Plugin\MigrateIdMapInterface;
-use Drupal\migrate\Plugin\MigrationInterface;
 use Drupal\migrate\Plugin\migrate\destination\EntityConfigBase;
 use Drupal\migrate\Row;
-use Drupal\search\Plugin\ConfigurableSearchPluginBase;
-use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
- * Migrate destination for search page.
- *
  * @MigrateDestination(
  *   id = "entity:search_page"
  * )
  */
 class EntitySearchPage extends EntityConfigBase {
 
-  /**
-   * The module handler.
-   *
-   * @var \Drupal\Core\Extension\ModuleHandlerInterface
-   */
-  protected $moduleHandler;
-
-  /**
-   * Constructs a new EntitySearchPage.
-   *
-   * @param array $configuration
-   *   A configuration array containing information about the plugin instance.
-   * @param string $plugin_id
-   *   The plugin_id for the plugin instance.
-   * @param mixed $plugin_definition
-   *   The plugin implementation definition.
-   * @param \Drupal\migrate\plugin\MigrationInterface $migration
-   *   The migration.
-   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
-   *   The storage for this entity type.
-   * @param array $bundles
-   *   The list of bundles this entity type has.
-   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
-   *   The language manager.
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The configuration factory.
-   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
-   *   The module handler.
-   */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration, EntityStorageInterface $storage, array $bundles, LanguageManagerInterface $language_manager, ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) {
-    parent::__construct($configuration, $plugin_id, $plugin_definition, $migration, $storage, $bundles, $language_manager, $config_factory);
-    $this->moduleHandler = $module_handler;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, MigrationInterface $migration = NULL) {
-    $entity_type_id = static::getEntityTypeId($plugin_id);
-    return new static(
-      $configuration,
-      $plugin_id,
-      $plugin_definition,
-      $migration,
-      $container->get('entity.manager')->getStorage($entity_type_id),
-      array_keys($container->get('entity.manager')->getBundleInfo($entity_type_id)),
-      $container->get('language_manager'),
-      $container->get('config.factory'),
-      $container->get('module_handler')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function import(Row $row, array $old_destination_id_values = []) {
-    // The search page settings may be for a module not enabled on the
-    // destination so make sure it is enabled for updating search page settings.
-    if ($this->moduleHandler->moduleExists($row->getDestinationProperty('module'))) {
-      return parent::import($row, $old_destination_id_values);
-    }
-    $msg = sprintf("Search module '%s' is not enabled on this site.", $row->getDestinationProperty('module'));
-    throw new MigrateException($msg, 0, NULL, MigrationInterface::MESSAGE_INFORMATIONAL, MigrateIdMapInterface::STATUS_IGNORED);
-  }
-
   /**
    * Updates the entity with the contents of a row.
    *
@@ -98,13 +22,8 @@ public function import(Row $row, array $old_destination_id_values = []) {
    *   The row object to update from.
    */
   protected function updateEntity(EntityInterface $entity, Row $row) {
-    parent::updateEntity($entity, $row);
     $entity->setPlugin($row->getDestinationProperty('plugin'));
-    // The user_search plugin does not have a setConfiguration() method.
-    $plugin = $entity->getPlugin();
-    if ($plugin instanceof ConfigurableSearchPluginBase) {
-      $plugin->setConfiguration($row->getDestinationProperty('configuration'));
-    }
+    $entity->getPlugin()->setConfiguration($row->getDestinationProperty('configuration'));
   }
 
 }
diff --git a/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php b/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
index 153161f9a9..8199215c32 100644
--- a/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
+++ b/core/modules/search/src/Plugin/migrate/process/SearchConfigurationRankings.php
@@ -21,7 +21,7 @@ class SearchConfigurationRankings extends ProcessPluginBase {
    * Generate the configuration rankings.
    */
   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
-    $return = NULL;
+    $return = [];
     foreach ($row->getSource() as $name => $rank) {
       if (substr($name, 0, 10) == 'node_rank_' && is_numeric($rank)) {
         $return[substr($name, 10)] = $rank;
diff --git a/core/modules/search/src/Plugin/migrate/process/d6/SearchConfigurationRankings.php b/core/modules/search/src/Plugin/migrate/process/d6/SearchConfigurationRankings.php
index 402d74f989..206f1f8060 100644
--- a/core/modules/search/src/Plugin/migrate/process/d6/SearchConfigurationRankings.php
+++ b/core/modules/search/src/Plugin/migrate/process/d6/SearchConfigurationRankings.php
@@ -2,28 +2,32 @@
 
 namespace Drupal\search\Plugin\migrate\process\d6;
 
-use Drupal\search\Plugin\migrate\process\SearchConfigurationRankings as BaseSearchConfigurationRankings;
+use Drupal\migrate\ProcessPluginBase;
+use Drupal\migrate\MigrateExecutableInterface;
+use Drupal\migrate\Row;
 
 /**
  * Generate configuration rankings.
  *
- * @deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.x. Use
- *   \Drupal\search\Plugin\migrate\process\SearchConfigurationRankings instead.
- *
  * @MigrateProcessPlugin(
  *   id = "d6_search_configuration_rankings"
  * )
- *
- * @see https://www.drupal.org/node/3009364
  */
-class SearchConfigurationRankings extends BaseSearchConfigurationRankings {
+class SearchConfigurationRankings extends ProcessPluginBase {
 
   /**
    * {@inheritdoc}
+   *
+   * Generate the configuration rankings.
    */
-  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
-    @trigger_error('SearchConfigurationRankings is deprecated in Drupal 8.7.x and will be removed before Drupal 9.0.0. Use Drupal\search\Plugin\migrate\process\SearchConfigurationRankings instead. See https://www.drupal.org/node/3009364.', E_USER_DEPRECATED);
-    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
+    $return = [];
+    foreach ($row->getSource() as $name => $rank) {
+      if (substr($name, 0, 10) == 'node_rank_' && $rank) {
+        $return[substr($name, 10)] = $rank;
+      }
+    }
+    return $return;
   }
 
 }
diff --git a/core/modules/search/src/Plugin/migrate/source/d6/SearchPage.php b/core/modules/search/src/Plugin/migrate/source/d6/SearchPage.php
deleted file mode 100644
index 085de33f17..0000000000
--- a/core/modules/search/src/Plugin/migrate/source/d6/SearchPage.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-
-namespace Drupal\search\Plugin\migrate\source\d6;
-
-use Drupal\migrate_drupal\Plugin\migrate\source\Variable;
-
-/**
- * Get node search rankings for core modules.
- *
- * @MigrateSource(
- *   id = "d6_search_page",
- *   source_module = "search"
- * )
- */
-class SearchPage extends Variable {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function values() {
-    // Add a module key to identify the source search provider, node. This value
-    // is used in the EntitySearchPage destination plugin.
-    return array_merge(['module' => 'node'], parent::values());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fields() {
-    return [
-      'module' => $this->t('The module providing a search page.'),
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIds() {
-    $ids['module']['type'] = 'string';
-    return $ids;
-  }
-
-}
diff --git a/core/modules/search/src/Plugin/migrate/source/d7/SearchPage.php b/core/modules/search/src/Plugin/migrate/source/d7/SearchPage.php
deleted file mode 100644
index 6ae2b22c38..0000000000
--- a/core/modules/search/src/Plugin/migrate/source/d7/SearchPage.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-namespace Drupal\search\Plugin\migrate\source\d7;
-
-use Drupal\migrate\Row;
-use Drupal\migrate_drupal\Plugin\migrate\source\Variable;
-
-/**
- * Get search_active_modules and rankings for core modules.
- *
- * @MigrateSource(
- *   id = "d7_search_page",
- *   source_module = "search"
- * )
- */
-class SearchPage extends Variable {
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function initializeIterator() {
-    return new \ArrayIterator($this->values());
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function values() {
-    $search_active_modules = $this->variableGet('search_active_modules', '');
-    $values = [];
-    foreach (['node', 'user'] as $module) {
-      if (isset($search_active_modules[$module])) {
-        // Add a module key to identify the source search provider. This value
-        // is used in the EntitySearchPage destination plugin.
-        $tmp = [
-          'module' => $module,
-          'status' => $search_active_modules[$module],
-        ];
-        // Add the node_rank_* variables (only relevant to the node module).
-        if ($module === 'node') {
-          $tmp = array_merge($tmp, parent::values());
-        }
-        $values[] = $tmp;
-      }
-    }
-    return $values;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function fields() {
-    return [
-      'module' => $this->t('The module providing a search page.'),
-      'status' => $this->t('Whether or not this module is enabled for search.'),
-    ];
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIds() {
-    $ids['module']['type'] = 'string';
-    return $ids;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function count($refresh = FALSE) {
-    return $this->initializeIterator()->count();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function prepareRow(Row $row) {
-    $exists = $this->moduleExists($row->getSourceProperty('module'));
-    $row->setSourceProperty('module_exists', $exists);
-    return parent::prepareRow($row);
-  }
-
-}
diff --git a/core/modules/search/tests/src/Kernel/Migrate/d6/MigrateSearchPageTest.php b/core/modules/search/tests/src/Kernel/Migrate/d6/MigrateSearchPageTest.php
index 2d25400dba..a70114a6a9 100644
--- a/core/modules/search/tests/src/Kernel/Migrate/d6/MigrateSearchPageTest.php
+++ b/core/modules/search/tests/src/Kernel/Migrate/d6/MigrateSearchPageTest.php
@@ -7,7 +7,7 @@
 use Drupal\search\Entity\SearchPage;
 
 /**
- * Upgrade search page variables.
+ * Upgrade search rank settings to search.page.*.yml.
  *
  * @group migrate_drupal_6
  */
diff --git a/core/modules/search/tests/src/Kernel/Migrate/d7/MigrateSearchPageTest.php b/core/modules/search/tests/src/Kernel/Migrate/d7/MigrateSearchPageTest.php
index 9e2e1a24fd..86d3b22009 100644
--- a/core/modules/search/tests/src/Kernel/Migrate/d7/MigrateSearchPageTest.php
+++ b/core/modules/search/tests/src/Kernel/Migrate/d7/MigrateSearchPageTest.php
@@ -7,7 +7,7 @@
 use Drupal\search\Entity\SearchPage;
 
 /**
- * Tests migration of search page status and settings.
+ * Upgrade search rank settings to search.page.*.yml.
  *
  * @group migrate_drupal_7
  */
@@ -18,50 +18,34 @@ class MigrateSearchPageTest extends MigrateDrupal7TestBase {
    *
    * {@inheritdoc}
    */
-  public static $modules = ['search'];
+  public static $modules = ['node', 'search'];
 
   /**
-   * Asserts various aspects of an SearchPage entity.
-   *
-   * @param string $id
-   *   The expected search page ID.
-   * @param string $path
-   *   The expected path of the search page.
-   * @param bool $status
-   *   The expected status of the search page.
-   * @param array $expected_config
-   *   An array of expected configuration for the search page.
+   * {@inheritdoc}
    */
-  protected function assertEntity($id, $path, $status = FALSE, array $expected_config = NULL) {
-    /** @var \Drupal\search\Entity\SearchPage $search_page */
-    $search_page = SearchPage::load($id);
-    $this->assertSame($id, $search_page->id());
-    $this->assertSame($path, $search_page->getPath());
-    $this->assertSame($status, $search_page->status());
-    if (isset($expected_config)) {
-      $configuration = $search_page->getPlugin()->getConfiguration();
-      $this->assertSame($expected_config, $configuration);
-    }
+  protected function setUp() {
+    parent::setUp();
+    $this->executeMigration('search_page');
   }
 
   /**
-   * Tests migration of search status and settings to search page entity.
+   * Tests Drupal 7 search ranking to Drupal 8 search page entity migration.
    */
   public function testSearchPage() {
-    $this->enableModules(['node']);
-    $this->installConfig(['search']);
-    $this->executeMigration('d7_search_page');
-    $configuration = [
-      'rankings' => [
-        'comments' => 0,
-        'promote' => 0,
-        'relevance' => 2,
-        'sticky' => 0,
-        'views' => 0,
-      ],
+    $id = 'node_search';
+    /** @var \Drupal\search\Entity\SearchPage $search_page */
+    $search_page = SearchPage::load($id);
+    $this->assertIdentical($id, $search_page->id());
+    $configuration = $search_page->getPlugin()->getConfiguration();
+    $expected_rankings = [
+      'comments' => 0,
+      'promote' => 0,
+      'relevance' => 2,
+      'sticky' => 0,
+      'views' => 0,
     ];
-    $this->assertEntity('node_search', 'node', TRUE, $configuration);
-    $this->assertEntity('user_search', 'user');
+    $this->assertIdentical($expected_rankings, $configuration['rankings']);
+    $this->assertIdentical('node', $search_page->getPath());
 
     // Test that we can re-import using the EntitySearchPage destination.
     Database::getConnection('default', 'migrate')
@@ -71,37 +55,13 @@ public function testSearchPage() {
       ->execute();
 
     /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
-    $migration = $this->getMigration('d7_search_page');
+    $migration = $this->getMigration('search_page');
     // Indicate we're rerunning a migration that's already run.
     $migration->getIdMap()->prepareUpdate();
     $this->executeMigration($migration);
-    $configuration['rankings']['comments'] = 4;
-    $this->assertEntity('node_search', 'node', TRUE, $configuration);
-  }
-
-  /**
-   * Tests that search page is only migrated for modules enabled on D8 site.
-   */
-  public function testModuleExists() {
-    $this->installConfig(['search']);
-    $this->executeMigration('d7_search_page');
-
-    $this->assertNull(SearchPage::load('node_search'));
-    $this->assertEntity('user_search', 'user');
-  }
-
-  /**
-   * Tests that a search page will be created if it does not exist.
-   */
-  public function testUserSearchCreate() {
-    $this->enableModules(['node']);
-    $this->installConfig(['search']);
-    /** @var \Drupal\search\Entity\SearchPage $search_page */
-    $search_page = SearchPage::load('user_search');
-    $search_page->delete();
-    $this->executeMigration('d7_search_page');
 
-    $this->assertEntity('user_search', 'user');
+    $configuration = SearchPage::load($id)->getPlugin()->getConfiguration();
+    $this->assertIdentical(4, $configuration['rankings']['comments']);
   }
 
 }
diff --git a/core/modules/search/tests/src/Unit/Plugin/migrate/source/d6/SearchPageTest.php b/core/modules/search/tests/src/Unit/Plugin/migrate/source/d6/SearchPageTest.php
deleted file mode 100644
index c42e90857f..0000000000
--- a/core/modules/search/tests/src/Unit/Plugin/migrate/source/d6/SearchPageTest.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-
-namespace Drupal\Tests\search\Unit\Plugin\migrate\source\d6;
-
-use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
-
-/**
- * Tests D6 search page source plugin.
- *
- * @covers \Drupal\search\Plugin\migrate\source\d6\SearchPage
- * @group search
- */
-class SearchPageTest extends MigrateSqlSourceTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = ['search', 'migrate_drupal'];
-
-  /**
-   * {@inheritdoc}
-   */
-  public function providerSource() {
-    $tests[0]['source_data'] = [
-      'variable' => [
-        [
-          'name' => 'node_rank_comments',
-          'value' => 's:1:"5";',
-        ],
-        [
-          'name' => 'node_rank_promote',
-          'value' => 's:1:"1";',
-        ],
-      ],
-      'system' => [
-        [
-          'name' => 'node',
-          'type' => 'module',
-          'status' => '1',
-        ],
-      ],
-    ];
-
-    $tests[0]['expected_data'] = [
-      [
-        'module' => 'node',
-        'node_rank_comments' => '5',
-        'node_rank_promote' => '1',
-      ],
-    ];
-
-    $tests[0]['expected_count'] = NULL;
-
-    $tests[0]['configuration'] = [
-      'variables' => ['node_rank_comments', 'node_rank_promote'],
-    ];
-
-    return $tests;
-  }
-
-}
diff --git a/core/modules/search/tests/src/Unit/Plugin/migrate/source/d7/SearchPageTest.php b/core/modules/search/tests/src/Unit/Plugin/migrate/source/d7/SearchPageTest.php
deleted file mode 100644
index 112892934c..0000000000
--- a/core/modules/search/tests/src/Unit/Plugin/migrate/source/d7/SearchPageTest.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-namespace Drupal\Tests\search\Unit\Plugin\migrate\source\d7;
-
-use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
-
-/**
- * Tests D7 search page source plugin.
- *
- * @covers \Drupal\search\Plugin\migrate\source\d7\SearchPage
- * @group search
- */
-class SearchPageTest extends MigrateSqlSourceTestBase {
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = ['search', 'migrate_drupal'];
-
-  /**
-   * {@inheritdoc}
-   */
-  public function providerSource() {
-    $tests[0]['source_data'] = [
-      'variable' => [
-        [
-          'name' => 'search_active_modules',
-          'value' => 'a:2:{s:4:"node";s:4:"node";s:4:"user";i:0;}',
-        ],
-        [
-          'name' => 'node_rank_comments',
-          'value' => 's:1:"5";',
-        ],
-        [
-          'name' => 'node_rank_promote',
-          'value' => 's:1:"1";',
-        ],
-      ],
-      'system' => [
-        [
-          'name' => 'node',
-          'type' => 'module',
-          'status' => '0',
-        ],
-        [
-          'name' => 'user',
-          'type' => 'module',
-          'status' => '1',
-        ],
-      ],
-    ];
-
-    $tests[0]['expected_data'] = [
-      [
-        'module' => 'node',
-        'status' => 'node',
-        'module_exists' => FALSE,
-        'node_rank_comments' => '5',
-        'node_rank_promote' => '1',
-      ],
-      [
-        'module' => 'user',
-        'status' => 0,
-        'module_exists' => TRUE,
-      ],
-    ];
-
-    $tests[0]['expected_count'] = NULL;
-
-    $tests[0]['configuration'] = [
-      'variables' => ['node_rank_comments', 'node_rank_promote'],
-    ];
-
-    return $tests;
-  }
-
-}
diff --git a/core/modules/user/src/Tests/RestRegisterUserTest.php b/core/modules/user/src/Tests/RestRegisterUserTest.php
new file mode 100644
index 0000000000..31bcce4d29
--- /dev/null
+++ b/core/modules/user/src/Tests/RestRegisterUserTest.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Drupal\user\Tests;
+
+use Drupal\Core\Url;
+use Drupal\rest\Tests\RESTTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * Tests user registration via REST resource.
+ *
+ * @group user
+ */
+class RestRegisterUserTest extends RESTTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['hal'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->enableService('user_registration', 'POST', 'hal_json');
+
+    Role::load(RoleInterface::ANONYMOUS_ID)
+      ->grantPermission('restful post user_registration')
+      ->save();
+
+    Role::load(RoleInterface::AUTHENTICATED_ID)
+      ->grantPermission('restful post user_registration')
+      ->save();
+  }
+
+  /**
+   * Tests that only anonymous users can register users.
+   */
+  public function testRegisterUser() {
+    // Verify that an authenticated user cannot register a new user, despite
+    // being granted permission to do so because only anonymous users can
+    // register themselves, authenticated users with the necessary permissions
+    // can POST a new user to the "user" REST resource.
+    $user = $this->createUser();
+    $this->drupalLogin($user);
+    $this->registerRequest('palmer.eldritch');
+    $this->assertResponse('403', 'Only anonymous users can register users.');
+    $this->drupalLogout();
+
+    $user_settings = $this->config('user.settings');
+
+    // Test out different setting User Registration and Email Verification.
+    // Allow visitors to register with no email verification.
+    $user_settings->set('register', USER_REGISTER_VISITORS);
+    $user_settings->set('verify_mail', 0);
+    $user_settings->save();
+    $user = $this->registerUser('Palmer.Eldritch');
+    $this->assertFalse($user->isBlocked());
+    $this->assertFalse(empty($user->getPassword()));
+    $email_count = count($this->drupalGetMails());
+    $this->assertEqual(0, $email_count);
+
+    // Attempt to register without sending a password.
+    $this->registerRequest('Rick.Deckard', FALSE);
+    $this->assertResponse('422', 'No password provided');
+
+    // Allow visitors to register with email verification.
+    $user_settings->set('register', USER_REGISTER_VISITORS);
+    $user_settings->set('verify_mail', 1);
+    $user_settings->save();
+    $user = $this->registerUser('Jason.Taverner', FALSE);
+    $this->assertTrue(empty($user->getPassword()));
+    $this->assertTrue($user->isBlocked());
+    $this->assertMailString('body', 'You may now log in by clicking this link', 1);
+
+    // Attempt to register with a password when e-mail verification is on.
+    $this->registerRequest('Estraven', TRUE);
+    $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.');
+
+    // Allow visitors to register with Admin approval and e-mail verification.
+    $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+    $user_settings->set('verify_mail', 1);
+    $user_settings->save();
+    $user = $this->registerUser('Bob.Arctor', FALSE);
+    $this->assertTrue(empty($user->getPassword()));
+    $this->assertTrue($user->isBlocked());
+    $this->assertMailString('body', 'Your application for an account is', 2);
+    $this->assertMailString('body', 'Bob.Arctor has applied for an account', 2);
+
+    // Attempt to register with a password when e-mail verification is on.
+    $this->registerRequest('Ursula', TRUE);
+    $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.');
+
+    // Allow visitors to register with Admin approval and no email verification.
+    $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+    $user_settings->set('verify_mail', 0);
+    $user_settings->save();
+    $user = $this->registerUser('Argaven');
+    $this->assertFalse(empty($user->getPassword()));
+    $this->assertTrue($user->isBlocked());
+    $this->assertMailString('body', 'Your application for an account is', 2);
+    $this->assertMailString('body', 'Argaven has applied for an account', 2);
+
+    // Attempt to register without sending a password.
+    $this->registerRequest('Tibe', FALSE);
+    $this->assertResponse('422', 'No password provided');
+  }
+
+  /**
+   * Creates serialize user values.
+   *
+   * @param string $name
+   *   The name of the user. Use only valid values for emails.
+   *
+   * @param bool $include_password
+   *   Whether to include a password in the user values.
+   *
+   * @return string
+   *   Serialized user values.
+   */
+  protected function createSerializedUser($name, $include_password = TRUE) {
+    global $base_url;
+    // New user info to be serialized.
+    $data = [
+      "_links" => ["type" => ["href" => $base_url . "/rest/type/user/user"]],
+      "langcode" => [["value" => "en"]],
+      "name" => [["value" => $name]],
+      "mail" => [["value" => "$name@example.com"]],
+    ];
+    if ($include_password) {
+      $data['pass']['value'] = 'SuperSecretPassword';
+    }
+
+    // Create a HAL+JSON version for the user entity we want to create.
+    $serialized = $this->container->get('serializer')
+      ->serialize($data, 'hal_json');
+    return $serialized;
+  }
+
+  /**
+   * Registers a user via REST resource.
+   *
+   * @param $name
+   *   User name.
+   *
+   * @param bool $include_password
+   *
+   * @return bool|\Drupal\user\Entity\User
+   */
+  protected function registerUser($name, $include_password = TRUE) {
+    // Verify that an anonymous user can register.
+    $this->registerRequest($name, $include_password);
+    $this->assertResponse('200', 'HTTP response code is correct.');
+    $user = user_load_by_name($name);
+    $this->assertFalse(empty($user), 'User was create as expected');
+    return $user;
+  }
+
+  /**
+   * Make a REST user registration request.
+   *
+   * @param $name
+   * @param $include_password
+   */
+  protected function registerRequest($name, $include_password = TRUE) {
+    $serialized = $this->createSerializedUser($name, $include_password);
+    $this->httpRequest(Url::fromRoute('rest.user_registration.POST', ['_format' => 'hal_json']), 'POST', $serialized, 'application/hal+json');
+  }
+
+}
diff --git a/core/modules/user/tests/src/Functional/RestRegisterUserTest.php b/core/modules/user/tests/src/Functional/RestRegisterUserTest.php
deleted file mode 100644
index 32e875cc31..0000000000
--- a/core/modules/user/tests/src/Functional/RestRegisterUserTest.php
+++ /dev/null
@@ -1,270 +0,0 @@
-<?php
-
-namespace Drupal\Tests\user\Functional;
-
-use Drupal\Core\Url;
-use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
-use Drupal\Tests\rest\Functional\ResourceTestBase;
-use GuzzleHttp\RequestOptions;
-use Drupal\Core\Test\AssertMailTrait;
-
-/**
- * Tests user registration via REST resource.
- *
- * @group user
- */
-class RestRegisterUserTest extends ResourceTestBase {
-
-  use CookieResourceTestTrait;
-
-  use AssertMailTrait {
-    getMails as drupalGetMails;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $format = 'hal_json';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $mimeType = 'application/hal+json';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $auth = 'cookie';
-
-  /**
-   * {@inheritdoc}
-   */
-  protected static $resourceConfigId = 'user_registration';
-
-  /**
-   * {@inheritdoc}
-   */
-  public static $modules = ['hal', 'user'];
-
-  const USER_EMAIL_DOMAIN = '@example.com';
-
-  const TEST_EMAIL_DOMAIN = 'simpletest@example.com';
-
-  /**
-   * {@inheritdoc}
-   */
-  public function setUp() {
-    parent::setUp();
-
-    $auth = isset(static::$auth) ? [static::$auth] : [];
-    $this->provisionResource([static::$format], $auth);
-
-    $this->setUpAuthorization('POST');
-  }
-
-  /**
-   * Tests that only anonymous users can register users.
-   */
-  public function testRegisterUser() {
-    $config = $this->config('user.settings');
-
-    // Test out different setting User Registration and Email Verification.
-    // Allow visitors to register with no email verification.
-    $config->set('register', USER_REGISTER_VISITORS);
-    $config->set('verify_mail', 0);
-    $config->save();
-    $user = $this->registerUser('Palmer.Eldritch');
-    $this->assertFalse($user->isBlocked());
-    $this->assertFalse(empty($user->getPassword()));
-    $email_count = count($this->drupalGetMails());
-
-    $this->assertEquals($email_count, 0);
-
-    // Attempt to register without sending a password.
-    $response = $this->registerRequest('Rick.Deckard', FALSE);
-    $this->assertResourceErrorResponse(422, "No password provided.", $response);
-
-    // Attempt to register with a password when e-mail verification is on.
-    $config->set('register', USER_REGISTER_VISITORS);
-    $config->set('verify_mail', 1);
-    $config->save();
-    $response = $this->registerRequest('Estraven', TRUE);
-    $this->assertResourceErrorResponse(422, 'A Password cannot be specified. It will be generated on login.', $response);
-
-    // Allow visitors to register with email verification.
-    $config->set('register', USER_REGISTER_VISITORS);
-    $config->set('verify_mail', 1);
-    $config->save();
-    $name = 'Jason.Taverner';
-    $user = $this->registerUser($name, FALSE);
-    $this->assertTrue(empty($user->getPassword()));
-    $this->assertTrue($user->isBlocked());
-    $this->resetAll();
-
-    $this->assertMailString('body', 'You may now log in by clicking this link', 1);
-
-    // Allow visitors to register with Admin approval and no email verification.
-    $config->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
-    $config->set('verify_mail', 0);
-    $config->save();
-    $name = 'Argaven';
-    $user = $this->registerUser($name);
-    $this->resetAll();
-    $this->assertFalse(empty($user->getPassword()));
-    $this->assertTrue($user->isBlocked());
-    $this->assertMailString('body', 'Your application for an account is', 2);
-    $this->assertMailString('body', 'Argaven has applied for an account', 2);
-
-    // Allow visitors to register with Admin approval and e-mail verification.
-    $config->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
-    $config->set('verify_mail', 1);
-    $config->save();
-    $name = 'Bob.Arctor';
-    $user = $this->registerUser($name, FALSE);
-    $this->resetAll();
-    $this->assertTrue(empty($user->getPassword()));
-    $this->assertTrue($user->isBlocked());
-
-    $this->assertMailString('body', 'Your application for an account is', 2);
-    $this->assertMailString('body', 'Bob.Arctor has applied for an account', 2);
-
-    // Verify that an authenticated user cannot register a new user, despite
-    // being granted permission to do so because only anonymous users can
-    // register themselves, authenticated users with the necessary permissions
-    // can POST a new user to the "user" REST resource.
-    $this->initAuthentication();
-    $response = $this->registerRequest($this->account->getAccountName());
-    $this->assertResourceErrorResponse(403, "Only anonymous users can register a user.", $response);
-  }
-
-  /**
-   * Create the request body.
-   *
-   * @param string $name
-   *   Name.
-   * @param bool $include_password
-   *   Include Password.
-   * @param bool $include_email
-   *   Include Email.
-   *
-   * @return array
-   *   Return the request body.
-   */
-  protected function createRequestBody($name, $include_password = TRUE, $include_email = TRUE) {
-    global $base_url;
-    $request_body = [
-      '_links' => ['type' => ["href" => $base_url . "/rest/type/user/user"]],
-      'langcode' => [['value' => 'en']],
-      'name' => [['value' => $name]],
-    ];
-
-    if ($include_email) {
-      $request_body['mail'] = [['value' => $name . self::USER_EMAIL_DOMAIN]];
-    }
-
-    if ($include_password) {
-      $request_body['pass']['value'] = 'SuperSecretPassword';
-    }
-
-    return $request_body;
-  }
-
-  /**
-   * Helper function to generate the request body.
-   *
-   * @param array $request_body
-   *   The request body array.
-   *
-   * @return array
-   *   Return the request options.
-   */
-  protected function createRequestOptions(array $request_body) {
-    $request_options = $this->getAuthenticationRequestOptions('POST');
-    $request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, static::$format);
-    $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
-
-    return $request_options;
-  }
-
-  /**
-   * Registers a user via REST resource.
-   *
-   * @param string $name
-   *   User name.
-   * @param bool $include_password
-   *   Include the password.
-   * @param bool $include_email
-   *   Include the email?
-   *
-   * @return bool|\Drupal\user\Entity\User
-   *   Return bool or the user.
-   */
-  protected function registerUser($name, $include_password = TRUE, $include_email = TRUE) {
-    // Verify that an anonymous user can register.
-    $response = $this->registerRequest($name, $include_password, $include_email);
-    $this->assertResourceResponse(200, FALSE, $response);
-    $user = user_load_by_name($name);
-    $this->assertFalse(empty($user), 'User was create as expected');
-    return $user;
-  }
-
-  /**
-   * Make a REST user registration request.
-   *
-   * @param string $name
-   *   The name.
-   * @param bool $include_password
-   *   Include the password?
-   * @param bool $include_email
-   *   Include the email?
-   *
-   * @return \Psr\Http\Message\ResponseInterface
-   *   Return the Response.
-   */
-  protected function registerRequest($name, $include_password = TRUE, $include_email = TRUE) {
-
-    $user_register_url = Url::fromRoute('user.register')
-      ->setRouteParameter('_format', static::$format);
-    $request_body = $this->createRequestBody($name, $include_password, $include_email);
-    $request_options = $this->createRequestOptions($request_body);
-    $response = $this->request('POST', $user_register_url, $request_options);
-
-    return $response;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function setUpAuthorization($method) {
-    switch ($method) {
-      case 'POST':
-        $this->grantPermissionsToAuthenticatedRole(['restful post user_registration']);
-        $this->grantPermissionsToAnonymousRole(['restful post user_registration']);
-        break;
-
-      default:
-        throw new \UnexpectedValueException();
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function assertNormalizationEdgeCases($method, Url $url, array $request_options) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getExpectedUnauthorizedAccessMessage($method) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getExpectedBcUnauthorizedAccessMessage($method) {}
-
-  /**
-   * {@inheritdoc}
-   */
-  protected function getExpectedUnauthorizedAccessCacheability() {}
-
-}
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
index 41a63ac4c5..1cf57dfa5c 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityStorageTest.php
@@ -118,9 +118,6 @@ protected function setUp() {
         'uuid' => 'uuid',
         'langcode' => 'langcode',
       ],
-      'config_export' => [
-        'id',
-      ],
       'list_cache_tags' => [$this->entityTypeId . '_list'],
     ]);
 
@@ -257,14 +254,7 @@ public function testSaveInsert(EntityInterface $entity) {
     $immutable_config_object->isNew()->willReturn(TRUE);
 
     $config_object = $this->prophesize(Config::class);
-    $config_object
-      ->setData([
-        'id' => 'foo',
-        'uuid' => 'bar',
-        'dependencies' => [],
-        'langcode' => 'hu',
-        'status' => TRUE,
-      ])
+    $config_object->setData(['id' => 'foo', 'uuid' => 'bar', 'dependencies' => []])
       ->shouldBeCalled();
     $config_object->save(FALSE)->shouldBeCalled();
     $config_object->get()->willReturn([]);
@@ -309,14 +299,7 @@ public function testSaveUpdate(EntityInterface $entity) {
     $immutable_config_object->isNew()->willReturn(FALSE);
 
     $config_object = $this->prophesize(Config::class);
-    $config_object
-      ->setData([
-        'id' => 'foo',
-        'uuid' => 'bar',
-        'dependencies' => [],
-        'langcode' => 'hu',
-        'status' => TRUE,
-      ])
+    $config_object->setData(['id' => 'foo', 'uuid' => 'bar', 'dependencies' => []])
       ->shouldBeCalled();
     $config_object->save(FALSE)->shouldBeCalled();
     $config_object->get()->willReturn([]);
@@ -364,14 +347,7 @@ public function testSaveRename(ConfigEntityInterface $entity) {
     $immutable_config_object->isNew()->willReturn(FALSE);
 
     $config_object = $this->prophesize(Config::class);
-    $config_object
-      ->setData([
-        'id' => 'bar',
-        'uuid' => 'bar',
-        'dependencies' => [],
-        'langcode' => 'hu',
-        'status' => TRUE,
-      ])
+    $config_object->setData(['id' => 'bar', 'uuid' => 'bar', 'dependencies' => []])
       ->shouldBeCalled();
     $config_object->save(FALSE)
       ->shouldBeCalled();
@@ -467,14 +443,7 @@ public function testSaveNoMismatch() {
 
     $config_object = $this->prophesize(Config::class);
     $config_object->get()->willReturn([]);
-    $config_object
-      ->setData([
-        'id' => 'foo',
-        'uuid' => NULL,
-        'dependencies' => [],
-        'langcode' => 'en',
-        'status' => TRUE,
-      ])
+    $config_object->setData(['id' => 'foo', 'uuid' => NULL, 'dependencies' => []])
       ->shouldBeCalled();
     $config_object->save(FALSE)->shouldBeCalled();
 
diff --git a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php
index 9cfbcdd7b2..0c1ac1bd8f 100644
--- a/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php
+++ b/core/tests/Drupal/Tests/Core/Config/Entity/ConfigEntityTypeTest.php
@@ -193,9 +193,6 @@ public function providerGetPropertiesToExport() {
 
   /**
    * @covers ::getPropertiesToExport
-   *
-   * @group legacy
-   * @expectedDeprecation Entity type "example_config_entity_type" is using config schema as a fallback for a missing `config_export` definition is deprecated in Drupal 8.7.0 and will be removed before Drupal 9.0.0. See https://www.drupal.org/node/2949023.
    */
   public function testGetPropertiesToExportSchemaFallback() {
     $this->typedConfigManager->expects($this->once())
