diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js
index 7e439974df6d15fafa49c9fe01ec1b64d38339e7..dca291e08e46094a4ca30bf743f5f42c39fa8a9d 100644
--- a/core/misc/dialog/dialog.ajax.js
+++ b/core/misc/dialog/dialog.ajax.js
@@ -94,6 +94,7 @@
         buttons.push({
           text: $originalButton.html() || $originalButton.attr('value'),
           class: $originalButton.attr('class'),
+          'data-once': $originalButton.data('once'),
           click(e) {
             // If the original button is an anchor tag, triggering the "click"
             // event will not simulate a click. Use the click method instead.
@@ -104,8 +105,8 @@
                 .trigger('mousedown')
                 .trigger('mouseup')
                 .trigger('click');
-              e.preventDefault();
             }
+            e.preventDefault();
           },
         });
       });
diff --git a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
index ec241187703d0de747c205723f593335d2c2fe27..0a3aee617359ec2a7ba4483e6053a795eeae6d8c 100644
--- a/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
+++ b/core/modules/system/tests/modules/ajax_test/src/Form/AjaxTestForm.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\ajax_test\Form;
 
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\MessageCommand;
 use Drupal\Core\Url;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -39,26 +41,44 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#value' => $this->t('Do it'),
     ];
     $form['actions']['preview'] = [
+      '#title' => 'Preview',
+      '#type' => 'link',
+      '#url' => Url::fromRoute('ajax_test.dialog_form'),
+      '#attributes' => [
+        'class' => ['use-ajax', 'button'],
+        'data-dialog-type' => 'modal',
+      ],
+    ];
+    $form['actions']['hello_world'] = [
       '#type' => 'submit',
-      '#value' => $this->t('Preview'),
+      '#value' => $this->t('Hello world'),
       // No regular submit-handler. This form only works via JavaScript.
       '#submit' => [],
       '#ajax' => [
-        // This means the ::preview() method on this class would be invoked in
-        // case of a click event. However, since Drupal core's test runner only
-        // is able to execute PHP, not JS, there is no point in actually
-        // implementing this method, because we can never let it be called from
-        // JS; we'd have to manually call it from PHP, at which point we would
-        // not actually be testing it.
-        // Therefore we consciously choose to not implement this method, because
-        // we cannot meaningfully test it anyway.
-        'callback' => '::preview',
+        'callback' => '::helloWorld',
         'event' => 'click',
       ],
     ];
     return $form;
   }
 
+  /**
+   * An AJAX callback that prints "Hello World!" as a message.
+   *
+   * @param array $form
+   *   The form array to remove elements from.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @return \Drupal\Core\Ajax\AjaxResponse
+   *   An AJAX response.
+   */
+  public function helloWorld(array &$form, FormStateInterface $form_state) {
+    $response = new AjaxResponse();
+    $response->addCommand(new MessageCommand('Hello world!'));
+    return $response;
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
index 1a537175db5c43fb991f68a57bec3a97592cb664..42701e8fcd48ac47b104b2095f10174d78ddb7f9 100644
--- a/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
+++ b/core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php
@@ -145,26 +145,32 @@ public function testDialog() {
     $preview = $form_dialog->findButton('Preview');
     $this->assertNotNull($preview, 'The dialog contains a "Preview" button.');
 
-    // When a form with submit inputs is in a dialog, the form's submit inputs
-    // are copied to the dialog buttonpane as buttons. The originals should have
-    // their styles set to display: none.
-    $hidden_buttons = $this->getSession()->getPage()->findAll('css', '.ajax-test-form [type="submit"]');
-    $this->assertCount(2, $hidden_buttons);
+    // Form submit inputs, link buttons, and buttons in dialog are copied to the
+    // dialog buttonpane as buttons. The originals should have their styles set
+    // to display: none.
+    $hidden_buttons = $this->getSession()->getPage()->findAll('css', '.ajax-test-form .button');
+    $this->assertCount(3, $hidden_buttons);
     $hidden_button_text = [];
     foreach ($hidden_buttons as $button) {
       $styles = $button->getAttribute('style');
       $this->assertStringContainsStringIgnoringCase('display: none;', $styles);
-      $hidden_button_text[] = $button->getAttribute('value');
+      $hidden_button_text[] = $button->hasAttribute('value') ? $button->getAttribute('value') : $button->getHtml();
     }
 
     // The copied buttons should have the same text as the submit inputs they
     // were copied from.
     $moved_to_buttonpane_buttons = $this->getSession()->getPage()->findAll('css', '.ui-dialog-buttonpane button');
-    $this->assertCount(2, $moved_to_buttonpane_buttons);
+    $this->assertCount(3, $moved_to_buttonpane_buttons);
     foreach ($moved_to_buttonpane_buttons as $key => $button) {
       $this->assertEquals($hidden_button_text[$key], $button->getText());
     }
 
+    // Press buttons in the dialog to ensure there are no AJAX errors.
+    $this->assertSession()->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Hello world');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+    $this->assertSession()->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Preview');
+    $this->assertSession()->assertWaitOnAjaxRequest();
+
     // Reset: close the form.
     $form_dialog->findButton('Close')->press();