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'
}
]
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
})
]
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"',
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
* --------------------------------------------------------------------------
*/
-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
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)'
}
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
return
}
- this._createPopper()
+ this.update()
// If this is a touch-enabled device we add extra
// empty mouseover listeners to the body's immediate children;
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
}
}
- 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() {
_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
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))
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'
}
* --------------------------------------------------------------------------
*/
-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
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'
}
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)',
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
this._timeout = 0
this._isHovered = null
this._activeTrigger = {}
- this._popper = null
+ this._positionHelper = new FloatingUi(this._element)
this._templateFactory = null
this._newContent = null
}
show() {
- if (this._element.style.display === 'none') {
+ if (!isVisible(this._element)) {
throw new Error('Please use show on visible elements')
}
}
// 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;
}
if (!this._isHovered) {
- this._disposePopper()
+ this._positionHelper.stop()
+ this.tip.remove()
}
this._element.removeAttribute('aria-describedby')
}
update() {
- if (this._popper) {
- this._popper.update()
- }
+ this._positionHelper.calculate(this._element, this._getTipElement(), this._getFloatingUiConfig(), { position: 'fixed' })
}
// Protected
_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
}
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()
setContent(content) {
this._newContent = content
if (this._isShown()) {
- this._disposePopper()
+ this._positionHelper.stop()
this.show()
}
}
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(' ')
// `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
--- /dev/null
+/**
+ * --------------------------------------------------------------------------
+ * 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
}
],
"license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/markdown-remark": "^6.3.6",
"@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",
"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",
"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"
+ }
}
}
@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,
// 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) {
.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;
- }
}
}
}
// 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);
}
.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 {
--#{$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);
@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);
$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;
--#{$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};
--#{$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();
&.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
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
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.
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).
- 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>
<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>
```
</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>
</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>
```
</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, you’ll 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>
<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"
}
}
<!-- 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>
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,