From d772fbb00bc99ab6c94564ce660731db78d7db1b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ga=CC=81bor=20Hojtsy?= <gabor@hojtsy.hu>
Date: Mon, 18 Mar 2019 11:56:39 +0100
Subject: [PATCH] Issue #3023801 by seanB, DamienMcKenna, lauriii,
 phenaproxima, Berdir, Pancho, marcoscano, Wim Leers, rainbreaw, larowlan,
 webchick, jrockowitz, andrewmacpherson, ckrina: Allow newly uploaded files to
 be deleted from the media library without saving them

---
 core/misc/icons/ee0000/ex.svg                 |   1 +
 .../media_library/css/media_library.theme.css | 148 ++++++++++++++---
 .../media_library/src/Form/AddFormBase.php    | 155 ++++++++++++++++--
 .../media_library/src/Form/FileUploadForm.php |  14 +-
 .../media_library/src/Form/OEmbedForm.php     |   3 +-
 .../FunctionalJavascript/MediaLibraryTest.php | 114 ++++++++++++-
 .../src/Kernel/MediaLibraryAddFormTest.php    |   4 +-
 7 files changed, 394 insertions(+), 45 deletions(-)
 create mode 100644 core/misc/icons/ee0000/ex.svg

diff --git a/core/misc/icons/ee0000/ex.svg b/core/misc/icons/ee0000/ex.svg
new file mode 100644
index 000000000000..6b45a1d5726e
--- /dev/null
+++ b/core/misc/icons/ee0000/ex.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#ee0000" d="M3.51 13.925c.194.194.512.195.706.001l3.432-3.431c.194-.194.514-.194.708 0l3.432 3.431c.192.194.514.193.707-.001l1.405-1.417c.191-.195.189-.514-.002-.709l-3.397-3.4c-.192-.193-.192-.514-.002-.708l3.401-3.43c.189-.195.189-.515 0-.709l-1.407-1.418c-.195-.195-.513-.195-.707-.001l-3.43 3.431c-.195.194-.516.194-.708 0l-3.432-3.431c-.195-.195-.512-.194-.706.001l-1.407 1.417c-.194.195-.194.515 0 .71l3.403 3.429c.193.195.193.514-.001.708l-3.4 3.399c-.194.195-.195.516-.001.709l1.406 1.419z"/></svg>
diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css
index 2bad77cefb3d..0e066d888748 100644
--- a/core/modules/media_library/css/media_library.theme.css
+++ b/core/modules/media_library/css/media_library.theme.css
@@ -27,8 +27,10 @@
   margin: 0;
 }
 
-/* @todo Use a class instead of the li element.
-     https://www.drupal.org/project/drupal/issues/3029227 */
+/**
+ * @todo Use a class instead of the li element.
+ *   https://www.drupal.org/project/drupal/issues/3029227
+ */
 .media-library-menu li {
   display: block;
 }
@@ -84,17 +86,27 @@
 }
 
 /* Generic media add form styles. */
-.media-library-add-form--without-input {
+.media-library-add-form--without-input .form-item {
+  margin: 0 0 1em;
+}
+
+/**
+ * Remove outline from added media container.
+ *
+ * The added media container receives focus after adding new media, but since
+ * it is not an interactive element it does not need an outline.
+ */
+.media-library-add-form__added-media {
+  outline: none;
+}
+
+.media-library-add-form__input-wrapper {
   padding: 16px;
   border: 1px solid #bfbfbf;
   border-radius: 2px;
   background: #fcfcfa;
 }
 
-.media-library-add-form--without-input .form-item {
-  margin: 0 0 1em;
-}
-
 /* Style the media add upload form. */
 .media-library-add-form--upload.media-library-add-form--without-input .form-item-upload {
   margin-bottom: 0;
@@ -105,12 +117,12 @@
 }
 
 /* Style the media add oEmbed form. */
-.media-library-add-form-oembed-wrapper {
+.media-library-add-form--oembed .media-library-add-form__input-wrapper {
   display: flex;
 }
 
 @media screen and (max-width: 37.5em) {
-  .media-library-add-form-oembed-wrapper {
+  .media-library-add-form--oembed .media-library-add-form__input-wrapper {
     display: block;
   }
 }
@@ -123,8 +135,10 @@
   width: 100%;
 }
 
-/* @todo Remove .button when styles are moved to the seven theme in
-     https://www.drupal.org/project/drupal/issues/2980769 */
+/**
+ * @todo Remove .button when styles are moved to the seven theme in
+ *   https://www.drupal.org/project/drupal/issues/2980769
+ */
 .button.media-library-add-form-oembed-submit {
   align-self: center;
 }
@@ -152,8 +166,13 @@
   margin: 0.75em 0;
 }
 
-/* Override the table display of the visually hidden labels so they won't take
-   up space. */
+/**
+ * Override the table display of the visually hidden labels.
+ *
+ * The width, height and overflow properties in the styles for the
+ * .visually-hidden class do not work correctly if the element has a table
+ * display.
+ */
 .media-library-item label {
   display: inline-block;
 }
@@ -166,8 +185,10 @@
   justify-content: space-between;
 }
 
