export { default as Button } from './src/button.js'
export { default as Carousel } from './src/carousel.js'
export { default as Collapse } from './src/collapse.js'
+export { default as Dialog } from './src/dialog.js'
export { default as Dropdown } from './src/dropdown.js'
-export { default as Modal } from './src/modal.js'
export { default as Offcanvas } from './src/offcanvas.js'
export { default as Popover } from './src/popover.js'
export { default as ScrollSpy } from './src/scrollspy.js'
import Button from './src/button.js'
import Carousel from './src/carousel.js'
import Collapse from './src/collapse.js'
+import Dialog from './src/dialog.js'
import Dropdown from './src/dropdown.js'
-import Modal from './src/modal.js'
import Offcanvas from './src/offcanvas.js'
import Popover from './src/popover.js'
import ScrollSpy from './src/scrollspy.js'
Button,
Carousel,
Collapse,
+ Dialog,
Dropdown,
- Modal,
Offcanvas,
Popover,
ScrollSpy,
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap dialog.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import BaseComponent from './base-component.js'
+import EventHandler from './dom/event-handler.js'
+import Manipulator from './dom/manipulator.js'
+import SelectorEngine from './dom/selector-engine.js'
+import { enableDismissTrigger } from './util/component-functions.js'
+import { isVisible } from './util/index.js'
+
+/**
+ * Constants
+ */
+
+const NAME = 'dialog'
+const DATA_KEY = 'bs.dialog'
+const EVENT_KEY = `.${DATA_KEY}`
+const DATA_API_KEY = '.data-api'
+
+const EVENT_SHOW = `show${EVENT_KEY}`
+const EVENT_SHOWN = `shown${EVENT_KEY}`
+const EVENT_HIDE = `hide${EVENT_KEY}`
+const EVENT_HIDDEN = `hidden${EVENT_KEY}`
+const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
+const EVENT_CANCEL = `cancel${EVENT_KEY}`
+const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
+
+const CLASS_NAME_STATIC = 'dialog-static'
+const CLASS_NAME_OPEN = 'dialog-open'
+const CLASS_NAME_NONMODAL = 'dialog-nonmodal'
+
+const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dialog"]'
+const SELECTOR_OPEN_MODAL_DIALOG = 'dialog.dialog[open]:not(.dialog-nonmodal)'
+
+const Default = {
+ backdrop: true, // true (click dismisses) or 'static' (click does nothing) - only applies to modal dialogs
+ keyboard: true,
+ modal: true // true uses showModal(), false uses show() for non-modal dialogs
+}
+
+const DefaultType = {
+ backdrop: '(boolean|string)',
+ keyboard: 'boolean',
+ modal: 'boolean'
+}
+
+/**
+ * Class definition
+ */
+
+class Dialog extends BaseComponent {
+ constructor(element, config) {
+ super(element, config)
+
+ this._isTransitioning = false
+ this._addEventListeners()
+ }
+
+ // Getters
+ static get Default() {
+ return Default
+ }
+
+ static get DefaultType() {
+ return DefaultType
+ }
+
+ static get NAME() {
+ return NAME
+ }
+
+ // Public
+ toggle(relatedTarget) {
+ return this._element.open ? this.hide() : this.show(relatedTarget)
+ }
+
+ show(relatedTarget) {
+ if (this._element.open || this._isTransitioning) {
+ return
+ }
+
+ const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
+ relatedTarget
+ })
+
+ if (showEvent.defaultPrevented) {
+ return
+ }
+
+ this._isTransitioning = true
+
+ if (this._config.modal) {
+ // Modal dialog: use showModal() for focus trapping, backdrop, and top layer
+ this._element.showModal()
+ // Prevent body scroll for modal dialogs
+ document.body.classList.add(CLASS_NAME_OPEN)
+ } else {
+ // Non-modal dialog: use show() - no backdrop, no focus trap, no top layer
+ this._element.classList.add(CLASS_NAME_NONMODAL)
+ this._element.show()
+ }
+
+ this._queueCallback(() => {
+ this._isTransitioning = false
+ EventHandler.trigger(this._element, EVENT_SHOWN, {
+ relatedTarget
+ })
+ }, this._element, this._isAnimated())
+ }
+
+ hide() {
+ if (!this._element.open || this._isTransitioning) {
+ return
+ }
+
+ const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
+
+ if (hideEvent.defaultPrevented) {
+ return
+ }
+
+ this._isTransitioning = true
+
+ this._queueCallback(() => this._hideDialog(), this._element, this._isAnimated())
+ }
+
+ dispose() {
+ EventHandler.off(this._element, EVENT_KEY)
+ super.dispose()
+ }
+
+ handleUpdate() {
+ // Provided for API consistency with Modal.
+ // Native dialogs handle their own positioning.
+ }
+
+ // Private
+ _hideDialog() {
+ this._element.close()
+ this._element.classList.remove(CLASS_NAME_NONMODAL)
+ this._isTransitioning = false
+
+ // Only restore body scroll if no other modal dialogs are open
+ if (!document.querySelector(SELECTOR_OPEN_MODAL_DIALOG)) {
+ document.body.classList.remove(CLASS_NAME_OPEN)
+ }
+
+ EventHandler.trigger(this._element, EVENT_HIDDEN)
+ }
+
+ _isAnimated() {
+ return this._element.classList.contains('fade')
+ }
+
+ _triggerBackdropTransition() {
+ const hidePreventedEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
+ if (hidePreventedEvent.defaultPrevented) {
+ return
+ }
+
+ this._element.classList.add(CLASS_NAME_STATIC)
+ this._queueCallback(() => {
+ this._element.classList.remove(CLASS_NAME_STATIC)
+ }, this._element)
+ }
+
+ _addEventListeners() {
+ // Handle native cancel event (Escape key) - only fires for modal dialogs
+ EventHandler.on(this._element, 'cancel', event => {
+ // Prevent native close behavior - we'll handle it
+ event.preventDefault()
+
+ if (!this._config.keyboard) {
+ this._triggerBackdropTransition()
+ return
+ }
+
+ EventHandler.trigger(this._element, EVENT_CANCEL)
+ this.hide()
+ })
+
+ // Handle Escape key for non-modal dialogs (native cancel doesn't fire for show())
+ EventHandler.on(this._element, 'keydown', event => {
+ if (event.key !== 'Escape' || this._config.modal) {
+ return
+ }
+
+ event.preventDefault()
+
+ if (!this._config.keyboard) {
+ return
+ }
+
+ EventHandler.trigger(this._element, EVENT_CANCEL)
+ this.hide()
+ })
+
+ // Handle backdrop clicks (only applies to modal dialogs)
+ // Native <dialog> fires click on the dialog element when backdrop is clicked
+ EventHandler.on(this._element, 'click', event => {
+ // Only handle clicks directly on the dialog (backdrop area)
+ // Non-modal dialogs don't have a backdrop
+ if (event.target !== this._element || !this._config.modal) {
+ return
+ }
+
+ if (this._config.backdrop === 'static') {
+ this._triggerBackdropTransition()
+ return
+ }
+
+ // Default: click backdrop to dismiss
+ this.hide()
+ })
+ }
+}
+
+/**
+ * Data API implementation
+ */
+
+EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
+ const target = SelectorEngine.getElementFromSelector(this)
+
+ if (['A', 'AREA'].includes(this.tagName)) {
+ event.preventDefault()
+ }
+
+ EventHandler.one(target, EVENT_SHOW, showEvent => {
+ if (showEvent.defaultPrevented) {
+ return
+ }
+
+ EventHandler.one(target, EVENT_HIDDEN, () => {
+ if (isVisible(this)) {
+ this.focus()
+ }
+ })
+ })
+
+ // Get config from trigger's data attributes
+ const config = Manipulator.getDataAttributes(this)
+
+ // Check if trigger is inside an open dialog
+ const currentDialog = this.closest('dialog[open]')
+ const shouldSwap = currentDialog && currentDialog !== target
+
+ if (shouldSwap) {
+ // Open new dialog first (its backdrop appears over current)
+ const newDialog = Dialog.getOrCreateInstance(target, config)
+ newDialog.show(this)
+
+ // Close the current dialog (no backdrop flash since new one is already open)
+ const currentInstance = Dialog.getInstance(currentDialog)
+ if (currentInstance) {
+ currentInstance.hide()
+ }
+
+ return
+ }
+
+ const data = Dialog.getOrCreateInstance(target, config)
+ data.toggle(this)
+})
+
+enableDismissTrigger(Dialog)
+
+export default Dialog
--- /dev/null
+import EventHandler from '../../src/dom/event-handler.js'
+import Dialog from '../../src/dialog.js'
+import {
+ clearBodyAndDocument, clearFixture, createEvent, getFixture
+} from '../helpers/fixture.js'
+
+describe('Dialog', () => {
+ let fixtureEl
+
+ beforeAll(() => {
+ fixtureEl = getFixture()
+ })
+
+ afterEach(() => {
+ clearFixture()
+ clearBodyAndDocument()
+ document.body.classList.remove('dialog-open')
+
+ for (const dialog of document.querySelectorAll('dialog[open]')) {
+ dialog.close()
+ }
+ })
+
+ beforeEach(() => {
+ clearBodyAndDocument()
+ })
+
+ describe('VERSION', () => {
+ it('should return plugin version', () => {
+ expect(Dialog.VERSION).toEqual(jasmine.any(String))
+ })
+ })
+
+ describe('Default', () => {
+ it('should return plugin default config', () => {
+ expect(Dialog.Default).toEqual(jasmine.any(Object))
+ })
+ })
+
+ describe('DATA_KEY', () => {
+ it('should return plugin data key', () => {
+ expect(Dialog.DATA_KEY).toEqual('bs.dialog')
+ })
+ })
+
+ describe('constructor', () => {
+ it('should take care of element either passed as a CSS selector or DOM element', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog" id="testDialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialogBySelector = new Dialog('#testDialog')
+ const dialogByElement = new Dialog(dialogEl)
+
+ expect(dialogBySelector._element).toEqual(dialogEl)
+ expect(dialogByElement._element).toEqual(dialogEl)
+ })
+ })
+
+ describe('toggle', () => {
+ it('should toggle the dialog open state', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialogEl.open).toBeTrue()
+ dialog.toggle()
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(dialogEl.open).toBeFalse()
+ resolve()
+ })
+
+ dialog.toggle()
+ })
+ })
+ })
+
+ describe('show', () => {
+ it('should show a dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('show.bs.dialog', event => {
+ expect(event).toBeDefined()
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialogEl.open).toBeTrue()
+ expect(document.body.classList.contains('dialog-open')).toBeTrue()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should pass relatedTarget to show event', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button id="trigger"></button>',
+ '<dialog class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('#trigger')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('show.bs.dialog', event => {
+ expect(event.relatedTarget).toEqual(trigger)
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', event => {
+ expect(event.relatedTarget).toEqual(trigger)
+ resolve()
+ })
+
+ dialog.show(trigger)
+ })
+ })
+
+ it('should do nothing if a dialog is already open', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ // Manually open the dialog
+ dialogEl.showModal()
+
+ const spy = spyOn(EventHandler, 'trigger')
+ dialog.show()
+
+ expect(spy).not.toHaveBeenCalled()
+ })
+
+ it('should do nothing if a dialog is transitioning', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ const spy = spyOn(EventHandler, 'trigger')
+ dialog._isTransitioning = true
+
+ dialog.show()
+
+ expect(spy).not.toHaveBeenCalled()
+ })
+
+ it('should not fire shown event when show is prevented', () => {
+ return new Promise((resolve, reject) => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('show.bs.dialog', event => {
+ event.preventDefault()
+
+ const expectedDone = () => {
+ expect().nothing()
+ resolve()
+ }
+
+ setTimeout(expectedDone, 10)
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ reject(new Error('shown event triggered'))
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should set is transitioning if fade class is present', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog fade"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('show.bs.dialog', () => {
+ setTimeout(() => {
+ expect(dialog._isTransitioning).toBeTrue()
+ })
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialog._isTransitioning).toBeFalse()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should close dialog when a click occurred on data-bs-dismiss="dialog" inside dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<dialog class="dialog">',
+ ' <div class="dialog-header">',
+ ' <button type="button" data-bs-dismiss="dialog"></button>',
+ ' </div>',
+ '</dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const btnClose = fixtureEl.querySelector('[data-bs-dismiss="dialog"]')
+ const dialog = new Dialog(dialogEl)
+
+ const spy = spyOn(dialog, 'hide').and.callThrough()
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ btnClose.click()
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+ })
+
+ describe('hide', () => {
+ it('should hide a dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ dialog.hide()
+ })
+
+ dialogEl.addEventListener('hide.bs.dialog', event => {
+ expect(event).toBeDefined()
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(dialogEl.open).toBeFalse()
+ expect(document.body.classList.contains('dialog-open')).toBeFalse()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should do nothing if the dialog is not shown', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialog.hide()
+
+ expect().nothing()
+ })
+
+ it('should do nothing if the dialog is transitioning', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.showModal()
+ dialog._isTransitioning = true
+ dialog.hide()
+
+ expect().nothing()
+ })
+
+ it('should not hide a dialog if hide is prevented', () => {
+ return new Promise((resolve, reject) => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ dialog.hide()
+ })
+
+ const hideCallback = () => {
+ setTimeout(() => {
+ expect(dialogEl.open).toBeTrue()
+ resolve()
+ }, 10)
+ }
+
+ dialogEl.addEventListener('hide.bs.dialog', event => {
+ event.preventDefault()
+ hideCallback()
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ reject(new Error('should not trigger hidden'))
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should close dialog when backdrop is clicked', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ const spy = spyOn(dialog, 'hide').and.callThrough()
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ // Click directly on the dialog element (backdrop area)
+ const clickEvent = createEvent('click')
+ Object.defineProperty(clickEvent, 'target', { value: dialogEl })
+ dialogEl.dispatchEvent(clickEvent)
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should not close dialog when clicking inside dialog content', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<dialog class="dialog">',
+ ' <div class="dialog-body">Content</div>',
+ '</dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialogBody = fixtureEl.querySelector('.dialog-body')
+ const dialog = new Dialog(dialogEl)
+
+ const spy = spyOn(dialog, 'hide')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ // Click on inner content - should not close
+ const clickEvent = createEvent('click', { bubbles: true })
+ dialogBody.dispatchEvent(clickEvent)
+
+ setTimeout(() => {
+ expect(spy).not.toHaveBeenCalled()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+ })
+
+ describe('backdrop static', () => {
+ it('should not close dialog when backdrop is static and backdrop is clicked', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ backdrop: 'static'
+ })
+
+ const spy = spyOn(dialog, 'hide')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const clickEvent = createEvent('click')
+ Object.defineProperty(clickEvent, 'target', { value: dialogEl })
+ dialogEl.dispatchEvent(clickEvent)
+
+ setTimeout(() => {
+ expect(spy).not.toHaveBeenCalled()
+ expect(dialogEl.open).toBeTrue()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should add dialog-static class when backdrop is static and clicked', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ backdrop: 'static'
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const clickEvent = createEvent('click')
+ Object.defineProperty(clickEvent, 'target', { value: dialogEl })
+ dialogEl.dispatchEvent(clickEvent)
+
+ expect(dialogEl.classList.contains('dialog-static')).toBeTrue()
+
+ setTimeout(() => {
+ expect(dialogEl.classList.contains('dialog-static')).toBeFalse()
+ resolve()
+ }, 300)
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should fire hidePrevented.bs.dialog event when static backdrop is clicked', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ backdrop: 'static'
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const clickEvent = createEvent('click')
+ Object.defineProperty(clickEvent, 'target', { value: dialogEl })
+ dialogEl.dispatchEvent(clickEvent)
+ })
+
+ dialogEl.addEventListener('hidePrevented.bs.dialog', () => {
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+ })
+
+ describe('non-modal dialogs', () => {
+ it('should open a non-modal dialog with show() when modal = false', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ modal: false
+ })
+
+ 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()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should remove dialog-nonmodal class on hide', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ modal: false
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue()
+ dialog.hide()
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(dialogEl.classList.contains('dialog-nonmodal')).toBeFalse()
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should not respond to backdrop clicks for non-modal dialogs', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ modal: false
+ })
+
+ const spy = spyOn(dialog, 'hide')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const clickEvent = createEvent('click')
+ Object.defineProperty(clickEvent, 'target', { value: dialogEl })
+ dialogEl.dispatchEvent(clickEvent)
+
+ setTimeout(() => {
+ expect(spy).not.toHaveBeenCalled()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should close non-modal dialog with escape key when keyboard = true', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ modal: false,
+ keyboard: true
+ })
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const keydownEvent = createEvent('keydown')
+ keydownEvent.key = 'Escape'
+ dialogEl.dispatchEvent(keydownEvent)
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should not close non-modal dialog with escape key when keyboard = false', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ modal: false,
+ keyboard: false
+ })
+
+ const spy = spyOn(dialog, 'hide')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const keydownEvent = createEvent('keydown')
+ keydownEvent.key = 'Escape'
+ dialogEl.dispatchEvent(keydownEvent)
+
+ setTimeout(() => {
+ expect(spy).not.toHaveBeenCalled()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should use data-bs-modal="false" to create non-modal dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog" data-bs-modal="false"></button>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const dialog = Dialog.getInstance(dialogEl)
+ expect(dialog._config.modal).toBeFalse()
+ expect(dialogEl.classList.contains('dialog-nonmodal')).toBeTrue()
+ resolve()
+ })
+
+ trigger.click()
+ })
+ })
+ })
+
+ describe('handleUpdate', () => {
+ it('should exist for API consistency', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ expect(typeof dialog.handleUpdate).toEqual('function')
+ // Should not throw
+ dialog.handleUpdate()
+ })
+ })
+
+ describe('keyboard', () => {
+ it('should close dialog when escape key is pressed', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const cancelEvent = createEvent('cancel')
+ dialogEl.dispatchEvent(cancelEvent)
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should fire cancel.bs.dialog event when escape is pressed', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const cancelEvent = createEvent('cancel')
+ dialogEl.dispatchEvent(cancelEvent)
+ })
+
+ dialogEl.addEventListener('cancel.bs.dialog', () => {
+ resolve()
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should not close dialog when escape key is pressed with keyboard = false', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ keyboard: false
+ })
+
+ const spy = spyOn(dialog, 'hide')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const cancelEvent = createEvent('cancel')
+ dialogEl.dispatchEvent(cancelEvent)
+
+ setTimeout(() => {
+ expect(spy).not.toHaveBeenCalled()
+ expect(dialogEl.open).toBeTrue()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+
+ it('should show static backdrop animation when escape pressed and keyboard = false', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl, {
+ keyboard: false
+ })
+
+ const spy = spyOn(dialog, '_triggerBackdropTransition').and.callThrough()
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const cancelEvent = createEvent('cancel')
+ dialogEl.dispatchEvent(cancelEvent)
+
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ }, 10)
+ })
+
+ dialog.show()
+ })
+ })
+ })
+
+ describe('dispose', () => {
+ it('should dispose a dialog', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog" id="exampleDialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+
+ expect(Dialog.getInstance(dialogEl)).toEqual(dialog)
+
+ const spyOff = spyOn(EventHandler, 'off')
+
+ dialog.dispose()
+
+ expect(Dialog.getInstance(dialogEl)).toBeNull()
+ expect(spyOff).toHaveBeenCalled()
+ })
+ })
+
+ describe('data-api', () => {
+ it('should toggle dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button type="button" data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialogEl.open).toBeTrue()
+ setTimeout(() => trigger.click(), 10)
+ })
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ expect(dialogEl.open).toBeFalse()
+ resolve()
+ })
+
+ trigger.click()
+ })
+ })
+
+ it('should not recreate a new dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button type="button" data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const dialog = new Dialog(dialogEl)
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ const spy = spyOn(dialog, 'toggle').and.callThrough()
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ })
+
+ trigger.click()
+ })
+ })
+
+ it('should prevent default when the trigger is <a> or <area>', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<a data-bs-toggle="dialog" href="#" data-bs-target="#exampleDialog"></a>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ expect(dialogEl.open).toBeTrue()
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ })
+
+ trigger.click()
+ })
+ })
+
+ it('should focus the trigger on hide', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog"></button>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ const spy = spyOn(trigger, 'focus')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const dialog = Dialog.getInstance(dialogEl)
+ dialog.hide()
+ })
+
+ const hideListener = () => {
+ setTimeout(() => {
+ expect(spy).toHaveBeenCalled()
+ resolve()
+ }, 20)
+ }
+
+ dialogEl.addEventListener('hidden.bs.dialog', () => {
+ hideListener()
+ })
+
+ trigger.click()
+ })
+ })
+
+ it('should use data attributes for config', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button data-bs-toggle="dialog" data-bs-target="#exampleDialog" data-bs-backdrop="static"></button>',
+ '<dialog id="exampleDialog" class="dialog"></dialog>'
+ ].join('')
+
+ const dialogEl = fixtureEl.querySelector('.dialog')
+ const trigger = fixtureEl.querySelector('[data-bs-toggle="dialog"]')
+
+ dialogEl.addEventListener('shown.bs.dialog', () => {
+ const dialog = Dialog.getInstance(dialogEl)
+ expect(dialog._config.backdrop).toEqual('static')
+ resolve()
+ })
+
+ trigger.click()
+ })
+ })
+ })
+
+ describe('dialog swapping', () => {
+ it('should swap dialogs when trigger is inside an open dialog', () => {
+ return new Promise(resolve => {
+ fixtureEl.innerHTML = [
+ '<button data-bs-toggle="dialog" data-bs-target="#dialog1">Open first</button>',
+ '<dialog id="dialog1" class="dialog">',
+ ' <button data-bs-toggle="dialog" data-bs-target="#dialog2">Go to second</button>',
+ '</dialog>',
+ '<dialog id="dialog2" class="dialog"></dialog>'
+ ].join('')
+
+ const dialog1El = fixtureEl.querySelector('#dialog1')
+ const dialog2El = fixtureEl.querySelector('#dialog2')
+ const firstTrigger = fixtureEl.querySelector('[data-bs-target="#dialog1"]')
+ const swapTrigger = dialog1El.querySelector('[data-bs-target="#dialog2"]')
+
+ dialog1El.addEventListener('shown.bs.dialog', () => {
+ // Now click the swap trigger inside dialog1
+ swapTrigger.click()
+ })
+
+ dialog2El.addEventListener('shown.bs.dialog', () => {
+ expect(dialog2El.open).toBeTrue()
+ })
+
+ dialog1El.addEventListener('hidden.bs.dialog', () => {
+ expect(dialog1El.open).toBeFalse()
+ expect(dialog2El.open).toBeTrue()
+ resolve()
+ })
+
+ firstTrigger.click()
+ })
+ })
+ })
+
+ describe('getInstance', () => {
+ it('should return dialog instance', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+ const dialog = new Dialog(dialogEl)
+
+ expect(Dialog.getInstance(dialogEl)).toEqual(dialog)
+ expect(Dialog.getInstance(dialogEl)).toBeInstanceOf(Dialog)
+ })
+
+ it('should return null when there is no dialog instance', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+
+ expect(Dialog.getInstance(dialogEl)).toBeNull()
+ })
+ })
+
+ describe('getOrCreateInstance', () => {
+ it('should return dialog instance', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+ const dialog = new Dialog(dialogEl)
+
+ expect(Dialog.getOrCreateInstance(dialogEl)).toEqual(dialog)
+ expect(Dialog.getInstance(dialogEl)).toEqual(Dialog.getOrCreateInstance(dialogEl, {}))
+ expect(Dialog.getOrCreateInstance(dialogEl)).toBeInstanceOf(Dialog)
+ })
+
+ it('should return new instance when there is no dialog instance', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+
+ expect(Dialog.getInstance(dialogEl)).toBeNull()
+ expect(Dialog.getOrCreateInstance(dialogEl)).toBeInstanceOf(Dialog)
+ })
+
+ it('should return new instance when there is no dialog instance with given configuration', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+
+ expect(Dialog.getInstance(dialogEl)).toBeNull()
+ const dialog = Dialog.getOrCreateInstance(dialogEl, {
+ backdrop: 'static'
+ })
+ expect(dialog).toBeInstanceOf(Dialog)
+ expect(dialog._config.backdrop).toEqual('static')
+ })
+
+ it('should return the instance when exists without given configuration', () => {
+ fixtureEl.innerHTML = '<dialog class="dialog"></dialog>'
+
+ const dialogEl = fixtureEl.querySelector('dialog')
+ const dialog = new Dialog(dialogEl, {
+ backdrop: 'static'
+ })
+ expect(Dialog.getInstance(dialogEl)).toEqual(dialog)
+
+ const dialog2 = Dialog.getOrCreateInstance(dialogEl, {
+ backdrop: true
+ })
+ expect(dialog).toBeInstanceOf(Dialog)
+ expect(dialog2).toEqual(dialog)
+
+ expect(dialog2._config.backdrop).toEqual('static')
+ })
+ })
+})
--- /dev/null
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <link href="../../../dist/css/bootstrap.min.css" rel="stylesheet">
+ <title>Dialog</title>
+ <style>
+ #tall {
+ height: 1500px;
+ width: 100px;
+ }
+ </style>
+ </head>
+ <body>
+ <div class="container mt-3">
+ <h1>Dialog <small>Bootstrap Visual Test</small></h1>
+
+ <dialog class="dialog" id="myDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Dialog title</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <h4>Text in a dialog</h4>
+ <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
+
+ <h4>Popover in a dialog</h4>
+ <p>This <button type="button" class="btn btn-primary" data-bs-toggle="popover" data-bs-placement="left" title="Popover title" data-bs-content="And here's some amazing content. It's very engaging. Right?">button</button> should trigger a popover on click.</p>
+
+ <h4>Tooltips in a dialog</h4>
+ <p><a href="#" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top">This link</a> and <a href="#" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Tooltip on bottom">that link</a> should have tooltips on hover.</p>
+
+ <hr>
+
+ <h4>Overflowing text to show scroll behavior</h4>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-primary">Save changes</button>
+ </div>
+ </dialog>
+
+ <dialog class="dialog" id="staticBackdropDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Static Backdrop Dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This dialog has a static backdrop. Clicking outside won't close it.</p>
+ <p>Press Escape or click the close button to dismiss.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+ </dialog>
+
+ <dialog class="dialog" id="noKeyboardDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">No Keyboard Dismiss</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This dialog cannot be dismissed with the Escape key.</p>
+ <p>You must use the close button.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+ </dialog>
+
+ <dialog class="dialog" id="swapDialog1">
+ <div class="dialog-header">
+ <h1 class="dialog-title">First Dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>Click the button below to swap to the second dialog.</p>
+ <p>Notice how the backdrop stays visible during the swap.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-primary" data-bs-toggle="dialog" data-bs-target="#swapDialog2">Go to Second Dialog</button>
+ </div>
+ </dialog>
+
+ <dialog class="dialog" id="swapDialog2">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Second Dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>You're now in the second dialog!</p>
+ <p>You can swap back to the first dialog or close this one.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-primary" data-bs-toggle="dialog" data-bs-target="#swapDialog1">Back to First Dialog</button>
+ </div>
+ </dialog>
+
+ <dialog class="dialog" id="nonModalDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Non-Modal Dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This is a <strong>non-modal</strong> dialog opened with <code>show()</code> instead of <code>showModal()</code>.</p>
+ <ul>
+ <li>No backdrop</li>
+ <li>No focus trapping</li>
+ <li>Can interact with the page behind</li>
+ <li>Escape key still closes (if keyboard: true)</li>
+ </ul>
+ <p>Try clicking the buttons on the page behind this dialog!</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+ </dialog>
+
+ <div class="d-flex flex-column gap-3">
+ <div>
+ <button type="button" class="btn btn-primary btn-lg" data-bs-toggle="dialog" data-bs-target="#myDialog">
+ Launch demo dialog
+ </button>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="dialog" data-bs-target="#staticBackdropDialog" data-bs-backdrop="static">
+ Launch static backdrop dialog
+ </button>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="dialog" data-bs-target="#noKeyboardDialog" data-bs-keyboard="false">
+ Launch no-keyboard dialog
+ </button>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="dialog" data-bs-target="#swapDialog1">
+ Launch swap dialog demo
+ </button>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-info btn-lg" data-bs-toggle="dialog" data-bs-target="#nonModalDialog" data-bs-modal="false">
+ Launch non-modal dialog
+ </button>
+ <small class="text-muted">(uses <code>show()</code> instead of <code>showModal()</code>)</small>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-secondary btn-lg" id="tall-toggle">
+ Toggle tall <body> content
+ </button>
+ </div>
+
+ <div>
+ <button type="button" class="btn btn-secondary btn-lg" id="jsOpen">
+ Open via JavaScript
+ </button>
+ </div>
+ </div>
+
+ <br><br>
+
+ <div class="text-bg-dark p-2" id="tall" style="display: none;">
+ Tall body content to force the page to have a scrollbar.
+ </div>
+ </div>
+
+ <script src="../../../dist/js/bootstrap.bundle.js"></script>
+ <script>
+ /* global bootstrap: false */
+
+ document.querySelectorAll('[data-bs-toggle="popover"]').forEach(popoverEl => new bootstrap.Popover(popoverEl))
+
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(tooltipEl => new bootstrap.Tooltip(tooltipEl))
+
+ const tall = document.getElementById('tall')
+ document.getElementById('tall-toggle').addEventListener('click', () => {
+ tall.style.display = tall.style.display === 'none' ? 'block' : 'none'
+ })
+
+ // Test JavaScript API
+ document.getElementById('jsOpen').addEventListener('click', () => {
+ const dialogEl = document.getElementById('myDialog')
+ const dialog = bootstrap.Dialog.getOrCreateInstance(dialogEl)
+ dialog.show()
+ })
+
+ // Log events for debugging
+ document.querySelectorAll('.dialog').forEach(dialogEl => {
+ dialogEl.addEventListener('show.bs.dialog', () => console.log('show.bs.dialog', dialogEl.id))
+ dialogEl.addEventListener('shown.bs.dialog', () => console.log('shown.bs.dialog', dialogEl.id))
+ dialogEl.addEventListener('hide.bs.dialog', () => console.log('hide.bs.dialog', dialogEl.id))
+ dialogEl.addEventListener('hidden.bs.dialog', () => console.log('hidden.bs.dialog', dialogEl.id))
+ dialogEl.addEventListener('cancel.bs.dialog', () => console.log('cancel.bs.dialog', dialogEl.id))
+ })
+ </script>
+ </body>
+</html>
--- /dev/null
+@use "config" as *;
+@use "variables" as *;
+@use "mixins/border-radius" as *;
+@use "mixins/box-shadow" as *;
+@use "mixins/transition" as *;
+
+// Native <dialog> component
+// Uses the browser's native dialog element with showModal()/show()/close() APIs
+// Leverages native [open] attribute and ::backdrop pseudo-element
+
+// scss-docs-start dialog-css-vars
+$dialog-padding: 1rem !default;
+$dialog-width: 500px !default;
+$dialog-margin: 1.75rem !default;
+$dialog-color: var(--#{$prefix}fg-body) !default;
+$dialog-bg: var(--#{$prefix}bg-body) !default;
+$dialog-border-color: var(--#{$prefix}border-color-translucent) !default;
+$dialog-border-width: var(--#{$prefix}border-width) !default;
+$dialog-border-radius: var(--#{$prefix}border-radius-lg) !default;
+$dialog-box-shadow: var(--#{$prefix}box-shadow-lg) !default;
+$dialog-backdrop-bg: rgba(0, 0, 0, .5) !default;
+$dialog-header-padding: 1rem !default;
+$dialog-header-border-color: var(--#{$prefix}border-color) !default;
+$dialog-header-border-width: var(--#{$prefix}border-width) !default;
+$dialog-footer-padding: 1rem !default;
+$dialog-footer-border-color: var(--#{$prefix}border-color) !default;
+$dialog-footer-border-width: var(--#{$prefix}border-width) !default;
+$dialog-footer-gap: .5rem !default;
+// scss-docs-end dialog-css-vars
+
+@layer components {
+ // Prevent body scroll when dialog is open
+ .dialog-open {
+ overflow: hidden;
+ scrollbar-gutter: stable;
+ }
+
+ .dialog {
+ --#{$prefix}dialog-padding: #{$dialog-padding};
+ --#{$prefix}dialog-width: #{$dialog-width};
+ --#{$prefix}dialog-margin: #{$dialog-margin};
+ --#{$prefix}dialog-color: #{$dialog-color};
+ --#{$prefix}dialog-bg: #{$dialog-bg};
+ --#{$prefix}dialog-border-color: #{$dialog-border-color};
+ --#{$prefix}dialog-border-width: #{$dialog-border-width};
+ --#{$prefix}dialog-border-radius: #{$dialog-border-radius};
+ --#{$prefix}dialog-box-shadow: #{$dialog-box-shadow};
+ --#{$prefix}dialog-backdrop-bg: #{$dialog-backdrop-bg};
+ --#{$prefix}dialog-header-padding: #{$dialog-header-padding};
+ --#{$prefix}dialog-header-border-color: #{$dialog-header-border-color};
+ --#{$prefix}dialog-header-border-width: #{$dialog-header-border-width};
+ --#{$prefix}dialog-footer-padding: #{$dialog-footer-padding};
+ --#{$prefix}dialog-footer-border-color: #{$dialog-footer-border-color};
+ --#{$prefix}dialog-footer-border-width: #{$dialog-footer-border-width};
+ --#{$prefix}dialog-footer-gap: #{$dialog-footer-gap};
+
+ // Reset native dialog styles
+ max-width: var(--#{$prefix}dialog-width);
+ max-height: calc(100% - var(--#{$prefix}dialog-margin) * 2);
+ padding: 0;
+ margin: auto;
+ color: var(--#{$prefix}dialog-color);
+ background-color: var(--#{$prefix}dialog-bg);
+ background-clip: padding-box;
+ border: var(--#{$prefix}dialog-border-width) solid var(--#{$prefix}dialog-border-color);
+ @include border-radius(var(--#{$prefix}dialog-border-radius));
+ @include box-shadow(var(--#{$prefix}dialog-box-shadow));
+
+ // Native backdrop styling via ::backdrop pseudo-element
+ &::backdrop {
+ background-color: var(--#{$prefix}dialog-backdrop-bg);
+ }
+
+ // Animation support using native [open] attribute
+ &.fade {
+ opacity: 0;
+ @include transition(opacity .15s linear);
+
+ &::backdrop {
+ opacity: 0;
+ @include transition(opacity .15s linear);
+ }
+
+ &[open] {
+ opacity: 1;
+
+ &::backdrop {
+ opacity: 1;
+ }
+ }
+ }
+
+ // Static backdrop "bounce" animation (modal dialogs only)
+ &.dialog-static {
+ transform: scale(1.02);
+ }
+
+ // Non-modal dialog positioning
+ // show() doesn't use the top layer, so we need explicit positioning and z-index
+ &.dialog-nonmodal {
+ position: fixed;
+ inset-block-start: 50%;
+ inset-inline-start: 50%;
+ z-index: $zindex-dialog;
+ margin-inline: 0;
+ transform: translate(-50%, -50%);
+ }
+
+ // Overflow dialog - scrollable viewport container with dialog box inside
+ &.dialog-overflow {
+ // Make dialog element the full-viewport scrollable container
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ max-width: 100%;
+ height: 100%;
+ max-height: 100%;
+ padding: var(--#{$prefix}dialog-margin);
+ margin: 0;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+ background: transparent;
+ border: 0;
+ box-shadow: none;
+
+ // The visual dialog box is a child wrapper
+ > .dialog-box {
+ max-width: var(--#{$prefix}dialog-width);
+ margin-block-end: var(--#{$prefix}dialog-margin);
+ margin-inline: auto;
+ color: var(--#{$prefix}dialog-color);
+ background-color: var(--#{$prefix}dialog-bg);
+ background-clip: padding-box;
+ border: var(--#{$prefix}dialog-border-width) solid var(--#{$prefix}dialog-border-color);
+ @include border-radius(var(--#{$prefix}dialog-border-radius));
+ @include box-shadow(var(--#{$prefix}dialog-box-shadow));
+ }
+ }
+
+ // Scrollable dialog body (header/footer stay fixed)
+ &.dialog-scrollable[open] {
+ display: flex;
+ flex-direction: column;
+ max-height: calc(100% - var(--#{$prefix}dialog-margin) * 2);
+
+ .dialog-body {
+ overflow-y: auto;
+ }
+ }
+ }
+
+ // Dialog header
+ .dialog-header {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ padding: var(--#{$prefix}dialog-header-padding);
+ border-block-end: var(--#{$prefix}dialog-header-border-width) solid var(--#{$prefix}dialog-header-border-color);
+
+ .btn-close {
+ margin-inline-start: auto;
+ }
+ }
+
+ // Dialog title
+ .dialog-title {
+ margin-bottom: 0;
+ font-size: var(--#{$prefix}font-size-md);
+ line-height: 1.5;
+ }
+
+ // Dialog body
+ .dialog-body {
+ position: relative;
+ flex: 1 1 auto;
+ padding: var(--#{$prefix}dialog-padding);
+ overflow-y: auto;
+ }
+
+ // Dialog footer
+ .dialog-footer {
+ display: flex;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+ gap: var(--#{$prefix}dialog-footer-gap);
+ align-items: center;
+ justify-content: flex-end;
+ padding: var(--#{$prefix}dialog-footer-padding);
+ border-block-start: var(--#{$prefix}dialog-footer-border-width) solid var(--#{$prefix}dialog-footer-border-color);
+ }
+}
$zindex-fixed: 1030 !default;
$zindex-offcanvas-backdrop: 1040 !default;
$zindex-offcanvas: 1045 !default;
-$zindex-modal-backdrop: 1050 !default;
-$zindex-modal: 1055 !default;
+$zindex-dialog: 1055 !default;
$zindex-popover: 1070 !default;
$zindex-tooltip: 1080 !default;
$zindex-toast: 1090 !default;
@forward "breadcrumb";
@forward "card";
@forward "carousel";
+@forward "dialog";
@forward "dropdown";
@forward "list-group";
@forward "modal";
- title: Carousel
- title: Close button
- title: Collapse
+ - title: Dialog
- title: Dropdowns
- title: List group
- title: Modal
--- /dev/null
+---
+title: Dialog
+description: A modern component built on the native `<dialog>` element with built-in accessibility and backdrop support that replaces the old Modal component.
+toc: true
+# aliases:
+ # - "/docs/[[config:docs_version]]/components/modal/"
+---
+
+## How it works
+
+The Dialog component leverages the browser's native `<dialog>` element, providing built-in accessibility features, focus management, and backdrop handling without the complexity of custom implementations.
+
+Key features of the native dialog:
+
+- **Native modal behavior** via `showModal()` with automatic focus trapping
+- **Built-in backdrop** using the `::backdrop` pseudo-element
+- **Escape key handling** closes the dialog by default
+- **Accessibility** with proper focus management and ARIA attributes
+- **Top layer rendering** ensures the dialog appears above all other content
+
+Native `<dialog>` elements support two methods: `show()` opens the dialog inline without a backdrop or focus trapping, while `showModal()` opens it as a true modal in the browser's top layer with a backdrop, focus trapping, and Escape key handling. Bootstrap's Dialog component uses `showModal()` to provide the expected modal experience.
+
+<Callout name="info-prefersreducedmotion" />
+
+## Example
+
+Toggle a dialog by clicking the button below. The dialog uses the native `showModal()` API for true modal behavior.
+
+<dialog class="dialog" id="exampleDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Dialog title</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This is a native dialog element. It uses the browser's built-in modal behavior for accessibility and focus management.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary">Save changes</button>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialog">
+ Open dialog
+</button>`} />
+
+The markup for a dialog is straightforward:
+
+```html
+<dialog class="dialog" id="exampleDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Dialog title</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>Dialog body content goes here.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary">Save changes</button>
+ </div>
+</dialog>
+
+<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialog">
+ Open dialog
+</button>
+```
+
+## Static backdrop
+
+When `backdrop` is set to `static`, the dialog will not close when clicking outside of it. Click the button below to try it.
+
+<dialog class="dialog" id="staticBackdropDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Static backdrop</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>I will not close if you click outside of me. Use the close button or press Escape.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#staticBackdropDialog" data-bs-backdrop="static">
+ Open static backdrop dialog
+</button>`} />
+
+## Scrolling long content
+
+When dialogs have content that exceeds the viewport height, the entire dialog scrolls within the viewport. The header, body, and footer all scroll together.
+
+<dialog class="dialog" id="scrollingLongDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Scrolling dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This is some placeholder content to show the scrolling behavior for dialogs. When the content exceeds the viewport height, you can scroll the entire dialog within the window—the header, body, and footer all move together.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>This content should appear at the bottom after you scroll.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary">Save changes</button>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#scrollingLongDialog">
+ Launch scrolling dialog
+</button>`} />
+
+You can also create a scrollable dialog that scrolls the dialog body while keeping the header and footer fixed. Add `.dialog-scrollable` to the `.dialog` element.
+
+<dialog class="dialog dialog-scrollable" id="scrollableBodyDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Scrollable body</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This is some placeholder content to show the scrolling behavior for dialogs. We use repeated line breaks to demonstrate how content can exceed the dialog's inner height, showing scrolling within the body while the header and footer remain fixed.</p>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+ <p>This content should appear at the bottom after you scroll.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary">Save changes</button>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#scrollableBodyDialog">
+ Launch scrollable body dialog
+</button>`} />
+
+```html
+<!-- Scrollable body dialog -->
+<dialog class="dialog dialog-scrollable" id="scrollableBodyDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Scrollable body</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <!-- Long content here -->
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+</dialog>
+```
+
+For a dialog that extends beyond the viewport and scrolls as a whole, add `.dialog-overflow` and wrap the content in a `.dialog-box` element. The `<dialog>` becomes a full-viewport scrollable container, and the `.dialog-box` is the visual dialog that scrolls up and down.
+
+<dialog class="dialog dialog-overflow" id="overflowDialog">
+ <div class="dialog-box">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Overflow dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This dialog extends beyond the viewport height. Scroll to see more content. Notice how the entire dialog—including header and footer—shifts up and down as you scroll.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.</p>
+ <p>Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.</p>
+ <p>Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus auctor fringilla.</p>
+ <p>You've reached the bottom of the overflow dialog!</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary">Save changes</button>
+ </div>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#overflowDialog">
+ Launch overflow dialog
+</button>`} />
+
+```html
+<!-- Overflow dialog (entire dialog scrolls within viewport) -->
+<dialog class="dialog dialog-overflow" id="overflowDialog">
+ <div class="dialog-box">
+ <div class="dialog-header">...</div>
+ <div class="dialog-body">
+ <!-- Very long content here -->
+ </div>
+ <div class="dialog-footer">...</div>
+ </div>
+</dialog>
+```
+
+## Swapping dialogs
+
+When a toggle trigger is inside an open dialog, clicking it will **swap** dialogs—opening the new one before closing the current. This ensures the backdrop stays visible throughout the transition with no flash.
+
+<dialog class="dialog" id="swapDialog1">
+ <div class="dialog-header">
+ <h1 class="dialog-title">First dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>Click below to swap to a second dialog. Notice the backdrop stays visible—no flash!</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#swapDialog2">Go to second dialog</button>
+ </div>
+</dialog>
+
+<dialog class="dialog" id="swapDialog2">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Second dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This is the second dialog. You can swap back to the first, or close this one entirely.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ <button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#swapDialog1">Back to first dialog</button>
+ </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#swapDialog1">
+ Open first dialog
+</button>`} />
+
+The swap behavior is automatic when a `data-bs-toggle="dialog"` trigger is inside an already-open dialog:
+
+```html
+<!-- First dialog -->
+<dialog class="dialog" id="dialog1">
+ <div class="dialog-body">
+ <p>Click below to swap to dialog 2.</p>
+ </div>
+ <div class="dialog-footer">
+ <!-- This trigger is inside dialog1, so clicking it will swap -->
+ <button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#dialog2">
+ Go to dialog 2
+ </button>
+ </div>
+</dialog>
+
+<!-- Second dialog -->
+<dialog class="dialog" id="dialog2">
+ <div class="dialog-body">
+ <p>You're now in dialog 2.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#dialog1">
+ Back to dialog 1
+ </button>
+ </div>
+</dialog>
+```
+
+## Non-modal dialogs
+
+By default, dialogs open as modals using the native `showModal()` method. You can also open dialogs as non-modal using `show()` by setting `modal` to `false`. Non-modal dialogs:
+
+- Have no backdrop
+- Don't trap focus
+- Don't block interaction with the rest of the page
+- Don't render in the browser's top layer
+- Still respond to Escape key (if `keyboard: true`)
+
+<dialog class="dialog" id="nonModalDialog">
+ <div class="dialog-header">
+ <h1 class="dialog-title">Non-modal dialog</h1>
+ <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+ </div>
+ <div class="dialog-body">
+ <p>This dialog doesn't block the page. You can still interact with content behind it.</p>
+ </div>
+ <div class="dialog-footer">
+ <button type="button" class="btn btn-solid theme-secondary" data-bs-dismiss="dialog">Close</button>
+ </div>
+</dialog>
+
+ <Example code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#nonModalDialog" data-bs-modal="false">
+ Open non-modal dialog
+ </button>`} />
+
+```js
+const dialog = new bootstrap.Dialog('#myDialog', { modal: false })
+dialog.show()
+```
+
+## JavaScript behavior
+
+### Via data attributes
+
+Toggle a dialog without writing JavaScript. Set `data-bs-toggle="dialog"` on a controller element, like a button, along with a `data-bs-target="#foo"` to target a specific dialog to toggle.
+
+```html
+<button type="button" data-bs-toggle="dialog" data-bs-target="#myDialog">
+ Launch dialog
+</button>
+```
+
+#### Dismiss
+
+Dismissal can be achieved with the `data-bs-dismiss` attribute on a button **within the dialog**:
+
+```html
+<button type="button" data-bs-dismiss="dialog">Close</button>
+```
+
+### Via JavaScript
+
+Create a dialog with a single line of JavaScript:
+
+```js
+const myDialog = new bootstrap.Dialog('#myDialog')
+```
+
+### Options
+
+Options can be passed via data attributes or JavaScript. For data attributes, append the option name to `data-bs-`, as in `data-bs-backdrop="static"`.
+
+| Name | Type | Default | Description |
+| --- | --- | --- | --- |
+| `backdrop` | boolean or `'static'` | `true` | For modal dialogs, clicking the backdrop dismisses the dialog. Specify `static` for a backdrop which doesn't close the dialog when clicked. Has no effect on non-modal dialogs. |
+| `keyboard` | boolean | `true` | Closes the dialog when escape key is pressed. |
+| `modal` | boolean | `true` | When `true`, opens the dialog as a modal using `showModal()` with backdrop, focus trapping, and top layer rendering. When `false`, opens as a non-modal dialog using `show()` without backdrop or focus trapping. |
+
+### Methods
+
+#### Passing options
+
+Activates your content as a dialog. Accepts an optional options object.
+
+```js
+const myDialog = new bootstrap.Dialog('#myDialog', {
+ keyboard: false
+})
+```
+
+| Method | Description |
+| --- | --- |
+| `show` | Opens the dialog. **Returns to the caller before the dialog has actually been shown** (i.e. before the `shown.bs.dialog` event occurs). |
+| `hide` | Hides the dialog. **Returns to the caller before the dialog has actually been hidden** (i.e. before the `hidden.bs.dialog` event occurs). |
+| `toggle` | Toggles the dialog. **Returns to the caller before the dialog has actually been shown or hidden** (i.e. before the `shown.bs.dialog` or `hidden.bs.dialog` event occurs). |
+| `handleUpdate` | Provided for API consistency with Modal. Native dialogs handle their own positioning. |
+| `dispose` | Destroys an element's dialog. |
+| `getInstance` | Static method which allows you to get the dialog instance associated with a DOM element. |
+| `getOrCreateInstance` | Static method which allows you to get the dialog instance associated with a DOM element, or create a new one in case it wasn't initialized. |
+
+### Events
+
+Bootstrap's dialog class exposes a few events for hooking into dialog functionality.
+
+| Event | Description |
+| --- | --- |
+| `show.bs.dialog` | Fires immediately when the `show` instance method is called. |
+| `shown.bs.dialog` | Fired when the dialog has been made visible to the user (will wait for CSS transitions to complete). |
+| `hide.bs.dialog` | Fires immediately when the `hide` instance method is called. |
+| `hidden.bs.dialog` | Fired when the dialog has finished being hidden from the user (will wait for CSS transitions to complete). |
+| `hidePrevented.bs.dialog` | Fired when the dialog is shown, its backdrop is `static`, and a click outside the dialog or an escape key press is performed (with `keyboard` set to `false`). |
+| `cancel.bs.dialog` | Fired when the user presses Escape and the dialog is about to close. |
+
+```js
+const myDialog = document.getElementById('myDialog')
+myDialog.addEventListener('hidden.bs.dialog', event => {
+ // do something...
+})
+```
Below is a _static_ modal example (meaning its `position` and `display` have been overridden). Included are the modal header, modal body (required for `padding`), and modal footer (optional). We ask that you include modal headers with dismiss actions whenever possible, or provide another explicit dismiss action.
-<div class="bd-example bg-body-tertiary">
- <div class="modal position-static d-block" tabindex="-1">
+<Example class="bg-1" code={`<div class="modal position-static d-block max-w-100" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
</div>
</div>
</div>
- </div>
-</div>
-
-```html
-<div class="modal" tabindex="-1">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title">Modal title</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <p>Modal body text goes here.</p>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
- <button type="button" class="btn btn-primary">Save changes</button>
+ </div>`} customMarkup={`<div class="modal" tabindex="-1">
+ <div class="modal-dialog">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h5 class="modal-title">Modal title</h5>
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+ </div>
+ <div class="modal-body">
+ <p>Modal body text goes here.</p>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
+ <button type="button" class="btn btn-primary">Save changes</button>
+ </div>
</div>
</div>
- </div>
-</div>
-```
+ </div>`} />
<Callout>
In the above static example, we use `<h5>`, to avoid issues with the heading hierarchy in the documentation page. Structurally, however, a modal dialog represents its own separate document/context, so the `.modal-title` should ideally be an `<h1>`. If necessary, you can use the [font size utilities]([[docsref:/utilities/font-size]]) to control the heading’s appearance. All the following live examples use this approach.