diff --git a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php index 19a701638d..8e74754fdb 100644 --- a/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php +++ b/core/modules/outside_in/src/Block/BlockEntityOffCanvasForm.php @@ -5,8 +5,10 @@ use Drupal\block\BlockForm; use Drupal\block\BlockInterface; use Drupal\Core\Block\BlockPluginInterface; +use Drupal\Core\Form\DialogFormTrait; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\PluginWithFormsInterface; +use Drupal\outside_in\OffCanvasFormDialogTrait; /** * Provides form for block instance forms when used in the off-canvas dialog. @@ -18,6 +20,8 @@ */ class BlockEntityOffCanvasForm extends BlockForm { + use OffCanvasFormDialogTrait; + /** * Provides a title callback to get the block's admin label. * @@ -111,4 +115,24 @@ protected function getPluginForm(BlockPluginInterface $block) { return $block; } + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + // \Drupal\block\BlockForm::submitForm() always redirects to block listing + // via \Drupal\Core\Form\FormStateInterface::setRedirect(). This method + // does not work with Ajax submit. + $form_state->disableRedirect(); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $form = parent::buildForm($form, $form_state); + $this->buildFormDialog($form, $form_state, FALSE, '#drupal-off-canvas'); + return $form; + } + } diff --git a/core/modules/outside_in/src/OffCanvasFormDialogTrait.php b/core/modules/outside_in/src/OffCanvasFormDialogTrait.php new file mode 100644 index 0000000000..990d897e4f --- /dev/null +++ b/core/modules/outside_in/src/OffCanvasFormDialogTrait.php @@ -0,0 +1,160 @@ +getRequest() + ->get(MainContentViewSubscriber::WRAPPER_FORMAT); + return $wrapper_format === 'drupal_dialog.off_canvas'; + } + + /** + * Adds modal dialog support to a form. + * + * @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. + */ + protected function buildFormDialog(array &$form, FormStateInterface $form_state) { + if (!$this->isOffCanvasDialog()) { + return; + } + + $form['actions']['submit']['#ajax'] = [ + 'dialogType' => $this->getDialogType(), + 'dialogRenderer' => $this->getDialogRenderer(), + 'callback' => '::submitFormDialog', + 'event' => 'click', + ]; + + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + $form['#attributes']['id'] = 'off-canvas-form'; + } + + /** + * @return mixed + */ + protected function getRequestWrapperFormat() { + $wrapper_format = $this->getRequest() + ->get(MainContentViewSubscriber::WRAPPER_FORMAT); + return $wrapper_format; + } + /** + * + */ + protected function getDialogType() { + $wrapper_format = $this->getRequestWrapperFormat(); + $wrapper_format_parts = explode('.', $wrapper_format); + // Remove the "drupal_" prefix. + return substr($wrapper_format_parts[0], 7); + } + + /** + * + */ + protected function getDialogRenderer() { + $wrapper_format = $this->getRequestWrapperFormat(); + $wrapper_format_parts = explode('.', $wrapper_format); + return isset($wrapper_format_parts[1]) ? $wrapper_format_parts[1] : NULL; + } + + /** + * 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 + */ + 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('#off-canvas-form', $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. + * + * Isolate a form's redirect URL/destination so that it can be used by + * ::submitFormDialog or ::submitForm. + * + * @return \Drupal\Core\Url|null + * The redirect URL or NULL if dialog should just be closed. + */ + protected function getRedirectUrl() { + return $this->getDestinationUrl(); + } + + /** + * Gets the URL from the destination service. + * + * @return \Drupal\Core\Url|null + * The destination URL or NULL no destination available. + */ + protected function getDestinationUrl() { + if ($destination = $this->getRedirectDestinationPath()) { + return Url::fromUserInput('/' . $destination); + } + } + + /** + * Gets the redirect destination path if specified in request. + * + * \Drupal\Core\Routing\RedirectDestination::get() cannot be used directly + * because it will use if 'destination' is not in the query string. + * + * @return string|null + * The redirect path or NULL if it is not specified. + */ + protected function getRedirectDestinationPath() { + if ($this->requestStack->getCurrentRequest()->get('destination')) { + return $this->getRedirectDestination()->get(); + } + } + +} diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml b/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml index 4ae56c4ee8..f0b3a43653 100644 --- a/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml +++ b/core/modules/outside_in/tests/modules/off_canvas_test/off_canvas_test.routing.yml @@ -27,3 +27,10 @@ off_canvas_test.dialog_links: _controller: '\Drupal\off_canvas_test\Controller\TestController::otherDialogLinks' requirements: _access: 'TRUE' + +off_canvas_test.form: + path: '/off-canvas-form' + defaults: + _form: '\Drupal\off_canvas_test\Form\TestForm' + requirements: + _access: 'TRUE' diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php b/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php index 6164b06fc2..58bebc3a0b 100644 --- a/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php +++ b/core/modules/outside_in/tests/modules/off_canvas_test/src/Controller/TestController.php @@ -92,6 +92,25 @@ public function linksDisplay() { ], ], ], + 'off_canvas_form' => [ + '#title' => 'Show form!', + '#type' => 'link', + '#url' => Url::fromRoute( + 'off_canvas_test.form', + [], + ['query' => ['destination' => 'off-canvas-test-links']] + ), + '#attributes' => [ + 'class' => ['use-ajax'], + 'data-dialog-type' => 'dialog', + 'data-dialog-renderer' => 'off_canvas', + ], + '#attached' => [ + 'library' => [ + 'outside_in/drupal.outside_in', + ], + ], + ], ]; } diff --git a/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php b/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php new file mode 100644 index 0000000000..a9a4350042 --- /dev/null +++ b/core/modules/outside_in/tests/modules/off_canvas_test/src/Form/TestForm.php @@ -0,0 +1,59 @@ + 'checkbox', + '#title' => $this->t('Force error?'), + ]; + $form['actions'] = [ + '#type' => 'actions', + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Submit'), + ], + ]; + $this->buildFormDialog($form, $form_state); + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + if ($form_state->getValue('force_error')) { + $form_state->setErrorByName('force_error', 'Validation error'); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + drupal_set_message('submitted'); + } + +} diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php index 2f64d9fe2c..a2bd7a46a5 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OffCanvasTest.php @@ -120,4 +120,32 @@ protected function getTestThemes() { return array_merge(parent::getTestThemes(), ['seven']); } + /** + * Tests form errors in the Off-Canvas dialog. + */ + public function testFormErrors() { + $web_assert = $this->assertSession(); + $page = $this->getSession()->getPage(); + + // Submit form with no error and sending a destination. + $this->drupalGet('/off-canvas-test-links'); + $page->clickLink('Show form!'); + $this->waitForOffCanvasToOpen(); + $page->pressButton('Submit'); + $web_assert->assertWaitOnAjaxRequest(); + // Make sure the changes are present. + $this->assertNotEmpty($web_assert->waitForElement('css', 'div.messages.messages--status:contains(submitted)')); + $web_assert->elementNotContains('css', 'body', 'Validation error'); + + // Submit form with an error and sending a destination. + $this->drupalGet('/off-canvas-test-links'); + $page->clickLink('Show form!'); + $this->waitForOffCanvasToOpen(); + $page->checkField('Force error?'); + $page->pressButton('Submit'); + $web_assert->assertWaitOnAjaxRequest(); + $web_assert->elementNotContains('css', 'body', 'submitted'); + $web_assert->elementContains('css', '#drupal-off-canvas', 'Validation error'); + } + } diff --git a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php index 04fc230f5e..5d873388b5 100644 --- a/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php +++ b/core/modules/outside_in/tests/src/FunctionalJavascript/OutsideInBlockFormTest.php @@ -114,10 +114,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 +130,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 +153,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 +161,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', @@ -225,8 +224,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();