Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@
},
{
"path": "./dist/js/bootstrap.bundle.js",
"maxSize": "82.5 kB"
"maxSize": "82.75 kB"
},
{
"path": "./dist/js/bootstrap.bundle.min.js",
"maxSize": "53.25 kB"
},
{
"path": "./dist/js/bootstrap.js",
"maxSize": "53.75 kB"
"maxSize": "54.0 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
Expand Down
12 changes: 8 additions & 4 deletions js/src/dialog-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,11 @@ class DialogBase extends BaseComponent {
}

if (preventBodyScroll) {
document.body.classList.add(CLASS_NAME_OPEN)
// Lock scroll on the root element (not <body>) so it lands on the same
// element that carries `scrollbar-gutter: stable`. Co-locating them keeps
// the gutter reserved while the scrollbar is hidden, so the page doesn't
// shift (and the ::backdrop covers the gutter instead of leaving a strip).
document.documentElement.classList.add(CLASS_NAME_OPEN)
}
}

Expand All @@ -181,15 +185,15 @@ class DialogBase extends BaseComponent {
}
}

// Closes the native <dialog> and tears down body-scroll prevention.
// Closes the native <dialog> and tears down scroll prevention.
// Safe to call multiple times — close() is a no-op on a closed dialog.
_closeAndCleanup() {
this._element.close()
this._openedAsModal = false

// Only restore body scroll if no other modal dialogs are open
// Only restore scroll if no other modal dialogs are open
if (!document.querySelector('dialog[open]:modal')) {
document.body.classList.remove(CLASS_NAME_OPEN)
document.documentElement.classList.remove(CLASS_NAME_OPEN)
}
}

