From 012172e51ea1862cd24c8e9c453042589aaabbc0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ga=CC=81bor=20Hojtsy?= <gabor@hojtsy.hu>
Date: Mon, 11 Feb 2019 11:30:07 +0100
Subject: [PATCH] =?UTF-8?q?Issue=20#3020716=20by=20seanB,=20lauriii,=20phe?=
 =?UTF-8?q?naproxima,=20G=C3=A1bor=20Hojtsy,=20dww,=20andrewmacpherson,=20?=
 =?UTF-8?q?alexpott,=20larowlan,=20xjm,=20benjifisher:=20Add=20vertical=20?=
 =?UTF-8?q?tabs=20style=20menu=20to=20media=20library?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../install/views.view.media_library.yml      | 128 +++++++
 .../config/schema/media_library.schema.yml    |  10 +
 .../css/media_library.module.css              |  27 +-
 .../media_library/css/media_library.theme.css | 137 +++++--
 .../js/media_library.click_to_select.es6.js   |   5 +
 .../media_library/js/media_library.ui.es6.js  | 312 ++++++++++++++++
 .../media_library/js/media_library.ui.js      | 159 ++++++++
 .../js/media_library.view.es6.js              |  24 +-
 .../media_library/js/media_library.view.js    |   6 +-
 .../js/media_library.widget.es6.js            |  67 +---
 .../media_library/js/media_library.widget.js  |  28 --
 .../media_library/media_library.install       | 156 ++++++++
 .../media_library/media_library.libraries.yml |  10 +-
 .../media_library/media_library.module        |  66 +---
 .../media_library/media_library.routing.yml   |   6 +
 .../media_library/media_library.services.yml  |   4 +
 .../src/Form/MediaLibraryUploadForm.php       |  28 +-
 .../media_library/src/MediaLibraryState.php   | 196 ++++++++++
 .../src/MediaLibraryUiBuilder.php             | 244 +++++++++++++
 .../Field/FieldWidget/MediaLibraryWidget.php  | 211 ++++++++++-
 .../views/field/MediaLibrarySelectForm.php    |  51 ++-
 ...dia_library-update-widget-view-3020716.php | 111 ++++++
 ...y_form_display.node.basic_page.default.yml |   7 +
 ...y_view_display.node.basic_page.default.yml |   9 +
 ...ode.basic_page.field_single_media_type.yml |  28 ++
 ...d.storage.node.field_single_media_type.yml |  19 +
 .../MediaLibraryUpdateWidgetViewTest.php      |  50 +++
 .../FunctionalJavascript/MediaLibraryTest.php | 339 ++++++++++++++----
 .../src/Kernel/MediaLibraryAccessTest.php     |  53 +++
 .../src/Kernel/MediaLibraryStateTest.php      | 259 +++++++++++++
 30 files changed, 2443 insertions(+), 307 deletions(-)
 create mode 100644 core/modules/media_library/config/schema/media_library.schema.yml
 create mode 100644 core/modules/media_library/js/media_library.ui.es6.js
 create mode 100644 core/modules/media_library/js/media_library.ui.js
 create mode 100644 core/modules/media_library/media_library.services.yml
 create mode 100644 core/modules/media_library/src/MediaLibraryState.php
 create mode 100644 core/modules/media_library/src/MediaLibraryUiBuilder.php
 create mode 100644 core/modules/media_library/tests/fixtures/update/drupal-8.media_library-update-widget-view-3020716.php
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_single_media_type.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_single_media_type.yml
 create mode 100644 core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateWidgetViewTest.php
 create mode 100644 core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php

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 32096a6f9cc0..b33c865e980b 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
@@ -529,11 +529,139 @@ display:
       defaults:
         fields: false
         access: false
+        filters: false
+        filter_groups: false
+        arguments: false
       display_description: ''
       access:
         type: perm
         options:
           perm: 'view media'
+      filters:
+        status:
+          id: status
+          table: media_field_data
+          field: status
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: '='
+          value: '1'
+          group: 1
+          exposed: false
+          expose:
+            operator_id: ''
+            label: ''
+            description: ''
+            use_operator: false
+            operator: ''
+            identifier: ''
+            required: false
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+          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: status
+          plugin_id: boolean
+        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
+      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: -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 000000000000..544c18974b99
--- /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: 'Allowed media types, in display order'
+      sequence:
+        type: string
+        label: 'Media type ID'
diff --git a/core/modules/media_library/css/media_library.module.css b/core/modules/media_library/css/media_library.module.css
index 89b15800634a..694c5b33e9cf 100644
--- a/core/modules/media_library/css/media_library.module.css
+++ b/core/modules/media_library/css/media_library.module.css
@@ -2,6 +2,22 @@
 * @file media_library.module.css
 */
 
+.media-library-wrapper {
+  display: flex;
+}
+
+.media-library-menu {
+  margin: 0;
+  padding: 0;
+}
+
+/* @todo Use a class instead of the li element.
+     https://www.drupal.org/project/drupal/issues/3029227 */
+.media-library-menu li {
+  list-style: none;
+  padding: 0;
+}
+
 .media-library-views-form > .form-actions {
   flex-basis: 100%;
 }
