]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Update v6 to Floating UI
authorMark Otto <markdotto@gmail.com>
Wed, 24 Sep 2025 16:10:28 +0000 (09:10 -0700)
committerMark Otto <markdotto@gmail.com>
Fri, 26 Sep 2025 21:55:20 +0000 (14:55 -0700)
20 files changed:
build/generate-sri.mjs
build/rollup.config.mjs
config.yml
js/src/dropdown.js
js/src/popover.js
js/src/tooltip.js
js/src/util/floating-ui.js [new file with mode: 0644]
package-lock.json
package.json
scss/_dropdown.scss
scss/_popover.scss
scss/_tooltip.scss
site/src/content/docs/components/dropdowns.mdx
site/src/content/docs/components/popovers.mdx
site/src/content/docs/components/tooltips.mdx
site/src/content/docs/getting-started/download.mdx
site/src/content/docs/getting-started/introduction.mdx
site/src/content/docs/getting-started/javascript.mdx
site/src/content/docs/getting-started/rtl.mdx
site/src/libs/config.ts

index 5622843f347944c180c00c757507cc08c01650e4..ec178ff81637bc6be37836a1e99b8edc73116ee9 100644 (file)
@@ -42,8 +42,8 @@ const files = [
     configPropertyName: 'js_bundle_hash'
   },
   {
-    file: 'node_modules/@popperjs/core/dist/umd/popper.min.js',
-    configPropertyName: 'popper_hash'
+    file: 'node_modules/@floating-ui/dom/dist/floating-ui.dom.umd.min.js',
+    configPropertyName: 'floating_ui_hash'
   }
 ]
 
index dd6c7d13e66f862121f92cae43af81a2f4af658b..46369de5bfe0d98ab8d1825b10417a41edf84023 100644 (file)
@@ -12,7 +12,7 @@ const BUNDLE = process.env.BUNDLE === 'true'
 const ESM = process.env.ESM === 'true'
 
 let destinationFile = `bootstrap${ESM ? '.esm' : ''}`
