diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index be000dc..1b10a51 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -970,7 +970,7 @@ public function doBuildForm($form_id, &$element, FormStateInterface &$form_state // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($unprocessed_id); } - else { + elseif (!isset($element['#attributes']['data-drupal-selector'])) { // Provide a selector usable by JavaScript. As the ID is unique, its not // possible to rely on it in JavaScript. $element['#attributes']['data-drupal-selector'] = Html::getId($element['#id']); diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php index 19a7016..0376d70 100644 --- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php @@ -4,9 +4,13 @@ use Drupal\block\BlockForm; use Drupal\block\BlockInterface; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\RedirectCommand; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Block\BlockPluginInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginWithFormsInterface; +use Drupal\Core\Url; /** * Provides form for block instance forms when used in the off-canvas dialog. @@ -111,4 +115,67 @@ protected function getPluginForm(BlockPluginInterface $block) { return $block; } + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $form['actions']['submit']['#ajax'] = [ + 'callback' => '::submitFormDialog', + ]; + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + return $form; + } + + /** + * Submit form dialog #ajax callback. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * An AJAX response that display validation error messages or redirects + * to a URL + * + * @todo Repalce this callback with generic trait in + * https://www.drupal.org/node/2896535. + */ + public function submitFormDialog(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if ($form_state->hasAnyErrors()) { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -1000, + ]; + $command = new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form); + } + else { + if ($redirect_url = $this->getRedirectUrl()) { + $command = new RedirectCommand($redirect_url->setAbsolute()->toString()); + } + else { + // Settings Tray always provides a destination. + throw new \Exception("No destination provide for Settings Tray form"); + } + } + return $response->addCommand($command); + } + + /** + * Gets the form's redirect URL from 'destination' provide in the request. + * + * @return \Drupal\Core\Url|null + * The redirect URL or NULL if dialog should just be closed. + */ + protected function getRedirectUrl() { + // \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly + // because it will use if 'destination' is not in the query + // string. + if ($this->getRequest()->query->has('destination') && $destination = $this->getRedirectDestination()->get()) { + return Url::fromUserInput('/' . $destination); + } + } + } diff --git a/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml new file mode 100644 index 0000000..590f559 --- /dev/null +++ b/core/modules/outside_in/tests/modules/outside_in_test/outside_in_test.info.yml @@ -0,0 +1,9 @@ +name: 'Settings Tray Test' +type: module +description: 'Provides Settings Tray test functionality.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - block + - outside_in diff --git a/core/modules/outside_in/tests/modules/outside_in_test/src/Plugin/Block/ValidationErrorBlock.php b/core/modules/outside_in/tests/modules/outside_in_test/src/Plugin/Block/ValidationErrorBlock.php new file mode 100644 index 0000000..095e279 --- /dev/null +++ b/core/modules/outside_in/tests/modules/outside_in_test/src/Plugin/Block/ValidationErrorBlock.php @@ -0,0 +1,33 @@ + "\u{2600} \u{27A1} \u{1F415} \u{1F6B6} \u{1F570}"]; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + parent::validateConfigurationForm($form, $form_state); + $form_state->setError($form['label'], "\u{1F525} Sorry system error. Please save again. \u{1F61C}"); + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php index 04fc230..5c7a791 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php @@ -32,6 +32,7 @@ class OutsideInBlockFormTest extends OutsideInJavascriptTestBase { 'quickedit', 'search', 'block_content', + 'outside_in_test', // Add test module to override CSS pointer-events properties because they // cause test failures. 'outside_in_test_css', @@ -114,10 +115,9 @@ public function testBlocks($block_plugin, $new_page_text, $element_selector, $la if (isset($new_page_text)) { $page->pressButton($button_text); // Make sure the changes are present. - // @todo Use a wait method that will take into account the form submitting - // and all JavaScript activity. https://www.drupal.org/node/2837676 - // The use \Behat\Mink\WebAssert::pageTextContains to check text. - $this->assertJsCondition('jQuery("' . $block_selector . ' ' . $label_selector . '").html() == "' . $new_page_text . '"'); + $new_page_text_locator = "$block_selector $label_selector:contains($new_page_text)"; + $this->assertElementVisibleAfterWait('css', $new_page_text_locator); + $web_assert->assertWaitOnAjaxRequest(); } $this->openBlockForm($block_selector); @@ -131,7 +131,7 @@ public function testBlocks($block_plugin, $new_page_text, $element_selector, $la // Open block form by clicking a element inside the block. // This confirms that default action for links and form elements is // suppressed. - $this->openBlockForm("$block_selector {$element_selector}"); + $this->openBlockForm("$block_selector {$element_selector}", $block_selector); $web_assert->elementTextContains('css', '.contextual-toolbar-tab button', 'Editing'); $web_assert->elementAttributeContains('css', '.dialog-off-canvas__main-canvas', 'class', 'js-outside-in-edit-mode'); // Simulate press the Escape key. @@ -154,7 +154,7 @@ public function providerTestBlocks() { $blocks = [ 'block-powered' => [ 'block_plugin' => 'system_powered_by_block', - 'new_page_text' => 'Can you imagine anyone showing the label on this block?', + 'new_page_text' => 'Can you imagine anyone showing the label on this block', 'element_selector' => 'span a', 'label_selector' => 'h2', 'button_text' => 'Save Powered by Drupal', @@ -162,7 +162,7 @@ public function providerTestBlocks() { ], 'block-branding' => [ 'block_plugin' => 'system_branding_block', - 'new_page_text' => 'The site that will live a very short life.', + 'new_page_text' => 'The site that will live a very short life', 'element_selector' => "a[rel='home']:last-child", 'label_selector' => "a[rel='home']:last-child", 'button_text' => 'Save Site branding', @@ -192,6 +192,7 @@ protected function enableEditMode() { * Disables edit mode by pressing edit button in the toolbar. */ protected function disableEditMode() { + $this->assertSession()->assertWaitOnAjaxRequest(); $this->pressToolbarEditButton(); $this->assertEditModeDisabled(); } @@ -225,8 +226,20 @@ protected function assertOffCanvasBlockFormIsValid() { * * @param string $block_selector * A css selector selects the block or an element within it. + * @param string $contextual_link_container + * The element that contains the contextual links. If none provide the + * $block_selector will be used. */ - protected function openBlockForm($block_selector) { + protected function openBlockForm($block_selector, $contextual_link_container = '') { + if (!$contextual_link_container) { + $contextual_link_container = $block_selector; + } + // Ensure that contextual link element is present because this is required + // to open the off-canvas dialog in edit mode. + $contextual_link = $this->assertSession()->waitForElement('css', "$contextual_link_container .contextual-links a"); + $this->assertNotEmpty($contextual_link); + // Ensure that all other Ajax activity is completed. + $this->assertSession()->assertWaitOnAjaxRequest(); $this->click($block_selector); $this->waitForOffCanvasToOpen(); $this->assertOffCanvasBlockFormIsValid(); @@ -490,4 +503,27 @@ protected function isLabelInputVisible() { return $this->getSession()->getPage()->find('css', static::LABEL_INPUT_SELECTOR)->isVisible(); } + /** + * Test that validation errors appear in the off-canvas dialog. + */ + public function testValidationMessages() { + $page = $this->getSession()->getPage(); + $web_assert = $this->assertSession(); + foreach ($this->getTestThemes() as $theme) { + $this->enableTheme($theme); + $block = $this->placeBlock('outside_in_test_validation'); + $this->drupalGet('user'); + $this->enableEditMode(); + $this->openBlockForm($this->getBlockSelector($block)); + // Use label that will trigger validation error. + // @see _outside_in_test_validate_title + $page->fillField('settings[label]', 'Block label'); + $page->pressButton('Save Block with validation error'); + $web_assert->assertWaitOnAjaxRequest(); + $web_assert->elementContains('css', '#drupal-off-canvas', 'Sorry system error. Please save again'); + $this->disableEditMode(); + $block->delete(); + } + } + }