Loading core/misc/debounce.js +18 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; }; core/misc/dialog/dialog.position.js +13 −1 Original line number Diff line number Diff line Loading @@ -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', Loading Loading @@ -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'); Loading @@ -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'); }); Loading core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php +35 −0 Original line number Diff line number Diff line Loading @@ -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); } } Loading
core/misc/debounce.js +18 −2 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; };
core/misc/dialog/dialog.position.js +13 −1 Original line number Diff line number Diff line Loading @@ -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', Loading Loading @@ -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'); Loading @@ -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'); }); Loading
core/tests/Drupal/FunctionalJavascriptTests/Ajax/DialogTest.php +35 −0 Original line number Diff line number Diff line Loading @@ -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); } }