Unverified Commit d0904d4a authored by Alex Pott's avatar Alex Pott
Browse files

fix: #3472624 Ensure the UI dialog instance is valid in Drupal.dialog.resetSize

By: herved
By: nod_
By: longwave
By: godotislate
By: alexpott
parent 8cb43d97
Loading
Loading
Loading
Loading
+18 −2
Original line number Diff line number Diff line
@@ -24,12 +24,14 @@
 *   Whether we wait at the beginning or end to execute the function.
 *
 * @return {function}
 *   The debounced function.
 *   The debounced function. Call its cancel() method to drop an invocation
 *   that is scheduled but has not run yet.
 */
Drupal.debounce = function (func, wait, immediate) {
  let timeout;
  let result;
  return function (...args) {

  const debounced = function (...args) {
    const context = this;
    const later = function () {
      timeout = null;
@@ -45,4 +47,18 @@ Drupal.debounce = function (func, wait, immediate) {
    }
    return result;
  };

  /**
   * Cancels a scheduled invocation that has not run yet.
   *
   * Unbinding the event handler that triggered the debounced function does
   * not stop an invocation that is already scheduled. Use this when the
   * callback would act on state that no longer exists.
   */
  debounced.cancel = () => {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
};
+13 −1
Original line number Diff line number Diff line
@@ -63,6 +63,11 @@
   * @fires event:dialogContentResize
   */
  function resetSize(event) {
    // Ensure the UI dialog instance exists/is valid.
    if (!event.data?.$element?.data('ui-dialog')) {
      return;
    }

    const positionOptions = [
      'width',
      'height',
@@ -129,6 +134,8 @@
        .dialog('option', { resizable: false, draggable: false })
        .dialog('widget');
      uiDialog[0].style.position = 'fixed';
      // Keep a reference so that a scheduled call can be cancelled on close.
      $element.data('drupalAutoResize', autoResize);
      $(window)
        .on('resize.dialogResize scroll.dialogResize', eventData, autoResize)
        .trigger('resize.dialogResize');
@@ -140,7 +147,12 @@
    }
  });

  window.addEventListener('dialog:beforeclose', () => {
  window.addEventListener('dialog:beforeclose', (e) => {
    // Unbinding the handlers below does not stop a resize that is already
    // scheduled. Closing a dialog removes its element from the DOM, which
    // destroys the jQuery UI instance, so a call that runs after this point
    // would act on a destroyed dialog and throw.
    $(e.target).data('drupalAutoResize')?.cancel();
    $(window).off('.dialogResize');
    $(document).off('.dialogResize');
  });
+35 −0
Original line number Diff line number Diff line
@@ -263,4 +263,39 @@ public function testHttpMethod(): void {
    $this->assertSame(808, $width);
  }

  /**
   * Tests that closing a dialog cancels a resize that is already scheduled.
   *
   * Dialog resizing is debounced by 20ms. Closing a dialog removes its element
   * from the DOM, which destroys the jQuery UI instance, so a resize that runs
   * after the dialog closed would act on a destroyed dialog and throw.
   *
   * @see https://www.drupal.org/node/3472624
   */
  public function testCloseCancelsScheduledResize(): void {
    $this->drupalGet('ajax-test/dialog');
    $this->getSession()->getPage()->clickLink('Link 1 (modal)');
    $this->assertNotNull($this->assertSession()->waitForElementVisible('css', 'div.ui-dialog'));

    // Schedule a resize, then close the dialog in the same tick so that the
    // resize is always still pending when the dialog goes away. Flag the end
    // of a period comfortably longer than the 20ms debounce, so that the test
    // does not assert before a resize would have run.
    $script = <<<SCRIPT
      (function () {
        window.dialogResizeElapsed = false;
        jQuery(document).trigger('drupalViewportOffsetChange', Drupal.displace.offsets);
        document.querySelector('.ui-dialog button[title="Close"]').click();
        window.setTimeout(function () {
          window.dialogResizeElapsed = true;
        }, 200);
      }())
      SCRIPT;
    $this->getSession()->executeScript($script);
    $this->assertJsCondition('window.dialogResizeElapsed === true');

    $errors = $this->getSession()->evaluateScript("JSON.parse(sessionStorage.getItem('js_testing_log_test.errors') || JSON.stringify([]))");
    $this->assertSame([], $errors);
  }

}