diff --git a/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml b/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml
new file mode 100644
index 0000000000..e082019a82
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_mode.media.add_inline.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+dependencies:
+  module:
+    - media
+id: media.add_inline
+label: 'File Upload'
+targetEntityType: media
+cache: true
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 406b45127b..767abfad83 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -39,6 +39,7 @@
  *       "edit" = "Drupal\media\MediaForm",
  *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
  *     },
+ *     "add_inline" = "Drupal\media\Form\MediaInlineForm",
  *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
  *     "views_data" = "Drupal\media\MediaViewsData",
  *     "route_provider" = {
diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php
new file mode 100644
index 0000000000..209b9d7c86
--- /dev/null
+++ b/core/modules/media/src/Form/MediaInlineForm.php
@@ -0,0 +1,233 @@
+<?php
+
+namespace Drupal\media\Form;
+
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\Element;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Media entity inline form handler.
+ *
+ * @internal
+ */
+class MediaInlineForm implements EntityHandlerInterface {
+
+   /**
+   * The entity type managed by this handler.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * Constructs the inline entity form controller.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   */
+  public function __construct(EntityTypeInterface $entity_type) {
+    $this->entityType = $entity_type;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static($entity_type);
+  }
+
+  /**
+   * Builds the entity form.
+   *
+   * @param array $entity_form
+   *   The entity form, containing the following basic properties:
+   *   - #entity: The entity for the current entity form.
+   *   - #form_mode: The form mode used to display the entity form.
+   *   - #parents: Identifies the position of the entity form in the overall
+   *     parent form, and identifies the location where the field values are
+   *     placed within $form_state->getValues().
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state of the parent form.
+   *
+   * @return array
+   *   The entity form array.
+   */
+  public function entityForm(array $entity_form, FormStateInterface $form_state) {
+    // Build the media entity form.
+    /** @var \Drupal\media\MediaInterface $entity */
+    $entity = $entity_form['#entity'];
+    $form_display = EntityFormDisplay::collectRenderDisplay($entity, $entity_form['#form_mode']);
+    $form_display->buildForm($entity, $entity_form, $form_state);
+
+    // In the service of keeping this form as user-friendly as possible in the
+    // context of a parent entity form, only show required fields.
+    /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */
+    foreach ($entity->getFieldDefinitions() as $field_definition) {
+      $field_name = $field_definition->getName();
+      if (isset($entity_form[$field_name])) {
+        $entity_form[$field_name]['#access'] = $field_definition->isRequired();
+
+        if ($field_definition->isRequired()) {
+          // Elements with "#required" key set will always be validated, even if
+          // '#limit_validation_errors' is set. Disable their validation here,
+          // we will enforce validation happens inside the real submit handler.
+          $entity_form[$field_name] = $this->disableElementChildrenValidation($entity_form[$field_name]);
+        }
+      }
+    }
+    // The media name will be automatically populated by the source plugin. We
+    // are OK with the default name in this case.
+    if (isset($entity_form['name'])) {
+      $entity_form['name']['#access'] = FALSE;
+    }
+
+    // By this point it is expected that the source field has already been
+    // populated, so hide it too. An example for the file widget can be found in
+    // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::value().
+    $source_field = $entity->getSource()
+      ->getSourceFieldDefinition($entity->bundle->entity)
+      ->getName();
+    $entity_form[$source_field]['#access'] = FALSE;
+
+    // If this is an image media entity, and the image field on that entity has
+    // either "Alt field required" or "Title field required", we take care of
+    // that in MediaImageWidget::process().
+    // @see \Drupal\media\Plugin\Field\FieldWidget\MediaImageWidget::process().
+    foreach (['#alt_field_required', '#title_field_required'] as $extra_image_field_attribute) {
+      if (isset($entity_form[$source_field]['widget'][0][$extra_image_field_attribute])) {
+        $entity_form[$source_field]['widget'][0][$extra_image_field_attribute]['#access'] = FALSE;
+      }
+    }
+
+    // Inline entities inherit the parent language, so hide translation-related
+    // fields as well.
+    if (isset($entity_form['langcode'])) {
+      $entity_form['langcode']['#access'] = FALSE;
+    }
+
+    return $entity_form;
+  }
+
+  /**
+   * Validates the entity form.
+   *
+   * @param array $entity_form
+   *   The entity form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state of the parent form.
+   */
+  public function entityFormValidate(array &$entity_form, FormStateInterface $form_state) {
+    // Perform entity validation only if the inline form was submitted,
+    // skipping other requests such as file uploads.
+    if (!$this->hostFormSubmitted($form_state)) {
+      return;
+    }
+    $mids = [];
+    // @todo use #parents if possible.
+    $field_values = $form_state->getValue($entity_form['#field_name']);
+    if ($field_values) {
+      foreach ($field_values as $field_value) {
+        if (!empty($field_value['mids'])) {
+          $mids[] = reset($field_value['mids']);
+        }
+      }
+    }
+    foreach ($mids as $mid) {
+      $media_form_key = 'media_' . $mid;
+      // @todo throw error in this case instead of quietly continuing.
+      if (empty($entity_form[$media_form_key]['form_wrapper']['form'])) {
+        continue;
+      }
+      $media_form = $entity_form[$media_form_key]['form_wrapper']['form'];
+      /** @var \Drupal\media\MediaInterface $entity */
+      $entity = $media_form['#entity'];
+      $triggering_element = $form_state->getTriggeringElement();
+      $this->buildEntity($media_form, $entity, $form_state);
+      $form_display = EntityFormDisplay::collectRenderDisplay($entity, $media_form['#form_mode']);
+      $form_display->validateFormValues($entity, $media_form, $form_state);
+      $entity->setValidationRequired(FALSE);
+
+      foreach ($form_state->getErrors() as $name => $message) {
+        // $name may be unknown in $form_state and
+        // $form_state->setErrorByName($name, $message) may suppress the error
+        // message.
+        $form_state->setError($triggering_element, $message);
+      }
+
+      // If no validation errors are present, set the media back to Published.
+      if (empty($form_state->getErrors())) {
+        $entity->setPublished()->save();
+      }
+    }
+  }
+
+  /**
+   * Bypass validation in all children of a given element.
+   *
+   * @param array $element
+   *   The element array.
+   *
+   * @return array
+   *   The same element array, after recursively checking all its children and
+   *   setting "#validated" => TRUE in all required elements.
+   */
+  private function disableElementChildrenValidation(array $element) {
+    foreach (Element::children($element) as $key) {
+      if (isset($element[$key]) && $element[$key]) {
+        $element[$key] = $this->disableElementChildrenValidation($element[$key]);
+      }
+    }
+
+    if (!empty($element['#needs_validation']) || !empty($element['#required'])) {
+      $element['#validated'] = TRUE;
+    }
+
+    return $element;
+  }
+
+  /**
+   * Builds an updated entity object based upon the submitted form values.
+   *
+   * @param array $entity_form
+   *   The entity form.
+   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+   *   The entity.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  protected function buildEntity(array $entity_form, ContentEntityInterface $entity, FormStateInterface $form_state) {
+    $form_display = EntityFormDisplay::collectRenderDisplay($entity, $entity_form['#form_mode']);
+    $form_display->extractFormValues($entity, $entity_form, $form_state);
+    // Invoke all specified builders for copying form values to entity fields.
+    if (isset($entity_form['#entity_builders'])) {
+      foreach ($entity_form['#entity_builders'] as $function) {
+        call_user_func_array($function, [$entity->getEntityTypeId(), $entity, &$entity_form, $form_state]);
+      }
+    }
+  }
+
+  /**
+   * Check if the form processing is due to a host submit.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The $form_state object.
+   *
+   * @return bool
+   *   TRUE if the form is being processed due to the host entity's form being
+   *   submitted (and not other AJAX-based submissions, such as uploads, etc.).
+   */
+  protected function hostFormSubmitted(FormStateInterface $form_state) {
+    $triggering_element = $form_state->getTriggeringElement();
+    // @TODO Figure out a more robust way of doing this.
+    if (strpos($triggering_element['#name'], '_button') === FALSE) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
new file mode 100644
index 0000000000..e77764b632
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,421 @@
+<?php
+
+namespace Drupal\media\Plugin\Field\FieldWidget;
+
+use Drupal\Core\Entity\EntityFormInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\ElementInfoManagerInterface;
+use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
+use Drupal\media\Entity\Media;
+use Drupal\media\Entity\MediaType;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Link;
+use Drupal\media\Plugin\media\Source\File;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Plugin implementation of the 'media_file' widget.
+ *
+ * @FieldWidget(
+ *   id = "media_file",
+ *   label = @Translation("File"),
+ *   field_types = {
+ *     "entity_reference"
+ *   }
+ * )
+ */
+class MediaFileWidget extends FileWidget {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info);
+  }
+
+  /**
+   * {@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('element_info')
+    );
+  }
+
+  /**
+   * Gets the default value structure this widget expects.
+   *
+   * @return array
+   */
+  protected static function getDefaultValues() {
+    return [
+      'mids' => [],
+      'fids' => [],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * Uses nearly the same code that FileWidget does here, except that we have to
+   * dig down through the target Media entity to *its* file field's upload
+   * location and upload validators.
+   */
+  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+    // We've ensured that there's only one target bundle. Get an instance of
+    // that bundle in order to get its source field.
+    // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::isApplicable()
+    $target_bundle = reset($this->fieldDefinition->getSetting('handler_settings')['target_bundles']);
+    /** @var \Drupal\media\Entity\MediaType $media_type */
+    $media_type = MediaType::load($target_bundle);
+    /** @var \Drupal\media\MediaInterface $media_item */
+    $media_item = Media::create(['bundle' => $target_bundle]);
+    $source_field_definition = $media_item->getSource()->getSourceFieldDefinition($media_type);
+    $source_field_items = $media_item->get($source_field_definition->getName());
+    $source_field_items->appendItem();
+
+    // Temporarily use the source field's fieldDefinition, so that
+    // FileWidget::formElement() returns a usable $element array.
+    // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget\FileWidget::formElement()
+    $real_field_definition = $this->fieldDefinition;
+    $this->fieldDefinition = $source_field_definition;
+    $element = parent::formElement($source_field_items, 0, $element, $form, $form_state);
+    $this->fieldDefinition = $real_field_definition;
+
+    // Now modify $element to account for the fact that it was originally built
+    // to reference file entities, whereas we are going to reference media
+    // entities.
+    $element['#element_validate'] = [
+      [static::class, 'validateEntityForm'],
+    ];
+    $element['#progress_indicator'] = $this->getSetting('progress_indicator');
+    $element['#field_name'] = $this->fieldDefinition->getName();
+    $element['#weight'] = $delta;
+
+    // Accommodate multiple uploads, if field settings require it.
+    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
+    $element['#cardinality'] = $cardinality;
+    $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE;
+    if ($cardinality != 1 && $cardinality != -1) {
+      $element['#element_validate'][] = [static::class, 'validateMultipleCount'];
+    }
+
+    // Save mid, the target_id value that this field ultimately cares about.
+    if (!isset($items[$delta]->mids) && isset($items[$delta]->target_id)) {
+      $items[$delta]->mids = [$items[$delta]->target_id];
+    }
+
+    // Finally, fill in any needed defaults that haven't already been set.
+    $element['#default_value'] = $items[$delta]->getValue() + static::getDefaultValues();
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    // Override FileWidget::massageFormValues() in order to reference Media
+    // entities instead of files.
+    $new_values = [];
+    foreach ($values as $value) {
+      $value['mids'] = !empty($value['mids']) ? $value['mids'] : [];
+      foreach ($value['mids'] as $mid) {
+        $new_value = $value;
+        $new_value['target_id'] = $mid;
+        unset($new_value['mids']);
+        $new_values[] = $new_value;
+      }
+    }
+
+    return $new_values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function value($element, $input, FormStateInterface $form_state) {
+    $return = parent::value($element, $input, $form_state);
+
+    $form_object = $form_state->getFormObject();
+    if (!$form_object instanceOf EntityFormInterface) {
+      throw new \LogicException('We are in bat country.');
+    }
+
+    // Although this method is mainly used as a value callback for real user-
+    // entered values, it is also called when site builders are configuring the
+    // field. In that case, $host_entity won't be a FieldableEntityInterface;
+    // just return the default value structure.
+    $host_entity =  $form_object->getEntity();
+    if (!$host_entity instanceof FieldableEntityInterface) {
+      return static::getDefaultValues();
+    }
+    // We're only allowing this widget to be used when there's a single target
+    // bundle. That means there's only one item in
+    // $handler_settings['target_bundles'].
+    // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::isApplicable()
+    $handler_settings = $host_entity->getFieldDefinitions()[$element['#field_name']]->getSetting('handler_settings');
+    $media_type = reset($handler_settings['target_bundles']);
+
+    // This widget target media references. Our element (managed_file) needs
+    // file ids, so load the file id for it to use.
+    $get_fid = isset($return['target_id']) && empty($return['fids']);
+
+    // The user has just uploaded file(s); create media accordingly.
+    $create_media = $input && empty($element['#default_value']['mids']) && !empty($return['fids']);
+
+    // The host form is now being saved with newly-created media entities. They
+    // might have required field values to save, so save those.
+    $resave_media = !empty($input['mids']);
+
+    if ($get_fid) {
+      $media_entity = Media::load($element['#default_value']['target_id']);
+      $return['fids'][] = $media_entity->getSource()->getSourceFieldValue($media_entity);
+    }
+
+    if ($create_media) {
+      foreach ($return['fids'] as $fid) {
+        /** @var \Drupal\media\MediaInterface $media_entity */
+        $media_entity = Media::create([
+          'bundle' => $media_type,
+          'uid' => \Drupal::currentUser()->id(),
+          'langcode' => $host_entity->language()->getId(),
+        ]);
+        // We create media items as unpublished to prevent "abandoned" uploads
+        // from producing visible media items on the site. In
+        // \Drupal\media\Form\MediaInlineForm::entityFormValidate() we make sure
+        // we set them back to published when the main form is submitted.
+        $media_entity->setUnpublished();
+        $source_field = $media_entity->getSource()
+          ->getSourceFieldDefinition($media_entity->bundle->entity)
+          ->getName();
+        $media_entity->set($source_field, $fid);
+        $media_entity->save();
+        $return['mids'][] = $media_entity->id();
+      }
+    }
+
+    if ($resave_media) {
+      $mid = $input['mids'];
+      $media_fields_key = 'media_' . $mid;
+
+      if (!empty($input[$media_fields_key])) {
+        $media_entity = Media::load($mid);
+        foreach ($input[$media_fields_key] as $field_name => $field_value) {
+          $media_entity->set($field_name, $field_value);
+        }
+        $media_entity->save();
+      }
+      else {
+        // Warn process() that this is a preexisting value.
+        $return['media_already_saved'] = TRUE;
+      }
+      $return['mids'] = [$mid];
+    }
+
+    return $return;
+  }
+
+  /**
+   * Expands the file element to include the inline version of the media form.
+   */
+  public static function process($element, FormStateInterface $form_state, $form) {
+    $element = parent::process($element, $form_state, $form);
+
+    // For readability, working with $value rather than $element['#value']. For
+    // how $element['#value'] gets set, see massageFormValues() in our related
+    // classes.
+    // * @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::massageFormValues()
+    // * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::massageFormValues()
+    $value = isset($element['#value']) ? $element['#value'] : [];
+
+    // $value['mids'] would have been set in MediaFileWidget::value() in the
+    // cases that a) the user has just uploaded a file or files, or b) they are
+    // viewing a form where a media item had been previously uploaded.
+    // @see \Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget::value()
+    $mids = isset($value['mids']) ? $value['mids'] : [];
+    $element['mids'] = [
+      '#type' => 'hidden',
+      '#value' => $mids,
+    ];
+
+    $mid = FALSE;
+    $show_form = FALSE;
+
+    if (!empty($value['target_id'])) {
+      // When a media entity reference value was previously saved, then loaded
+      // again on this host entity form, it has this 'target_id' key. So we know
+      // this is an existing entity ref; just show the icon and name.
+      $mid = $value['target_id'];
+    }
+    elseif (!empty($value['mids']) && count($value['mids']) === 1) {
+      // If $element['mids']['#value'] has multiple values, that means this is
+      // the first pass after someone uploaded several files; ignore. If it has
+      // only ONE, then this media entity was just created and we want to force
+      // the user to fill out all the new entity's required fields. I.e., show
+      // the form, as well as the standard icon and name.
+      $mid = reset($value['mids']);
+
+      if (empty($value['media_already_saved'])) {
+        $show_form = TRUE;
+      }
+    }
+
+    // $mid can be false here, in the event that this value is the final one
+    // for the media_managed_file element. That value becomes the upload button.
+    if ($mid) {
+      // First, get rid of the file link that FileManaged has created.
+      $fid = reset($value['fids']);
+      unset($element['file_' . $fid]);
+
+      // Under some circumstances this button said "remove selected," but since
+      // we just removed the checkbox this button should instead always just say
+      // "remove."
+      $element['remove_button']['#value'] = t('Remove');
+      // Also hide the upload button.
+      $element['upload']['#access'] = FALSE;
+
+      // Now show the media icon and name.
+      $form_element_name = 'media_' . $mid;
+      $form_element_parents = array_merge($element['#parents'], [$form_element_name]);
+      $media_entity = Media::load($mid);
+
+      // @todo: entity query to be sure whether or not thumbnail image style
+      // actually exists. If not stick the user with the "original" image style.
+      $element[$form_element_name]['icon'] = [
+        '#theme' => 'image_style',
+        '#style_name' => 'thumbnail',
+        '#uri' => $media_entity->getSource()->getMetadata($media_entity, 'thumbnail_uri'),
+        '#weight' => -20,
+      ];
+      $link_build = Link::createFromRoute($media_entity->getName(), 'entity.media.canonical', ['media' => $mid])->toRenderable();
+      $element[$form_element_name]['link'] = $link_build + ['#weight' => -10];
+
+      if ($show_form) {
+        // Create and add the media entity form.
+        $form_mode = 'add_inline';
+        $media_form = [
+          '#entity' => $media_entity,
+          '#form_mode' => $form_mode,
+          '#parents' => $form_element_parents,
+        ];
+        $inline_form_handler = \Drupal::entityTypeManager()
+          ->getHandler('media', $form_mode);
+        $media_form = $inline_form_handler->entityForm($media_form, $form_state);
+        $element[$form_element_name]['form_wrapper'] = [
+          '#type' => 'container',
+          'form' => $media_form,
+        ];
+      }
+    }
+
+    return $element;
+  }
+
+  /**
+   * Validates the media form using our custom form handler.
+   *
+   * @param array $entity_form
+   *   The entity form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public static function validateEntityForm(array &$entity_form, FormStateInterface $form_state) {
+    \Drupal::entityTypeManager()->getHandler('media', 'add_inline')
+      ->entityFormValidate($entity_form, $form_state);
+  }
+
+  /**
+   * Form submission handler for upload/remove button of formElement().
+   *
+   * Overrides FileWidget::submit(), performing the exact same functionality,
+   * only maintaining awareness of mids as well as fids.
+   *
+   * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit()
+   * @see file_managed_file_submit()
+   */
+  public static function submit($form, FormStateInterface $form_state) {
+    // During the form rebuild, formElement() will create field item widget
+    // elements using re-indexed deltas, so clear out FormState::$input to
+    // avoid a mismatch between old and new deltas. The rebuilt elements will
+    // have #default_value set appropriately for the current state of the field,
+    // so nothing is lost in doing this.
+    $button = $form_state->getTriggeringElement();
+    $parents = array_slice($button['#parents'], 0, -2);
+    NestedArray::setValue($form_state->getUserInput(), $parents, NULL);
+
+    // Go one level up in the form, to the widgets container.
+    $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $field_name = $element['#field_name'];
+    $parents = $element['#field_parents'];
+
+    $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2));
+    foreach ($submitted_values as $delta => $submitted_value) {
+      if (empty($submitted_value['fids'])) {
+        unset($submitted_values[$delta]);
+      }
+    }
+
+    // If there are more files uploaded via the same widget, we have to separate
+    // them, as we display each file in its own widget.
+    $new_values = [];
+    foreach ($submitted_values as $submitted_value) {
+      if (is_array($submitted_value['fids']) && is_array($submitted_value['mids'])) {
+        foreach ($submitted_value['fids'] as $delta => $fid) {
+          $new_value = $submitted_value;
+          $new_value['fids'] = [$fid];
+          $new_value['mids'] = [$submitted_value['mids'][$delta]];
+          $new_values[] = $new_value;
+        }
+      }
+    }
+
+    // Re-index deltas after removing empty items.
+    $submitted_values = array_values($new_values);
+
+    // Update form_state values.
+    NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values);
+
+    // Update items.
+    $field_state = static::getWidgetState($parents, $field_name, $form_state);
+    $field_state['items'] = $submitted_values;
+    static::setWidgetState($parents, $field_name, $form_state, $field_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static final function isApplicable(FieldDefinitionInterface $field_definition) {
+    // This widget is restricted to entity_reference fields pointing to media
+    // entities.
+    if ($field_definition->getSetting('target_type') !== 'media') {
+      return FALSE;
+    }
+
+    // This widget can be chosen only if the field points to a single target
+    // Media Type, and this type has a source of type "File".
+    $handler_settings = $field_definition->getSetting('handler_settings');
+    if (empty($handler_settings['target_bundles']) || count($handler_settings['target_bundles']) !== 1) {
+      return FALSE;
+    }
+    $type_name = reset($handler_settings['target_bundles']);
+    $media_type = MediaType::load($type_name);
+    if ($media_type) {
+      $source = $media_type->getSource();
+      if ($source instanceof File) {
+        return TRUE;
+      }
+    }
+
+    return FALSE;
+  }
+
+}
diff --git a/core/modules/media/tests/fixtures/example_1.pdf b/core/modules/media/tests/fixtures/example_1.pdf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/core/modules/media/tests/fixtures/example_2.pdf b/core/modules/media/tests/fixtures/example_2.pdf
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php
new file mode 100644
index 0000000000..d3707281f6
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php
@@ -0,0 +1,276 @@
+<?php
+
+namespace Drupal\Tests\media\FunctionalJavascript;
+
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+
+/**
+ * @group media
+ */
+class MediaInlineFileWidgetTest extends MediaJavascriptTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node'];
+
+  protected $strictConfigSchema = FALSE;
+
+  /**
+   * The bundle name of the test node.
+   *
+   * @var string
+   */
+  protected $nodeTypeId;
+
+  /**
+   * The bundle name of the test media.
+   *
+   * @var string
+   */
+  protected $mediaTypeId;
+
+  /**
+   * An indexed array of valid paths for test files.
+   *
+   * @var array
+   */
+  protected $testFilePaths;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $media_type = $this->createMediaType(['bundle' => 'file'], 'file');
+    $this->mediaTypeId = $media_type->id();
+    // Create two test fields on this media type, one of them required.
+    $media_required_field_storage = FieldStorageConfig::create([
+      'type' => 'string',
+      'entity_type' => 'media',
+      'field_name' => 'field_media_required',
+      'settings' => [
+        'max_length' => 255,
+      ],
+    ]);
+    $media_required_field_storage->save();
+    FieldConfig::create([
+      'field_storage' => $media_required_field_storage,
+      'bundle' => $this->mediaTypeId,
+      'required' => TRUE,
+    ])->save();
+    entity_get_form_display('media', $media_type->id(), 'add_inline')
+      ->setComponent('field_media_required', ['type' => 'string_textfield'])
+      ->save();
+    $media_nonrequired_field_storage = FieldStorageConfig::create([
+      'type' => 'string',
+      'entity_type' => 'media',
+      'field_name' => 'field_media_nonrequired',
+      'settings' => [
+        'max_length' => 255,
+      ],
+    ]);
+    $media_nonrequired_field_storage->save();
+    FieldConfig::create([
+      'field_storage' => $media_nonrequired_field_storage,
+      'bundle' => $this->mediaTypeId,
+      'required' => FALSE,
+    ])->save();
+    // Deliberately add it to the form, to test that even then we don't show
+    // this field when embedding the form inside the widget.
+    entity_get_form_display('media', $this->mediaTypeId, 'add_inline')
+      ->setComponent('field_media_nonrequired', ['type' => 'string_textfield'])
+      ->save();
+    entity_get_display('media', $this->mediaTypeId, 'default')
+      ->setComponent('field_media_file', [
+        'type' => 'file_default',
+        'label' => 'hidden',
+        'settings' => [],
+      ])
+      ->setComponent('field_media_required', [
+        'type' => 'string',
+        'label' => 'hidden',
+        'settings' => [],
+      ])
+      ->setComponent('field_media_nonrequired', [
+        'type' => 'string',
+        'label' => 'hidden',
+        'settings' => [],
+      ])
+      ->save();
+
+    $node_type = $this->drupalCreateContentType();
+    $this->nodeTypeId = $node_type->id();
+    $field_storage = FieldStorageConfig::create([
+      'type' => 'entity_reference',
+      'cardinality' => -1,
+      'entity_type' => 'node',
+      'field_name' => 'field_media_reference',
+      'settings' => [
+        'target_type' => 'media',
+      ],
+    ]);
+    $field_storage->save();
+    FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => $this->nodeTypeId,
+      'settings' => [
+        'handler_settings' => [
+          'target_bundles' => [
+            $this->mediaTypeId => $this->mediaTypeId,
+          ],
+        ],
+      ],
+    ])->save();
+    entity_get_form_display('node', $this->nodeTypeId, 'default')
+      ->setComponent('field_media_reference', ['type' => 'media_file'])
+      ->save();
+    entity_get_display('node', $this->nodeTypeId, 'default')
+      ->setComponent('field_media_reference', [
+        'type' => 'entity_reference_entity_view',
+        'label' => 'hidden',
+        'settings' => [
+          'view_mode' => 'full',
+        ],
+      ])->save();
+
+    // Create a couple of files for testing.
+    for ($i = 0; $i < 2; $i++) {
+      $test_filename = $this->randomMachineName() . '.txt';
+      $test_filepath = 'public://' . $test_filename;
+      file_put_contents($test_filepath, $this->randomMachineName());
+      $this->testFilePaths[$i] = \Drupal::service('file_system')->realpath($test_filepath);
+    }
+
+  }
+
+  /**
+   * Tests the file widget behavior.
+   */
+  public function testInlineFileWidget() {
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    $this->drupalGet('/node/add/' . $this->nodeTypeId);
+
+    // Ensure the media field is present, but no required fields exist in there.
+    $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]');
+    $assert_session->elementNotExists('css', '.required', $media_field);
+
+    // Upload a file and see that new required fields appeared.
+    $first_upload_element_id = 'edit-field-media-reference-0-upload';
+    $page->attachFileToField($first_upload_element_id, $this->testFilePaths[0]);
+    $result = $assert_session->waitForButton('Remove');
+    $this->assertNotEmpty($result);
+    $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]');
+    // The required field sould be present and required.
+    $assert_session->elementExists('css', '.field--name-field-media-required input.required', $media_field);
+    // The non-required field should not be there.
+    $assert_session->elementNotExists('css', '.field--name-field-media-nonrequired input', $media_field);
+    // Since cardinality is -1, we should be allowed to upload another file now.
+    $assert_session->elementExists('css', 'input[name="files[field_media_reference_1][]"]');
+
+    // By now the media entity should already exist, but unpublished.
+    $filename1 = basename($this->testFilePaths[0]);
+    /** @var \Drupal\media\MediaInterface $media */
+    $media = $this->container->get('entity_type.manager')->getStorage('media')
+      ->loadByProperties(['name' => $filename1]);
+    $media = reset($media);
+    $this->assertNotNull($media);
+    $this->assertFalse($media->isPublished());
+
+    // Save the node and check page elements correspond to what is expected.
+    $node_title = 'Host Node 1';
+    $page->fillField('Title', $node_title);
+    $required_text = $this->randomMachineName();
+    $page->fillField('field_media_required', $required_text);
+    $page->pressButton('Save');
+    // The node has been correctly saved.
+    $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been created");
+    // We have a reference media container.
+    $media_field = $assert_session->elementExists('css', '.field--name-field-media-reference');
+    // We have a file link inside that container.
+    $assert_session->elementExists('css', '.field--name-field-media-file a', $media_field);
+    // The required field is present.
+    $required_element = $assert_session->elementExists('css', '.field--name-field-media-required', $media_field);
+    // The required field text is what we expect.
+    $this->assertEquals($required_text, $required_element->getText());
+
+    // The media item should now be published.
+    $media = $this->container->get('entity_type.manager')->getStorage('media')
+      ->loadUnchanged($media->id());
+    /** @var \Drupal\media\MediaInterface $media */
+    $this->assertTrue($media->isPublished());
+
+    // Edit the node, add a second file, check the widget supports multiple
+    // values and creates a second media without messing up.
+    $node = $this->container->get('entity_type.manager')->getStorage('node')
+      ->loadByProperties(['title' => $node_title]);
+    $node = reset($node);
+    $this->drupalGet("/node/{$node->id()}/edit");
+    // First file is still there, and remove button is present.
+    $assert_session->elementContains('css', '#edit-field-media-reference-table', $filename1);
+    $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]');
+    $remove_button = $media_field->findButton('Remove');
+    $this->assertNotNull($remove_button);
+    $second_upload_element_id = 'edit-field-media-reference-1-upload';
+    // Upload the second file.
+    $filename2 = basename($this->testFilePaths[1]);
+    $page->attachFileToField($second_upload_element_id, $this->testFilePaths[1]);
+    $assert_session->assertWaitOnAjaxRequest();
+
+    // Fill in the second media required field and save.
+    $required_text2 = $this->randomMachineName();
+    $page->fillField('field_media_required', $required_text2);
+    $page->pressButton('Save');
+
+    // Check everything was saved correctly.
+    $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been updated");
+    $assert_session->pageTextContains($filename1);
+    $assert_session->pageTextContains($filename2);
+    $assert_session->pageTextContains($required_text);
+    $assert_session->pageTextContains($required_text2);
+
+    // Both media entities should exist now.
+    foreach ([$filename1, $filename2] as $filename) {
+      $media = $this->container->get('entity_type.manager')->getStorage('media')
+        ->loadByProperties(['name' => $filename]);
+      $media = reset($media);
+      /** @var \Drupal\media\MediaInterface $media */
+      $this->assertTrue($media->isPublished());
+    }
+
+    // Edit the node, verify we can successfuly remove one of the items.
+    $this->drupalGet("/node/{$node->id()}/edit");
+    $assert_session->pageTextContains($filename1);
+    $assert_session->pageTextContains($filename2);
+    $assert_session->buttonExists('edit-field-media-reference-0-remove-button');
+    $page->pressButton('edit-field-media-reference-0-remove-button');
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextNotContains($filename1);
+    $assert_session->pageTextContains($filename2);
+    $page->pressButton('Save');
+    $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been updated");
+    $assert_session->pageTextNotContains($filename1);
+    $assert_session->pageTextContains($filename2);
+    $assert_session->pageTextNotContains($required_text);
+    $assert_session->pageTextContains($required_text2);
+
+    // Removing the file without saving the node should also be possible.
+    $this->drupalGet('/node/add/' . $this->nodeTypeId);
+    $first_upload_element_id = 'edit-field-media-reference-0-upload';
+    $page->attachFileToField($first_upload_element_id, $this->testFilePaths[0]);
+    $result = $assert_session->waitForButton('Remove');
+    $this->assertNotEmpty($result);
+    $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]');
+    // The additional field sould be present and required.
+    $assert_session->elementExists('css', '.field--name-field-media-required input.required', $media_field);
+    $page->pressButton('Remove');
+    $assert_session->assertWaitOnAjaxRequest();
+    $media_field = $assert_session->elementExists('css', 'details[data-drupal-selector="edit-field-media-reference"]');
+    // The non-required field should have disappeared.
+    $assert_session->elementNotExists('css', '.field--name-field-media-required input.required', $media_field);
+  }
+
+}
