From 2a7053645addc433217952c00dab44bc3518ee78 Mon Sep 17 00:00:00 2001
From: nagyad <joevagyok@gmail.com>
Date: Thu, 22 Aug 2019 08:21:05 +0200
Subject: [PATCH] Ensure visibility of invalid fields.

---
 field_group.libraries.yml                     |  24 ++
 js/field_group.details_validation.js          |  26 +++
 js/field_group.tab_validation.js              |  22 ++
 js/field_group.tabs_validation.js             |  34 +++
 .../FieldGroupFormatter/Details.php           |   1 +
 .../field_group/FieldGroupFormatter/Tab.php   |   2 +
 .../field_group/FieldGroupFormatter/Tabs.php  |   1 +
 .../FunctionalJavascript/EntityFormTest.php   | 219 ++++++++++++++++++
 8 files changed, 329 insertions(+)
 create mode 100644 js/field_group.details_validation.js
 create mode 100644 js/field_group.tab_validation.js
 create mode 100644 js/field_group.tabs_validation.js
 create mode 100644 tests/src/FunctionalJavascript/EntityFormTest.php

diff --git a/field_group.libraries.yml b/field_group.libraries.yml
index e09631b..2b9a811 100644
--- a/field_group.libraries.yml
+++ b/field_group.libraries.yml
@@ -11,6 +11,30 @@ field_ui:
     - core/drupal
     - core/drupalSettings
 
+details_validation:
+  header: false
+  version: VERSION
+  js:
+    js/field_group.details_validation.js: {}
+  dependencies:
+    - core/jquery
+
+tab_validation:
+  header: false
+  version: VERSION
+  js:
+    js/field_group.tab_validation.js: {}
+  dependencies:
+    - core/jquery
+
+tabs_validation:
+  header: false
+  version: VERSION
+  js:
+    js/field_group.tabs_validation.js: {}
+  dependencies:
+    - core/jquery
+
 core:
   version: VERSION
   js:
diff --git a/js/field_group.details_validation.js b/js/field_group.details_validation.js
new file mode 100644
index 0000000..13a00fd
--- /dev/null
+++ b/js/field_group.details_validation.js
@@ -0,0 +1,26 @@
+(function ($) {
+  'use strict';
+
+  /**
+   * Behaviors for details validation.
+   */
+  Drupal.behaviors.fieldGroupDetailsValidation = {
+    attach: function (context, settings) {
+      $('.field-group-details :input', context).each(function (i) {
+        var $field_group_input = $(this);
+        this.addEventListener('invalid', function (e) {
+          // Open any hidden parents first.
+          $(e.target).parents('details:not([open])').each(function () {
+            $(this).attr('open', '');
+          });
+        }, false);
+        if ($field_group_input.hasClass('error')) {
+          $field_group_input.parents('details:not([open])').each(function () {
+            $(this).attr('open', '');
+          });
+        }
+      });
+    }
+  };
+
+})(jQuery);
diff --git a/js/field_group.tab_validation.js b/js/field_group.tab_validation.js
new file mode 100644
index 0000000..3101d35
--- /dev/null
+++ b/js/field_group.tab_validation.js
@@ -0,0 +1,22 @@
+(function ($) {
+  'use strict';
+
+  /**
+   * Make sure tab field groups which contain invalid data are expanded when they first load, and also
+   * when someone clicks the submit button.
+   */
+  Drupal.behaviors.fieldGroupTabValidation = {
+    attach: function () {
+      var openTabsWithInvalidFields = function() {
+        $('.field-group-tab input:invalid').parents('details').children('summary[aria-expanded=false]').click();
+      }
+
+      // When a form is first loaded, open tabs with invalid fields.
+      openTabsWithInvalidFields();
+
+      // Also, when someone tries to submit a form, open tabs with invalid fields.
+      $('#edit-submit').on('click', openTabsWithInvalidFields);
+    }
+  };
+
+})(jQuery);
diff --git a/js/field_group.tabs_validation.js b/js/field_group.tabs_validation.js
new file mode 100644
index 0000000..775842b
--- /dev/null
+++ b/js/field_group.tabs_validation.js
@@ -0,0 +1,34 @@
+(function ($) {
+  'use strict';
+
+  /**
+   * Behaviors for tab validation.
+   */
+  Drupal.behaviors.fieldGroupTabsValidation = {
+    attach: function () {
+        var fieldGroupTabsOpen = function ($field_group) {
+            if ($field_group.data('verticalTab')) {
+                $field_group.data('verticalTab').tabShow();
+            } else {
+                if ($field_group.data('horizontalTab')) {
+                    $field_group.data('horizontalTab').tabShow();
+                } else {
+                    $field_group.attr('open', '');
+                }
+            }
+        };
+
+        var onInvalid = function(e) {
+            $inputs.off('invalid', onInvalid);
+            $(e.target).parents('details:not(:visible), details.horizontal-tab-hidden, details.vertical-tab-hidden').each(function() {
+              fieldGroupTabsOpen($(this));
+            });
+            requestAnimationFrame(function () { $inputs.on('invalid', onInvalid); });
+        };
+
+        var $inputs = $('.field-group-tabs-wrapper :input');
+        $inputs.on('invalid', onInvalid);
+    }
+  };
+
+})(jQuery);
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Details.php b/src/Plugin/field_group/FieldGroupFormatter/Details.php
index d1384cc..18f902d 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Details.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Details.php
@@ -48,6 +48,7 @@ class Details extends FieldGroupFormatterBase {
       $element['#attached']['library'][] = 'field_group/core';
     }
 