-const external = ['@popperjs/core']
+const external = ['@floating-ui/dom']
 const plugins = [
   babel({
     // Only transpile our source code
@@ -22,14 +22,14 @@ const plugins = [
   })
 ]
 const globals = {
-  '@popperjs/core': 'Popper'
+  '@floating-ui/dom': 'FloatingUIDOM'
 }
 
 if (BUNDLE) {
   destinationFile += '.bundle'
-  // Remove last entry in external array to bundle Popper
+  // Remove last entry in external array to bundle FloatingUI
   external.pop()
-  delete globals['@popperjs/core']
+  delete globals['@floating-ui/dom']
   plugins.push(
     replace({
       'process.env.NODE_ENV': '"production"',
index 24d49eae5774582bc2cd4b1c1841cb5cbc035c99..8fd8438ddda674e5f839f64ae3782b7d95ba70bb 100644 (file)
@@ -42,9 +42,9 @@ cdn:
   js_hash:              "sha384-G/EV+4j2dNv+tEPo3++6LCgdCROaejBqfUeNjuKAiuXbjrxilcCdDz6ZAVfHWe1Y"
   js_bundle:            "https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
   js_bundle_hash:       "sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
-  popper:               "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
-  popper_hash:          "sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
-  popper_esm:           "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/esm/popper.min.js"
+  floating_ui:          "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.12/dist/floating-ui.dom.umd.min.js"
+  floating_ui_hash:     "sha384-Os8n9bzoYJ/ESbGD7cW0VOTLk0hO++SO+Y4swXBE2dHrxiZkjADEr5ZGOcc9CorD"
+  floating_ui_esm:      "https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.12/dist/floating-ui.dom.min.js"
 
 anchors:
   min:                  2
index a31f801d5b4bc187a18bdcdb3ba77d7009641e73..edada78aa95be72139acaed106af09e58785e25a 100644 (file)
@@ -5,21 +5,18 @@
  * --------------------------------------------------------------------------
  */
 
-import * as Popper from '@popperjs/core'
+import { inline, offset, shift } from '@floating-ui/dom'
 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 {
   execute,
-  getElement,
   getNextActiveElement,
   isDisabled,
-  isElement,
-  isRTL,
   isVisible,
   noop
 } from './util/index.js'
+import FloatingUi from './util/floating-ui.js'
 
 /**
  * Constants
@@ -58,30 +55,28 @@ const SELECTOR_NAVBAR = '.navbar'
 const SELECTOR_NAVBAR_NAV = '.navbar-nav'
 const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
 
-const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'
-const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'
-const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'
-const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'
-const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'
-const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
 const PLACEMENT_TOPCENTER = 'top'
+const PLACEMENT_TOPEND = 'top-end'
+const PLACEMENT_TOP = 'top-start'
 const PLACEMENT_BOTTOMCENTER = 'bottom'
+const PLACEMENT_BOTTOMEND = 'bottom-end'
+const PLACEMENT_BOTTOM = 'bottom-start'
+const PLACEMENT_RIGHT = 'right-start'
+const PLACEMENT_LEFT = 'left-start'
 
 const Default = {
   autoClose: true,
-  boundary: 'clippingParents',
   display: 'dynamic',
-  offset: [0, 2],
-  popperConfig: null,
+  offset: 10,
+  positionConfig: null,
   reference: 'toggle'
 }
 
 const DefaultType = {
   autoClose: '(boolean|string)',
-  boundary: '(string|element)',
   display: 'string',
-  offset: '(array|string|function)',
-  popperConfig: '(null|object|function)',
+  offset: '(number|array|string|function)',
+  positionConfig: '(null|object|function)',
   reference: '(string|element|object)'
 }
 
@@ -93,13 +88,12 @@ class Dropdown extends BaseComponent {
   constructor(element, config) {
     super(element, config)
 
-    this._popper = null
     this._parent = this._element.parentNode // dropdown wrapper
     // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
     this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
       SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
       SelectorEngine.findOne(SELECTOR_MENU, this._parent)
-    this._inNavbar = this._detectNavbar()
+    this._positionHelper = new FloatingUi(this._element)
   }
 
   // Getters
@@ -135,7 +129,7 @@ class Dropdown extends BaseComponent {
       return
     }
 
-    this._createPopper()
+    this.update()
 
     // If this is a touch-enabled device we add extra
     // empty mouseover listeners to the body's immediate children;
@@ -167,19 +161,9 @@ class Dropdown extends BaseComponent {
     this._completeHide(relatedTarget)
   }
 
-  dispose() {
-    if (this._popper) {
-      this._popper.destroy()
-    }
-
-    super.dispose()
-  }
-
   update() {
-    this._inNavbar = this._detectNavbar()
-    if (this._popper) {
-      this._popper.update()
-    }
+    const reference = this._positionHelper.getReferenceElement(this._config.reference, this._parent, NAME)
+    this._positionHelper.calculate(reference, this._menu, this._getFloatingUiConfig())
   }
 
   // Private
@@ -197,47 +181,28 @@ class Dropdown extends BaseComponent {
       }
     }
 
-    if (this._popper) {
-      this._popper.destroy()
-    }
-
     this._menu.classList.remove(CLASS_NAME_SHOW)
     this._element.classList.remove(CLASS_NAME_SHOW)
     this._element.setAttribute('aria-expanded', 'false')
-    Manipulator.removeDataAttribute(this._menu, 'popper')
+    this._positionHelper.stop()
     EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
   }
 
-  _getConfig(config) {
-    config = super._getConfig(config)
-
-    if (typeof config.reference === 'object' && !isElement(config.reference) &&
-      typeof config.reference.getBoundingClientRect !== 'function'
-    ) {
-      // Popper virtual elements require a getBoundingClientRect method
-      throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
+  _getFloatingUiConfig() {
+    const defaultBsConfig = {
+      placement: this._getPlacement(),
+      middleware: [offset(this._positionHelper.parseOffset(this._config.offset)), shift()]
     }
 
-    return config
-  }
-
-  _createPopper() {
-    if (typeof Popper === 'undefined') {
-      throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org/docs/v2/)')
+    // Disable positioning if we have a static display or Dropdown is in Navbar
+    if (this._detectNavbar() || this._config.display === 'static') {
+      defaultBsConfig.middleware.push(inline())
     }
 
-    let referenceElement = this._element
-
-    if (this._config.reference === 'parent') {
-      referenceElement = this._parent
-    } else if (isElement(this._config.reference)) {
-      referenceElement = getElement(this._config.reference)
-    } else if (typeof this._config.reference === 'object') {
-      referenceElement = this._config.reference
+    return {
+      ...defaultBsConfig,
+      ...execute(this._config.positionConfig, [undefined, defaultBsConfig])
     }
-
-    const popperConfig = this._getPopperConfig()
-    this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
   }
 
   _isShown() {
@@ -247,20 +212,15 @@ class Dropdown extends BaseComponent {
   _getPlacement() {
     const parentDropdown = this._parent
 
-    if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
-      return PLACEMENT_RIGHT
+    const matches = {
+      [CLASS_NAME_DROPEND]: PLACEMENT_RIGHT,
+      [CLASS_NAME_DROPSTART]: PLACEMENT_LEFT,
+      [CLASS_NAME_DROPUP_CENTER]: PLACEMENT_TOPCENTER,
+      [CLASS_NAME_DROPDOWN_CENTER]: PLACEMENT_BOTTOMCENTER
     }
-
-    if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
-      return PLACEMENT_LEFT
-    }
-
-    if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {
-      return PLACEMENT_TOPCENTER
-    }
-
-    if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {
-      return PLACEMENT_BOTTOMCENTER
+    const match = Object.keys(matches).find(keyClass => parentDropdown.classList.contains(keyClass))
+    if (match) {
+      return matches[match]
     }
 
     // We need to trim the value because custom properties can also include spaces
@@ -277,52 +237,6 @@ class Dropdown extends BaseComponent {
     return this._element.closest(SELECTOR_NAVBAR) !== null
   }
 
-  _getOffset() {
-    const { offset } = this._config
-
-    if (typeof offset === 'string') {
-      return offset.split(',').map(value => Number.parseInt(value, 10))
-    }
-
-    if (typeof offset === 'function') {
-      return popperData => offset(popperData, this._element)
-    }
-
-    return offset
-  }
-
-  _getPopperConfig() {
-    const defaultBsPopperConfig = {
-      placement: this._getPlacement(),
-      modifiers: [{
-        name: 'preventOverflow',
-        options: {
-          boundary: this._config.boundary
-        }
-      },
-      {
-        name: 'offset',
-        options: {
-          offset: this._getOffset()
-        }
-      }]
-    }
-
-    // Disable Popper if we have a static display or Dropdown is in Navbar
-    if (this._inNavbar || this._config.display === 'static') {
-      Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove
-      defaultBsPopperConfig.modifiers = [{
-        name: 'applyStyles',
-        enabled: false
-      }]
-    }
-
-    return {
-      ...defaultBsPopperConfig,
-      ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
-    }
-  }
-
   _selectMenuItem({ key, target }) {
     const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
 
index b8383dc0d5b1558a5a28dcad31c27d2b51d164cd..74cbe9c20ef9de54cf5a2d57ade5f4e71991a1e9 100644 (file)
@@ -22,9 +22,10 @@ const Default = {
   offset: [0, 8],
   placement: 'right',
   template: '<div class="popover" role="tooltip">' +
-    '<div class="popover-arrow"></div>' +
+    '<div class="popover-inner">' +
     '<h3 class="popover-header"></h3>' +
     '<div class="popover-body"></div>' +
+    '</div>' +
     '</div>',
   trigger: 'click'
 }
index ca603d20d082f5b7070b7440d46c34ef90548ce8..008f79461d3b5f451687d003c766d497a2b65c51 100644 (file)
@@ -5,15 +5,18 @@
  * --------------------------------------------------------------------------
  */
 
-import * as Popper from '@popperjs/core'
+import {
+  flip, hide, offset, shift
+} from '@floating-ui/dom'
 import BaseComponent from './base-component.js'
 import EventHandler from './dom/event-handler.js'
-import Manipulator from './dom/manipulator.js'
 import {
-  execute, findShadowRoot, getElement, getUID, isRTL, noop
+  execute, findShadowRoot, getElement, getUID, isVisible, noop
 } from './util/index.js'
+import Manipulator from './dom/manipulator.js'
 import { DefaultAllowlist } from './util/sanitizer.js'
 import TemplateFactory from './util/template-factory.js'
+import FloatingUi from './util/floating-ui.js'
 
 /**
  * Constants
@@ -50,30 +53,28 @@ const EVENT_MOUSELEAVE = 'mouseleave'
 const AttachmentMap = {
   AUTO: 'auto',
   TOP: 'top',
-  RIGHT: isRTL() ? 'left' : 'right',
+  RIGHT: 'right',
   BOTTOM: 'bottom',
-  LEFT: isRTL() ? 'right' : 'left'
+  LEFT: 'left'
 }
 
 const Default = {
   allowList: DefaultAllowlist,
   animation: true,
-  boundary: 'clippingParents',
   container: false,
   customClass: '',
   delay: 0,
   fallbackPlacements: ['top', 'right', 'bottom', 'left'],
   html: false,
-  offset: [0, 6],
+  offset: 0,
   placement: 'top',
-  popperConfig: null,
+  positionConfig: null,
   sanitize: true,
   sanitizeFn: null,
   selector: false,
   template: '<div class="tooltip" role="tooltip">' +
-            '<div class="tooltip-arrow"></div>' +
-            '<div class="tooltip-inner"></div>' +
-            '</div>',
+    '<div class="tooltip-inner"></div>' +
+    '</div>',
   title: '',
   trigger: 'hover focus'
 }
@@ -81,15 +82,14 @@ const Default = {
 const DefaultType = {
   allowList: 'object',
   animation: 'boolean',
-  boundary: '(string|element)',
   container: '(string|element|boolean)',
   customClass: '(string|function)',
   delay: '(number|object)',
   fallbackPlacements: 'array',
   html: 'boolean',
-  offset: '(array|string|function)',
+  offset: '(number|array|string|function)',
   placement: '(string|function)',
-  popperConfig: '(null|object|function)',
+  positionConfig: '(null|object|function)',
   sanitize: 'boolean',
   sanitizeFn: '(null|function)',
   selector: '(string|boolean)',
@@ -104,10 +104,6 @@ const DefaultType = {
 
 class Tooltip extends BaseComponent {
   constructor(element, config) {
-    if (typeof Popper === 'undefined') {
-      throw new TypeError('Bootstrap\'s tooltips require Popper (https://popper.js.org/docs/v2/)')
-    }
-
     super(element, config)
 
     // Private
@@ -115,7 +111,7 @@ class Tooltip extends BaseComponent {
     this._timeout = 0
     this._isHovered = null
     this._activeTrigger = {}
-    this._popper = null
+    this._positionHelper = new FloatingUi(this._element)
     this._templateFactory = null
     this._newContent = null
 
@@ -182,7 +178,7 @@ class Tooltip extends BaseComponent {
   }
 
   show() {
-    if (this._element.style.display === 'none') {
+    if (!isVisible(this._element)) {
       throw new Error('Please use show on visible elements')
     }
 
@@ -199,22 +195,14 @@ class Tooltip extends BaseComponent {
     }
 
     // TODO: v6 remove this or make it optional
-    this._disposePopper()
-
-    const tip = this._getTipElement()
-
-    this._element.setAttribute('aria-describedby', tip.getAttribute('id'))
-
-    const { container } = this._config
-
-    if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
-      container.append(tip)
-      EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
+    if (this.tip) {
+      this.tip.remove()
+      this.tip = null
     }
 
-    this._popper = this._createPopper(tip)
+    this.update()
 
-    tip.classList.add(CLASS_NAME_SHOW)
+    this.tip.classList.add(CLASS_NAME_SHOW)
 
     // If this is a touch-enabled device we add extra
     // empty mouseover listeners to the body's immediate children;
@@ -271,7 +259,8 @@ class Tooltip extends BaseComponent {
       }
 
       if (!this._isHovered) {
-        this._disposePopper()
+        this._positionHelper.stop()
+        this.tip.remove()
       }
 
       this._element.removeAttribute('aria-describedby')
@@ -282,9 +271,7 @@ class Tooltip extends BaseComponent {
   }
 
   update() {
-    if (this._popper) {
-      this._popper.update()
-    }
+    this._positionHelper.calculate(this._element, this._getTipElement(), this._getFloatingUiConfig(), { position: 'fixed' })
   }
 
   // Protected
@@ -295,6 +282,14 @@ class Tooltip extends BaseComponent {
   _getTipElement() {
     if (!this.tip) {
       this.tip = this._createTipElement(this._newContent || this._getContentForTemplate())
+      this._element.setAttribute('aria-describedby', this.tip.getAttribute('id'))
+    }
+
+    const { container } = this._config
+
+    if (!this._element.ownerDocument.documentElement.contains(this.tip)) {
+      container.append(this.tip)
+      EventHandler.trigger(this._element, this.constructor.eventName(EVENT_INSERTED))
     }
 
     return this.tip
@@ -309,8 +304,6 @@ class Tooltip extends BaseComponent {
     }
 
     tip.classList.remove(CLASS_NAME_FADE, CLASS_NAME_SHOW)
-    // TODO: v6 the following can be achieved with CSS only
-    tip.classList.add(`bs-${this.constructor.NAME}-auto`)
 
     const tipId = getUID(this.constructor.NAME).toString()
 
@@ -326,7 +319,7 @@ class Tooltip extends BaseComponent {
   setContent(content) {
     this._newContent = content
     if (this._isShown()) {
-      this._disposePopper()
+      this._positionHelper.stop()
       this.show()
     }
   }
@@ -370,77 +363,33 @@ class Tooltip extends BaseComponent {
     return this.tip && this.tip.classList.contains(CLASS_NAME_SHOW)
   }
 
-  _createPopper(tip) {
-    const placement = execute(this._config.placement, [this, tip, this._element])
-    const attachment = AttachmentMap[placement.toUpperCase()]
-    return Popper.createPopper(this._element, tip, this._getPopperConfig(attachment))
-  }
-
-  _getOffset() {
-    const { offset } = this._config
-
-    if (typeof offset === 'string') {
-      return offset.split(',').map(value => Number.parseInt(value, 10))
+  _getFloatingUiConfig() {
+    const defaultBsConfig = {
+      strategy: 'fixed',
+      placement: this._getPlacement(),
+      middleware: [
+        offset(this._positionHelper.parseOffset(this._config.offset)),
+        flip({ fallbackPlacements: this._config.fallbackPlacements }),
+        shift(),
+        hide()
+      ]
     }
 
-    if (typeof offset === 'function') {
-      return popperData => offset(popperData, this._element)
+    return {
+      ...defaultBsConfig,
+      ...execute(this._config.positionConfig, [undefined, defaultBsConfig])
     }
+  }
 
-    return offset
+  _getPlacement() {
+    const placement = execute(this._config.placement, [this, this.tip, this._element])
+    return AttachmentMap[placement.toUpperCase()]
   }
 
   _resolvePossibleFunction(arg) {
     return execute(arg, [this._element, this._element])
   }
 
-  _getPopperConfig(attachment) {
-    const defaultBsPopperConfig = {
-      placement: attachment,
-      modifiers: [
-        {
-          name: 'flip',
-          options: {
-            fallbackPlacements: this._config.fallbackPlacements
-          }
-        },
-        {
-          name: 'offset',
-          options: {
-            offset: this._getOffset()
-          }
-        },
-        {
-          name: 'preventOverflow',
-          options: {
-            boundary: this._config.boundary
-          }
-        },
-        {
-          name: 'arrow',
-          options: {
-            element: `.${this.constructor.NAME}-arrow`
-          }
-        },
-        {
-          name: 'preSetPlacement',
-          enabled: true,
-          phase: 'beforeMain',
-          fn: data => {
-            // Pre-set Popper's placement attribute in order to read the arrow sizes properly.
-            // Otherwise, Popper mixes up the width and height dimensions since the initial arrow style is for top placement
-            this._getTipElement().setAttribute('data-popper-placement', data.state.placement)
-          }
-        }
-      ]
-    }
-
-    return {
-      ...defaultBsPopperConfig,
-      ...execute(this._config.popperConfig, [undefined, defaultBsPopperConfig])
-    }
-  }
-
   _setListeners() {
     const triggers = this._config.trigger.split(' ')
 
@@ -593,17 +542,5 @@ class Tooltip extends BaseComponent {
     // `Object.fromEntries(keysWithDifferentValues)`
     return config
   }
-
-  _disposePopper() {
-    if (this._popper) {
-      this._popper.destroy()
-      this._popper = null
-    }
-
-    if (this.tip) {
-      this.tip.remove()
-      this.tip = null
-    }
-  }
 }
 export default Tooltip
diff --git a/js/src/util/floating-ui.js b/js/src/util/floating-ui.js
new file mode 100644 (file)
index 0000000..bad1249
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap floating-ui.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+import { autoUpdate, computePosition } from '@floating-ui/dom'
+import Manipulator from '../dom/manipulator.js'
+import { getElement, isElement } from './index.js'
+
+/**
+ * Class definition
+ */
+class FloatingUi {
+  constructor(element) {
+    if (typeof computePosition === 'undefined') {
+      throw new TypeError('Bootstrap\'s tooltips and dropdowns require Floating UI (https://floating-ui.com/)')
+    }
+
+    this._element = element
+    this._cleanup = null
+  }
+
+  calculate(reference, floatingEl, config, extraCss = {}) {
+    this._cleanup = autoUpdate(reference, floatingEl, () => {
+      computePosition(reference, floatingEl, config)
+        .then(({ x, y, placement, middlewareData }) => {
+          const positionCss = {
+            left: `${x}px`,
+            top: `${y}px`
+          }
+
+          if (middlewareData.hide) {
+            const { referenceHidden } = middlewareData.hide
+
+            Object.assign(floatingEl.style, {
+              visibility: referenceHidden ? 'hidden' : 'visible'
+            })
+          }
+
+          Object.assign(floatingEl.style, { ...positionCss, ...extraCss })
+          Manipulator.setDataAttribute(floatingEl, 'placement', placement)
+        })
+    })
+  }
+
+  stop() {
+    if (this._cleanup) {
+      this._cleanup()
+    }
+  }
+
+  getReferenceElement(reference, parent, PluginName) {
+    if (reference === 'parent') {
+      return parent
+    }
+
+    if (isElement(reference)) {
+      return getElement(reference)
+    }
+
+    if (typeof reference === 'object') {
+      if (typeof reference.getBoundingClientRect !== 'function') {
+        throw new TypeError(`${PluginName.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
+      }
+
+      return reference
+    }
+
+    return this._element
+  }
+
+  parseOffset(value) {
+    if (typeof value === 'function') {
+      return popperData => value(popperData, this._element)
+    }
+
+    if (typeof value === 'string') {
+      const values = value.split(',')
+      value = [
+        Number.parseInt(values[0], 10),
+        Number.parseInt(values[1] || 0, 10)
+      ]
+    }
+
+    if (Array.isArray(value)) {
+      return {
+        mainAxis: value[0],
+        crossAxis: value[1] || 0
+      }
+    }
+
+    return value
+  }
+}
+
+export default FloatingUi
index 4a3c716ca858d2a324a0dc4bdf5367cad469d47c..284ad2f6689fe6944819d02ce395e16b3c40439e 100644 (file)
@@ -18,6 +18,9 @@
         }
       ],
       "license": "MIT",
+      "dependencies": {
+        "@floating-ui/dom": "^1.0.0"
+      },
       "devDependencies": {
         "@astrojs/check": "^0.9.4",
         "@astrojs/markdown-remark": "^6.3.6",
@@ -28,7 +31,7 @@
         "@babel/core": "^7.28.4",
         "@babel/preset-env": "^7.28.3",
         "@docsearch/js": "^3.9.0",
-        "@popperjs/core": "^2.11.8",
+        "@floating-ui/dom": "^1.0.0",
         "@rollup/plugin-babel": "^6.0.4",
         "@rollup/plugin-commonjs": "^28.0.6",
         "@rollup/plugin-node-resolve": "^16.0.1",
@@ -94,7 +97,7 @@
         "zod": "^4.1.9"
       },
       "peerDependencies": {
-        "@popperjs/core": "^2.11.8"
+        "@floating-ui/dom": "^1.0.0"
       }
     },
     "node_modules/@adobe/css-tools": {
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/@floating-ui/core": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+      "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+      "dev": true,
+      "dependencies": {
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/dom": {
+      "version": "1.7.4",
+      "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+      "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+      "dev": true,
+      "dependencies": {
+        "@floating-ui/core": "^1.7.3",
+        "@floating-ui/utils": "^0.2.10"
+      }
+    },
+    "node_modules/@floating-ui/utils": {
+      "version": "0.2.10",
+      "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+      "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+      "dev": true
+    },
     "node_modules/@humanwhocodes/config-array": {
       "version": "0.13.0",
       "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
         "node": ">=14"
       }
     },
-    "node_modules/@popperjs/core": {
-      "version": "2.11.8",
-      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
-      "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
-      "dev": true,
-      "license": "MIT",
-      "funding": {
-        "type": "opencollective",
-        "url": "https://opencollective.com/popperjs"
-      }
-    },
     "node_modules/@rollup/plugin-babel": {
       "version": "6.0.4",
       "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz",
index 5102b58bf32d89dd94e1cc04d7abe0e250f27261..1b0066115afda8dce4565e44cfcab9dffc405405 100644 (file)
     "astro-preview": "astro preview --root site --port 9001"
   },
   "peerDependencies": {
-    "@popperjs/core": "^2.11.8"
+    "@floating-ui/dom": "^1.0.0"
+  },
+  "dependencies": {
+    "@floating-ui/dom": "^1.0.0"
   },
   "devDependencies": {
     "@astrojs/check": "^0.9.4",
     "@babel/core": "^7.28.4",
     "@babel/preset-env": "^7.28.3",
     "@docsearch/js": "^3.9.0",
-    "@popperjs/core": "^2.11.8",
+    "@floating-ui/dom": "^1.0.0",
     "@rollup/plugin-babel": "^6.0.4",
     "@rollup/plugin-commonjs": "^28.0.6",
     "@rollup/plugin-node-resolve": "^16.0.1",
     "directories": {
       "lib": "dist"
     },
-    "shim": {
-      "js/bootstrap": {
-        "deps": [
-          "@popperjs/core"
-        ]
-      }
-    },
+      "shim": {
+        "js/bootstrap": {
+          "deps": [
+            "@floating-ui/dom"
+          ]
+        }
+      },
     "dependencies": {},
-    "peerDependencies": {
-      "@popperjs/core": "^2.11.8"
-    }
+      "peerDependencies": {
+        "@floating-ui/dom": "^1.0.0"
+      }
   }
 }
index 2de07ffae3056acc33947be188265d1b2936adf3..16b4616f5fde33292b51a244033dcab446d30233 100644 (file)
@@ -123,11 +123,6 @@ $dropdown-dark-header-color:        $gray-500 !default;
     @include border-radius(var(--#{$prefix}dropdown-border-radius));
     @include box-shadow(var(--#{$prefix}dropdown-box-shadow));
 
-    &[data-bs-popper] {
-      top: 100%;
-      left: 0;
-      margin-top: var(--#{$prefix}dropdown-spacer);
-    }
 
     @if $dropdown-padding-y == 0 {
       > .dropdown-item:first-child,
@@ -144,7 +139,7 @@ $dropdown-dark-header-color:        $gray-500 !default;
 
   // scss-docs-start responsive-breakpoints
   // We deliberately hardcode the `bs-` prefix because we check
-  // this custom property in JS to determine Popper's positioning
+  // this custom property in JS to determine positioning
 
   @each $breakpoint in map.keys($grid-breakpoints) {
     @include media-breakpoint-up($breakpoint) {
@@ -152,20 +147,10 @@ $dropdown-dark-header-color:        $gray-500 !default;
 
       .dropdown-menu#{$infix}-start {
         --bs-position: start;
-
-        &[data-bs-popper] {
-          right: auto;
-          left: 0;
-        }
       }
 
       .dropdown-menu#{$infix}-end {
         --bs-position: end;
-
-        &[data-bs-popper] {
-          right: 0;
-          left: auto;
-        }
       }
     }
   }
@@ -174,26 +159,12 @@ $dropdown-dark-header-color:        $gray-500 !default;
   // Allow for dropdowns to go bottom up (aka, dropup-menu)
   // Just add .dropup after the standard .dropdown class and you're set.
   .dropup {
-    .dropdown-menu[data-bs-popper] {
-      top: auto;
-      bottom: 100%;
-      margin-top: 0;
-      margin-bottom: var(--#{$prefix}dropdown-spacer);
-    }
-
     .dropdown-toggle {
       @include caret(up);
     }
   }
 
   .dropend {
-    .dropdown-menu[data-bs-popper] {
-      top: 0;
-      right: auto;
-      left: 100%;
-      margin-top: 0;
-      margin-left: var(--#{$prefix}dropdown-spacer);
-    }
 
     .dropdown-toggle {
       @include caret(end);
@@ -204,14 +175,6 @@ $dropdown-dark-header-color:        $gray-500 !default;
   }
 
   .dropstart {
-    .dropdown-menu[data-bs-popper] {
-      top: 0;
-      right: 100%;
-      left: auto;
-      margin-top: 0;
-      margin-right: var(--#{$prefix}dropdown-spacer);
-    }
-
     .dropdown-toggle {
       @include caret(start);
       &::before {
index 3093ed265b2147fe808151bf484ad8bb416ea290..a64bf1bfdb00f69ced2bd52b7756c7ccc8e1f596 100644 (file)
@@ -54,6 +54,9 @@ $popover-arrow-height:              .5rem !default;
     --#{$prefix}popover-arrow-border: var(--#{$prefix}popover-border-color);
     // scss-docs-end popover-css-vars
 
+    position: absolute;
+    top: 0;
+    left: 0;
     z-index: var(--#{$prefix}popover-zindex);
     display: block;
     max-width: var(--#{$prefix}popover-max-width);
@@ -63,150 +66,121 @@ $popover-arrow-height:              .5rem !default;
     @include font-size(var(--#{$prefix}popover-font-size));
     // Allow breaking very long words so they don't overflow the popover's bounds
     word-wrap: break-word;
+
+    &::before,
+    &::after {
+      position: absolute;
+      display: block;
+      content: "";
+      border-color: transparent;
+      border-style: solid;
+      border-width: 0;
+      transform: translateX(-50%);
+    }
+  }
+
+  .popover-inner {
+    margin: var(--#{$prefix}popover-arrow-height);
     background-color: var(--#{$prefix}popover-bg);
     background-clip: padding-box;
     border: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-border-color);
     @include border-radius(var(--#{$prefix}popover-border-radius));
     @include box-shadow(var(--#{$prefix}popover-box-shadow));
-
-    .popover-arrow {
-      display: block;
-      width: var(--#{$prefix}popover-arrow-width);
-      height: var(--#{$prefix}popover-arrow-height);
-
-      &::before,
-      &::after {
-        position: absolute;
-        display: block;
-        content: "";
-        border-color: transparent;
-        border-style: solid;
-        border-width: 0;
-      }
-    }
   }
 
-  .bs-popover-top {
-    > .popover-arrow {
-      bottom: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width));
-
-      &::before,
-      &::after {
-        border-width: var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0;
-      }
+  .popover[data-bs-placement="top"] {
+    &::before,
+    &::after {
+      left: 50%;
+      border-width: var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0;
+    }
 
-      &::before {
-        bottom: 0;
-        border-top-color: var(--#{$prefix}popover-arrow-border);
-      }
+    &::before {
+      bottom: 0;
+      border-top-color: var(--#{$prefix}popover-arrow-border);
+    }
 
-      &::after {
-        bottom: var(--#{$prefix}popover-border-width);
-        border-top-color: var(--#{$prefix}popover-bg);
-      }
+    &::after {
+      bottom: var(--#{$prefix}popover-border-width);
+      border-top-color: var(--#{$prefix}popover-bg);
     }
   }
 
-  /* rtl:begin:ignore */
-  .bs-popover-end {
-    > .popover-arrow {
-      left: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width));
-      width: var(--#{$prefix}popover-arrow-height);
-      height: var(--#{$prefix}popover-arrow-width);
-
-      &::before,
-      &::after {
-        border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0;
-      }
-
-      &::before {
-        left: 0;
-        border-right-color: var(--#{$prefix}popover-arrow-border);
-      }
-
-      &::after {
-        left: var(--#{$prefix}popover-border-width);
-        border-right-color: var(--#{$prefix}popover-bg);
-      }
+  .popover[data-bs-placement="bottom"] {
+    &::before,
+    &::after {
+      left: 50%;
+      border-width: 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height);
     }
-  }
 
-  /* rtl:end:ignore */
+    &::before {
+      top: 0;
+      border-bottom-color: var(--#{$prefix}popover-arrow-border);
+    }
 
-  .bs-popover-bottom {
-    > .popover-arrow {
-      top: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width));
+    &::after {
+      top: var(--#{$prefix}popover-border-width);
+      border-bottom-color: var(--#{$prefix}popover-bg);
+    }
+  }
 
-      &::before,
-      &::after {
-        border-width: 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height);
-      }
 
-      &::before {
-        top: 0;
-        border-bottom-color: var(--#{$prefix}popover-arrow-border);
-      }
+  /* rtl:begin:ignore */
 
-      &::after {
-        top: var(--#{$prefix}popover-border-width);
-        border-bottom-color: var(--#{$prefix}popover-bg);
-      }
+  .popover[data-bs-placement="right"] {
+    &::before,
+    &::after {
+      top: 50%;
+      border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height) calc(var(--#{$prefix}popover-arrow-width) * .5) 0;
+      transform: translateY(-50%);
     }
 
-    // This will remove the popover-header's border just below the arrow
-    .popover-header::before {
-      position: absolute;
-      top: 0;
-      left: 50%;
-      display: block;
-      width: var(--#{$prefix}popover-arrow-width);
-      margin-left: calc(-.5 * var(--#{$prefix}popover-arrow-width));
-      content: "";
-      border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-header-bg);
+    &::before {
+      left: 0;
+      border-right-color: var(--#{$prefix}popover-arrow-border);
     }
-  }
 
-  /* rtl:begin:ignore */
-  .bs-popover-start {
-    > .popover-arrow {
-      right: calc(-1 * (var(--#{$prefix}popover-arrow-height)) - var(--#{$prefix}popover-border-width));
-      width: var(--#{$prefix}popover-arrow-height);
-      height: var(--#{$prefix}popover-arrow-width);
-
-      &::before,
-      &::after {
-        border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height);
-      }
-
-      &::before {
-        right: 0;
-        border-left-color: var(--#{$prefix}popover-arrow-border);
-      }
-
-      &::after {
-        right: var(--#{$prefix}popover-border-width);
-        border-left-color: var(--#{$prefix}popover-bg);
-      }
+    &::after {
+      left: var(--#{$prefix}popover-border-width);
+      border-right-color: var(--#{$prefix}popover-bg);
     }
   }
 
-  /* rtl:end:ignore */
 
-  .bs-popover-auto {
-    &[data-popper-placement^="top"] {
-      @extend .bs-popover-top;
-    }
-    &[data-popper-placement^="right"] {
-      @extend .bs-popover-end;
+  .popover[data-bs-placement="left"] {
+    &::before,
+    &::after {
+      top: 50%;
+      border-width: calc(var(--#{$prefix}popover-arrow-width) * .5) 0 calc(var(--#{$prefix}popover-arrow-width) * .5) var(--#{$prefix}popover-arrow-height);
+      transform: translateY(-50%);
     }
-    &[data-popper-placement^="bottom"] {
-      @extend .bs-popover-bottom;
+
+    &::before {
+      right: 0;
+      border-left-color: var(--#{$prefix}popover-arrow-border);
     }
-    &[data-popper-placement^="left"] {
-      @extend .bs-popover-start;
+
+    &::after {
+      right: var(--#{$prefix}popover-border-width);
+      border-left-color: var(--#{$prefix}popover-bg);
     }
   }
 
+
+  /* rtl:end:ignore */
+
+  // This will remove the popover-header's border just below the arrow
+  .popover-header::before {
+    position: absolute;
+    top: 0;
+    left: 50%;
+    display: block;
+    width: var(--#{$prefix}popover-arrow-width);
+    margin-left: calc(var(--#{$prefix}popover-arrow-width) * -.5);
+    content: "";
+    border-bottom: var(--#{$prefix}popover-border-width) solid var(--#{$prefix}popover-header-bg);
+  }
+
   // Offset the popover to account for the popover arrow
   .popover-header {
     padding: var(--#{$prefix}popover-header-padding-y) var(--#{$prefix}popover-header-padding-x);
index 12339704ad7b5d9eb11b55b55a0a65675c541cc0..d605bed481af1a3b32f770e4acda7b760c2f2fba 100644 (file)
@@ -14,7 +14,6 @@ $tooltip-border-radius:             var(--#{$prefix}border-radius) !default;
 $tooltip-opacity:                   .9 !default;
 $tooltip-padding-y:                 $spacer * .25 !default;
 $tooltip-padding-x:                 $spacer * .5 !default;
-$tooltip-margin:                    null !default; // TODO: remove this in v6
 
 $tooltip-arrow-width:               .8rem !default;
 $tooltip-arrow-height:              .4rem !default;
@@ -38,7 +37,6 @@ $form-feedback-tooltip-border-radius: $tooltip-border-radius !default;
     --#{$prefix}tooltip-max-width: #{$tooltip-max-width};
     --#{$prefix}tooltip-padding-x: #{$tooltip-padding-x};
     --#{$prefix}tooltip-padding-y: #{$tooltip-padding-y};
-    --#{$prefix}tooltip-margin: #{$tooltip-margin};
     @include rfs($tooltip-font-size, --#{$prefix}tooltip-font-size);
     --#{$prefix}tooltip-color: #{$tooltip-color};
     --#{$prefix}tooltip-bg: #{$tooltip-bg};
@@ -48,10 +46,12 @@ $form-feedback-tooltip-border-radius: $tooltip-border-radius !default;
     --#{$prefix}tooltip-arrow-height: #{$tooltip-arrow-height};
     // scss-docs-end tooltip-css-vars
 
+    position: absolute;
+    top: 0;
+    left: 0;
     z-index: var(--#{$prefix}tooltip-zindex);
     display: block;
-    margin: var(--#{$prefix}tooltip-margin);
-    @include deprecate("`$tooltip-margin`", "v5", "v5.x", true);
+    padding: var(--#{$prefix}tooltip-arrow-height);
     // Our parent element can be arbitrary since tooltips are by default inserted as a sibling of their target element.
     // So reset our font and text properties to avoid inheriting weird values.
     @include reset-text();
@@ -62,83 +62,43 @@ $form-feedback-tooltip-border-radius: $tooltip-border-radius !default;
 
     &.show { opacity: var(--#{$prefix}tooltip-opacity); }
 
-    .tooltip-arrow {
-      display: block;
-      width: var(--#{$prefix}tooltip-arrow-width);
-      height: var(--#{$prefix}tooltip-arrow-height);
-
-      &::before {
-        position: absolute;
-        content: "";
-        border-color: transparent;
-        border-style: solid;
-      }
-    }
-  }
-
-  .bs-tooltip-top .tooltip-arrow {
-    bottom: calc(-1 * var(--#{$prefix}tooltip-arrow-height));
-
     &::before {
-      top: -1px;
-      border-width: var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0;
-      border-top-color: var(--#{$prefix}tooltip-bg);
+      position: absolute;
+      content: "";
+      border-color: transparent;
+      border-style: solid;
+      transform: translateX(-50%);
     }
   }
 
-  /* rtl:begin:ignore */
-  .bs-tooltip-end .tooltip-arrow {
-    left: calc(-1 * var(--#{$prefix}tooltip-arrow-height));
-    width: var(--#{$prefix}tooltip-arrow-height);
-    height: var(--#{$prefix}tooltip-arrow-width);
-
-    &::before {
-      right: -1px;
-      border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0;
-      border-right-color: var(--#{$prefix}tooltip-bg);
-    }
+  .tooltip[data-bs-placement="top"]::before {
+    bottom: 0;
+    left: 50%;
+    border-width: var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0;
+    border-top-color: var(--#{$prefix}tooltip-bg);
   }
 
-  /* rtl:end:ignore */
-
-  .bs-tooltip-bottom .tooltip-arrow {
-    top: calc(-1 * var(--#{$prefix}tooltip-arrow-height));
-
-    &::before {
-      bottom: -1px;
-      border-width: 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height);
-      border-bottom-color: var(--#{$prefix}tooltip-bg);
-    }
+  .tooltip[data-bs-placement="bottom"]::before {
+    top: 0;
+    left: 50%;
+    border-width: 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height);
+    border-bottom-color: var(--#{$prefix}tooltip-bg);
   }
 
-  /* rtl:begin:ignore */
-  .bs-tooltip-start .tooltip-arrow {
-    right: calc(-1 * var(--#{$prefix}tooltip-arrow-height));
-    width: var(--#{$prefix}tooltip-arrow-height);
-    height: var(--#{$prefix}tooltip-arrow-width);
-
-    &::before {
-      left: -1px;
-      border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height);
-      border-left-color: var(--#{$prefix}tooltip-bg);
-    }
+  .tooltip[data-bs-placement="right"]::before {
+    top: 50%;
+    left: 0;
+    border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height) calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0;
+    border-right-color: var(--#{$prefix}tooltip-bg);
+    transform: translateY(-50%);
   }
 
-  /* rtl:end:ignore */
-
-  .bs-tooltip-auto {
-    &[data-popper-placement^="top"] {
-      @extend .bs-tooltip-top;
-    }
-    &[data-popper-placement^="right"] {
-      @extend .bs-tooltip-end;
-    }
-    &[data-popper-placement^="bottom"] {
-      @extend .bs-tooltip-bottom;
-    }
-    &[data-popper-placement^="left"] {
-      @extend .bs-tooltip-start;
-    }
+  .tooltip[data-bs-placement="left"]::before {
+    top: 50%;
+    right: 0;
+    border-width: calc(var(--#{$prefix}tooltip-arrow-width) * .5) 0 calc(var(--#{$prefix}tooltip-arrow-width) * .5) var(--#{$prefix}tooltip-arrow-height);
+    border-left-color: var(--#{$prefix}tooltip-bg);
+    transform: translateY(-50%);
   }
 
   // Wrapper for the tooltip content
index 4cf96bafcf96422958ed706cd52839b816ffb6af..143a1eab3d56d845338ca558b6a73dd8a8218339 100644 (file)
@@ -8,7 +8,7 @@ toc: true
 
 Dropdowns are toggleable, contextual overlays for displaying lists of links and more. They’re made interactive with the included Bootstrap dropdown JavaScript plugin. They’re toggled by clicking, not by hovering; this is [an intentional design decision](https://markdotto.com/blog/bootstrap-explained-dropdowns/).
 
-Dropdowns are built on a third party library, [Popper](https://popper.js.org/docs/v2/), which provides dynamic positioning and viewport detection. Be sure to include [popper.min.js]([[config:cdn.popper]]) before Bootstrap’s JavaScript or use `bootstrap.bundle.min.js` / `bootstrap.bundle.js` which contains Popper. Popper isn’t used to position dropdowns in navbars though as dynamic positioning isn’t required.
+Dropdowns are built on a third party library, [Floating UI](https://floating-ui.com/), which provides dynamic positioning and viewport detection. Be sure to include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before Bootstrap's JavaScript or use `bootstrap.bundle.min.js` / `bootstrap.bundle.js` which contains Floating UI. Floating UI isn't used to position dropdowns in navbars though as dynamic positioning isn't required.
 
 ## Accessibility
 
index f4430b2c496f025557a00540f5d137a19bb3e80c..cf62750dcd2d32952054dc67836c83a32a518059 100644 (file)
@@ -8,7 +8,7 @@ toc: true
 
 Things to know when using the popover plugin:
 
-- Popovers rely on the third party library [Popper](https://popper.js.org/docs/v2/) for positioning. You must include [popper.min.js]([[config:cdn.popper]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Popper.
+- Popovers rely on the third party library [Floating UI](https://floating-ui.com/) for positioning. You must include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Floating UI.
 - Popovers require the [popover plugin]([[docsref:/components/popovers]]) as a dependency.
 - Popovers are opt-in for performance reasons, so **you must initialize them yourself**.
 - Zero-length `title` and `content` values will never show a popover.
index 2f26ed8c4522576f3da37c6d9d1298ffeb44052f..097f3a5ecf114127c4139741938da2495b0fb3fe 100644 (file)
@@ -8,7 +8,7 @@ toc: true
 
 Things to know when using the tooltip plugin:
 
-- Tooltips rely on the third party library [Popper](https://popper.js.org/docs/v2/) for positioning. You must include [popper.min.js]([[config:cdn.popper]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Popper.
+- Tooltips rely on the third party library [Floating UI](https://floating-ui.com/) for positioning. You must include [floating-ui.dom.umd.min.js]([[config:cdn.floating_ui]]) before `bootstrap.js`, or use one `bootstrap.bundle.min.js` which contains Floating UI.
 - Tooltips are opt-in for performance reasons, so **you must initialize them yourself**.
 - Tooltips with zero-length titles are never displayed.
 - Specify `container: 'body'` to avoid rendering problems in more complex components (like our input groups, button groups, etc).
index 4088b79d30ef40da89c1f85c492cb7a1ba4b8b60..50cbfd07edd24348ac8f2680fd937f800455528b 100644 (file)
@@ -11,7 +11,7 @@ Download ready-to-use compiled code for **Bootstrap v[[config:current_version]]*
 - Compiled and minified CSS bundles (see [CSS files comparison]([[docsref:/getting-started/contents#css-files]]))
 - Compiled and minified JavaScript plugins (see [JS files comparison]([[docsref:/getting-started/contents#js-files]]))
 
-This doesn’t include documentation, source files, or any optional JavaScript dependencies like Popper.
+This doesn't include documentation, source files, or any optional JavaScript dependencies like Floating UI.
 
 <a href="[[config:download.dist]]" class="btn btn-bd-primary">Download</a>
 
@@ -41,10 +41,10 @@ Skip the download with [jsDelivr](https://www.jsdelivr.com/) to deliver cached v
 <script src="[[config:cdn.js_bundle]]" integrity="[[config:cdn.js_bundle_hash]]" crossorigin="anonymous"></script>
 ```
 
-If you’re using our compiled JavaScript and prefer to include Popper separately, add Popper before our JS, via a CDN preferably.
+If you're using our compiled JavaScript and prefer to include Floating UI separately, add Floating UI before our JS, via a CDN preferably.
 
 ```html
-<script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+<script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
 <script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
 ```
 
index dc2c2d71a72c641fb99d2dfd49af6b5638105f76..5415ecd505885117faeb45e2674862f8212ef930 100644 (file)
@@ -30,7 +30,7 @@ Get started by including Bootstrap’s production-ready CSS and JavaScript via C
    </html>
    ```
 
-2. **Include Bootstrap’s CSS and JS.** Place the `<link>` tag in the `<head>` for our CSS, and the `<script>` tag for our JavaScript bundle (including Popper for positioning dropdowns, popovers, and tooltips) before the closing `</body>`. Learn more about our [CDN links](#cdn-links).
+2. **Include Bootstrap's CSS and JS.** Place the `<link>` tag in the `<head>` for our CSS, and the `<script>` tag for our JavaScript bundle (including Floating UI for positioning dropdowns, popovers, and tooltips) before the closing `</body>`. Learn more about our [CDN links](#cdn-links).
 
    ```html
    <!doctype html>
@@ -48,10 +48,10 @@ Get started by including Bootstrap’s production-ready CSS and JavaScript via C
    </html>
    ```
 
-   You can also include [Popper](https://popper.js.org/docs/v2/) and our JS separately. If you don’t plan to use dropdowns, popovers, or tooltips, save some kilobytes by not including Popper.
+   You can also include [Floating UI](https://floating-ui.com/) and our JS separately. If you don't plan to use dropdowns, popovers, or tooltips, save some kilobytes by not including Floating UI.
 
    ```html
-   <script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+   <script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
    <script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
    ```
 
index 81177a074aee244f7db39cb83e99f9e0929b2681..e6fca8db2f9ede79fa2aa0bf8948cb0302592079 100644 (file)
@@ -41,19 +41,19 @@ We provide a version of Bootstrap built as `ESM` (`bootstrap.esm.js` and `bootst
 </script>
 ```
 
-Compared to JS bundlers, using ESM in the browser requires you to use the full path and filename instead of the module name. [Read more about JS modules in the browser.](https://v8.dev/features/modules#specifiers) That’s why we use `'bootstrap.esm.min.js'` instead of `'bootstrap'` above. However, this is further complicated by our Popper dependency, which imports Popper into our JavaScript like so:
+Compared to JS bundlers, using ESM in the browser requires you to use the full path and filename instead of the module name. [Read more about JS modules in the browser.](https://v8.dev/features/modules#specifiers) That’s why we use `'bootstrap.esm.min.js'` instead of `'bootstrap'` above. However, this is further complicated by our Floating UI dependency, which imports Floating UI into our JavaScript like so:
 
 ```js
-import * as Popper from "@popperjs/core"
+import { computePosition } from "@floating-ui/dom"
 ```
 
-If you try this as-is, youll see an error in the console like the following:
+If you try this as-is, you'll see an error in the console like the following:
 
 ```text
-Uncaught TypeError: Failed to resolve module specifier "@popperjs/core". Relative references must start with either "/", "./", or "../".
+Uncaught TypeError: Failed to resolve module specifier "@floating-ui/dom". Relative references must start with either "/", "./", or "../".
 ```
 
-To fix this, you can use an `importmap` to resolve the arbitrary module names to complete paths. If your [targeted browsers](https://caniuse.com/?search=importmap) do not support `importmap`, you’ll need to use the [es-module-shims](https://github.com/guybedford/es-module-shims) project. Here’s how it works for Bootstrap and Popper:
+To fix this, you can use an `importmap` to resolve the arbitrary module names to complete paths. If your [targeted browsers](https://caniuse.com/?search=importmap) do not support `importmap`, you'll need to use the [es-module-shims](https://github.com/guybedford/es-module-shims) project. Here's how it works for Bootstrap and Floating UI:
 
 ```html
 <!doctype html>
@@ -72,7 +72,7 @@ To fix this, you can use an `importmap` to resolve the arbitrary module names to
     <script type="importmap">
     {
       "imports": {
-        "@popperjs/core": "[[config:cdn.popper_esm]]",
+        "@floating-ui/dom": "[[config:cdn.floating_ui_esm]]",
         "bootstrap": "https://cdn.jsdelivr.net/npm/bootstrap@[[config:current_version]]/dist/js/bootstrap.esm.min.js"
       }
     }
index 8c3ce87453c2818fc1fa22de8873881c782a6992..5cb6ce77cbc8b06749325b6681dced601ffb978f 100644 (file)
@@ -49,12 +49,12 @@ You can see the above requirements reflected in this modified RTL starter templa
 
     <!-- Optional JavaScript; choose one of the two! -->
 
-    <!-- Option 1: Bootstrap Bundle with Popper -->
+    <!-- Option 1: Bootstrap Bundle with Floating UI -->
     <script src="[[config:cdn.js_bundle]]" integrity="[[config:cdn.js_bundle_hash]]" crossorigin="anonymous"></script>
 
-    <!-- Option 2: Separate Popper and Bootstrap JS -->
+    <!-- Option 2: Separate Floating UI and Bootstrap JS -->
     <!--
-    <script src="[[config:cdn.popper]]" integrity="[[config:cdn.popper_hash]]" crossorigin="anonymous"></script>
+    <script src="[[config:cdn.floating_ui]]" integrity="[[config:cdn.floating_ui_hash]]" crossorigin="anonymous"></script>
     <script src="[[config:cdn.js]]" integrity="[[config:cdn.js_hash]]" crossorigin="anonymous"></script>
     -->
   </body>
index be01d8550e61da7b912a47bd8c8a6f541738b700..0154108f95f8bbdcce891a1ed68e6567dcfcb287 100644 (file)
@@ -29,9 +29,9 @@ const configSchema = z.object({
     js_hash: z.string(),
     js_bundle: z.string().url(),
     js_bundle_hash: z.string(),
-    popper: z.string().url(),
-    popper_esm: z.string().url(),
-    popper_hash: z.string()
+    floating_ui: z.string().url(),
+    floating_ui_esm: z.string().url(),
+    floating_ui_hash: z.string()
   }),
   current_version: zVersionSemver,
   current_ruby_version: zVersionSemver,