-/* @todo Remove order and reorder the views header and filters via a views
-     template in https://www.drupal.org/project/drupal/issues/3035994 */
+/**
+ * @todo Remove order and reorder the views header and filters via a views
+ *   template in https://www.drupal.org/project/drupal/issues/3035994
+ */
 .media-library-wrapper .view-header {
   order: 2;
   align-self: flex-end;
@@ -178,21 +199,27 @@
   text-align: left;
 }
 
-/* @todo Remove order and reorder the views header and filters via a views
-     template in https://www.drupal.org/project/drupal/issues/3035994 */
+/**
+ * @todo Remove order and reorder the views header and filters via a views
+ *   template in https://www.drupal.org/project/drupal/issues/3035994
+ */
 .media-library-wrapper .media-library-view .view-filters {
   order: 1;
 }
 
-/* @todo Remove order and reorder the views header and filters via a views
-     template in https://www.drupal.org/project/drupal/issues/3035994 */
+/**
+ * @todo Remove order and reorder the views header and filters via a views
+ *   template in https://www.drupal.org/project/drupal/issues/3035994
+ */
 .media-library-wrapper .media-library-view .view-content {
   flex: 0 0 100%;
   order: 3;
 }
 
-/* @todo Remove order and reorder the views header and filters via a views
-     template in https://www.drupal.org/project/drupal/issues/3035994 */
+/**
+ * @todo Remove order and reorder the views header and filters via a views
+ *   template in https://www.drupal.org/project/drupal/issues/3035994
+ */
 .media-library-wrapper .media-library-view .pager {
   order: 4;
 }
@@ -403,8 +430,10 @@
   position: relative;
 }
 
-/* @todo Change to .media-library-open-button when styles are moved to the
-     seven theme in https://www.drupal.org/project/drupal/issues/2980769 */
+/**
+ * @todo Change to .media-library-open-button when styles are moved to the
+ *   seven theme in https://www.drupal.org/project/drupal/issues/2980769
+ */
 .button.media-library-open-button {
   margin-left: 0; /* LTR */
 }
@@ -464,11 +493,19 @@
   border-color: #40b6ff;
 }
 
-/* Style the wrappers around new media and files. */
+/**
+ * Style the added media item container.
+ *
+ * The added media container receives screen reader focus since it is has the
+ * role 'listitem'. Since it is not an interactive element, it does not need
+ * an outline.
+ */
 .media-library-add-form__media {
+  position: relative;
   display: flex;
   padding: 20px 0 20px 0;
   border-bottom: 1px solid #c0c0c0;
+  outline: none;
 }
 
 /* Do not show the top padding for the first item. */
@@ -476,6 +513,16 @@
   padding-top: 0;
 }
 
+/**
+ * Change the position of the remove button for the first item.
+ *
+ * The first item doesn't have a top padding, change the location of the remove
+ * button as well.
+ */
+.media-library-add-form__media:first-child .media-library-add-form__remove-button[type="submit"] {
+  top: 5px;
+}
+
 /* Do not show the bottom border and padding for the last item. */
 .media-library-add-form__media:last-child {
   padding-bottom: 0;
@@ -504,6 +551,59 @@
   margin-left: 20px;
 }
 