Expand Down
18 changes: 9 additions & 9 deletions js/tests/unit/dialog.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('Dialog', () => {
afterEach(() => {
clearFixture()
clearBodyAndDocument()
document.body.classList.remove('dialog-open')
document.documentElement.classList.remove('dialog-open')

for (const dialog of document.querySelectorAll('dialog[open]')) {
dialog.close()
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('Dialog', () => {

dialogEl.addEventListener('shown.bs.dialog', () => {
expect(dialogEl.open).toBeTrue()
expect(document.body.classList.contains('dialog-open')).toBeTrue()
expect(document.documentElement.classList.contains('dialog-open')).toBeTrue()
resolve()
})

Expand Down Expand Up @@ -251,7 +251,7 @@ describe('Dialog', () => {

dialogEl.addEventListener('hidden.bs.dialog', () => {
expect(dialogEl.open).toBeFalse()
expect(document.body.classList.contains('dialog-open')).toBeFalse()
expect(document.documentElement.classList.contains('dialog-open')).toBeFalse()
resolve()
})

Expand Down Expand Up @@ -460,8 +460,8 @@ describe('Dialog', () => {
dialogEl.addEventListener('shown.bs.dialog', () => {
expect(dialogEl.open).toBeTrue()
expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue()
// Non-modal dialogs should not add dialog-open to body
expect(document.body.classList.contains('dialog-open')).toBeFalse()
// Non-modal dialogs should not add dialog-open to the root element
expect(document.documentElement.classList.contains('dialog-open')).toBeFalse()
resolve()
})

Expand Down Expand Up @@ -1036,7 +1036,7 @@ describe('Dialog', () => {
})

describe('stacked modals', () => {
it('should keep dialog-open on body when closing one of two open modal dialogs', () => {
it('should keep dialog-open on the root element when closing one of two open modal dialogs', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = [
'<dialog id="dialog1" class="dialog"></dialog>',
Expand All @@ -1053,18 +1053,18 @@ describe('Dialog', () => {
})

dialog2El.addEventListener('shown.bs.dialog', () => {
expect(document.body.classList.contains('dialog-open')).toBeTrue()
expect(document.documentElement.classList.contains('dialog-open')).toBeTrue()
dialog1.hide()
})

dialog1El.addEventListener('hidden.bs.dialog', () => {
expect(dialog2El.open).toBeTrue()
expect(document.body.classList.contains('dialog-open')).toBeTrue()
expect(document.documentElement.classList.contains('dialog-open')).toBeTrue()
dialog2.hide()
})

dialog2El.addEventListener('hidden.bs.dialog', () => {
expect(document.body.classList.contains('dialog-open')).toBeFalse()
expect(document.documentElement.classList.contains('dialog-open')).toBeFalse()
resolve()
})

Expand Down
12 changes: 6 additions & 6 deletions js/tests/unit/drawer.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Drawer', () => {

afterEach(() => {
clearFixture()
document.body.classList.remove('dialog-open')
document.documentElement.classList.remove('dialog-open')
clearBodyAndDocument()

for (const dialog of document.querySelectorAll('dialog[open]')) {
Expand Down Expand Up @@ -255,15 +255,15 @@ describe('Drawer', () => {
})

describe('options', () => {
it('if scroll is enabled, should not add dialog-open class to body', () => {
it('if scroll is enabled, should not add dialog-open class to the root element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<dialog class="drawer"></dialog>'

const drawerEl = fixtureEl.querySelector('.drawer')
const drawer = new Drawer(drawerEl, { scroll: true, backdrop: false })

drawerEl.addEventListener('shown.bs.drawer', () => {
expect(document.body.classList.contains('dialog-open')).toBeFalse()
expect(document.documentElement.classList.contains('dialog-open')).toBeFalse()
drawer.hide()
})
drawerEl.addEventListener('hidden.bs.drawer', () => {
Expand All @@ -273,19 +273,19 @@ describe('Drawer', () => {
})
})

it('if scroll is disabled, should add dialog-open class to body', () => {
it('if scroll is disabled, should add dialog-open class to the root element', () => {
return new Promise(resolve => {
fixtureEl.innerHTML = '<dialog class="drawer"></dialog>'

const drawerEl = fixtureEl.querySelector('.drawer')
const drawer = new Drawer(drawerEl, { scroll: false })

drawerEl.addEventListener('shown.bs.drawer', () => {
expect(document.body.classList.contains('dialog-open')).toBeTrue()
expect(document.documentElement.classList.contains('dialog-open')).toBeTrue()
drawer.hide()
})
drawerEl.addEventListener('hidden.bs.drawer', () => {
expect(document.body.classList.contains('dialog-open')).toBeFalse()
expect(document.documentElement.classList.contains('dialog-open')).toBeFalse()
resolve()
})
drawer.show()
Expand Down
7 changes: 5 additions & 2 deletions scss/_dialog.scss
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,11 @@ $dialog-sizes: defaults(
// scss-docs-end dialog-sizes

@layer components {
// Prevent body scroll when dialog is open
.dialog-open {
// Prevent page scroll when a dialog is open. Applied to the root element so
// `overflow: hidden` sits on the same element as `scrollbar-gutter: stable`
// (see _root.scss): the gutter stays reserved while the scrollbar is hidden,
// so the page doesn't shift when a dialog opens.
:root.dialog-open {
overflow: hidden;
}

Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/migration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ Bootstrap 6 is a major release with many breaking changes to modernize our codeb
- Data key: `bs.modal` &rarr; `bs.dialog` (affects `Dialog.getInstance()` and `Dialog.getOrCreateInstance()`).
- CSS variables: `--modal-*` &rarr; `--dialog-*`.
- Backdrop: The `.modal-backdrop` DOM element and the legacy `util/backdrop` helper are gone — Dialog uses the native `::backdrop` pseudo-element with `backdrop-filter: blur()` support.
- Body scroll prevention: `.modal-open` on `<body>` &rarr; `.dialog-open` on the `<body>` element.
- Scroll prevention: `.modal-open` on `<body>` &rarr; `.dialog-open` on the root (`<html>`) element, so it pairs with `scrollbar-gutter: stable` and the page doesn't shift when a dialog opens.
- New variant classes: `.dialog-slide-up`, `.dialog-slide-down` (slide animations), `.dialog-instant` (no animation), `.dialog-static` (static backdrop bounce), `.dialog-nonmodal` (non-modal positioning), `.dialog-scrollable`.
- Non-modal support: Set `modal: false` or `data-bs-modal="false"` for non-modal dialogs.
- Dialog swapping: Triggers inside an open dialog can open a new dialog and close the current one automatically.
Expand Down