@@ -33,10 +49,10 @@
 
 .media-library-item .js-click-to-select-checkbox {
   position: absolute;
-  display: block;
   z-index: 1;
   top: 5px;
   right: 0;
+  display: block;
 }
 
 .media-library-item__status {
@@ -69,6 +85,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 b1bb0ab074da..61be1a750795 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -5,6 +5,84 @@
  * @see https://www.drupal.org/project/drupal/issues/2980769
  */
 
+.media-library-wrapper {
+  margin: -1em;
+}
+
+/**
+ * @todo Reuse or build on vertical tabs styling for the media library menu.
+ *   https://www.drupal.org/project/drupal/issues/3023767
+ */
+.media-library-menu {
+  display: block;
+  width: 600px;
+  max-width: 20%;
+  margin: 0;
+  padding: 0;
+  border-bottom: 1px solid #ccc;
+  background-color: #e6e5e1;
+  line-height: 1;
+}
+[dir="rtl"] .media-library-menu {
+  margin: 0;
+}
+
+/* @todo Use a class instead of the li element.
+     https://www.drupal.org/project/drupal/issues/3029227 */
+.media-library-menu li {
+  display: block;
+}
+
+.media-library-menu__link {
+  position: relative;
+  display: block;
+  box-sizing: border-box;
+  padding: 10px 15px 15px;
+  text-decoration: none;
+  border-bottom: 1px solid #b3b2ad;
+  background-color: #f2f2f0;
+  text-shadow: 0 1px hsla(0, 0%, 100%, 0.6);
+}
+
+.media-library-menu__link:active,
+.media-library-menu__link:hover,
+.media-library-menu__link:focus {
+  background: #fcfcfa;
+  text-shadow: none;
+}
+
+.media-library-menu__link:focus,
+.media-library-menu__link:active {
+  outline: none;
+}
+
+.media-library-menu__link.active {
+  z-index: 1;
+  margin-right: -1px;
+  color: #000;
+  border-right: 1px solid #fcfcfa;
+  border-bottom: 1px solid #b3b2ad;
+  background-color: #fff;
+  box-shadow: 0 5px 5px -5px hsla(0, 0%, 0%, 0.3);
+}
+[dir="rtl"] .media-library-menu__link.active {
+  margin-right: 0;
+  margin-left: -1px;
+  border-right: 0;
+  border-left: 1px solid #fcfcfa;
+}
+
+.media-library-content {
+  width: 100%;
+  padding: 1em;
+  border-left: 1px solid #b3b2ad;
+  outline: none;
+}
+[dir="rtl"] .media-library-content {
+  border-right: 1px solid #b3b2ad;
+  border-left: 0;
+}
+
 .media-library-views-form__header .form-item {
   margin-right: 8px;
 }
@@ -15,12 +93,12 @@
 
 .media-library-item {
   justify-content: center;
+  width: 180px;
+  margin: 16px 16px 2px 2px;
+  transition: border-color 0.2s, color 0.2s, background 0.2s;
   vertical-align: top;
   border: 1px solid #dbdbdb;
-  margin: 16px 16px 2px 2px;
-  width: 180px;
   background: #fff;
-  transition: border-color 0.2s, color 0.2s, background 0.2s;
 }
 
 .media-library-view {
@@ -33,14 +111,14 @@
 
 .media-library-view .media-library-view--form-actions {
   clear: left;
-  margin: 0.75em 0;
   align-self: flex-end;
+  margin: 0.75em 0;
 }
 
 .media-library-item .field--name-thumbnail {
-  background-color: #ebebeb;
   overflow: hidden;
   text-align: center;
+  background-color: #ebebeb;
 }
 
 .media-library-item .field--name-thumbnail img {
@@ -52,10 +130,10 @@
 .media-library-item.is-hover,
 .media-library-item.checked,
 .media-library-item.is-focus {
-  border-color: #40b6ff;
+  margin: 14px 14px 0 0;
   border-width: 3px;
+  border-color: #40b6ff;
   border-radius: 3px;
-  margin: 14px 14px 0 0;
 }
 
 .media-library-item.checked {
@@ -76,11 +154,11 @@
 }
 
 .media-library-item__status {
+  padding: 5px 10px;
   color: #e4e4e4;
-  font-style: italic;
   background: #666;
-  padding: 5px 10px;
   font-size: 12px;
+  font-style: italic;
 }
 
 .media-library-item .views-field-operations {
@@ -88,20 +166,20 @@
 }
 
 .media-library-item .views-field-operations .dropbutton-wrapper {
-  display: inline-block;
   position: absolute;
   right: 5px;
   bottom: 5px;
+  display: inline-block;
 }
 
 .media-library-item__attributes {
   position: absolute;
   bottom: 0;
   display: block;
-  padding: 5px;
+  overflow: hidden;
   max-width: calc(100% - 10px);
   max-height: calc(100% - 50px);
-  overflow: hidden;
+  padding: 5px;
   background: white;
 }
 
@@ -111,10 +189,10 @@
 
 .media-library-item__name a {
   display: block;
-  text-decoration: underline;
+  overflow: hidden;
   margin: 2px;
   white-space: nowrap;
-  overflow: hidden;
+  text-decoration: underline;
   text-overflow: ellipsis;
 }
 
@@ -126,13 +204,13 @@
 }
 
 .media-library-item__name a:focus {
-  border: 2px solid;
   margin: 0;
+  border: 2px solid;
 }
 
 .media-library-item__type {
-  font-size: 12px;
   color: #696969;
+  font-size: 12px;
 }
 
 .media-library-select-all {
@@ -157,8 +235,8 @@
 
 .media-library-widget__toggle-weight {
   position: absolute;
-  right: 5px;
   top: 5px;
+  right: 5px;
 }
 
 .media-library-item .form-item {
@@ -181,13 +259,13 @@
   height: 24px;
   margin: 5px;
   padding: 0;
-  background: url("../../../misc/icons/787878/ex.svg") #fff center no-repeat;
-  background-size: 16px 16px;
+  transition: 0.2s border-color;
+  color: transparent;
   border: 2px solid #ccc;
   border-radius: 20px;
-  color: transparent;
+  background: url("../../../misc/icons/787878/ex.svg") #fff center no-repeat;
+  background-size: 16px 16px;
   text-shadow: none;
-  transition: 0.2s border-color;
 }
 
 .media-library-item .media-library-item__remove:hover,
@@ -202,7 +280,6 @@
 .media-library-upload__media,
 .media-library-upload__file {
   display: flex;
-  flex-wrap: wrap;
   padding: 20px 0 20px 0;
 }
 
@@ -224,12 +301,16 @@
 }
 
 .media-library-upload__media-preview {
-  margin-right: 20px;
-  width: 220px;
-  background: #ebebeb;
   display: flex;
-  align-items: center;
   justify-content: center;
+  align-items: center;
+  width: 220px;
+  margin-right: 20px;
+  background: #ebebeb;
+}
+[dir="rtl"] .media-library-upload__media-preview {
+  margin-right: 0;
+  margin-left: 20px;
 }
 
 .media-library-upload__media-preview img {
@@ -239,8 +320,8 @@
 /* @todo Remove or re-work in https://www.drupal.org/node/2985168 */
 .media-library-widget .media-library-item__name a,
 .media-library-view.view-display-id-widget .media-library-item__name a {
-  color: black;
   text-decoration: none;
+  color: black;
 }
 
 @media screen and (max-width: 600px) {
@@ -248,8 +329,8 @@
     width: 150px;
   }
   .media-library-item .field--name-thumbnail img {
-    height: 150px;
     width: 150px;
+    height: 150px;
   }
   .media-library-item .views-field-operations .dropbutton-wrapper {
     position: relative;
diff --git a/core/modules/media_library/js/media_library.click_to_select.es6.js b/core/modules/media_library/js/media_library.click_to_select.es6.js
index 90c58b665bb3..db42fe4790b9 100644
--- a/core/modules/media_library/js/media_library.click_to_select.es6.js
+++ b/core/modules/media_library/js/media_library.click_to_select.es6.js
@@ -5,6 +5,11 @@
 (($, Drupal) => {
   /**
    * Allows users to select an element which checks a hidden checkbox.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior for selecting media library item.
    */
   Drupal.behaviors.ClickToSelect = {
     attach(context) {
diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js
new file mode 100644
index 000000000000..d0aff3baf95c
--- /dev/null
+++ b/core/modules/media_library/js/media_library.ui.es6.js
@@ -0,0 +1,312 @@
+/**
+ * @file media_library.widget.js
+ */
+(($, Drupal, window) => {
+  /**
+   * Wrapper object for the current state of the media library.
+   */
+  Drupal.MediaLibrary = {
+    /**
+     * 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 tabs.
+     */
+    currentSelection: [],
+  };
+
+  /**
+   * Warn users when clicking outgoing links from the library or widget.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to links in the media library.
+   */
+  Drupal.behaviors.MediaLibraryWidgetWarn = {
+    attach(context) {
+      $('.js-media-library-item a[href]', context)
+        .once('media-library-warn-link')
+        .on('click', e => {
+          const message = Drupal.t(
+            'Unsaved changes to the form will be lost. Are you sure you want to leave?',
+          );
+          const confirmation = window.confirm(message);
+          if (!confirmation) {
+            e.preventDefault();
+          }
+        });
+    },
+  };
+
+  /**
+   * Load media library content through AJAX.
+   *
+   * Standard AJAX links (using the 'use-ajax' class) replace the entire library
+   * dialog. When navigating to a media type through the vertical tabs, we only
+   * want to load the changed library content. This is not only more efficient,
+   * but also provides a more accessible user experience for screen readers.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to vertical tabs in the media library.
+   *
+   * @todo Remove when the AJAX system adds support for replacing a specific
+   *   selector via a link.
+   *   https://www.drupal.org/project/drupal/issues/3026636
+   */
+  Drupal.behaviors.MediaLibraryTabs = {
+    attach(context) {
+      const $menu = $('.js-media-library-menu');
+      $menu
+        .find('a', context)
+        .once('media-library-menu-item')
+        .on('click', e => {
+          e.preventDefault();
+          e.stopPropagation();
+
+          // Replace the library content.
+          const ajaxObject = Drupal.ajax({
+            wrapper: 'media-library-content',
+            url: e.currentTarget.href,
+            dialogType: 'ajax',
+            progress: {
+              type: 'fullscreen',
+              message: Drupal.t('Please wait...'),
+            },
+          });
+
+          // Override the AJAX success callback to shift focus to the media
+          // library content.
+          ajaxObject.success = function(response, status) {
+            // Remove the progress element.
+            if (this.progress.element) {
+              $(this.progress.element).remove();
+            }
+            if (this.progress.object) {
+              this.progress.object.stopMonitoring();
+            }
+            $(this.element).prop('disabled', false);
+
+            // Execute the AJAX commands.
+            Object.keys(response || {}).forEach(i => {
+              if (response[i].command && this.commands[response[i].command]) {
+                this.commands[response[i].command](this, response[i], status);
+              }
+            });
+
+            // Set focus to the media library content.
+            document.getElementById('media-library-content').focus();
+
+            // Remove any response-specific settings so they don't get used on
+            // the next call by mistake.
+            this.settings = null;
+          };
+          ajaxObject.execute();
+
+          // Set the active tab.
+          $menu.find('.active-tab').remove();
+          $menu.find('a').removeClass('active');
+          $(e.currentTarget)
+            .addClass('active')
+            .html(
+              Drupal.t(
+                '@title<span class="active-tab visually-hidden"> (active tab)</span>',
+                { '@title': $(e.currentTarget).html() },
+              ),
+            );
+        });
+    },
+  };
+
+  /**
+   * Update the media library selection when loaded or media items are selected.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to select media items.
+   */
+  Drupal.behaviors.MediaLibraryItemSelection = {
+    attach(context, settings) {
+      const $form = $('.js-media-library-views-form', context);
+      const currentSelection = Drupal.MediaLibrary.currentSelection;
+
+      if (!$form.length) {
+        return;
+      }
+
+      const $mediaItems = $(
+        '.js-media-library-item input[type="checkbox"]',
+        $form,
+      );
+
+      // Update the selection array and the hidden form field when a media item
+      // is selected.
+      $mediaItems.once('media-item-change').on('change', e => {
+        const id = e.currentTarget.value;
+
+        // Update the selection.
+        const position = currentSelection.indexOf(id);
+        if (e.currentTarget.checked) {
+          // Check if the ID is not already in the selection and add if needed.
+          if (position === -1) {
+            currentSelection.push(id);
+          }
+        } else if (position !== -1) {
+          // Remove the ID when it is in the current selection.
+          currentSelection.splice(position, 1);
+        }
+
+        // Set the selection in the hidden form element.
+        $form
+          .find('#media-library-modal-selection')
+          .val(currentSelection.join())
+          .trigger('change');
+      });
+
+      /**
+       * Disable media items.
+       *
+       * @param {jQuery} $items
+       *   A jQuery object representing the media items that should be disabled.
+       */
+      function disableItems($items) {
+        $items
+          .prop('disabled', true)
+          .closest('.js-media-library-item')
+          .addClass('media-library-item--disabled');
+      }
+
+      /**
+       * Enable media items.
+       *
+       * @param {jQuery} $items
+       *   A jQuery object representing the media items that should be enabled.
+       */
+      function enableItems($items) {
+        $items
+          .prop('disabled', false)
+          .closest('.js-media-library-item')
+          .removeClass('media-library-item--disabled');
+      }
+
+      /**
+       * Update the number of selected items in the button pane.
+       *
+       * @param {number} remaining
+       *   The number of remaining slots.
+       */
+      function updateSelectionInfo(remaining) {
+        const $buttonPane = $(
+          '.media-library-widget-modal .ui-dialog-buttonpane',
+        );
+        if (!$buttonPane.length) {
+          return;
+        }
+
+        // Add the selection count.
+        const latestCount = Drupal.theme(
+          'mediaLibrarySelectionCount',
+          Drupal.MediaLibrary.currentSelection,
+          remaining,
+        );
+        const $existingCount = $buttonPane.find(
+          '.media-library-selected-count',
+        );
+        if ($existingCount.length) {
+          $existingCount.replaceWith(latestCount);
+        } else {
+          $buttonPane.append(latestCount);
+        }
+      }
+
+      // The hidden selection form field changes when the selection is updated.
+      $('#media-library-modal-selection', $form)
+        .once('media-library-selection-change')
+        .on('change', e => {
+          updateSelectionInfo(settings.media_library.selection_remaining);
+
+          // Prevent users from selecting more items than allowed.
+          if (
+            currentSelection.length ===
+            settings.media_library.selection_remaining
+          ) {
+            disableItems($mediaItems.not(':checked'));
+            enableItems($mediaItems.filter(':checked'));
+          } else {
+            enableItems($mediaItems);
+          }
+        });
+
+      // Apply the current selection to the media library view. Changing the
+      // checkbox values triggers the change event for the media items. The
+      // change event handles updating the hidden selection field for the form.
+      currentSelection.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', () => {
+          updateSelectionInfo(settings.media_library.selection_remaining);
+        });
+    },
+  };
+
+  /**
+   * Clear the current selection.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to clear the selection when the library modal closes.
+   */
+  Drupal.behaviors.MediaLibraryModalClearSelection = {
+    attach() {
+      $(window)
+        .once('media-library-clear-selection')
+        .on('dialog:afterclose', () => {
+          Drupal.MediaLibrary.currentSelection = [];
+        });
+    },
+  };
+
+  /**
+   * Theme function for the selection count.
+   *
+   * @param {Array.<number>} selection
+   *   An array containing the selected media item IDs.
+   * @param {number} remaining
+   *   The number of remaining slots.
+   *
+   * @return {string}
+   *   The corresponding HTML.
+   */
+  Drupal.theme.mediaLibrarySelectionCount = function(selection, remaining) {
+    // When the remaining number of items is -1, we allow an unlimited number of
+    // items. In that case we don't want to show the number of remaining slots.
+    let selectItemsText = Drupal.formatPlural(
+      remaining,
+      '@selected of @count item selected',
+      '@selected of @count items selected',
+      {
+        '@selected': selection.length,
+      },
+    );
+    if (remaining === -1) {
+      selectItemsText = Drupal.formatPlural(
+        selection.length,
+        '1 item selected',
+        '@count items selected',
+      );
+    }
+    return `<div class="media-library-selected-count" aria-live="polite">${selectItemsText}</div>`;
+  };
+})(jQuery, Drupal, window);
diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js
new file mode 100644
index 000000000000..163f989bdca8
--- /dev/null
+++ b/core/modules/media_library/js/media_library.ui.js
@@ -0,0 +1,159 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal, window) {
+  Drupal.MediaLibrary = {
+    currentSelection: []
+  };
+
+  Drupal.behaviors.MediaLibraryWidgetWarn = {
+    attach: function attach(context) {
+      $('.js-media-library-item a[href]', context).once('media-library-warn-link').on('click', function (e) {
+        var message = Drupal.t('Unsaved changes to the form will be lost. Are you sure you want to leave?');
+        var confirmation = window.confirm(message);
+        if (!confirmation) {
+          e.preventDefault();
+        }
+      });
+    }
+  };
+
+  Drupal.behaviors.MediaLibraryTabs = {
+    attach: function attach(context) {
+      var $menu = $('.js-media-library-menu');
+      $menu.find('a', context).once('media-library-menu-item').on('click', function (e) {
+        e.preventDefault();
+        e.stopPropagation();
+
+        var ajaxObject = Drupal.ajax({
+          wrapper: 'media-library-content',
+          url: e.currentTarget.href,
+          dialogType: 'ajax',
+          progress: {
+            type: 'fullscreen',
+            message: Drupal.t('Please wait...')
+          }
+        });
+
+        ajaxObject.success = function (response, status) {
+          var _this = this;
+
+          if (this.progress.element) {
+            $(this.progress.element).remove();
+          }
+          if (this.progress.object) {
+            this.progress.object.stopMonitoring();
+          }
+          $(this.element).prop('disabled', false);
+
+          Object.keys(response || {}).forEach(function (i) {
+            if (response[i].command && _this.commands[response[i].command]) {
+              _this.commands[response[i].command](_this, response[i], status);
+            }
+          });
+
+          document.getElementById('media-library-content').focus();
+
+          this.settings = null;
+        };
+        ajaxObject.execute();
+
+        $menu.find('.active-tab').remove();
+        $menu.find('a').removeClass('active');
+        $(e.currentTarget).addClass('active').html(Drupal.t('@title<span class="active-tab visually-hidden"> (active tab)</span>', { '@title': $(e.currentTarget).html() }));
+      });
+    }
+  };
+
+  Drupal.behaviors.MediaLibraryItemSelection = {
+    attach: function attach(context, settings) {
+      var $form = $('.js-media-library-views-form', context);
+      var currentSelection = Drupal.MediaLibrary.currentSelection;
+
+      if (!$form.length) {
+        return;
+      }
+
+      var $mediaItems = $('.js-media-library-item input[type="checkbox"]', $form);
+
+      $mediaItems.once('media-item-change').on('change', function (e) {
+        var id = e.currentTarget.value;
+
+        var position = currentSelection.indexOf(id);
+        if (e.currentTarget.checked) {
+          if (position === -1) {
+            currentSelection.push(id);
+          }
+        } else if (position !== -1) {
+          currentSelection.splice(position, 1);
+        }
+
+        $form.find('#media-library-modal-selection').val(currentSelection.join()).trigger('change');
+      });
+
+      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');
+      }
+
+      function updateSelectionInfo(remaining) {
+        var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane');
+        if (!$buttonPane.length) {
+          return;
+        }
+
+        var latestCount = Drupal.theme('mediaLibrarySelectionCount', Drupal.MediaLibrary.currentSelection, remaining);
+        var $existingCount = $buttonPane.find('.media-library-selected-count');
+        if ($existingCount.length) {
+          $existingCount.replaceWith(latestCount);
+        } else {
+          $buttonPane.append(latestCount);
+        }
+      }
+
+      $('#media-library-modal-selection', $form).once('media-library-selection-change').on('change', function (e) {
+        updateSelectionInfo(settings.media_library.selection_remaining);
+
+        if (currentSelection.length === settings.media_library.selection_remaining) {
+          disableItems($mediaItems.not(':checked'));
+          enableItems($mediaItems.filter(':checked'));
+        } else {
+          enableItems($mediaItems);
+        }
+      });
+
+      currentSelection.forEach(function (value) {
+        $form.find('input[type="checkbox"][value="' + value + '"]').prop('checked', true).trigger('change');
+      });
+
+      $(window).once('media-library-toggle-buttons').on('dialog:aftercreate', function () {
+        updateSelectionInfo(settings.media_library.selection_remaining);
+      });
+    }
+  };
+
+  Drupal.behaviors.MediaLibraryModalClearSelection = {
+    attach: function attach() {
+      $(window).once('media-library-clear-selection').on('dialog:afterclose', function () {
+        Drupal.MediaLibrary.currentSelection = [];
+      });
+    }
+  };
+
+  Drupal.theme.mediaLibrarySelectionCount = function (selection, remaining) {
+    var selectItemsText = Drupal.formatPlural(remaining, '@selected of @count item selected', '@selected of @count items selected', {
+      '@selected': selection.length
+    });
+    if (remaining === -1) {
+      selectItemsText = Drupal.formatPlural(selection.length, '1 item selected', '@count items selected');
+    }
+    return '<div class="media-library-selected-count" aria-live="polite">' + selectItemsText + '</div>';
+  };
+})(jQuery, Drupal, window);
\ No newline at end of file
diff --git a/core/modules/media_library/js/media_library.view.es6.js b/core/modules/media_library/js/media_library.view.es6.js
index dcda58d62ecf..8bc7fc1c131a 100644
--- a/core/modules/media_library/js/media_library.view.es6.js
+++ b/core/modules/media_library/js/media_library.view.es6.js
@@ -4,13 +4,15 @@
 (($, Drupal) => {
   /**
    * Adds hover effect to media items.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to add a class when hovering over media items.
    */
   Drupal.behaviors.MediaLibraryHover = {
     attach(context) {
-      $(
-        '.media-library-item .js-click-to-select-trigger,.media-library-item .js-click-to-select-checkbox',
-        context,
-      )
+      $('.js-click-to-select-trigger, .js-click-to-select-checkbox', context)
         .once('media-library-item-hover')
         .on('mouseover mouseout', ({ currentTarget, type }) => {
           $(currentTarget)
@@ -22,10 +24,15 @@
 
   /**
    * Adds focus effect to media items.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to add a focus effect to media items.
    */
   Drupal.behaviors.MediaLibraryFocus = {
     attach(context) {
-      $('.media-library-item .js-click-to-select-checkbox input', context)
+      $('.js-click-to-select-checkbox input', context)
         .once('media-library-item-focus')
         .on('focus blur', ({ currentTarget, type }) => {
           $(currentTarget)
@@ -37,10 +44,15 @@
 
   /**
    * Adds checkbox to select all items in the library.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to select all media items.
    */
   Drupal.behaviors.MediaLibrarySelectAll = {
     attach(context) {
-      const $view = $('.media-library-view', context).once(
+      const $view = $('.js-media-library-view', context).once(
         'media-library-select-all',
       );
       if ($view.length && $view.find('.media-library-item').length) {
diff --git a/core/modules/media_library/js/media_library.view.js b/core/modules/media_library/js/media_library.view.js
index 18facca93a05..73028f6af81d 100644
--- a/core/modules/media_library/js/media_library.view.js
+++ b/core/modules/media_library/js/media_library.view.js
@@ -8,7 +8,7 @@
 (function ($, Drupal) {
   Drupal.behaviors.MediaLibraryHover = {
     attach: function attach(context) {
-      $('.media-library-item .js-click-to-select-trigger,.media-library-item .js-click-to-select-checkbox', context).once('media-library-item-hover').on('mouseover mouseout', function (_ref) {
+      $('.js-click-to-select-trigger, .js-click-to-select-checkbox', context).once('media-library-item-hover').on('mouseover mouseout', function (_ref) {
         var currentTarget = _ref.currentTarget,
             type = _ref.type;
 
@@ -19,7 +19,7 @@
 
   Drupal.behaviors.MediaLibraryFocus = {
     attach: function attach(context) {
-      $('.media-library-item .js-click-to-select-checkbox input', context).once('media-library-item-focus').on('focus blur', function (_ref2) {
+      $('.js-click-to-select-checkbox input', context).once('media-library-item-focus').on('focus blur', function (_ref2) {
         var currentTarget = _ref2.currentTarget,
             type = _ref2.type;
 
@@ -30,7 +30,7 @@
 
   Drupal.behaviors.MediaLibrarySelectAll = {
     attach: function attach(context) {
-      var $view = $('.media-library-view', context).once('media-library-select-all');
+      var $view = $('.js-media-library-view', context).once('media-library-select-all');
       if ($view.length && $view.find('.media-library-item').length) {
         var $checkbox = $('<input type="checkbox" class="form-checkbox" />').on('click', function (_ref3) {
           var currentTarget = _ref3.currentTarget;
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 a784cb3884cb..93878a0f6a34 100644
--- a/core/modules/media_library/js/media_library.widget.es6.js
+++ b/core/modules/media_library/js/media_library.widget.es6.js
@@ -4,6 +4,11 @@
 (($, Drupal) => {
   /**
    * Allows users to re-order their selection with drag+drop.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to re-order selected media items.
    */
   Drupal.behaviors.MediaLibraryWidgetSortable = {
     attach(context) {
@@ -30,6 +35,11 @@
 
   /**
    * Allows selection order to be set without drag+drop for accessibility.
+   *
+   * @type {Drupal~behavior}
+   *
+   * @prop {Drupal~behaviorAttach} attach
+   *   Attaches behavior to toggle the weight field for media items.
    */
   Drupal.behaviors.MediaLibraryWidgetToggleWeight = {
     attach(context) {
@@ -60,61 +70,4 @@
         .hide();
     },
   };
-
-  /**
-   * Warn users when clicking outgoing links from the library or widget.
-   */
-  Drupal.behaviors.MediaLibraryWidgetWarn = {
-    attach(context) {
-      $('.js-media-library-item a[href]', context)
-        .once('media-library-warn-link')
-        .on('click', e => {
-          const message = Drupal.t(
-            'Unsaved changes to the form will be lost. Are you sure you want to leave?',
-          );
-          const confirmation = window.confirm(message);
-          if (!confirmation) {
-            e.preventDefault();
-          }
-        });
-    },
-  };
-
-  /**
-   * Prevent users from selecting more items than allowed in the view.
-   */
-  Drupal.behaviors.MediaLibraryWidgetRemaining = {
-    attach(context, settings) {
-      const $view = $('.js-media-library-view', context).once(
-        'media-library-remaining',
-      );
-      $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');
-            }
-          }
-        });
-    },
-  };
 })(jQuery, Drupal);
diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js
index ad6fbd76e5de..f2fcf82b3b01 100644
--- a/core/modules/media_library/js/media_library.widget.js
+++ b/core/modules/media_library/js/media_library.widget.js
@@ -36,32 +36,4 @@
       $('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide();
     }
   };
-
-  Drupal.behaviors.MediaLibraryWidgetWarn = {
-    attach: function attach(context) {
-      $('.js-media-library-item a[href]', context).once('media-library-warn-link').on('click', function (e) {
-        var message = Drupal.t('Unsaved changes to the form will be lost. Are you sure you want to leave?');
-        var confirmation = window.confirm(message);
-        if (!confirmation) {
-          e.preventDefault();
-        }
-      });
-    }
-  };
-
-  Drupal.behaviors.MediaLibraryWidgetRemaining = {
-    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');
-          }
-        }
-      });
-    }
-  };
 })(jQuery, Drupal);
\ No newline at end of file
diff --git a/core/modules/media_library/media_library.install b/core/modules/media_library/media_library.install
index 670d7a36c7c6..3123d8e954b7 100644
--- a/core/modules/media_library/media_library.install
+++ b/core/modules/media_library/media_library.install
@@ -76,3 +76,159 @@ function media_library_update_8701() {
   ]);
   $image_style->save();
 }
+
+/**
+ * Updates the media library view widget display (contextual) filters.
+ */
+function media_library_update_8702() {
+  $view = \Drupal::configFactory()->getEditable('views.view.media_library');
+  if ($view && $view->get('display.widget')) {
+    $view->set('display.widget.display_options.defaults.filters', FALSE);
+    $view->set('display.widget.display_options.defaults.filter_groups', FALSE);
+    $view->set('display.widget.display_options.defaults.arguments', FALSE);
+    $view->set('display.widget.display_options.filters', [
+      'status' => [
+        'id' => 'status',
+        'table' => 'media_field_data',
+        'field' => 'status',
+        'relationship' => 'none',
+        'group_type' => 'group',
+        'admin_label' => '',
+        'operator' => '=',
+        'value' => '1',
+        'group' => 1,
+        'exposed' => FALSE,
+        'expose' => [
+          'operator_id' => '',
+          'label' => '',
+          'description' => '',
+          'use_operator' => FALSE,
+          'operator' => '',
+          'identifier' => '',
+          'required' => FALSE,
+          'remember' => FALSE,
+          'multiple' => FALSE,
+          'remember_roles' => [
+            'authenticated' => 'authenticated',
+          ],
+        ],
+        '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' => 'status',
+        'plugin_id' => 'boolean',
+      ],
+      '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',
+      ],
+    ]);
+    $view->set('display.widget.display_options.filter_groups', [
+      'operator' => 'AND',
+      'groups' => [
+        1 => 'AND',
+      ],
+    ]);
+    $view->set('display.widget.display_options.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',
+      ],
+    ]);
+    $view->save();
+  }
+}
diff --git a/core/modules/media_library/media_library.libraries.yml b/core/modules/media_library/media_library.libraries.yml
index 848222543e4f..b7e0408d2f04 100644
--- a/core/modules/media_library/media_library.libraries.yml
+++ b/core/modules/media_library/media_library.libraries.yml
@@ -27,8 +27,14 @@ widget:
   js:
     js/media_library.widget.js: {}
   dependencies:
-    - core/drupal.ajax
     - core/jquery.ui.sortable
+    - core/jquery.once
+
+ui:
+  version: VERSION
+  js:
+    js/media_library.ui.js: {}
+  dependencies:
+    - core/drupal.ajax
     - media_library/view
-    - core/drupal.announce
     - core/jquery.once
diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module
index a0d12fa929e0..b5d5d1d3c761 100644
--- a/core/modules/media_library/media_library.module
+++ b/core/modules/media_library/media_library.module
@@ -22,10 +22,9 @@
 use Drupal\image\Plugin\Field\FieldType\ImageItem;
 use Drupal\media\MediaTypeForm;
 use Drupal\media\MediaTypeInterface;
+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;
 
 /**
@@ -90,11 +89,7 @@ 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',
-      ]));
+      $query = MediaLibraryState::fromRequest(\Drupal::request())->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
@@ -217,49 +212,6 @@ function _media_library_media_type_form_submit(array &$form, FormStateInterface
   }
 }
 
-/**
- * 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().
  */
@@ -303,20 +255,6 @@ function media_library_image_style_access(EntityInterface $entity, $operation, A
   }
 }
 
-/**
- * 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 [];
-}
-
 /**
  * Ensures that the given media type has a media_library form display.
  *
diff --git a/core/modules/media_library/media_library.routing.yml b/core/modules/media_library/media_library.routing.yml
index 1724760acb1e..8f0fb5f87e12 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.ui:
+  path: '/media-library'
+  defaults:
+    _controller: 'media_library.ui_builder:buildUi'
+  requirements:
+    _custom_access: 'media_library.ui_builder:checkAccess'
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 000000000000..9550f5190b98
--- /dev/null
+++ b/core/modules/media_library/media_library.services.yml
@@ -0,0 +1,4 @@
+services:
+  media_library.ui_builder:
+    class: Drupal\media_library\MediaLibraryUiBuilder
+    arguments: ['@entity_type.manager', '@request_stack', '@views.executable']
diff --git a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
index e897a2178ccb..66670ffe1bcd 100644
--- a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
+++ b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
@@ -18,6 +18,8 @@
 use Drupal\file\Plugin\Field\FieldType\FileItem;
 use Drupal\media\MediaInterface;
 use Drupal\media\MediaTypeInterface;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
@@ -312,26 +314,24 @@ public function selectType(array &$form, FormStateInterface $form_state) {
    *
    * @return \Drupal\Core\Ajax\AjaxResponse
    *   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.
    */
   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.');
-    }
+
     $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());
+    $opener_id = MediaLibraryState::fromRequest($this->getRequest())->getOpenerId();
+    if ($field_id = MediaLibraryWidget::getOpenerFieldId($opener_id)) {
+      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());
+    }
   }
 
   /**
@@ -484,9 +484,11 @@ protected function getTypes(array $allowed_types = NULL) {
     if (!isset($this->types)) {
       $media_type_storage = $this->entityTypeManager->getStorage('media_type');
       if (!$allowed_types) {
-        $allowed_types = _media_library_get_allowed_types() ?: NULL;
+        $types = $media_type_storage->loadMultiple(MediaLibraryState::fromRequest($this->getRequest())->getAllowedTypeIds());
+      }
+      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/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php
new file mode 100644
index 000000000000..ea740dba8003
--- /dev/null
+++ b/core/modules/media_library/src/MediaLibraryState.php
@@ -0,0 +1,196 @@
+<?php
+
+namespace Drupal\media_library;
+
+use Symfony\Component\HttpFoundation\ParameterBag;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A value object for the media library state.
+ *
+ * When the media library is opened it needs several parameters to work
+ * properly. The parameters are retrieved from the MediaLibraryState value
+ * object. Since the parameters are passed via the URL, the value object is
+ * extended from ParameterBag. This also allows an opener to add extra
+ * parameters if needed. The following parameters are needed to open the media
+ * library:
+ * - media_library_opener_id: The opener ID is used to describe the "thing" that
+ *   opened the media library. Most of the time this is going to be a form
+ *   field.
+ * - media_library_allowed_types: The media types available in the library can
+ *   be restricted to a list of allowed types. This should be an array of media
+ *   type IDs.
+ * - media_library_selected_type: The media library contains tabs to navigate
+ *   between the different media types. The selected type contains the ID of the
+ *   media type whose tab that should be opened.
+ * - media_library_remaining: When the opener wants to limit the amount of media
+ *   items that can be selected, it can pass the number of remaining slots. When
+ *   the number of remaining slots is a negative number, an unlimited amount of
+ *   items can be selected.
+ *
+ * @internal
+ *   This class is an internal part of the media library and should not be
+ *   instantiated or used by external code.
+ */
+class MediaLibraryState extends ParameterBag {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $parameters = []) {
+    $this->validateParameters($parameters['media_library_opener_id'], $parameters['media_library_allowed_types'], $parameters['media_library_selected_type'], $parameters['media_library_remaining']);
+    parent::__construct($parameters);
+  }
+
+  /**
+   * Creates a new MediaLibraryState object.
+   *
+   * @param string $opener_id
+   *   The opener ID.
+   * @param string[] $allowed_media_type_ids
+   *   The allowed media type IDs.
+   * @param string $selected_type_id
+   *   The selected media type ID.
+   * @param int $remaining_slots
+   *   The number of remaining items the user is allowed to select or add in the
+   *   library.
+   *
+   * @return \Drupal\media_library\MediaLibraryState
+   *   A state object.
+   */
+  public static function create($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots) {
+    return new static([
+      'media_library_opener_id' => $opener_id,
+      'media_library_allowed_types' => $allowed_media_type_ids,
+      'media_library_selected_type' => $selected_type_id,
+      'media_library_remaining' => $remaining_slots,
+    ]);
+  }
+
+  /**
+   * Get the media library state from a request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request.
+   *
+   * @return \Drupal\media_library\MediaLibraryState
+   *   A state object.
+   */
+  public static function fromRequest(Request $request) {
+    // Create a MediaLibraryState object through the create method to make sure
+    // all validation runs.
+    $state = static::create(
+      $request->query->get('media_library_opener_id'),
+      $request->query->get('media_library_allowed_types'),
+      $request->query->get('media_library_selected_type'),
+      $request->query->get('media_library_remaining')
+    );
+    // Once we have validated the required parameters, we restore the parameters
+    // from the request since there might be additional values.
+    $state->replace($request->query->all());
+    return $state;
+  }
+
+  /**
+   * Validate the required parameters for a new MediaLibraryState object.
+   *
+   * @param string $opener_id
+   *   The opener ID.
+   * @param string[] $allowed_media_type_ids
+   *   The allowed media type IDs.
+   * @param string $selected_type_id
+   *   The selected media type ID.
+   * @param int $remaining_slots
+   *   The number of remaining items the user is allowed to select or add in the
+   *   library.
+   *
+   * @throws \InvalidArgumentException
+   *   If one of the passed arguments is missing or does not pass the
+   *   validation.
+   */
+  protected function validateParameters($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots) {
+    // The opener ID must be a non-empty string.
+    if (!is_string($opener_id) || empty(trim($opener_id))) {
+      throw new \InvalidArgumentException('The opener ID parameter is required and must be a string.');
+    }
+
+    // The allowed media type IDs must be an array of non-empty strings.
+    if (empty($allowed_media_type_ids) || !is_array($allowed_media_type_ids)) {
+      throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
+    }
+    foreach ($allowed_media_type_ids as $allowed_media_type_id) {
+      if (!is_string($allowed_media_type_id) || empty(trim($allowed_media_type_id))) {
+        throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
+      }
+    }
+
+    // The selected type ID must be a non-empty string.
+    if (!is_string($selected_type_id) || empty(trim($selected_type_id))) {
+      throw new \InvalidArgumentException('The selected type parameter is required and must be a string.');
+    }
+    // The selected type ID must be present in the list of allowed types.
+    if (!in_array($selected_type_id, $allowed_media_type_ids, TRUE)) {
+      throw new \InvalidArgumentException('The selected type parameter must be present in the list of allowed types.');
+    }
+
+    // The remaining slots must be numeric.
+    if (!is_numeric($remaining_slots)) {
+      throw new \InvalidArgumentException('The remaining slots parameter is required and must be numeric.');
+    }
+  }
+
+  /**
+   * Returns the ID of the opener of the media library.
+   *
+   * @return string
+   *   The opener ID.
+   */
+  public function getOpenerId() {
+    return $this->get('media_library_opener_id');
+  }
+
+  /**
+   * Returns the media type IDs which can be selected.
+   *
+   * @return string[]
+   *   The media type IDs.
+   */
+  public function getAllowedTypeIds() {
+    return $this->get('media_library_allowed_types');
+  }
+
+  /**
+   * Returns the selected media type.
+   *
+   * @return string
+   *   The selected media type.
+   */
+  public function getSelectedTypeId() {
+    return $this->get('media_library_selected_type');
+  }
+
+  /**
+   * 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.
+   *
+   * When the value is not available in the URL the default is 0. When a
+   * negative integer is passed, an unlimited amount of media items can be
+   * selected.
+   *
+   * @return int
+   *   The number of additional media items that can be selected.
+   */
+  public function getAvailableSlots() {
+    return $this->getInt('media_library_remaining');
+  }
+
+}
diff --git a/core/modules/media_library/src/MediaLibraryUiBuilder.php b/core/modules/media_library/src/MediaLibraryUiBuilder.php
new file mode 100644
index 000000000000..c97cc33764ad
--- /dev/null
+++ b/core/modules/media_library/src/MediaLibraryUiBuilder.php
@@ -0,0 +1,244 @@
+<?php
+
+namespace Drupal\media_library;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+use Drupal\views\ViewExecutableFactory;
+use Symfony\Component\HttpFoundation\RequestStack;
+
+/**
+ * Service which builds the media library.
+ *
+ * @internal
+ *   This class is an internal part of the media library and should not be
+ *   instantiated or used by external code.
+ */
+class MediaLibraryUiBuilder {
+
+  use StringTranslationTrait;
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The currently active request object.
+   *
+   * @var \Symfony\Component\HttpFoundation\Request
+   */
+  protected $request;
+
+  /**
+   * The views executable factory.
+   *
+   * @var \Drupal\views\ViewExecutableFactory
+   */
+  protected $viewsExecutableFactory;
+
+  /**
+   * Constructs a MediaLibraryUiBuilder instance.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The request stack.
+   * @param \Drupal\views\ViewExecutableFactory $views_executable_factory
+   *   The views executable factory.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->request = $request_stack->getCurrentRequest();
+    $this->viewsExecutableFactory = $views_executable_factory;
+  }
+
+  /**
+   * 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 UI.
+   *
+   * @return array
+   *   The render array for the media library.
+   */
+  public function buildUi() {
+    $state = MediaLibraryState::fromRequest($this->request);
+    // When navigating to a media type through the vertical tabs, we only want
+    // to load the changed library content. This is not only more efficient, but
+    // also provides a more accessible user experience for screen readers.
+    if ($state->get('media_library_content') === '1') {
+      return $this->buildLibraryContent($state);
+    }
+    else {
+      return [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#attributes' => [
+          'id' => 'media-library-wrapper',
+          'class' => ['media-library-wrapper'],
+        ],
+        'menu' => $this->buildMediaTypeMenu($state),
+        'content' => $this->buildLibraryContent($state),
+        '#attached' => [
+          'library' => ['media_library/ui'],
+        ],
+      ];
+    }
+  }
+
+  /**
+   * Build the media library content area.
+   *
+   * @param \Drupal\media_library\MediaLibraryState $state
+   *   The current state of the media library, derived from the current request.
+   *
+   * @return array
+   *   The render array for the media library.
+   */
+  protected function buildLibraryContent(MediaLibraryState $state) {
+    return [
+      '#type' => 'html_tag',
+      '#tag' => 'div',
+      '#attributes' => [
+        'id' => 'media-library-content',
+        'class' => ['media-library-content'],
+        'tabindex' => -1,
+      ],
+      'view' => $this->buildMediaLibraryView($state),
+    ];
+  }
+
+  /**
+   * 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 checkAccess(AccountInterface $account = NULL) {
+    // Deny access if the view or display are removed.
+    $view = $this->entityTypeManager->getStorage('view')->load('media_library');
+    if (!$view) {
+      return AccessResult::forbidden('The media library view does not exist.')
+        ->setCacheMaxAge(0);
+    }
+    if (!$view->getDisplay('widget')) {
+      return AccessResult::forbidden('The media library widget display does not exist.')
+        ->addCacheableDependency($view);
+    }
+    return AccessResult::allowedIfHasPermission($account, 'view media')
+      ->addCacheableDependency($view);
+  }
+
+  /**
+   * Get the media type menu for the media library.
+   *
+   * @param \Drupal\media_library\MediaLibraryState $state
+   *   The current state of the media library, derived from the current request.
+   *
+   * @return array
+   *   The render array for the media type menu.
+   */
+  protected function buildMediaTypeMenu(MediaLibraryState $state) {
+    // Add the menu for each type if we have more than 1 media type enabled for
+    // the field.
+    $allowed_type_ids = $state->getAllowedTypeIds();
+    if (count($allowed_type_ids) === 1) {
+      return [];
+    }
+
+    // @todo: Add a class to the li element.
+    //   https://www.drupal.org/project/drupal/issues/3029227
+    $menu = [
+      '#theme' => 'links',
+      '#links' => [],
+      '#attributes' => [
+        'class' => ['media-library-menu', 'js-media-library-menu'],
+      ],
+    ];
+
+    // Get the state parameters but remove the wrapper format. Also add the
+    // 'media_library_content' argument to fetch only the updated content for
+    // the tab.
+    // @see self::buildUi()
+    $state->remove(MainContentViewSubscriber::WRAPPER_FORMAT);
+    $state->add(['media_library_content' => 1]);
+    $query = $state->all();
+
+    $allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids);
+
+    $selected_type_id = $state->getSelectedTypeId();
+    foreach ($allowed_types as $allowed_type_id => $allowed_type) {
+      $query['media_library_selected_type'] = $allowed_type_id;
+
+      $title = $allowed_type->label();
+      if ($allowed_type_id === $selected_type_id) {
+        $title = [
+          '#markup' => $this->t('@title<span class="active-tab visually-hidden"> (active tab)</span>', ['@title' => $title]),
+        ];
+      }
+
+      $menu['#links']['media-library-menu-' . $allowed_type_id] = [
+        'title' => $title,
+        'url' => Url::fromRoute('media_library.ui', [], [
+          'query' => $query,
+        ]),
+        'attributes' => [
+          'class' => ['media-library-menu__link'],
+        ],
+      ];
+    }
+
+    // Set the active menu item.
+    $menu['#links']['media-library-menu-' . $selected_type_id]['attributes']['class'][] = 'active';
+
+    return $menu;
+  }
+
+  /**
+   * Get the media library view.
+   *
+   * @param \Drupal\media_library\MediaLibraryState $state
+   *   The current state of the media library, derived from the current request.
+   *
+   * @return array
+   *   The render array for the media library view.
+   */
+  protected function buildMediaLibraryView(MediaLibraryState $state) {
+    // @todo Make the view configurable in
+    //   https://www.drupal.org/project/drupal/issues/2971209
+    $view = $this->entityTypeManager->getStorage('view')->load('media_library');
+    $view_executable = $this->viewsExecutableFactory->get($view);
+    $display_id = 'widget';
+
+    $args = [$state->getSelectedTypeId()];
+
+    $view_executable->setDisplay($display_id);
+    $view_executable->preExecute($args);
+    $view_executable->execute($display_id);
+
+    return $view_executable->buildRenderable($display_id, $args, FALSE);
+  }
+
+}
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 ec4e1c501eb6..033c7da139ff 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -14,6 +14,8 @@
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Url;
 use Drupal\media\Entity\Media;
+use Drupal\media_library\MediaLibraryUiBuilder;
+use Drupal\media_library\MediaLibraryState;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\Validator\ConstraintViolationInterface;
 
@@ -41,6 +43,13 @@ class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInt
    */
   protected $entityTypeManager;
 
+  /**
+   * The prefix to use with a field ID for media library opener IDs.
+   *
+   * @var string
+   */
+  protected static $openerIdPrefix = 'field:';
+
   /**
    * Constructs a MediaLibraryWidget widget.
    *
@@ -83,6 +92,155 @@ 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 type IDs sorted by weight.
+   *
+   * @return string[]
+   *   The media type IDs sorted by weight.
+   */
+  protected function getAllowedMediaTypeIdsSorted() {
+    // Get the media type IDs sorted by the user in the settings form.
+    $sorted_media_type_ids = $this->getSetting('media_types');
+
+    // Get the configured media types from the field storage.
+    $handler_settings = $this->getFieldSetting('handler_settings');
+    $allowed_media_type_ids = !empty($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : [];
+
+    // When no target bundles are configured for the field, all are allowed.
+    if (!$allowed_media_type_ids) {
+      $allowed_media_type_ids = $this->entityTypeManager->getStorage('media_type')->getQuery()->execute();
+    }
+
+    // When the user did not sort the media types, return the media type IDs
+    // configured for the field.
+    if (empty($sorted_media_type_ids)) {
+      return $allowed_media_type_ids;
+    }
+
+    // Some of the media types may no longer exist, and new media types may have
+    // been added that we don't yet know about. We need to make sure new media
+    // types are added to the list and remove media types that are no longer
+    // configured for the field.
+    $new_media_type_ids = array_diff($allowed_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, array_values($new_media_type_ids));
+    // Remove media types that are no longer available.
+    $sorted_media_type_ids = array_intersect($sorted_media_type_ids, $allowed_media_type_ids);
+
+    // Make sure the keys are numeric.
+    return array_values($sorted_media_type_ids);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $media_type_ids = $this->getAllowedMediaTypeIdsSorted();
+
+    if (count($media_type_ids) <= 1) {
+      return $form;
+    }
+
+    $form['media_types'] = [
+      '#type' => 'table',
+      '#header' => [
+        $this->t('Tab order'),
+        $this->t('Weight'),
+      ],
+      '#tabledrag' => [
+        [
+          'action' => 'order',
+          'relationship' => 'sibling',
+          'group' => 'weight',
+        ],
+      ],
+      '#value_callback' => [static::class, 'setMediaTypesValue'],
+    ];
+
+    $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($media_type_ids);
+    $weight = 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' => $weight,
+          '#attributes' => ['class' => ['weight']],
+        ],
+        '#weight' => $weight,
+        '#attributes' => ['class' => ['draggable']],
+      ];
+      $weight++;
+    }
+
+    return $form;
+  }
+
+  /**
+   * Value callback to optimize the way the media type weights are stored.
+   *
+   * The tabledrag functionality needs a specific weight field, but we don't
+   * want to store this extra weight field in our settings.
+   *
+   * @param array $element
+   *   An associative array containing the properties of the element.
+   * @param mixed $input
+   *   The incoming input to populate the form element. If this is FALSE,
+   *   the element's default value should be returned.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return mixed
+   *   The value to assign to the element.
+   */
+  public static function setMediaTypesValue(array &$element, $input, FormStateInterface $form_state) {
+    if ($input === FALSE) {
+      return isset($element['#default_value']) ? $element['#default_value'] : [];
+    }
+
+    // Sort the media types by weight value and set the value in the form state.
+    uasort($input, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
+    $sorted_media_type_ids = array_keys($input);
+    $form_state->setValue($element['#parents'], $sorted_media_type_ids);
+
+    // We have to unset the child elements containing the weight fields for each
+    // media type to stop FormBuilder::doBuildForm() from processing the weight
+    // fields as well.
+    foreach ($sorted_media_type_ids as $media_type_id) {
+      unset($element[$media_type_id]);
+    }
+
+    return $sorted_media_type_ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsSummary() {
+    $summary = [];
+    $media_type_labels = [];
+    $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($this->getAllowedMediaTypeIdsSorted());
+    if (count($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}
    */
@@ -106,7 +264,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])];
 
@@ -218,26 +376,24 @@ 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.
+    $allowed_media_type_ids = $this->getAllowedMediaTypeIdsSorted();
+    $selected_type_id = reset($allowed_media_type_ids);
+    $remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;
+    // The opener ID is used by the select form and the upload form to add the
+    // selected/uploaded media items to the widget.
+    $opener_id = static::$openerIdPrefix . $field_name . $id_suffix;
+
+    $state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_type_id, $remaining);
+    $dialog_options = Json::encode(MediaLibraryUiBuilder::dialogOptions());
 
     // Add a button that will load the Media library in a modal using AJAX.
     $element['media_library_open_button'] = [
       '#type' => 'link',
       '#title' => $this->t('Add 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 = Url::fromRoute('media_library.ui', [], [
+        'query' => $state->all(),
       ]),
       '#attributes' => [
         'class' => ['button', 'use-ajax', 'media-library-open-button'],
@@ -419,13 +575,15 @@ public static function updateItems(array $form, FormStateInterface $form_state)
 
     $media = static::getNewMediaItems($element, $form_state);
     if (!empty($media)) {
-      $weight = count($field_state['items']);
+      // Get the weight of the last items and count from there.
+      $last_element = end($field_state['items']);
+      $weight = $last_element ? $last_element['weight'] : 0;
       foreach ($media as $media_item) {
         // Any ID can be passed to the widget, so we have to check access.
         if ($media_item->access('view')) {
           $field_state['items'][] = [
             'target_id' => $media_item->id(),
-            'weight' => $weight++,
+            'weight' => ++$weight,
           ];
         }
       }
@@ -505,4 +663,23 @@ protected static function setFieldState(array $element, FormStateInterface $form
     static::setWidgetState($element['#field_parents'], $element['#field_name'], $form_state, $field_state);
   }
 
+  /**
+   * Get the field ID of the widget from an opener ID.
+   *
+   * @param string $opener_id
+   *   The opener ID of the media library.
+   *
+   * @return string|null
+   *   The field ID or NULL if the opener ID is not valid for the widget.
+   *
+   * @see \Drupal\media_library\MediaLibraryState
+   */
+  public static function getOpenerFieldId($opener_id) {
+    // Media library widget opener IDs are always prefixed with 'field:' in .
+    if (preg_match('/^' . static::$openerIdPrefix . '([a-z0-9_-]+)$/', $opener_id, $matches)) {
+      return $matches[1];
+    }
+    return NULL;
+  }
+
 }
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 72a4b50bbfcb..d1d6b4cad634 100644
--- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -8,10 +8,11 @@
 use Drupal\Core\Form\FormBuilderInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget;
 use Drupal\views\Plugin\views\field\FieldPluginBase;
 use Drupal\views\Render\ViewsRenderPipelineMarkup;
 use Drupal\views\ResultRow;
-use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 
 /**
  * Defines a field that outputs a checkbox and form for selecting media.
@@ -45,10 +46,9 @@ 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;
-    }
+    $form['#attributes'] = [
+      'class' => ['media-library-views-form', 'js-media-library-views-form'],
+    ];
 
     // Render checkboxes for all rows.
     $form[$this->options['id']]['#tree'] = TRUE;
@@ -64,6 +64,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 +91,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 +109,22 @@ 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());
+
+    $ids = implode(',', $selected);
+
+    $opener_id = MediaLibraryState::fromRequest(\Drupal::request())->getOpenerId();
+    if ($field_id = MediaLibraryWidget::getOpenerFieldId($opener_id)) {
+      $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/fixtures/update/drupal-8.media_library-update-widget-view-3020716.php b/core/modules/media_library/tests/fixtures/update/drupal-8.media_library-update-widget-view-3020716.php
new file mode 100644
index 000000000000..a07a69003ec5
--- /dev/null
+++ b/core/modules/media_library/tests/fixtures/update/drupal-8.media_library-update-widget-view-3020716.php
@@ -0,0 +1,111 @@
+<?php
+// @codingStandardsIgnoreFile
+/**
+ * @file
+ * Contains database additions to drupal-8.bare.standard.php.gz for testing
+ * the upgrade paths of the media library module widget view.
+ *
+ * @see https://www.drupal.org/project/drupal/issues/3020716
+ */
+
+use Drupal\Core\Database\Database;
+
+$connection = Database::getConnection();
+
+// Set the schema version.
+$connection->merge('key_value')
+  ->fields([
+    'value' => 'i:8000;',
+    'name' => 'media_library',
+    'collection' => 'system.schema',
+  ])
+  ->condition('collection', 'system.schema')
+  ->condition('name', 'media_library')
+  ->execute();
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['media_library'] = 0;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+    'collection' => '',
+    'name' => 'core.extension',
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
+// Insert media library config objects.
+$connection->insert('config')
+->fields(array(
+  'collection',
+  'name',
+  'data',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'core.entity_form_display.media.file.media_library',
+  'data' => 'a:11:{s:4:"uuid";s:36:"86ab9619-c970-4416-971d-e5c8614b3368";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:1:{s:6:"config";a:3:{i:0;s:41:"core.entity_form_mode.media.media_library";i:1;s:39:"field.field.media.file.field_media_file";i:2;s:15:"media.type.file";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"RFmywOcuem167havmD4VLgBTO1Swq9hyA-_f5aYTi8c";}s:2:"id";s:24:"media.file.media_library";s:16:"targetEntityType";s:5:"media";s:6:"bundle";s:4:"file";s:4:"mode";s:13:"media_library";s:7:"content";a:1:{s:4:"name";a:5:{s:4:"type";s:16:"string_textfield";s:6:"weight";i:0;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:5:{s:7:"created";b:1;s:16:"field_media_file";b:1;s:4:"path";b:1;s:6:"status";b:1;s:3:"uid";b:1;}}',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'core.entity_form_display.media.image.media_library',
+  'data' => 'a:11:{s:4:"uuid";s:36:"2bbea060-3cd8-4881-a3aa-c898d6619b16";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:4:{i:0;s:41:"core.entity_form_mode.media.media_library";i:1;s:41:"field.field.media.image.field_media_image";i:2;s:21:"image.style.thumbnail";i:3;s:16:"media.type.image";}s:6:"module";a:1:{i:0;s:5:"image";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"PlyfyVZfALLkP7nbxLpaVKIDUWRioZghWpFDv0_rJ68";}s:2:"id";s:25:"media.image.media_library";s:16:"targetEntityType";s:5:"media";s:6:"bundle";s:5:"image";s:4:"mode";s:13:"media_library";s:7:"content";a:2:{s:17:"field_media_image";a:5:{s:4:"type";s:11:"image_image";s:6:"weight";i:1;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:18:"progress_indicator";s:8:"throbber";s:19:"preview_image_style";s:9:"thumbnail";}s:20:"third_party_settings";a:0:{}}s:4:"name";a:5:{s:4:"type";s:16:"string_textfield";s:6:"weight";i:0;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:4:{s:7:"created";b:1;s:4:"path";b:1;s:6:"status";b:1;s:3:"uid";b:1;}}',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'core.entity_view_display.media.file.media_library',
+  'data' => 'a:11:{s:4:"uuid";s:36:"67e6d857-8ecb-49f5-95e1-6b1c4306c31f";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:4:{i:0;s:41:"core.entity_view_mode.media.media_library";i:1;s:39:"field.field.media.file.field_media_file";i:2;s:21:"image.style.thumbnail";i:3;s:15:"media.type.file";}s:6:"module";a:1:{i:0;s:5:"image";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"vhAK2lCOWK2paUpJawj7yiSLFO9wwsx6WE8_oDmvbwU";}s:2:"id";s:24:"media.file.media_library";s:16:"targetEntityType";s:5:"media";s:6:"bundle";s:4:"file";s:4:"mode";s:13:"media_library";s:7:"content";a:1:{s:9:"thumbnail";a:6:{s:4:"type";s:5:"image";s:6:"weight";i:0;s:6:"region";s:7:"content";s:5:"label";s:6:"hidden";s:8:"settings";a:2:{s:11:"image_style";s:9:"thumbnail";s:10:"image_link";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:4:{s:7:"created";b:1;s:16:"field_media_file";b:1;s:4:"name";b:1;s:3:"uid";b:1;}}',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'core.entity_view_display.media.image.media_library',
+  'data' => 'a:11:{s:4:"uuid";s:36:"277ca98b-2ada-4251-ad69-aa73e72d60fe";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:4:{i:0;s:41:"core.entity_view_mode.media.media_library";i:1;s:41:"field.field.media.image.field_media_image";i:2;s:18:"image.style.medium";i:3;s:16:"media.type.image";}s:6:"module";a:1:{i:0;s:5:"image";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"PaGXvzRcL9eII--JV4eCVfObjrNo0l-u1dB_WJtB9ig";}s:2:"id";s:25:"media.image.media_library";s:16:"targetEntityType";s:5:"media";s:6:"bundle";s:5:"image";s:4:"mode";s:13:"media_library";s:7:"content";a:1:{s:9:"thumbnail";a:6:{s:4:"type";s:5:"image";s:6:"weight";i:0;s:6:"region";s:7:"content";s:5:"label";s:6:"hidden";s:8:"settings";a:2:{s:11:"image_style";s:6:"medium";s:10:"image_link";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:4:{s:7:"created";b:1;s:17:"field_media_image";b:1;s:4:"name";b:1;s:3:"uid";b:1;}}',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'core.entity_view_mode.media.media_library',
+  'data' => 'a:9:{s:4:"uuid";s:36:"20b2f1f7-a864-4d41-a15f-32f66789f73d";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:8:"enforced";a:1:{s:6:"module";a:1:{i:0;s:13:"media_library";}}s:6:"module";a:1:{i:0;s:5:"media";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"pkq0uj-IoqEQRBOP_ddUDV0ZJ-dKQ_fLcppsEDF2UO8";}s:2:"id";s:19:"media.media_library";s:5:"label";s:13:"Media library";s:16:"targetEntityType";s:5:"media";s:5:"cache";b:1;}',
+))
+->values(array(
+  'collection' => '',
+  'name' => 'views.view.media_library',
+  'data' => 'a:14:{s:4:"uuid";s:36:"3bc9cf0f-cb66-4dbe-8d7e-862cb85e5932";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:3:{s:6:"config";a:1:{i:0;s:41:"core.entity_view_mode.media.media_library";}s:8:"enforced";a:1:{s:6:"module";a:1:{i:0;s:13:"media_library";}}s:6:"module";a:3:{i:0;s:5:"media";i:1;s:13:"media_library";i:2;s:4:"user";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"1F1cSZ5MlvxdwjdyrwnH2I8CWngOp8Pu2SXDzix2QUc";}s:2:"id";s:13:"media_library";s:5:"label";s:13:"Media library";s:6:"module";s:5:"views";s:11:"description";s:0:"";s:3:"tag";s:0:"";s:10:"base_table";s:16:"media_field_data";s:10:"base_field";s:3:"mid";s:4:"core";s:3:"8.x";s:7:"display";a:3:{s:7:"default";a:6:{s:14:"display_plugin";s:7:"default";s:2:"id";s:7:"default";s:13:"display_title";s:6:"Master";s:8:"position";i:0;s:15:"display_options";a:18:{s:6:"access";a:2:{s:4:"type";s:4:"perm";s:7:"options";a:1:{s:4:"perm";s:21:"access media overview";}}s:5:"cache";a:2:{s:4:"type";s:3:"tag";s:7:"options";a:0:{}}s:5:"query";a:2:{s:4:"type";s:11:"views_query";s:7:"options";a:5:{s:19:"disable_sql_rewrite";b:0;s:8:"distinct";b:0;s:7:"replica";b:0;s:13:"query_comment";s:0:"";s:10:"query_tags";a:0:{}}}s:12:"exposed_form";a:2:{s:4:"type";s:5:"basic";s:7:"options";a:7:{s:13:"submit_button";s:13:"Apply Filters";s:12:"reset_button";b:0;s:18:"reset_button_label";s:5:"Reset";s:19:"exposed_sorts_label";s:7:"Sort by";s:17:"expose_sort_order";b:0;s:14:"sort_asc_label";s:3:"Asc";s:15:"sort_desc_label";s:4:"Desc";}}s:5:"pager";a:2:{s:4:"type";s:4:"mini";s:7:"options";a:6:{s:14:"items_per_page";i:25;s:6:"offset";i:0;s:2:"id";i:0;s:11:"total_pages";N;s:6:"expose";a:7:{s:14:"items_per_page";b:0;s:20:"items_per_page_label";s:14:"Items per page";s:22:"items_per_page_options";s:13:"5, 10, 25, 50";s:26:"items_per_page_options_all";b:0;s:32:"items_per_page_options_all_label";s:7:"- All -";s:6:"offset";b:0;s:12:"offset_label";s:6:"Offset";}s:4:"tags";a:2:{s:8:"previous";s:6:"‹‹";s:4:"next";s:6:"››";}}}s:5:"style";a:2:{s:4:"type";s:7:"default";s:7:"options";a:3:{s:8:"grouping";a:0:{}s:9:"row_class";s:59:"media-library-item js-media-library-item js-click-to-select";s:17:"default_row_class";b:1;}}s:3:"row";a:2:{s:4:"type";s:6:"fields";s:7:"options";a:4:{s:22:"default_field_elements";b:1;s:6:"inline";a:0:{}s:9:"separator";s:0:"";s:10:"hide_empty";b:0;}}s:6:"fields";a:2:{s:15:"media_bulk_form";a:26:{s:2:"id";s:15:"media_bulk_form";s:5:"table";s:5:"media";s:5:"field";s:15:"media_bulk_form";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"label";s:0:"";s:7:"exclude";b:0;s:5:"alter";a:26:{s:10:"alter_text";b:0;s:4:"text";s:0:"";s:9:"make_link";b:0;s:4:"path";s:0:"";s:8:"absolute";b:0;s:8:"external";b:0;s:14:"replace_spaces";b:0;s:9:"path_case";s:4:"none";s:15:"trim_whitespace";b:0;s:3:"alt";s:0:"";s:3:"rel";s:0:"";s:10:"link_class";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:6:"target";s:0:"";s:5:"nl2br";b:0;s:10:"max_length";i:0;s:13:"word_boundary";b:1;s:8:"ellipsis";b:1;s:9:"more_link";b:0;s:14:"more_link_text";s:0:"";s:14:"more_link_path";s:0:"";s:10:"strip_tags";b:0;s:4:"trim";b:0;s:13:"preserve_tags";s:0:"";s:4:"html";b:0;}s:12:"element_type";s:0:"";s:13:"element_class";s:27:"js-click-to-select-checkbox";s:18:"element_label_type";s:0:"";s:19:"element_label_class";s:0:"";s:19:"element_label_colon";b:0;s:20:"element_wrapper_type";s:0:"";s:21:"element_wrapper_class";s:0:"";s:23:"element_default_classes";b:1;s:5:"empty";s:0:"";s:10:"hide_empty";b:0;s:10:"empty_zero";b:0;s:16:"hide_alter_empty";b:1;s:12:"action_title";s:6:"Action";s:15:"include_exclude";s:7:"exclude";s:16:"selected_actions";a:0:{}s:11:"entity_type";s:5:"media";s:9:"plugin_id";s:9:"bulk_form";}s:15:"rendered_entity";a:24:{s:2:"id";s:15:"rendered_entity";s:5:"table";s:5:"media";s:5:"field";s:15:"rendered_entity";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"label";s:0:"";s:7:"exclude";b:0;s:5:"alter";a:26:{s:10:"alter_text";b:0;s:4:"text";s:0:"";s:9:"make_link";b:0;s:4:"path";s:0:"";s:8:"absolute";b:0;s:8:"external";b:0;s:14:"replace_spaces";b:0;s:9:"path_case";s:4:"none";s:15:"trim_whitespace";b:0;s:3:"alt";s:0:"";s:3:"rel";s:0:"";s:10:"link_class";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:6:"target";s:0:"";s:5:"nl2br";b:0;s:10:"max_length";i:0;s:13:"word_boundary";b:1;s:8:"ellipsis";b:1;s:9:"more_link";b:0;s:14:"more_link_text";s:0:"";s:14:"more_link_path";s:0:"";s:10:"strip_tags";b:0;s:4:"trim";b:0;s:13:"preserve_tags";s:0:"";s:4:"html";b:0;}s:12:"element_type";s:0:"";s:13:"element_class";s:27:"media-library-item__content";s:18:"element_label_type";s:0:"";s:19:"element_label_class";s:0:"";s:19:"element_label_colon";b:0;s:20:"element_wrapper_type";s:0:"";s:21:"element_wrapper_class";s:0:"";s:23:"element_default_classes";b:1;s:5:"empty";s:0:"";s:10:"hide_empty";b:0;s:10:"empty_zero";b:0;s:16:"hide_alter_empty";b:1;s:9:"view_mode";s:13:"media_library";s:11:"entity_type";s:5:"media";s:9:"plugin_id";s:15:"rendered_entity";}}s:7:"filters";a:3:{s:6:"status";a:16:{s:2:"id";s:6:"status";s:5:"table";s:16:"media_field_data";s:5:"field";s:6:"status";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:8:"operator";s:1:"=";s:5:"value";s:1:"1";s:5:"group";i:1;s:7:"exposed";b:1;s:6:"expose";a:10:{s:11:"operator_id";s:0:"";s:5:"label";s:17:"Publishing status";s:11:"description";N;s:12:"use_operator";b:0;s:8:"operator";s:9:"status_op";s:10:"identifier";s:6:"status";s:8:"required";b:1;s:8:"remember";b:0;s:8:"multiple";b:0;s:14:"remember_roles";a:1:{s:13:"authenticated";s:13:"authenticated";}}s:10:"is_grouped";b:1;s:10:"group_info";a:10:{s:5:"label";s:9:"Published";s:11:"description";s:0:"";s:10:"identifier";s:6:"status";s:8:"optional";b:1;s:6:"widget";s:6:"select";s:8:"multiple";b:0;s:8:"remember";b:0;s:13:"default_group";s:3:"All";s:22:"default_group_multiple";a:0:{}s:11:"group_items";a:2:{i:1;a:3:{s:5:"title";s:9:"Published";s:8:"operator";s:1:"=";s:5:"value";s:1:"1";}i:2;a:3:{s:5:"title";s:11:"Unpublished";s:8:"operator";s:1:"=";s:5:"value";s:1:"0";}}}s:9:"plugin_id";s:7:"boolean";s:11:"entity_type";s:5:"media";s:12:"entity_field";s:6:"status";}s:4:"name";a:16:{s:2:"id";s:4:"name";s:5:"table";s:16:"media_field_data";s:5:"field";s:4:"name";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:8:"operator";s:8:"contains";s:5:"value";s:0:"";s:5:"group";i:1;s:7:"exposed";b:1;s:6:"expose";a:10:{s:11:"operator_id";s:7:"name_op";s:5:"label";s:4:"Name";s:11:"description";s:0:"";s:12:"use_operator";b:0;s:8:"operator";s:7:"name_op";s:10:"identifier";s:4:"name";s:8:"required";b:0;s:8:"remember";b:0;s:8:"multiple";b:0;s:14:"remember_roles";a:3:{s:13:"authenticated";s:13:"authenticated";s:9:"anonymous";s:1:"0";s:13:"administrator";s:1:"0";}}s:10:"is_grouped";b:0;s:10:"group_info";a:10:{s:5:"label";s:0:"";s:11:"description";s:0:"";s:10:"identifier";s:0:"";s:8:"optional";b:1;s:6:"widget";s:6:"select";s:8:"multiple";b:0;s:8:"remember";b:0;s:13:"default_group";s:3:"All";s:22:"default_group_multiple";a:0:{}s:11:"group_items";a:0:{}}s:11:"entity_type";s:5:"media";s:12:"entity_field";s:4:"name";s:9:"plugin_id";s:6:"string";}s:6:"bundle";a:16:{s:2:"id";s:6:"bundle";s:5:"table";s:16:"media_field_data";s:5:"field";s:6:"bundle";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:8:"operator";s:2:"in";s:5:"value";a:0:{}s:5:"group";i:1;s:7:"exposed";b:1;s:6:"expose";a:11:{s:11:"operator_id";s:9:"bundle_op";s:5:"label";s:10:"Media type";s:11:"description";s:0:"";s:12:"use_operator";b:0;s:8:"operator";s:9:"bundle_op";s:10:"identifier";s:4:"type";s:8:"required";b:0;s:8:"remember";b:0;s:8:"multiple";b:0;s:14:"remember_roles";a:3:{s:13:"authenticated";s:13:"authenticated";s:9:"anonymous";s:1:"0";s:13:"administrator";s:1:"0";}s:6:"reduce";b:0;}s:10:"is_grouped";b:0;s:10:"group_info";a:10:{s:5:"label";s:10:"Media type";s:11:"description";N;s:10:"identifier";s:6:"bundle";s:8:"optional";b:1;s:6:"widget";s:6:"select";s:8:"multiple";b:0;s:8:"remember";b:0;s:13:"default_group";s:3:"All";s:22:"default_group_multiple";a:0:{}s:11:"group_items";a:3:{i:1;a:0:{}i:2;a:0:{}i:3;a:0:{}}}s:11:"entity_type";s:5:"media";s:12:"entity_field";s:6:"bundle";s:9:"plugin_id";s:6:"bundle";}}s:5:"sorts";a:3:{s:7:"created";a:13:{s:2:"id";s:7:"created";s:5:"table";s:16:"media_field_data";s:5:"field";s:7:"created";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"order";s:4:"DESC";s:7:"exposed";b:1;s:6:"expose";a:1:{s:5:"label";s:12:"Newest first";}s:11:"granularity";s:6:"second";s:11:"entity_type";s:5:"media";s:12:"entity_field";s:7:"created";s:9:"plugin_id";s:4:"date";}s:4:"name";a:12:{s:2:"id";s:4:"name";s:5:"table";s:16:"media_field_data";s:5:"field";s:4:"name";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"order";s:3:"ASC";s:7:"exposed";b:1;s:6:"expose";a:1:{s:5:"label";s:10:"Name (A-Z)";}s:11:"entity_type";s:5:"media";s:12:"entity_field";s:4:"name";s:9:"plugin_id";s:8:"standard";}s:6:"name_1";a:12:{s:2:"id";s:6:"name_1";s:5:"table";s:16:"media_field_data";s:5:"field";s:4:"name";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"order";s:4:"DESC";s:7:"exposed";b:1;s:6:"expose";a:1:{s:5:"label";s:10:"Name (Z-A)";}s:11:"entity_type";s:5:"media";s:12:"entity_field";s:4:"name";s:9:"plugin_id";s:8:"standard";}}s:5:"title";s:5:"Media";s:6:"header";a:0:{}s:6:"footer";a:0:{}s:5:"empty";a:1:{s:16:"area_text_custom";a:10:{s:2:"id";s:16:"area_text_custom";s:5:"table";s:5:"views";s:5:"field";s:16:"area_text_custom";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"empty";b:1;s:8:"tokenize";b:0;s:7:"content";s:19:"No media available.";s:9:"plugin_id";s:11:"text_custom";}}s:13:"relationships";a:0:{}s:17:"display_extenders";a:0:{}s:8:"use_ajax";b:1;s:9:"css_class";s:40:"media-library-view js-media-library-view";}s:14:"cache_metadata";a:3:{s:7:"max-age";i:0;s:8:"contexts";a:5:{i:0;s:28:"languages:language_interface";i:1;s:3:"url";i:2;s:14:"url.query_args";i:3;s:22:"url.query_args:sort_by";i:4;s:16:"user.permissions";}s:4:"tags";a:5:{i:0;s:51:"config:core.entity_view_display.media.audio.default";i:1;s:50:"config:core.entity_view_display.media.file.default";i:2;s:51:"config:core.entity_view_display.media.image.default";i:3;s:58:"config:core.entity_view_display.media.remote_video.default";i:4;s:51:"config:core.entity_view_display.media.video.default";}}}s:4:"page";a:6:{s:14:"display_plugin";s:4:"page";s:2:"id";s:4:"page";s:13:"display_title";s:4:"Page";s:8:"position";i:1;s:15:"display_options";a:3:{s:17:"display_extenders";a:0:{}s:4:"path";s:19:"admin/content/media";s:4:"menu";a:8:{s:4:"type";s:3:"tab";s:5:"title";s:5:"Media";s:11:"description";s:49:"Allows users to browse and administer media items";s:8:"expanded";b:0;s:6:"parent";s:20:"system.admin_content";s:6:"weight";i:5;s:7:"context";s:1:"0";s:9:"menu_name";s:5:"admin";}}s:14:"cache_metadata";a:3:{s:7:"max-age";i:0;s:8:"contexts";a:5:{i:0;s:28:"languages:language_interface";i:1;s:3:"url";i:2;s:14:"url.query_args";i:3;s:22:"url.query_args:sort_by";i:4;s:16:"user.permissions";}s:4:"tags";a:5:{i:0;s:51:"config:core.entity_view_display.media.audio.default";i:1;s:50:"config:core.entity_view_display.media.file.default";i:2;s:51:"config:core.entity_view_display.media.image.default";i:3;s:58:"config:core.entity_view_display.media.remote_video.default";i:4;s:51:"config:core.entity_view_display.media.video.default";}}}s:6:"widget";a:6:{s:14:"display_plugin";s:4:"page";s:2:"id";s:6:"widget";s:13:"display_title";s:6:"Widget";s:8:"position";i:2;s:15:"display_options";a:6:{s:17:"display_extenders";a:0:{}s:4:"path";s:26:"admin/content/media-widget";s:6:"fields";a:2:{s:15:"rendered_entity";a:24:{s:2:"id";s:15:"rendered_entity";s:5:"table";s:5:"media";s:5:"field";s:15:"rendered_entity";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"label";s:0:"";s:7:"exclude";b:0;s:5:"alter";a:26:{s:10:"alter_text";b:0;s:4:"text";s:0:"";s:9:"make_link";b:0;s:4:"path";s:0:"";s:8:"absolute";b:0;s:8:"external";b:0;s:14:"replace_spaces";b:0;s:9:"path_case";s:4:"none";s:15:"trim_whitespace";b:0;s:3:"alt";s:0:"";s:3:"rel";s:0:"";s:10:"link_class";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:6:"target";s:0:"";s:5:"nl2br";b:0;s:10:"max_length";i:0;s:13:"word_boundary";b:1;s:8:"ellipsis";b:1;s:9:"more_link";b:0;s:14:"more_link_text";s:0:"";s:14:"more_link_path";s:0:"";s:10:"strip_tags";b:0;s:4:"trim";b:0;s:13:"preserve_tags";s:0:"";s:4:"html";b:0;}s:12:"element_type";s:0:"";s:13:"element_class";s:27:"media-library-item__content";s:18:"element_label_type";s:0:"";s:19:"element_label_class";s:0:"";s:19:"element_label_colon";b:0;s:20:"element_wrapper_type";s:0:"";s:21:"element_wrapper_class";s:0:"";s:23:"element_default_classes";b:1;s:5:"empty";s:0:"";s:10:"hide_empty";b:0;s:10:"empty_zero";b:0;s:16:"hide_alter_empty";b:1;s:9:"view_mode";s:13:"media_library";s:11:"entity_type";s:5:"media";s:9:"plugin_id";s:15:"rendered_entity";}s:25:"media_library_select_form";a:23:{s:2:"id";s:25:"media_library_select_form";s:5:"table";s:5:"media";s:5:"field";s:25:"media_library_select_form";s:12:"relationship";s:4:"none";s:10:"group_type";s:5:"group";s:11:"admin_label";s:0:"";s:5:"label";s:0:"";s:7:"exclude";b:0;s:5:"alter";a:26:{s:10:"alter_text";b:0;s:4:"text";s:0:"";s:9:"make_link";b:0;s:4:"path";s:0:"";s:8:"absolute";b:0;s:8:"external";b:0;s:14:"replace_spaces";b:0;s:9:"path_case";s:4:"none";s:15:"trim_whitespace";b:0;s:3:"alt";s:0:"";s:3:"rel";s:0:"";s:10:"link_class";s:0:"";s:6:"prefix";s:0:"";s:6:"suffix";s:0:"";s:6:"target";s:0:"";s:5:"nl2br";b:0;s:10:"max_length";i:0;s:13:"word_boundary";b:1;s:8:"ellipsis";b:1;s:9:"more_link";b:0;s:14:"more_link_text";s:0:"";s:14:"more_link_path";s:0:"";s:10:"strip_tags";b:0;s:4:"trim";b:0;s:13:"preserve_tags";s:0:"";s:4:"html";b:0;}s:12:"element_type";s:0:"";s:13:"element_class";s:0:"";s:18:"element_label_type";s:0:"";s:19:"element_label_class";s:0:"";s:19:"element_label_colon";b:0;s:20:"element_wrapper_type";s:0:"";s:21:"element_wrapper_class";s:27:"js-click-to-select-checkbox";s:23:"element_default_classes";b:1;s:5:"empty";s:0:"";s:10:"hide_empty";b:0;s:10:"empty_zero";b:0;s:16:"hide_alter_empty";b:1;s:11:"entity_type";s:5:"media";s:9:"plugin_id";s:25:"media_library_select_form";}}s:8:"defaults";a:2:{s:6:"fields";b:0;s:6:"access";b:0;}s:19:"display_description";s:0:"";s:6:"access";a:2:{s:4:"type";s:4:"perm";s:7:"options";a:1:{s:4:"perm";s:10:"view media";}}}s:14:"cache_metadata";a:3:{s:7:"max-age";i:-1;s:8:"contexts";a:5:{i:0;s:28:"languages:language_interface";i:1;s:3:"url";i:2;s:14:"url.query_args";i:3;s:22:"url.query_args:sort_by";i:4;s:16:"user.permissions";}s:4:"tags";a:5:{i:0;s:51:"config:core.entity_view_display.media.audio.default";i:1;s:50:"config:core.entity_view_display.media.file.default";i:2;s:51:"config:core.entity_view_display.media.image.default";i:3;s:58:"config:core.entity_view_display.media.remote_video.default";i:4;s:51:"config:core.entity_view_display.media.video.default";}}}}}',
+))
+->execute();
+
+// Insert media library key_value entries.
+$connection->insert('key_value')
+->fields(array(
+  'collection',
+  'name',
+  'value',
+))
+->values(array(
+  'collection' => 'config.entity.key_store.entity_view_display',
+  'name' => 'uuid:67e6d857-8ecb-49f5-95e1-6b1c4306c31f',
+  'value' => 'a:1:{i:0;s:49:"core.entity_view_display.media.file.media_library";}',
+))
+->values(array(
+  'collection' => 'config.entity.key_store.entity_view_display',
+  'name' => 'uuid:277ca98b-2ada-4251-ad69-aa73e72d60fe',
+  'value' => 'a:1:{i:0;s:50:"core.entity_view_display.media.image.media_library";}',
+))
+->values(array(
+  'collection' => 'config.entity.key_store.entity_view_mode',
+  'name' => 'uuid:20b2f1f7-a864-4d41-a15f-32f66789f73d',
+  'value' => 'a:1:{i:0;s:41:"core.entity_view_mode.media.media_library";}',
+))
+->values(array(
+  'collection' => 'config.entity.key_store.view',
+  'name' => 'uuid:3bc9cf0f-cb66-4dbe-8d7e-862cb85e5932',
+  'value' => 'a:1:{i:0;s:24:"views.view.media_library";}',
+))
+->execute();
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 e18981cd8de3..f7fd27b327f6 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 a66daea429d3..17fb52793fca 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 000000000000..4565087a3829
--- /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_one: type_one
+    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 000000000000..cd1485a897e3
--- /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: 1
+translatable: true
+indexes: {  }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateWidgetViewTest.php b/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateWidgetViewTest.php
new file mode 100644
index 000000000000..eeb31e0cc9c5
--- /dev/null
+++ b/core/modules/media_library/tests/src/Functional/Update/MediaLibraryUpdateWidgetViewTest.php
@@ -0,0 +1,50 @@
+<?php
+
+namespace Drupal\Tests\media_library\Functional\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+
+/**
+ * Tests the media library module updates for the widget view.
+ *
+ * @group media_library
+ * @group legacy
+ */
+class MediaLibraryUpdateWidgetViewTest extends UpdatePathTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
+      __DIR__ . '/../../../../../media/tests/fixtures/update/drupal-8.4.0-media_installed.php',
+      __DIR__ . '/../../../fixtures/update/drupal-8.media_library-update-widget-view-3020716.php',
+    ];
+  }
+
+  /**
+   * Tests that the media library view config is updated.
+   *
+   * @see media_library_update_8700()
+   */
+  public function testMediaLibraryViewsConfig() {
+    $config = $this->config('views.view.media_library');
+    $this->assertNull($config->get('display.widget.display_options.defaults.filters'));
+    $this->assertNull($config->get('display.widget.display_options.defaults.arguments'));
+    $this->assertArrayNotHasKey('filters', $config->get('display.widget.display_options'));
+    $this->assertArrayNotHasKey('arguments', $config->get('display.widget.display_options'));
+
+    $this->runUpdates();
+
+    $config = $this->config('views.view.media_library');
+    $this->assertFalse($config->get('display.widget.display_options.defaults.filters'));
+    $this->assertFalse($config->get('display.widget.display_options.defaults.arguments'));
+    $this->assertArrayHasKey('filters', $config->get('display.widget.display_options'));
+    $this->assertArrayHasKey('arguments', $config->get('display.widget.display_options'));
+    $this->assertSame('1', $config->get('display.widget.display_options.filters.status.value'));
+    $this->assertTrue($config->get('display.widget.display_options.filters.name.exposed'));
+    $this->assertSame('ignore', $config->get('display.widget.display_options.arguments.bundle.default_action'));
+  }
+
+}
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index 3b7ff1fab98d..64f3ff59b3a1 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -4,6 +4,7 @@
 
 use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 use Drupal\media\Entity\Media;
+use Drupal\media_library\MediaLibraryState;
 use Drupal\Tests\TestFileCreationTrait;
 use Drupal\user\Entity\Role;
 use Drupal\user\RoleInterface;
@@ -20,7 +21,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 +66,7 @@ protected function setUp() {
       'create media',
       'delete any media',
       'view media',
+      'administer node form display',
     ]);
     $this->drupalLogin($user);
     $this->drupalPlaceBlock('local_tasks_block');
@@ -135,6 +137,40 @@ public function testAdministrationPage() {
     $assert_session->linkExists('Add media');
   }
 
+  /**
+   * Tests that the widget access works as expected.
+   */
+  public function testWidgetAccess() {
+    $assert_session = $this->assertSession();
+
+    $this->drupalLogout();
+
+    $role = Role::load(RoleInterface::ANONYMOUS_ID);
+    $role->revokePermission('view media');
+    $role->save();
+
+    // Create a working state.
+    $allowed_types = ['type_one', 'type_two'];
+    $state = MediaLibraryState::create('test', $allowed_types, 'type_two', 2);
+    $url_options = ['query' => $state->all()];
+
+    // Verify that unprivileged users can't access the widget view.
+    $this->drupalGet('admin/content/media-widget', $url_options);
+    $assert_session->responseContains('Access denied');
+    $this->drupalGet('media-library', $url_options);
+    $assert_session->responseContains('Access denied');
+
+    // Allow users with 'view media' permission to access the media library view
+    // and controller.
+    $this->grantPermissions($role, [
+      'view media',
+    ]);
+    $this->drupalGet('admin/content/media-widget', $url_options);
+    $assert_session->elementExists('css', '.view-media-library');
+    $this->drupalGet('media-library', $url_options);
+    $assert_session->elementExists('css', '.view-media-library');
+  }
+
   /**
    * Tests that the Media library's widget works as expected.
    */
@@ -145,121 +181,291 @@ public function testWidget() {
     // Visit a node create page.
     $this->drupalGet('node/add/basic_page');
 
-    // Verify that both media widget instances are present.
+    // Assert 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.
-    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
-    $unlimited_button->click();
+    // Assert generic media library elements.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $menu = $assert_session->elementExists('css', '.media-library-menu');
+    $assert_session->elementExists('named', ['link', 'Type One'], $menu);
+    $assert_session->elementNotExists('named', ['link', 'Type Two'], $menu);
+    $assert_session->elementExists('named', ['link', 'Type Three'], $menu);
+    $assert_session->elementNotExists('named', ['link', 'Type Four'], $menu);
+    $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.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_single_media_type"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '0 of 1 item selected');
+    // Select a media item, assert 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();
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 of 1 item selected');
+    $assert_session->elementNotExists('css', '.media-library-menu');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert the menu links can be sorted through the widget configuration.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->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 (active tab)', 'Type Two', 'Type Three', 'Type Four'];
+    $this->assertSame($link_titles, $expected_link_titles);
+    $this->drupalGet('admin/structure/types/manage/basic_page/form-display');
+    $assert_session->buttonExists('field_twin_media_settings_edit')->press();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->buttonExists('Show row weights')->press();
+    $assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_one][weight]')->selectOption(0);
+    $assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_three][weight]')->selectOption(1);
+    $assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_four][weight]')->selectOption(2);
+    $assert_session->fieldExists('fields[field_twin_media][settings_edit_form][settings][media_types][type_two][weight]')->selectOption(3);
+    $assert_session->buttonExists('Save')->press();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->buttonExists('Hide row weights')->press();
+    $this->drupalGet('node/add/basic_page');
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $link_titles = array_map(function ($link) {
+      return $link->getText();
+    }, $page->findAll('css', '.media-library-menu a'));
+    $this->assertSame($link_titles, ['Type One (active tab)', 'Type Three', 'Type Four', 'Type Two']);
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Assert media is only visible on the tab for the related media type.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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.
+    $assert_session->elementExists('named', ['link', 'Type Three'])->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('named', ['link', 'Type Three (active tab)']);
+    $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.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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, and
+    // the number of selected items is displayed correctly.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // 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', '0 of 2 items selected');
+    $assert_session->elementAttributeContains('css', '.media-library-selected-count', 'aria-live', 'polite');
+    // Select a media item, assert 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();
+    $assert_session->hiddenFieldValueEquals('media-library-modal-selection', '4');
+    // Assert the number of selected items is displayed correctly.
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 of 2 items 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 of 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 of 2 items 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.
+    $assert_session->elementExists('named', ['link', 'Type Three'])->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('named', ['link', 'Type One'])->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 of 2 items selected');
+    // Add to selection from another type.
+    $assert_session->elementExists('named', ['link', 'Type Two'])->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 of 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.
+    $assert_session->elementExists('named', ['link', 'Type One'])->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->pageTextContains('Cat');
-    $assert_session->pageTextContains('Bear');
-    // Remove "Dog" (happens to be the first remove button on the page).
-    $assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Dog');
-    $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->pageTextContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
 
-    // Open another Media library on the same page.
-    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
-    $twin_button->click();
+    // Remove "Cat" (happens to be the first remove button on the page).
+    $assert_session->elementAttributeContains('css', '.media-library-item__remove', 'aria-label', 'Remove Cat');
+    $assert_session->elementExists('css', '.media-library-item__remove')->click();
     $assert_session->assertWaitOnAjaxRequest();
-    // This field allows both media types.
-    $assert_session->pageTextContains('Media library');
-    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Cat');
     $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'));
+
+    // Open the media library again and select another item.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $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.
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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());
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '0 items selected');
+    // Select the first 2 items.
+    $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
+    $checkboxes[0]->click();
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '1 item selected');
+    $checkboxes[1]->click();
+    $assert_session->elementTextContains('css', '.media-library-selected-count', '2 items selected');
+    $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();
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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');
-    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
-    $unlimited_button->click();
+    $assert_session->pageTextNotContains('Snake');
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->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');
   }
 
   /**
@@ -270,15 +476,8 @@ public function testWidgetAnonymous() {
 
     $this->drupalLogout();
 
-    $role = Role::load(RoleInterface::ANONYMOUS_ID);
-    $role->revokePermission('view media');
-    $role->save();
-
-    // Verify that unprivileged users can't access the widget view.
-    $this->drupalGet('admin/content/media-widget');
-    $assert_session->responseContains('Access denied');
-
     // Allow the anonymous user to create pages and view media.
+    $role = Role::load(RoleInterface::ANONYMOUS_ID);
     $this->grantPermissions($role, [
       'access content',
       'create basic_page content',
@@ -289,8 +488,7 @@ public function testWidgetAnonymous() {
     $this->drupalGet('node/add/basic_page');
 
     // Add to the unlimited cardinality field.
-    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
-    $unlimited_button->click();
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
 
     // Select the first media item (should be Dog).
@@ -343,8 +541,7 @@ public function testWidgetUpload() {
     $file_system = $this->container->get('file_system');
 
     // Add to the twin media field.
-    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
-    $twin_button->click();
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Media library');
     $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media');
@@ -377,8 +574,7 @@ public function testWidgetUpload() {
     $assert_session->pageTextContains($png_image->filename);
 
     // Also make sure that we can upload to the unlimited cardinality field.
-    $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
-    $unlimited_button->click();
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Media library');
     $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media');
@@ -400,8 +596,7 @@ public function testWidgetUpload() {
     $assert_session->pageTextContains('Unlimited Cardinality Image');
 
     // Open the browser again to test type resolution.
-    $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
-    $twin_button->click();
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextContains('Media library');
     $assert_session->elementExists('css', '#drupal-modal')->clickLink('Add media');
diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php
index f02347d7b93b..5f305b4f832e 100644
--- a/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php
+++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAccessTest.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\Tests\media_library\Kernel;
 
+use Drupal\Core\Access\AccessResult;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\Tests\user\Traits\UserCreationTrait;
+use Drupal\views\Views;
 
 /**
  * Tests the media library access.
@@ -69,4 +71,55 @@ public function testMediaLibraryImageStyleAccess() {
     $this->assertFalse(ImageStyle::load('media_library')->access('delete', $user));
   }
 
+  /**
+   * Tests the Media Library access.
+   */
+  public function testMediaLibraryAccess() {
+    /** @var \Drupal\media_library\MediaLibraryUiBuilder $ui_builder */
+    $ui_builder = $this->container->get('media_library.ui_builder');
+
+    $view_original = clone Views::getView('media_library');
+
+    // Create our test users.
+    $forbidden_account = $this->createUser([]);
+    $allowed_account = $this->createUser(['view media']);
+
+    // Assert the 'view media' permission is needed to access the library and
+    // validate the cache dependencies.
+    $this->assertSame(AccessResult::forbidden()->isAllowed(), $ui_builder->checkAccess($forbidden_account)->isAllowed());
+    $this->assertSame("The 'view media' permission is required.", $ui_builder->checkAccess($forbidden_account)->getReason());
+    $this->assertSame($view_original->storage->getCacheTags(), $ui_builder->checkAccess($forbidden_account)->getCacheTags());
+    $this->assertSame(['user.permissions'], $ui_builder->checkAccess($forbidden_account)->getCacheContexts());
+    $this->assertSame(AccessResult::allowed()->isAllowed(), $ui_builder->checkAccess($allowed_account)->isAllowed());
+    $this->assertSame($view_original->storage->getCacheTags(), $ui_builder->checkAccess($allowed_account)->getCacheTags());
+    $this->assertSame(['user.permissions'], $ui_builder->checkAccess($allowed_account)->getCacheContexts());
+
+    // Assert that the media library access is denied when the view widget
+    // display is deleted.
+    $view_storage = Views::getView('media_library')->storage;
+    $displays = $view_storage->get('display');
+    unset($displays['widget']);
+    $view_storage->set('display', $displays);
+    $view_storage->save();
+    $this->assertSame(AccessResult::forbidden()->isAllowed(), $ui_builder->checkAccess($allowed_account)->isAllowed());
+    $this->assertSame('The media library widget display does not exist.', $ui_builder->checkAccess($forbidden_account)->getReason());
+    $this->assertSame($view_original->storage->getCacheTags(), $ui_builder->checkAccess($forbidden_account)->getCacheTags());
+    $this->assertSame([], $ui_builder->checkAccess($forbidden_account)->getCacheContexts());
+
+    // Restore the original view and assert that the media library controller
+    // works again.
+    $view_original->storage->save();
+    $this->assertSame(AccessResult::allowed()->isAllowed(), $ui_builder->checkAccess($allowed_account)->isAllowed());
+    $this->assertSame($view_original->storage->getCacheTags(), $ui_builder->checkAccess($allowed_account)->getCacheTags());
+    $this->assertSame(['user.permissions'], $ui_builder->checkAccess($allowed_account)->getCacheContexts());
+
+    // Assert that the media library access is denied when the entire media
+    // library view is deleted.
+    Views::getView('media_library')->storage->delete();
+    $this->assertSame(AccessResult::forbidden()->isAllowed(), $ui_builder->checkAccess($allowed_account)->isAllowed());
+    $this->assertSame('The media library view does not exist.', $ui_builder->checkAccess($forbidden_account)->getReason());
+    $this->assertSame([], $ui_builder->checkAccess($forbidden_account)->getCacheTags());
+    $this->assertSame([], $ui_builder->checkAccess($forbidden_account)->getCacheContexts());
+  }
+
 }
diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php
new file mode 100644
index 000000000000..b3b027abb22b
--- /dev/null
+++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryStateTest.php
@@ -0,0 +1,259 @@
+<?php
+
+namespace Drupal\Tests\media_library\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+
+/**
+ * Tests the media library state value object.
+ *
+ * @group media_library
+ *
+ * @coversDefaultClass \Drupal\media_library\MediaLibraryState
+ */
+class MediaLibraryStateTest extends KernelTestBase {
+
+  use MediaTypeCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'media',
+    'media_library',
+    'file',
+    'field',
+    'image',
+    'system',
+    'views',
+    'user',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    $this->installEntitySchema('user');
+    $this->installEntitySchema('file');
+    $this->installSchema('file', 'file_usage');
+    $this->installSchema('system', 'sequences');
+    $this->installEntitySchema('media');
+    $this->installConfig([
+      'field',
+      'system',
+      'file',
+      'image',
+      'media',
+      'media_library',
+    ]);
+
+    // Create some media types to validate against.
+    $this->createMediaType('file', ['id' => 'file']);
+    $this->createMediaType('image', ['id' => 'image']);
+    $this->createMediaType('video_file', ['id' => 'video']);
+  }
+
+  /**
+   * Tests the media library state methods.
+   */
+  public function testMethods() {
+    $opener_id = 'test';
+    $allowed_media_type_ids = ['file', 'image'];
+    $selected_media_type_id = 'image';
+    $remaining_slots = 2;
+
+    $state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_media_type_id, $remaining_slots);
+    $this->assertSame($opener_id, $state->getOpenerId());
+    $this->assertSame($allowed_media_type_ids, $state->getAllowedTypeIds());
+    $this->assertSame($selected_media_type_id, $state->getSelectedTypeId());
+    $this->assertSame($remaining_slots, $state->getAvailableSlots());
+    $this->assertTrue($state->hasSlotsAvailable());
+
+    $state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_media_type_id, 0);
+    $this->assertFalse($state->hasSlotsAvailable());
+  }
+
+  /**
+   * Tests the media library state creation.
+   *
+   * @param string $opener_id
+   *   The opener ID.
+   * @param string[] $allowed_media_type_ids
+   *   The allowed media type IDs.
+   * @param string $selected_type_id
+   *   The selected media type ID.
+   * @param int $remaining_slots
+   *   The number of remaining items the user is allowed to select or add in the
+   *   library.
+   * @param string $exception_message
+   *   The expected exception message.
+   *
+   * @covers ::create
+   * @dataProvider providerCreate
+   */
+  public function testCreate($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots, $exception_message = '') {
+    if ($exception_message) {
+      $this->setExpectedException(\InvalidArgumentException::class, $exception_message);
+    }
+    $state = MediaLibraryState::create($opener_id, $allowed_media_type_ids, $selected_type_id, $remaining_slots);
+    $this->assertInstanceOf(MediaLibraryState::class, $state);
+  }
+
+  /**
+   * Data provider for testCreate().
+   *
+   * @return array
+   *   The data sets to test.
+   */
+  public function providerCreate() {
+    $test_data = [];
+
+    // Assert no exception is thrown when we add the parameters as expected.
+    $test_data['valid parameters'] = [
+      'test',
+      ['file', 'image'],
+      'image',
+      2,
+    ];
+
+    // Assert an exception is thrown when the opener ID parameter is empty.
+    $test_data['empty opener ID'] = [
+      '',
+      ['file', 'image'],
+      'image',
+      2,
+      'The opener ID parameter is required and must be a string.',
+    ];
+    // Assert an exception is thrown when the opener ID parameter is not a
+    // valid string.
+    $test_data['integer opener ID'] = [
+      1,
+      ['file', 'image'],
+      'image',
+      2,
+      'The opener ID parameter is required and must be a string.',
+    ];
+    $test_data['boolean opener ID'] = [
+      TRUE,
+      ['file', 'image'],
+      'image',
+      2,
+      'The opener ID parameter is required and must be a string.',
+    ];
+    $test_data['spaces opener ID'] = [
+      '   ',
+      ['file', 'image'],
+      'image',
+      2,
+      'The opener ID parameter is required and must be a string.',
+    ];
+
+    // Assert an exception is thrown when the allowed types parameter is empty.
+    $test_data['empty allowed types'] = [
+      'test',
+      [],
+      'image',
+      2,
+      'The allowed types parameter is required and must be an array of strings.',
+    ];
+    // It is not possible to assert a non-array allowed types parameter, since
+    // that would throw a TypeError which is not a subclass of Exception.
+    // Continue asserting an exception is thrown when the allowed types
+    // parameter contains elements that are not a valid string.
+    $test_data['integer in allowed types'] = [
+      'test',
+      [1, 'image'],
+      'image',
+      2,
+      'The allowed types parameter is required and must be an array of strings.',
+    ];
+    $test_data['boolean in allowed types'] = [
+      'test',
+      [TRUE, 'image'],
+      'image',
+      2,
+      'The allowed types parameter is required and must be an array of strings.',
+    ];
+    $test_data['spaces in allowed types'] = [
+      'test',
+      ['   ', 'image'],
+      'image',
+      2,
+      'The allowed types parameter is required and must be an array of strings.',
+    ];
+
+    // Assert an exception is thrown when the selected type parameter is empty.
+    $test_data['empty selected type'] = [
+      'test',
+      ['file', 'image'],
+      '',
+      2,
+      'The selected type parameter is required and must be a string.',
+    ];
+    // Assert an exception is thrown when the selected type parameter is not a
+    // valid string.
+    $test_data['numeric selected type'] = [
+      'test',
+      ['file', 'image'],
+      1,
+      2,
+      'The selected type parameter is required and must be a string.',
+    ];
+    $test_data['boolean selected type'] = [
+      'test',
+      ['file', 'image'],
+      TRUE,
+      2,
+      'The selected type parameter is required and must be a string.',
+    ];
+    $test_data['spaces selected type'] = [
+      'test',
+      ['file', 'image'],
+      '   ',
+      2,
+      'The selected type parameter is required and must be a string.',
+    ];
+    // Assert an exception is thrown when the selected type parameter is not in
+    // the list of allowed types.
+    $test_data['non-present selected type'] = [
+      'test',
+      ['file', 'image'],
+      'video',
+      2,
+      'The selected type parameter must be present in the list of allowed types.',
+    ];
+
+    // Assert an exception is thrown when the remaining slots parameter is
+    // empty.
+    $test_data['empty remaining slots'] = [
+      'test',
+      ['file', 'image'],
+      'image',
+      '',
+      'The remaining slots parameter is required and must be numeric.',
+    ];
+    // Assert an exception is thrown when the remaining slots parameter is
+    // not numeric.
+    $test_data['string remaining slots'] = [
+      'test',
+      ['file', 'image'],
+      'image',
+      'fail',
+      'The remaining slots parameter is required and must be numeric.',
+    ];
+    $test_data['boolean remaining slots'] = [
+      'test',
+      ['file', 'image'],
+      'image',
+      TRUE,
+      'The remaining slots parameter is required and must be numeric.',
+    ];
+
+    return $test_data;
+  }
+
+}
-- 
GitLab