From 3ea11aab352161270b3170b56f9b8df39fed5b1c Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 23 Jun 2026 15:32:26 -0700 Subject: [PATCH 1/3] Dialog/Drawer: don't scroll the page when restoring focus on close Pass { preventScroll: true } when returning focus to the trigger after close, so the page no longer jumps to the trigger (or to the top when scroll-padding-top is set). Fixes #38070, #41615, #35391. --- js/src/dialog.js | 2 +- js/src/drawer.js | 2 +- js/tests/unit/dialog.spec.js | 2 +- js/tests/unit/drawer.spec.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/src/dialog.js b/js/src/dialog.js index c87c81445593..2542f242f788 100644 --- a/js/src/dialog.js +++ b/js/src/dialog.js @@ -119,7 +119,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( EventHandler.one(target, EVENT_HIDDEN, () => { if (isVisible(this)) { - this.focus() + this.focus({ preventScroll: true }) } }) }) diff --git a/js/src/drawer.js b/js/src/drawer.js index ce71ad0f8cb0..3c7ef67aec9f 100644 --- a/js/src/drawer.js +++ b/js/src/drawer.js @@ -149,7 +149,7 @@ EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function ( EventHandler.one(target, EVENT_HIDDEN, () => { if (isVisible(this)) { - this.focus() + this.focus({ preventScroll: true }) } }) diff --git a/js/tests/unit/dialog.spec.js b/js/tests/unit/dialog.spec.js index c763abe5d25c..7f574949d427 100644 --- a/js/tests/unit/dialog.spec.js +++ b/js/tests/unit/dialog.spec.js @@ -803,7 +803,7 @@ describe('Dialog', () => { const hideListener = () => { setTimeout(() => { - expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith({ preventScroll: true }) resolve() }, 20) } diff --git a/js/tests/unit/drawer.spec.js b/js/tests/unit/drawer.spec.js index a57cbeb1ab83..210d302d991a 100644 --- a/js/tests/unit/drawer.spec.js +++ b/js/tests/unit/drawer.spec.js @@ -649,7 +649,7 @@ describe('Drawer', () => { }) drawerEl.addEventListener('hidden.bs.drawer', () => { setTimeout(() => { - expect(spy).toHaveBeenCalled() + expect(spy).toHaveBeenCalledWith({ preventScroll: true }) resolve() }, 5) }) From f0a0b67548fe684809f8b826327d5f97678be24b Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Tue, 23 Jun 2026 15:32:31 -0700 Subject: [PATCH 2/3] Dialog/Drawer: restore body scroll when disposed while open dispose() now closes the native and removes the dialog-open body class if the instance is torn down while still open (e.g. an SPA route change), instead of leaving overflow: hidden stuck on the body. Fixes #35934, #39910. --- js/src/dialog-base.js | 11 +++++++++++ js/tests/unit/dialog.spec.js | 22 ++++++++++++++++++++++ js/tests/unit/drawer.spec.js | 22 ++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/js/src/dialog-base.js b/js/src/dialog-base.js index 29c66278311b..0aaf5ff8e453 100644 --- a/js/src/dialog-base.js +++ b/js/src/dialog-base.js @@ -117,6 +117,17 @@ class DialogBase extends BaseComponent { }, this._element, this._isAnimated()) } + dispose() { + // If disposed while still open, close the native and restore body + // scroll. Otherwise `dialog-open` (overflow: hidden) would stay stuck on the + // body — e.g. when an SPA tears the component down mid-navigation. + if (this._element.open) { + this._closeAndCleanup() + } + + super.dispose() + } + // Protected — hooks for subclasses to override _getShowOptions() { diff --git a/js/tests/unit/dialog.spec.js b/js/tests/unit/dialog.spec.js index 7f574949d427..5e6c40773089 100644 --- a/js/tests/unit/dialog.spec.js +++ b/js/tests/unit/dialog.spec.js @@ -713,6 +713,28 @@ describe('Dialog', () => { expect(Dialog.getInstance(dialogEl)).toBeNull() expect(spyOff).toHaveBeenCalled() }) + + it('should close the dialog and restore body scroll when disposed while open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const dialogEl = fixtureEl.querySelector('.dialog') + const dialog = new Dialog(dialogEl) + + dialogEl.addEventListener('shown.bs.dialog', () => { + expect(dialogEl.open).toBeTrue() + expect(document.body.classList.contains('dialog-open')).toBeTrue() + + dialog.dispose() + + expect(dialogEl.open).toBeFalse() + expect(document.body.classList.contains('dialog-open')).toBeFalse() + resolve() + }) + + dialog.show() + }) + }) }) describe('data-api', () => { diff --git a/js/tests/unit/drawer.spec.js b/js/tests/unit/drawer.spec.js index 210d302d991a..3cc31cb03010 100644 --- a/js/tests/unit/drawer.spec.js +++ b/js/tests/unit/drawer.spec.js @@ -570,6 +570,28 @@ describe('Drawer', () => { expect(Drawer.getInstance(drawerEl)).toBeNull() expect(spyOff).toHaveBeenCalled() }) + + it('should close the drawer and restore body scroll when disposed while open', () => { + return new Promise(resolve => { + fixtureEl.innerHTML = '' + + const drawerEl = fixtureEl.querySelector('dialog') + const drawer = new Drawer(drawerEl) + + drawerEl.addEventListener('shown.bs.drawer', () => { + expect(drawerEl.open).toBeTrue() + expect(document.body.classList.contains('dialog-open')).toBeTrue() + + drawer.dispose() + + expect(drawerEl.open).toBeFalse() + expect(document.body.classList.contains('dialog-open')).toBeFalse() + resolve() + }) + + drawer.show() + }) + }) }) describe('data-api', () => { From f4228c5bdf77fcc7e62bb11d6e5ce4786d7fdc0d Mon Sep 17 00:00:00 2001 From: Mark Otto Date: Thu, 25 Jun 2026 12:50:06 -0700 Subject: [PATCH 3/3] Bump bundlewatch size thresholds --- .bundlewatch.config.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 2c89f7ea47a8..eb14781be686 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -34,7 +34,7 @@ }, { "path": "./dist/js/bootstrap.bundle.js", - "maxSize": "82.5 kB" + "maxSize": "82.75 kB" }, { "path": "./dist/js/bootstrap.bundle.min.js", @@ -42,7 +42,7 @@ }, { "path": "./dist/js/bootstrap.js", - "maxSize": "53.75 kB" + "maxSize": "54.0 kB" }, { "path": "./dist/js/bootstrap.min.js",