+    $element['#attached']['library'][] = 'field_group/details_validation';
   }
 
   /**
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Tab.php b/src/Plugin/field_group/FieldGroupFormatter/Tab.php
index fe58da6..5e962f6 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Tab.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Tab.php
@@ -62,6 +62,8 @@ class Tab extends FieldGroupFormatterBase {
       $element['#attached']['library'][] = 'field_group/core';
     }
 
+    $element['#attached']['library'][] = 'field_group/tab_validation';
+
     $element += $add;
 
   }
diff --git a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
index 56d58d4..a851698 100644
--- a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
+++ b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php
@@ -62,6 +62,7 @@ class Tabs extends FieldGroupFormatterBase {
       }
     }
 
+    $element['#attached']['library'][] = 'field_group/tabs_validation';
   }
 
   /**
diff --git a/tests/src/FunctionalJavascript/EntityFormTest.php b/tests/src/FunctionalJavascript/EntityFormTest.php
new file mode 100644
index 0000000..35306b3
--- /dev/null
+++ b/tests/src/FunctionalJavascript/EntityFormTest.php
@@ -0,0 +1,219 @@
+<?php
+
+namespace Drupal\Tests\field_group\Functional;
+
+use Drupal\Core\Entity\Entity\EntityFormDisplay;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
+
+/**
+ * Tests for form display.
+ *
+ * @group field_group
+ */
+class EntityFormTest extends WebDriverTestBase {
+
+  use FieldGroupTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'node',
+    'field_test',
+    'field_ui',
+    'field_group',
+    'field_group_test',
+  ];
+
+  /**
+   * The node type id.
+   *
+   * @var string
+   */
+  protected $type;
+
+  /**
+   * A node to use for testing.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create test user.
+    $admin_user = $this->drupalCreateUser([
+      'access content',
+      'administer content types',
+      'administer node fields',
+      'administer node form display',
+      'administer node display',
+      'bypass node access',
+    ]);
+    $this->drupalLogin($admin_user);
+
+    // Create content type, with underscores.
+    $type_name = strtolower($this->randomMachineName(8)) . '_test';
+    $type = $this->drupalCreateContentType(['name' => $type_name, 'type' => $type_name]);
+    $this->type = $type->id();
+
+    // Create test field.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'field_test',
+      'entity_type' => 'node',
+      'type' => 'test_field',
+    ]);
+    $field_storage->save();
+
+    $instance = FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => $type_name,
+      'label' => 'field_test',
+      'required' => TRUE,
+    ]);
+    $instance->save();
+
+    /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
+    $form_display = EntityFormDisplay::load('node.' . $this->type . '.default');
+
+    // Set the field visible on the form display object.
+    $display_options = [
+      'type' => 'string_textfield',
+      'region' => 'content',
+      'settings' => [
+        'size' => 60,
+      ],
+    ];
+    $form_display->setComponent('field_test', $display_options);
+
+    // Save the form display.
+    $form_display->save();
+  }
+
+  /**
+   * Test required fields validation with tabs are visible.
+   */
+  public function testHtmlElement() {
+    $data = [
+      'label' => 'Tab 1',
+      'weight' => '1',
+      'children' => [
+        0 => 'title',
+        1 => 'body',
+      ],
+      'format_type' => 'tab',
+      'format_settings' => [
+        'label' => 'Tab 1',
+        'classes' => 'test-class',
+        'description' => '',
+        'formatter' => 'open',
+      ],
+    ];
+    $first_tab = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Field group collapsible',
+      'weight' => '1',
+      'children' => [
+        0 => 'field_test',
+      ],
+      'format_type' => 'details',
+      'format_settings' => [
+        'open' => FALSE,
+        'required_fields' => TRUE,
+      ],
+    ];
+    $field_group_collapsible = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Tab 2',
+      'weight' => '1',
+      'children' => [
+        0 => $field_group_collapsible->group_name,
+      ],
+      'format_type' => 'tab',
+      'format_settings' => [
+        'label' => 'Tab 1',
+        'classes' => 'test-class-2',
+        'description' => 'description of second tab',
+        'formatter' => 'closed',
+      ],
+    ];
+    $second_tab = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    $data = [
+      'label' => 'Tabs',
+      'weight' => '1',
+      'children' => [
+        0 => $first_tab->group_name,
+        1 => $second_tab->group_name,
+      ],
+      'format_type' => 'tabs',
+      'format_settings' => [
+        'direction' => 'vertical',
+        'label' => 'Tab 1',
+        'classes' => 'test-class-wrapper',
+      ],
+    ];
+    $tabs_group = $this->createGroup('node', $this->type, 'form', 'default', $data);
+
+    // Load the node creation page.
+    $this->drupalGet('node/add/' . $this->type);
+
+    // Test if it's a vertical tab.
+    $this->assertTrue($this->xpath('//div[@data-vertical-tabs-panes=""]'), 'Tabs are shown vertical.');
+
+    // Assert that the required field_test is present but not visible.
+    $this->assertSession()->fieldExists('field_test');
+    $this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Submit the form without filling any required field.
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is not visible because it's in the first tab.
+    $this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Fill in the title field and leave the required field_test empty.
+    $this->getSession()->getPage()->fillField('Title', 'Node title');
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is visible because the second tab is in focus
+    // and the collapsible field group is open.
+    $this->assertTrue($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Switch to horizontal.
+    $tabs_group->format_settings['direction'] = 'horizontal';
+    field_group_group_save($tabs_group);
+
+    // Reload the node creation page.
+    $this->drupalGet('node/add/' . $this->type);
+
+    // Test if it's a horizontal tab.
+    $this->assertTrue($this->xpath('//div[@data-horizontal-tabs-panes=""]'), 'Tabs are shown horizontal.');
+
+    // Assert that the required field, field_test is present but not visible.
+    $this->assertSession()->fieldExists('field_test');
+    $this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Submit the form without filling any required field.
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is not visible because it's in the first tab.
+    $this->assertFalse($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+
+    // Fill in the title field and leave the required field_test empty.
+    $this->getSession()->getPage()->fillField('Title', 'Node title');
+    $this->getSession()->getPage()->pressButton('Save');
+
+    // Assert that the field_test is visible because the second tab is in focus
+    // and the collapsible field group is open.
+    $this->assertTrue($this->getSession()->getDriver()->isVisible($this->cssSelectToXpath('input[name="field_test[0][value]"]')));
+  }
+
+}
-- 
2.20.1