diff --git a/core/modules/media/src/Annotation/MediaSource.php b/core/modules/media/src/Annotation/MediaSource.php
index 2f7bdad23d400bda035e781fe0d5f44f92e2e253..1286601b2967d3d55fac2c05f6babd711d208e5c 100644
--- a/core/modules/media/src/Annotation/MediaSource.php
+++ b/core/modules/media/src/Annotation/MediaSource.php
@@ -57,6 +57,16 @@ class MediaSource extends Plugin {
    */
   public $allowed_field_types = [];
 
+  /**
+   * The classes used to define media source-specific forms.
+   *
+   * An array of form class names, keyed by ID. The ID represents the operation
+   * the form is used for.
+   *
+   * @var string[]
+   */
+  public $forms = [];
+
   /**
    * A filename for the default thumbnail.
    *
diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 61be1a750795b29a6652f1ff4ef28adaf3fc2e62..e5ca9ab6d5d7be50035e8f38fceb5429799c7dbd 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -83,6 +83,19 @@
   border-left: 0;
 }
 
+.media-library-add-form--without-input {
+  margin-bottom: 1em;
+  border-bottom: 1px solid #c0c0c0;
+}
+
+.media-library-add-form--without-input .form-item {
+  margin: 0 0 1em;
+}
+
+.media-library-add-form .file-upload-help {
+  margin: 8px 0 0;
+}
+
 .media-library-views-form__header .form-item {
   margin-right: 8px;
 }
@@ -276,31 +289,34 @@
   border-color: #40b6ff;
 }
 
-/* Style the wrappers around new media and files */
-.media-library-upload__media,
-.media-library-upload__file {
+/* Style the wrappers around new media and files. */
+.media-library-add-form__media {
   display: flex;
   padding: 20px 0 20px 0;
+  border-bottom: 1px solid #c0c0c0;
 }
 
-.media-library-upload__file {
-  align-items: center;
+/* Do not show the top padding for the first item. */
+.media-library-add-form__media:first-child {
+  padding-top: 0;
 }
 
-.media-library-upload__file-label {
-  margin-right: 10px;
+/* Do not show the bottom border and padding for the last item. */
+.media-library-add-form__media:last-child {
+  border-bottom: 0;
+  padding-bottom: 0;
 }
 
 /* @todo Remove in https://www.drupal.org/project/drupal/issues/2987921 */
-.media-library-upload__source-field .file,
-.media-library-upload__source-field .button,
-.media-library-upload__source-field .image-preview,
-.media-library-upload__source-field .form-type-managed-file > label,
-.media-library-upload__source-field .file-size {
+.media-library-add-form__source-field .file,
+.media-library-add-form__source-field .button,
+.media-library-add-form__source-field .image-preview,
+.media-library-add-form__source-field .form-type-managed-file > label,
+.media-library-add-form__source-field .file-size {
   display: none;
 }
 
-.media-library-upload__media-preview {
+.media-library-add-form__preview {
   display: flex;
   justify-content: center;
   align-items: center;
@@ -308,15 +324,11 @@
   margin-right: 20px;
   background: #ebebeb;
 }
-[dir="rtl"] .media-library-upload__media-preview {
+[dir="rtl"] .media-library-add-form__preview {
   margin-right: 0;
   margin-left: 20px;
 }
 
-.media-library-upload__media-preview img {
-  display: block;
-}
-
 /* @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 {
diff --git a/core/modules/media_library/js/media_library.ui.es6.js b/core/modules/media_library/js/media_library.ui.es6.js
index d0aff3baf95cca71ffd6ba1f638f3920330104e5..f0676d10136d8c483c5411f017c09ed73a9e1ea2 100644
--- a/core/modules/media_library/js/media_library.ui.es6.js
+++ b/core/modules/media_library/js/media_library.ui.es6.js
@@ -15,6 +15,26 @@
     currentSelection: [],
   };
 
+  /**
+   * Command to update the current media library selection.
+   *
+   * @param {Drupal.Ajax} [ajax]
+   *   The Drupal Ajax object.
+   * @param {object} response
+   *   Object holding the server response.
+   * @param {number} [status]
+   *   The HTTP status code.
+   */
+  Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function(
+    ajax,
+    response,
+    status,
+  ) {
+    Object.values(response.mediaIds).forEach(value => {
+      Drupal.MediaLibrary.currentSelection.push(value);
+    });
+  };
+
   /**
    * Warn users when clicking outgoing links from the library or widget.
    *
diff --git a/core/modules/media_library/js/media_library.ui.js b/core/modules/media_library/js/media_library.ui.js
index 163f989bdca8930b714ba163eaeba2d9c7cff5c6..4d3e91693459a865a537b9476b88aaa4d8f6957a 100644
--- a/core/modules/media_library/js/media_library.ui.js
+++ b/core/modules/media_library/js/media_library.ui.js
@@ -10,6 +10,12 @@
     currentSelection: []
   };
 
+  Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function (ajax, response, status) {
+    Object.values(response.mediaIds).forEach(function (value) {
+      Drupal.MediaLibrary.currentSelection.push(value);
+    });
+  };
+
   Drupal.behaviors.MediaLibraryWidgetWarn = {
     attach: function attach(context) {
       $('.js-media-library-item a[href]', context).once('media-library-warn-link').on('click', function (e) {
diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module
index b5d5d1d3c7610238e7a2e65d96d4b2585f9c1005..6533258899b7f720d19196073c36f2251c54d6f9 100644
--- a/core/modules/media_library/media_library.module
+++ b/core/modules/media_library/media_library.module
@@ -5,7 +5,6 @@
  * Contains hook implementations for the media_library module.
  */
 
-use Drupal\Component\Serialization\Json;
 use Drupal\Component\Utility\UrlHelper;
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Entity\EntityInterface;
@@ -17,11 +16,11 @@
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Template\Attribute;
-use Drupal\Core\Url;
 use Drupal\image\Entity\ImageStyle;
 use Drupal\image\Plugin\Field\FieldType\ImageItem;
 use Drupal\media\MediaTypeForm;
 use Drupal\media\MediaTypeInterface;
+use Drupal\media_library\Form\FileUploadForm;
 use Drupal\media_library\MediaLibraryState;
 use Drupal\views\Form\ViewsForm;
 use Drupal\views\Plugin\views\cache\CachePluginBase;
@@ -41,6 +40,16 @@ function media_library_help($route_name, RouteMatchInterface $route_match) {
   }
 }
 
+/**
+ * Implements hook_media_source_info_alter().
+ */
+function media_library_media_source_info_alter(array &$sources) {
+  $sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class;
+  $sources['file']['forms']['media_library_add'] = FileUploadForm::class;
+  $sources['image']['forms']['media_library_add'] = FileUploadForm::class;
+  $sources['video_file']['forms']['media_library_add'] = FileUploadForm::class;
+}
+
 /**
  * Implements hook_theme().
  */
@@ -52,36 +61,6 @@ function media_library_theme() {
   ];
 }
 
-/**
- * Implements hook_preprocess_view().
- *
- * Adds a link to add media above the view.
- */
-function media_library_preprocess_views_view(&$variables) {
-  $view = $variables['view'];
-  if ($view->id() === 'media_library' && $view->current_display === 'widget') {
-    $url = Url::fromRoute('media_library.upload');
-    if ($url->access()) {
-      $url->setOption('query', \Drupal::request()->query->all());
-      $variables['header']['add_media'] = [
-        '#type' => 'link',
-        '#title' => t('Add media'),
-        '#url' => $url,
-        '#attributes' => [
-          'class' => ['button', 'button-action', 'button--primary', 'use-ajax'],
-          'data-dialog-type' => 'modal',
-          'data-dialog-options' => Json::encode([
-            'dialogClass' => 'media-library-widget-modal',
-            'height' => '75%',
-            'width' => '75%',
-            'title' => t('Add media'),
-          ]),
-        ],
-      ];
-    }
-  }
-}
-
 /**
  * Implements hook_views_post_render().
  */
diff --git a/core/modules/media_library/media_library.routing.yml b/core/modules/media_library/media_library.routing.yml
index 8f0fb5f87e12365a34ac59d24d560ee0ddf753cc..efc9836e4702586322931ebedc2edf34c12ea325 100644
--- a/core/modules/media_library/media_library.routing.yml
+++ b/core/modules/media_library/media_library.routing.yml
@@ -1,9 +1,3 @@
-media_library.upload:
-  path: '/admin/content/media-widget-upload'
-  defaults:
-    _form: '\Drupal\media_library\Form\MediaLibraryUploadForm'
-  requirements:
-    _custom_access: '\Drupal\media_library\Form\MediaLibraryUploadForm::access'
 media_library.ui:
   path: '/media-library'
   defaults:
diff --git a/core/modules/media_library/media_library.services.yml b/core/modules/media_library/media_library.services.yml
index 9550f5190b987f942210ac0914fbbbae3c96b688..acd28df2493e013a74a5352834b726c7d052e863 100644
--- a/core/modules/media_library/media_library.services.yml
+++ b/core/modules/media_library/media_library.services.yml
@@ -1,4 +1,4 @@
 services:
   media_library.ui_builder:
     class: Drupal\media_library\MediaLibraryUiBuilder
-    arguments: ['@entity_type.manager', '@request_stack', '@views.executable']
+    arguments: ['@entity_type.manager', '@request_stack', '@views.executable', '@form_builder']
diff --git a/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php b/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php
new file mode 100644
index 0000000000000000000000000000000000000000..c9f329ce6bccbb9d0178599c7fcb149bc7e93ce0
--- /dev/null
+++ b/core/modules/media_library/src/Ajax/UpdateSelectionCommand.php
@@ -0,0 +1,54 @@
+<?php
+
+namespace Drupal\media_library\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * AJAX command for adding media items to the media library selection.
+ *
+ * This command instructs the client to add the given media item IDs to the
+ * current selection of the media library stored in
+ * Drupal.MediaLibrary.currentSelection.
+ *
+ * This command is implemented by
+ * Drupal.AjaxCommands.prototype.updateMediaLibrarySelection() defined in
+ * media_library.ui.js.
+ *
+ * @ingroup ajax
+ *
+ * @internal
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
+ */
+class UpdateSelectionCommand implements CommandInterface {
+
+  /**
+   * An array of media IDs to add to the current selection.
+   *
+   * @var int[]
+   */
+  protected $mediaIds;
+
+  /**
+   * Constructs an UpdateSelectionCommand object.
+   *
+   * @param int[] $media_ids
+   *   An array of media IDs to add to the current selection.
+   */
+  public function __construct(array $media_ids) {
+    $this->mediaIds = $media_ids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return [
+      'command' => 'updateMediaLibrarySelection',
+      'mediaIds' => $this->mediaIds,
+    ];
+  }
+
+}
diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php
new file mode 100644
index 0000000000000000000000000000000000000000..da783d36ab0dfbb244d1755014f082a2553e94ce
--- /dev/null
+++ b/core/modules/media_library/src/Form/AddFormBase.php
@@ -0,0 +1,447 @@
+<?php
+
+namespace Drupal\media_library\Form;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\ReplaceCommand;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaTypeInterface;
+use Drupal\media_library\Ajax\UpdateSelectionCommand;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\media_library\MediaLibraryUiBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a base class for creating media items from within the media library.
+ *
+ * @internal
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
+ */
+abstract class AddFormBase extends FormBase {
+
+  /**
+   * The entity type manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * The media library UI builder.
+   *
+   * @var \Drupal\media_library\MediaLibraryUiBuilder
+   */
+  protected $libraryUiBuilder;
+
+  /**
+   * The type of media items being created by this form.
+   *
+   * @var \Drupal\media\MediaTypeInterface
+   */
+  protected $mediaType;
+
+  /**
+   * Constructs a AddFormBase object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
+   *   The media library UI builder.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder) {
+    $this->entityTypeManager = $entity_type_manager;
+    $this->libraryUiBuilder = $library_ui_builder;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('media_library.ui_builder')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'media_library_add_form';
+  }
+
+  /**
+   * Get the media type from the form state.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return \Drupal\media\MediaTypeInterface
+   *   The media type.
+   */
+  protected function getMediaType(FormStateInterface $form_state) {
+    if ($this->mediaType) {
+      return $this->mediaType;
+    }
+
+    $state = $form_state->get('media_library_state');
+
+    if (!$state) {
+      throw new \InvalidArgumentException('The media library state is not present in the form state.');
+    }
+
+    $selected_type_id = $form_state->get('media_library_state')->getSelectedTypeId();
+    $this->mediaType = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
+
+    if (!$this->mediaType) {
+      throw new \InvalidArgumentException("The '$selected_type_id' media type does not exist.");
+    }
+
+    return $this->mediaType;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form['#prefix'] = '<div id="media-library-add-form-wrapper">';
+    $form['#suffix'] = '</div>';
+    $form['#attached']['library'][] = 'media_library/style';
+
+    // The form is posted via AJAX. When there are messages set during the
+    // validation or submission of the form, the messages need to be shown to
+    // the user.
+    $form['status_messages'] = [
+      '#type' => 'status_messages',
+    ];
+
+    $form['#attributes']['class'][] = 'media-library-add-form';
+    $added_media = $form_state->get('media');
+    if (empty($added_media)) {
+      $form['#attributes']['class'][] = 'media-library-add-form--without-input';
+      $form = $this->buildInputElement($form, $form_state);
+    }
+    else {
+      $form['#attributes']['class'][] = 'media-library-add-form--with-input';
+
+      $form['media'] = [
+        '#type' => 'container',
+      ];
+
+      foreach ($added_media as $delta => $media) {
+        $form['media'][$delta] = $this->buildEntityFormElement($media, $form, $form_state, $delta);
+      }
+
+      $form['actions'] = $this->buildActions($form, $form_state);
+    }
+    return $form;
+  }
+
+  /**
+   * Builds the element for submitting source field value(s).
+   *
+   * The input element needs to have a submit handler to create media items from
+   * the user input and store them in the form state using
+   * ::processInputValues().
+   *
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return array
+   *   The complete form, with the element added.
+   *
+   * @see ::processInputValues()
+   */
+  abstract protected function buildInputElement(array $form, FormStateInterface $form_state);
+
+  /**
+   * Builds the sub-form for setting required fields on a new media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   A new, unsaved media item.
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   * @param int $delta
+   *   The delta of the media item.
+   *
+   * @return array
+   *   The element containing the required fields sub-form.
+   */
+  protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
+    $element = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => [
+          'media-library-add-form__media',
+        ],
+      ],
+      'preview' => [
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'media-library-add-form__preview',
+          ],
+        ],
+      ],
+      'fields' => [
+        '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'media-library-add-form__fields',
+          ],
+        ],
+        // The '#parents' are set here because the entity form display needs it
+        // to build the entity form fields.
+        '#parents' => ['media', $delta, 'fields'],
+      ],
+    ];
+    // @todo Make the image style configurable in
+    //   https://www.drupal.org/node/2988223
+    $source = $media->getSource();
+    $plugin_definition = $source->getPluginDefinition();
+    if ($thumbnail_uri = $source->getMetadata($media, $plugin_definition['thumbnail_uri_metadata_attribute'])) {
+      $element['preview']['thumbnail'] = [
+        '#theme' => 'image_style',
+        '#style_name' => 'media_library',
+        '#uri' => $thumbnail_uri,
+      ];
+    }
+
+    $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
+    // When the name is not added to the form as an editable field, output
+    // the name as a fixed element to confirm the right file was uploaded.
+    if (!$form_display->getComponent('name')) {
+      $element['fields']['name'] = [
+        '#type' => 'item',
+        '#title' => $this->t('Name'),
+        '#markup' => $media->getName(),
+      ];
+    }
+    $form_display->buildForm($media, $element['fields'], $form_state);
+
+    // We hide the preview of the uploaded file in the image widget with CSS.
+    // @todo Improve hiding file widget elements in
+    //   https://www.drupal.org/project/drupal/issues/2987921
+    $source_field_name = $this->getSourceFieldName($media->bundle->entity);
+    if (isset($element['fields'][$source_field_name])) {
+      $element['fields'][$source_field_name]['#attributes']['class'][] = 'media-library-add-form__source-field';
+    }
+    // The revision log field is currently not configurable from the form
+    // display, so hide it by changing the access.
+    // @todo Make the revision_log_message field configurable in
+    //   https://www.drupal.org/project/drupal/issues/2696555
+    if (isset($element['fields']['revision_log_message'])) {
+      $element['fields']['revision_log_message']['#access'] = FALSE;
+    }
+    return $element;
+  }
+
+  /**
+   * Returns an array of supported actions for the form.
+   *
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return array
+   *   An actions element containing the actions of the form.
+   */
+  protected function buildActions(array $form, FormStateInterface $form_state) {
+    return [
+      '#type' => 'actions',
+      'submit' => [
+        '#type' => 'submit',
+        '#value' => $this->t('Save'),
+        '#ajax' => [
+          'callback' => '::updateWidget',
+          'wrapper' => 'media-library-add-form-wrapper',
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * Creates media items from source field input values.
+   *
+   * @param mixed[] $source_field_values
+   *   The values for source fields of the media items.
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   */
+  protected function processInputValues(array $source_field_values, array $form, FormStateInterface $form_state) {
+    $media_type = $this->getMediaType($form_state);
+    $media_storage = $this->entityTypeManager->getStorage('media');
+    $source_field_name = $this->getSourceFieldName($media_type);
+    $media = array_map(function ($source_field_value) use ($media_type, $media_storage, $source_field_name) {
+      return $this->createMediaFromValue($media_type, $media_storage, $source_field_name, $source_field_value);
+    }, $source_field_values);
+    $form_state->set('media', $media)->setRebuild();
+  }
+
+  /**
+   * Creates a new, unsaved media item from a source field value.
+   *
+   * @param \Drupal\media\MediaTypeInterface $media_type
+   *   The media type of the media item.
+   * @param \Drupal\Core\Entity\EntityStorageInterface $media_storage
+   *   The media storage.
+   * @param string $source_field_name
+   *   The name of the media type's source field.
+   * @param mixed $source_field_value
+   *   The value for the source field of the media item.
+   *
+   * @return \Drupal\media\MediaInterface
+   *   An unsaved media entity.
+   */
+  protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $source_field_value) {
+    return $media_storage->create([
+      'bundle' => $media_type->id(),
+      $source_field_name => $source_field_value,
+    ]);
+  }
+
+  /**
+   * Prepares a created media item to be permanently saved.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The unsaved media item.
+   */
+  protected function prepareMediaEntityForSave(MediaInterface $media) {
+    // Intentionally empty by default.
+  }
+
+  /**
+   * AJAX callback to update the entire form based on source field input.
+   *
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse|array
+   *   The form render array or an AJAX response object.
+   */
+  public function updateFormCallback(array &$form, FormStateInterface $form_state) {
+    // When the source field input contains errors, replace the existing form to
+    // let the user change the source field input. If the user input is valid,
+    // the entire modal is replaced with the second step of the form to show the
+    // form fields for each media item.
+    if ($form_state::hasAnyErrors()) {
+      $response = new AjaxResponse();
+      $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $form));
+      return $response;
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $added_media = $form_state->get('media') ?: [];
+    foreach ($added_media as $delta => $media) {
+      $this->validateMediaEntity($media, $form, $form_state, $delta);
+    }
+  }
+
+  /**
+   * Validate a created media item.
+   *
+   * @param \Drupal\media\MediaInterface $media
+   *   The media item to validate.
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   * @param int $delta
+   *   The delta of the media item.
+   */
+  protected function validateMediaEntity(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
+    $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
+    $form_display->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
+    $form_display->validateFormValues($media, $form['media'][$delta]['fields'], $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $added_media = $form_state->get('media') ?: [];
+    foreach ($added_media as $delta => $media) {
+      EntityFormDisplay::collectRenderDisplay($media, 'media_library')
+        ->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
+      $this->prepareMediaEntityForSave($media);
+      $media->save();
+    }
+  }
+
+  /**
+   * AJAX callback to send the new media item(s) to the calling code.
+   *
+   * @param array $form
+   *   The complete form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return array|\Drupal\Core\Ajax\AjaxResponse
+   *   The form array when there are form errors or a AJAX response to select
+   *   the created items in the media library.
+   */
+  public function updateWidget(array &$form, FormStateInterface $form_state) {
+    if ($form_state::hasAnyErrors()) {
+      return $form;
+    }
+
+    $added_media = $form_state->get('media') ?: [];
+    $media_ids = array_map(function (MediaInterface $media) {
+      return $media->id();
+    }, $added_media);
+
+    // Get the render array for the media library. The media library state might
+    // contain the 'media_library_content' when it has been opened from a
+    // vertical tab. We need to remove that to make sure the render array
+    // contains the vertical tabs. Besides that, we also need to force the media
+    // library to create a new instance of the media add form.
+    // @see \Drupal\media_library\MediaLibraryUiBuilder::buildMediaTypeAddForm()
+    $state = MediaLibraryState::fromRequest($this->getRequest());
+    $state->remove('media_library_content');
+    $state->set('_media_library_form_rebuild', TRUE);
+    $library_ui = $this->libraryUiBuilder->buildUi($state);
+
+    $response = new AjaxResponse();
+    $response->addCommand(new UpdateSelectionCommand($media_ids));
+    $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $library_ui));
+    return $response;
+  }
+
+  /**
+   * Returns the name of the source field for a media type.
+   *
+   * @param \Drupal\media\MediaTypeInterface $media_type
+   *   The media type to get the source field name for.
+   *
+   * @return string
+   *   The name of the media type's source field.
+   */
+  protected function getSourceFieldName(MediaTypeInterface $media_type) {
+    return $media_type->getSource()
+      ->getSourceFieldDefinition($media_type)
+      ->getName();
+  }
+
+}
diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php
new file mode 100644
index 0000000000000000000000000000000000000000..e89b67e7d150c688c0cfd9e08e29eb5b3f1ea710
--- /dev/null
+++ b/core/modules/media_library/src/Form/FileUploadForm.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace Drupal\media_library\Form;
+
+use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\file\FileInterface;
+use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
+use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\media\MediaInterface;
+use Drupal\media\MediaTypeInterface;
+use Drupal\media_library\MediaLibraryUiBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Creates a form to create media entities from uploaded files.
+ *
+ * @internal
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
+ */
+class FileUploadForm extends AddFormBase {
+
+  /**
+   * The element info manager.
+   *
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface
+   */
+  protected $elementInfo;
+
+  /**
+   * The renderer service.
+   *
+   * @var \Drupal\Core\Render\ElementInfoManagerInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new FileUploadForm.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity type manager.
+   * @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
+   *   The media library UI builder.
+   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
+   *   The element info manager.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer service.
+   */
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, ElementInfoManagerInterface $element_info, RendererInterface $renderer) {
+    parent::__construct($entity_type_manager, $library_ui_builder);
+    $this->elementInfo = $element_info;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('entity_type.manager'),
+      $container->get('media_library.ui_builder'),
+      $container->get('element_info'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getMediaType(FormStateInterface $form_state) {
+    if ($this->mediaType) {
+      return $this->mediaType;
+    }
+
+    $media_type = parent::getMediaType($form_state);
+    // The file upload form only supports media types which use a file field as
+    // a source field.
+    $field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
+    if (!is_a($field_definition->getClass(), FileFieldItemList::class, TRUE)) {
+      throw new \InvalidArgumentException('Can only add media types which use a file field as a source field.');
+    }
+    return $media_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function buildInputElement(array $form, FormStateInterface $form_state) {
+    $form['#attributes']['class'][] = 'media-library-add-form-upload';
+
+    // Create a file item to get the upload validators.
+    $media_type = $this->getMediaType($form_state);
+    $item = $this->createFileItem($media_type);
+
+    /** @var \Drupal\media_library\MediaLibraryState $state */
+    $state = $form_state->get('media_library_state');
+    if (!$state->hasSlotsAvailable()) {
+      return $form;
+    }
+
+    $slots = $state->getAvailableSlots();
+
+    $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
+    $form['upload'] = [
+      '#type' => 'managed_file',
+      '#title' => $this->formatPlural($slots, 'Add file', 'Add files'),
+      // @todo Move validation in https://www.drupal.org/node/2988215
+      '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
+      '#upload_validators' => $item->getUploadValidators(),
+      '#multiple' => $slots > 1 || $slots === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+      '#cardinality' => $slots,
+      '#remaining_slots' => $slots,
+    ];
+
+    $file_upload_help = [
+      '#theme' => 'file_upload_help',
+      '#upload_validators' => $form['upload']['#upload_validators'],
+      '#cardinality' => $slots,
+    ];
+
+    // The file upload help needs to be rendered since the description does not
+    // accept render arrays. The FileWidget::formElement() method adds the file
+    // upload help in the same way, so any theming improvements made to file
+    // fields would also be applied to this upload field.
+    // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formElement()
+    $form['upload']['#description'] = $this->renderer->renderPlain($file_upload_help);
+
+    return $form;
+  }
+
+  /**
+   * Validates the upload element.
+   *
+   * @param array $element
+   *   The upload element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The processed upload element.
+   */
+  public function validateUploadElement(array $element, FormStateInterface $form_state) {
+    if ($form_state::hasAnyErrors()) {
+      // When an error occurs during uploading files, remove all files so the
+      // user can re-upload the files.
+      $element['#value'] = [];
+    }
+    $values = $form_state->getValue('upload', []);
+    if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
+      $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [
+        '@count' => $element['#cardinality'],
+      ]));
+      $form_state->setValue('upload', []);
+      $element['#value'] = [];
+    }
+    return $element;
+  }
+
+  /**
+   * Processes an upload (managed_file) element.
+   *
+   * @param array $element
+   *   The upload element.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   *
+   * @return array
+   *   The processed upload element.
+   */
+  public function processUploadElement(array $element, FormStateInterface $form_state) {
+    $element['upload_button']['#submit'] = ['::uploadButtonSubmit'];
+    $element['upload_button']['#ajax'] = [
+      'callback' => '::updateFormCallback',
+      'wrapper' => 'media-library-wrapper',
+    ];
+    return $element;
+  }
+
+  /**
+   * Submit handler for the upload button, inside the managed_file element.
+   *
+   * @param array $form
+   *   The form render array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function uploadButtonSubmit(array $form, FormStateInterface $form_state) {
+    $files = $this->entityTypeManager
+      ->getStorage('file')
+      ->loadMultiple($form_state->getValue('upload', []));
+    $this->processInputValues($files, $form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $file) {
+    if (!($file instanceof FileInterface)) {
+      throw new \InvalidArgumentException('Cannot create a media item without a file entity.');
+    }
+
+    // Create a file item to get the upload location.
+    $item = $this->createFileItem($media_type);
+    $upload_location = $item->getUploadLocation();
+    if (!file_prepare_directory($upload_location, FILE_CREATE_DIRECTORY)) {
+      throw new \Exception("The destination directory '$upload_location' is not writable");
+    }
+    $file = file_move($file, $upload_location);
+    if (!$file) {
+      throw new \RuntimeException("Unable to move file to '$upload_location'");
+    }
+
+    return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file)->setName($file->getFilename());
+  }
+
+  /**
+   * Create a file field item.
+   *
+   * @param \Drupal\media\MediaTypeInterface $media_type
+   *   The media type of the media item.
+   *
+   * @return \Drupal\file\Plugin\Field\FieldType\FileItem
+   *   A created file item.
+   */
+  protected function createFileItem(MediaTypeInterface $media_type) {
+    $field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
+    $data_definition = FieldItemDataDefinition::create($field_definition);
+    return new FileItem($data_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareMediaEntityForSave(MediaInterface $media) {
+    /** @var \Drupal\file\FileInterface $file */
+    $file = $media->get($this->getSourceFieldName($media->bundle->entity))->entity;
+    $file->setPermanent();
+    $file->save();
+  }
+
+}
diff --git a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
deleted file mode 100644
index 66670ffe1bcda5cb419f82348c57058116ebd0de..0000000000000000000000000000000000000000
--- a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php
+++ /dev/null
@@ -1,639 +0,0 @@
-<?php
-
-namespace Drupal\media_library\Form;
-
-use Drupal\Core\Access\AccessResultAllowed;
-use Drupal\Core\Ajax\AjaxResponse;
-use Drupal\Core\Ajax\CloseDialogCommand;
-use Drupal\Core\Ajax\InvokeCommand;
-use Drupal\Core\Entity\Entity\EntityFormDisplay;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
-use Drupal\Core\Field\FieldStorageDefinitionInterface;
-use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
-use Drupal\Core\Form\FormBase;
-use Drupal\Core\Form\FormStateInterface;
-use Drupal\Core\Render\ElementInfoManagerInterface;
-use Drupal\file\FileInterface;
-use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
-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;
-
-/**
- * Creates a form to create media entities from uploaded files.
- *
- * @internal
- */
-class MediaLibraryUploadForm extends FormBase {
-
-  /**
-   * The element info manager.
-   *
-   * @var \Drupal\Core\Render\ElementInfoManagerInterface
-   */
-  protected $elementInfo;
-
-  /**
-   * The entity type manager.
-   *
-   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
-   */
-  protected $entityTypeManager;
-
-  /**
-   * Media types the current user has access to.
-   *
-   * @var \Drupal\media\MediaTypeInterface[]
-   */
-  protected $types;
-
-  /**
-   * The media being processed.
-   *
-   * @var \Drupal\media\MediaInterface[]
-   */
-  protected $media = [];
-
-  /**
-   * The files waiting for type selection.
-   *
-   * @var \Drupal\file\FileInterface[]
-   */
-  protected $files = [];
-
-  /**
-   * Indicates whether the 'medium' image style exists.
-   *
-   * @var bool
-   */
-  protected $mediumStyleExists = FALSE;
-
-  /**
-   * Constructs a new MediaLibraryUploadForm.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
-   *   The entity type manager.
-   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
-   *   The element info manager.
-   */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info) {
-    $this->entityTypeManager = $entity_type_manager;
-    $this->elementInfo = $element_info;
-    $this->mediumStyleExists = !empty($entity_type_manager->getStorage('image_style')->load('medium'));
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('entity_type.manager'),
-      $container->get('element_info')
-    );
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getFormId() {
-    return 'media_library_upload_form';
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['#prefix'] = '<div id="media-library-upload-wrapper">';
-    $form['#suffix'] = '</div>';
-
-    $form['#attached']['library'][] = 'media_library/style';
-
-    $form['#attributes']['class'][] = 'media-library-upload';
-
-    if (empty($this->media) && empty($this->files)) {
-      $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
-      $upload_validators = $this->mergeUploadValidators($this->getTypes());
-      $form['upload'] = [
-        '#type' => 'managed_file',
-        '#title' => $this->t('Upload'),
-        // @todo Move validation in https://www.drupal.org/node/2988215
-        '#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
-        '#upload_validators' => $upload_validators,
-      ];
-      $form['upload_help'] = [
-        '#theme' => 'file_upload_help',
-        '#description' => $this->t('Upload files here to add new media.'),
-        '#upload_validators' => $upload_validators,
-      ];
-      $remaining = (int) $this->getRequest()->query->get('media_library_remaining');
-      if ($remaining || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
-        $form['upload']['#multiple'] = $remaining > 1 || $remaining === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
-        $form['upload']['#cardinality'] = $form['upload_help']['#cardinality'] = $remaining;
-      }
-    }
-    else {
-      $form['media'] = [
-        '#type' => 'container',
-      ];
-      foreach ($this->media as $i => $media) {
-        $source_field = $media->getSource()
-          ->getSourceFieldDefinition($media->bundle->entity)
-          ->getName();
-
-        $element = [
-          '#type' => 'container',
-          '#attributes' => [
-            'class' => [
-              'media-library-upload__media',
-            ],
-          ],
-          'preview' => [
-            '#type' => 'container',
-            '#attributes' => [
-              'class' => [
-                'media-library-upload__media-preview',
-              ],
-            ],
-          ],
-          'fields' => [
-            '#type' => 'container',
-            '#attributes' => [
-              'class' => [
-                'media-library-upload__media-fields',
-              ],
-            ],
-            // Parents is set here as it is used in the form display.
-            '#parents' => ['media', $i, 'fields'],
-          ],
-        ];
-        // @todo Make this configurable in https://www.drupal.org/node/2988223
-        if ($this->mediumStyleExists && $thumbnail_uri = $media->getSource()->getMetadata($media, 'thumbnail_uri')) {
-          $element['preview']['thumbnail'] = [
-            '#theme' => 'image_style',
-            '#style_name' => 'medium',
-            '#uri' => $thumbnail_uri,
-          ];
-        }
-
-        $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
-        // When the name is not added to the form as a editable field, output
-        // the name as a fixed element to confirm the right file was uploaded.
-        if (!$form_display->getComponent('name')) {
-          $element['fields']['name'] = [
-            '#type' => 'item',
-            '#title' => $this->t('Name'),
-            '#markup' => $media->getName(),
-          ];
-        }
-        $form_display->buildForm($media, $element['fields'], $form_state);
-
-        // We hide certain elements in the image widget with CSS.
-        if (isset($element['fields'][$source_field])) {
-          $element['fields'][$source_field]['#attributes']['class'][] = 'media-library-upload__source-field';
-        }
-        if (isset($element['fields']['revision_log_message'])) {
-          $element['fields']['revision_log_message']['#access'] = FALSE;
-        }
-        $form['media'][$i] = $element;
-      }
-
-      $form['files'] = [
-        '#type' => 'container',
-      ];
-      foreach ($this->files as $i => $file) {
-        $types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
-        $form['files'][$i] = [
-          '#type' => 'container',
-          '#attributes' => [
-            'class' => [
-              'media-library-upload__file',
-            ],
-          ],
-          'help' => [
-            '#markup' => '<strong class="media-library-upload__file-label">' . $this->t('Select a media type for %filename:', [
-              '%filename' => $file->getFilename(),
-            ]) . '</strong>',
-          ],
-        ];
-        foreach ($types as $type) {
-          $form['files'][$i][$type->id()] = [
-            '#type' => 'submit',
-            '#media_library_index' => $i,
-            '#media_library_type' => $type->id(),
-            '#value' => $type->label(),
-            '#submit' => ['::selectType'],
-            '#ajax' => [
-              'callback' => '::updateFormCallback',
-              'wrapper' => 'media-library-upload-wrapper',
-            ],
-            '#limit_validation_errors' => [['files', $i, $type->id()]],
-          ];
-        }
-      }
-
-      $form['actions'] = [
-        '#type' => 'actions',
-      ];
-      $form['actions']['submit'] = [
-        '#type' => 'submit',
-        '#value' => $this->t('Save'),
-        '#ajax' => [
-          'callback' => '::updateWidget',
-          'wrapper' => 'media-library-upload-wrapper',
-        ],
-      ];
-    }
-
-    return $form;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    if (count($this->files)) {
-      $form_state->setError($form['files'], $this->t('Please select a media type for all files.'));
-    }
-    foreach ($this->media as $i => $media) {
-      $form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
-      $form_display->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
-      $form_display->validateFormValues($media, $form['media'][$i]['fields'], $form_state);
-    }
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function submitForm(array &$form, FormStateInterface $form_state) {
-    foreach ($this->media as $i => $media) {
-      EntityFormDisplay::collectRenderDisplay($media, 'media_library')
-        ->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
-      $source_field = $media->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName();
-      /** @var \Drupal\file\FileInterface $file */
-      $file = $media->get($source_field)->entity;
-      $file->setPermanent();
-      $file->save();
-      $media->save();
-    }
-  }
-
-  /**
-   * AJAX callback to select a media type for a file.
-   *
-   * @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.
-   *
-   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
-   *   If the triggering element is missing required properties.
-   */
-  public function selectType(array &$form, FormStateInterface $form_state) {
-    $element = $form_state->getTriggeringElement();
-    if (!isset($element['#media_library_index']) || !isset($element['#media_library_type'])) {
-      throw new BadRequestHttpException('The "#media_library_index" and "#media_library_type" properties on the triggering element are required for type selection.');
-    }
-    $i = $element['#media_library_index'];
-    $type = $element['#media_library_type'];
-    $this->media[] = $this->createMediaEntity($this->files[$i], $this->getTypes()[$type]);
-    unset($this->files[$i]);
-    $form_state->setRebuild();
-  }
-
-  /**
-   * AJAX callback to update the field widget.
-   *
-   * @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 function updateWidget(array &$form, FormStateInterface $form_state) {
-    if ($form_state->getErrors()) {
-      return $form;
-    }
-
-    $mids = array_map(function (MediaInterface $media) {
-      return $media->id();
-    }, $this->media);
-
-    // Pass the selection to the field widget based on the current widget ID.
-    $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());
-    }
-  }
-
-  /**
-   * Processes an upload (managed_file) element.
-   *
-   * @param array $element
-   *   The upload element.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The form state.
-   *
-   * @return array
-   *   The processed upload element.
-   */
-  public function processUploadElement(array $element, FormStateInterface $form_state) {
-    $element['upload_button']['#submit'] = ['::uploadButtonSubmit'];
-    $element['upload_button']['#ajax'] = [
-      'callback' => '::updateFormCallback',
-      'wrapper' => 'media-library-upload-wrapper',
-    ];
-    return $element;
-  }
-
-  /**
-   * Validates the upload element.
-   *
-   * @param array $element
-   *   The upload element.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The form state.
-   *
-   * @return array
-   *   The processed upload element.
-   */
-  public function validateUploadElement(array $element, FormStateInterface $form_state) {
-    if ($form_state->getErrors()) {
-      $element['#value'] = [];
-    }
-    $values = $form_state->getValue('upload', []);
-    if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
-      $form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [
-        '@count' => $element['#cardinality'],
-      ]));
-      $form_state->setValue('upload', []);
-      $element['#value'] = [];
-    }
-    return $element;
-  }
-
-  /**
-   * Submit handler for the upload button, inside the managed_file element.
-   *
-   * @param array $form
-   *   The form render array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The form state.
-   */
-  public function uploadButtonSubmit(array $form, FormStateInterface $form_state) {
-    $fids = $form_state->getValue('upload', []);
-    $files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
-    /** @var \Drupal\file\FileInterface $file */
-    foreach ($files as $file) {
-      $types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
-      if (!empty($types)) {
-        if (count($types) === 1) {
-          $this->media[] = $this->createMediaEntity($file, reset($types));
-        }
-        else {
-          $this->files[] = $file;
-        }
-      }
-    }
-    $form_state->setRebuild();
-  }
-
-  /**
-   * Creates a new, unsaved media entity.
-   *
-   * @param \Drupal\file\FileInterface $file
-   *   A file for the media source field.
-   * @param \Drupal\media\MediaTypeInterface $type
-   *   A media type.
-   *
-   * @return \Drupal\media\MediaInterface
-   *   An unsaved media entity.
-   *
-   * @throws \Exception
-   *   If a file operation failed when moving the upload.
-   */
-  protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) {
-    $media = $this->entityTypeManager->getStorage('media')->create([
-      'bundle' => $type->id(),
-      'name' => $file->getFilename(),
-    ]);
-    $source_field = $type->getSource()->getSourceFieldDefinition($type)->getName();
-    $location = $this->getUploadLocationForType($media->bundle->entity);
-    if (!file_prepare_directory($location, FILE_CREATE_DIRECTORY)) {
-      throw new \Exception("The destination directory '$location' is not writable");
-    }
-    $file = file_move($file, $location);
-    if (!$file) {
-      throw new \Exception("Unable to move file to '$location'");
-    }
-    $media->set($source_field, $file->id());
-    return $media;
-  }
-
-  /**
-   * AJAX callback for refreshing the entire form.
-   *
-   * @param array $form
-   *   The form render array.
-   * @param \Drupal\Core\Form\FormStateInterface $form_state
-   *   The form state.
-   *
-   * @return array
-   *   The form render array.
-   */
-  public function updateFormCallback(array &$form, FormStateInterface $form_state) {
-    return $form;
-  }
-
-  /**
-   * Access callback to check that the user can create file based media.
-   *
-   * @param array $allowed_types
-   *   (optional) The contextually allowed types.
-   *
-   * @return \Drupal\Core\Access\AccessResultInterface
-   *   The access result.
-   *
-   * @todo Remove $allowed_types param in https://www.drupal.org/node/2956747
-   */
-  public function access(array $allowed_types = NULL) {
-    return AccessResultAllowed::allowedIf(count($this->getTypes($allowed_types)))->mergeCacheMaxAge(0);
-  }
-
-  /**
-   * Returns media types which use files that the current user can create.
-   *
-   * @param array $allowed_types
-   *   (optional) The contextually allowed types.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @return \Drupal\media\MediaTypeInterface[]
-   *   A list of media types that are valid for this form.
-   */
-  protected function getTypes(array $allowed_types = NULL) {
-    // Cache results if possible.
-    if (!isset($this->types)) {
-      $media_type_storage = $this->entityTypeManager->getStorage('media_type');
-      if (!$allowed_types) {
-        $types = $media_type_storage->loadMultiple(MediaLibraryState::fromRequest($this->getRequest())->getAllowedTypeIds());
-      }
-      else {
-        $types = $media_type_storage->loadMultiple($allowed_types);
-      }
-      $types = $this->filterTypesWithFileSource($types);
-      $types = $this->filterTypesWithCreateAccess($types);
-      $this->types = $types;
-    }
-    return $this->types;
-  }
-
-  /**
-   * Filters media types that accept a given file.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\file\FileInterface $file
-   *   A file entity.
-   * @param \Drupal\media\MediaTypeInterface[] $types
-   *   An array of available media types.
-   *
-   * @return \Drupal\media\MediaTypeInterface[]
-   *   An array of media types that accept the file.
-   */
-  protected function filterTypesThatAcceptFile(FileInterface $file, array $types) {
-    $types = $this->filterTypesWithFileSource($types);
-    return array_filter($types, function (MediaTypeInterface $type) use ($file) {
-      $validators = $this->getUploadValidatorsForType($type);
-      $errors = file_validate($file, $validators);
-      return empty($errors);
-    });
-  }
-
-  /**
-   * Filters an array of media types that accept file sources.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface[] $types
-   *   An array of media types.
-   *
-   * @return \Drupal\media\MediaTypeInterface[]
-   *   An array of media types that accept file sources.
-   */
-  protected function filterTypesWithFileSource(array $types) {
-    return array_filter($types, function (MediaTypeInterface $type) {
-      return is_a($type->getSource()->getSourceFieldDefinition($type)->getClass(), FileFieldItemList::class, TRUE);
-    });
-  }
-
-  /**
-   * Merges file upload validators for an array of media types.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface[] $types
-   *   An array of media types.
-   *
-   * @return array
-   *   An array suitable for passing to file_save_upload() or the file field
-   *   element's '#upload_validators' property.
-   */
-  protected function mergeUploadValidators(array $types) {
-    $max_size = 0;
-    $extensions = [];
-    $types = $this->filterTypesWithFileSource($types);
-    foreach ($types as $type) {
-      $validators = $this->getUploadValidatorsForType($type);
-      if (isset($validators['file_validate_size'])) {
-        $max_size = max($max_size, $validators['file_validate_size'][0]);
-      }
-      if (isset($validators['file_validate_extensions'])) {
-        $extensions = array_unique(array_merge($extensions, explode(' ', $validators['file_validate_extensions'][0])));
-      }
-    }
-    // If no field defines a max size, default to the system wide setting.
-    if ($max_size === 0) {
-      $max_size = file_upload_max_size();
-    }
-    return [
-      'file_validate_extensions' => [implode(' ', $extensions)],
-      'file_validate_size' => [$max_size],
-    ];
-  }
-
-  /**
-   * Gets upload validators for a given media type.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface $type
-   *   A media type.
-   *
-   * @return array
-   *   An array suitable for passing to file_save_upload() or the file field
-   *   element's '#upload_validators' property.
-   */
-  protected function getUploadValidatorsForType(MediaTypeInterface $type) {
-    return $this->getFileItemForType($type)->getUploadValidators();
-  }
-
-  /**
-   * Gets upload destination for a given media type.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface $type
-   *   A media type.
-   *
-   * @return string
-   *   An unsanitized file directory URI with tokens replaced.
-   */
-  protected function getUploadLocationForType(MediaTypeInterface $type) {
-    return $this->getFileItemForType($type)->getUploadLocation();
-  }
-
-  /**
-   * Creates a file item for a given media type.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface $type
-   *   A media type.
-   *
-   * @return \Drupal\file\Plugin\Field\FieldType\FileItem
-   *   The file item.
-   */
-  protected function getFileItemForType(MediaTypeInterface $type) {
-    $source = $type->getSource();
-    $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($type));
-    return new FileItem($source_data_definition);
-  }
-
-  /**
-   * Filters an array of media types that can be created by the current user.
-   *
-   * @todo Move in https://www.drupal.org/node/2987924
-   *
-   * @param \Drupal\media\MediaTypeInterface[] $types
-   *   An array of media types.
-   *
-   * @return \Drupal\media\MediaTypeInterface[]
-   *   An array of media types that accept file sources.
-   */
-  protected function filterTypesWithCreateAccess(array $types) {
-    $access_handler = $this->entityTypeManager->getAccessControlHandler('media');
-    return array_filter($types, function (MediaTypeInterface $type) use ($access_handler) {
-      return $access_handler->createAccess($type->id());
-    });
-  }
-
-}
diff --git a/core/modules/media_library/src/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php
index ea740dba8003b8c8a1c394c25db8ce2157eb0e1b..018c42a74c368b2de6015701ae1e0cae8829abd6 100644
--- a/core/modules/media_library/src/MediaLibraryState.php
+++ b/core/modules/media_library/src/MediaLibraryState.php
@@ -29,8 +29,9 @@
  *   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.
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
  */
 class MediaLibraryState extends ParameterBag {
 
diff --git a/core/modules/media_library/src/MediaLibraryUiBuilder.php b/core/modules/media_library/src/MediaLibraryUiBuilder.php
index c97cc33764ad93f6b5a775b1cfef0ae6bb0244b2..4359e0b0e4e27668bb1050437925efa63d3428d3 100644
--- a/core/modules/media_library/src/MediaLibraryUiBuilder.php
+++ b/core/modules/media_library/src/MediaLibraryUiBuilder.php
@@ -3,6 +3,8 @@
 namespace Drupal\media_library;
 
 use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Form\FormState;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
 use Drupal\Core\Session\AccountInterface;
@@ -15,13 +17,21 @@
  * 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.
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
  */
 class MediaLibraryUiBuilder {
 
   use StringTranslationTrait;
 
+  /**
+   * The form builder.
+   *
+   * @var \Drupal\Core\Form\FormBuilderInterface
+   */
+  protected $formBuilder;
+
   /**
    * The entity type manager.
    *
@@ -52,11 +62,14 @@ class MediaLibraryUiBuilder {
    *   The request stack.
    * @param \Drupal\views\ViewExecutableFactory $views_executable_factory
    *   The views executable factory.
+   * @param \Drupal\Core\Form\FormBuilderInterface $form_builder
+   *   The currently active request object.
    */
-  public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory) {
+  public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory, FormBuilderInterface $form_builder) {
     $this->entityTypeManager = $entity_type_manager;
     $this->request = $request_stack->getCurrentRequest();
     $this->viewsExecutableFactory = $views_executable_factory;
+    $this->formBuilder = $form_builder;
   }
 
   /**
@@ -77,11 +90,17 @@ public static function dialogOptions() {
   /**
    * Build the media library UI.
    *
+   * @param \Drupal\media_library\MediaLibraryState $state
+   *   (optional) The current state of the media library, derived from the
+   *   current request.
+   *
    * @return array
    *   The render array for the media library.
    */
-  public function buildUi() {
-    $state = MediaLibraryState::fromRequest($this->request);
+  public function buildUi(MediaLibraryState $state = NULL) {
+    if (!$state) {
+      $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.
@@ -123,6 +142,7 @@ protected function buildLibraryContent(MediaLibraryState $state) {
         'class' => ['media-library-content'],
         'tabindex' => -1,
       ],
+      'form' => $this->buildMediaTypeAddForm($state),
       'view' => $this->buildMediaLibraryView($state),
     ];
   }
@@ -178,13 +198,15 @@ protected function buildMediaTypeMenu(MediaLibraryState $state) {
       ],
     ];
 
-    // 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]);
+    // Get the state parameters but remove the wrapper format, AJAX form and
+    // form rebuild parameters. These are internal parameters that should never
+    // be part of the vertical tab links.
     $query = $state->all();
+    unset($query[MainContentViewSubscriber::WRAPPER_FORMAT], $query[FormBuilderInterface::AJAX_FORM_REQUEST], $query['_media_library_form_rebuild']);
+    // Add the 'media_library_content' parameter so the response will contain
+    // only the updated content for the tab.
+    // @see self::buildUi()
+    $query['media_library_content'] = 1;
 
     $allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids);
 
