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..ec5bd7e5ef
--- /dev/null
+++ b/core/modules/media/src/Form/MediaInlineForm.php
@@ -0,0 +1,228 @@
+<?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 Drupal\media\Plugin\Field\FieldWidget\MediaFileWidget;
+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;
+    }
+    $delta = $entity_form['#delta'];
+    $field_name = $entity_form['#field_name'];
+    $field_value = $form_state->getValue([$field_name, $delta]);
+    if (!empty($field_value['fids'])) {
+      $fid = reset($field_value['fids']);
+      $media_form_key = 'file_' . $fid;
+      if (empty($entity_form[$media_form_key]['form_wrapper']['form'])) {
+        return;
+      }
+      $media_form = $entity_form[$media_form_key]['form_wrapper']['form'];
+      /** @var \Drupal\media\MediaInterface $media_entity */
+      $media_entity = MediaFileWidget::getParentMedia($form_state, $fid);
+      $triggering_element = $form_state->getTriggeringElement();
+      $this->buildEntity($media_form, $media_entity, $form_state);
+      $form_display = EntityFormDisplay::collectRenderDisplay($media_entity, $media_form['#form_mode']);
+      $form_display->validateFormValues($media_entity, $media_form, $form_state);
+      $media_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, publish and save the media.
+      if (empty($form_state->getErrors())) {
+        $media_entity->setPublished()->save();
+        MediaFileWidget::setParentMedia($form_state, $fid, $media_entity);
+      }
+    }
+  }
+
+  /**
+   * 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..ce0beb607e
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,330 @@
+<?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\MediaInterface;
+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')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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::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 AND this is
+    // the empty upload-field-only "value" provided by FileWidget.
+    // @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formMultipleElements()
+    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
+    $multiple = ($cardinality != 1 && $items[$delta]->isEmpty() && empty($items[$delta]->getValue()['fids']));
+    $element['#cardinality'] = $cardinality;
+    $element['#multiple'] = $multiple;
+    if ($cardinality != 1 && $cardinality != -1 && $multiple) {
+      $element['#element_validate'][] = [static::class, 'validateMultipleCount'];
+    }
+
+    $item = $items->get($delta);
+    if (!$item->isEmpty() && !$form_state->has('media_entities')) {
+      $fid = $item->entity->getSource()->getSourceFieldValue($item->entity);
+      static::setParentMedia($form_state, $fid, $item->entity);
+    }
+
+    // Return the default value, filling in an empty fids value if none exists.
+    $element['#default_value'] = $items[$delta]->getValue() + ['fids' => []];
+
+    return $element;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+    // Only validate saved media entities.
+    // @see submit()
+    $values = parent::massageFormValues($values, $form, $form_state);
+    foreach ($values as $delta => $value) {
+      $fid = isset($value['target_id']) ? $value['target_id'] : 0;
+      $media_entity = static::getParentMedia($form_state, $fid);
+      if ($media_entity && !$media_entity->isNew()) {
+        $values[$delta]['target_id'] = $media_entity->id();
+      }
+      else {
+        unset($values[$delta]);
+      }
+    }
+    return $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('MediaFileWidget::value() should only be used by entity forms.');
+    }
+    $host_entity = $form_object->getEntity();
+
+    // If we have a target_id, it's a mid; we're loading a previously-saved
+    // media entity reference. Translate it to a fid for ManagedFile to use and
+    // store the media entity for later.
+    if (!empty($return['target_id'])) {
+      /** @var \Drupal\media\MediaInterface $media_entity */
+      $media_entity = Media::load($return['target_id']);
+      $fid = $media_entity->getSource()->getSourceFieldValue($media_entity);
+      $return['fids'][] = $fid;
+
+      static::setParentMedia($form_state, $fid, $media_entity);
+    }
+    // The user has just uploaded new file(s). In this case, create a temporary,
+    // unsaved media entity for each file, and store it.
+    else if (!empty($return['fids'])) {
+      foreach ($return['fids'] as $fid) {
+        $media_entity = static::getParentMedia($form_state, $fid);
+        if (empty($media_entity)) {
+          // This widget can only be used when there's a single target bundle,
+          // so 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']);
+
+          /** @var \Drupal\media\MediaInterface $media_entity */
+          $media_entity = Media::create([
+            'bundle' => $media_type,
+            'uid' => \Drupal::currentUser()->id(),
+            'langcode' => $host_entity->language()->getId(),
+          ]);
+          $media_entity->setUnpublished();
+          $source_field = $media_entity->getSource()
+            ->getSourceFieldDefinition($media_entity->bundle->entity)
+            ->getName();
+          $media_entity->set($source_field, $fid);
+
+          static::setParentMedia($form_state, $fid, $media_entity);
+        }
+      }
+    }
+    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'].
+    $value = isset($element['#value']) ? $element['#value'] : [];
+
+    // MediaFileWidget::value() stored the parent media entity for this fid if
+    // a) the user 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()
+    $fid = count($value['fids']) === 1 ? reset($value['fids']) : FALSE;
+    /** @var MediaInterface $media_entity */
+    $media_entity = static::getParentMedia($form_state, $fid);
+
+    // $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 ($media_entity) {
+      // Show the media icon and name in the element ManagedFile created.
+      $form_element_name = 'file_' . $fid;
+
+      /** @var \Drupal\image\ImageStyleInterface $image_style */
+      $element[$form_element_name]['icon'] = [
+        '#theme' => 'image',
+        '#uri' => $media_entity->getSource()->getMetadata($media_entity, 'thumbnail_uri'),
+        '#weight' => -20,
+      ];
+
+      /** @var \Drupal\image\ImageStyleInterface $style */
+      if ($style = \Drupal::entityTypeManager()->getStorage('image_style')->load('thumbnail')) {
+        $element[$form_element_name]['icon']['#theme'] = 'image_style';
+        $element[$form_element_name]['icon']['#style_name'] = $style->getName();
+      }
+
+      if ($media_entity->isNew()) {
+        // Create and add the media entity form.
+        $form_mode = 'add_inline';
+        $media_form = [
+          '#entity' => $media_entity,
+          '#form_mode' => $form_mode,
+          '#parents' => array_merge($element['#parents'], [$form_element_name]),
+        ];
+        $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);
+  }
+
+  /**
+   * {@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;
+  }
+
+  /**
+   * Internal method to get stored media entities.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *  The host form state, where we've stored the fid-to-media map.
+   * @param $fid
+   *  The file id we're being asked about. If 'all', return the whole array.
+   *
+   * @return bool|mixed
+   */
+  public static function getParentMedia(FormStateInterface $form_state, $fid = 'all') {
+    // @todo Debug this to use $form_state storage rather than tempstore.
+    //    $media_entities = $form_state->get('media_entities');
+    $tempstore = \Drupal::service('user.private_tempstore')->get('media');
+    $media_entities = $tempstore->get('media_entities');
+    if ($fid && is_array($media_entities)) {
+      if ($fid == 'all') {
+        return $media_entities;
+      }
+      if (array_key_exists($fid, $media_entities)) {
+        return $media_entities[$fid];
+      }
+    }
+    return FALSE;
+  }
+
+  /**
+   * Internal method to map fids to media entities.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *  The host form state.
+   * @param $fid
+   *  The fid of the source file.
+   * @param \Drupal\media\MediaInterface $media_entity
+   *  The media entity that uses this file as a source.
+   */
+  public static function setParentMedia(FormStateInterface $form_state, $fid, MediaInterface $media_entity) {
+    // @todo Debug this to use $form_state storage rather than tempstore.
+    //    $media_entities = $form_state->get('media_entities') ?: [];
+    //    $media_entities[$fid] = $media_entity;
+    //    $form_state->set('media_entities', $media_entities);
+    $tempstore = \Drupal::service('user.private_tempstore')->get('media');
+    $media_entities = $tempstore->get('media_entities');
+    $media_entities[$fid] = $media_entity;
+    $tempstore->set('media_entities', $media_entities);
+  }
+
+}
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..df17159c8d
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php
@@ -0,0 +1,279 @@
+<?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', 'options'];
+
+  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' => 'list_string',
+      'entity_type' => 'media',
+      'field_name' => 'field_media_required',
+      'settings' => [
+        'allowed_values' => [
+          'foo' => 'Foo',
+          'bar' => 'Bar',
+        ],
+      ],
+    ]);
+    $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' => 'options_select'])
+      ->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' => 'list_default',
+        '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 select.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 = 'foo';
+    $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 = 'bar';
+    $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);
+  }
+
+}
