From cf1e3ee787d7d9ce13861742c75906fced3dadf3 Mon Sep 17 00:00:00 2001
From: Francesco Placella <plach@183211.no-reply.drupal.org>
Date: Tue, 24 Jul 2018 10:35:32 +0200
Subject: [PATCH] =?UTF-8?q?Issue=20#2962525=20by=20samuel.mortenson,=20jro?=
 =?UTF-8?q?ckowitz,=20seanB,=20drpal,=20chr.fritsch,=20ckrina,=20phenaprox?=
 =?UTF-8?q?ima,=20webchick,=20lauriii,=20beautifulmind,=20andrewmacpherson?=
 =?UTF-8?q?,=20xjm,=20G=C3=A1bor=20Hojtsy:=20Create=20a=20field=20widget?=
 =?UTF-8?q?=20for=20the=20Media=20library=20module?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

(cherry picked from commit de52834d763e2f3ff12d7d75d19e6146cb49da17)
---
 core/modules/media/media.module               |  61 +-
 .../install/views.view.media_library.yml      | 177 ++++--
 .../css/media_library.module.css              |  36 +-
 .../media_library/css/media_library.theme.css |  90 ++-
 .../js/media_library.click_to_select.es6.js   |   6 +-
 .../js/media_library.click_to_select.js       |   6 +-
 .../js/media_library.view.es6.js              |   4 +-
 .../media_library/js/media_library.view.js    |   4 +-
 .../js/media_library.widget.es6.js            |  93 ++++
 .../media_library/js/media_library.widget.js  |  67 +++
 .../media_library/media_library.libraries.yml |   9 +
 .../media_library/media_library.module        | 148 ++++-
 .../media_library/media_library.views.inc     |  22 +
 .../Field/FieldWidget/MediaLibraryWidget.php  | 524 ++++++++++++++++++
 .../views/field/MediaLibrarySelectForm.php    | 128 +++++
 ...y_form_display.node.basic_page.default.yml |  71 +++
 ...y_view_display.node.basic_page.default.yml |  38 ++
 ...field.node.basic_page.field_twin_media.yml |  29 +
 ....node.basic_page.field_unlimited_media.yml |  27 +
 .../field.storage.node.field_twin_media.yml   |  19 +
 ...eld.storage.node.field_unlimited_media.yml |  19 +
 .../config/install/node.type.basic_page.yml   |   9 +
 .../media_library_test.info.yml               |   1 +
 .../FunctionalJavascript/MediaLibraryTest.php | 203 ++++++-
 24 files changed, 1665 insertions(+), 126 deletions(-)
 create mode 100644 core/modules/media_library/js/media_library.widget.es6.js
 create mode 100644 core/modules/media_library/js/media_library.widget.js
 create mode 100644 core/modules/media_library/media_library.views.inc
 create mode 100644 core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
 create mode 100644 core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml
 create mode 100644 core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml

diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 20d12b856cc5..a95b972d6656 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -243,31 +243,9 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt
   // Retrieve the media bundle list and add information for the user based on
   // which bundles are available to be created or referenced.
   $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
-  $allowed_bundles = isset($settings['target_bundles']) ? $settings['target_bundles'] : [];
-  $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
-  $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
-  $bundle_labels = [];
-  $create_bundles = [];
-  foreach ($allowed_bundles as $bundle) {
-    $bundle_labels[] = $all_bundles[$bundle]['label'];
-    if ($access_handler->createAccess($bundle)) {
-      $create_bundles[] = $bundle;
-      if (count($create_bundles) > 1) {
-        // If the user has access to create more than 1 bundle then the
-        // individual media type form can not be used.
-        break;
-      }
-    }
-  }
-
-  // Add a section about how to create media if the user has access to do so.
-  if (!empty($create_bundles)) {
-    if (count($create_bundles) === 1) {
-      $add_url = Url::fromRoute('entity.media.add_form', ['media_type' => $create_bundles[0]])->toString();
-    }
-    elseif (count($create_bundles) > 1) {
-      $add_url = Url::fromRoute('entity.media.add_page')->toString();
-    }
+  $allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
+  $add_url = _media_get_add_url($allowed_bundles);
+  if ($add_url) {
     $elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
   }
 
@@ -308,6 +286,10 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt
     if ($overview_url->access()) {
       $elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
     }
+    $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
+    $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
+      return $all_bundles[$bundle]['label'];
+    }, $allowed_bundles);
     $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
   }
 }
@@ -336,3 +318,32 @@ function media_preprocess_media_reference_help(&$variables) {
     }
   }
 }
+
+/**
+ * Returns the appropriate URL to add media for the current user.
+ *
+ * @todo Remove in https://www.drupal.org/project/drupal/issues/2938116
+ *
+ * @param string[] $allowed_bundles
+ *   An array of bundles that should be checked for create access.
+ *
+ * @return bool|\Drupal\Core\Url
+ *   The URL to add media, or FALSE if the user cannot create any media.
+ *
+ * @internal
+ *   This function is internal and may be removed in a minor release.
+ */
+function _media_get_add_url($allowed_bundles) {
+  $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
+  $create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']);
+
+  // Add a section about how to create media if the user has access to do so.
+  if (count($create_bundles) === 1) {
+    return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString();
+  }
+  elseif (count($create_bundles) > 1) {
+    return Url::fromRoute('entity.media.add_page')->toString();
+  }
+
+  return FALSE;
+}
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 61ac13600c6e..32096a6f9cc0 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
@@ -8,6 +8,7 @@ dependencies:
       - media_library
   module:
     - media
+    - media_library
     - user
 id: media_library
 label: 'Media library'
@@ -71,7 +72,7 @@ display:
         type: default
         options:
           grouping: {  }
-          row_class: 'media-library-item js-click-to-select'
+          row_class: 'media-library-item js-media-library-item js-click-to-select'
           default_row_class: true
       row:
         type: fields
@@ -118,7 +119,7 @@ display:
             preserve_tags: ''
             html: false
           element_type: ''
-          element_class: js-click-to-select__checkbox
+          element_class: js-click-to-select-checkbox
           element_label_type: ''
           element_label_class: ''
           element_label_colon: false
@@ -376,51 +377,9 @@ display:
           content: 'No media available.'
           plugin_id: text_custom
       relationships: {  }
-      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
       display_extenders: {  }
       use_ajax: true
-      css_class: media-library-view
+      css_class: 'media-library-view js-media-library-view'
     cache_metadata:
       max-age: 0
       contexts:
@@ -456,3 +415,131 @@ display:
         - 'url.query_args:sort_by'
         - user.permissions
       tags: {  }
