]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Replace Modal with new Dialog component (#41917)
authorMark Otto <markd.otto@gmail.com>
Tue, 16 Dec 2025 19:19:06 +0000 (11:19 -0800)
committerMark Otto <markdotto@gmail.com>
Fri, 9 Jan 2026 04:08:32 +0000 (20:08 -0800)
* Add Dialog component using native HTML dialog element

New component that leverages the native HTML <dialog> element for modals
and non-modal dialogs with built-in backdrop and accessibility support.

Features:
- Modal dialogs using showModal() with automatic backdrop
- Non-modal dialogs using show() for persistent UI elements
- Static backdrop option (prevents close on outside click)
- Keyboard support (Escape to close, focus trapping for modals)
- Smooth open/close animations via CSS
- Events: show, shown, hide, hidden, hidePrevented
- Data API for toggling with data-bs-toggle="dialog"

JavaScript:
- js/src/dialog.js - Main component class
- js/tests/unit/dialog.spec.js - Unit tests
- js/tests/visual/dialog.html - Visual test page

SCSS:
- scss/_dialog.scss - Component styles

Docs:
- Add dialog component documentation
- Update modal docs with dialog references

* Modal examples now Dialog examples, needs improvement

* Remove all of Modal

* real words

* Fix layout while I'm here

* Lint Markdown

* New dialog size options

31 files changed:
js/dist/modal.js [deleted file]
js/dist/modal.js.map [deleted file]
js/index.esm.js
js/index.umd.js
js/src/dialog.js [new file with mode: 0644]
js/src/modal.js [deleted file]
js/tests/unit/dialog.spec.js [new file with mode: 0644]
js/tests/unit/modal.spec.js [deleted file]
js/tests/visual/dialog.html [new file with mode: 0644]
js/tests/visual/modal.html [deleted file]
scss/_dialog.scss [new file with mode: 0644]
scss/_modal.scss [deleted file]
scss/_variables.scss
scss/bootstrap.scss
site/data/examples.yml
site/data/sidebar.yml
site/src/assets/examples/cheatsheet-rtl/index.astro
site/src/assets/examples/cheatsheet/index.astro
site/src/assets/examples/dialogs/dialogs.css [new file with mode: 0644]
site/src/assets/examples/dialogs/index.astro [new file with mode: 0644]
site/src/assets/examples/modals/index.astro [deleted file]
site/src/assets/examples/modals/modals.css [deleted file]
site/src/assets/partials/snippets.js
site/src/components/home/Plugins.astro
site/src/content/docs/components/dialog.mdx [new file with mode: 0644]
site/src/content/docs/components/modal.mdx [deleted file]
site/src/content/docs/components/offcanvas.mdx
site/src/content/docs/components/popovers.mdx
site/src/content/docs/customize/optimize.mdx
site/src/content/docs/getting-started/javascript.mdx
site/src/content/docs/utilities/z-index.mdx

diff --git a/js/dist/modal.js b/js/dist/modal.js
deleted file mode 100644 (file)
index 21fe4e0..0000000
+++ /dev/null
@@ -1,300 +0,0 @@
-/*!
-  * Bootstrap modal.js v5.3.8 (https://getbootstrap.com/)
-  * Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
-  * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
-  */
-(function (global, factory) {
-  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('./base-component.js'), require('./dom/event-handler.js'), require('./dom/selector-engine.js'), require('./util/backdrop.js'), require('./util/component-functions.js'), require('./util/focustrap.js'), require('./util/index.js'), require('./util/scrollbar.js')) :
-  typeof define === 'function' && define.amd ? define(['./base-component', './dom/event-handler', './dom/selector-engine', './util/backdrop', './util/component-functions', './util/focustrap', './util/index', './util/scrollbar'], factory) :
-  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Modal = factory(global.BaseComponent, global.EventHandler, global.SelectorEngine, global.Backdrop, global.ComponentFunctions, global.Focustrap, global.Index, global.Scrollbar));
-})(this, (function (BaseComponent, EventHandler, SelectorEngine, Backdrop, componentFunctions_js, FocusTrap, index_js, ScrollBarHelper) { 'use strict';
-
-  /**
-   * --------------------------------------------------------------------------
-   * Bootstrap modal.js
-   * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
-   * --------------------------------------------------------------------------
-   */
-
-
-  /**
-   * Constants
-   */
-
-  const NAME = 'modal';
-  const DATA_KEY = 'bs.modal';
-  const EVENT_KEY = `.${DATA_KEY}`;
-  const DATA_API_KEY = '.data-api';
-  const ESCAPE_KEY = 'Escape';
-  const EVENT_HIDE = `hide${EVENT_KEY}`;
-  const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`;
-  const EVENT_HIDDEN = `hidden${EVENT_KEY}`;
-  const EVENT_SHOW = `show${EVENT_KEY}`;
-  const EVENT_SHOWN = `shown${EVENT_KEY}`;
-  const EVENT_RESIZE = `resize${EVENT_KEY}`;
-  const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`;
-  const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`;
-  const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`;
-  const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`;
-  const CLASS_NAME_OPEN = 'modal-open';
-  const CLASS_NAME_FADE = 'fade';
-  const CLASS_NAME_SHOW = 'show';
-  const CLASS_NAME_STATIC = 'modal-static';
-  const OPEN_SELECTOR = '.modal.show';
-  const SELECTOR_DIALOG = '.modal-dialog';
-  const SELECTOR_MODAL_BODY = '.modal-body';
-  const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]';
-  const Default = {
-    backdrop: true,
-    focus: true,
-    keyboard: true
-  };
-  const DefaultType = {
-    backdrop: '(boolean|string)',
-    focus: 'boolean',
-    keyboard: 'boolean'
-  };
-
-  /**
-   * Class definition
-   */
-
-  class Modal extends BaseComponent {
-    constructor(element, config) {
-      super(element, config);
-      this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element);
-      this._backdrop = this._initializeBackDrop();
-      this._focustrap = this._initializeFocusTrap();
-      this._isShown = false;
-      this._isTransitioning = false;
-      this._scrollBar = new ScrollBarHelper();
-      this._addEventListeners();
-    }
-
-    // Getters
-    static get Default() {
-      return Default;
-    }
-    static get DefaultType() {
-      return DefaultType;
-    }
-    static get NAME() {
-      return NAME;
-    }
-
-    // Public
-    toggle(relatedTarget) {
-      return this._isShown ? this.hide() : this.show(relatedTarget);
-    }
-    show(relatedTarget) {
-      if (this._isShown || this._isTransitioning) {
-        return;
-      }
-      const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
-        relatedTarget
-      });
-      if (showEvent.defaultPrevented) {
-        return;
-      }
-      this._isShown = true;
-      this._isTransitioning = true;
-      this._scrollBar.hide();
-      document.body.classList.add(CLASS_NAME_OPEN);
-      this._adjustDialog();
-      this._backdrop.show(() => this._showElement(relatedTarget));
-    }
-    hide() {
-      if (!this._isShown || this._isTransitioning) {
-        return;
-      }
-      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE);
-      if (hideEvent.defaultPrevented) {
-        return;
-      }
-      this._isShown = false;
-      this._isTransitioning = true;
-      this._focustrap.deactivate();
-      this._element.classList.remove(CLASS_NAME_SHOW);
-      this._queueCallback(() => this._hideModal(), this._element, this._isAnimated());
-    }
-    dispose() {
-      EventHandler.off(window, EVENT_KEY);
-      EventHandler.off(this._dialog, EVENT_KEY);
-      this._backdrop.dispose();
-      this._focustrap.deactivate();
-      super.dispose();
-    }
-    handleUpdate() {
-      this._adjustDialog();
-    }
-
-    // Private
-    _initializeBackDrop() {
-      return new Backdrop({
-        isVisible: Boolean(this._config.backdrop),
-        // 'static' option will be translated to true, and booleans will keep their value,
-        isAnimated: this._isAnimated()
-      });
-    }
-    _initializeFocusTrap() {
-      return new FocusTrap({
-        trapElement: this._element
-      });
-    }
-    _showElement(relatedTarget) {
-      // try to append dynamic modal
-      if (!document.body.contains(this._element)) {
-        document.body.append(this._element);
-      }
-      this._element.style.display = 'block';
-      this._element.removeAttribute('aria-hidden');
-      this._element.setAttribute('aria-modal', true);
-      this._element.setAttribute('role', 'dialog');
-      this._element.scrollTop = 0;
-      const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog);
-      if (modalBody) {
-        modalBody.scrollTop = 0;
-      }
-      index_js.reflow(this._element);
-      this._element.classList.add(CLASS_NAME_SHOW);
-      const transitionComplete = () => {
-        if (this._config.focus) {
-          this._focustrap.activate();
-        }
-        this._isTransitioning = false;
-        EventHandler.trigger(this._element, EVENT_SHOWN, {
-          relatedTarget
-        });
-      };
-      this._queueCallback(transitionComplete, this._dialog, this._isAnimated());
-    }
-    _addEventListeners() {
-      EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
-        if (event.key !== ESCAPE_KEY) {
-          return;
-        }
-        if (this._config.keyboard) {
-          this.hide();
-          return;
-        }
-        this._triggerBackdropTransition();
-      });
-      EventHandler.on(window, EVENT_RESIZE, () => {
-        if (this._isShown && !this._isTransitioning) {
-          this._adjustDialog();
-        }
-      });
-      EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {
-        // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
-        EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {
-          if (this._element !== event.target || this._element !== event2.target) {
-            return;
-          }
-          if (this._config.backdrop === 'static') {
-            this._triggerBackdropTransition();
-            return;
-          }
-          if (this._config.backdrop) {
-            this.hide();
-          }
-        });
-      });
-    }
-    _hideModal() {
-      this._element.style.display = 'none';
-      this._element.setAttribute('aria-hidden', true);
-      this._element.removeAttribute('aria-modal');
-      this._element.removeAttribute('role');
-      this._isTransitioning = false;
-      this._backdrop.hide(() => {
-        document.body.classList.remove(CLASS_NAME_OPEN);
-        this._resetAdjustments();
-        this._scrollBar.reset();
-        EventHandler.trigger(this._element, EVENT_HIDDEN);
-      });
-    }
-    _isAnimated() {
-      return this._element.classList.contains(CLASS_NAME_FADE);
-    }
-    _triggerBackdropTransition() {
-      const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED);
-      if (hideEvent.defaultPrevented) {
-        return;
-      }
-      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
-      const initialOverflowY = this._element.style.overflowY;
-      // return if the following background transition hasn't yet completed
-      if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
-        return;
-      }
-      if (!isModalOverflowing) {
-        this._element.style.overflowY = 'hidden';
-      }
-      this._element.classList.add(CLASS_NAME_STATIC);
-      this._queueCallback(() => {
-        this._element.classList.remove(CLASS_NAME_STATIC);
-        this._queueCallback(() => {
-          this._element.style.overflowY = initialOverflowY;
-        }, this._dialog);
-      }, this._dialog);
-      this._element.focus();
-    }
-
-    /**
-     * The following methods are used to handle overflowing modals
-     */
-
-    _adjustDialog() {
-      const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight;
-      const scrollbarWidth = this._scrollBar.getWidth();
-      const isBodyOverflowing = scrollbarWidth > 0;
-      if (isBodyOverflowing && !isModalOverflowing) {
-        const property = index_js.isRTL() ? 'paddingLeft' : 'paddingRight';
-        this._element.style[property] = `${scrollbarWidth}px`;
-      }
-      if (!isBodyOverflowing && isModalOverflowing) {
-        const property = index_js.isRTL() ? 'paddingRight' : 'paddingLeft';
-        this._element.style[property] = `${scrollbarWidth}px`;
-      }
-    }
-    _resetAdjustments() {
-      this._element.style.paddingLeft = '';
-      this._element.style.paddingRight = '';
-    }
-  }
-
-  /**
-   * 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) {
-        // only register focus restorer if modal will actually get shown
-        return;
-      }
-      EventHandler.one(target, EVENT_HIDDEN, () => {
-        if (index_js.isVisible(this)) {
-          this.focus();
-        }
-      });
-    });
-
-    // avoid conflict when clicking modal toggler while another one is open
-    const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR);
-    if (alreadyOpen) {
-      Modal.getInstance(alreadyOpen).hide();
-    }
-    const data = Modal.getOrCreateInstance(target);
-    data.toggle(this);
-  });
-  componentFunctions_js.enableDismissTrigger(Modal);
-
-  return Modal;
-
-}));
-//# sourceMappingURL=modal.js.map
diff --git a/js/dist/modal.js.map b/js/dist/modal.js.map
deleted file mode 100644 (file)
index 227a507..0000000
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"modal.js","sources":["../src/modal.js"],"sourcesContent":["/**\n * --------------------------------------------------------------------------\n * Bootstrap modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport BaseComponent from './base-component.js'\nimport EventHandler from './dom/event-handler.js'\nimport SelectorEngine from './dom/selector-engine.js'\nimport Backdrop from './util/backdrop.js'\nimport { enableDismissTrigger } from './util/component-functions.js'\nimport FocusTrap from './util/focustrap.js'\nimport {\n  isRTL, isVisible, reflow\n} from './util/index.js'\nimport ScrollBarHelper from './util/scrollbar.js'\n\n/**\n * Constants\n */\n\nconst NAME = 'modal'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst ESCAPE_KEY = 'Escape'\n\nconst EVENT_HIDE = `hide${EVENT_KEY}`\nconst EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`\nconst EVENT_HIDDEN = `hidden${EVENT_KEY}`\nconst EVENT_SHOW = `show${EVENT_KEY}`\nconst EVENT_SHOWN = `shown${EVENT_KEY}`\nconst EVENT_RESIZE = `resize${EVENT_KEY}`\nconst EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`\nconst EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`\nconst EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`\nconst EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`\n\nconst CLASS_NAME_OPEN = 'modal-open'\nconst CLASS_NAME_FADE = 'fade'\nconst CLASS_NAME_SHOW = 'show'\nconst CLASS_NAME_STATIC = 'modal-static'\n\nconst OPEN_SELECTOR = '.modal.show'\nconst SELECTOR_DIALOG = '.modal-dialog'\nconst SELECTOR_MODAL_BODY = '.modal-body'\nconst SELECTOR_DATA_TOGGLE = '[data-bs-toggle=\"modal\"]'\n\nconst Default = {\n  backdrop: true,\n  focus: true,\n  keyboard: true\n}\n\nconst DefaultType = {\n  backdrop: '(boolean|string)',\n  focus: 'boolean',\n  keyboard: 'boolean'\n}\n\n/**\n * Class definition\n */\n\nclass Modal extends BaseComponent {\n  constructor(element, config) {\n    super(element, config)\n\n    this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)\n    this._backdrop = this._initializeBackDrop()\n    this._focustrap = this._initializeFocusTrap()\n    this._isShown = false\n    this._isTransitioning = false\n    this._scrollBar = new ScrollBarHelper()\n\n    this._addEventListeners()\n  }\n\n  // Getters\n  static get Default() {\n    return Default\n  }\n\n  static get DefaultType() {\n    return DefaultType\n  }\n\n  static get NAME() {\n    return NAME\n  }\n\n  // Public\n  toggle(relatedTarget) {\n    return this._isShown ? this.hide() : this.show(relatedTarget)\n  }\n\n  show(relatedTarget) {\n    if (this._isShown || this._isTransitioning) {\n      return\n    }\n\n    const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {\n      relatedTarget\n    })\n\n    if (showEvent.defaultPrevented) {\n      return\n    }\n\n    this._isShown = true\n    this._isTransitioning = true\n\n    this._scrollBar.hide()\n\n    document.body.classList.add(CLASS_NAME_OPEN)\n\n    this._adjustDialog()\n\n    this._backdrop.show(() => this._showElement(relatedTarget))\n  }\n\n  hide() {\n    if (!this._isShown || this._isTransitioning) {\n      return\n    }\n\n    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)\n\n    if (hideEvent.defaultPrevented) {\n      return\n    }\n\n    this._isShown = false\n    this._isTransitioning = true\n    this._focustrap.deactivate()\n\n    this._element.classList.remove(CLASS_NAME_SHOW)\n\n    this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())\n  }\n\n  dispose() {\n    EventHandler.off(window, EVENT_KEY)\n    EventHandler.off(this._dialog, EVENT_KEY)\n\n    this._backdrop.dispose()\n    this._focustrap.deactivate()\n\n    super.dispose()\n  }\n\n  handleUpdate() {\n    this._adjustDialog()\n  }\n\n  // Private\n  _initializeBackDrop() {\n    return new Backdrop({\n      isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,\n      isAnimated: this._isAnimated()\n    })\n  }\n\n  _initializeFocusTrap() {\n    return new FocusTrap({\n      trapElement: this._element\n    })\n  }\n\n  _showElement(relatedTarget) {\n    // try to append dynamic modal\n    if (!document.body.contains(this._element)) {\n      document.body.append(this._element)\n    }\n\n    this._element.style.display = 'block'\n    this._element.removeAttribute('aria-hidden')\n    this._element.setAttribute('aria-modal', true)\n    this._element.setAttribute('role', 'dialog')\n    this._element.scrollTop = 0\n\n    const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)\n    if (modalBody) {\n      modalBody.scrollTop = 0\n    }\n\n    reflow(this._element)\n\n    this._element.classList.add(CLASS_NAME_SHOW)\n\n    const transitionComplete = () => {\n      if (this._config.focus) {\n        this._focustrap.activate()\n      }\n\n      this._isTransitioning = false\n      EventHandler.trigger(this._element, EVENT_SHOWN, {\n        relatedTarget\n      })\n    }\n\n    this._queueCallback(transitionComplete, this._dialog, this._isAnimated())\n  }\n\n  _addEventListeners() {\n    EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {\n      if (event.key !== ESCAPE_KEY) {\n        return\n      }\n\n      if (this._config.keyboard) {\n        this.hide()\n        return\n      }\n\n      this._triggerBackdropTransition()\n    })\n\n    EventHandler.on(window, EVENT_RESIZE, () => {\n      if (this._isShown && !this._isTransitioning) {\n        this._adjustDialog()\n      }\n    })\n\n    EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {\n      // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks\n      EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {\n        if (this._element !== event.target || this._element !== event2.target) {\n          return\n        }\n\n        if (this._config.backdrop === 'static') {\n          this._triggerBackdropTransition()\n          return\n        }\n\n        if (this._config.backdrop) {\n          this.hide()\n        }\n      })\n    })\n  }\n\n  _hideModal() {\n    this._element.style.display = 'none'\n    this._element.setAttribute('aria-hidden', true)\n    this._element.removeAttribute('aria-modal')\n    this._element.removeAttribute('role')\n    this._isTransitioning = false\n\n    this._backdrop.hide(() => {\n      document.body.classList.remove(CLASS_NAME_OPEN)\n      this._resetAdjustments()\n      this._scrollBar.reset()\n      EventHandler.trigger(this._element, EVENT_HIDDEN)\n    })\n  }\n\n  _isAnimated() {\n    return this._element.classList.contains(CLASS_NAME_FADE)\n  }\n\n  _triggerBackdropTransition() {\n    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)\n    if (hideEvent.defaultPrevented) {\n      return\n    }\n\n    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n    const initialOverflowY = this._element.style.overflowY\n    // return if the following background transition hasn't yet completed\n    if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {\n      return\n    }\n\n    if (!isModalOverflowing) {\n      this._element.style.overflowY = 'hidden'\n    }\n\n    this._element.classList.add(CLASS_NAME_STATIC)\n    this._queueCallback(() => {\n      this._element.classList.remove(CLASS_NAME_STATIC)\n      this._queueCallback(() => {\n        this._element.style.overflowY = initialOverflowY\n      }, this._dialog)\n    }, this._dialog)\n\n    this._element.focus()\n  }\n\n  /**\n   * The following methods are used to handle overflowing modals\n   */\n\n  _adjustDialog() {\n    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight\n    const scrollbarWidth = this._scrollBar.getWidth()\n    const isBodyOverflowing = scrollbarWidth > 0\n\n    if (isBodyOverflowing && !isModalOverflowing) {\n      const property = isRTL() ? 'paddingLeft' : 'paddingRight'\n      this._element.style[property] = `${scrollbarWidth}px`\n    }\n\n    if (!isBodyOverflowing && isModalOverflowing) {\n      const property = isRTL() ? 'paddingRight' : 'paddingLeft'\n      this._element.style[property] = `${scrollbarWidth}px`\n    }\n  }\n\n  _resetAdjustments() {\n    this._element.style.paddingLeft = ''\n    this._element.style.paddingRight = ''\n  }\n}\n\n/**\n * Data API implementation\n */\n\nEventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {\n  const target = SelectorEngine.getElementFromSelector(this)\n\n  if (['A', 'AREA'].includes(this.tagName)) {\n    event.preventDefault()\n  }\n\n  EventHandler.one(target, EVENT_SHOW, showEvent => {\n    if (showEvent.defaultPrevented) {\n      // only register focus restorer if modal will actually get shown\n      return\n    }\n\n    EventHandler.one(target, EVENT_HIDDEN, () => {\n      if (isVisible(this)) {\n        this.focus()\n      }\n    })\n  })\n\n  // avoid conflict when clicking modal toggler while another one is open\n  const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)\n  if (alreadyOpen) {\n    Modal.getInstance(alreadyOpen).hide()\n  }\n\n  const data = Modal.getOrCreateInstance(target)\n\n  data.toggle(this)\n})\n\nenableDismissTrigger(Modal)\n\nexport default Modal\n"],"names":["NAME","DATA_KEY","EVENT_KEY","DATA_API_KEY","ESCAPE_KEY","EVENT_HIDE","EVENT_HIDE_PREVENTED","EVENT_HIDDEN","EVENT_SHOW","EVENT_SHOWN","EVENT_RESIZE","EVENT_CLICK_DISMISS","EVENT_MOUSEDOWN_DISMISS","EVENT_KEYDOWN_DISMISS","EVENT_CLICK_DATA_API","CLASS_NAME_OPEN","CLASS_NAME_FADE","CLASS_NAME_SHOW","CLASS_NAME_STATIC","OPEN_SELECTOR","SELECTOR_DIALOG","SELECTOR_MODAL_BODY","SELECTOR_DATA_TOGGLE","Default","backdrop","focus","keyboard","DefaultType","Modal","BaseComponent","constructor","element","config","_dialog","SelectorEngine","findOne","_element","_backdrop","_initializeBackDrop","_focustrap","_initializeFocusTrap","_isShown","_isTransitioning","_scrollBar","ScrollBarHelper","_addEventListeners","toggle","relatedTarget","hide","show","showEvent","EventHandler","trigger","defaultPrevented","document","body","classList","add","_adjustDialog","_showElement","hideEvent","deactivate","remove","_queueCallback","_hideModal","_isAnimated","dispose","off","window","handleUpdate","Backdrop","isVisible","Boolean","_config","isAnimated","FocusTrap","trapElement","contains","append","style","display","removeAttribute","setAttribute","scrollTop","modalBody","reflow","transitionComplete","activate","on","event","key","_triggerBackdropTransition","one","event2","target","_resetAdjustments","reset","isModalOverflowing","scrollHeight","documentElement","clientHeight","initialOverflowY","overflowY","scrollbarWidth","getWidth","isBodyOverflowing","property","isRTL","paddingLeft","paddingRight","getElementFromSelector","includes","tagName","preventDefault","alreadyOpen","getInstance","data","getOrCreateInstance","enableDismissTrigger"],"mappings":";;;;;;;;;;;EAAA;EACA;EACA;EACA;EACA;EACA;;;EAaA;EACA;EACA;;EAEA,MAAMA,IAAI,GAAG,OAAO;EACpB,MAAMC,QAAQ,GAAG,UAAU;EAC3B,MAAMC,SAAS,GAAG,CAAA,CAAA,EAAID,QAAQ,CAAA,CAAE;EAChC,MAAME,YAAY,GAAG,WAAW;EAChC,MAAMC,UAAU,GAAG,QAAQ;EAE3B,MAAMC,UAAU,GAAG,CAAA,IAAA,EAAOH,SAAS,CAAA,CAAE;EACrC,MAAMI,oBAAoB,GAAG,CAAA,aAAA,EAAgBJ,SAAS,CAAA,CAAE;EACxD,MAAMK,YAAY,GAAG,CAAA,MAAA,EAASL,SAAS,CAAA,CAAE;EACzC,MAAMM,UAAU,GAAG,CAAA,IAAA,EAAON,SAAS,CAAA,CAAE;EACrC,MAAMO,WAAW,GAAG,CAAA,KAAA,EAAQP,SAAS,CAAA,CAAE;EACvC,MAAMQ,YAAY,GAAG,CAAA,MAAA,EAASR,SAAS,CAAA,CAAE;EACzC,MAAMS,mBAAmB,GAAG,CAAA,aAAA,EAAgBT,SAAS,CAAA,CAAE;EACvD,MAAMU,uBAAuB,GAAG,CAAA,iBAAA,EAAoBV,SAAS,CAAA,CAAE;EAC/D,MAAMW,qBAAqB,GAAG,CAAA,eAAA,EAAkBX,SAAS,CAAA,CAAE;EAC3D,MAAMY,oBAAoB,GAAG,CAAA,KAAA,EAAQZ,SAAS,CAAA,EAAGC,YAAY,CAAA,CAAE;EAE/D,MAAMY,eAAe,GAAG,YAAY;EACpC,MAAMC,eAAe,GAAG,MAAM;EAC9B,MAAMC,eAAe,GAAG,MAAM;EAC9B,MAAMC,iBAAiB,GAAG,cAAc;EAExC,MAAMC,aAAa,GAAG,aAAa;EACnC,MAAMC,eAAe,GAAG,eAAe;EACvC,MAAMC,mBAAmB,GAAG,aAAa;EACzC,MAAMC,oBAAoB,GAAG,0BAA0B;EAEvD,MAAMC,OAAO,GAAG;EACdC,EAAAA,QAAQ,EAAE,IAAI;EACdC,EAAAA,KAAK,EAAE,IAAI;EACXC,EAAAA,QAAQ,EAAE;EACZ,CAAC;EAED,MAAMC,WAAW,GAAG;EAClBH,EAAAA,QAAQ,EAAE,kBAAkB;EAC5BC,EAAAA,KAAK,EAAE,SAAS;EAChBC,EAAAA,QAAQ,EAAE;EACZ,CAAC;;EAED;EACA;EACA;;EAEA,MAAME,KAAK,SAASC,aAAa,CAAC;EAChCC,EAAAA,WAAWA,CAACC,OAAO,EAAEC,MAAM,EAAE;EAC3B,IAAA,KAAK,CAACD,OAAO,EAAEC,MAAM,CAAC;EAEtB,IAAA,IAAI,CAACC,OAAO,GAAGC,cAAc,CAACC,OAAO,CAACf,eAAe,EAAE,IAAI,CAACgB,QAAQ,CAAC;EACrE,IAAA,IAAI,CAACC,SAAS,GAAG,IAAI,CAACC,mBAAmB,EAAE;EAC3C,IAAA,IAAI,CAACC,UAAU,GAAG,IAAI,CAACC,oBAAoB,EAAE;MAC7C,IAAI,CAACC,QAAQ,GAAG,KAAK;MACrB,IAAI,CAACC,gBAAgB,GAAG,KAAK;EAC7B,IAAA,IAAI,CAACC,UAAU,GAAG,IAAIC,eAAe,EAAE;MAEvC,IAAI,CAACC,kBAAkB,EAAE;EAC3B,EAAA;;EAEA;IACA,WAAWtB,OAAOA,GAAG;EACnB,IAAA,OAAOA,OAAO;EAChB,EAAA;IAEA,WAAWI,WAAWA,GAAG;EACvB,IAAA,OAAOA,WAAW;EACpB,EAAA;IAEA,WAAW3B,IAAIA,GAAG;EAChB,IAAA,OAAOA,IAAI;EACb,EAAA;;EAEA;IACA8C,MAAMA,CAACC,aAAa,EAAE;EACpB,IAAA,OAAO,IAAI,CAACN,QAAQ,GAAG,IAAI,CAACO,IAAI,EAAE,GAAG,IAAI,CAACC,IAAI,CAACF,aAAa,CAAC;EAC/D,EAAA;IAEAE,IAAIA,CAACF,aAAa,EAAE;EAClB,IAAA,IAAI,IAAI,CAACN,QAAQ,IAAI,IAAI,CAACC,gBAAgB,EAAE;EAC1C,MAAA;EACF,IAAA;MAEA,MAAMQ,SAAS,GAAGC,YAAY,CAACC,OAAO,CAAC,IAAI,CAAChB,QAAQ,EAAE5B,UAAU,EAAE;EAChEuC,MAAAA;EACF,KAAC,CAAC;MAEF,IAAIG,SAAS,CAACG,gBAAgB,EAAE;EAC9B,MAAA;EACF,IAAA;MAEA,IAAI,CAACZ,QAAQ,GAAG,IAAI;MACpB,IAAI,CAACC,gBAAgB,GAAG,IAAI;EAE5B,IAAA,IAAI,CAACC,UAAU,CAACK,IAAI,EAAE;MAEtBM,QAAQ,CAACC,IAAI,CAACC,SAAS,CAACC,GAAG,CAAC1C,eAAe,CAAC;MAE5C,IAAI,CAAC2C,aAAa,EAAE;EAEpB,IAAA,IAAI,CAACrB,SAAS,CAACY,IAAI,CAAC,MAAM,IAAI,CAACU,YAAY,CAACZ,aAAa,CAAC,CAAC;EAC7D,EAAA;EAEAC,EAAAA,IAAIA,GAAG;MACL,IAAI,CAAC,IAAI,CAACP,QAAQ,IAAI,IAAI,CAACC,gBAAgB,EAAE;EAC3C,MAAA;EACF,IAAA;MAEA,MAAMkB,SAAS,GAAGT,YAAY,CAACC,OAAO,CAAC,IAAI,CAAChB,QAAQ,EAAE/B,UAAU,CAAC;MAEjE,IAAIuD,SAAS,CAACP,gBAAgB,EAAE;EAC9B,MAAA;EACF,IAAA;MAEA,IAAI,CAACZ,QAAQ,GAAG,KAAK;MACrB,IAAI,CAACC,gBAAgB,GAAG,IAAI;EAC5B,IAAA,IAAI,CAACH,UAAU,CAACsB,UAAU,EAAE;MAE5B,IAAI,CAACzB,QAAQ,CAACoB,SAAS,CAACM,MAAM,CAAC7C,eAAe,CAAC;EAE/C,IAAA,IAAI,CAAC8C,cAAc,CAAC,MAAM,IAAI,CAACC,UAAU,EAAE,EAAE,IAAI,CAAC5B,QAAQ,EAAE,IAAI,CAAC6B,WAAW,EAAE,CAAC;EACjF,EAAA;EAEAC,EAAAA,OAAOA,GAAG;EACRf,IAAAA,YAAY,CAACgB,GAAG,CAACC,MAAM,EAAElE,SAAS,CAAC;MACnCiD,YAAY,CAACgB,GAAG,CAAC,IAAI,CAAClC,OAAO,EAAE/B,SAAS,CAAC;EAEzC,IAAA,IAAI,CAACmC,SAAS,CAAC6B,OAAO,EAAE;EACxB,IAAA,IAAI,CAAC3B,UAAU,CAACsB,UAAU,EAAE;MAE5B,KAAK,CAACK,OAAO,EAAE;EACjB,EAAA;EAEAG,EAAAA,YAAYA,GAAG;MACb,IAAI,CAACX,aAAa,EAAE;EACtB,EAAA;;EAEA;EACApB,EAAAA,mBAAmBA,GAAG;MACpB,OAAO,IAAIgC,QAAQ,CAAC;QAClBC,SAAS,EAAEC,OAAO,CAAC,IAAI,CAACC,OAAO,CAACjD,QAAQ,CAAC;EAAE;EAC3CkD,MAAAA,UAAU,EAAE,IAAI,CAACT,WAAW;EAC9B,KAAC,CAAC;EACJ,EAAA;EAEAzB,EAAAA,oBAAoBA,GAAG;MACrB,OAAO,IAAImC,SAAS,CAAC;QACnBC,WAAW,EAAE,IAAI,CAACxC;EACpB,KAAC,CAAC;EACJ,EAAA;IAEAuB,YAAYA,CAACZ,aAAa,EAAE;EAC1B;MACA,IAAI,CAACO,QAAQ,CAACC,IAAI,CAACsB,QAAQ,CAAC,IAAI,CAACzC,QAAQ,CAAC,EAAE;QAC1CkB,QAAQ,CAACC,IAAI,CAACuB,MAAM,CAAC,IAAI,CAAC1C,QAAQ,CAAC;EACrC,IAAA;EAEA,IAAA,IAAI,CAACA,QAAQ,CAAC2C,KAAK,CAACC,OAAO,GAAG,OAAO;EACrC,IAAA,IAAI,CAAC5C,QAAQ,CAAC6C,eAAe,CAAC,aAAa,CAAC;MAC5C,IAAI,CAAC7C,QAAQ,CAAC8C,YAAY,CAAC,YAAY,EAAE,IAAI,CAAC;MAC9C,IAAI,CAAC9C,QAAQ,CAAC8C,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC;EAC5C,IAAA,IAAI,CAAC9C,QAAQ,CAAC+C,SAAS,GAAG,CAAC;MAE3B,MAAMC,SAAS,GAAGlD,cAAc,CAACC,OAAO,CAACd,mBAAmB,EAAE,IAAI,CAACY,OAAO,CAAC;EAC3E,IAAA,IAAImD,SAAS,EAAE;QACbA,SAAS,CAACD,SAAS,GAAG,CAAC;EACzB,IAAA;EAEAE,IAAAA,eAAM,CAAC,IAAI,CAACjD,QAAQ,CAAC;MAErB,IAAI,CAACA,QAAQ,CAACoB,SAAS,CAACC,GAAG,CAACxC,eAAe,CAAC;MAE5C,MAAMqE,kBAAkB,GAAGA,MAAM;EAC/B,MAAA,IAAI,IAAI,CAACb,OAAO,CAAChD,KAAK,EAAE;EACtB,QAAA,IAAI,CAACc,UAAU,CAACgD,QAAQ,EAAE;EAC5B,MAAA;QAEA,IAAI,CAAC7C,gBAAgB,GAAG,KAAK;QAC7BS,YAAY,CAACC,OAAO,CAAC,IAAI,CAAChB,QAAQ,EAAE3B,WAAW,EAAE;EAC/CsC,QAAAA;EACF,OAAC,CAAC;MACJ,CAAC;EAED,IAAA,IAAI,CAACgB,cAAc,CAACuB,kBAAkB,EAAE,IAAI,CAACrD,OAAO,EAAE,IAAI,CAACgC,WAAW,EAAE,CAAC;EAC3E,EAAA;EAEApB,EAAAA,kBAAkBA,GAAG;MACnBM,YAAY,CAACqC,EAAE,CAAC,IAAI,CAACpD,QAAQ,EAAEvB,qBAAqB,EAAE4E,KAAK,IAAI;EAC7D,MAAA,IAAIA,KAAK,CAACC,GAAG,KAAKtF,UAAU,EAAE;EAC5B,QAAA;EACF,MAAA;EAEA,MAAA,IAAI,IAAI,CAACqE,OAAO,CAAC/C,QAAQ,EAAE;UACzB,IAAI,CAACsB,IAAI,EAAE;EACX,QAAA;EACF,MAAA;QAEA,IAAI,CAAC2C,0BAA0B,EAAE;EACnC,IAAA,CAAC,CAAC;EAEFxC,IAAAA,YAAY,CAACqC,EAAE,CAACpB,MAAM,EAAE1D,YAAY,EAAE,MAAM;QAC1C,IAAI,IAAI,CAAC+B,QAAQ,IAAI,CAAC,IAAI,CAACC,gBAAgB,EAAE;UAC3C,IAAI,CAACgB,aAAa,EAAE;EACtB,MAAA;EACF,IAAA,CAAC,CAAC;MAEFP,YAAY,CAACqC,EAAE,CAAC,IAAI,CAACpD,QAAQ,EAAExB,uBAAuB,EAAE6E,KAAK,IAAI;EAC/D;QACAtC,YAAY,CAACyC,GAAG,CAAC,IAAI,CAACxD,QAAQ,EAAEzB,mBAAmB,EAAEkF,MAAM,IAAI;EAC7D,QAAA,IAAI,IAAI,CAACzD,QAAQ,KAAKqD,KAAK,CAACK,MAAM,IAAI,IAAI,CAAC1D,QAAQ,KAAKyD,MAAM,CAACC,MAAM,EAAE;EACrE,UAAA;EACF,QAAA;EAEA,QAAA,IAAI,IAAI,CAACrB,OAAO,CAACjD,QAAQ,KAAK,QAAQ,EAAE;YACtC,IAAI,CAACmE,0BAA0B,EAAE;EACjC,UAAA;EACF,QAAA;EAEA,QAAA,IAAI,IAAI,CAAClB,OAAO,CAACjD,QAAQ,EAAE;YACzB,IAAI,CAACwB,IAAI,EAAE;EACb,QAAA;EACF,MAAA,CAAC,CAAC;EACJ,IAAA,CAAC,CAAC;EACJ,EAAA;EAEAgB,EAAAA,UAAUA,GAAG;EACX,IAAA,IAAI,CAAC5B,QAAQ,CAAC2C,KAAK,CAACC,OAAO,GAAG,MAAM;MACpC,IAAI,CAAC5C,QAAQ,CAAC8C,YAAY,CAAC,aAAa,EAAE,IAAI,CAAC;EAC/C,IAAA,IAAI,CAAC9C,QAAQ,CAAC6C,eAAe,CAAC,YAAY,CAAC;EAC3C,IAAA,IAAI,CAAC7C,QAAQ,CAAC6C,eAAe,CAAC,MAAM,CAAC;MACrC,IAAI,CAACvC,gBAAgB,GAAG,KAAK;EAE7B,IAAA,IAAI,CAACL,SAAS,CAACW,IAAI,CAAC,MAAM;QACxBM,QAAQ,CAACC,IAAI,CAACC,SAAS,CAACM,MAAM,CAAC/C,eAAe,CAAC;QAC/C,IAAI,CAACgF,iBAAiB,EAAE;EACxB,MAAA,IAAI,CAACpD,UAAU,CAACqD,KAAK,EAAE;QACvB7C,YAAY,CAACC,OAAO,CAAC,IAAI,CAAChB,QAAQ,EAAE7B,YAAY,CAAC;EACnD,IAAA,CAAC,CAAC;EACJ,EAAA;EAEA0D,EAAAA,WAAWA,GAAG;MACZ,OAAO,IAAI,CAAC7B,QAAQ,CAACoB,SAAS,CAACqB,QAAQ,CAAC7D,eAAe,CAAC;EAC1D,EAAA;EAEA2E,EAAAA,0BAA0BA,GAAG;MAC3B,MAAM/B,SAAS,GAAGT,YAAY,CAACC,OAAO,CAAC,IAAI,CAAChB,QAAQ,EAAE9B,oBAAoB,CAAC;MAC3E,IAAIsD,SAAS,CAACP,gBAAgB,EAAE;EAC9B,MAAA;EACF,IAAA;EAEA,IAAA,MAAM4C,kBAAkB,GAAG,IAAI,CAAC7D,QAAQ,CAAC8D,YAAY,GAAG5C,QAAQ,CAAC6C,eAAe,CAACC,YAAY;MAC7F,MAAMC,gBAAgB,GAAG,IAAI,CAACjE,QAAQ,CAAC2C,KAAK,CAACuB,SAAS;EACtD;EACA,IAAA,IAAID,gBAAgB,KAAK,QAAQ,IAAI,IAAI,CAACjE,QAAQ,CAACoB,SAAS,CAACqB,QAAQ,CAAC3D,iBAAiB,CAAC,EAAE;EACxF,MAAA;EACF,IAAA;MAEA,IAAI,CAAC+E,kBAAkB,EAAE;EACvB,MAAA,IAAI,CAAC7D,QAAQ,CAAC2C,KAAK,CAACuB,SAAS,GAAG,QAAQ;EAC1C,IAAA;MAEA,IAAI,CAAClE,QAAQ,CAACoB,SAAS,CAACC,GAAG,CAACvC,iBAAiB,CAAC;MAC9C,IAAI,CAAC6C,cAAc,CAAC,MAAM;QACxB,IAAI,CAAC3B,QAAQ,CAACoB,SAAS,CAACM,MAAM,CAAC5C,iBAAiB,CAAC;QACjD,IAAI,CAAC6C,cAAc,CAAC,MAAM;EACxB,QAAA,IAAI,CAAC3B,QAAQ,CAAC2C,KAAK,CAACuB,SAAS,GAAGD,gBAAgB;EAClD,MAAA,CAAC,EAAE,IAAI,CAACpE,OAAO,CAAC;EAClB,IAAA,CAAC,EAAE,IAAI,CAACA,OAAO,CAAC;EAEhB,IAAA,IAAI,CAACG,QAAQ,CAACX,KAAK,EAAE;EACvB,EAAA;;EAEA;EACF;EACA;;EAEEiC,EAAAA,aAAaA,GAAG;EACd,IAAA,MAAMuC,kBAAkB,GAAG,IAAI,CAAC7D,QAAQ,CAAC8D,YAAY,GAAG5C,QAAQ,CAAC6C,eAAe,CAACC,YAAY;MAC7F,MAAMG,cAAc,GAAG,IAAI,CAAC5D,UAAU,CAAC6D,QAAQ,EAAE;EACjD,IAAA,MAAMC,iBAAiB,GAAGF,cAAc,GAAG,CAAC;EAE5C,IAAA,IAAIE,iBAAiB,IAAI,CAACR,kBAAkB,EAAE;QAC5C,MAAMS,QAAQ,GAAGC,cAAK,EAAE,GAAG,aAAa,GAAG,cAAc;QACzD,IAAI,CAACvE,QAAQ,CAAC2C,KAAK,CAAC2B,QAAQ,CAAC,GAAG,CAAA,EAAGH,cAAc,CAAA,EAAA,CAAI;EACvD,IAAA;EAEA,IAAA,IAAI,CAACE,iBAAiB,IAAIR,kBAAkB,EAAE;QAC5C,MAAMS,QAAQ,GAAGC,cAAK,EAAE,GAAG,cAAc,GAAG,aAAa;QACzD,IAAI,CAACvE,QAAQ,CAAC2C,KAAK,CAAC2B,QAAQ,CAAC,GAAG,CAAA,EAAGH,cAAc,CAAA,EAAA,CAAI;EACvD,IAAA;EACF,EAAA;EAEAR,EAAAA,iBAAiBA,GAAG;EAClB,IAAA,IAAI,CAAC3D,QAAQ,CAAC2C,KAAK,CAAC6B,WAAW,GAAG,EAAE;EACpC,IAAA,IAAI,CAACxE,QAAQ,CAAC2C,KAAK,CAAC8B,YAAY,GAAG,EAAE;EACvC,EAAA;EACF;;EAEA;EACA;EACA;;EAEA1D,YAAY,CAACqC,EAAE,CAAClC,QAAQ,EAAExC,oBAAoB,EAAEQ,oBAAoB,EAAE,UAAUmE,KAAK,EAAE;EACrF,EAAA,MAAMK,MAAM,GAAG5D,cAAc,CAAC4E,sBAAsB,CAAC,IAAI,CAAC;EAE1D,EAAA,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAACC,QAAQ,CAAC,IAAI,CAACC,OAAO,CAAC,EAAE;MACxCvB,KAAK,CAACwB,cAAc,EAAE;EACxB,EAAA;IAEA9D,YAAY,CAACyC,GAAG,CAACE,MAAM,EAAEtF,UAAU,EAAE0C,SAAS,IAAI;MAChD,IAAIA,SAAS,CAACG,gBAAgB,EAAE;EAC9B;EACA,MAAA;EACF,IAAA;EAEAF,IAAAA,YAAY,CAACyC,GAAG,CAACE,MAAM,EAAEvF,YAAY,EAAE,MAAM;EAC3C,MAAA,IAAIgE,kBAAS,CAAC,IAAI,CAAC,EAAE;UACnB,IAAI,CAAC9C,KAAK,EAAE;EACd,MAAA;EACF,IAAA,CAAC,CAAC;EACJ,EAAA,CAAC,CAAC;;EAEF;EACA,EAAA,MAAMyF,WAAW,GAAGhF,cAAc,CAACC,OAAO,CAAChB,aAAa,CAAC;EACzD,EAAA,IAAI+F,WAAW,EAAE;MACftF,KAAK,CAACuF,WAAW,CAACD,WAAW,CAAC,CAAClE,IAAI,EAAE;EACvC,EAAA;EAEA,EAAA,MAAMoE,IAAI,GAAGxF,KAAK,CAACyF,mBAAmB,CAACvB,MAAM,CAAC;EAE9CsB,EAAAA,IAAI,CAACtE,MAAM,CAAC,IAAI,CAAC;EACnB,CAAC,CAAC;AAEFwE,4CAAoB,CAAC1F,KAAK,CAAC;;;;;;;;"}
\ No newline at end of file
index 155d9fb6a59a98c706939cb6ec3f4dc3e1ed7944..d67e5f69c8dbcaa7be7f574f60affef79489fdf7 100644 (file)
@@ -9,8 +9,8 @@ export { default as Alert } from './src/alert.js'
 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'
index a33df74657ff5a73a02430a438b37d6ada453cc8..d228e7f2b9abef4bb0cb359ebd3931f8a99a476b 100644 (file)
@@ -9,8 +9,8 @@ import Alert from './src/alert.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'
@@ -23,8 +23,8 @@ export default {
   Button,
   Carousel,
   Collapse,
+  Dialog,
   Dropdown,
-  Modal,
   Offcanvas,
   Popover,
   ScrollSpy,
diff --git a/js/src/dialog.js b/js/src/dialog.js
new file mode 100644 (file)
index 0000000..4cd59f8
--- /dev/null
@@ -0,0 +1,272 @@
+/**
+ * --------------------------------------------------------------------------
+ * 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
diff --git a/js/src/modal.js b/js/src/modal.js
deleted file mode 100644 (file)
index 538d218..0000000
+++ /dev/null
@@ -1,355 +0,0 @@
-/**
- * --------------------------------------------------------------------------
- * Bootstrap modal.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 SelectorEngine from './dom/selector-engine.js'
-import Backdrop from './util/backdrop.js'
-import { enableDismissTrigger } from './util/component-functions.js'
-import FocusTrap from './util/focustrap.js'
-import {
-  isRTL, isVisible, reflow
-} from './util/index.js'
-import ScrollBarHelper from './util/scrollbar.js'
-
-/**
- * Constants
- */
-
-const NAME = 'modal'
-const DATA_KEY = 'bs.modal'
-const EVENT_KEY = `.${DATA_KEY}`
-const DATA_API_KEY = '.data-api'
-const ESCAPE_KEY = 'Escape'
-
-const EVENT_HIDE = `hide${EVENT_KEY}`
-const EVENT_HIDE_PREVENTED = `hidePrevented${EVENT_KEY}`
-const EVENT_HIDDEN = `hidden${EVENT_KEY}`
-const EVENT_SHOW = `show${EVENT_KEY}`
-const EVENT_SHOWN = `shown${EVENT_KEY}`
-const EVENT_RESIZE = `resize${EVENT_KEY}`
-const EVENT_CLICK_DISMISS = `click.dismiss${EVENT_KEY}`
-const EVENT_MOUSEDOWN_DISMISS = `mousedown.dismiss${EVENT_KEY}`
-const EVENT_KEYDOWN_DISMISS = `keydown.dismiss${EVENT_KEY}`
-const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
-
-const CLASS_NAME_OPEN = 'modal-open'
-const CLASS_NAME_FADE = 'fade'
-const CLASS_NAME_SHOW = 'show'
-const CLASS_NAME_STATIC = 'modal-static'
-
-const OPEN_SELECTOR = '.modal.show'
-const SELECTOR_DIALOG = '.modal-dialog'
-const SELECTOR_MODAL_BODY = '.modal-body'
-const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="modal"]'
-
-const Default = {
-  backdrop: true,
-  focus: true,
-  keyboard: true
-}
-
-const DefaultType = {
-  backdrop: '(boolean|string)',
-  focus: 'boolean',
-  keyboard: 'boolean'
-}
-
-/**
- * Class definition
- */
-
-class Modal extends BaseComponent {
-  constructor(element, config) {
-    super(element, config)
-
-    this._dialog = SelectorEngine.findOne(SELECTOR_DIALOG, this._element)
-    this._backdrop = this._initializeBackDrop()
-    this._focustrap = this._initializeFocusTrap()
-    this._isShown = false
-    this._isTransitioning = false
-    this._scrollBar = new ScrollBarHelper()
-
-    this._addEventListeners()
-  }
-
-  // Getters
-  static get Default() {
-    return Default
-  }
-
-  static get DefaultType() {
-    return DefaultType
-  }
-
-  static get NAME() {
-    return NAME
-  }
-
-  // Public
-  toggle(relatedTarget) {
-    return this._isShown ? this.hide() : this.show(relatedTarget)
-  }
-
-  show(relatedTarget) {
-    if (this._isShown || this._isTransitioning) {
-      return
-    }
-
-    const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, {
-      relatedTarget
-    })
-
-    if (showEvent.defaultPrevented) {
-      return
-    }
-
-    this._isShown = true
-    this._isTransitioning = true
-
-    this._scrollBar.hide()
-
-    document.body.classList.add(CLASS_NAME_OPEN)
-
-    this._adjustDialog()
-
-    this._backdrop.show(() => this._showElement(relatedTarget))
-  }
-
-  hide() {
-    if (!this._isShown || this._isTransitioning) {
-      return
-    }
-
-    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE)
-
-    if (hideEvent.defaultPrevented) {
-      return
-    }
-
-    this._isShown = false
-    this._isTransitioning = true
-    this._focustrap.deactivate()
-
-    this._element.classList.remove(CLASS_NAME_SHOW)
-
-    this._queueCallback(() => this._hideModal(), this._element, this._isAnimated())
-  }
-
-  dispose() {
-    EventHandler.off(window, EVENT_KEY)
-    EventHandler.off(this._dialog, EVENT_KEY)
-
-    this._backdrop.dispose()
-    this._focustrap.deactivate()
-
-    super.dispose()
-  }
-
-  handleUpdate() {
-    this._adjustDialog()
-  }
-
-  // Private
-  _initializeBackDrop() {
-    return new Backdrop({
-      isVisible: Boolean(this._config.backdrop), // 'static' option will be translated to true, and booleans will keep their value,
-      isAnimated: this._isAnimated()
-    })
-  }
-
-  _initializeFocusTrap() {
-    return new FocusTrap({
-      trapElement: this._element
-    })
-  }
-
-  _showElement(relatedTarget) {
-    // try to append dynamic modal
-    if (!document.body.contains(this._element)) {
-      document.body.append(this._element)
-    }
-
-    this._element.style.display = 'block'
-    this._element.removeAttribute('aria-hidden')
-    this._element.setAttribute('aria-modal', true)
-    this._element.setAttribute('role', 'dialog')
-    this._element.scrollTop = 0
-
-    const modalBody = SelectorEngine.findOne(SELECTOR_MODAL_BODY, this._dialog)
-    if (modalBody) {
-      modalBody.scrollTop = 0
-    }
-
-    reflow(this._element)
-
-    this._element.classList.add(CLASS_NAME_SHOW)
-
-    const transitionComplete = () => {
-      if (this._config.focus) {
-        this._focustrap.activate()
-      }
-
-      this._isTransitioning = false
-      EventHandler.trigger(this._element, EVENT_SHOWN, {
-        relatedTarget
-      })
-    }
-
-    this._queueCallback(transitionComplete, this._dialog, this._isAnimated())
-  }
-
-  _addEventListeners() {
-    EventHandler.on(this._element, EVENT_KEYDOWN_DISMISS, event => {
-      if (event.key !== ESCAPE_KEY) {
-        return
-      }
-
-      if (this._config.keyboard) {
-        this.hide()
-        return
-      }
-
-      this._triggerBackdropTransition()
-    })
-
-    EventHandler.on(window, EVENT_RESIZE, () => {
-      if (this._isShown && !this._isTransitioning) {
-        this._adjustDialog()
-      }
-    })
-
-    EventHandler.on(this._element, EVENT_MOUSEDOWN_DISMISS, event => {
-      // a bad trick to segregate clicks that may start inside dialog but end outside, and avoid listen to scrollbar clicks
-      EventHandler.one(this._element, EVENT_CLICK_DISMISS, event2 => {
-        if (this._element !== event.target || this._element !== event2.target) {
-          return
-        }
-
-        if (this._config.backdrop === 'static') {
-          this._triggerBackdropTransition()
-          return
-        }
-
-        if (this._config.backdrop) {
-          this.hide()
-        }
-      })
-    })
-  }
-
-  _hideModal() {
-    this._element.style.display = 'none'
-    this._element.setAttribute('aria-hidden', true)
-    this._element.removeAttribute('aria-modal')
-    this._element.removeAttribute('role')
-    this._isTransitioning = false
-
-    this._backdrop.hide(() => {
-      document.body.classList.remove(CLASS_NAME_OPEN)
-      this._resetAdjustments()
-      this._scrollBar.reset()
-      EventHandler.trigger(this._element, EVENT_HIDDEN)
-    })
-  }
-
-  _isAnimated() {
-    return this._element.classList.contains(CLASS_NAME_FADE)
-  }
-
-  _triggerBackdropTransition() {
-    const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE_PREVENTED)
-    if (hideEvent.defaultPrevented) {
-      return
-    }
-
-    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
-    const initialOverflowY = this._element.style.overflowY
-    // return if the following background transition hasn't yet completed
-    if (initialOverflowY === 'hidden' || this._element.classList.contains(CLASS_NAME_STATIC)) {
-      return
-    }
-
-    if (!isModalOverflowing) {
-      this._element.style.overflowY = 'hidden'
-    }
-
-    this._element.classList.add(CLASS_NAME_STATIC)
-    this._queueCallback(() => {
-      this._element.classList.remove(CLASS_NAME_STATIC)
-      this._queueCallback(() => {
-        this._element.style.overflowY = initialOverflowY
-      }, this._dialog)
-    }, this._dialog)
-
-    this._element.focus()
-  }
-
-  /**
-   * The following methods are used to handle overflowing modals
-   */
-
-  _adjustDialog() {
-    const isModalOverflowing = this._element.scrollHeight > document.documentElement.clientHeight
-    const scrollbarWidth = this._scrollBar.getWidth()
-    const isBodyOverflowing = scrollbarWidth > 0
-
-    if (isBodyOverflowing && !isModalOverflowing) {
-      const property = isRTL() ? 'paddingLeft' : 'paddingRight'
-      this._element.style[property] = `${scrollbarWidth}px`
-    }
-
-    if (!isBodyOverflowing && isModalOverflowing) {
-      const property = isRTL() ? 'paddingRight' : 'paddingLeft'
-      this._element.style[property] = `${scrollbarWidth}px`
-    }
-  }
-
-  _resetAdjustments() {
-    this._element.style.paddingLeft = ''
-    this._element.style.paddingRight = ''
-  }
-}
-
-/**
- * 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) {
-      // only register focus restorer if modal will actually get shown
-      return
-    }
-
-    EventHandler.one(target, EVENT_HIDDEN, () => {
-      if (isVisible(this)) {
-        this.focus()
-      }
-    })
-  })
-
-  // avoid conflict when clicking modal toggler while another one is open
-  const alreadyOpen = SelectorEngine.findOne(OPEN_SELECTOR)
-  if (alreadyOpen) {
-    Modal.getInstance(alreadyOpen).hide()
-  }
-
-  const data = Modal.getOrCreateInstance(target)
-
-  data.toggle(this)
-})
-
-enableDismissTrigger(Modal)
-
-export default Modal
diff --git a/js/tests/unit/dialog.spec.js b/js/tests/unit/dialog.spec.js
new file mode 100644 (file)
index 0000000..1a2ff53
--- /dev/null
@@ -0,0 +1,947 @@
+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')
+    })
+  })
+})
diff --git a/js/tests/unit/modal.spec.js b/js/tests/unit/modal.spec.js
deleted file mode 100644 (file)
index 1480521..0000000
+++ /dev/null
@@ -1,1239 +0,0 @@
-import EventHandler from '../../src/dom/event-handler.js'
-import Modal from '../../src/modal.js'
-import ScrollBarHelper from '../../src/util/scrollbar.js'
-import {
-  clearBodyAndDocument, clearFixture, createEvent, getFixture
-} from '../helpers/fixture.js'
-
-describe('Modal', () => {
-  let fixtureEl
-
-  beforeAll(() => {
-    fixtureEl = getFixture()
-  })
-
-  afterEach(() => {
-    clearFixture()
-    clearBodyAndDocument()
-    document.body.classList.remove('modal-open')
-
-    for (const backdrop of document.querySelectorAll('.modal-backdrop')) {
-      backdrop.remove()
-    }
-  })
-
-  beforeEach(() => {
-    clearBodyAndDocument()
-  })
-
-  describe('VERSION', () => {
-    it('should return plugin version', () => {
-      expect(Modal.VERSION).toEqual(jasmine.any(String))
-    })
-  })
-
-  describe('Default', () => {
-    it('should return plugin default config', () => {
-      expect(Modal.Default).toEqual(jasmine.any(Object))
-    })
-  })
-
-  describe('DATA_KEY', () => {
-    it('should return plugin data key', () => {
-      expect(Modal.DATA_KEY).toEqual('bs.modal')
-    })
-  })
-
-  describe('constructor', () => {
-    it('should take care of element either passed as a CSS selector or DOM element', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modalBySelector = new Modal('.modal')
-      const modalByElement = new Modal(modalEl)
-
-      expect(modalBySelector._element).toEqual(modalEl)
-      expect(modalByElement._element).toEqual(modalEl)
-    })
-  })
-
-  describe('toggle', () => {
-    it('should call ScrollBarHelper to handle scrollBar on body', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const spyHide = spyOn(ScrollBarHelper.prototype, 'hide').and.callThrough()
-        const spyReset = spyOn(ScrollBarHelper.prototype, 'reset').and.callThrough()
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(spyHide).toHaveBeenCalled()
-          modal.toggle()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spyReset).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.toggle()
-      })
-    })
-  })
-
-  describe('show', () => {
-    it('should show a modal', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('show.bs.modal', event => {
-          expect(event).toBeDefined()
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toEqual('true')
-          expect(modalEl.getAttribute('role')).toEqual('dialog')
-          expect(modalEl.getAttribute('aria-hidden')).toBeNull()
-          expect(modalEl.style.display).toEqual('block')
-          expect(document.querySelector('.modal-backdrop')).not.toBeNull()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should show a modal without backdrop', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: false
-        })
-
-        modalEl.addEventListener('show.bs.modal', event => {
-          expect(event).toBeDefined()
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toEqual('true')
-          expect(modalEl.getAttribute('role')).toEqual('dialog')
-          expect(modalEl.getAttribute('aria-hidden')).toBeNull()
-          expect(modalEl.style.display).toEqual('block')
-          expect(document.querySelector('.modal-backdrop')).toBeNull()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should show a modal and append the element', () => {
-      return new Promise(resolve => {
-        const modalEl = document.createElement('div')
-        const id = 'dynamicModal'
-
-        modalEl.setAttribute('id', id)
-        modalEl.classList.add('modal')
-        modalEl.innerHTML = '<div class="modal-dialog"></div>'
-
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const dynamicModal = document.getElementById(id)
-          expect(dynamicModal).not.toBeNull()
-          dynamicModal.remove()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should do nothing if a modal is shown', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-
-      const spy = spyOn(EventHandler, 'trigger')
-      modal._isShown = true
-
-      modal.show()
-
-      expect(spy).not.toHaveBeenCalled()
-    })
-
-    it('should do nothing if a modal is transitioning', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-
-      const spy = spyOn(EventHandler, 'trigger')
-      modal._isTransitioning = true
-
-      modal.show()
-
-      expect(spy).not.toHaveBeenCalled()
-    })
-
-    it('should not fire shown event when show is prevented', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('show.bs.modal', event => {
-          event.preventDefault()
-
-          const expectedDone = () => {
-            expect().nothing()
-            resolve()
-          }
-
-          setTimeout(expectedDone, 10)
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          reject(new Error('shown event triggered'))
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should be shown after the first call to show() has been prevented while fading is enabled ', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        let prevented = false
-        modalEl.addEventListener('show.bs.modal', event => {
-          if (!prevented) {
-            event.preventDefault()
-            prevented = true
-
-            setTimeout(() => {
-              modal.show()
-            })
-          }
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(prevented).toBeTrue()
-          expect(modal._isAnimated()).toBeTrue()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-    it('should set is transitioning if fade class is present', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('show.bs.modal', () => {
-          setTimeout(() => {
-            expect(modal._isTransitioning).toBeTrue()
-          })
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modal._isTransitioning).toBeFalse()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should close modal when a click occurred on data-bs-dismiss="modal" inside modal', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal fade">',
-          '  <div class="modal-dialog">',
-          '    <div class="modal-header">',
-          '      <button type="button" data-bs-dismiss="modal"></button>',
-          '    </div>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          btnClose.click()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should close modal when a click occurred on a data-bs-dismiss="modal" with "bs-target" outside of modal element', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<button type="button" data-bs-dismiss="modal" data-bs-target="#modal1"></button>',
-          '<div id="modal1" class="modal fade">',
-          '  <div class="modal-dialog"></div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const btnClose = fixtureEl.querySelector('[data-bs-dismiss="modal"]')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          btnClose.click()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should set .modal\'s scroll top to 0', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal fade">',
-          '  <div class="modal-dialog"></div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalEl.scrollTop).toEqual(0)
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should set modal body scroll top to 0 if modal body do not exists', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal fade">',
-          '  <div class="modal-dialog">',
-          '    <div class="modal-body"></div>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modalBody = modalEl.querySelector('.modal-body')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalBody.scrollTop).toEqual(0)
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not trap focus if focus equal to false', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal fade"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          focus: false
-        })
-
-        const spy = spyOn(modal._focustrap, 'activate').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(spy).not.toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should add listener when escape touch is pressed', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const keydownEscape = createEvent('keydown')
-          keydownEscape.key = 'Escape'
-
-          modalEl.dispatchEvent(keydownEscape)
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should do nothing when the pressed key is not escape', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide')
-
-        const expectDone = () => {
-          expect(spy).not.toHaveBeenCalled()
-
-          resolve()
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const keydownTab = createEvent('keydown')
-          keydownTab.key = 'Tab'
-
-          modalEl.dispatchEvent(keydownTab)
-          setTimeout(expectDone, 30)
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should adjust dialog on resize', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, '_adjustDialog').and.callThrough()
-
-        const expectDone = () => {
-          expect(spy).toHaveBeenCalled()
-
-          resolve()
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const resizeEvent = createEvent('resize')
-
-          window.dispatchEvent(resizeEvent)
-          setTimeout(expectDone, 10)
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not close modal when clicking on modal-content', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = [
-          '<div class="modal">',
-          '  <div class="modal-dialog">',
-          '    <div class="modal-content"></div>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        const shownCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toEqual(true)
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          fixtureEl.querySelector('.modal-dialog').click()
-          fixtureEl.querySelector('.modal-content').click()
-          shownCallback()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          reject(new Error('Should not hide a modal'))
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not close modal when clicking outside of modal-content if backdrop = false', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: false
-        })
-
-        const shownCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toBeTrue()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modalEl.click()
-          shownCallback()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          reject(new Error('Should not hide a modal'))
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not close modal when clicking outside of modal-content if backdrop = static', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: 'static'
-        })
-
-        const shownCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toBeTrue()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modalEl.click()
-          shownCallback()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          reject(new Error('Should not hide a modal'))
-        })
-
-        modal.show()
-      })
-    })
-    it('should close modal when escape key is pressed with keyboard = true and backdrop is static', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: 'static',
-          keyboard: true
-        })
-
-        const shownCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toBeFalse()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const keydownEscape = createEvent('keydown')
-          keydownEscape.key = 'Escape'
-
-          modalEl.dispatchEvent(keydownEscape)
-          shownCallback()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not close modal when escape key is pressed with keyboard = false', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          keyboard: false
-        })
-
-        const shownCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toBeTrue()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const keydownEscape = createEvent('keydown')
-          keydownEscape.key = 'Escape'
-
-          modalEl.dispatchEvent(keydownEscape)
-          shownCallback()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          reject(new Error('Should not hide a modal'))
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not overflow when clicking outside of modal-content if backdrop = static', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 20ms;"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: 'static'
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modalEl.click()
-          setTimeout(() => {
-            expect(modalEl.clientHeight).toEqual(modalEl.scrollHeight)
-            resolve()
-          }, 20)
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not queue multiple callbacks when clicking outside of modal-content and backdrop = static', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog" style="transition-duration: 50ms;"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl, {
-          backdrop: 'static'
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const spy = spyOn(modal, '_queueCallback').and.callThrough()
-          const mouseDown = createEvent('mousedown')
-
-          modalEl.dispatchEvent(mouseDown)
-          modalEl.click()
-          modalEl.dispatchEvent(mouseDown)
-          modalEl.click()
-
-          setTimeout(() => {
-            expect(spy).toHaveBeenCalledTimes(1)
-            resolve()
-          }, 20)
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should trap focus', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal._focustrap, 'activate').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-  })
-
-  describe('hide', () => {
-    it('should hide a modal', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-        const backdropSpy = spyOn(modal._backdrop, 'hide').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modal.hide()
-        })
-
-        modalEl.addEventListener('hide.bs.modal', event => {
-          expect(event).toBeDefined()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toBeNull()
-          expect(modalEl.getAttribute('role')).toBeNull()
-          expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
-          expect(modalEl.style.display).toEqual('none')
-          expect(backdropSpy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should close modal when clicking outside of modal-content', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const dialogEl = modalEl.querySelector('.modal-dialog')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide')
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const mouseDown = createEvent('mousedown')
-
-          dialogEl.dispatchEvent(mouseDown)
-          modalEl.click()
-          expect(spy).not.toHaveBeenCalled()
-
-          modalEl.dispatchEvent(mouseDown)
-          modalEl.click()
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should not close modal when clicking on an element removed from modal content', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal">',
-          ' <div class="modal-dialog">',
-          '   <button class="btn">BTN</button>',
-          ' </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const buttonEl = modalEl.querySelector('.btn')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(modal, 'hide')
-        buttonEl.addEventListener('click', () => {
-          buttonEl.remove()
-        })
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modalEl.dispatchEvent(createEvent('mousedown'))
-          buttonEl.click()
-          expect(spy).not.toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should do nothing is the modal is not shown', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-
-      modal.hide()
-
-      expect().nothing()
-    })
-
-    it('should do nothing is the modal is transitioning', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-
-      modal._isTransitioning = true
-      modal.hide()
-
-      expect().nothing()
-    })
-
-    it('should not hide a modal if hide is prevented', () => {
-      return new Promise((resolve, reject) => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modal.hide()
-        })
-
-        const hideCallback = () => {
-          setTimeout(() => {
-            expect(modal._isShown).toBeTrue()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('hide.bs.modal', event => {
-          event.preventDefault()
-          hideCallback()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          reject(new Error('should not trigger hidden'))
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should release focus trap', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-        const spy = spyOn(modal._focustrap, 'deactivate').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          modal.hide()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-  })
-
-  describe('dispose', () => {
-    it('should dispose a modal', () => {
-      fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-      const focustrap = modal._focustrap
-      const spyDeactivate = spyOn(focustrap, 'deactivate').and.callThrough()
-
-      expect(Modal.getInstance(modalEl)).toEqual(modal)
-
-      const spyOff = spyOn(EventHandler, 'off')
-
-      modal.dispose()
-
-      expect(Modal.getInstance(modalEl)).toBeNull()
-      expect(spyOff).toHaveBeenCalledTimes(3)
-      expect(spyDeactivate).toHaveBeenCalled()
-    })
-  })
-
-  describe('handleUpdate', () => {
-    it('should call adjust dialog', () => {
-      fixtureEl.innerHTML = '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-
-      const modalEl = fixtureEl.querySelector('.modal')
-      const modal = new Modal(modalEl)
-
-      const spy = spyOn(modal, '_adjustDialog')
-
-      modal.handleUpdate()
-
-      expect(spy).toHaveBeenCalled()
-    })
-  })
-
-  describe('data-api', () => {
-    it('should toggle modal', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
-          '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toEqual('true')
-          expect(modalEl.getAttribute('role')).toEqual('dialog')
-          expect(modalEl.getAttribute('aria-hidden')).toBeNull()
-          expect(modalEl.style.display).toEqual('block')
-          expect(document.querySelector('.modal-backdrop')).not.toBeNull()
-          setTimeout(() => trigger.click(), 10)
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toBeNull()
-          expect(modalEl.getAttribute('role')).toBeNull()
-          expect(modalEl.getAttribute('aria-hidden')).toEqual('true')
-          expect(modalEl.style.display).toEqual('none')
-          expect(document.querySelector('.modal-backdrop')).toBeNull()
-          resolve()
-        })
-
-        trigger.click()
-      })
-    })
-
-    it('should not recreate a new modal', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<button type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"></button>',
-          '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const modal = new Modal(modalEl)
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        const spy = spyOn(modal, 'show').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          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="modal" href="#" data-bs-target="#exampleModal"></a>',
-          '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          expect(modalEl.getAttribute('aria-modal')).toEqual('true')
-          expect(modalEl.getAttribute('role')).toEqual('dialog')
-          expect(modalEl.getAttribute('aria-hidden')).toBeNull()
-          expect(modalEl.style.display).toEqual('block')
-          expect(document.querySelector('.modal-backdrop')).not.toBeNull()
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        trigger.click()
-      })
-    })
-
-    it('should focus the trigger on hide', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
-          '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        const spy = spyOn(trigger, 'focus')
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const modal = Modal.getInstance(modalEl)
-
-          modal.hide()
-        })
-
-        const hideListener = () => {
-          setTimeout(() => {
-            expect(spy).toHaveBeenCalled()
-            resolve()
-          }, 20)
-        }
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          hideListener()
-        })
-
-        trigger.click()
-      })
-    })
-
-    it('should open modal, having special characters in its id', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#j_id22:exampleModal">',
-          '   Launch demo modal',
-          '</button>',
-          '<div class="modal fade" id="j_id22:exampleModal" aria-labelledby="exampleModalLabel" aria-hidden="true">',
-          '  <div class="modal-dialog">',
-          '    <div class="modal-content">',
-          '      <div class="modal-body">',
-          '        <p>modal body</p>',
-          '      </div>',
-          '    </div>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          resolve()
-        })
-
-        trigger.click()
-      })
-    })
-
-    it('should not prevent default when a click occurred on data-bs-dismiss="modal" where tagName is DIFFERENT than <a> or <area>', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal">',
-          '  <div class="modal-dialog">',
-          '    <button type="button" data-bs-dismiss="modal"></button>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const btnClose = fixtureEl.querySelector('button[data-bs-dismiss="modal"]')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          btnClose.click()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).not.toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-
-    it('should prevent default when a click occurred on data-bs-dismiss="modal" where tagName is <a> or <area>', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<div class="modal">',
-          '  <div class="modal-dialog">',
-          '    <a type="button" data-bs-dismiss="modal"></a>',
-          '  </div>',
-          '</div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const btnClose = fixtureEl.querySelector('a[data-bs-dismiss="modal"]')
-        const modal = new Modal(modalEl)
-
-        const spy = spyOn(Event.prototype, 'preventDefault').and.callThrough()
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          btnClose.click()
-        })
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          expect(spy).toHaveBeenCalled()
-          resolve()
-        })
-
-        modal.show()
-      })
-    })
-    it('should not focus the trigger if the modal is not visible', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal" style="display: none;"></a>',
-          '<div id="exampleModal" class="modal" style="display: none;"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        const spy = spyOn(trigger, 'focus')
-
-        modalEl.addEventListener('shown.bs.modal', () => {
-          const modal = Modal.getInstance(modalEl)
-
-          modal.hide()
-        })
-
-        const hideListener = () => {
-          setTimeout(() => {
-            expect(spy).not.toHaveBeenCalled()
-            resolve()
-          }, 20)
-        }
-
-        modalEl.addEventListener('hidden.bs.modal', () => {
-          hideListener()
-        })
-
-        trigger.click()
-      })
-    })
-    it('should not focus the trigger if the modal is not shown', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<a data-bs-toggle="modal" href="#" data-bs-target="#exampleModal"></a>',
-          '<div id="exampleModal" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const modalEl = fixtureEl.querySelector('.modal')
-        const trigger = fixtureEl.querySelector('[data-bs-toggle="modal"]')
-
-        const spy = spyOn(trigger, 'focus')
-
-        const showListener = () => {
-          setTimeout(() => {
-            expect(spy).not.toHaveBeenCalled()
-            resolve()
-          }, 10)
-        }
-
-        modalEl.addEventListener('show.bs.modal', event => {
-          event.preventDefault()
-          showListener()
-        })
-
-        trigger.click()
-      })
-    })
-
-    it('should call hide first, if another modal is open', () => {
-      return new Promise(resolve => {
-        fixtureEl.innerHTML = [
-          '<button data-bs-toggle="modal"  data-bs-target="#modal2"></button>',
-          '<div id="modal1" class="modal fade"><div class="modal-dialog"></div></div>',
-          '<div id="modal2" class="modal"><div class="modal-dialog"></div></div>'
-        ].join('')
-
-        const trigger2 = fixtureEl.querySelector('button')
-        const modalEl1 = document.querySelector('#modal1')
-        const modalEl2 = document.querySelector('#modal2')
-        const modal1 = new Modal(modalEl1)
-
-        modalEl1.addEventListener('shown.bs.modal', () => {
-          trigger2.click()
-        })
-        modalEl1.addEventListener('hidden.bs.modal', () => {
-          expect(Modal.getInstance(modalEl2)).not.toBeNull()
-          expect(modalEl2).toHaveClass('show')
-          resolve()
-        })
-        modal1.show()
-      })
-    })
-  })
-
-  describe('getInstance', () => {
-    it('should return modal instance', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const div = fixtureEl.querySelector('div')
-      const modal = new Modal(div)
-
-      expect(Modal.getInstance(div)).toEqual(modal)
-      expect(Modal.getInstance(div)).toBeInstanceOf(Modal)
-    })
-
-    it('should return null when there is no modal instance', () => {
-      fixtureEl.innerHTML = '<div class="modal"><div class="modal-dialog"></div></div>'
-
-      const div = fixtureEl.querySelector('div')
-
-      expect(Modal.getInstance(div)).toBeNull()
-    })
-  })
-
-  describe('getOrCreateInstance', () => {
-    it('should return modal instance', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-      const modal = new Modal(div)
-
-      expect(Modal.getOrCreateInstance(div)).toEqual(modal)
-      expect(Modal.getInstance(div)).toEqual(Modal.getOrCreateInstance(div, {}))
-      expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
-    })
-
-    it('should return new instance when there is no modal instance', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-
-      expect(Modal.getInstance(div)).toBeNull()
-      expect(Modal.getOrCreateInstance(div)).toBeInstanceOf(Modal)
-    })
-
-    it('should return new instance when there is no modal instance with given configuration', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-
-      expect(Modal.getInstance(div)).toBeNull()
-      const modal = Modal.getOrCreateInstance(div, {
-        backdrop: true
-      })
-      expect(modal).toBeInstanceOf(Modal)
-
-      expect(modal._config.backdrop).toBeTrue()
-    })
-
-    it('should return the instance when exists without given configuration', () => {
-      fixtureEl.innerHTML = '<div></div>'
-
-      const div = fixtureEl.querySelector('div')
-      const modal = new Modal(div, {
-        backdrop: true
-      })
-      expect(Modal.getInstance(div)).toEqual(modal)
-
-      const modal2 = Modal.getOrCreateInstance(div, {
-        backdrop: false
-      })
-      expect(modal).toBeInstanceOf(Modal)
-      expect(modal2).toEqual(modal)
-
-      expect(modal2._config.backdrop).toBeTrue()
-    })
-  })
-})
diff --git a/js/tests/visual/dialog.html b/js/tests/visual/dialog.html
new file mode 100644 (file)
index 0000000..68ed336
--- /dev/null
@@ -0,0 +1,207 @@
+<!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 &lt;body&gt; 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>
diff --git a/js/tests/visual/modal.html b/js/tests/visual/modal.html
deleted file mode 100644 (file)
index efb5127..0000000
+++ /dev/null
@@ -1,279 +0,0 @@
-<!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>Modal</title>
-    <style>
-      #tall {
-        height: 1500px;
-        width: 100px;
-      }
-    </style>
-  </head>
-  <body>
-    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
-      <div class="container-fluid">
-        <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
-          <span class="navbar-toggler-icon"></span>
-        </button>
-        <div class="collapse navbar-collapse" id="navbarResponsive">
-          <a class="navbar-brand" href="#">This shouldn't jump!</a>
-          <ul class="navbar-nav">
-            <li class="nav-item">
-              <a class="nav-link active" href="#" aria-current="page">Home</a>
-            </li>
-            <li class="nav-item">
-              <a class="nav-link" href="#">Link</a>
-            </li>
-            <li class="nav-item">
-              <a class="nav-link" href="#">Link</a>
-            </li>
-          </ul>
-        </div>
-      </div>
-    </nav>
-
-    <div class="container mt-3">
-      <h1>Modal <small>Bootstrap Visual Test</small></h1>
-
-      <div class="modal fade" id="myModal" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
-        <div class="modal-dialog">
-          <div class="modal-content">
-            <div class="modal-header">
-              <h1 class="modal-title fs-4" id="myModalLabel">Modal title</h1>
-              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-            </div>
-            <div class="modal-body">
-              <h4>Text in a modal</h4>
-              <p>Duis mollis, est non commodo luctus, nisi erat porttitor ligula.</p>
-
-              <h4>Popover in a modal</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 modal</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>
-
-              <div id="accordion" role="tablist">
-                <div class="card" role="presentation">
-                  <div class="card-header" role="tab" id="headingOne">
-                    <h5 class="mb-0">
-                      <a data-bs-toggle="collapse" href="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
-                        Collapsible Group Item #1
-                      </a>
-                    </h5>
-                  </div>
-
-                  <div id="collapseOne" class="collapse show" data-bs-parent="#accordion" role="tabpanel" aria-labelledby="headingOne">
-                    <div class="card-body">
-                      Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
-                    </div>
-                  </div>
-                </div>
-                <div class="card" role="presentation">
-                  <div class="card-header" role="tab" id="headingTwo">
-                    <h5 class="mb-0">
-                      <a class="collapsed" data-bs-toggle="collapse" href="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
-                        Collapsible Group Item #2
-                      </a>
-                    </h5>
-                  </div>
-                  <div id="collapseTwo" class="collapse" data-bs-parent="#accordion" role="tabpanel" aria-labelledby="headingTwo">
-                    <div class="card-body">
-                      Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
-                    </div>
-                  </div>
-                </div>
-                <div class="card" role="presentation">
-                  <div class="card-header" role="tab" id="headingThree">
-                    <h5 class="mb-0">
-                      <a class="collapsed" data-bs-toggle="collapse" href="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
-                        Collapsible Group Item #3
-                      </a>
-                    </h5>
-                  </div>
-                  <div id="collapseThree" class="collapse" data-bs-parent="#accordion" role="tabpanel" aria-labelledby="headingThree">
-                    <div class="card-body">
-                      Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus labore sustainable VHS.
-                    </div>
-                  </div>
-                </div>
-              </div>
-
-              <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>
-              <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>
-            </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 class="modal fade" id="firefoxModal" tabindex="-1" aria-labelledby="firefoxModalLabel" aria-hidden="true">
-        <div class="modal-dialog">
-          <div class="modal-content">
-            <div class="modal-header">
-              <h1 class="modal-title fs-4" id="firefoxModalLabel">Firefox Bug Test</h1>
-              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-            </div>
-            <div class="modal-body">
-              <ol>
-                <li>Ensure you're using Firefox.</li>
-                <li>Open a new tab and then switch back to this tab.</li>
-                <li>Click into this input: <input type="text" id="ff-bug-input"></li>
-                <li>Switch to the other tab and then back to this tab.</li>
-              </ol>
-              <p>Test result: <strong id="ff-bug-test-result"></strong></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 class="modal fade" id="slowModal" tabindex="-1" aria-labelledby="slowModalLabel" aria-hidden="true" style="transition-duration: 5s;">
-        <div class="modal-dialog" style="transition-duration: inherit;">
-          <div class="modal-content">
-            <div class="modal-header">
-              <h1 class="modal-title fs-4" id="slowModalLabel">Lorem slowly</h1>
-              <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-            </div>
-            <div class="modal-body">
-              <p>Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Duis mollis, est non commodo luctus, nisi erat porttitor ligula, eget lacinia odio sem nec elit. Donec sed odio dui. Nullam quis risus eget urna mollis ornare vel eu leo. Nulla vitae elit libero, a pharetra augue.</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>
-
-      <button type="button" class="btn btn-primary btn-lg" data-bs-toggle="modal" data-bs-target="#myModal">
-        Launch demo modal
-      </button>
-
-      <button type="button" class="btn btn-primary btn-lg" id="tall-toggle">
-        Toggle tall &lt;body&gt; content
-      </button>
-
-      <br><br>
-
-      <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#firefoxModal">
-        Launch Firefox bug test modal
-      </button>
-      (<a href="https://github.com/twbs/bootstrap/issues/18365">See Issue #18365</a>)
-
-      <br><br>
-
-      <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#slowModal">
-        Launch modal with slow transition
-      </button>
-
-      <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>
-
-      <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="&#x3C;div class=&#x22;modal fade the-bad&#x22; tabindex=&#x22;-1&#x22;&#x3E;&#x3C;div class=&#x22;modal-dialog&#x22;&#x3E;&#x3C;div class=&#x22;modal-content&#x22;&#x3E;&#x3C;div class=&#x22;modal-header&#x22;&#x3E;&#x3C;button type=&#x22;button&#x22; class=&#x22;btn-close&#x22; data-bs-dismiss=&#x22;modal&#x22; aria-label=&#x22;Close&#x22;&#x3E;&#x3C;span aria-hidden=&#x22;true&#x22;&#x3E;&#x26;times;&#x3C;/span&#x3E;&#x3C;/button&#x3E;&#x3C;h1 class=&#x22;modal-title fs-4&#x22;&#x3E;The Bad Modal&#x3C;/h1&#x3E;&#x3C;/div&#x3E;&#x3C;div class=&#x22;modal-body&#x22;&#x3E;This modal&#x27;s HTTML source code is declared inline, inside the data-bs-target attribute of it&#x27;s show-button&#x3C;/div&#x3E;&#x3C;/div&#x3E;&#x3C;/div&#x3E;&#x3C;/div&#x3E;">
-        Modal with an XSS inside the data-bs-target
-      </button>
-
-      <br><br>
-
-      <button type="button" class="btn btn-secondary btn-lg" id="btnPreventModal">
-        Launch prevented modal on hide (to see the result open your console)
-      </button>
-    </div>
-
-    <script src="../../../dist/js/bootstrap.bundle.js"></script>
-    <script>
-      /* global bootstrap: false */
-
-      const ffBugTestResult = document.getElementById('ff-bug-test-result')
-      const firefoxTestDone = false
-
-      function reportFirefoxTestResult(result) {
-        if (!firefoxTestDone) {
-          ffBugTestResult.classList.add(result ? 'text-success' : 'text-danger')
-          ffBugTestResult.textContent = result ? 'PASS' : 'FAIL'
-        }
-      }
-
-      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'
-      })
-
-      const ffBugInput = document.getElementById('ff-bug-input')
-      const firefoxModal = document.getElementById('firefoxModal')
-      function handlerClickFfBugInput() {
-        firefoxModal.addEventListener('focus', reportFirefoxTestResult.bind(false))
-        ffBugInput.addEventListener('focus', reportFirefoxTestResult.bind(true))
-        ffBugInput.removeEventListener('focus', handlerClickFfBugInput)
-      }
-
-      ffBugInput.addEventListener('focus', handlerClickFfBugInput)
-
-      const modalFf = new bootstrap.Modal(firefoxModal)
-
-      document.getElementById('btnPreventModal').addEventListener('click', () => {
-        const shownFirefoxModal = () => {
-          modalFf.hide()
-          firefoxModal.removeEventListener('shown.bs.modal', hideFirefoxModal)
-        }
-
-        const hideFirefoxModal = event => {
-          event.preventDefault()
-          firefoxModal.removeEventListener('hide.bs.modal', hideFirefoxModal)
-
-          if (modalFf._isTransitioning) {
-            console.error('Modal plugin should not set _isTransitioning when hide event is prevented')
-          } else {
-            console.log('Test passed')
-            modalFf.hide() // work as expected
-          }
-        }
-
-        firefoxModal.addEventListener('shown.bs.modal', shownFirefoxModal)
-        firefoxModal.addEventListener('hide.bs.modal', hideFirefoxModal)
-        modalFf.show()
-      })
-
-      // Test transition duration
-      let t0
-      let t1
-      const slowModal = document.getElementById('slowModal')
-
-      slowModal.addEventListener('shown.bs.modal', () => {
-        t1 = performance.now()
-        console.log(`transition-duration took ${t1 - t0}ms.`)
-      })
-
-      slowModal.addEventListener('show.bs.modal', () => {
-        t0 = performance.now()
-      })
-    </script>
-  </body>
-</html>
diff --git a/scss/_dialog.scss b/scss/_dialog.scss
new file mode 100644 (file)
index 0000000..9989dba
--- /dev/null
@@ -0,0 +1,234 @@
+@use "sass:map";
+@use "config" as *;
+@use "variables" as *;
+@use "layout/breakpoints" 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-width-sm:               280px !default;
+$dialog-width-lg:               800px !default;
+$dialog-width-xl:               1140px !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 sizes
+  .dialog-sm { --#{$prefix}dialog-width: #{$dialog-width-sm}; }
+  .dialog-lg { --#{$prefix}dialog-width: #{$dialog-width-lg}; }
+  .dialog-xl { --#{$prefix}dialog-width: #{$dialog-width-xl}; }
+
+  // Fullscreen dialog
+  .dialog-fullscreen {
+    --#{$prefix}dialog-width: 100vw;
+    --#{$prefix}dialog-margin: 0;
+    --#{$prefix}dialog-border-radius: 0;
+
+    width: 100%;
+    max-width: none;
+    height: 100%;
+    max-height: none;
+  }
+
+  // Responsive fullscreen dialogs
+  @each $breakpoint in map.keys($grid-breakpoints) {
+    $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
+    $postfix: if($infix != "", #{$infix}-down, "");
+
+    @if $postfix != "" {
+      @include media-breakpoint-down($breakpoint) {
+        .dialog-fullscreen#{$postfix} {
+          --#{$prefix}dialog-width: 100vw;
+          --#{$prefix}dialog-margin: 0;
+          --#{$prefix}dialog-border-radius: 0;
+
+          width: 100%;
+          max-width: none;
+          height: 100%;
+          max-height: none;
+        }
+      }
+    }
+  }
+
+  // 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);
+  }
+}
diff --git a/scss/_modal.scss b/scss/_modal.scss
deleted file mode 100644 (file)
index b6cee70..0000000
+++ /dev/null
@@ -1,248 +0,0 @@
-@use "sass:map";
-@use "config" as *;
-@use "variables" as *;
-@use "mixins/border-radius" as *;
-@use "mixins/box-shadow" as *;
-@use "mixins/transition" as *;
-@use "mixins/gradients" as *;
-@use "mixins/backdrop" as *;
-@use "vendor/rfs" as *;
-@use "layout/breakpoints" as *;
-
-// .modal-open      - body class for killing the scroll
-// .modal           - container to scroll within
-// .modal-dialog    - positioning shell for the actual modal
-// .modal-content   - actual modal w/ bg and corners and stuff
-
-@layer components {
-  // Container that the modal scrolls within
-  .modal {
-    // scss-docs-start modal-css-vars
-    --#{$prefix}modal-zindex: #{$zindex-modal};
-    --#{$prefix}modal-width: #{$modal-md};
-    --#{$prefix}modal-padding: #{$modal-inner-padding};
-    --#{$prefix}modal-margin: #{$modal-dialog-margin};
-    --#{$prefix}modal-color: #{$modal-content-color};
-    --#{$prefix}modal-bg: #{$modal-content-bg};
-    --#{$prefix}modal-border-color: #{$modal-content-border-color};
-    --#{$prefix}modal-border-width: #{$modal-content-border-width};
-    --#{$prefix}modal-border-radius: #{$modal-content-border-radius};
-    --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-xs};
-    --#{$prefix}modal-inner-border-radius: #{$modal-content-inner-border-radius};
-    --#{$prefix}modal-header-padding-x: #{$modal-header-padding-x};
-    --#{$prefix}modal-header-padding-y: #{$modal-header-padding-y};
-    --#{$prefix}modal-header-padding: #{$modal-header-padding}; // Todo in v6: Split this padding into x and y
-    --#{$prefix}modal-header-border-color: #{$modal-header-border-color};
-    --#{$prefix}modal-header-border-width: #{$modal-header-border-width};
-    --#{$prefix}modal-title-line-height: #{$modal-title-line-height};
-    --#{$prefix}modal-footer-gap: #{$modal-footer-margin-between};
-    --#{$prefix}modal-footer-bg: #{$modal-footer-bg};
-    --#{$prefix}modal-footer-border-color: #{$modal-footer-border-color};
-    --#{$prefix}modal-footer-border-width: #{$modal-footer-border-width};
-    // scss-docs-end modal-css-vars
-
-    position: fixed;
-    inset: 0 auto auto 0;
-    z-index: var(--#{$prefix}modal-zindex);
-    display: none;
-    width: 100%;
-    height: 100%;
-    overflow-x: hidden;
-    overflow-y: auto;
-    // Prevent Chrome on Windows from adding a focus outline. For details, see
-    // https://github.com/twbs/bootstrap/pull/10951.
-    outline: 0;
-    // We deliberately don't use `-webkit-overflow-scrolling: touch;` due to a
-    // gnarly iOS Safari bug: https://bugs.webkit.org/show_bug.cgi?id=158342
-    // See also https://github.com/twbs/bootstrap/issues/17695
-  }
-
-  // Shell div to position the modal with bottom padding
-  .modal-dialog {
-    position: relative;
-    width: auto;
-    margin: var(--#{$prefix}modal-margin);
-    // allow clicks to pass through for custom click handling to close modal
-    pointer-events: none;
-
-    // When fading in the modal, animate it to slide down
-    .modal.fade & {
-      transform: $modal-fade-transform;
-      @include transition($modal-transition);
-    }
-    .modal.show & {
-      transform: $modal-show-transform;
-    }
-
-    // When trying to close, animate focus to scale
-    .modal.modal-static & {
-      transform: $modal-scale-transform;
-    }
-  }
-
-  .modal-dialog-scrollable {
-    height: calc(100% - var(--#{$prefix}modal-margin) * 2);
-
-    .modal-content {
-      max-height: 100%;
-      overflow: hidden;
-    }
-
-    .modal-body {
-      overflow-y: auto;
-    }
-  }
-
-  .modal-dialog-centered {
-    display: flex;
-    align-items: center;
-    min-height: calc(100% - var(--#{$prefix}modal-margin) * 2);
-  }
-
-  // Actual modal
-  .modal-content {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    width: 100%; // Ensure `.modal-content` extends the full width of the parent `.modal-dialog`
-    // counteract the pointer-events: none; in the .modal-dialog
-    color: var(--#{$prefix}modal-color);
-    pointer-events: auto;
-    background-color: var(--#{$prefix}modal-bg);
-    background-clip: padding-box;
-    border: var(--#{$prefix}modal-border-width) solid var(--#{$prefix}modal-border-color);
-    @include border-radius(var(--#{$prefix}modal-border-radius));
-    @include box-shadow(var(--#{$prefix}modal-box-shadow));
-    // Remove focus outline from opened modal
-    outline: 0;
-  }
-
-  // Modal background
-  .modal-backdrop {
-    // scss-docs-start modal-backdrop-css-vars
-    --#{$prefix}backdrop-zindex: #{$zindex-modal-backdrop};
-    --#{$prefix}backdrop-bg: #{$modal-backdrop-bg};
-    --#{$prefix}backdrop-opacity: #{$modal-backdrop-opacity};
-    // scss-docs-end modal-backdrop-css-vars
-
-    @include overlay-backdrop(var(--#{$prefix}backdrop-zindex), var(--#{$prefix}backdrop-bg), var(--#{$prefix}backdrop-opacity));
-  }
-
-  // Modal header
-  // Top section of the modal w/ title and dismiss
-  .modal-header {
-    display: flex;
-    flex-shrink: 0;
-    align-items: center;
-    padding: var(--#{$prefix}modal-header-padding);
-    border-block-end: var(--#{$prefix}modal-header-border-width) solid var(--#{$prefix}modal-header-border-color);
-    @include border-top-radius(var(--#{$prefix}modal-inner-border-radius));
-
-    .btn-close {
-      padding: calc(var(--#{$prefix}modal-header-padding-y) * .5) calc(var(--#{$prefix}modal-header-padding-x) * .5);
-      // Split properties to avoid invalid calc() function if value is 0
-      margin-inline-start: auto;
-      margin-inline-end: calc(-.5 * var(--#{$prefix}modal-header-padding-x));
-      margin-top: calc(-.5 * var(--#{$prefix}modal-header-padding-y));
-      margin-bottom: calc(-.5 * var(--#{$prefix}modal-header-padding-y));
-    }
-  }
-
-  // Title text within header
-  .modal-title {
-    margin-bottom: 0;
-    line-height: var(--#{$prefix}modal-title-line-height);
-  }
-
-  // Modal body
-  // Where all modal content resides (sibling of .modal-header and .modal-footer)
-  .modal-body {
-    position: relative;
-    // Enable `flex-grow: 1` so that the body take up as much space as possible
-    // when there should be a fixed height on `.modal-dialog`.
-    flex: 1 1 auto;
-    padding: var(--#{$prefix}modal-padding);
-  }
-
-  // Footer (for actions)
-  .modal-footer {
-    display: flex;
-    flex-shrink: 0;
-    flex-wrap: wrap;
-    align-items: center; // vertically center
-    justify-content: flex-end; // Right align buttons with flex property because text-align doesn't work on flex items
-    padding: calc(var(--#{$prefix}modal-padding) - var(--#{$prefix}modal-footer-gap) * .5);
-    background-color: var(--#{$prefix}modal-footer-bg);
-    border-block-start: var(--#{$prefix}modal-footer-border-width) solid var(--#{$prefix}modal-footer-border-color);
-    @include border-bottom-radius(var(--#{$prefix}modal-inner-border-radius));
-
-    // Place margin between footer elements
-    // This solution is far from ideal because of the universal selector usage,
-    // but is needed to fix https://github.com/twbs/bootstrap/issues/24800
-    > * {
-      margin: calc(var(--#{$prefix}modal-footer-gap) * .5); // Todo in v6: replace with gap on parent class
-    }
-  }
-
-  // Scale up the modal
-  @include media-breakpoint-up(sm) {
-    .modal {
-      --#{$prefix}modal-margin: #{$modal-dialog-margin-y-sm-up};
-      --#{$prefix}modal-box-shadow: #{$modal-content-box-shadow-sm-up};
-    }
-
-    // Automatically set modal's width for larger viewports
-    .modal-dialog {
-      max-width: var(--#{$prefix}modal-width);
-      margin-inline: auto;
-    }
-
-    .modal-sm {
-      --#{$prefix}modal-width: #{$modal-sm};
-    }
-  }
-
-  @include media-breakpoint-up(lg) {
-    .modal-lg,
-    .modal-xl {
-      --#{$prefix}modal-width: #{$modal-lg};
-    }
-  }
-
-  @include media-breakpoint-up(xl) {
-    .modal-xl {
-      --#{$prefix}modal-width: #{$modal-xl};
-    }
-  }
-
-  // scss-docs-start modal-fullscreen-loop
-  @each $breakpoint in map.keys($grid-breakpoints) {
-    $infix: breakpoint-infix($breakpoint, $grid-breakpoints);
-    $postfix: if($infix != "", $infix + "-down", "");
-
-    @include media-breakpoint-down($breakpoint) {
-      .modal-fullscreen#{$postfix} {
-        width: 100vw;
-        max-width: none;
-        height: 100%;
-        margin: 0;
-
-        .modal-content {
-          height: 100%;
-          border: 0;
-          @include border-radius(0);
-        }
-
-        .modal-header,
-        .modal-footer {
-          @include border-radius(0);
-        }
-
-        .modal-body {
-          overflow-y: auto;
-        }
-      }
-    }
-  }
-  // scss-docs-end modal-fullscreen-loop
-}
index 7a657e2f0c117b4fead5271ba1a5f56bb3cc5d22..cffadfe33c7bddeb4352c1a657f69d941aeb2954 100644 (file)
@@ -7,7 +7,7 @@
 // Variables
 //
 // Variables should follow the `$component-state-property-size` formula for
-// consistent naming. Ex: $nav-link-disabled-color and $modal-content-box-shadow-xs.
+// consistent naming. Ex: $nav-link-disabled-color and $btn-padding-y-lg.
 
 // scss-docs-start theme-color-variables
 // $primary:       $blue-500 !default;
@@ -428,8 +428,7 @@ $zindex-sticky:                     1020 !default;
 $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;
@@ -446,67 +445,22 @@ $zindex-levels: (
 // scss-docs-end zindex-levels-map
 
 
-// Modals
-
-// scss-docs-start modal-variables
-$modal-inner-padding:               $spacer !default;
-
-$modal-footer-margin-between:       .5rem !default;
-
-$modal-dialog-margin:               .5rem !default;
-$modal-dialog-margin-y-sm-up:       1.75rem !default;
-
-$modal-title-line-height:           $line-height-base !default;
-
-$modal-content-color:               var(--#{$prefix}color-body) !default;
-$modal-content-bg:                  var(--#{$prefix}bg-body) !default;
-$modal-content-border-color:        var(--#{$prefix}border-color-translucent) !default;
-$modal-content-border-width:        var(--#{$prefix}border-width) !default;
-$modal-content-border-radius:       var(--#{$prefix}border-radius-lg) !default;
-$modal-content-inner-border-radius: calc(#{$modal-content-border-radius} - #{$modal-content-border-width}) !default;
-$modal-content-box-shadow-xs:       var(--#{$prefix}box-shadow-sm) !default;
-$modal-content-box-shadow-sm-up:    var(--#{$prefix}box-shadow) !default;
-
-$modal-backdrop-bg:                 $black !default;
-$modal-backdrop-opacity:            .5 !default;
-
-$modal-header-border-color:         var(--#{$prefix}border-color) !default;
-$modal-header-border-width:         $modal-content-border-width !default;
-$modal-header-padding-y:            $modal-inner-padding !default;
-$modal-header-padding-x:            $modal-inner-padding !default;
-$modal-header-padding:              $modal-header-padding-y $modal-header-padding-x !default; // Keep this for backwards compatibility
-
-$modal-footer-bg:                   null !default;
-$modal-footer-border-color:         $modal-header-border-color !default;
-$modal-footer-border-width:         $modal-header-border-width !default;
-
-$modal-sm:                          300px !default;
-$modal-md:                          500px !default;
-$modal-lg:                          800px !default;
-$modal-xl:                          1200px !default;
-
-$modal-fade-transform:              translate(0, -50px) !default;
-$modal-show-transform:              none !default;
-$modal-transition:                  transform .3s ease-out !default;
-$modal-scale-transform:             scale(1.02) !default;
-// scss-docs-end modal-variables
-
 // Offcanvas
 
 // scss-docs-start offcanvas-variables
-$offcanvas-padding-y:               $modal-inner-padding !default;
-$offcanvas-padding-x:               $modal-inner-padding !default;
+$offcanvas-padding-y:               $spacer !default;
+$offcanvas-padding-x:               $spacer !default;
 $offcanvas-horizontal-width:        400px !default;
 $offcanvas-vertical-height:         30vh !default;
 $offcanvas-transition-duration:     .3s !default;
-$offcanvas-border-color:            $modal-content-border-color !default;
-$offcanvas-border-width:            $modal-content-border-width !default;
-$offcanvas-title-line-height:       $modal-title-line-height !default;
+$offcanvas-border-color:            var(--#{$prefix}border-color-translucent) !default;
+$offcanvas-border-width:            var(--#{$prefix}border-width) !default;
+$offcanvas-title-line-height:       $line-height-base !default;
 $offcanvas-bg-color:                var(--#{$prefix}bg-body) !default;
 $offcanvas-color:                   var(--#{$prefix}color-body) !default;
-$offcanvas-box-shadow:              $modal-content-box-shadow-xs !default;
-$offcanvas-backdrop-bg:             $modal-backdrop-bg !default;
-$offcanvas-backdrop-opacity:        $modal-backdrop-opacity !default;
+$offcanvas-box-shadow:              var(--#{$prefix}box-shadow-sm) !default;
+$offcanvas-backdrop-bg:             $black !default;
+$offcanvas-backdrop-opacity:        .5 !default;
 // scss-docs-end offcanvas-variables
 
 // Code
index 57f8ed03bc309768ef531a11b49cfdacf38613e9..52fc04d4b1793e3aa68bfd951cad4d5d4552df17 100644 (file)
@@ -17,9 +17,9 @@
 @forward "breadcrumb";
 @forward "card";
 @forward "carousel";
+@forward "dialog";
 @forward "dropdown";
 @forward "list-group";
-@forward "modal";
 @forward "nav";
 @forward "navbar";
 @forward "offcanvas";
index ad9c5d2cc247332d2e1a825066f159b49a040304..30d24da58b0fcae9fb35e51ca2793bad77d1e7c1 100644 (file)
@@ -53,8 +53,8 @@
       description: 'Enhance your dropdowns with filters, icons, custom styles, and more.'
     - name: List groups
       description: 'Extend list groups with utilities and custom styles for any content.'
-    - name: Modals
-      description: 'Transform modals to serve any purpose, from feature tours to dialogs.'
+    - name: Dialogs
+      description: 'Transform dialogs to serve any purpose, from feature tours to confirmations.'
     - name: Badges
       description: 'Make badges work with custom inner HTML and new looks.'
     - name: Breadcrumbs
index 53a6f200afeecdd2a521c4e3cb774ce0b3e19c9b..2cbfc909997cadea657306735ca0258bc61e44d7 100644 (file)
@@ -92,9 +92,9 @@
     - title: Carousel
     - title: Close button
     - title: Collapse
+    - title: Dialog
     - title: Dropdowns
     - title: List group
-    - title: Modal
     - title: Navbar
     - title: Navs & tabs
     - title: Offcanvas
index 66a2ce8ef78ad09c2f64f0f98b00db16d48477ea..eb494e1572b62ed589f836a97d862f5405c7d343 100644 (file)
@@ -57,7 +57,6 @@ import Placeholder from "@shortcodes/Placeholder.astro"
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#carousel">شرائح العرض</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#dropdowns">القوائم المنسدلة</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#list-group">مجموعة العناصر</a></li>
-          <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#modal">الصندوق العائم</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#navs">التنقل</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#navbar">شريط التنقل</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#pagination">ترقيم الصفحات</a></li>
@@ -1118,31 +1117,6 @@ import Placeholder from "@shortcodes/Placeholder.astro"
         `]} />
       </div>
     </article>
-    <article class="my-3" id="modal">
-      <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
-        <h3>الصندوق العائم</h3>
-        <a class="d-flex align-items-center" hreflang="en" href={getVersionedDocsPath('components/modal')}>دليل الإستخدام</a>
-      </div>
-
-      <div>
-        <Example showMarkup={false} code={`
-        <div class="d-flex justify-content-between flex-wrap">
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalDefault">
-            إطلاق صندوق عائم تجريبي
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdropLive">
-            إطلاق صندوق عائم عالق
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalCenteredScrollable">
-            صندوق عائم متنصف عاموديًا وقابل للتمرير
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreen">
-            صندوق عائم يملأ الشاشة
-          </button>
-        </div>
-        `} />
-      </div>
-    </article>
     <article class="my-3" id="navs">
       <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
         <h3>التنقل</h3>
@@ -1514,85 +1488,3 @@ import Placeholder from "@shortcodes/Placeholder.astro"
     </article>
   </section>
 </main>
-
-<div class="modal fade" id="exampleModalDefault" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalLabel">عنوان الصندوق العائم</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button>
-        <button type="button" class="btn btn-primary">حفظ التغيرات</button>
-      </div>
-    </div>
-  </div>
-</div>
-<div class="modal fade" id="staticBackdropLive" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLiveLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="staticBackdropLiveLabel">عنوان الصندوق العائم</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
-      </div>
-      <div class="modal-body">
-        <p>لن أغلق إذا نقرت خارجي. لا تحاول حتى الضغط على مفتاح الهروب.</p>
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button>
-        <button type="button" class="btn btn-primary">حسنًا</button>
-      </div>
-    </div>
-  </div>
-</div>
-<div class="modal fade" id="exampleModalCenteredScrollable" tabindex="-1" aria-labelledby="exampleModalCenteredScrollableTitle" aria-hidden="true">
-  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalCenteredScrollableTitle">عنوان الصندوق العائم</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
-      </div>
-      <div class="modal-body">
-        <p>نص لتوضيح عمل الصندوق العائم المتنصّف عاموديًا القابل للتمرير</p>
-        <p>في هذه الحالة، هذا الصندوق يحتوي على محتوى أكثر قليلًا، ولذلك لتوضيح كيف  يمكن إضافة خاصية الإنتصاف العامودي لصندوق عائم قابل للتمرير.</p>
-        <p>يعتبر كوب أو فنجان القهوة عادة يومية عند غالبية سكان الكرة الأرضية في الصباح والمساء، وفي حين تكثر الدراسات حول إيجابياتها هناك أيضا سلبيات كثيرة للمشروب المفضل للكثيرين.</p>
-        <p>وفي هذا الشأن، أظهرت دراسة جديدة أن تناول الكافيين بانتظام يقلل من حجم المادة الرمادية في الدماغ، مما يشير إلى أن تناول القهوة يمكن أن يضعف القدرة على معالجة المعلومات، وفقاً لما نشرته "ديلي ميل" البريطانية.</p>
-        <p>وأعطى باحثون سويسريون متطوعين ثلاث حصص من الكافيين 150 ملغم يوميًا لمدة 10 أيام - وهو مقدار كافيين يعادل حوالي أربعة أو خمسة أكواب صغيرة من القهوة المخمرة يوميًا، أو سبعة إسبريسو فردي.</p>
-        <p>وتبين حدوث انخفاض في المادة الرمادية، والتي توجد غالبًا في الطبقة الخارجية للدماغ، أو القشرة، وتعمل على معالجة المعلومات.</p>
-        <p>كما كان الانخفاض مذهلاً بشكل خاص في الفص الصدغي الإنسي الأيمن، بما في ذلك الحُصين، وهي منطقة من الدماغ ضرورية لتقوية الذاكرة.</p>
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button>
-        <button type="button" class="btn btn-primary">حفظ التغيرات</button>
-      </div>
-    </div>
-  </div>
-</div>
-<div class="modal fade" id="exampleModalFullscreen" tabindex="-1" aria-labelledby="exampleModalFullscreenLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenLabel">صندوق عائم يملأ الشاشة</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="إغلاق"></button>
-      </div>
-      <div class="modal-body">
-        <p>في ما يلي، نص طويل لتعبئة كامل الشاشة. قد يبدو أنني أثرثر كثيرًا، لذا سأقوم بنسخ مقالة من إحدى الصحف العربية.</p>
-        <p>تكييف الهواء هو من التكنولوجيا التي ما إن نمتلكها حتى نصبح عاجزين عن تخيّل العيش من دونها. وتكييف الهواء في مناطق عديدة من العالم ليس ترفاً يمكن الاستغناء عنه. ولكن هذه الحاجة الحيوية تثير تحديات كبيرة على مستوى استهلاك الطاقة في معظم بلدان العالم. ويُتوقع أن تتفاقم هذه التحديات خلال العقود الثلاثة المقبلة، مع توقُّع ارتفاع عدد المكيفات في العالم نحو ثلاثة أضعاف. وفي حين أن الابتكارات التكنولوجية تحقِّق كل يوم إنجازاً جديداً، وعلى الرغم من بعض التحسينات والتدابير المحدودة التي طرأت على صناعة المكيفات، “فإن تكنولوجيتها الأساسية لا تزال تعمل كما كانت، منذ اعتمادها قبل نحو قرن من السنين"، حسبما جاء في تقرير لمعهد ماساشوستس للتكنولوجيا (MIT)، في الأول من سبتمبر 2020م. ولمعالجة هذه المشكلات الخطيرة، لا مفر من إعادة التفكير بجد.</p>
-        <p>التكييف حاجة حيويّة. فالتعرّض للحرارة لمدة طويلة ضارٌ بالصحة. ووفقاً لمنظمة الصحة العالميّة يمكن للتعرُّض الطويل للحرارة أيضاً أن يؤدي إلى "إعياء حراري، وضربة شمس، وتورّم في الرجلين، وطفح جلدي على العنق، وتشنج، وصداع، وحساسيّة، والخمول والوهَن. ويمكن للحرارة أن تسبِّب جفافاً خطراً، وأعراضاً حادة في أوعية الدماغ الدمويّة، وتسهم في تكوّن الجلطات".</p>
-        <p>وموجات الحر أيضاً من أشد المخاطر الطبيعيّة المميتة. إذ يقدَّر أن بين عامي 1998 و2017م، توفي نحو 166 ألف شخص من موجات الحر. ومات 70 ألفاً من هؤلاء في أوروبا سنة 2003م وحدها. ومن دون أن يصل الخطر إلى الموت، تتسبَّب الحرارة العالية بانخفاض الأداء المعرفي لدى الشبان البالغين في المباني غير المكيّفة. فقد بيّنت دراسة أجرتها "كليّة ت. هـ. تشان" للصحة العامة في جامعة هارفرد، نشرتها "بلوس ميديسين" في 10 يوليو 2018م، أن التلاميذ الذين ينامون في مهاجع غير مكيّفة، يقل أداؤهم عن أولئك الذين ينامون في مهاجع مكيّفة. ولتجنّب مخاطر التعرّض للحرارة، يلتفت الناس إلى استخدام التكييف.</p>
-        <p>يتطلّب التكييف كثيراً من الطاقة. فنحو %10 من استهلاك الكهرباء في العالم يُنفَق في تشغيل المكيّفات. وتصل هذه النسبة إلى %50 في المملكة حسب "المركز السعودي لكفاءة الطاقة". أضف إلى ذلك أن معظم البشر في بلدان العالم النامي، لم يقتنوا بعد مكيّفهم الأول، والمشكلة إلى ازدياد. فمعظم البلدان النامية هي من البلدان الأشد حرارة والأكثر اكتظاظاً بالسكان في العالم. وجاء في تقرير لوكالة الطاقة الدوليّة بعنوان "مستقبل التبريد"، ونُشر في مايو 2018م، أن في أجزاء من أمريكا الجنوبيّة وإفريقيا وآسيا والشرق الأوسط، يعيش 2.8 مليار نسمة، ولا يملك وحدات تكييف سوى %8 من المنازل. في حين الدول المتقدِّمة مثل كوريا الجنوبيّة، واليابان، والولايات المتحدة، فإن %89 من المنازل تملك مكيفات، وفي الصين %60 من البيوت تملكها أيضاً.</p>
-        <p>ولكن مع تنامي الدخل في بلدان الاقتصاد الصاعد، يتوقّع أن تزداد المنازل التي تقتني مكيّفاً. وبحسب مقالة نُشرت في صحيفة "نيويورك تايمز"، في 15 مايو 2018م، سيرتفع عدد المكيّفات في العالم من نحو 1.6 مليار حالياً، إلى 5.6 مليارات عام 2050م، طبقاً لمعدَّلات النمو الاقتصادي.</p>
-        <p>ومع الافتقار إلى الابتكار وتطوير النظم لفرض معايير الجدوى الأعلى، فسيتضاعف استهلاك الطاقة لتشغيل المكيّفات ثلاثة أضعاف. وسينتج من ذلك طلب إضافي للطاقة يساوي مجموع إنتاج الطاقة في الصين حالياً. فمبيعات المكيّفات ترتفع اليوم في البلدان النامية، لكن فعاليّة هذه الوحدات مشكوك فيها. فمثلاً، أوسع وحدات التكييف انتشاراً في بعض الأسواق الآسيوية تتطلّب ضعفي ما تتطلّبه وحدات تكييف أجدى. والمكيّفات التي تباع في اليابان والاتحاد الأوروبي أجدى بنسبة %25 عادة، من تلك التي تباع في الصين والولايات المتحدة.</p>
-        <p>وبصرف النظر عن كثير من الطاقة التي يحتاج إليها المكيّف، فإنه يبث مقادير وفيرة من غازات الدفيئة نفسها. وطبقاً لموقع تكييف الهواء (airconditioning.com)، فإن المبرِّدات في معظم المكيّفات مثل مواد مركبات الكربون الكلورية فلورية أو مواد هيدروفلوروولفينات، ومركبات ثاني أكسيد الكربون الهيدروكلورية فلورية، أسوأ بكثير للبيئة من ثاني أكسيد الكربون، لأنها تحتبس من الحرارة مقادير أكبر حين تتسرّب إلى الجو.</p>
-        <p>وبالإضافة إلى ظاهرة الدفيئة، فهذه الغازات تسهم أيضاً بالإضرار بطبقة الأوزون. ويمكن لهذه المواد الكيميائية أن تتسرّب خلال عملية التصنيع أو التصليح. ويعرف كل من يملك في منزله مكيّفاً كم تتكرر أعطال هذه الأجهزة. ثم إن المكيف يُرمَى حين يتعطّل نهائياً ولا يعود قابلاً للإصلاح، والمواد المبرِّدة فيه ستتسرّب على الأرجح لتلوث الهواء.</p>
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">إغلاق</button>
-      </div>
-    </div>
-  </div>
-</div>
index 08b18bb6a91357c036d02aa07e09b1e15aca7594..d5e5c6ad34bd410b0d1aa729f6ddb4969af5122e 100644 (file)
@@ -54,9 +54,9 @@ export const body_class = 'bg-body-tertiary'
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#button-group">Button group</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#card">Card</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#carousel">Carousel</a></li>
+          <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#dialog">Dialog</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#dropdowns">Dropdowns</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#list-group">List group</a></li>
-          <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#modal">Modal</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#navs">Navs</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#navbar">Navbar</a></li>
           <li><a class="d-inline-flex align-items-center rounded text-decoration-none" href="#pagination">Pagination</a></li>
@@ -879,6 +879,23 @@ export const body_class = 'bg-body-tertiary'
         `} />
       </div>
     </article>
+    <article class="my-3" id="dialog">
+      <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
+        <h3>Dialog</h3>
+        <a class="d-flex align-items-center" href={getVersionedDocsPath('components/dialog')}>Documentation</a>
+      </div>
+
+      <div>
+        <Example showMarkup={false} code={`
+        <button type="button" class="btn btn-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialog">
+          Launch demo dialog
+        </button>
+        <button type="button" class="btn btn-primary" data-bs-toggle="dialog" data-bs-target="#staticBackdropDialog">
+          Launch static backdrop dialog
+        </button>
+        `} />
+      </div>
+    </article>
     <article class="my-3" id="dropdowns">
       <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
         <h3>Dropdowns</h3>
@@ -1097,31 +1114,6 @@ export const body_class = 'bg-body-tertiary'
         `]} />
       </div>
     </article>
-    <article class="my-3" id="modal">
-      <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
-        <h3>Modal</h3>
-        <a class="d-flex align-items-center" href={getVersionedDocsPath('components/modal')}>Documentation</a>
-      </div>
-
-      <div>
-        <Example showMarkup={false} code={`
-        <div class="d-flex justify-content-between flex-wrap">
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalDefault">
-            Launch demo modal
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdropLive">
-            Launch static backdrop modal
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalCenteredScrollable">
-            Vertically centered scrollable modal
-          </button>
-          <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreen">
-            Full screen
-          </button>
-        </div>
-        `} />
-      </div>
-    </article>
     <article class="my-3" id="navs">
       <div class="bd-heading sticky-xl-top align-self-start mt-5 mb-3 mt-xl-0 mb-xl-2">
         <h3>Navs</h3>
@@ -1490,73 +1482,30 @@ export const body_class = 'bg-body-tertiary'
   </section>
 </main>
 
-<div class="modal fade" id="exampleModalDefault" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </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>
+<dialog class="dialog" id="exampleDialog">
+  <div class="dialog-header">
+    <h1 class="dialog-title fs-5">Dialog title</h1>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
   </div>
-</div>
-<div class="modal fade" id="staticBackdropLive" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLiveLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="staticBackdropLiveLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <p>I will not close if you click outside me. Don't even try to press escape key.</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">Understood</button>
-      </div>
-    </div>
+  <div class="dialog-body">
+    <p>This is a native dialog element using Bootstrap's Dialog component.</p>
   </div>
-</div>
-<div class="modal fade" id="exampleModalCenteredScrollable" tabindex="-1" aria-labelledby="exampleModalCenteredScrollableTitle" aria-hidden="true">
-  <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalCenteredScrollableTitle">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <p>This is some placeholder content to show the scrolling behavior for modals. We use repeated line breaks to demonstrate how content can exceed minimum inner height, thereby showing inner scrolling. When content becomes longer than the predefined max-height of modal, content will be cropped and scrollable within the modal.</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>
-        <p>This content should appear at the bottom after you scroll.</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 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>
-</div>
-<div class="modal fade" id="exampleModalFullscreen" tabindex="-1" aria-labelledby="exampleModalFullscreenLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenLabel">Full screen modal</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
+</dialog>
 
+<dialog class="dialog" id="staticBackdropDialog" data-bs-backdrop="static" data-bs-keyboard="false">
+  <div class="dialog-header">
+    <h1 class="dialog-title fs-5">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-secondary" data-bs-dismiss="dialog">Close</button>
+    <button type="button" class="btn btn-primary">Understood</button>
+  </div>
+</dialog>
diff --git a/site/src/assets/examples/dialogs/dialogs.css b/site/src/assets/examples/dialogs/dialogs.css
new file mode 100644 (file)
index 0000000..5982b43
--- /dev/null
@@ -0,0 +1,9 @@
+/* Override default dialog positioning for static examples */
+.dialog-example {
+  position: relative;
+  max-width: 500px;
+}
+
+.w-340px {
+  max-width: 340px;
+}
diff --git a/site/src/assets/examples/dialogs/index.astro b/site/src/assets/examples/dialogs/index.astro
new file mode 100644 (file)
index 0000000..cd88884
--- /dev/null
@@ -0,0 +1,138 @@
+---
+export const title = 'Dialogs'
+export const extra_css = ['dialogs.css']
+---
+
+<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
+  <symbol id="bookmark-star" viewBox="0 0 16 16">
+    <path d="M7.84 4.1a.178.178 0 0 1 .32 0l.634 1.285a.178.178 0 0 0 .134.098l1.42.206c.145.021.204.2.098.303L9.42 6.993a.178.178 0 0 0-.051.158l.242 1.414a.178.178 0 0 1-.258.187l-1.27-.668a.178.178 0 0 0-.165 0l-1.27.668a.178.178 0 0 1-.257-.187l.242-1.414a.178.178 0 0 0-.05-.158l-1.03-1.001a.178.178 0 0 1 .098-.303l1.42-.206a.178.178 0 0 0 .134-.098L7.84 4.1z"/>
+    <path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z"/>
+  </symbol>
+  <symbol id="grid-fill" viewBox="0 0 16 16">
+    <path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
+  </symbol>
+  <symbol id="film" viewBox="0 0 16 16">
+    <path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm4 0v6h8V1H4zm8 8H4v6h8V9zM1 1v2h2V1H1zm2 3H1v2h2V4zM1 7v2h2V7H1zm2 3H1v2h2v-2zm-2 3v2h2v-2H1zM15 1h-2v2h2V1zm-2 3v2h2V4h-2zm2 3h-2v2h2V7zm-2 3v2h2v-2h-2zm2 3h-2v2h2v-2z"/>
+  </symbol>
+  <symbol id="google" viewBox="0 0 16 16">
+    <path d="M15.545 6.558a9.4 9.4 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.7 7.7 0 0 1 5.352 2.082l-2.284 2.284A4.35 4.35 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.8 4.8 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.7 3.7 0 0 0 1.599-2.431H8v-3.08z"/>
+  </symbol>
+  <symbol id="github" viewBox="0 0 16 16">
+    <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
+  </symbol>
+  <symbol id="facebook" viewBox="0 0 16 16">
+    <path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
+  </symbol>
+</svg>
+
+<!-- Modal sheet style -->
+<div class="d-flex flex-column p-4 gap-4 py-md-5 align-items-center justify-content-center">
+  <dialog class="dialog dialog-example rounded-4 shadow w-340px" open data-bs-theme="light">
+    <div class="dialog-header border-0">
+      <h1 class="dialog-title">Modal title</h1>
+      <button type="button" class="btn-close" aria-label="Close"></button>
+    </div>
+    <div class="dialog-body pt-0">
+      <p>This is a modal sheet, a variation of the modal that docs itself to the bottom of the viewport like the newer share sheets in iOS.</p>
+    </div>
+    <div class="dialog-footer flex-column border-0 pt-0">
+      <button type="button" class="btn btn-solid theme-primary w-100">Save changes</button>
+      <button type="button" class="btn btn-solid theme-secondary w-100">Close</button>
+    </div>
+  </dialog>
+</div>
+
+<div class="b-example-divider"></div>
+
+<!-- Confirmation dialog -->
+<div class="d-flex flex-column p-4 gap-4 py-md-5 align-items-center justify-content-center">
+  <dialog class="dialog dialog-example rounded-4 shadow w-340px" open data-bs-theme="light">
+    <div class="dialog-body text-center py-4">
+      <h5 class="fw-bold mb-2">Enable this setting?</h5>
+      <p class="text-body-secondary mb-0">You can always change your mind in your account settings.</p>
+    </div>
+    <div class="dialog-footer p-0 border-top">
+      <button type="button" class="btn btn-link text-primary flex-fill py-3 m-0 rounded-0 border-end text-decoration-none fw-semibold">Yes, enable</button>
+      <button type="button" class="btn btn-link text-primary flex-fill py-3 m-0 rounded-0 text-decoration-none">No thanks</button>
+    </div>
+  </dialog>
+</div>
+
+<div class="b-example-divider"></div>
+
+<!-- What's new dialog -->
+<div class="d-flex flex-column p-4 gap-4 py-md-5 align-items-center justify-content-center">
+  <dialog class="dialog dialog-example rounded-4 shadow w-340px" open data-bs-theme="light">
+    <div class="dialog-header border-0 pt-4">
+      <h1 class="dialog-title fs-2 fw-bold">What's new</h1>
+    </div>
+    <div class="dialog-body">
+      <ul class="list-unstyled mb-0">
+        <li class="d-flex gap-3 mb-3">
+          <svg class="bi text-body-secondary flex-shrink-0" width="48" height="48"><use xlink:href="#grid-fill"/></svg>
+          <div>
+            <h6 class="mb-1 fw-bold">Grid view</h6>
+            <p class="mb-0 text-body-secondary">Not into lists? Try the new grid view.</p>
+          </div>
+        </li>
+        <li class="d-flex gap-3 mb-3">
+          <svg class="bi text-warning flex-shrink-0" width="48" height="48"><use xlink:href="#bookmark-star"/></svg>
+          <div>
+            <h6 class="mb-1 fw-bold">Bookmarks</h6>
+            <p class="mb-0 text-body-secondary">Save items you love for easy access later.</p>
+          </div>
+        </li>
+        <li class="d-flex gap-3">
+          <svg class="bi text-primary flex-shrink-0" width="48" height="48"><use xlink:href="#film"/></svg>
+          <div>
+            <h6 class="mb-1 fw-bold">Video embeds</h6>
+            <p class="mb-0 text-body-secondary">Share videos wherever you go.</p>
+          </div>
+        </li>
+      </ul>
+    </div>
+    <div class="dialog-footer border-0 pt-3 pb-4">
+      <button type="button" class="btn btn-solid theme-primary w-100 py-2">Great, thanks!</button>
+    </div>
+  </dialog>
+</div>
+
+<div class="b-example-divider"></div>
+
+<!-- Sign up dialog -->
+<div class="d-flex flex-column p-4 gap-4 py-md-5 align-items-center justify-content-center">
+  <dialog class="dialog dialog-example rounded-4 shadow w-340px" open data-bs-theme="light">
+    <div class="dialog-header border-0">
+      <h1 class="dialog-title">Sign up for free</h1>
+      <button type="button" class="btn-close" aria-label="Close"></button>
+    </div>
+    <div class="dialog-body pt-0">
+      <form>
+        <div class="form-floating mb-3">
+          <input type="email" class="form-control rounded-3" id="floatingEmail" placeholder="name@example.com">
+          <label for="floatingEmail">Email address</label>
+        </div>
+        <div class="form-floating mb-3">
+          <input type="password" class="form-control rounded-3" id="floatingPassword" placeholder="Password">
+          <label for="floatingPassword">Password</label>
+        </div>
+        <button class="btn btn-solid theme-primary w-100 py-2 mb-2" type="submit">Sign up</button>
+        <small class="text-body-secondary">By clicking Sign up, you agree to the terms of use.</small>
+        <hr class="my-4">
+        <h6 class="fw-bold mb-3">Or use a third-party</h6>
+        <button type="button" class="btn btn-outline theme-inverse w-100 py-2 mb-2">
+          <svg class="bi" width="16" height="16"><use xlink:href="#google"/></svg>
+          Sign up with Google
+        </button>
+        <button type="button" class="btn btn-outline theme-facebook w-100 py-2 mb-2">
+          <svg class="bi" width="16" height="16" fill="currentColor"><use xlink:href="#facebook"/></svg>
+          Sign up with Facebook
+        </button>
+        <button type="button" class="btn btn-outline theme-success w-100 py-2">
+          <svg class="bi" width="16" height="16"><use xlink:href="#github"/></svg>
+          Sign up with GitHub
+        </button>
+      </form>
+    </div>
+  </dialog>
+</div>
diff --git a/site/src/assets/examples/modals/index.astro b/site/src/assets/examples/modals/index.astro
deleted file mode 100644 (file)
index 9514f6f..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
----
-export const title = 'Modals'
-export const extra_css = ['modals.css']
----
-
-<svg xmlns="http://www.w3.org/2000/svg" class="d-none">
-  <symbol id="bookmark-star" viewBox="0 0 16 16">
-    <path d="M7.84 4.1a.178.178 0 0 1 .32 0l.634 1.285a.178.178 0 0 0 .134.098l1.42.206c.145.021.204.2.098.303L9.42 6.993a.178.178 0 0 0-.051.158l.242 1.414a.178.178 0 0 1-.258.187l-1.27-.668a.178.178 0 0 0-.165 0l-1.27.668a.178.178 0 0 1-.257-.187l.242-1.414a.178.178 0 0 0-.05-.158l-1.03-1.001a.178.178 0 0 1 .098-.303l1.42-.206a.178.178 0 0 0 .134-.098L7.84 4.1z"/>
-    <path d="M2 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v13.5a.5.5 0 0 1-.777.416L8 13.101l-5.223 2.815A.5.5 0 0 1 2 15.5V2zm2-1a1 1 0 0 0-1 1v12.566l4.723-2.482a.5.5 0 0 1 .554 0L13 14.566V2a1 1 0 0 0-1-1H4z"/>
-  </symbol>
-
-  <symbol id="grid-fill" viewBox="0 0 16 16">
-    <path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3a1.5 1.5 0 0 1 1.5 1.5v3a1.5 1.5 0 0 1-1.5 1.5h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
-  </symbol>
-
-  <symbol id="film" viewBox="0 0 16 16">
-    <path d="M0 1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V1zm4 0v6h8V1H4zm8 8H4v6h8V9zM1 1v2h2V1H1zm2 3H1v2h2V4zM1 7v2h2V7H1zm2 3H1v2h2v-2zm-2 3v2h2v-2H1zM15 1h-2v2h2V1zm-2 3v2h2V4h-2zm2 3h-2v2h2V7zm-2 3v2h2v-2h-2zm2 3h-2v2h2v-2z"/>
-  </symbol>
-
-  <symbol id="google" viewBox="0 0 16 16">
-    <path d="M15.545 6.558a9.4 9.4 0 0 1 .139 1.626c0 2.434-.87 4.492-2.384 5.885h.002C11.978 15.292 10.158 16 8 16A8 8 0 1 1 8 0a7.7 7.7 0 0 1 5.352 2.082l-2.284 2.284A4.35 4.35 0 0 0 8 3.166c-2.087 0-3.86 1.408-4.492 3.304a4.8 4.8 0 0 0 0 3.063h.003c.635 1.893 2.405 3.301 4.492 3.301 1.078 0 2.004-.276 2.722-.764h-.003a3.7 3.7 0 0 0 1.599-2.431H8v-3.08z"/>
-  </symbol>
-
-  <symbol id="github" viewBox="0 0 16 16">
-    <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
-  </symbol>
-
-  <symbol id="facebook" viewBox="0 0 16 16">
-    <path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951z"/>
-  </symbol>
-</svg>
-
-<div class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1" role="dialog" id="modalSheet">
-  <div class="modal-dialog">
-    <div class="modal-content rounded-4 shadow">
-      <div class="modal-header border-bottom-0">
-        <h1 class="modal-title fs-5">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body py-0">
-        <p>This is a modal sheet, a variation of the modal that docs itself to the bottom of the viewport like the newer share sheets in iOS.</p>
-      </div>
-      <div class="modal-footer flex-column align-items-stretch w-100 gap-2 pb-3 border-top-0">
-        <button type="button" class="btn btn-lg btn-primary">Save changes</button>
-        <button type="button" class="btn btn-lg btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="b-example-divider"></div>
-
-<div class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1" role="dialog" id="modalChoice">
-  <div class="modal-dialog">
-    <div class="modal-content rounded-3 shadow">
-      <div class="modal-body p-4 text-center">
-        <h5 class="mb-0">Enable this setting?</h5>
-        <p class="mb-0">You can always change your mind in your account settings.</p>
-      </div>
-      <div class="modal-footer flex-nowrap p-0">
-        <button type="button" class="btn btn-lg btn-link fs-6 text-decoration-none col-6 py-3 m-0 rounded-0 border-end"><strong>Yes, enable</strong></button>
-        <button type="button" class="btn btn-lg btn-link fs-6 text-decoration-none col-6 py-3 m-0 rounded-0" data-bs-dismiss="modal">No thanks</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="b-example-divider"></div>
-
-<div class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1" role="dialog" id="modalTour">
-  <div class="modal-dialog">
-    <div class="modal-content rounded-4 shadow">
-      <div class="modal-body p-5">
-        <h2 class="fw-bold mb-0">What's new</h2>
-
-        <ul class="d-grid gap-4 my-5 list-unstyled small">
-          <li class="d-flex gap-4">
-            <svg class="bi text-body-secondary flex-shrink-0" width="48" height="48" aria-hidden="true"><use xlink:href="#grid-fill"/></svg>
-            <div>
-              <h5 class="mb-0">Grid view</h5>
-              Not into lists? Try the new grid view.
-            </div>
-          </li>
-          <li class="d-flex gap-4">
-            <svg class="bi text-warning flex-shrink-0" width="48" height="48" aria-hidden="true"><use xlink:href="#bookmark-star"/></svg>
-            <div>
-              <h5 class="mb-0">Bookmarks</h5>
-              Save items you love for easy access later.
-            </div>
-          </li>
-          <li class="d-flex gap-4">
-            <svg class="bi text-primary flex-shrink-0" width="48" height="48" aria-hidden="true"><use xlink:href="#film"/></svg>
-            <div>
-              <h5 class="mb-0">Video embeds</h5>
-              Share videos wherever you go.
-            </div>
-          </li>
-        </ul>
-        <button type="button" class="btn btn-lg btn-primary mt-5 w-100" data-bs-dismiss="modal">Great, thanks!</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="b-example-divider"></div>
-
-<div class="modal modal-sheet position-static d-block bg-body-secondary p-4 py-md-5" tabindex="-1" role="dialog" id="modalSignin">
-  <div class="modal-dialog">
-    <div class="modal-content rounded-4 shadow">
-      <div class="modal-header p-5 pb-4 border-bottom-0">
-        <h1 class="fw-bold mb-0 fs-2">Sign up for free</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-
-      <div class="modal-body p-5 pt-0">
-        <form class="">
-          <div class="form-floating mb-3">
-            <input type="email" class="form-control rounded-3" id="floatingInput" placeholder="name@example.com">
-            <label for="floatingInput">Email address</label>
-          </div>
-          <div class="form-floating mb-3">
-            <input type="password" class="form-control rounded-3" id="floatingPassword" placeholder="Password">
-            <label for="floatingPassword">Password</label>
-          </div>
-          <button class="w-100 mb-2 btn btn-lg rounded-3 btn-primary" type="submit">Sign up</button>
-          <small class="text-body-secondary">By clicking Sign up, you agree to the terms of use.</small>
-          <hr class="my-4">
-          <h2 class="fs-5 fw-bold mb-3">Or use a third-party</h2>
-          <button class="w-100 py-2 mb-2 btn btn-outline-secondary rounded-3" type="submit">
-            <svg class="bi me-1" width="16" height="16" aria-hidden="true"><use xlink:href="#google"/></svg>
-            Sign up with Google
-          </button>
-          <button class="w-100 py-2 mb-2 btn btn-outline-primary rounded-3" type="submit">
-            <svg class="bi me-1" width="16" height="16" aria-hidden="true"><use xlink:href="#facebook"/></svg>
-            Sign up with Facebook
-          </button>
-          <button class="w-100 py-2 mb-2 btn btn-outline-secondary rounded-3" type="submit">
-            <svg class="bi me-1" width="16" height="16" aria-hidden="true"><use xlink:href="#github"/></svg>
-            Sign up with GitHub
-          </button>
-        </form>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="b-example-divider"></div>
diff --git a/site/src/assets/examples/modals/modals.css b/site/src/assets/examples/modals/modals.css
deleted file mode 100644 (file)
index 194e16a..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-.modal-sheet .modal-dialog {
-  width: 380px;
-  transition: bottom .75s ease-in-out;
-}
-.modal-sheet .modal-footer {
-  padding-bottom: 2rem;
-}
index 498071b415a2f23e0198bb32c26d992501b9a0e8..3be645a3ba27ed345b319ffe152837e6635fb606 100644 (file)
@@ -128,31 +128,6 @@ export default () => {
       })
     })
 
-  // -------------------------------
-  // Modal
-  // -------------------------------
-  // Modal 'Varying modal content' example in docs and StackBlitz
-  // js-docs-start varying-modal-content
-  const exampleModal = document.getElementById('exampleModal')
-  if (exampleModal) {
-    exampleModal.addEventListener('show.bs.modal', event => {
-      // Button that triggered the modal
-      const button = event.relatedTarget
-      // Extract info from data-bs-* attributes
-      const recipient = button.getAttribute('data-bs-whatever')
-      // If necessary, you could initiate an Ajax request here
-      // and then do the updating in a callback.
-
-      // Update the modal's content.
-      const modalTitle = exampleModal.querySelector('.modal-title')
-      const modalBodyInput = exampleModal.querySelector('.modal-body input')
-
-      modalTitle.textContent = `New message to ${recipient}`
-      modalBodyInput.value = recipient
-    })
-  }
-  // js-docs-end varying-modal-content
-
   // -------------------------------
   // Offcanvas
   // -------------------------------
index 236ac5a151d65cd3ef0529cd166cd7d053589ed3..e8afcbbb8fbec67190193b17b4c6e497868696e3 100644 (file)
@@ -13,7 +13,7 @@ const plugins = getData('plugins')
     </div>
     <h2 class="display-5 mb-3 fw-semibold lh-sm">Powerful JavaScript plugins without&nbsp;jQuery</h2>
     <p class="lead fw-normal">
-      Add toggleable hidden elements, modals and offcanvas menus, popovers and tooltips, and so much more—all without
+      Add toggleable hidden elements, dialogs and offcanvas menus, popovers and tooltips, and so much more—all without
       jQuery. Bootstrap's JavaScript is HTML-first, meaning most plugins are added with <code>data</code> attributes in your
       HTML. Need more control? Include individual plugins programmatically.
     </p>
diff --git a/site/src/content/docs/components/dialog.mdx b/site/src/content/docs/components/dialog.mdx
new file mode 100644 (file)
index 0000000..486c32b
--- /dev/null
@@ -0,0 +1,492 @@
+---
+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>The dialog component is built on the native HTML <code>&lt;dialog&gt;</code> element, which provides built-in accessibility features like focus trapping, backdrop handling, and proper keyboard interactions. This approach leverages browser-native functionality rather than custom JavaScript implementations.</p>
+    <p>When using <code>showModal()</code>, the dialog is rendered in the browser's top layer, ensuring it appears above all other content regardless of z-index values. The backdrop is created using the <code>::backdrop</code> pseudo-element, which can be styled with CSS.</p>
+    <p>Focus management is handled automatically by the browser. When a modal dialog opens, focus moves to the first focusable element inside. When closed, focus returns to the element that triggered the dialog. This behavior is essential for accessibility and keyboard navigation.</p>
+    <p>The Escape key closes modal dialogs by default, though this can be prevented using the <code>cancel</code> event. Non-modal dialogs opened with <code>show()</code> don't trap focus or block interaction with the rest of the page.</p>
+    <p>Bootstrap's Dialog component wraps the native element with additional features: custom animations, static backdrop option, programmatic control via JavaScript, and integration with data attributes for declarative usage.</p>
+    <p>Scrolling behavior varies based on content length. Short dialogs center in the viewport. Long dialogs scroll within the viewport by default, though you can use <code>.dialog-scrollable</code> to scroll only the body while keeping header and footer fixed.</p>
+    <p>The component fires standard Bootstrap events: <code>show.bs.dialog</code>, <code>shown.bs.dialog</code>, <code>hide.bs.dialog</code>, and <code>hidden.bs.dialog</code>. These allow you to hook into the dialog lifecycle and execute custom logic.</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>The <code>.dialog-overflow</code> modifier creates a full-viewport scrollable container. The <code>.dialog-box</code> wrapper contains the visual dialog that moves up and down as you scroll. This pattern is useful for very long content like terms of service or detailed forms.</p>
+      <p>Unlike the default scrolling behavior where the dialog is constrained to the viewport, overflow dialogs can extend beyond it. This gives users a more document-like reading experience for lengthy content.</p>
+      <p>The backdrop remains fixed while the dialog content scrolls. Clicking outside the dialog box still closes it (unless using a static backdrop). Keyboard navigation and focus trapping work the same as standard dialogs.</p>
+      <p>Consider using <code>.dialog-scrollable</code> instead if you want the header and footer to remain visible while scrolling. The scrollable variant keeps navigation controls accessible at all times, which may be preferable for forms with submit buttons.</p>
+      <p>Both approaches leverage the native <code>&lt;dialog&gt;</code> element's top layer rendering. This ensures the dialog appears above all other content, including elements with high z-index values, fixed positioning, or transforms.</p>
+      <p>The choice between scrolling behaviors depends on your content and user experience goals. Document-like content often works well with overflow scrolling, while interactive content may benefit from fixed header and footer.</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()
+```
+
+## Optional sizes
+
+Dialogs have three optional sizes, available via modifier classes to be placed on a `.dialog`. These sizes kick in at certain breakpoints to avoid horizontal scrollbars on narrower viewports.
+
+<BsTable>
+  | Size | Class | Dialog max-width |
+  | --- | --- | --- |
+  | Small | `.dialog-sm` | `300px` |
+  | Default | — | `500px` |
+  | Large | `.dialog-lg` | `800px` |
+  | Extra large | `.dialog-xl` | `1140px` |
+</BsTable>
+
+<dialog class="dialog dialog-xl" id="exampleDialogXl">
+  <div class="dialog-header">
+    <h1 class="dialog-title">Extra large dialog</h1>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+  </div>
+  <div class="dialog-body">
+    <p>This is an extra large dialog using the <code>.dialog-xl</code> class.</p>
+  </div>
+</dialog>
+
+<dialog class="dialog dialog-lg" id="exampleDialogLg">
+  <div class="dialog-header">
+    <h1 class="dialog-title">Large 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 large dialog using the <code>.dialog-lg</code> class.</p>
+  </div>
+</dialog>
+
+<dialog class="dialog dialog-sm" id="exampleDialogSm">
+  <div class="dialog-header">
+    <h1 class="dialog-title">Small 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 small dialog using the <code>.dialog-sm</code> class.</p>
+  </div>
+</dialog>
+
+<Example showMarkup={false} code={`<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialogXl">
+  Extra large dialog
+</button>
+<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialogLg">
+  Large dialog
+</button>
+<button type="button" class="btn btn-solid theme-primary" data-bs-toggle="dialog" data-bs-target="#exampleDialogSm">
+  Small dialog
+</button>`} />
+
+```html
+<dialog class="dialog dialog-xl">...</dialog>
+<dialog class="dialog dialog-lg">...</dialog>
+<dialog class="dialog dialog-sm">...</dialog>
+```
+
+## Fullscreen dialog
+
+Use `.dialog-fullscreen` to make the dialog cover the entire viewport.
+
+<dialog class="dialog dialog-fullscreen" id="exampleDialogFullscreen">
+  <div class="dialog-header">
+    <h1 class="dialog-title">Fullscreen dialog</h1>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+  </div>
+  <div class="dialog-body">
+    <p>This dialog covers the entire viewport.</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="#exampleDialogFullscreen">
+  Fullscreen dialog
+</button>`} />
+
+Responsive fullscreen variants are also available. These make the dialog fullscreen only below a specific breakpoint.
+
+| Class | Fullscreen below |
+| --- | --- |
+| `.dialog-fullscreen` | Always |
+| `.dialog-fullscreen-sm-down` | `576px` |
+| `.dialog-fullscreen-md-down` | `768px` |
+| `.dialog-fullscreen-lg-down` | `1024px` |
+| `.dialog-fullscreen-xl-down` | `1280px` |
+| `.dialog-fullscreen-2xl-down` | `1536px` |
+
+<dialog class="dialog dialog-fullscreen-lg-down" id="exampleDialogFullscreenLg">
+  <div class="dialog-header">
+    <h1 class="dialog-title">Fullscreen below lg</h1>
+    <button type="button" class="btn-close" data-bs-dismiss="dialog" aria-label="Close"></button>
+  </div>
+  <div class="dialog-body">
+    <p>This dialog is fullscreen below the <code>lg</code> breakpoint.</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="#exampleDialogFullscreenLg">
+  Fullscreen below lg
+</button>`} />
+
+```html
+<dialog class="dialog dialog-fullscreen-lg-down">...</dialog>
+```
+
+## 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...
+})
+```
diff --git a/site/src/content/docs/components/modal.mdx b/site/src/content/docs/components/modal.mdx
deleted file mode 100644 (file)
index 56b5960..0000000
+++ /dev/null
@@ -1,833 +0,0 @@
----
-title: Modal
-description: Use Bootstrap’s JavaScript modal plugin to add dialogs to your site for lightboxes, user notifications, or completely custom content.
-toc: true
----
-
-## How it works
-
-Before getting started with Bootstrap’s modal component, be sure to read the following as our menu options have recently changed.
-
-- Modals are built with HTML, CSS, and JavaScript. They’re positioned over everything else in the document and remove scroll from the `<body>` so that modal content scrolls instead.
-- Clicking on the modal “backdrop” will automatically close the modal.
-- Bootstrap only supports one modal window at a time. Nested modals aren’t supported as we believe them to be poor user experiences.
-- Modals use `position: fixed`, which can sometimes be a bit particular about its rendering. Whenever possible, place your modal HTML in a top-level position to avoid potential interference from other elements. You’ll likely run into issues when nesting a `.modal` within another fixed element.
-- Once again, due to `position: fixed`, there are some caveats with using modals on mobile devices.
-- Due to how HTML5 defines its semantics, [the `autofocus` HTML attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#attr-autofocus) has no effect in Bootstrap modals. To achieve the same effect, use some custom JavaScript:
-
-```js
-const myModal = document.getElementById('myModal')
-const myInput = document.getElementById('myInput')
-
-myModal.addEventListener('shown.bs.modal', () => {
-  myInput.focus()
-})
-```
-
-<Callout name="info-prefersreducedmotion" />
-
-Keep reading for demos and usage guidelines.
-
-## Examples
-
-### Modal components
-
-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">
-    <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>
-
-```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>
-    </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.
-</Callout>
-
-### Live demo
-
-Toggle a working modal demo by clicking the button below. It will slide down and fade in from the top of the page.
-
-<div class="modal fade" id="exampleModalLive" tabindex="-1" aria-labelledby="exampleModalLiveLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalLiveLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <p>Woo-hoo, you’re reading this text in a modal!</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>
-
-<Example showMarkup={false} code={`
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalLive">Launch demo modal</button>
-`} />
-
-```html
-<!-- Button trigger modal -->
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal">
-  Launch demo modal
-</button>
-
-<!-- Modal -->
-<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </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>
-```
-
-### Static backdrop
-
-When backdrop is set to static, the modal will not close when clicking outside of it. Click the button below to try it.
-
-<div class="modal fade" id="staticBackdropLive" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLiveLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="staticBackdropLiveLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <p>I will not close if you click outside of me. Don’t even try to press escape key.</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">Understood</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<Example showMarkup={false} code={`
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdropLive">Launch static backdrop modal</button>
-`} />
-
-```html
-<!-- Button trigger modal -->
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticBackdrop">
-  Launch static backdrop modal
-</button>
-
-<!-- Modal -->
-<div class="modal fade" id="staticBackdrop" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="staticBackdropLabel">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </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">Understood</button>
-      </div>
-    </div>
-  </div>
-</div>
-```
-
-### Scrolling long content
-
-When modals become too long for the user’s viewport or device, they scroll independent of the page itself. Try the demo below to see what we mean.
-
-<div class="modal fade" id="exampleModalLong" tabindex="-1" aria-labelledby="exampleModalLongTitle" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalLongTitle">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body" style="min-height: 100vh">
-        <p>This is some placeholder content to show the scrolling behavior for modals. Instead of repeating the text in the modal, we use an inline style to set a minimum height, thereby extending the length of the overall modal and demonstrating the overflow scrolling. When content becomes longer than the height of the viewport, scrolling will move the modal as needed.</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>
-
-<Example showMarkup={false} code={`
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalLong">Launch demo modal</button>
-`} />
-
-You can also create a scrollable modal that allows scrolling the modal body by adding `.modal-dialog-scrollable` to `.modal-dialog`.
-
-<div class="modal fade" id="exampleModalScrollable" tabindex="-1" aria-labelledby="exampleModalScrollableTitle" aria-hidden="true">
-  <div class="modal-dialog modal-dialog-scrollable">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="exampleModalScrollableTitle">Modal title</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <p>This is some placeholder content to show the scrolling behavior for modals. We use repeated line breaks to demonstrate how content can exceed minimum inner height, thereby showing inner scrolling. When content becomes longer than the predefined max-height of modal, content will be cropped and scrollable within the modal.</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/>
-        <p>This content should appear at the bottom after you scroll.</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>
-
-<Example showMarkup={false} code={`
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalScrollable">Launch demo modal</button>
-`} />
-
-```html
-<!-- Scrollable modal -->
-<div class="modal-dialog modal-dialog-scrollable">
-  ...
-</div>
-```
-
-### Vertically centered
-
-Add `.modal-dialog-centered` to `.modal-dialog` to vertically center the modal.
-
-<Example showMarkup={false} code={`<div class="modal fade" id="exampleModalCenter" tabindex="-1" aria-labelledby="exampleModalCenterTitle" aria-hidden="true">
-    <div class="modal-dialog modal-dialog-centered">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalCenterTitle">Modal title</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <p>This is a vertically centered modal.</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 class="modal fade" id="exampleModalCenteredScrollable" tabindex="-1" aria-labelledby="exampleModalCenteredScrollableTitle" aria-hidden="true">
-    <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalCenteredScrollableTitle">Modal title</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <p>This is some placeholder content to show a vertically centered modal. We’ve added some extra copy here to show how vertically centering the modal works when combined with scrollable modals. We also use some repeated line breaks to quickly extend the height of the content, thereby triggering the scrolling. When content becomes longer than the predefined max-height of modal, content will be cropped and scrollable within the modal.</p>
-          <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
-          <p>Just like that.</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>
-
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalCenter">Vertically centered modal</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalCenteredScrollable">Vertically centered scrollable modal</button>`} />
-
-```html
-<!-- Vertically centered modal -->
-<div class="modal-dialog modal-dialog-centered">
-  ...
-</div>
-
-<!-- Vertically centered scrollable modal -->
-<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable">
-  ...
-</div>
-```
-
-### Tooltips and popovers
-
-[Tooltips]([[docsref:/components/tooltips]]) and [popovers]([[docsref:/components/popovers]]) can be placed within modals as needed. When modals are closed, any tooltips and popovers within are also automatically dismissed.
-
-<Example showMarkup={false} code={`<div class="modal fade" id="exampleModalPopovers" tabindex="-1" aria-labelledby="exampleModalPopoversLabel" aria-hidden="true">
-    <div class="modal-dialog">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalPopoversLabel">Modal title</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <h2 class="fs-5">Popover in a modal</h2>
-          <p>This <button class="btn btn-secondary" data-bs-toggle="popover" title="Popover title" data-bs-content="Popover body content is set in this attribute." data-bs-container="#exampleModalPopovers">button</button> triggers a popover on click.</p>
-          <hr>
-          <h2 class="fs-5">Tooltips in a modal</h2>
-          <p><a href="#" data-bs-toggle="tooltip" title="Tooltip" data-bs-container="#exampleModalPopovers">This link</a> and <a href="#" data-bs-toggle="tooltip" title="Tooltip" data-bs-container="#exampleModalPopovers">that link</a> have tooltips on hover.</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>
-
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalPopovers">Launch demo modal</button>`} />
-
-```html
-<div class="modal-body">
-  <h2 class="fs-5">Popover in a modal</h2>
-  <p>This <button class="btn btn-secondary" data-bs-toggle="popover" title="Popover title" data-bs-content="Popover body content is set in this attribute.">button</button> triggers a popover on click.</p>
-  <hr>
-  <h2 class="fs-5">Tooltips in a modal</h2>
-  <p><a href="#" data-bs-toggle="tooltip" title="Tooltip">This link</a> and <a href="#" data-bs-toggle="tooltip" title="Tooltip">that link</a> have tooltips on hover.</p>
-</div>
-```
-
-### Using the grid
-
-Utilize the Bootstrap grid system within a modal by nesting `.container-fluid` within the `.modal-body`. Then, use the normal grid system classes as you would anywhere else.
-
-<div class="modal fade" id="gridSystemModal" tabindex="-1" aria-labelledby="gridModalLabel" aria-hidden="true">
-  <div class="modal-dialog">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-5" id="gridModalLabel">Grids in modals</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        <div class="container-fluid bd-example-row">
-          <div class="row">
-            <div class="col-md-4">.col-md-4</div>
-            <div class="col-md-4 ms-auto">.col-md-4 .ms-auto</div>
-          </div>
-          <div class="row">
-            <div class="col-md-3 ms-auto">.col-md-3 .ms-auto</div>
-            <div class="col-md-2 ms-auto">.col-md-2 .ms-auto</div>
-          </div>
-          <div class="row">
-            <div class="col-md-6 ms-auto">.col-md-6 .ms-auto</div>
-          </div>
-          <div class="row">
-            <div class="col-sm-9">
-              Level 1: .col-sm-9
-              <div class="row">
-                <div class="col-8 col-sm-6">
-                  Level 2: .col-8 .col-sm-6
-                </div>
-                <div class="col-4 col-sm-6">
-                  Level 2: .col-4 .col-sm-6
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </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>
-
-<Example showMarkup={false} code={`
-<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#gridSystemModal">Launch demo modal</button>
-`} />
-
-```html
-<div class="modal-body">
-  <div class="container-fluid">
-    <div class="row">
-      <div class="col-md-4">.col-md-4</div>
-      <div class="col-md-4 ms-auto">.col-md-4 .ms-auto</div>
-    </div>
-    <div class="row">
-      <div class="col-md-3 ms-auto">.col-md-3 .ms-auto</div>
-      <div class="col-md-2 ms-auto">.col-md-2 .ms-auto</div>
-    </div>
-    <div class="row">
-      <div class="col-md-6 ms-auto">.col-md-6 .ms-auto</div>
-    </div>
-    <div class="row">
-      <div class="col-sm-9">
-        Level 1: .col-sm-9
-        <div class="row">
-          <div class="col-8 col-sm-6">
-            Level 2: .col-8 .col-sm-6
-          </div>
-          <div class="col-4 col-sm-6">
-            Level 2: .col-4 .col-sm-6
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-```
-
-### Varying modal content
-
-Have a bunch of buttons that all trigger the same modal with slightly different contents? Use `event.relatedTarget` and [HTML `data-bs-*` attributes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes) to vary the contents of the modal depending on which button was clicked.
-
-Below is a live demo followed by example HTML and JavaScript. For more information, [read the modal events docs](#events) for details on `relatedTarget`.
-
-<Example addStackblitzJs code={`<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" data-bs-whatever="@mdo">Open modal for @mdo</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" data-bs-whatever="@fat">Open modal for @fat</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModal" data-bs-whatever="@getbootstrap">Open modal for @getbootstrap</button>
-
-  <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
-    <div class="modal-dialog">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalLabel">New message</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          <form>
-            <div class="mb-3">
-              <label for="recipient-name" class="col-form-label">Recipient:</label>
-              <input type="text" class="form-control" id="recipient-name">
-            </div>
-            <div class="mb-3">
-              <label for="message-text" class="col-form-label">Message:</label>
-              <textarea class="form-control" id="message-text"></textarea>
-            </div>
-          </form>
-        </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">Send message</button>
-        </div>
-      </div>
-    </div>
-  </div>`} />
-
-<JsDocs name="varying-modal-content" file="site/src/assets/partials/snippets.js" />
-
-### Toggle between modals
-
-Toggle between multiple modals with some clever placement of the `data-bs-target` and `data-bs-toggle` attributes. For example, you could toggle a password reset modal from within an already open sign in modal. **Please note multiple modals cannot be open at the same time**—this method simply toggles between two separate modals.
-
-<Example code={`<div class="modal fade" id="exampleModalToggle" aria-hidden="true" aria-labelledby="exampleModalToggleLabel" tabindex="-1">
-    <div class="modal-dialog modal-dialog-centered">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalToggleLabel">Modal 1</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          Show a second modal and hide this one with the button below.
-        </div>
-        <div class="modal-footer">
-          <button class="btn btn-primary" data-bs-target="#exampleModalToggle2" data-bs-toggle="modal">Open second modal</button>
-        </div>
-      </div>
-    </div>
-  </div>
-  <div class="modal fade" id="exampleModalToggle2" aria-hidden="true" aria-labelledby="exampleModalToggleLabel2" tabindex="-1">
-    <div class="modal-dialog modal-dialog-centered">
-      <div class="modal-content">
-        <div class="modal-header">
-          <h1 class="modal-title fs-5" id="exampleModalToggleLabel2">Modal 2</h1>
-          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-        </div>
-        <div class="modal-body">
-          Hide this modal and show the first with the button below.
-        </div>
-        <div class="modal-footer">
-          <button class="btn btn-primary" data-bs-target="#exampleModalToggle" data-bs-toggle="modal">Back to first</button>
-        </div>
-      </div>
-    </div>
-  </div>
-  <button class="btn btn-primary" data-bs-target="#exampleModalToggle" data-bs-toggle="modal">Open first modal</button>`} />
-
-### Change animation
-
-The `$modal-fade-transform` variable determines the transform state of `.modal-dialog` before the modal fade-in animation, the `$modal-show-transform` variable determines the transform of `.modal-dialog` at the end of the modal fade-in animation.
-
-If you want for example a zoom-in animation, you can set `$modal-fade-transform: scale(.8)`.
-
-### Remove animation
-
-For modals that simply appear rather than fade in to view, remove the `.fade` class from your modal markup.
-
-```html
-<div class="modal" tabindex="-1" aria-labelledby="..." aria-hidden="true">
-  ...
-</div>
-```
-
-### Dynamic heights
-
-If the height of a modal changes while it is open, you should call `myModal.handleUpdate()` to readjust the modal’s position in case a scrollbar appears.
-
-### Accessibility
-
-Be sure to add `aria-labelledby="..."`, referencing the modal title, to `.modal`. Additionally, you may give a description of your modal dialog with `aria-describedby` on `.modal`. Note that you don’t need to add `role="dialog"` since we already add it via JavaScript.
-
-### Embedding YouTube videos
-
-Embedding YouTube videos in modals requires additional JavaScript not in Bootstrap to automatically stop playback and more. [See this helpful Stack Overflow post](https://stackoverflow.com/questions/18622508/bootstrap-3-and-youtube-in-modal) for more information.
-
-## Optional sizes
-
-Modals have three optional sizes, available via modifier classes to be placed on a `.modal-dialog`. These sizes kick in at certain breakpoints to avoid horizontal scrollbars on narrower viewports.
-
-<BsTable>
-| Size | Class | Modal max-width
-| --- | --- | --- |
-| Small | `.modal-sm` | `300px` |
-| Default | <span class="text-body-secondary">None</span> | `500px` |
-| Large | `.modal-lg` | `800px` |
-| Extra large | `.modal-xl` | `1200px` |
-</BsTable>
-
-Our default modal without modifier class constitutes the “medium” size modal.
-
-<Example showMarkup={false} code={`<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalXl">Extra large modal</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalLg">Large modal</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalSm">Small modal</button>`} />
-
-```html
-<div class="modal-dialog modal-xl">...</div>
-<div class="modal-dialog modal-lg">...</div>
-<div class="modal-dialog modal-sm">...</div>
-```
-
-<div class="modal fade" id="exampleModalXl" tabindex="-1" aria-labelledby="exampleModalXlLabel" aria-hidden="true">
-  <div class="modal-dialog modal-xl">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalXlLabel">Extra large modal</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalLg" tabindex="-1" aria-labelledby="exampleModalLgLabel" aria-hidden="true">
-  <div class="modal-dialog modal-lg">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalLgLabel">Large modal</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalSm" tabindex="-1" aria-labelledby="exampleModalSmLabel" aria-hidden="true">
-  <div class="modal-dialog modal-sm">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalSmLabel">Small modal</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-    </div>
-  </div>
-</div>
-
-## Fullscreen Modal
-
-Another override is the option to pop up a modal that covers the user viewport, available via modifier classes that are placed on a `.modal-dialog`.
-
-<BsTable>
-| Class | Availability |
-| --- | --- |
-| `.modal-fullscreen` | Always |
-| `.modal-fullscreen-sm-down` | `576px` |
-| `.modal-fullscreen-md-down` | `768px` |
-| `.modal-fullscreen-lg-down` | `1024px` |
-| `.modal-fullscreen-xl-down` | `1280px` |
-| `.modal-fullscreen-2xl-down` | `1536px` |
-</BsTable>
-
-<Example showMarkup={false} code={`<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreen">Full screen</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreenSm">Full screen below sm</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreenMd">Full screen below md</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreenLg">Full screen below lg</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreenXl">Full screen below xl</button>
-  <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#exampleModalFullscreen2xl">Full screen below 2xl</button>`} />
-
-```html
-<!-- Full screen modal -->
-<div class="modal-dialog modal-fullscreen-sm-down">
-  ...
-</div>
-```
-
-<div class="modal fade" id="exampleModalFullscreen" tabindex="-1" aria-labelledby="exampleModalFullscreenLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenLabel">Full screen modal</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalFullscreenSm" tabindex="-1" aria-labelledby="exampleModalFullscreenSmLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen-sm-down">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenSmLabel">Full screen below sm</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalFullscreenMd" tabindex="-1" aria-labelledby="exampleModalFullscreenMdLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen-md-down">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenMdLabel">Full screen below md</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalFullscreenLg" tabindex="-1" aria-labelledby="exampleModalFullscreenLgLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen-lg-down">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenLgLabel">Full screen below lg</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalFullscreenXl" tabindex="-1" aria-labelledby="exampleModalFullscreenXlLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen-xl-down">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreenXlLabel">Full screen below xl</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<div class="modal fade" id="exampleModalFullscreen2xl" tabindex="-1" aria-labelledby="exampleModalFullscreen2xlLabel" aria-hidden="true">
-  <div class="modal-dialog modal-fullscreen-2xl-down">
-    <div class="modal-content">
-      <div class="modal-header">
-        <h1 class="modal-title fs-4" id="exampleModalFullscreen2xlLabel">Full screen below 2xl</h1>
-        <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
-      </div>
-      <div class="modal-body">
-        ...
-      </div>
-      <div class="modal-footer">
-        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-## CSS
-
-### Variables
-
-<CSSVariables component="Modals" className="modal" />
-
-<ScssDocs name="modal-css-vars" file="scss/_modal.scss" />
-
-<ScssDocs name="modal-backdrop-css-vars" file="scss/_modal.scss" />
-
-### Sass variables
-
-<ScssDocs name="modal-variables" file="scss/_variables.scss" />
-
-### Sass loops
-
-[Responsive fullscreen modals](#fullscreen-modal) are generated via the `$breakpoints` map and a loop in `scss/_modal.scss`.
-
-<ScssDocs name="modal-fullscreen-loop" file="scss/_modal.scss" />
-
-## Usage
-
-The modal plugin toggles your hidden content on demand, via data attributes or JavaScript. It also overrides default scrolling behavior and generates a `.modal-backdrop` to provide a click area for dismissing shown modals when clicking outside the modal.
-
-### Via data attributes
-
-#### Toggle
-
-Activate a modal without writing JavaScript. Set `data-bs-toggle="modal"` on a controller element, like a button, along with a `data-bs-target="#foo"` or `href="#foo"` to target a specific modal to toggle.
-
-```html
-<button type="button" data-bs-toggle="modal" data-bs-target="#myModal">Launch modal</button>
-```
-
-#### Dismiss
-
-<JsDismiss name="modal" />
-
-<Callout type="warning">
-While both ways to dismiss a modal are supported, keep in mind that dismissing from outside a modal does not match the [ARIA Authoring Practices Guide dialog (modal) pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialogmodal/). Do this at your own risk.
-</Callout>
-
-### Via JavaScript
-
-Create a modal with a single line of JavaScript:
-
-```js
-const myModal = new bootstrap.Modal(document.getElementById('myModal'), options)
-// or
-const myModalAlternative = new bootstrap.Modal('#myModal', options)
-```
-
-### Options
-
-<JsDataAttributes />
-
-<BsTable>
-| Name | Type | Default | Description |
-| --- | --- | --- | --- |
-| `backdrop` | boolean, `’static'` | `true` | Includes a modal-backdrop element. Alternatively, specify `static` for a backdrop which doesn’t close the modal when clicked. |
-| `focus` | boolean | `true` | Puts the focus on the modal when initialized. |
-| `keyboard` | boolean | `true` | Closes the modal when escape key is pressed. |
-</BsTable>
-
-### Methods
-
-<Callout name="danger-async-methods" type="danger" />
-
-#### Passing options
-
-Activates your content as a modal. Accepts an optional options `object`.
-
-```js
-const myModal = new bootstrap.Modal('#myModal', {
-  keyboard: false
-})
-```
-
-<BsTable>
-| Method | Description |
-| --- | --- |
-| `dispose` | Destroys an element’s modal. (Removes stored data on the DOM element) |
-| `getInstance` | _Static_ method which allows you to get the modal instance associated with a DOM element. |
-| `getOrCreateInstance` | _Static_ method which allows you to get the modal instance associated with a DOM element, or create a new one in case it wasn’t initialized. |
-| `handleUpdate` | Manually readjust the modal’s position if the height of a modal changes while it is open (i.e. in case a scrollbar appears). |
-| `hide` | Manually hides a modal. **Returns to the caller before the modal has actually been hidden** (i.e. before the `hidden.bs.modal` event occurs). |
-| `show` | Manually opens a modal. **Returns to the caller before the modal has actually been shown** (i.e. before the `shown.bs.modal` event occurs). Also, you can pass a DOM element as an argument that can be received in the modal events (as the `relatedTarget` property). (i.e. `const modalToggle = document.getElementById('toggleMyModal'); myModal.show(modalToggle)`. |
-| `toggle` | Manually toggles a modal. **Returns to the caller before the modal has actually been shown or hidden** (i.e. before the `shown.bs.modal` or `hidden.bs.modal` event occurs). |
-</BsTable>
-
-### Events
-
-Bootstrap’s modal class exposes a few events for hooking into modal functionality. All modal events are fired at the modal itself (i.e. at the `<div class="modal">`).
-
-<BsTable>
-| Event | Description |
-| --- | --- |
-| `hide.bs.modal` | This event is fired immediately when the `hide` instance method has been called. Can be prevented by calling `event.preventDefault()`. See [JavaScript events documentation]([[docsref:/getting-started/javascript#events]]) for more details on event prevention. |
-| `hidden.bs.modal` | This event is fired when the modal has finished being hidden from the user (will wait for CSS transitions to complete). |
-| `hidePrevented.bs.modal` | This event is fired when the modal is shown, its backdrop is `static` and a click outside of the modal is performed. The event is also fired when the escape key is pressed and the `keyboard` option is set to `false`. |
-| `show.bs.modal` | This event fires immediately when the `show` instance method is called. If caused by a click, the clicked element is available as the `relatedTarget` property of the event. |
-| `shown.bs.modal` | This event is fired when the modal has been made visible to the user (will wait for CSS transitions to complete). If caused by a click, the clicked element is available as the `relatedTarget` property of the event. |
-</BsTable>
-
-```js
-const myModalEl = document.getElementById('myModal')
-myModalEl.addEventListener('hidden.bs.modal', event => {
-  // do something...
-})
-```
index 7ff1aec54c4478a2da230397b00060555166b285..f20873c0e3402f654dcc58bf7571610c3a41d7b4 100644 (file)
@@ -8,10 +8,10 @@ toc: true
 
 Offcanvas is a sidebar component that can be toggled via JavaScript to appear from the left, right, top, or bottom edge of the viewport. Buttons or anchors are used as triggers that are attached to specific elements you toggle, and `data` attributes are used to invoke our JavaScript.
 
-- Offcanvas shares some of the same JavaScript code as modals. Conceptually, they are quite similar, but they are separate plugins.
+- Offcanvas shares some of the same JavaScript code as dialogs. Conceptually, they are quite similar, but they are separate plugins.
 - Similarly, some [source Sass](#sass-variables) variables for offcanvas’s styles and dimensions are inherited from the modal’s variables.
 - When shown, offcanvas includes a default backdrop that can be clicked to hide the offcanvas.
-- Similar to modals, only one offcanvas can be shown at a time.
+- Similar to dialogs, only one offcanvas can be shown at a time.
 
 **Heads up!** Given how CSS handles animations, you cannot use `margin` or `translate` on an `.offcanvas` element. Instead, use the class as an independent wrapping element.
 
index f4430b2c496f025557a00540f5d137a19bb3e80c..8c5b5acbec5737c797f869a949f92c87fb9f89aa 100644 (file)
@@ -71,11 +71,11 @@ const popover = new bootstrap.Popover('.example-popover', {
 })
 ```
 
-Another situation where you’ll want to set an explicit custom `container` are popovers inside a [modal dialog]([[docsref:/components/modal]]), to make sure that the popover itself is appended to the modal. This is particularly important for popovers that contain interactive elements – modal dialogs will trap focus, so unless the popover is a child element of the modal, users won’t be able to focus or activate these interactive elements.
+Another situation where you’ll want to set an explicit custom `container` are popovers inside a [dialog]([[docsref:/components/dialog]]), to make sure that the popover itself is appended to the dialog. This is particularly important for popovers that contain interactive elements – dialogs will trap focus, so unless the popover is a child element of the dialog, users won’t be able to focus or activate these interactive elements.
 
 ```js
 const popover = new bootstrap.Popover('.example-popover', {
-  container: '.modal-body'
+  container: '.dialog-body'
 })
 ```
 
@@ -145,7 +145,7 @@ const popover = new bootstrap.Popover(exampleEl, options)
 
 Avoid adding an excessive amount of content in popovers with the `html` option. Once popovers are displayed, their content is tied to the trigger element with the `aria-describedby` attribute, causing all of the popover’s content to be announced to assistive technology users as one long, uninterrupted stream.
 
-Popovers do not manage keyboard focus order, and their placement can be random in the DOM, so be careful when adding interactive elements (like forms or links), as it may lead to an illogical focus order or make the popover content itself completely unreachable for keyboard users. In cases where you must use these elements, consider using a modal dialog instead.
+Popovers do not manage keyboard focus order, and their placement can be random in the DOM, so be careful when adding interactive elements (like forms or links), as it may lead to an illogical focus order or make the popover content itself completely unreachable for keyboard users. In cases where you must use these elements, consider using a dialog instead.
 </Callout>
 
 ### Options
index 5d89b9045a7294caa87e29e4fde042f8f4396039..b660a6569ef9ea18c57419b7d1fe2da3017610ce 100644 (file)
@@ -16,7 +16,7 @@ If you’re not using a component, comment it out or delete it entirely. For exa
 
 Bootstrap’s JavaScript includes every component in our primary dist files (`bootstrap.js` and `bootstrap.min.js`), and even our primary dependency (Popper) with our bundle files (`bootstrap.bundle.js` and `bootstrap.bundle.min.js`). While you’re customizing via Sass, be sure to remove related JavaScript.
 
-For instance, assuming you’re using your own JavaScript bundler like Webpack, Parcel, or Vite, you’d only import the JavaScript you plan on using. In the example below, we show how to just include our modal JavaScript:
+For instance, assuming you’re using your own JavaScript bundler like Webpack, Parcel, or Vite, you’d only import the JavaScript you plan on using. In the example below, we show how to just include our dialog JavaScript:
 
 ```js
 // Import just what we need
@@ -25,8 +25,8 @@ For instance, assuming you’re using your own JavaScript bundler like Webpack,
 // import 'bootstrap/js/dist/button';
 // import 'bootstrap/js/dist/carousel';
 // import 'bootstrap/js/dist/collapse';
+import 'bootstrap/js/dist/dialog';
 // import 'bootstrap/js/dist/dropdown';
-import 'bootstrap/js/dist/modal';
 // import 'bootstrap/js/dist/offcanvas';
 // import 'bootstrap/js/dist/popover';
 // import 'bootstrap/js/dist/scrollspy';
@@ -41,8 +41,8 @@ This way, you’re not including any JavaScript you don’t intend to use for co
 **Heads up!** Files in `bootstrap/js/dist` use the **default export**. To use them, do the following:
 
 ```js
-import Modal from 'bootstrap/js/dist/modal'
-const modal = new Modal(document.getElementById('myModal'))
+import Dialog from 'bootstrap/js/dist/dialog'
+const dialog = new Dialog(document.getElementById('myDialog'))
 ```
 </Callout>
 
index 3dc6f2cf90f232392732670001d7c5f73ef7bb08..59f08110f98218a82a2b2134a1c0c531dcb27c17 100644 (file)
@@ -112,7 +112,7 @@ Our dropdowns, popovers, and tooltips also depend on [Popper](https://popper.js.
 
 ## Data attributes
 
-Nearly all Bootstrap plugins can be enabled and configured through HTML alone with data attributes (our preferred way of using JavaScript functionality). Be sure to **only use one set of data attributes on a single element** (e.g., you cannot trigger a tooltip and modal from the same button.)
+Nearly all Bootstrap plugins can be enabled and configured through HTML alone with data attributes (our preferred way of using JavaScript functionality). Be sure to **only use one set of data attributes on a single element** (e.g., you cannot trigger a tooltip and dialog from the same button.)
 
 <JsDataAttributes />
 
@@ -127,10 +127,10 @@ Bootstrap provides custom events for most plugins’ unique actions. Generally,
 All infinitive events provide [`preventDefault()`](https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault) functionality. This provides the ability to stop the execution of an action before it starts. Returning false from an event handler will also automatically call `preventDefault()`.
 
 ```js
-const myModal = document.querySelector('#myModal')
+const myDialog = document.querySelector('#myDialog')
 
-myModal.addEventListener('show.bs.modal', event => {
-  return event.preventDefault() // stops modal from being shown
+myDialog.addEventListener('show.bs.dialog', event => {
+  return event.preventDefault() // stops dialog from being shown
 })
 ```
 
@@ -139,11 +139,11 @@ myModal.addEventListener('show.bs.modal', event => {
 All constructors accept an optional options object or nothing (which initiates a plugin with its default behavior):
 
 ```js
-const myModalEl = document.querySelector('#myModal')
-const modal = new bootstrap.Modal(myModalEl) // initialized with defaults
+const myDialogEl = document.querySelector('#myDialog')
+const dialog = new bootstrap.Dialog(myDialogEl) // initialized with defaults
 
 const configObject = { keyboard: false }
-const modal1 = new bootstrap.Modal(myModalEl, configObject) // initialized with no keyboard
+const dialog1 = new bootstrap.Dialog(myDialogEl, configObject) // initialized with no keyboard
 ```
 
 If you’d like to get a particular plugin instance, each plugin exposes a `getInstance` method. For example, to retrieve an instance directly from an element:
@@ -167,7 +167,7 @@ In case an instance wasn’t initialized, it may accept and use an optional conf
 In addition to the `getInstance` and `getOrCreateInstance` methods, all plugin constructors can accept a DOM element or a valid [CSS selector](#selectors) as the first argument. Plugin elements are found with the `querySelector` method since our plugins only support a single element.
 
 ```js
-const modal = new bootstrap.Modal('#myModal')
+const dialog = new bootstrap.Dialog('#myDialog')
 const dropdown = new bootstrap.Dropdown('[data-bs-toggle="dropdown"]')
 const offcanvas = bootstrap.Offcanvas.getInstance('#myOffcanvas')
 const alert = bootstrap.Alert.getOrCreateInstance('#myAlert')
@@ -204,11 +204,12 @@ carousel.to('2') // !! Will be ignored, as the transition to the slide 1 is not
 While it may seem correct to use the `dispose` method immediately after `hide()`, it will lead to incorrect results. Here’s an example of the problem use:
 
 ```js
-const myModal = document.querySelector('#myModal')
-myModal.hide() // it is asynchronous
+const myToast = document.querySelector('#myToast')
+const toast = bootstrap.Toast.getInstance(myToast)
+toast.hide() // it is asynchronous
 
-myModal.addEventListener('shown.bs.hidden', event => {
-  myModal.dispose()
+myToast.addEventListener('hidden.bs.toast', event => {
+  toast.dispose()
 })
 ```
 
@@ -217,8 +218,8 @@ myModal.addEventListener('shown.bs.hidden', event => {
 You can change the default settings for a plugin by modifying the plugin’s `Constructor.Default` object:
 
 ```js
-// changes default for the modal plugin’s `keyboard` option to false
-bootstrap.Modal.Default.keyboard = false
+// changes default for the toast plugin’s `autohide` option to false
+bootstrap.Toast.Default.autohide = false
 ```
 
 ## Methods and properties
@@ -228,9 +229,9 @@ Every Bootstrap plugin exposes the following methods and static properties.
 <BsTable class="table">
 | Method | Description |
 | --- | --- |
-| `dispose` | Destroys an element’s modal. (Removes stored data on the DOM element) |
-| `getInstance` | *Static* method which allows you to get the modal instance associated with a DOM element. |
-| `getOrCreateInstance` | *Static* method which allows you to get the modal instance associated with a DOM element, or create a new one in case it wasn’t initialized. |
+| `dispose` | Destroys an element's plugin instance. (Removes stored data on the DOM element) |
+| `getInstance` | *Static* method which allows you to get the plugin instance associated with a DOM element. |
+| `getOrCreateInstance` | *Static* method which allows you to get the plugin instance associated with a DOM element, or create a new one in case it wasn’t initialized. |
 </BsTable>
 
 <BsTable class="table">
index 1be84f7700a1989cfde2fdcc432884b63bdb7d24..8e902ef6fa625f2238b26fc4de2cd8f0f0c4c72d 100644 (file)
@@ -22,7 +22,7 @@ We call these “low-level” `z-index` utilities because of their default value
 
 ## Overlays
 
-Bootstrap overlay components—dropdown, modal, offcanvas, popover, toast, and tooltip—all have their own `z-index` values to ensure a usable experience with competing “layers” of an interface.
+Bootstrap overlay components—dropdown, dialog, offcanvas, popover, toast, and tooltip—all have their own `z-index` values to ensure a usable experience with competing “layers” of an interface.
 
 Read about them in the [`z-index` layout page]([[docsref:/layout/z-index]]).