From 716a102849c7943b1ae4e260230d2f3309e9cd90 Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Fri, 2 Feb 2024 07:18:48 +1000
Subject: [PATCH] Issue #3076054 by godotislate, joaopauloc.dev, smustgrave,
 nod_, nayana_mvr, larowlan, alexpott, lauriii, quietone, benjifisher,
 anup.singh, bnjmnm: Existing field items should not be validated when adding
 another item in widget for unlimited cardinality field

---
 core/lib/Drupal/Core/Field/WidgetBase.php     | 17 ++++++--
 .../MultipleValueWidgetTest.php               | 41 +++++++++++++++++++
 ...EntityFormFieldValidationFilteringTest.php |  4 +-
 3 files changed, 57 insertions(+), 5 deletions(-)

diff --git a/core/lib/Drupal/Core/Field/WidgetBase.php b/core/lib/Drupal/Core/Field/WidgetBase.php
index 9e4de97ff602..6fdd8088099f 100644
--- a/core/lib/Drupal/Core/Field/WidgetBase.php
+++ b/core/lib/Drupal/Core/Field/WidgetBase.php
@@ -5,6 +5,9 @@
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SortArray;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\FocusFirstCommand;
+use Drupal\Core\Ajax\InsertCommand;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Drupal\Core\Render\Element;
@@ -280,7 +283,7 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
           '#name' => strtr($id_prefix, '-', '_') . '_add_more',
           '#value' => t('Add another item'),
           '#attributes' => ['class' => ['field-add-more-submit']],
-          '#limit_validation_errors' => [array_merge($parents, [$field_name])],
+          '#limit_validation_errors' => [],
           '#submit' => [[static::class, 'addMoreSubmit']],
           '#ajax' => [
             'callback' => [static::class, 'addMoreAjax'],
@@ -349,10 +352,18 @@ public static function addMoreAjax(array $form, FormStateInterface $form_state)
 
     // Add a DIV around the delta receiving the Ajax effect.
     $delta = $element['#max_delta'];
-    $element[$delta]['#prefix'] = '<div class="ajax-new-content">' . ($element[$delta]['#prefix'] ?? '');
+    // Construct an attribute to add to div for use as selector to set the focus on.
+    $button_parent = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+    $focus_attribute = 'data-drupal-selector="field-' . $button_parent['#field_name'] . '-more-focus-target"';
+    $element[$delta]['#prefix'] = '<div class="ajax-new-content" ' . $focus_attribute . '>' . ($element[$delta]['#prefix'] ?? '');
     $element[$delta]['#suffix'] = ($element[$delta]['#suffix'] ?? '') . '</div>';
 
-    return $element;
+    // Turn render array into response with AJAX commands.
+    $response = new AjaxResponse();
+    $response->addCommand(new InsertCommand(NULL, $element));
+    // Add command to set the focus on first focusable element within the div.
+    $response->addCommand(new FocusFirstCommand("[$focus_attribute]"));
+    return $response;
   }
 
   /**
diff --git a/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php b/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php
index 1b037e2ee17e..582bf3a4db89 100644
--- a/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php
+++ b/core/modules/field/tests/src/FunctionalJavascript/MultipleValueWidgetTest.php
@@ -169,4 +169,45 @@ public function testFieldMultipleValueWidget() {
     $this->assertSame('', $field_1->getValue());
   }
 
+  /**
+   * Tests that no validation occurs on field on "Add more" click.
+   */
+  public function testFieldMultipleValueWidgetAddMoreNoValidation() {
+    // Set unlimited field to be required.
+    $field_name = 'field_unlimited';
+    $field = FieldConfig::loadByName('entity_test', 'entity_test', $field_name);
+    $field->setRequired(TRUE);
+    $field->save();
+
+    $this->drupalGet('entity_test/add');
+    $assert_session = $this->assertSession();
+    $page = $this->getSession()->getPage();
+
+    // Add another item with the first item being empty, even though the field
+    // is required.
+    $add_more_button = $page->findButton('field_unlimited_add_more');
+    $add_more_button->click();
+    $field_1 = $assert_session->waitForField('field_unlimited[1][value]');
+    $this->assertNotEmpty($field_1, 'Successfully added another item.');
+    // Confirm the new item has focus.
+    $this->assertHasFocusByAttribute('name', 'field_unlimited[1][value]');
+    // The first item should not be in error state.
+    $assert_session->elementNotExists('css', 'input[name="field_unlimited[0][value]"].error');
+  }
+
+  /**
+   * Asserts an element specified by an attribute value has focus.
+   *
+   * @param string $name
+   *   The attribute name.
+   * @param string $value
+   *   The attribute value.
+   *
+   * @todo Replace with assertHasFocus() in https://drupal.org/i/3041768.
+   */
+  private function assertHasFocusByAttribute(string $name, string $value): void {
+    $active_element = $this->getSession()->evaluateScript('document.activeElement');
+    $this->assertSame($value, $active_element->attribute($name));
+  }
+
 }
diff --git a/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php
index 1cb3b7808e78..63765337939d 100644
--- a/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php
+++ b/core/tests/Drupal/FunctionalTests/Entity/ContentEntityFormFieldValidationFilteringTest.php
@@ -151,14 +151,14 @@ public function testFieldWidgetsWithLimitedValidationErrors() {
     $assert_session->elementExists('css', 'input#edit-test-file-0-remove-button');
 
     // Make the 'Test multiple' field required and check that adding another
-    // item throws a validation error.
+    // item does not throw a validation error.
     $field_config = FieldConfig::loadByName($this->entityTypeId, $this->entityTypeId, $this->fieldNameMultiple);
     $field_config->setRequired(TRUE);
     $field_config->save();
 
     $this->drupalGet($this->entityTypeId . '/add');
     $this->submitForm([], 'Add another item');
-    $assert_session->pageTextContains('Test multiple (value 1) field is required.');
+    $assert_session->pageTextNotContains('Test multiple (value 1) field is required.');
 
     // Check that saving the form without entering any value for the required
     // field still throws the proper validation errors.
-- 
GitLab