+  # @todo Lock down access in https://www.drupal.org/node/2983179
+  widget:
+    display_plugin: page
+    id: widget
+    display_title: Widget
+    position: 2
+    display_options:
+      display_extenders: {  }
+      path: admin/content/media-widget
+      fields:
+        rendered_entity:
+          id: rendered_entity
+          table: media
+          field: rendered_entity
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: media-library-item__content
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: ''
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          view_mode: media_library
+          entity_type: media
+          plugin_id: rendered_entity
+        media_library_select_form:
+          id: media_library_select_form
+          table: media
+          field: media_library_select_form
+          relationship: none
+          group_type: group
+          admin_label: ''
+          label: ''
+          exclude: false
+          alter:
+            alter_text: false
+            text: ''
+            make_link: false
+            path: ''
+            absolute: false
+            external: false
+            replace_spaces: false
+            path_case: none
+            trim_whitespace: false
+            alt: ''
+            rel: ''
+            link_class: ''
+            prefix: ''
+            suffix: ''
+            target: ''
+            nl2br: false
+            max_length: 0
+            word_boundary: true
+            ellipsis: true
+            more_link: false
+            more_link_text: ''
+            more_link_path: ''
+            strip_tags: false
+            trim: false
+            preserve_tags: ''
+            html: false
+          element_type: ''
+          element_class: ''
+          element_label_type: ''
+          element_label_class: ''
+          element_label_colon: false
+          element_wrapper_type: ''
+          element_wrapper_class: js-click-to-select-checkbox
+          element_default_classes: true
+          empty: ''
+          hide_empty: false
+          empty_zero: false
+          hide_alter_empty: true
+          entity_type: media
+          plugin_id: media_library_select_form
+      defaults:
+        fields: false
+        access: false
+      display_description: ''
+      access:
+        type: perm
+        options:
+          perm: 'view media'
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - 'url.query_args:sort_by'
+        - user.permissions
+      tags: {  }
diff --git a/core/modules/media_library/css/media_library.module.css b/core/modules/media_library/css/media_library.module.css
index 11ad56dd922e..89b15800634a 100644
--- a/core/modules/media_library/css/media_library.module.css
+++ b/core/modules/media_library/css/media_library.module.css
@@ -2,22 +2,19 @@
 * @file media_library.module.css
 */
 
