diff --git a/field_group.libraries.yml b/field_group.libraries.yml index cef15b0..765d953 100644 --- a/field_group.libraries.yml +++ b/field_group.libraries.yml @@ -56,3 +56,21 @@ element.horizontal_tabs: - core/jquery - core/once - core/drupal.collapse + +details_validation: + js: + js/field_group.details_validation.js: {} + dependencies: + - core/jquery + +tab_validation: + js: + js/field_group.tab_validation.js: {} + dependencies: + - core/jquery + +tabs_validation: + js: + js/field_group.tabs_validation.js: {} + dependencies: + - core/jquery \ No newline at end of file diff --git a/js/field_group.details_validation.js b/js/field_group.details_validation.js new file mode 100644 index 0000000..afe2400 --- /dev/null +++ b/js/field_group.details_validation.js @@ -0,0 +1,24 @@ +(function ($, once) { + + 'use strict'; + + /** + * Invalid event handler for input elements in Details field group. + */ + var onDetailsInvalid = function(e) { + // Open any hidden parents first. + $(e.target).parents('details:not([open])').each(function () { + $(this).attr('open', ''); + }); + } + + /** + * Behaviors for details validation. + */ + Drupal.behaviors.fieldGroupDetailsValidation = { + attach: function (context) { + $(once('field-group-details-validation', $('.field-group-details :input', context))).on('invalid.field_group', onDetailsInvalid); + } + }; + +})(jQuery, once); diff --git a/js/field_group.tab_validation.js b/js/field_group.tab_validation.js new file mode 100644 index 0000000..a0adb92 --- /dev/null +++ b/js/field_group.tab_validation.js @@ -0,0 +1,22 @@ +(function ($, once) { + + 'use strict'; + + /** + * Invalid event handler for input elements in Tab field group. + */ + var onTabInvalid = function (e) { + $(e.target).parents('details ').children('summary[aria-expanded=false]:not(.horizontal-tabs-pane > summary, .vertical-tabs__pane > summary)').parent().attr('open', 'open'); + }; + + /** + * 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 (context) { + $(once('field-group-tab-validation', $('.field-group-tab :input', context))).on('invalid.field_group', onTabInvalid); + } + }; + +})(jQuery, once); diff --git a/js/field_group.tabs_validation.js b/js/field_group.tabs_validation.js new file mode 100644 index 0000000..1022dba --- /dev/null +++ b/js/field_group.tabs_validation.js @@ -0,0 +1,43 @@ +(function ($, once) { + + 'use strict'; + + /** + * Opens Tab field group with invalid input elements. + */ + 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', ''); + } + }; + + /** + * Behaviors for tab validation. + */ + Drupal.behaviors.fieldGroupTabsValidation = { + attach: function (context) { + /** + * Invalid event handler for input elements in Tabs field group. + */ + var onTabsInvalid = function (e) { + $inputs.off('invalid.field_group', onTabsInvalid); + $(e.target).parents('details:not(:visible), details.horizontal-tab-hidden, details.vertical-tab-hidden').each(function () { + fieldGroupTabsOpen($(this)); + }); + requestAnimationFrame(function () { + $inputs.on('invalid.field_group', onTabsInvalid); + }); + }; + + var $inputs = $('.field-group-tabs-wrapper :input', context); + $(once('field-group-tabs-validation', $inputs)).on('invalid.field_group', onTabsInvalid); + } + }; + +})(jQuery, once); diff --git a/src/Plugin/field_group/FieldGroupFormatter/Details.php b/src/Plugin/field_group/FieldGroupFormatter/Details.php index 3fa04af..34845de 100644 --- a/src/Plugin/field_group/FieldGroupFormatter/Details.php +++ b/src/Plugin/field_group/FieldGroupFormatter/Details.php @@ -51,6 +51,8 @@ class Details extends FieldGroupFormatterBase { $element['#attached']['library'][] = 'field_group/core'; } + // Add details validation behaviour. + $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 4135a29..9dfd420 100644 --- a/src/Plugin/field_group/FieldGroupFormatter/Tab.php +++ b/src/Plugin/field_group/FieldGroupFormatter/Tab.php @@ -66,6 +66,8 @@ class Tab extends FieldGroupFormatterBase { $element += $add; + // Add tab validation behaviour. + $element['#attached']['library'][] = 'field_group/tab_validation'; } /** diff --git a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php index 5218a43..07e7226 100644 --- a/src/Plugin/field_group/FieldGroupFormatter/Tabs.php +++ b/src/Plugin/field_group/FieldGroupFormatter/Tabs.php @@ -59,6 +59,9 @@ class Tabs extends FieldGroupFormatterBase { if ($width_breakpoint = $this->getSetting('width_breakpoint')) { $element['#attached']['drupalSettings']['widthBreakpoint'] = $width_breakpoint; } + + // Add tabs validation behaviour. + $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..67fd829 --- /dev/null +++ b/tests/src/FunctionalJavascript/EntityFormTest.php @@ -0,0 +1,225 @@ +<?php + +namespace Drupal\Tests\field_group\FunctionalJavascript; + +use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\FunctionalJavascriptTests\WebDriverTestBase; +use Drupal\Tests\field_group\Functional\FieldGroupTestTrait; + +/** + * Tests for form display. + * + * @group field_group + */ +class EntityFormTest extends WebDriverTestBase { + + use FieldGroupTestTrait; + + /** + * {@inheritdoc} + */ + protected 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} + */ + protected $defaultTheme = 'stark'; + + /** + * {@inheritdoc} + */ + public function setUp(): void { + 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 required 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(); + } + + /** + * Tests required field validation visibility. + */ + public function testRequiredFieldValidationVisibility() { + $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 details', + 'weight' => '1', + 'children' => [ + 0 => 'field_test', + ], + 'format_type' => 'details', + 'format_settings' => [ + 'open' => FALSE, + 'required_fields' => TRUE, + ], + ]; + $field_group_details = $this->createGroup('node', $this->type, 'form', 'default', $data); + + $data = [ + 'label' => 'Tab 2', + 'weight' => '1', + 'children' => [ + 0 => $field_group_details->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->assertSession()->elementExists('xpath', $this->assertSession() + ->buildXPathQuery('//div[@data-vertical-tabs-panes=""]')); + $this->requiredFieldVisibilityAssertions(); + + // 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->assertSession()->elementExists('xpath', $this->assertSession() + ->buildXPathQuery('//div[@data-horizontal-tabs-panes=""]')); + $this->requiredFieldVisibilityAssertions(); + } + + /** + * Tests the required field_test to assert its visibility. + */ + private function requiredFieldVisibilityAssertions(): void { + // 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]"]'))); + } + +}