+/**
+ * @todo Remove [type="submit"] when styles are moved to the seven theme in
+ *   https://www.drupal.org/project/drupal/issues/2980769
+ */
+.media-library-add-form__remove-button[type="submit"] {
+  position: absolute;
+  top: 25px;
+  right: 6px; /* LTR */
+  width: auto;
+  margin: 0;
+  padding: 2px 20px 2px 2px; /* LTR */
+  text-transform: lowercase;
+  color: transparent;
+  border: 0;
+  border-radius: 0;
+  background: transparent url(../../../misc/icons/787878/ex.svg) right 2px no-repeat; /* LTR */
+  font-weight: normal;
+  line-height: 16px;
+}
+[dir="rtl"] .media-library-add-form__remove-button[type="submit"] {
+  right: auto;
+  left: 13px;
+  padding: 2px 2px 2px 20px;
+  background-position: left 2px;
+}
+
+.media-library-add-form__remove-button:focus,
+.media-library-add-form__remove-button.button:disabled,
+.media-library-add-form__remove-button.button:disabled:active,
+.media-library-add-form__remove-button.button:focus {
+  color: #787878;
+  border: 0;
+  background: transparent url(../../../misc/icons/787878/ex.svg) right 2px no-repeat; /* LTR */
+}
+[dir="rtl"] .media-library-add-form__remove-button:focus,
+[dir="rtl"] .media-library-add-form__remove-button.button:disabled,
+[dir="rtl"] .media-library-add-form__remove-button.button:disabled:active,
+[dir="rtl"] .media-library-add-form__remove-button.button:focus {
+  background-position: left 2px;
+}
+
+.media-library-add-form__remove-button:hover,
+.media-library-add-form__remove-button.button:hover {
+  color: #e00;
+  border: 0;
+  background: transparent url(../../../misc/icons/ee0000/ex.svg) right 2px no-repeat; /* LTR */
+  box-shadow: none;
+}
+[dir="rtl"] .media-library-add-form__remove-button:hover,
+[dir="rtl"] .media-library-add-form__remove-button.button:hover {
+  background-position: left 2px;
+}
+
 /* @todo Remove or re-work in https://www.drupal.org/node/2985168 */
 .media-library-widget .media-library-item__name a,
 .media-library-view--widget .media-library-item__name a {
diff --git a/core/modules/media_library/src/Form/AddFormBase.php b/core/modules/media_library/src/Form/AddFormBase.php
index d6e76a5a03a1..b4ef7ad1e0d1 100644
--- a/core/modules/media_library/src/Form/AddFormBase.php
+++ b/core/modules/media_library/src/Form/AddFormBase.php
@@ -3,6 +3,7 @@
 namespace Drupal\media_library\Form;
 
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\InvokeCommand;
 use Drupal\Core\Ajax\ReplaceCommand;
 use Drupal\Core\Entity\Entity\EntityFormDisplay;
 use Drupal\Core\Entity\EntityStorageInterface;
@@ -109,7 +110,9 @@ protected function getMediaType(FormStateInterface $form_state) {
    * {@inheritdoc}
    */
   public function buildForm(array $form, FormStateInterface $form_state) {
-    $form['#prefix'] = '<div id="media-library-add-form-wrapper">';
+    // @todo Remove the ID when we can use selectors to replace content via
+    //   AJAX in https://www.drupal.org/project/drupal/issues/2821793.
+    $form['#prefix'] = '<div id="media-library-add-form-wrapper" class="media-library-add-form-wrapper">';
     $form['#suffix'] = '</div>';
     $form['#attached']['library'][] = 'media_library/style';
 
@@ -139,6 +142,19 @@ public function buildForm(array $form, FormStateInterface $form_state) {
 
       $form['media'] = [
         '#type' => 'container',
+        '#attributes' => [
+          'class' => [
+            'media-library-add-form__added-media',
+          ],
+          'aria-label' => $this->t('Added media items'),
+          'role' => 'list',
+          // Add the tabindex '-1' to allow the focus to be shifted to the added
+          // media wrapper when items are added. We set focus to the container
+          // because a media item does not necessarily have required fields and
+          // we do not want to set focus to the remove button automatically.
+          // @see ::updateFormCallback()
+          'tabindex' => '-1',
+        ],
       ];
 
       foreach ($added_media as $delta => $media) {
@@ -185,15 +201,36 @@ abstract protected function buildInputElement(array $form, FormStateInterface $f
    *   The element containing the required fields sub-form.
    */
   protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
+    // We need to make sure each button has a unique name attribute. The default
+    // name for button elements is 'op'. If the name is not unique, the
+    // triggering element is not set correctly and the wrong media item is
+    // removed.
+    // @see ::removeButtonSubmit()
+    $parents = $form['#parents'];
+    $id_suffix = $parents ? '-' . implode('-', $parents) : '';
+
     $element = [
       '#type' => 'container',
       '#attributes' => [
         'class' => [
           'media-library-add-form__media',
         ],
+        'aria-label' => $media->getName(),
+        'role' => 'listitem',
+        // Add the tabindex '-1' to allow the focus to be shifted to the next
+        // media item when an item is removed. We set focus to the container
+        // because a media item does not necessarily have required fields and we
+        // do not want to set focus to the remove button automatically.
+        // @see ::updateFormCallback()
+        'tabindex' => '-1',
+        // Add a data attribute containing the delta to allow us to easily shift
+        // the focus to a specific media item.
+        // @see ::updateFormCallback()
+        'data-media-library-added-delta' => $delta,
       ],
       'preview' => [
         '#type' => 'container',
+        '#weight' => 10,
         '#attributes' => [
           'class' => [
             'media-library-add-form__preview',
@@ -202,6 +239,7 @@ protected function buildEntityFormElement(MediaInterface $media, array $form, Fo
       ],
       'fields' => [
         '#type' => 'container',
+        '#weight' => 20,
         '#attributes' => [
           'class' => [
             'media-library-add-form__fields',
@@ -211,6 +249,24 @@ protected function buildEntityFormElement(MediaInterface $media, array $form, Fo
         // to build the entity form fields.
         '#parents' => ['media', $delta, 'fields'],
       ],
+      'remove_button' => [
+        '#type' => 'submit',
+        '#value' => $this->t('Remove'),
+        '#name' => 'media-' . $delta . '-remove-button' . $id_suffix,
+        '#weight' => 30,
+        '#attributes' => [
+          'class' => ['media-library-add-form__remove-button'],
+          'aria-label' => $this->t('Remove @label', ['@label' => $media->getName()]),
+        ],
+        '#ajax' => [
+          'callback' => '::updateFormCallback',
+          'wrapper' => 'media-library-add-form-wrapper',
+          'message' => $this->t('Removing @label.', ['@label' => $media->getName()]),
+        ],
+        '#submit' => ['::removeButtonSubmit'],
+        // Ensure errors in other media items do not prevent removal.
+        '#limit_validation_errors' => [],
+      ],
     ];
     // @todo Make the image style configurable in
     //   https://www.drupal.org/node/2988223
@@ -295,7 +351,8 @@ protected function processInputValues(array $source_field_values, array $form, F
     $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();
+    // Re-key the media items before setting them in the form state.
+    $form_state->set('media', array_values($media))->setRebuild();
   }
 
   /**
@@ -332,6 +389,33 @@ protected function prepareMediaEntityForSave(MediaInterface $media) {
     // Intentionally empty by default.
   }
 
+  /**
+   * Submit handler for the remove button.
+   *
+   * @param array $form
+   *   The form render array.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The form state.
+   */
+  public function removeButtonSubmit(array $form, FormStateInterface $form_state) {
+    // Retrieve the delta of the media item from the parents of the remove
+    // button.
+    $triggering_element = $form_state->getTriggeringElement();
+    $delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
+
+    $added_media = $form_state->get('media');
+    $removed_media = $added_media[$delta];
+
+    // Update the list of added media items in the form state.
+    unset($added_media[$delta]);
+
+    // Update the media items in the form state.
+    $form_state->set('media', $added_media)->setRebuild();
+
+    // Show a message to the user to confirm the media is removed.
+    $this->messenger()->addStatus($this->t('The media item %label has been removed.', ['%label' => $removed_media->label()]));
+  }
+
   /**
    * AJAX callback to update the entire form based on source field input.
    *
@@ -344,17 +428,57 @@ protected function prepareMediaEntityForSave(MediaInterface $media) {
    *   The form render array or an AJAX response object.
    */
   public function updateFormCallback(array &$form, FormStateInterface $form_state) {
+    $triggering_element = $form_state->getTriggeringElement();
+    $wrapper_id = $triggering_element['#ajax']['wrapper'];
+    $added_media = $form_state->get('media');
+
+    $response = new AjaxResponse();
+
     // 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;
+    // Check if the remove button is clicked.
+    if (end($triggering_element['#parents']) === 'remove_button') {
+      // When the list of added media is empty, return to the media library and
+      // shift focus back to the first tabbable element (which should be the
+      // source field).
+      if (empty($added_media)) {
+        $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
+        $response->addCommand(new InvokeCommand('#media-library-add-form-wrapper :tabbable', 'focus'));
+      }
+      // When there are still more items, update the form and shift the focus to
+      // the next media item. If the last list item is removed, shift focus to
+      // the previous item.
+      else {
+        $response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
+
+        // Find the delta of the next media item. If there is no item with a
+        // bigger delta, we automatically use the delta of the previous item and
+        // shift the focus there.
+        $removed_delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
+        $delta_to_focus = 0;
+        foreach ($added_media as $delta => $media) {
+          $delta_to_focus = $delta;
+          if ($delta > $removed_delta) {
+            break;
+          }
+        }
+        $response->addCommand(new InvokeCommand(".media-library-add-form__media[data-media-library-added-delta=$delta_to_focus]", 'focus'));
+      }
+    }
+    // Update the form and shift focus to the added media items.
+    else {
+      $response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
+      $response->addCommand(new InvokeCommand('.media-library-add-form__added-media', 'focus'));
+    }
+
+    return $response;
   }
 
   /**
@@ -420,6 +544,22 @@ public function updateWidget(array &$form, FormStateInterface $form_state) {
       return $media->id();
     }, $added_media);
 
+    $response = new AjaxResponse();
+    $response->addCommand(new UpdateSelectionCommand($media_ids));
+    $response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
+    return $response;
+  }
+
+  /**
+   * Build the render array of the media library UI.
+   *
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current form state.
+   *
+   * @return array
+   *   The render array for the media library.
+   */
+  protected function buildMediaLibraryUi(FormStateInterface $form_state) {
     // 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
@@ -429,12 +569,7 @@ public function updateWidget(array &$form, FormStateInterface $form_state) {
     $state = $this->getMediaLibraryState($form_state);
     $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;
+    return $this->libraryUiBuilder->buildUi($state);
   }
 
   /**
diff --git a/core/modules/media_library/src/Form/FileUploadForm.php b/core/modules/media_library/src/Form/FileUploadForm.php
index 8614a2e2d8ec..283ae74e977e 100644
--- a/core/modules/media_library/src/Form/FileUploadForm.php
+++ b/core/modules/media_library/src/Form/FileUploadForm.php
@@ -122,8 +122,16 @@ protected function buildInputElement(array $form, FormStateInterface $form_state
 
     $slots = $state->getAvailableSlots();
 
+    // Add a container to group the input elements for styling purposes.
+    $form['container'] = [
+      '#type' => 'container',
+      '#attributes' => [
+        'class' => ['media-library-add-form__input-wrapper'],
+      ],
+    ];
+
     $process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
-    $form['upload'] = [
+    $form['container']['upload'] = [
       '#type' => 'managed_file',
       '#title' => $this->formatPlural($slots, 'Add file', 'Add files'),
       // @todo Move validation in https://www.drupal.org/node/2988215
@@ -136,7 +144,7 @@ protected function buildInputElement(array $form, FormStateInterface $form_state
 
     $file_upload_help = [
       '#theme' => 'file_upload_help',
-      '#upload_validators' => $form['upload']['#upload_validators'],
+      '#upload_validators' => $form['container']['upload']['#upload_validators'],
       '#cardinality' => $slots,
     ];
 
@@ -145,7 +153,7 @@ protected function buildInputElement(array $form, FormStateInterface $form_state
     // 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);
+    $form['container']['upload']['#description'] = $this->renderer->renderPlain($file_upload_help);
 
     return $form;
   }
diff --git a/core/modules/media_library/src/Form/OEmbedForm.php b/core/modules/media_library/src/Form/OEmbedForm.php
index 72b2b1c54746..4af94334c03a 100644
--- a/core/modules/media_library/src/Form/OEmbedForm.php
+++ b/core/modules/media_library/src/Form/OEmbedForm.php
@@ -91,10 +91,11 @@ protected function buildInputElement(array $form, FormStateInterface $form_state
     $media_type = $this->getMediaType($form_state);
     $providers = $media_type->getSource()->getProviders();
 
+    // Add a container to group the input elements for styling purposes.
     $form['container'] = [
       '#type' => 'container',
       '#attributes' => [
-        'class' => ['media-library-add-form-oembed-wrapper'],
+        'class' => ['media-library-add-form__input-wrapper'],
       ],
     ];
 
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index 16db27bfd8e2..cbba55dc81e4 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -597,6 +597,7 @@ public function testWidgetAnonymous() {
   public function testWidgetUpload() {
     $assert_session = $this->assertSession();
     $page = $this->getSession()->getPage();
+    $driver = $this->getSession()->getDriver();
 
     foreach ($this->getTestFiles('image') as $image) {
       $extension = pathinfo($image->filename, PATHINFO_EXTENSION);
@@ -735,11 +736,6 @@ public function testWidgetUpload() {
     // 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('Add files')->hasAttribute('multiple'));
-
     $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
     $assert_session->assertWaitOnAjaxRequest();
     $page->fillField('Name', 'Unlimited Cardinality Image');
@@ -822,6 +818,87 @@ public function testWidgetUpload() {
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextNotContains('Add or select media');
     $assert_session->pageTextContains($jpg_image->filename);
+
+    // Assert removing an uploaded media item before save works as expected.
+    $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('Add or select media');
+    $page->clickLink('Type Three');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->attachFileToField('Add files', $this->container->get('file_system')->realpath($png_image->uri));
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the focus is shifted to the added media items.
+    $this->assertJsCondition('jQuery(".media-library-add-form__added-media").is(":focus")');
+    // Assert the media item fields are shown and the vertical tabs are no
+    // longer shown.
+    $assert_session->elementExists('css', '.media-library-add-form__fields');
+    $assert_session->elementNotExists('css', '.media-library-menu');
+    // Press the 'Remove button' and assert the user is sent back to the media
+    // library.
+    $assert_session->elementExists('css', '.media-library-add-form__remove-button')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the remove message is shown.
+    $assert_session->pageTextContains("The media item $png_image->filename has been removed.");
+    // Assert the focus is shifted to the first tabbable element of the add
+    // form, which should be the source field.
+    $this->assertJsCondition('jQuery("#media-library-add-form-wrapper :tabbable").is(":focus")');
+    $assert_session->elementNotExists('css', '.media-library-add-form__fields');
+    $assert_session->elementExists('css', '.media-library-menu');
+    $page->find('css', '.ui-dialog-titlebar-close')->click();
+
+    // Assert uploading multiple files.
+    $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('Add or select media');
+    $page->clickLink('Type Three');
+    $assert_session->assertWaitOnAjaxRequest();
+    $this->assertTrue($assert_session->fieldExists('Add files')->hasAttribute('multiple'));
+    // Create a list of new files to upload.
+    $filenames = [];
+    $remote_paths = [];
+    foreach (range(1, 3) as $i) {
+      $path = $file_system->copy($png_image->uri);
+      $filenames[] = $file_system->basename($path);
+      $remote_paths[] = $driver->uploadFileAndGetRemoteFilePath($file_system->realpath($path));
+    }
+    $page->findField('Add files')->setValue(implode("\n", $remote_paths));
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the media item fields are shown and the vertical tabs are no
+    // longer shown.
+    $assert_session->elementExists('css', '.media-library-add-form__fields');
+    $assert_session->elementNotExists('css', '.media-library-menu');
+    // Assert all files have been added.
+    $assert_session->fieldValueEquals('media[0][fields][name][0][value]', $filenames[0]);
+    $assert_session->fieldValueEquals('media[1][fields][name][0][value]', $filenames[1]);
+    $assert_session->fieldValueEquals('media[2][fields][name][0][value]', $filenames[2]);
+    // Set alt texts for items 1 and 2, leave the alt text empty for item 3 to
+    // assert the field validation does not stop users from removing items.
+    $page->fillField('media[0][fields][field_media_test_image][0][alt]', $filenames[0]);
+    $page->fillField('media[1][fields][field_media_test_image][0][alt]', $filenames[1]);
+    // Remove the second file and assert the focus is shifted to the container
+    // of the next media item and field values are still correct.
+    $page->pressButton('media-1-remove-button');
+    $this->assertJsCondition('jQuery(".media-library-add-form__media[data-media-library-added-delta=2]").is(":focus")');
+    $assert_session->pageTextContains('The media item ' . $filenames[1] . ' has been removed.');
+    // The second media item should be removed (this has the delta 1 since we
+    // start counting from 0).
+    $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=1]');
+    $media_item_one = $assert_session->elementExists('css', '.media-library-add-form__media[data-media-library-added-delta=0]');
+    $assert_session->fieldValueEquals('Name', $filenames[0], $media_item_one);
+    $assert_session->fieldValueEquals('Alternative text', $filenames[0], $media_item_one);
+    $media_item_three = $assert_session->elementExists('css', '.media-library-add-form__media[data-media-library-added-delta=2]');
+    $assert_session->fieldValueEquals('Name', $filenames[2], $media_item_three);
+    $assert_session->fieldValueEquals('Alternative text', '', $media_item_three);
+    // Remove the last file and assert the focus is shifted to the container
+    // of the first media item and field values are still correct.
+    $page->pressButton('media-2-remove-button');
+    $this->assertJsCondition('jQuery(".media-library-add-form__media[data-media-library-added-delta=0]").is(":focus")');
+    $assert_session->pageTextContains('The media item ' . $filenames[2] . ' has been removed.');
+    $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=1]');
+    $assert_session->elementNotExists('css', '.media-library-add-form__media[data-media-library-added-delta=2]');
+    $media_item_one = $assert_session->elementExists('css', '.media-library-add-form__media[data-media-library-added-delta=0]');
+    $assert_session->fieldValueEquals('Name', $filenames[0], $media_item_one);
+    $assert_session->fieldValueEquals('Alternative text', $filenames[0], $media_item_one);
   }
 
   /**
@@ -921,6 +998,33 @@ public function testWidgetOEmbed() {
     $assert_session->assertWaitOnAjaxRequest();
     $assert_session->pageTextNotContains('Add or select media');
     $assert_session->pageTextContains('Custom video title');
+
+    // Assert removing an added oEmbed media item before save works as expected.
+    $assert_session->elementExists('css', '.media-library-open-button[name^="field_unlimited_media"]')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    $assert_session->pageTextContains('Add or select media');
+    $page->clickLink('Type Five');
+    $assert_session->assertWaitOnAjaxRequest();
+    $page->fillField('Add Type Five via URL', $video_url);
+    $page->pressButton('Add');
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the focus is shifted to the added media items.
+    $this->assertJsCondition('jQuery(".media-library-add-form__added-media").is(":focus")');
+    // Assert the media item fields are shown and the vertical tabs are no
+    // longer shown.
+    $assert_session->elementExists('css', '.media-library-add-form__fields');
+    $assert_session->elementNotExists('css', '.media-library-menu');
+    // Press the 'Remove button' and assert the user is sent back to the media
+    // library.
+    $assert_session->elementExists('css', '.media-library-add-form__remove-button')->click();
+    $assert_session->assertWaitOnAjaxRequest();
+    // Assert the remove message is shown.
+    $assert_session->pageTextContains("The media item $video_title has been removed.");
+    // Assert the focus is shifted to the first tabbable element of the add
+    // form, which should be the source field.
+    $this->assertJsCondition('jQuery("#media-library-add-form-wrapper :tabbable").is(":focus")');
+    $assert_session->elementNotExists('css', '.media-library-add-form__fields');
+    $assert_session->elementExists('css', '.media-library-menu');
   }
 
 }
diff --git a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php
index 0d180bb8801d..833ede9a6546 100644
--- a/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php
+++ b/core/modules/media_library/tests/src/Kernel/MediaLibraryAddFormTest.php
@@ -87,7 +87,7 @@ public function testMediaTypeAddForm() {
     ]));
     // Assert the media library UI only contains the add form for the image
     // media type.
-    $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['upload']['#type']);
+    $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['container']['upload']['#type']);
     $this->assertEmpty($this->buildLibraryUi('remote_video')['content']['form']);
 
     // Create a user that has access to create both media types.
@@ -97,7 +97,7 @@ public function testMediaTypeAddForm() {
     ]));
     // Assert the media library UI only contains the add form for both media
     // types.
-    $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['upload']['#type']);
+    $this->assertSame('managed_file', $this->buildLibraryUi('image')['content']['form']['container']['upload']['#type']);
     $this->assertSame('url', $this->buildLibraryUi('remote_video')['content']['form']['container']['url']['#type']);
   }
 
-- 
GitLab