@@ -216,6 +238,42 @@ protected function buildMediaTypeMenu(MediaLibraryState $state) {
     return $menu;
   }
 
+  /**
+   * Get the add form for the selected media type.
+   *
+   * @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 add form.
+   */
+  protected function buildMediaTypeAddForm(MediaLibraryState $state) {
+    $selected_type_id = $state->getSelectedTypeId();
+
+    if (!$this->entityTypeManager->getAccessControlHandler('media')->createAccess($selected_type_id)) {
+      return [];
+    }
+
+    $selected_type = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
+    $plugin_definition = $selected_type->getSource()->getPluginDefinition();
+
+    if (empty($plugin_definition['forms']['media_library_add'])) {
+      return [];
+    }
+
+    // After the form to add new media is submitted, we need to rebuild the
+    // media library with a new instance of the media add form. The form API
+    // allows us to do that by forcing empty user input.
+    // @see \Drupal\Core\Form\FormBuilder::doBuildForm()
+    $form_state = new FormState();
+    if ($state->get('_media_library_form_rebuild')) {
+      $form_state->setUserInput([]);
+      $state->remove('_media_library_form_rebuild');
+    }
+    $form_state->set('media_library_state', $state);
+    return $this->formBuilder->buildForm($plugin_definition['forms']['media_library_add'], $form_state);
+  }
+
   /**
    * Get the media library view.
    *
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 033c7da139ff91c6f4149445fb025bca05f366d3..3ecf3ed78e0852c8672f635a13112be064f04fea 100644
--- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -33,6 +33,9 @@
  * )
  *
  * @internal
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
  */
 class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface {
 
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 d1d6b4cad6342fee376110c775b90d2a0686a624..5660e43f4a731e55efee7fd9d900278b8430ac96 100644
--- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -20,6 +20,9 @@
  * @ViewsField("media_library_select_form")
  *
  * @internal
+ *   Media Library is an experimental module and its internal code may be
+ *   subject to change in minor releases. External code should not instantiate
+ *   or extend this class.
  */
 class MediaLibrarySelectForm extends FieldPluginBase {
 
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index 64f3ff59b3a191e5096cf49c5677b69f57396837..34245daec406712eadccbd198f6f349ebb1a01a0 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -150,8 +150,8 @@ public function testWidgetAccess() {
     $role->save();
 
     // Create a working state.
-    $allowed_types = ['type_one', 'type_two'];
-    $state = MediaLibraryState::create('test', $allowed_types, 'type_two', 2);
+    $allowed_types = ['type_one', 'type_two', 'type_three', 'type_four'];
+    $state = MediaLibraryState::create('test', $allowed_types, 'type_three', 2);
     $url_options = ['query' => $state->all()];
 
     // Verify that unprivileged users can't access the widget view.
@@ -169,6 +169,18 @@ public function testWidgetAccess() {
     $assert_session->elementExists('css', '.view-media-library');
     $this->drupalGet('media-library', $url_options);
     $assert_session->elementExists('css', '.view-media-library');
+    // Assert the user does not have access to the media add form if the user
+    // does not have the 'create media' permission.
+    $assert_session->fieldNotExists('files[upload][]');
+
+    // Assert users with the 'create media' permission can access the media add
+    // form.
+    $this->grantPermissions($role, [
+      'create media',
+    ]);
+    $this->drupalGet('media-library', $url_options);
+    $assert_session->elementExists('css', '.view-media-library');
+    $assert_session->fieldExists('Add files');
   }
 
   /**
@@ -258,7 +270,7 @@ public function testWidget() {
     $assert_session->pageTextContains('Dog');
     $assert_session->pageTextContains('Bear');
     $assert_session->pageTextNotContains('Turtle');
-    $assert_session->elementExists('named', ['link', 'Type Three'])->click();
+    $page->clickLink('Type Three');
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->elementExists('named', ['link', 'Type Three (active tab)']);
     $assert_session->pageTextNotContains('Dog');
@@ -316,9 +328,9 @@ public function testWidget() {
     $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();
+    $page->clickLink('Type Three');
     $assert_session->assertWaitOnAjaxRequest();
-    $assert_session->elementExists('named', ['link', 'Type One'])->click();
+    $page->clickLink('Type One');
     $assert_session->assertWaitOnAjaxRequest();
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
     $selected_checkboxes = [];
@@ -331,7 +343,7 @@ public function testWidget() {
     $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();
+    $page->clickLink('Type Two');
     $assert_session->assertWaitOnAjaxRequest();
     $checkboxes = $page->findAll('css', '.media-library-view .js-click-to-select-checkbox input');
     $checkboxes[0]->click();
@@ -345,7 +357,7 @@ public function testWidget() {
     $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();
+    $page->clickLink('Type One');
     $assert_session->assertWaitOnAjaxRequest();
     $this->assertTrue($checkboxes[0]->hasAttribute('disabled'));
     $this->assertFalse($checkboxes[1]->hasAttribute('disabled'));
@@ -473,6 +485,7 @@ public function testWidget() {
    */
   public function testWidgetAnonymous() {
     $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
 
     $this->drupalLogout();
 
@@ -492,9 +505,7 @@ public function testWidgetAnonymous() {
     $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();
+    $page->find('css', '.media-library-view .js-click-to-select-checkbox input')->click();
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
     $assert_session->assertWaitOnAjaxRequest();
 
@@ -533,6 +544,44 @@ public function testWidgetUpload() {
       $this->fail('Expected test files not present.');
     }
 
+    // Create a user that can only add media of type four.
+    $user = $this->drupalCreateUser([
+      'access administration pages',
+      'access content',
+      'create basic_page content',
+      'create type_four media',
+      'view media',
+    ]);
+    $this->drupalLogin($user);
+
+    // Visit a node create page and open the media library.
+    $this->drupalGet('node/add/basic_page');
+    $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('Media library');
+
+    // Assert the upload form is visible for type_four.
+    $page->clickLink('Type Four');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldExists('Add files');
+    $assert_session->pageTextContains('Maximum 2 files.');
+
+    // Assert the upload form is not visible for type_three.
+    $page->clickLink('Type Three');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->fieldNotExists('files[upload][]');
+    $assert_session->pageTextNotContains('Maximum 2 files.');
+
+    // Create a user that can create media for all media types.
+    $user = $this->drupalCreateUser([
+      'access administration pages',
+      'access content',
+      'create basic_page content',
+      'create media',
+      'view media',
+    ]);
+    $this->drupalLogin($user);
+
     // Visit a node create page.
     $this->drupalGet('node/add/basic_page');
 
@@ -544,11 +593,19 @@ public function testWidgetUpload() {
     $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');
-    $assert_session->assertWaitOnAjaxRequest();
 
-    $page->attachFileToField('Upload', $this->container->get('file_system')->realpath($png_image->uri));
+    // Assert the default tab for media type one does not have an upload form.
+    $assert_session->fieldNotExists('files[upload][]');
+
+    // Assert we can upload a file to media type three.
+    $page->clickLink('Type Three');
     $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '.media-library-add-form--without-input');
+    $assert_session->elementNotExists('css', '.media-library-add-form--with-input');
+    $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->elementExists('css', '.media-library-add-form--with-input');
+    $assert_session->elementNotExists('css', '.media-library-add-form--without-input');
 
     // Files are temporary until the form is saved.
     $files = $file_storage->loadMultiple();
@@ -556,6 +613,11 @@ public function testWidgetUpload() {
     $this->assertSame('public://type-three-dir', $file_system->dirname($file->getFileUri()));
     $this->assertTrue($file->isTemporary());
 
+    // Assert the revision_log_message field is not shown.
+    $upload_form = $assert_session->elementExists('css', '.media-library-add-form');
+    $assert_session->fieldNotExists('Revision log message', $upload_form);
+
+    // Assert the name field contains the filename and the alt text is required.
     $this->assertSame($assert_session->fieldExists('Name')->getValue(), $png_image->filename);
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save');
     $assert_session->assertWaitOnAjaxRequest();
@@ -569,7 +631,23 @@ public function testWidgetUpload() {
     $file = array_pop($files);
     $this->assertFalse($file->isTemporary());
 
-    // Ensure the media item was added.
+    // Load the created media item.
+    $media_storage = $this->container->get('entity_type.manager')->getStorage('media');
+    $media_items = $media_storage->loadMultiple();
+    $added_media = array_pop($media_items);
+
+    // Ensure the media item was saved to the library and automatically
+    // selected. The added media items should be in the first position of the
+    // add form.
+    $assert_session->pageTextContains('Media library');
+    $assert_session->pageTextContains($png_image->filename);
+    $assert_session->fieldValueEquals('media_library_select_form[0]', $added_media->id());
+    $assert_session->checkboxChecked('media_library_select_form[0]');
+    $assert_session->pageTextContains('1 of 2 items selected');
+
+    // Ensure the created item is added in the widget.
+    $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+    $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextNotContains('Media library');
     $assert_session->pageTextContains($png_image->filename);
 
@@ -577,52 +655,77 @@ public function testWidgetUpload() {
     $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');
+
+    // Navigate to the media type three tab first.
+    $page->clickLink('Type Three');
     $assert_session->assertWaitOnAjaxRequest();
 
+    // Select a media item.
+    $page->find('css', '.media-library-view .js-click-to-select-checkbox input')->click();
+    $assert_session->pageTextContains('1 item selected');
+
     // Multiple uploads should be allowed.
     // @todo Add test when https://github.com/minkphp/Mink/issues/358 is closed
-    $this->assertTrue($assert_session->fieldExists('Upload')->hasAttribute('multiple'));
+    $this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
 
-    $page->attachFileToField('Upload', $this->container->get('file_system')->realpath($png_image->uri));
+    $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
     $assert_session->assertWaitOnAjaxRequest();
     $page->fillField('Name', 'Unlimited Cardinality Image');
     $page->fillField('Alternative text', $this->randomString());
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save');
     $assert_session->assertWaitOnAjaxRequest();
 
-    // Ensure the media item was added.
+    // Load the created media item.
+    $media_storage = $this->container->get('entity_type.manager')->getStorage('media');
+    $media_items = $media_storage->loadMultiple();
+    $added_media = array_pop($media_items);
+
+    // Ensure the media item was saved to the library and automatically
+    // selected. The added media items should be in the first position of the
+    // add form.
+    $assert_session->pageTextContains('Media library');
+    $assert_session->pageTextContains('Unlimited Cardinality Image');
+    $assert_session->fieldValueEquals('media_library_select_form[0]', $added_media->id());
+    $assert_session->checkboxChecked('media_library_select_form[0]');
+
+    // Assert the item that was selected before uploading the file is still
+    // selected.
+    $assert_session->pageTextContains('2 items selected');
+    $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(2, $selected_checkboxes);
+
+    // Ensure the created item is added in the widget.
+    $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+    $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextNotContains('Media library');
     $assert_session->pageTextContains('Unlimited Cardinality Image');
 
-    // Open the browser again to test type resolution.
+    // Verify we can only upload the files allowed by the media type.
     $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');
+    $page->clickLink('Type Four');
     $assert_session->assertWaitOnAjaxRequest();
 
-    $page->attachFileToField('Upload', $file_system->realpath($jpg_image->uri));
-    $assert_session->assertWaitOnAjaxRequest();
-
-    $assert_session->pageTextContains('Select a media type for ' . $jpg_image->filename);
-
-    // Before the type is determined, the file lives in the default upload
-    // location (temporary://).
-    $files = $file_storage->loadMultiple();
-    $file = array_pop($files);
-    $this->assertSame('temporary', $file_system->uriScheme($file->getFileUri()));
+    // Assert we can now only upload one more media item.
+    $this->assertFalse($assert_session->fieldExists('Add file')->hasAttribute('multiple'));
+    $assert_session->pageTextContains('One file only.');
 
-    // Both the type_three and type_four media types accept jpg images.
-    $assert_session->buttonExists('Type Three');
-    $assert_session->buttonExists('Type Four')->click();
+    // Assert media type four should only allow jpg files by trying a png file
+    // first.
+    $page->attachFileToField('Add file', $file_system->realpath($png_image->uri));
     $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('Only files with the following extensions are allowed');
 
-    // The file should have been moved when the type was selected.
-    $files = $file_storage->loadMultiple();
-    $file = array_pop($files);
-    $this->assertSame('public://type-four-dir', $file_system->dirname($file->getFileUri()));
-    $this->assertSame($assert_session->fieldExists('Name')->getValue(), $jpg_image->filename);
+    // Assert that jpg files are accepted by type four.
+    $page->attachFileToField('Add file', $file_system->realpath($jpg_image->uri));
+    $assert_session->assertWaitOnAjaxRequest();
     $page->fillField('Alternative text', $this->randomString());
 
     // The type_four media type has another optional image field.
@@ -637,6 +740,14 @@ public function testWidgetUpload() {
     $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Save');
     $assert_session->assertWaitOnAjaxRequest();
 
+    // Ensure the media item was saved to the library and automatically
+    // selected.
+    $assert_session->pageTextContains('Media library');
+    $assert_session->pageTextContains($jpg_image->filename);
+
+    // Ensure the created item is added in the widget.
+    $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+    $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextNotContains('Media library');
     $assert_session->pageTextContains($jpg_image->filename);
   }
diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..78d12f50ff3e699403b7ebfa43507f6c341dd74c
--- /dev/null
+++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php
@@ -0,0 +1,111 @@
+<?php
+
+namespace Drupal\Tests\media_library\Kernel;
+
+use Drupal\Core\Form\FormState;
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\media_library\Form\FileUploadForm;
+use Drupal\media_library\MediaLibraryState;
+use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
+
+/**
+ * Tests the media library add form.
+ *
+ * @group media_library
+ */
+class MediaLibraryAddFormTest extends KernelTestBase {
+
+  use MediaTypeCreationTrait;
+  use UserCreationTrait;
+
+  /**
+   * {@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', 'key_value_expire']);
+    $this->installEntitySchema('media');
+    $this->installConfig([
+      'field',
+      'system',
+      'file',
+      'image',
+      'media',
+      'media_library',
+    ]);
+
+    // Create an account with special UID 1.
+    $this->createUser([]);
+
+    $this->createMediaType('image', ['id' => 'image']);
+    $this->createMediaType('oembed:video', ['id' => 'remote_video']);
+  }
+
+  /**
+   * Tests the media library add form.
+   */
+  public function testMediaTypeAddForm() {
+    $entity_type_manager = \Drupal::entityTypeManager();
+    $image = $entity_type_manager->getStorage('media_type')->load('image');
+    $remote_video = $entity_type_manager->getStorage('media_type')->load('remote_video');
+    $image_source_definition = $image->getSource()->getPluginDefinition();
+    $remote_video_source_definition = $remote_video->getSource()->getPluginDefinition();
+
+    // Assert the form class is added to the media source.
+    $this->assertSame(FileUploadForm::class, $image_source_definition['forms']['media_library_add']);
+    $this->assertArrayNotHasKey('media_library_add', $remote_video_source_definition['forms']);
+
+    // Assert the media library UI does not contains the add form when the user
+    // does not have access.
+    $state = MediaLibraryState::create('test', ['image', 'remote_video'], 'image', -1);
+    $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state);
+    $this->assertEmpty($library_ui['content']['form']);
+
+    // Create a user that has access to the media add form.
+    $this->setCurrentUser($this->createUser([
+      'create image media',
+    ]));
+    $library_ui = \Drupal::service('media_library.ui_builder')->buildUi($state);
+    $this->assertSame('managed_file', $library_ui['content']['form']['upload']['#type']);
+  }
+
+  /**
+   * Tests the validation of the library state in the media library add form.
+   */
+  public function testFormStateValidation() {
+    $form_state = new FormState();
+    $this->setExpectedException(\InvalidArgumentException::class, 'The media library state is not present in the form state.');
+    \Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state);
+  }
+
+  /**
+   * Tests the validation of the selected type in the media library add form.
+   */
+  public function testSelectedTypeValidation() {
+    $state = MediaLibraryState::create('test', ['image', 'remote_video', 'header_image'], 'header_image', -1);
+    $form_state = new FormState();
+    $form_state->set('media_library_state', $state);
+    $this->setExpectedException(\InvalidArgumentException::class, "The 'header_image' media type does not exist.");
+    \Drupal::formBuilder()->buildForm(FileUploadForm::class, $form_state);
+  }
+
+}