-.media-library-page-form {
-  display: flex;
-  flex-wrap: wrap;
-}
-
-.media-library-page-form > .form-actions {
+.media-library-views-form > .form-actions {
   flex-basis: 100%;
 }
 
-.media-library-page-form__header > div,
+.media-library-views-form,
+.media-library-selection,
+.media-library-views-form__bulk_form,
 .media-library-view .form--inline {
   display: flex;
   flex-wrap: wrap;
 }
 
-.media-library-page-form__header {
+.media-library-views-form__header {
   flex-basis: 100%;
 }
 
@@ -25,7 +22,7 @@
   position: relative;
 }
 
-.media-library-item .js-click-to-select__trigger {
+.media-library-item .js-click-to-select-trigger {
   overflow: hidden;
   cursor: pointer;
 }
@@ -34,7 +31,7 @@
   align-self: flex-end;
 }
 
-.media-library-item .js-click-to-select__checkbox {
+.media-library-item .js-click-to-select-checkbox {
   position: absolute;
   display: block;
   z-index: 1;
@@ -51,6 +48,25 @@
 
 .media-library-select-all {
   flex-basis: 100%;
+  width: 100%;
+}
+
+.media-library-view.view-display-id-widget .media-library-select-all {
+  display: none;
+}
+
+.media-library-item--disabled {
+  pointer-events: none;
+}
+
+.media-library-selection .media-library-item__preview {
+  cursor: move;
+}
+
+/* @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 {
+  pointer-events: none;
 }
 
 @media screen and (max-width: 600px) {
diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 0427143538e3..5aab3c5bbd17 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -5,7 +5,7 @@
  * @see https://www.drupal.org/project/drupal/issues/2980769
  */
 
-.media-library-page-form__header .form-item {
+.media-library-views-form__header .form-item {
   margin-right: 8px;
 }
 
@@ -16,21 +16,29 @@
 .media-library-item {
   justify-content: center;
   vertical-align: top;
-  padding: 2px;
-  border: 1px solid #ebebeb;
+  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 {
+  min-height: 300px;
+}
+
 .media-library-view .form-actions {
   margin: 0.75em 0;
 }
 
+.media-library-view .media-library-view--form-actions {
+  clear: left;
+  margin: 0.75em 0;
+  align-self: flex-end;
+}
+
 .media-library-item .field--name-thumbnail {
   background-color: #ebebeb;
-  margin: 2px;
   overflow: hidden;
   text-align: center;
 }
@@ -54,17 +62,17 @@
   border-color: #0076c0;
 }
 
-.media-library-item .js-click-to-select__checkbox input {
+.media-library-item .js-click-to-select-checkbox input {
   width: 30px;
   height: 30px;
 }
 
-.media-library-item .js-click-to-select__checkbox .form-item {
+.media-library-item .js-click-to-select-checkbox .form-item {
   margin: 0;
 }
 
 .media-library-item__preview {
-  padding-bottom: 44px;
+  padding-bottom: 34px;
 }
 
 .media-library-item__status {
@@ -90,9 +98,9 @@
   position: absolute;
   bottom: 0;
   display: block;
-  padding: 10px;
-  max-width: calc(100% - 20px);
-  max-height: calc(100% - 60px);
+  padding: 5px;
+  max-width: calc(100% - 10px);
+  max-height: calc(100% - 50px);
   overflow: hidden;
   background: white;
 }
@@ -135,6 +143,68 @@
   margin-right: 10px;
 }
 
+.media-library-item--disabled {
+  opacity: 0.5;
+}
+
+.media-library-selection {
+  margin-bottom: 1.5rem;
+}
+
+.media-library-widget {
+  position: relative;
+}
+
+.media-library-widget__toggle-weight {
+  position: absolute;
+  right: 5px;
+  top: 5px;
+}
+
+.media-library-item .form-item {
+  margin: 0.75em;
+}
+
+.media-library-item__remove,
+.media-library-item__remove:hover,
+.media-library-item__remove:focus,
+.media-library-item__remove.button,
+.media-library-item__remove.button:disabled,
+.media-library-item__remove.button:disabled:active,
+.media-library-item__remove.button:hover,
+.media-library-item__remove.button:focus {
+  position: absolute;
+  z-index: 1;
+  top: 0;
+  right: 0;
+  width: 24px;
+  height: 24px;
+  margin: 5px;
+  padding: 0;
+  background: url('../../../misc/icons/787878/ex.svg') #fff center no-repeat;
+  background-size: 16px 16px;
+  border: 2px solid #ccc;
+  border-radius: 20px;
+  color: transparent;
+  text-shadow: none;
+  transition: 0.2s border-color;
+}
+
+.media-library-item__remove:hover,
+.media-library-item__remove:focus,
+.media-library-item__remove.button:hover,
+.media-library-item__remove.button:focus,
+.media-library-item__remove.button:disabled:active {
+  border-color: #40b6ff;
+}
+
+/* @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;
+}
+
 @media screen and (max-width: 600px) {
   .media-library-item {
     width: 150px;
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 321ffe0c73a9..d5179c34997c 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
@@ -8,7 +8,7 @@
    */
   Drupal.behaviors.ClickToSelect = {
     attach(context) {
-      $('.js-click-to-select__trigger', context)
+      $('.js-click-to-select-trigger', context)
         .once('media-library-click-to-select')
         .on('click', (event) => {
           // Links inside the trigger should not be click-able.
@@ -16,10 +16,10 @@
           // Click the hidden checkbox when the trigger is clicked.
           const $input = $(event.currentTarget)
             .closest('.js-click-to-select')
-            .find('.js-click-to-select__checkbox input');
+            .find('.js-click-to-select-checkbox input');
           $input.prop('checked', !$input.prop('checked')).trigger('change');
         });
-      $('.js-click-to-select__checkbox input', context)
+      $('.js-click-to-select-checkbox input', context)
         .once('media-library-click-to-select')
         .on('change', ({ currentTarget }) => {
           $(currentTarget)
diff --git a/core/modules/media_library/js/media_library.click_to_select.js b/core/modules/media_library/js/media_library.click_to_select.js
index 4bc041c43e56..01cbb1431b4c 100644
--- a/core/modules/media_library/js/media_library.click_to_select.js
+++ b/core/modules/media_library/js/media_library.click_to_select.js
@@ -8,13 +8,13 @@
 (function ($, Drupal) {
   Drupal.behaviors.ClickToSelect = {
     attach: function attach(context) {
-      $('.js-click-to-select__trigger', context).once('media-library-click-to-select').on('click', function (event) {
+      $('.js-click-to-select-trigger', context).once('media-library-click-to-select').on('click', function (event) {
         event.preventDefault();
 
-        var $input = $(event.currentTarget).closest('.js-click-to-select').find('.js-click-to-select__checkbox input');
+        var $input = $(event.currentTarget).closest('.js-click-to-select').find('.js-click-to-select-checkbox input');
         $input.prop('checked', !$input.prop('checked')).trigger('change');
       });
-      $('.js-click-to-select__checkbox input', context).once('media-library-click-to-select').on('change', function (_ref) {
+      $('.js-click-to-select-checkbox input', context).once('media-library-click-to-select').on('change', function (_ref) {
         var currentTarget = _ref.currentTarget;
 
         $(currentTarget).closest('.js-click-to-select').toggleClass('checked', $(currentTarget).prop('checked'));
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 26e9a5b2635e..98b85c9ed159 100644
--- a/core/modules/media_library/js/media_library.view.es6.js
+++ b/core/modules/media_library/js/media_library.view.es6.js
@@ -7,7 +7,7 @@
    */
   Drupal.behaviors.MediaLibraryHover = {
     attach(context) {
-      $('.media-library-item .js-click-to-select__trigger,.media-library-item .js-click-to-select__checkbox', context).once('media-library-item-hover')
+      $('.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', ({ currentTarget, type }) => {
           $(currentTarget).closest('.media-library-item').toggleClass('is-hover', type === 'mouseover');
         });
@@ -19,7 +19,7 @@
    */
   Drupal.behaviors.MediaLibraryFocus = {
     attach(context) {
-      $('.media-library-item .js-click-to-select__checkbox input', context).once('media-library-item-focus')
+      $('.media-library-item .js-click-to-select-checkbox input', context).once('media-library-item-focus')
         .on('focus blur', ({ currentTarget, type }) => {
           $(currentTarget).closest('.media-library-item').toggleClass('is-focus', type === 'focus');
         });
diff --git a/core/modules/media_library/js/media_library.view.js b/core/modules/media_library/js/media_library.view.js
index 1cde60c3acfb..90573005711e 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) {
+      $('.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) {
         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) {
+      $('.media-library-item .js-click-to-select-checkbox input', context).once('media-library-item-focus').on('focus blur', function (_ref2) {
         var currentTarget = _ref2.currentTarget,
             type = _ref2.type;
 
diff --git a/core/modules/media_library/js/media_library.widget.es6.js b/core/modules/media_library/js/media_library.widget.es6.js
new file mode 100644
index 000000000000..654c2cde2871
--- /dev/null
+++ b/core/modules/media_library/js/media_library.widget.es6.js
@@ -0,0 +1,93 @@
+/**
+ * @file media_library.widget.js
+ */
+(($, Drupal) => {
+  /**
+   * Allows users to re-order their selection with drag+drop.
+   */
+  Drupal.behaviors.MediaLibraryWidgetSortable = {
+    attach(context) {
+      // Allow media items to be re-sorted with drag+drop in the widget.
+      $('.js-media-library-selection', context).once('media-library-sortable').sortable({
+        tolerance: 'pointer',
+        helper: 'clone',
+        handle: '.js-media-library-item-preview',
+        stop: ({ target }) => {
+          // Update all the hidden "weight" fields.
+          $(target).children().each((index, child) => {
+            $(child).find('.js-media-library-item-weight').val(index);
+          });
+        },
+      });
+    },
+  };
+
+  /**
+   * Allows selection order to be set without drag+drop for accessibility.
+   */
+  Drupal.behaviors.MediaLibraryWidgetToggleWeight = {
+    attach(context) {
+      const strings = {
+        show: Drupal.t('Show media item weights'),
+        hide: Drupal.t('Hide media item weights'),
+      };
+      $('.js-media-library-widget-toggle-weight', context).once('media-library-toggle')
+        .on('click', (e) => {
+          e.preventDefault();
+          $(e.currentTarget)
+            .toggleClass('active')
+            .text($(e.currentTarget).hasClass('active') ? strings.hide : strings.show)
+            .parent()
+            .find('.js-media-library-item-weight')
+            .parent()
+            .toggle();
+        })
+        .text(strings.show);
+      $('.js-media-library-item-weight', context).once('media-library-toggle').parent().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
new file mode 100644
index 000000000000..ad6fbd76e5de
--- /dev/null
+++ b/core/modules/media_library/js/media_library.widget.js
@@ -0,0 +1,67 @@
+/**
+* DO NOT EDIT THIS FILE.
+* See the following change record for more information,
+* https://www.drupal.org/node/2815083
+* @preserve
+**/
+
+(function ($, Drupal) {
+  Drupal.behaviors.MediaLibraryWidgetSortable = {
+    attach: function attach(context) {
+      $('.js-media-library-selection', context).once('media-library-sortable').sortable({
+        tolerance: 'pointer',
+        helper: 'clone',
+        handle: '.js-media-library-item-preview',
+        stop: function stop(_ref) {
+          var target = _ref.target;
+
+          $(target).children().each(function (index, child) {
+            $(child).find('.js-media-library-item-weight').val(index);
+          });
+        }
+      });
+    }
+  };
+
+  Drupal.behaviors.MediaLibraryWidgetToggleWeight = {
+    attach: function attach(context) {
+      var strings = {
+        show: Drupal.t('Show media item weights'),
+        hide: Drupal.t('Hide media item weights')
+      };
+      $('.js-media-library-widget-toggle-weight', context).once('media-library-toggle').on('click', function (e) {
+        e.preventDefault();
+        $(e.currentTarget).toggleClass('active').text($(e.currentTarget).hasClass('active') ? strings.hide : strings.show).parent().find('.js-media-library-item-weight').parent().toggle();
+      }).text(strings.show);
+      $('.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.libraries.yml b/core/modules/media_library/media_library.libraries.yml
index e32dbe074b49..848222543e4f 100644
--- a/core/modules/media_library/media_library.libraries.yml
+++ b/core/modules/media_library/media_library.libraries.yml
@@ -21,5 +21,14 @@ view:
   dependencies:
     - media_library/style
     - media_library/click_to_select
+
+widget:
+  version: VERSION
+  js:
+    js/media_library.widget.js: {}
+  dependencies:
+    - core/drupal.ajax
+    - core/jquery.ui.sortable
+    - 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 63b3cadb96a8..b72a4d47ce7c 100644
--- a/core/modules/media_library/media_library.module
+++ b/core/modules/media_library/media_library.module
@@ -5,12 +5,17 @@
  * Contains hook implementations for the media_library module.
  */
 
+use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
 use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Template\Attribute;
+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;
-use Drupal\Core\Template\Attribute;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\Element;
 
 /**
  * Implements hook_help().
@@ -43,6 +48,27 @@ function media_library_theme() {
 function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) {
   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',
+      ]));
+      // 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
+      // parameters are prefixed and should not interfere with any other views.
+      // @todo Rework or remove this in https://www.drupal.org/node/2983451
+      if (!empty($query)) {
+        $ajax_path = &$output['#attached']['drupalSettings']['views']['ajax_path'];
+        $parsed_url = UrlHelper::parse($ajax_path);
+        $query = array_merge($query, $parsed_url['query']);
+        $ajax_path = $parsed_url['path'] . '?' . UrlHelper::buildQuery($query);
+        if (isset($query['media_library_remaining'])) {
+          $output['#attached']['drupalSettings']['media_library']['selection_remaining'] = (int) $query['media_library_remaining'];
+        }
+      }
+    }
   }
 }
 
@@ -59,7 +85,7 @@ function media_library_preprocess_media(&$variables) {
       'language' => $media->language(),
     ]);
     $variables['preview_attributes'] = new Attribute();
-    $variables['preview_attributes']->addClass('media-library-item__preview', 'js-click-to-select__trigger');
+    $variables['preview_attributes']->addClass('media-library-item__preview', 'js-media-library-item-preview', 'js-click-to-select-trigger');
     $variables['metadata_attributes'] = new Attribute();
     $variables['metadata_attributes']->addClass('media-library-item__attributes');
     $variables['status'] = $media->isPublished();
@@ -74,12 +100,10 @@ function media_library_preprocess_media(&$variables) {
  * @param \Drupal\Core\Form\FormStateInterface $form_state
  *   The current state of the form.
  *
- * @todo Remove in https://www.drupal.org/project/drupal/issues/2969660
+ * @todo Remove in https://www.drupal.org/node/2983454
  */
 function media_library_form_views_form_media_library_page_alter(array &$form, FormStateInterface $form_state) {
   if (isset($form['media_bulk_form']) && isset($form['output'])) {
-    $form['#attributes']['class'][] = 'media-library-page-form';
-    $form['header']['#attributes']['class'][] = 'media-library-page-form__header';
     /** @var \Drupal\views\ViewExecutable $view */
     $view = $form['output'][0]['#view'];
     foreach (Element::getVisibleChildren($form['media_bulk_form']) as $key) {
@@ -93,6 +117,102 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
   }
 }
 
+/**
+ * Implements hook_form_alter().
+ */
+function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
+  $form_object = $form_state->getFormObject();
+  if ($form_object instanceof ViewsForm && strpos($form_object->getBaseFormId(), 'views_form_media_library') === 0) {
+    $form['#attributes']['class'][] = 'media-library-views-form';
+    if (isset($form['header'])) {
+      $form['header']['#attributes']['class'][] = 'media-library-views-form__header';
+      $form['header']['media_bulk_form']['#attributes']['class'][] = 'media-library-views-form__bulk_form';
+    }
+  }
+
+  // Add after build to fix media library views exposed filter's submit button.
+  if ($form_id === 'views_exposed_form' && $form['#id'] === 'views-exposed-form-media-library-widget') {
+    $form['#after_build'][] = '_media_library_views_form_media_library_after_build';
+  }
+}
+
+/**
+ * After build callback for views form media library.
+ */
+function _media_library_views_form_media_library_after_build(array $form, FormStateInterface $form_state) {
+  // Remove .form-actions from media library views exposed filter actions
+  // and replace with .media-library-view--form-actions.
+  //
+  // This prevents the views exposed filter's 'Apply filter' submit button from
+  // being moved into the dialog's buttons.
+  // @see \Drupal\Core\Render\Element\Actions::processActions
+  // @see Drupal.behaviors.dialog.prepareDialogButtons
+  if (($key = array_search('form-actions', $form['actions']['#attributes']['class'])) !== FALSE) {
+    unset($form['actions']['#attributes']['class'][$key]);
+  }
+  $form['actions']['#attributes']['class'][] = 'media-library-view--form-actions';
+  return $form;
+}
+
+/**
+ * Implements hook_views_query_alter().
+ *
+ * Alters the widget view's query to only show media that can be selected,
+ * based on what types are allowed in the field settings.
+ *
+ * @todo Remove in https://www.drupal.org/node/2983454
+ */
+function media_library_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
+  if ($query instanceof Sql && $view->id() === 'media_library' && $view->current_display === 'widget') {
+    $types = _media_library_get_allowed_types();
+    if ($types) {
+      $entity_type = \Drupal::entityTypeManager()->getDefinition('media');
+      $group = $query->setWhereGroup();
+      $query->addWhere($group, $entity_type->getDataTable() . '.' . $entity_type->getKey('bundle'), $types, 'in');
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Limits the types available in the exposed filter to avoid users trying to
+ * filter by a type that is un-selectable.
+ *
+ * @see media_library_views_query_alter()
+ *
+ * @todo Remove in https://www.drupal.org/node/2983454
+ */
+function media_library_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state) {
+  if (isset($form['#id']) && $form['#id'] === 'views-exposed-form-media-library-widget') {
+    $types = _media_library_get_allowed_types();
+    if ($types && isset($form['type']['#options'])) {
+      $keys = array_flip($types);
+      // Ensure that the default value (by default "All") persists.
+      if (isset($form['type']['#default_value'])) {
+        $keys[$form['type']['#default_value']] = TRUE;
+      }
+      $form['type']['#options'] = array_intersect_key($form['type']['#options'], $keys);
+    }
+  }
+}
+
+/**
+ * Implements hook_field_ui_preconfigured_options_alter().
+ */
+function media_library_field_ui_preconfigured_options_alter(array &$options, $field_type) {
+  // If the field is not an "entity_reference"-based field, bail out.
+  $class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($field_type);
+  if (!is_a($class, EntityReferenceItem::class, TRUE)) {
+    return;
+  }
+
+  // Set the default field widget for media to be the Media library.
+  if (!empty($options['media'])) {
+    $options['media']['entity_form_display']['type'] = 'media_library_widget';
+  }
+}
+
 /**
  * Implements hook_local_tasks_alter().
  *
@@ -107,3 +227,17 @@ function media_library_local_tasks_alter(&$local_tasks) {
     }
   }
 }
+
+/**
+ * Determines what types are allowed based on the current request.
+ *
+ * @return array
+ *   An array of allowed types.
+ */
+function _media_library_get_allowed_types() {
+  $types = \Drupal::request()->query->get('media_library_allowed_types');
+  if ($types && is_array($types)) {
+    return array_filter($types, 'is_string');
+  }
+  return [];
+}
diff --git a/core/modules/media_library/media_library.views.inc b/core/modules/media_library/media_library.views.inc
new file mode 100644
index 000000000000..7d1ead4c8faa
--- /dev/null
+++ b/core/modules/media_library/media_library.views.inc
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains Views integration for the media_library module.
+ */
+
+/**
+ * Implements hook_views_data().
+ */
+function media_library_views_data() {
+  $data = [];
+  $data['media']['media_library_select_form'] = [
+    'title' => t('Select media'),
+    'help' => t('Provides a field for selecting media entities in our media library view'),
+    'real field' => 'mid',
+    'field' => [
+      'id' => 'media_library_select_form',
+    ],
+  ];
+  return $data;
+}
diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
new file mode 100644
index 000000000000..86aa2e979539
--- /dev/null
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -0,0 +1,524 @@
+<?php
+
+namespace Drupal\media_library\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\SortArray;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\media\Entity\Media;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\ConstraintViolationInterface;
+
+/**
+ * Plugin implementation of the 'media_library_widget' widget.
+ *
+ * @FieldWidget(
+ *   id = "media_library_widget",
+ *   label = @Translation("Media library"),
+ *   description = @Translation("Allows you to select items from the media library."),
+ *   field_types = {
+ *     "entity_reference"
+ *   },
+ *   multiple_values = TRUE,
+ * )
+ *
+ * @internal
+ */
+class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * Entity type manager service.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * Constructs a MediaLibraryWidget widget.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the widget is associated.
+   * @param array $settings
+   *   The widget settings.
+   * @param array $third_party_settings
+   *   Any third party settings.
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   Entity type manager service.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+    $this->entityTypeManager = $entity_type_manager;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['third_party_settings'],
+      $container->get('entity_type.manager')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function isApplicable(FieldDefinitionInterface $field_definition) {
+    return $field_definition->getSetting('target_type') === 'media';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
+    // Load the items for form rebuilds from the field state.
+    $field_state = static::getWidgetState($form['#parents'], $this->fieldDefinition->getName(), $form_state);
+    if (isset($field_state['items'])) {
+      usort($field_state['items'], [SortArray::class, 'sortByWeightElement']);
+      $items->setValue($field_state['items']);
+    }
+
+    return parent::form($items, $form, $form_state, $get_delta);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
+    $referenced_entities = $items->referencedEntities();
+    $view_builder = $this->entityTypeManager->getViewBuilder('media');
+    $field_name = $this->fieldDefinition->getName();
+    $parents = $form['#parents'];
+    $id_suffix = '-' . implode('-', $parents);
+    $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix;
+    $limit_validation_errors = [array_merge($parents, [$field_name])];
+
+    $settings = $this->getFieldSetting('handler_settings');
+    $element += [
+      '#type' => 'fieldset',
+      '#cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(),
+      '#target_bundles' => isset($settings['target_bundles']) ? $settings['target_bundles'] : FALSE,
+      '#attributes' => [
+        'id' => $wrapper_id,
+        'class' => ['media-library-widget'],
+      ],
+      '#attached' => [
+        'library' => ['media_library/widget'],
+      ],
+    ];
+
+    // @todo Remove in https://www.drupal.org/project/drupal/issues/2938116
+    $allowed_bundles = !empty($element['#target_bundles']) ? $element['#target_bundles'] : [];
+    $add_url = _media_get_add_url($allowed_bundles);
+    if ($add_url) {
+      $element['create_help'] = [
+        '#type' => 'container',
+      ];
+      $element['create_help']['label'] = [
+        '#type' => 'html_tag',
+        '#tag' => 'h4',
+        '#attributes' => [
+          'class' => ['label'],
+        ],
+        '#value' => $this->t('Create new media'),
+      ];
+      $element['create_help']['description'] = [
+        '#type' => 'html_tag',
+        '#tag' => 'div',
+        '#attributes' => [
+          'class' => ['description'],
+        ],
+        '#value' => $this->t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then select it in the library.', [':add_page' => $add_url]),
+      ];
+    }
+
+    $element['selection'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => [
+          'js-media-library-selection',
+          'media-library-selection',
+        ],
+      ],
+    ];
+
+    if (empty($referenced_entities)) {
+      $element['empty_selection'] = [
+        '#markup' => $this->t('<p>No media items are selected.</p>'),
+      ];
+    }
+    else {
+      $element['weight_toggle'] = [
+        '#type' => 'html_tag',
+        '#tag' => 'button',
+        '#value' => $this->t('Show media item weights'),
+        '#attributes' => [
+          'class' => [
+            'link',
+            'media-library-widget__toggle-weight',
+            'js-media-library-widget-toggle-weight',
+          ],
+          'title' => $this->t('Re-order media by numerical weight instead of dragging'),
+        ],
+      ];
+    }
+
+    foreach ($referenced_entities as $delta => $media_item) {
+      $element['selection'][$delta] = [
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'media-library-item',
+            'js-media-library-item',
+          ],
+        ],
+        'preview' => [
+          '#type' => 'container',
+          // @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209
+          'rendered_entity' => $view_builder->view($media_item, 'media_library'),
+          'remove_button' => [
+            '#type' => 'submit',
+            '#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
+            '#value' => $this->t('Remove'),
+            '#attributes' => [
+              'class' => ['media-library-item__remove'],
+            ],
+            '#ajax' => [
+              'callback' => [static::class, 'updateWidget'],
+              'wrapper' => $wrapper_id,
+            ],
+            '#submit' => [[static::class, 'removeItem']],
+            // Prevent errors in other widgets from preventing removal.
+            '#limit_validation_errors' => $limit_validation_errors,
+          ],
+        ],
+        'target_id' => [
+          '#type' => 'hidden',
+          '#value' => $media_item->id(),
+        ],
+        // This hidden value can be toggled visible for accessibility.
+        'weight' => [
+          '#type' => 'number',
+          '#title' => $this->t('Weight'),
+          '#default_value' => $delta,
+          '#attributes' => [
+            'class' => [
+              'js-media-library-item-weight',
+              'media-library-item__weight',
+            ],
+          ],
+        ],
+      ];
+    }
+
+    $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    $remaining = $element['#cardinality'] - count($referenced_entities);
+
+    // Inform the user of how many items are remaining.
+    if (!$cardinality_unlimited) {
+      if ($remaining) {
+        $cardinality_message = $this->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.');
+      }
+      else {
+        $cardinality_message = $this->t('The maximum number of media items have been selected.');
+      }
+      $element['#description'] .= '<br />' . $cardinality_message;
+    }
+
+    // Add a button that will load the Media library in a modal using AJAX.
+    $element['media_library_open_button'] = [
+      '#type' => 'link',
+      '#title' => $this->t('Browse media'),
+      '#name' => $field_name . '-media-library-open-button' . $id_suffix,
+      // @todo Make the view configurable in https://www.drupal.org/project/drupal/issues/2971209
+      '#url' => Url::fromRoute('view.media_library.widget', [], [
+        'query' => [
+          'media_library_widget_id' => $field_name . $id_suffix,
+          'media_library_allowed_types' => $element['#target_bundles'],
+          'media_library_remaining' => $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining,
+        ],
+      ]),
+      '#attributes' => [
+        'class' => ['button', 'use-ajax', 'media-library-open-button'],
+        'data-dialog-type' => 'modal',
+        'data-dialog-options' => Json::encode([
+          'dialogClass' => 'media-library-widget-modal',
+          'height' => '75%',
+          'width' => '75%',
+          'title' => $this->t('Media library'),
+        ]),
+      ],
+      // Prevent errors in other widgets from preventing addition.
+      '#limit_validation_errors' => $limit_validation_errors,
+      '#access' => $cardinality_unlimited || $remaining > 0,
+    ];
+
+    // This hidden field and button are used to add new items to the widget.
+    $element['media_library_selection'] = [
+      '#type' => 'hidden',
+      '#attributes' => [
+        // This is used to pass the selection from the modal to the widget.
+        'data-media-library-widget-value' => $field_name . $id_suffix,
+      ],
+    ];
+
+    // When a selection is made this hidden button is pressed to add new media
+    // items based on the "media_library_selection" value.
+    $element['media_library_update_widget'] = [
+      '#type' => 'submit',
+      '#value' => $this->t('Update widget'),
+      '#name' => $field_name . '-media-library-update' . $id_suffix,
+      '#ajax' => [
+        'callback' => [static::class, 'updateWidget'],
+        'wrapper' => $wrapper_id,
+      ],
+      '#attributes' => [
+        'data-media-library-widget-update' => $field_name . $id_suffix,
+        'class' => ['js-hide'],
+      ],
+      '#validate' => [[static::class, 'validateItems']],
+      '#submit' => [[static::class, 'updateItems']],
+      // Prevent errors in other widgets from preventing updates.
+      '#limit_validation_errors' => $limit_validation_errors,
+    ];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
+    return isset($element['target_id']) ? $element['target_id'] : FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    if (isset($values['selection'])) {
+      usort($values['selection'], [SortArray::class, 'sortByWeightElement']);
+      return $values['selection'];
+    }
+    return [];
+  }
+
+  /**
+   * AJAX callback to update the widget when the selection changes.
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   An array representing the updated widget.
+   */
+  public static function updateWidget(array $form, FormStateInterface $form_state) {
+    $triggering_element = $form_state->getTriggeringElement();
+    // This callback is either invoked from the remove button or the update
+    // button, which have different nesting levels.
+    $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
+    if (count($triggering_element['#array_parents']) < abs($length)) {
+      throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
+    }
+    $parents = array_slice($triggering_element['#array_parents'], 0, $length);
+    $element = NestedArray::getValue($form, $parents);
+    // Always clear the textfield selection to prevent duplicate additions.
+    $element['media_library_selection']['#value'] = '';
+    return $element;
+  }
+
+  /**
+   * Submit callback for remove buttons.
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public static function removeItem(array $form, FormStateInterface $form_state) {
+    $triggering_element = $form_state->getTriggeringElement();
+
+    // Get the parents required to find the top-level widget element.
+    if (count($triggering_element['#array_parents']) < 4) {
+      throw new \LogicException('Expected the remove button to be more than four levels deep in the form. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
+    }
+    $parents = array_slice($triggering_element['#array_parents'], 0, -4);
+    // Get the delta of the item being removed.
+    $delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
+    $element = NestedArray::getValue($form, $parents);
+
+    // Get the field state.
+    $path = $element['#parents'];
+    $values = NestedArray::getValue($form_state->getValues(), $path);
+    $field_state = static::getFieldState($element, $form_state);
+
+    // Remove the item from the field state and update it.
+    if (isset($values['selection'][$delta])) {
+      array_splice($values['selection'], $delta, 1);
+      $field_state['items'] = $values['selection'];
+      static::setFieldState($element, $form_state, $field_state);
+    }
+
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Validates that newly selected items can be added to the widget.
+   *
+   * Making an invalid selection from the view should not be possible, but we
+   * still validate in case other selection methods (ex: upload) are valid.
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public static function validateItems(array $form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+
+    $field_state = static::getFieldState($element, $form_state);
+    $media = static::getNewMediaItems($element, $form_state);
+    if (empty($media)) {
+      return;
+    }
+
+    // Check if more items were selected than we allow.
+    $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+    $selection = count($field_state['items']) + count($media);
+    if (!$cardinality_unlimited && ($selection > $element['#cardinality'])) {
+      $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'Only one item can be selected.', 'Only @count items can be selected.'));
+    }
+
+    // Validate that each selected media is of an allowed bundle.
+    $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
+    $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
+      return $all_bundles[$bundle]['label'];
+    }, $element['#target_bundles']);
+    foreach ($media as $media_item) {
+      if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) {
+        $form_state->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
+          '@label' => $media_item->label(),
+          '@types' => implode(', ', $bundle_labels),
+        ]));
+      }
+    }
+  }
+
+  /**
+   * Updates the field state and flags the form for rebuild.
+   *
+   * @param array $form
+   *   The form array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public static function updateItems(array $form, FormStateInterface $form_state) {
+    $button = $form_state->getTriggeringElement();
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+
+    $field_state = static::getFieldState($element, $form_state);
+
+    $media = static::getNewMediaItems($element, $form_state);
+    if (!empty($media)) {
+      $weight = count($field_state['items']);
+      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++,
+          ];
+        }
+      }
+      static::setFieldState($element, $form_state, $field_state);
+    }
+
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Gets newly selected media items.
+   *
+   * @param array $element
+   *   The wrapping element for this widget.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\media\MediaInterface[]
+   *   An array of selected media items.
+   */
+  protected static function getNewMediaItems(array $element, FormStateInterface $form_state) {
+    // Get the new media IDs passed to our hidden button.
+    $values = $form_state->getValues();
+    $path = $element['#parents'];
+    $value = NestedArray::getValue($values, $path);
+
+    if (!empty($value['media_library_selection'])) {
+      $ids = explode(',', $value['media_library_selection']);
+      $ids = array_filter($ids, 'is_numeric');
+      if (!empty($ids)) {
+        /** @var \Drupal\media\MediaInterface[] $media */
+        return Media::loadMultiple($ids);
+      }
+    }
+    return [];
+  }
+
+  /**
+   * Gets the field state for the widget.
+   *
+   * @param array $element
+   *   The wrapping element for this widget.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return array[]
+   *   An array of arrays with the following key/value pairs:
+   *   - items: (array) An array of selections.
+   *     - target_id: (int) A media entity ID.
+   *     - weight: (int) A weight for the selection.
+   */
+  protected static function getFieldState(array $element, FormStateInterface $form_state) {
+    $widget_state = static::getWidgetState($element['#field_parents'], $element['#field_name'], $form_state);
+    $widget_state['items'] = isset($widget_state['items']) ? $widget_state['items'] : [];
+    return $widget_state;
+  }
+
+  /**
+   * Sets the field state for the widget.
+   *
+   * @param array $element
+   *   The wrapping element for this widget.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   * @param array[] $field_state
+   *   An array of arrays with the following key/value pairs:
+   *   - items: (array) An array of selections.
+   *     - target_id: (int) A media entity ID.
+   *     - weight: (int) A weight for the selection.
+   */
+  protected static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
+    static::setWidgetState($element['#field_parents'], $element['#field_name'], $form_state, $field_state);
+  }
+
+}
diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
new file mode 100644
index 000000000000..72a4b50bbfcb
--- /dev/null
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\media_library\Plugin\views\field;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\InvokeCommand;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+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.
+ *
+ * @ViewsField("media_library_select_form")
+ *
+ * @internal
+ */
+class MediaLibrarySelectForm extends FieldPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue(ResultRow $row, $field = NULL) {
+    return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render(ResultRow $values) {
+    return ViewsRenderPipelineMarkup::create($this->getValue($values));
+  }
+
+  /**
+   * Form constructor for the media library select form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function viewsForm(array &$form, FormStateInterface $form_state) {
+    // Only add the bulk form options and buttons if there are results.
+    if (empty($this->view->result)) {
+      return;
+    }
+
+    // Render checkboxes for all rows.
+    $form[$this->options['id']]['#tree'] = TRUE;
+    foreach ($this->view->result as $row_index => $row) {
+      $entity = $this->getEntity($row);
+      $form[$this->options['id']][$row_index] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Select @label', [
+          '@label' => $entity->label(),
+        ]),
+        '#title_display' => 'invisible',
+        '#return_value' => $entity->id(),
+      ];
+    }
+
+    // @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
+    // AJAX path like /views/ajax, which cannot process AJAX form submits.
+    $url = parse_url($form['#action'], PHP_URL_PATH);
+    $query = \Drupal::request()->query->all();
+    $query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+    $form['actions']['submit']['#ajax'] = [
+      'url' => Url::fromUserInput($url),
+      'options' => [
+        'query' => $query,
+      ],
+      'callback' => [static::class, 'updateWidget'],
+    ];
+
+    $form['actions']['submit']['#value'] = $this->t('Select media');
+    $form['actions']['submit']['#field_id'] = $this->options['id'];
+  }
+
+  /**
+   * Submit handler for the media library select form.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   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());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewsFormValidate(array &$form, FormStateInterface $form_state) {
+    $selected = array_filter($form_state->getValue($this->options['id']));
+    if (empty($selected)) {
+      $form_state->setErrorByName('', $this->t('No items selected.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function clickSortable() {
+    return FALSE;
+  }
+
+}
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
new file mode 100644
index 000000000000..071486373f51
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
@@ -0,0 +1,71 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.basic_page.field_twin_media
+    - field.field.node.basic_page.field_unlimited_media
+    - node.type.basic_page
+  module:
+    - media_library
+id: node.basic_page.default
+targetEntityType: node
+bundle: basic_page
+mode: default
+content:
+  created:
+    type: datetime_timestamp
+    weight: 10
+    region: content
+    settings: {  }
+    third_party_settings: {  }
+  field_twin_media:
+    type: media_library_widget
+    weight: 122
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+  field_unlimited_media:
+    type: media_library_widget
+    weight: 121
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+  promote:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 15
+    region: content
+    third_party_settings: {  }
+  status:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 120
+    region: content
+    third_party_settings: {  }
+  sticky:
+    type: boolean_checkbox
+    settings:
+      display_label: true
+    weight: 16
+    region: content
+    third_party_settings: {  }
+  title:
+    type: string_textfield
+    weight: -5
+    region: content
+    settings:
+      size: 60
+      placeholder: ''
+    third_party_settings: {  }
+  uid:
+    type: entity_reference_autocomplete
+    weight: 5
+    settings:
+      match_operator: CONTAINS
+      size: 60
+      placeholder: ''
+    region: content
+    third_party_settings: {  }
+hidden: {  }
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
new file mode 100644
index 000000000000..b747e2ae8478
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
@@ -0,0 +1,38 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.field.node.basic_page.field_twin_media
+    - field.field.node.basic_page.field_unlimited_media
+    - node.type.basic_page
+  module:
+    - user
+id: node.basic_page.default
+targetEntityType: node
+bundle: basic_page
+mode: default
+content:
+  field_twin_media:
+    type: entity_reference_entity_view
+    weight: 102
+    label: above
+    settings:
+      view_mode: default
+      link: false
+    third_party_settings: {  }
+    region: content
+  field_unlimited_media:
+    type: entity_reference_entity_view
+    weight: 101
+    label: above
+    settings:
+      view_mode: default
+      link: false
+    third_party_settings: {  }
+    region: content
+  links:
+    weight: 100
+    settings: {  }
+    third_party_settings: {  }
+    region: content
+hidden: {  }
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml
new file mode 100644
index 000000000000..7d4d8cdc0a01
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml
@@ -0,0 +1,29 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.field_twin_media
+    - media.type.type_one
+    - media.type.type_two
+    - node.type.basic_page
+id: node.basic_page.field_twin_media
+field_name: field_twin_media
+entity_type: node
+bundle: basic_page
+label: 'Twin media'
+description: ''
+required: false
+translatable: false
+default_value: {  }
+default_value_callback: ''
+settings:
+  handler: 'default:media'
+  handler_settings:
+    target_bundles:
+      type_one: type_one
+      type_two: type_two
+    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.field.node.basic_page.field_unlimited_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml
new file mode 100644
index 000000000000..c1564e15c50a
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml
@@ -0,0 +1,27 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - field.storage.node.field_unlimited_media
+    - media.type.type_one
+    - node.type.basic_page
+id: node.basic_page.field_unlimited_media
+field_name: field_unlimited_media
+entity_type: node
+bundle: basic_page
+label: 'Unlimited media'
+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: audio
+field_type: entity_reference
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml
new file mode 100644
index 000000000000..c7b38e6a6568
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+    - node
+id: node.field_twin_media
+field_name: field_twin_media
+entity_type: node
+type: entity_reference
+settings:
+  target_type: media
+module: core
+locked: false
+cardinality: 2
+translatable: true
+indexes: {  }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml
new file mode 100644
index 000000000000..a2391d2a10fa
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+    - node
+id: node.field_unlimited_media
+field_name: field_unlimited_media
+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/modules/media_library_test/config/install/node.type.basic_page.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml
new file mode 100644
index 000000000000..23abb54599c6
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+name: 'Basic Page'
+type: basic_page
+description: ''
+help: ''
+new_revision: true
+preview_mode: 1
+display_submitted: true
diff --git a/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml b/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml
index 3ad1ae5f46ea..41979c8eed14 100644
--- a/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml
+++ b/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml
@@ -7,4 +7,5 @@ dependencies:
   - drupal:media_library
   - drupal:media_test_source
   - drupal:menu_ui
+  - drupal:node
   - drupal:path
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index f7852a4fb7bd..ab6e8a55f8a1 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -4,6 +4,8 @@
 
 use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
 use Drupal\media\Entity\Media;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
 
 /**
  * Contains Media library integration tests.
@@ -26,20 +28,26 @@ protected function setUp() {
     // Create a few example media items for use in selection.
     $media = [
       'type_one' => [
-        'media_1',
-        'media_2',
+        'Horse',
+        'Bear',
+        'Cat',
+        'Dog',
       ],
       'type_two' => [
-        'media_3',
-        'media_4',
+        'Crocodile',
+        'Lizard',
+        'Snake',
+        'Turtle',
       ],
     ];
 
+    $time = time();
     foreach ($media as $type => $names) {
       foreach ($names as $name) {
         $entity = Media::create(['name' => $name, 'bundle' => $type]);
         $source_field = $type === 'type_one' ? 'field_media_test' : 'field_media_test_1';
-        $entity->set($source_field, $this->randomString());
+        $entity->setCreatedTime(++$time);
+        $entity->set($source_field, $name);
         $entity->save();
       }
     }
@@ -47,7 +55,10 @@ protected function setUp() {
     // Create a user who can use the Media library.
     $user = $this->drupalCreateUser([
       'access administration pages',
+      'access content',
       'access media overview',
+      'edit own basic_page content',
+      'create basic_page content',
       'create media',
       'delete any media',
       'view media',
@@ -72,33 +83,33 @@ public function testAdministrationPage() {
     $assert_session->linkExists('Add media');
 
     // Verify that media from two separate types is present.
-    $assert_session->pageTextContains('media_1');
-    $assert_session->pageTextContains('media_3');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextContains('Turtle');
 
     // Test that users can filter by type.
     $page->selectFieldOption('Media type', 'Type One');
     $page->pressButton('Apply Filters');
     $assert_session->assertWaitOnAjaxRequest();
-    $assert_session->pageTextContains('media_2');
-    $assert_session->pageTextNotContains('media_4');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Turtle');
     $page->selectFieldOption('Media type', 'Type Two');
     $page->pressButton('Apply Filters');
     $assert_session->assertWaitOnAjaxRequest();
-    $assert_session->pageTextNotContains('media_2');
-    $assert_session->pageTextContains('media_4');
+    $assert_session->pageTextNotContains('Dog');
+    $assert_session->pageTextContains('Turtle');
 
     // Test that selecting elements as a part of bulk operations works.
     $page->selectFieldOption('Media type', '- Any -');
     $page->pressButton('Apply Filters');
     $assert_session->assertWaitOnAjaxRequest();
     // This tests that anchor tags clicked inside the preview are suppressed.
-    $this->getSession()->executeScript('jQuery(".js-click-to-select__trigger a")[0].click()');
+    $this->getSession()->executeScript('jQuery(".js-click-to-select-trigger a")[0].click()');
     $this->submitForm([], 'Apply to selected items');
-    $assert_session->pageTextContains('media_1');
-    $assert_session->pageTextNotContains('media_2');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextNotContains('Cat');
     $this->submitForm([], 'Delete');
-    $assert_session->pageTextNotContains('media_1');
-    $assert_session->pageTextContains('media_2');
+    $assert_session->pageTextNotContains('Dog');
+    $assert_session->pageTextContains('Cat');
 
     // Test 'Select all media'.
     $this->getSession()->getPage()->checkField('Select all media');
@@ -106,9 +117,9 @@ public function testAdministrationPage() {
     $this->submitForm([], 'Apply to selected items');
     $this->getSession()->getPage()->pressButton('Delete');
 
-    $assert_session->pageTextNotContains('media_2');
-    $assert_session->pageTextNotContains('media_3');
-    $assert_session->pageTextNotContains('media_4');
+    $assert_session->pageTextNotContains('Cat');
+    $assert_session->pageTextNotContains('Turtle');
+    $assert_session->pageTextNotContains('Snake');
 
     // Test empty text.
     $assert_session->pageTextContains('No media available.');
@@ -121,4 +132,158 @@ public function testAdministrationPage() {
     $assert_session->linkExists('Add media');
   }
 
+  /**
+   * Tests that the Media library's widget works as expected.
+   */
+  public function testWidget() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Visit a node create page.
+    $this->drupalGet('node/add/basic_page');
+
+    // Verify that both media widget instances are present.
+    $assert_session->pageTextContains('Unlimited media');
+    $assert_session->pageTextContains('Twin media');
+
+    // 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->assertWaitOnAjaxRequest();
+    // Assert that only type_one media items exist, since this field only
+    // accepts items of that type.
+    $assert_session->pageTextContains('Media library');
+    $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.
+    $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);
+    $checkboxes[0]->click();
+    $checkboxes[1]->click();
+    $checkboxes[2]->click();
+    $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->elementExists('css', '.media-library-item__remove')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextNotContains('Dog');
+    $assert_session->pageTextContains('Cat');
+    $assert_session->pageTextContains('Bear');
+
+    // 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();
+    $assert_session->assertWaitOnAjaxRequest();
+    // This field allows both media types.
+    $assert_session->pageTextContains('Media library');
+    $assert_session->pageTextContains('Dog');
+    $assert_session->pageTextContains('Turtle');
+    // Attempt to select three items - the cardinality of this field is two so
+    // the third selection should be disabled.
+    $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
+    $checkboxes = $page->findAll('css', $checkbox_selector);
+    $this->assertFalse($checkboxes[5]->hasAttribute('disabled'));
+    $checkboxes[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('Turtle');
+    $assert_session->pageTextNotContains('Snake');
+
+    // 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',
+    ], '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('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")');
+    // Make sure everything that was selected shows up.
+    $assert_session->pageTextContains('Cat');
+    $assert_session->pageTextContains('Bear');
+    $assert_session->pageTextContains('Horse');
+    $assert_session->pageTextContains('Turtle');
+  }
+
+  /**
+   * Tests that the widget works as expected for anonymous users.
+   */
+  public function testWidgetAnonymous() {
+    $assert_session = $this->assertSession();
+
+    $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.
+    $this->grantPermissions($role, [
+      'access content',
+      'create basic_page content',
+      'view media',
+    ]);
+
+    // Ensure the widget works as an anonymous user.
+    $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->assertWaitOnAjaxRequest();
+
+    // Select the first media item (should be Dog).
+    $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
+    $checkboxes = $this->getSession()->getPage()->findAll('css', $checkbox_selector);
+    $checkboxes[0]->click();
+    $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');
+
+    // 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]' => '0',
+    ], 'Save');
+    $assert_session->pageTextContains('Basic Page My page has been created');
+    $assert_session->pageTextContains('Dog');
+  }
+
 }
-- 
GitLab